/** * One-time migration: consolidate every agent item into the flat, file-based store * ($OFFICER_ITEMS_DIR) and export the DB-backed `tasks` table to TASK.md files. * * Idempotent — safe to re-run. Run this BEFORE applying the drop-tables DB migration * (it reads the `tasks` table, which still exists until that migration runs). * * Sources, in precedence order (later overwrites earlier on a dirName collision): * - tasks: officer_db.tasks rows (native → global → user) * - skills / tools / processes / extensions: $DATA_PATH/ then $DATA_PATH// * - tools: marketplace registry tools not already present (archive safety) * * Usage: bun run scripts/migrate-items-to-files.ts */ import { join, resolve } from 'node:path'; import { readdir, cp } from 'node:fs/promises'; import { existsSync } from 'node:fs'; import { db } from 'officerdb/db'; import { sql } from 'drizzle-orm'; import { itemsDir, ensureItemDirs, DATA_PATH, OFFICER_ITEMS_DIR, type ItemType } from '../src/servers/data-path'; import { importTask } from '../src/servers/api/tasks/task-files'; ensureItemDirs(); console.log(`Target store: ${OFFICER_ITEMS_DIR}`); async function listSubdirs(dir: string): Promise { try { return (await readdir(dir, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name); } catch { return []; } } // ── 1. Tasks: Postgres → TASK.md files ── // Order native → global → user so user/global overwrite native on a dirName collision. const scopeRank = (s: string) => (s === 'user' ? 2 : s === 'global' ? 1 : 0); console.log('\n── Tasks (DB → files) ──'); let taskRows: Record[] = []; try { taskRows = (await db.execute(sql.raw('SELECT * FROM tasks'))) as unknown as Record[]; } catch (err) { console.log(` could not read tasks table (already dropped?): ${err instanceof Error ? err.message : err}`); } taskRows.sort((a, b) => scopeRank(String(a.scope)) - scopeRank(String(b.scope))); for (const row of taskRows) { const dirName = String(row.dir_name); await importTask(dirName, { name: String(row.name ?? dirName), description: row.description == null ? null : String(row.description), version: Number(row.version) || 1, mode: String(row.mode ?? 'agentic'), language: row.language == null ? null : String(row.language), args: (row.args as string[] | null) ?? null, tags: (row.tags as string[] | null) ?? null, tools: (row.tools as string[] | null) ?? null, skills: (row.skills as string[] | null) ?? null, inputs: row.inputs ?? null, outputs: row.outputs ?? null, dependencies: row.dependencies ?? null, config: row.config ?? null, trigger: row.trigger ?? null, body: row.body == null ? '' : String(row.body), implementation: row.implementation == null ? null : String(row.implementation), }); console.log(` ${dirName} (${row.scope})`); } console.log(` ${taskRows.length} task file(s) written`); // ── 2. On-disk items → flat store ── const DISK_TYPES: ItemType[] = ['skills', 'tools', 'processes', 'extensions']; async function copyItemsFrom(srcTypeDir: string, type: ItemType): Promise { let n = 0; for (const name of await listSubdirs(srcTypeDir)) { await cp(join(srcTypeDir, name), join(itemsDir(type), name), { recursive: true, force: true }); n++; } return n; } console.log('\n── Disk items (DATA_PATH → flat store) ──'); const emailDirs = (await listSubdirs(DATA_PATH)).filter((n) => n.includes('@')); for (const type of DISK_TYPES) { let n = await copyItemsFrom(join(DATA_PATH, type), type); // global for (const email of emailDirs) n += await copyItemsFrom(join(DATA_PATH, email, type), type); // user (overwrites) console.log(` ${type}: ${n} item(s) copied`); } // ── 3. Marketplace registry tools not already present (archive safety) ── const MARKETPLACE_REGISTRY = process.env.MARKETPLACE_REGISTRY ?? resolve(import.meta.dir, '../../marketplace/registry'); console.log(`\n── Marketplace registry (${MARKETPLACE_REGISTRY}) ──`); if (existsSync(MARKETPLACE_REGISTRY)) { let n = 0; for (const name of await listSubdirs(join(MARKETPLACE_REGISTRY, 'tools'))) { const target = join(itemsDir('tools'), name); if (existsSync(target)) continue; // don't clobber a synced/user version await cp(join(MARKETPLACE_REGISTRY, 'tools', name), target, { recursive: true }); n++; console.log(` tool ${name} (from registry)`); } console.log(` ${n} registry tool(s) added`); console.log(' registry tasks come from the DB export above (native scope) — skipped here'); } else { console.log(' registry not found, skipping'); } console.log('\nDone. Verify counts in the UI, then apply the drop-tables DB migration.'); process.exit(0);