/** * One-shot migration: legacy NocoDB Interests → new client/interest split. * * Usage: * * pnpm tsx scripts/migrate-from-nocodb.ts --dry-run * Pulls the live NocoDB base, runs the transform + dedup pipeline, * writes a report to .migration//. NO database writes. * * pnpm tsx scripts/migrate-from-nocodb.ts --dry-run --port-slug harbor-royale * Same, but tags the planned writes with the named port (matters for * the apply phase — every client/interest belongs to one port). * * pnpm tsx scripts/migrate-from-nocodb.ts --apply --report .migration// * [Not yet implemented — apply phase comes in a follow-up PR.] * * Design reference: docs/superpowers/specs/2026-05-03-dedup-and-migration-design.md §9. */ import 'dotenv/config'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { fetchSnapshot, loadNocoDbConfig } from '@/lib/dedup/nocodb-source'; import { transformSnapshot } from '@/lib/dedup/migration-transform'; import { resolveReportPaths, writeReport } from '@/lib/dedup/migration-report'; interface CliArgs { dryRun: boolean; apply: boolean; portSlug: string | null; reportDir: string | null; } function parseArgs(argv: string[]): CliArgs { const args: CliArgs = { dryRun: false, apply: false, portSlug: null, reportDir: null, }; for (let i = 0; i < argv.length; i += 1) { const a = argv[i]!; if (a === '--dry-run') args.dryRun = true; else if (a === '--apply') args.apply = true; else if (a === '--port-slug') args.portSlug = argv[++i] ?? null; else if (a === '--report') args.reportDir = argv[++i] ?? null; else if (a === '-h' || a === '--help') { printHelp(); process.exit(0); } else { console.error(`Unknown argument: ${a}`); printHelp(); process.exit(1); } } return args; } function printHelp(): void { console.log(`Usage: pnpm tsx scripts/migrate-from-nocodb.ts --dry-run [--port-slug ] Pulls NocoDB → transforms → writes report to .migration//. No database writes. pnpm tsx scripts/migrate-from-nocodb.ts --apply --report .migration// Apply phase. (Not yet implemented.) Flags: --dry-run Read NocoDB, write report only. --apply Actually write to the new DB. (Not yet supported.) --port-slug Port slug to attach to all imported entities. Defaults to the first available port if omitted. --report Path to a previously-generated report dir (only used by --apply). -h, --help Show this help. `); } async function main(): Promise { const args = parseArgs(process.argv.slice(2)); if (!args.dryRun && !args.apply) { console.error('Must specify --dry-run or --apply'); printHelp(); process.exit(1); } if (args.apply) { console.error('--apply is not yet implemented in this version. P3 ships dry-run first.'); console.error('See docs/superpowers/specs/2026-05-03-dedup-and-migration-design.md §9.2.'); process.exit(2); } // ── Dry-run path ─────────────────────────────────────────────────────────── console.log('[migrate] Loading NocoDB config…'); const config = loadNocoDbConfig(); console.log(`[migrate] Source: ${config.url}`); console.log('[migrate] Fetching snapshot from NocoDB…'); const start = Date.now(); const snapshot = await fetchSnapshot(config); const elapsed = ((Date.now() - start) / 1000).toFixed(1); console.log( `[migrate] Snapshot fetched in ${elapsed}s — ${snapshot.interests.length} interests, ${snapshot.residentialInterests.length} residential, ${snapshot.berths.length} berths.`, ); console.log('[migrate] Running transform + dedup pipeline…'); const plan = transformSnapshot(snapshot); // Resolve output paths relative to the worktree root (the script itself // lives in scripts/; we want the .migration dir at the repo root). const scriptDir = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(scriptDir, '..'); const generatedAt = new Date().toISOString(); const paths = resolveReportPaths(repoRoot); console.log(`[migrate] Writing report to ${paths.rootDir}…`); await writeReport(paths, plan, generatedAt); // ── Console summary ────────────────────────────────────────────────────── const s = plan.stats; console.log(''); console.log('=== Migration Plan Summary ==='); console.log( ` Input: ${s.inputInterestRows} interests, ${s.inputResidentialRows} residential interests`, ); console.log(` Output: ${s.outputClients} clients, ${s.outputInterests} interests`); console.log(` ${s.outputContacts} contacts, ${s.outputAddresses} addresses`); console.log( ` Dedup: ${s.autoLinkedClusters} auto-linked clusters, ${s.needsReviewPairs} pairs flagged for review`, ); console.log(` Quality: ${s.flaggedRows} rows flagged (see report.csv)`); console.log(''); console.log(` Full report: ${paths.summaryPath}`); console.log(''); } main().catch((err) => { console.error('[migrate] Fatal error:', err); process.exit(1); });