import * as fs from "node:fs/promises"; import path from "node:path"; import { cancel, isCancel, select, text } from "@clack/prompts"; import { Command } from "commander"; import type { FumaDB } from ".."; export function createCli(options: { db: FumaDB; /** * CLI command name, must be lowercase without whitespaces. */ command: string; description?: string; /** * CLI Version */ version: string; }) { const db = options.db as FumaDB; async function selectVersion(defaultValue?: string) { const schemas = db.schemas; const selected = await select({ message: "Select target schema version:", options: schemas.map((s, i) => { let hint: string | undefined; if (s.version === defaultValue) { hint = "current"; } else if (i === schemas.length - 1) { hint = "latest"; } return { value: s.version, label: s.version, hint, }; }), initialValue: defaultValue, }); if (isCancel(selected)) { cancel("Migration cancelled."); process.exit(0); } return selected; } async function inputOutputPath(type: "sql" | "orm", suggestion: string) { const result = await text({ message: type === "sql" ? "Where to output the SQL migration file?" : "Where to output the generated schema? (it will override the destination)", defaultValue: suggestion, placeholder: suggestion, }); if (isCancel(result)) { cancel("Migration cancelled."); process.exit(0); } return result; } return { async main() { const program = new Command(); program .name(options.command) .description( options.description ?? "FumaDB CLI for migrations and schema generation" ) .version(options.version); program .command("migrate:up") .description("Migrate to the next schema version") .action(async () => { const migrator = db.createMigrator(); const next = await migrator.next(); if (!next) { console.log("Already up to date."); process.exit(1); } const result = await migrator.migrateTo(next.version); await result.execute(); console.log(`Migration to ${next.version} executed.`); }); program .command("migrate:down") .description("Rollback to the previous schema version") .action(async () => { const migrator = db.createMigrator(); const prev = await migrator.previous(); if (!prev) { console.log("Cannot downgrade."); process.exit(1); } const result = await migrator.migrateTo(prev.version); await result.execute(); console.log(`Migration to ${prev.version} executed.`); }); program .command("migrate:to [version]") .alias("migrate") .description( "Migrate to a specific schema version (interactive if not provided)" ) .action(async (version: string | undefined) => { const migrator = db.createMigrator(); version ??= await selectVersion(await migrator.getVersion()); const result = version === "latest" ? await migrator.migrateToLatest() : await migrator.migrateTo(version); await result.execute(); console.log(`Migrated to version ${version}.`); }); program .command("generate [version]") .description( "Output SQL (for Kysely) or database schema (for ORMs) for the migration." ) .option( "-o, --output ", "the output path of generated SQL/schema file" ) .action( async ( version: string | undefined, { output }: { output?: string } ) => { let generated: string; if (db.adapter.createMigrationEngine) { const migrator = db.createMigrator(); version ??= await selectVersion(await migrator.getVersion()); const result = version === "latest" ? await migrator.migrateToLatest() : await migrator.migrateTo(version); if (!result.getSQL) throw new Error( "The adapter doesn't support migration file generation." ); generated = result.getSQL(); output ??= await inputOutputPath( "sql", `./migrations/${Date.now()}.sql` ); } else if (db.adapter.generateSchema) { version ??= await selectVersion(); const result = db.generateSchema(version); generated = result.code; output ??= await inputOutputPath("orm", result.path); } else { throw new Error( "The adapter doesn't support migration generation." ); } await fs.mkdir(path.dirname(output), { recursive: true }); await fs.writeFile(output, generated); console.log("Successful."); } ); await program.parseAsync(process.argv); }, }; }