/** * Migrate Command * * Run pending migrations against a database. * * @module cli/commands/migrate */ import { resolve } from 'node:path' import { existsSync } from 'node:fs' import type { MigrateCLIOptions } from '../types.js' import { formatSuccess, formatError, formatInfo, formatMigrationResult, formatDuration } from '../formatting.js' import { loadMigrationsFromDirectory, createQueryExecutor } from '../utils.js' /** * Run migrations command */ export async function runMigrate(options: MigrateCLIOptions): Promise { const url = options.url || process.env['DATABASE_URL'] if (!url) { console.error(formatError('Database URL is required')) console.error(formatInfo('Provide --url or set DATABASE_URL environment variable')) process.exit(1) } // Load migrations from directory const migrationsDir = resolve(process.cwd(), options.migrationsDir || './migrations') if (!existsSync(migrationsDir)) { console.error(formatError(`Migrations directory not found: ${migrationsDir}`)) console.error(formatInfo('Create a migrations directory or specify one with --migrations-dir')) process.exit(1) } console.log(formatInfo(`Loading migrations from: ${migrationsDir}`)) try { const migrations = loadMigrationsFromDirectory(migrationsDir) if (migrations.length === 0) { console.log(formatInfo('No migrations found')) return } console.log(formatInfo(`Found ${migrations.length} migration(s)`)) // Import migration system dynamically to avoid bundling issues const { createMigrationRegistry, createMigrationRunner } = await import('@dotdo/postgres/migrations') // Create registry - cast MigrationDefinition to Migration (structurally compatible) const registry = createMigrationRegistry({ migrations: migrations as import('@dotdo/postgres/migrations').Migration[] }) // Connect to database console.log(formatInfo('Connecting to database...')) const executor = await createQueryExecutor(url) // Create runner with progress callback - use conditional spreading for optional properties const runner = createMigrationRunner(executor, registry, { ...(options.verbose ? { debug: options.verbose } : {}), onProgress: (event) => { if (options.verbose || event.phase === 'completed' || event.phase === 'failed') { // Adapt MigrationProgressEvent to MigrationProgressDisplay const displayEvent = { migration: event.migration, index: event.index, total: event.total, phase: event.phase, ...(event.durationMs !== undefined && { durationMs: event.durationMs }), ...(event.error !== undefined && { error: event.error }), } console.log(formatMigrationResult(displayEvent)) } }, }) // Run migrations console.log(formatInfo('Running migrations...')) const startTime = Date.now() const result = await runner.migrate() const duration = Date.now() - startTime // Output result if (options.json) { console.log(JSON.stringify(result, null, 2)) } else { console.log('') if (result.success) { if (result.migrationsRun === 0) { console.log(formatSuccess('Database is up to date')) } else { console.log(formatSuccess(`Successfully ran ${result.migrationsRun} migration(s) in ${formatDuration(duration)}`)) } console.log(formatInfo(`Schema version: ${result.fromVersion} -> ${result.toVersion}`)) } else { console.error(formatError('Migration failed')) const failedResult = result.results.find((r) => !r.success) if (failedResult) { console.error(formatError(`Failed on: ${failedResult.id}`)) console.error(formatError(`Error: ${failedResult.error}`)) } process.exit(1) } } // Cleanup if ('end' in executor && typeof executor.end === 'function') { await executor.end() } } catch (error) { console.error(formatError('Migration error:'), error instanceof Error ? error.message : error) if (options.verbose && error instanceof Error && error.stack) { console.error(error.stack) } process.exit(1) } }