/** * Dev Reset Command * * Reset the local development database to a clean state: * - Drop all tables and recreate * - Optionally re-run seeds * - Can target specific DO instance * * @module cli/commands/dev-reset */ import { resolve, join } from 'node:path' import { existsSync, readFileSync, readdirSync, statSync, rmSync, mkdirSync } from 'node:fs' import type { DevCLIOptions } from '../types.js' import { formatSuccess, formatError, formatInfo, formatWarning, formatDim, formatMigrationResult, formatDuration, createSpinner, } from '../formatting.js' import { loadMigrationsFromDirectory } from '../utils.js' /** * Reset the development database */ export async function runDevReset(options: DevCLIOptions): Promise { const migrationsDir = resolve(process.cwd(), options.migrationsDir || './migrations') const seedsDir = resolve(process.cwd(), options.seedsDir || './seeds') const dataDir = options.persist ? resolve(process.cwd(), options.dataDir || './.postgres.do') : null const verbose = options.verbose || false const shouldSeed = options.seed || false console.log('') console.log(formatInfo('Resetting development database...')) console.log('') try { // If using persistent storage, we can delete the data directory if (dataDir && existsSync(dataDir)) { console.log(formatDim(` Removing data directory: ${dataDir}`)) rmSync(dataDir, { recursive: true, force: true }) mkdirSync(dataDir, { recursive: true }) } // Initialize PGLite const spinner = createSpinner('Initializing fresh database...') spinner.start() // Dynamic import const { PGlite } = await import('@dotdo/pglite') const { createPGLiteSql } = await import('../../pglite/index.js') // Create fresh PGLite instance const pgliteConfig = dataDir ? { dataDir } : {} const pglite = new PGlite(pgliteConfig) await pglite.waitReady // Create SQL interface const sql = await createPGLiteSql(pglite) spinner.succeed('Fresh database initialized') // Run migrations if (existsSync(migrationsDir)) { console.log('') console.log(formatInfo('Running migrations...')) const migrations = loadMigrationsFromDirectory(migrationsDir) if (migrations.length === 0) { console.log(formatDim(' No migrations found')) } else { // Import migration system 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[] }) // Create query executor from PGLite const executor = { async query(sqlStr: string): Promise<{ rows: T[]; fields: { name: string; dataTypeID: number }[] }> { const result = await sql.unsafe(sqlStr) return { rows: result as T[], fields: [] } }, } // Create runner - use conditional spreading for optional properties const runner = createMigrationRunner(executor, registry, { ...(verbose ? { debug: verbose } : {}), onProgress: (event) => { if (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 const startTime = Date.now() const result = await runner.migrate() const duration = Date.now() - startTime if (result.success) { console.log(formatSuccess(` Applied ${result.migrationsRun} migration(s) in ${formatDuration(duration)}`)) } else { console.error(formatError(' Migration failed')) const failedResult = result.results.find((r: { success: boolean }) => !r.success) if (failedResult) { console.error(formatError(` Failed on: ${failedResult.id}`)) console.error(formatError(` Error: ${failedResult.error}`)) } await sql.end() process.exit(1) } } } // Run seeds if requested if (shouldSeed && existsSync(seedsDir)) { console.log('') console.log(formatInfo('Running seeds...')) const seedFiles = findSeedFiles(seedsDir) if (seedFiles.length === 0) { console.log(formatDim(' No seed files found')) } else { for (const seedFile of seedFiles) { console.log(formatDim(` Running ${seedFile.name}...`)) try { // For SQL seeds if (seedFile.name.endsWith('.sql')) { const content = readFileSync(seedFile.path, 'utf-8') await sql.unsafe(content) console.log(formatSuccess(` Applied ${seedFile.name}`)) } // For TypeScript/JavaScript seeds else if (seedFile.name.endsWith('.ts') || seedFile.name.endsWith('.js')) { // Dynamic import const seedModule = await import(seedFile.path) const seedFn = seedModule.default || seedModule.seed if (typeof seedFn === 'function') { await seedFn(sql) console.log(formatSuccess(` Applied ${seedFile.name}`)) } else { console.log(formatWarning(` Skipped ${seedFile.name} (no default export or seed function)`)) } } } catch (error) { console.error(formatError(` Failed ${seedFile.name}:`), error instanceof Error ? error.message : error) } } } } else if (shouldSeed) { console.log('') console.log(formatWarning(`Seeds directory not found: ${seedsDir}`)) } // Cleanup await sql.end() console.log('') console.log(formatSuccess('Database reset complete!')) console.log('') // Output JSON if requested if (options.json) { console.log(JSON.stringify({ success: true, dataDir, migrationsApplied: existsSync(migrationsDir) ? loadMigrationsFromDirectory(migrationsDir).length : 0, seedsApplied: shouldSeed && existsSync(seedsDir) ? findSeedFiles(seedsDir).length : 0, }, null, 2)) } } catch (error) { console.error(formatError('Reset failed:'), error instanceof Error ? error.message : error) if (verbose && error instanceof Error && error.stack) { console.error(error.stack) } process.exit(1) } } /** * Find seed files in directory */ function findSeedFiles(dir: string): Array<{ name: string; path: string }> { if (!existsSync(dir)) { return [] } const entries = readdirSync(dir) const seedFiles: Array<{ name: string; path: string }> = [] for (const entry of entries) { if (entry.startsWith('.') || entry.startsWith('_')) { continue } const entryPath = join(dir, entry) const stat = statSync(entryPath) if (stat.isFile() && (entry.endsWith('.sql') || entry.endsWith('.ts') || entry.endsWith('.js'))) { seedFiles.push({ name: entry, path: entryPath }) } } // Sort by name for consistent ordering return seedFiles.sort((a, b) => a.name.localeCompare(b.name)) }