@strav/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,68 @@
1
+ import { join } from 'node:path'
2
+ import { existsSync } from 'node:fs'
3
+ import type { Command } from 'commander'
4
+ import chalk from 'chalk'
5
+ import { toSnakeCase } from '@stravigor/kernel/helpers/strings'
6
+ import { formatAndWrite } from '../generators/config.ts'
7
+
8
+ const SEEDERS_PATH = 'database/seeders'
9
+
10
+ export function register(program: Command): void {
11
+ program
12
+ .command('generate:seeder <name>')
13
+ .alias('g:seeder')
14
+ .description('Generate a database seeder class')
15
+ .option('-f, --force', 'Overwrite existing file')
16
+ .action(async (name: string, { force }: { force?: boolean }) => {
17
+ try {
18
+ // Normalize: "UserSeeder" | "User" → class UserSeeder, file user_seeder.ts
19
+ const className = name.endsWith('Seeder') ? name : `${name}Seeder`
20
+ const baseName = className.replace(/Seeder$/, '') || 'Database'
21
+ const fileName = toSnakeCase(baseName) + '_seeder'
22
+ const filePath = join(SEEDERS_PATH, `${fileName}.ts`)
23
+
24
+ if (existsSync(filePath) && !force) {
25
+ console.log(chalk.yellow(`File already exists: `) + chalk.dim(filePath))
26
+ console.log(chalk.dim(' Use --force to overwrite.'))
27
+ return
28
+ }
29
+
30
+ const isDatabaseSeeder = baseName === 'Database'
31
+
32
+ const content = isDatabaseSeeder
33
+ ? generateDatabaseSeeder(className)
34
+ : generateSeeder(className)
35
+
36
+ await formatAndWrite([{ path: filePath, content }])
37
+
38
+ console.log(chalk.green(`Created: `) + chalk.dim(filePath))
39
+ } catch (err) {
40
+ console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
41
+ process.exit(1)
42
+ }
43
+ })
44
+ }
45
+
46
+ function generateDatabaseSeeder(className: string): string {
47
+ return `import { Seeder } from '@stravigor/database/database'
48
+
49
+ export default class ${className} extends Seeder {
50
+ async run(): Promise<void> {
51
+ // Call sub-seeders:
52
+ // await this.call(UserSeeder)
53
+ }
54
+ }
55
+ `
56
+ }
57
+
58
+ function generateSeeder(className: string): string {
59
+ return `import { Seeder } from '@stravigor/database/database'
60
+
61
+ export default class ${className} extends Seeder {
62
+ async run(): Promise<void> {
63
+ // Use factories or direct inserts to seed data:
64
+ // await UserFactory.createMany(10)
65
+ }
66
+ }
67
+ `
68
+ }
@@ -0,0 +1,153 @@
1
+ import type { Command } from 'commander'
2
+ import chalk from 'chalk'
3
+ import { bootstrap, shutdown } from '../cli/bootstrap.ts'
4
+ import SchemaDiffer from '@stravigor/database/database/migration/differ'
5
+
6
+ export function register(program: Command): void {
7
+ program
8
+ .command('compare')
9
+ .alias('migration:compare')
10
+ .description('Compare schema with database and report differences')
11
+ .action(async () => {
12
+ let db
13
+ try {
14
+ const { db: database, registry, introspector } = await bootstrap()
15
+ db = database
16
+
17
+ console.log(chalk.cyan('Comparing schema with database...\n'))
18
+
19
+ const desired = registry.buildRepresentation()
20
+ const actual = await introspector.introspect()
21
+ const diff = new SchemaDiffer().diff(desired, actual)
22
+
23
+ const hasChanges =
24
+ diff.enums.length > 0 ||
25
+ diff.tables.length > 0 ||
26
+ diff.constraints.length > 0 ||
27
+ diff.indexes.length > 0
28
+
29
+ if (!hasChanges) {
30
+ console.log(chalk.green('Schema is in sync with the database.'))
31
+ return
32
+ }
33
+
34
+ // --- Enum changes ---
35
+ if (diff.enums.length > 0) {
36
+ console.log(chalk.bold('Enum changes:'))
37
+ for (const e of diff.enums) {
38
+ if (e.kind === 'create') {
39
+ console.log(chalk.green(` + CREATE ${e.name} (${e.values.join(', ')})`))
40
+ } else if (e.kind === 'drop') {
41
+ console.log(chalk.red(` - DROP ${e.name}`))
42
+ } else if (e.kind === 'modify') {
43
+ console.log(chalk.yellow(` ~ MODIFY ${e.name} (add: ${e.addedValues.join(', ')})`))
44
+ }
45
+ }
46
+ console.log()
47
+ }
48
+
49
+ // --- Table changes ---
50
+ if (diff.tables.length > 0) {
51
+ console.log(chalk.bold('Table changes:'))
52
+ for (const t of diff.tables) {
53
+ if (t.kind === 'create') {
54
+ console.log(
55
+ chalk.green(` + CREATE ${t.table.name}`) +
56
+ chalk.dim(` (${t.table.columns.length} columns)`)
57
+ )
58
+ } else if (t.kind === 'drop') {
59
+ console.log(chalk.red(` - DROP ${t.table.name}`))
60
+ } else if (t.kind === 'modify') {
61
+ console.log(chalk.yellow(` ~ MODIFY ${t.tableName}`))
62
+ for (const c of t.columns) {
63
+ if (c.kind === 'add') {
64
+ console.log(
65
+ chalk.green(
66
+ ` + ADD COLUMN ${c.column.name} (${typeof c.column.pgType === 'string' ? c.column.pgType : 'custom'})`
67
+ )
68
+ )
69
+ } else if (c.kind === 'drop') {
70
+ console.log(chalk.red(` - DROP COLUMN ${c.column.name}`))
71
+ } else if (c.kind === 'alter') {
72
+ const changes: string[] = []
73
+ if (c.typeChange) changes.push(`type: ${c.typeChange.from} -> ${c.typeChange.to}`)
74
+ if (c.nullableChange)
75
+ changes.push(c.nullableChange.to ? 'set NOT NULL' : 'drop NOT NULL')
76
+ if (c.defaultChange) changes.push('default changed')
77
+ console.log(
78
+ chalk.yellow(` ~ ALTER COLUMN ${c.columnName} (${changes.join(', ')})`)
79
+ )
80
+ }
81
+ }
82
+ }
83
+ }
84
+ console.log()
85
+ }
86
+
87
+ // --- Constraint changes ---
88
+ if (diff.constraints.length > 0) {
89
+ console.log(chalk.bold('Constraint changes:'))
90
+ for (const c of diff.constraints) {
91
+ if (c.kind === 'add_fk') {
92
+ console.log(
93
+ chalk.green(
94
+ ` + ADD FK ${c.tableName}(${c.constraint.columns.join(',')}) -> ${c.constraint.referencedTable}`
95
+ )
96
+ )
97
+ } else if (c.kind === 'drop_fk') {
98
+ console.log(
99
+ chalk.red(
100
+ ` - DROP FK ${c.tableName}(${c.constraint.columns.join(',')}) -> ${c.constraint.referencedTable}`
101
+ )
102
+ )
103
+ } else if (c.kind === 'add_unique') {
104
+ console.log(
105
+ chalk.green(` + ADD UQ ${c.tableName}(${c.constraint.columns.join(',')})`)
106
+ )
107
+ } else if (c.kind === 'drop_unique') {
108
+ console.log(
109
+ chalk.red(` - DROP UQ ${c.tableName}(${c.constraint.columns.join(',')})`)
110
+ )
111
+ }
112
+ }
113
+ console.log()
114
+ }
115
+
116
+ // --- Index changes ---
117
+ if (diff.indexes.length > 0) {
118
+ console.log(chalk.bold('Index changes:'))
119
+ for (const i of diff.indexes) {
120
+ if (i.kind === 'add') {
121
+ const unique = i.index.unique ? 'UNIQUE ' : ''
122
+ console.log(
123
+ chalk.green(
124
+ ` + CREATE ${unique}INDEX ${i.tableName}(${i.index.columns.join(',')})`
125
+ )
126
+ )
127
+ } else if (i.kind === 'drop') {
128
+ console.log(
129
+ chalk.red(` - DROP INDEX ${i.tableName}(${i.index.columns.join(',')})`)
130
+ )
131
+ }
132
+ }
133
+ console.log()
134
+ }
135
+
136
+ // --- Summary ---
137
+ const creates = diff.tables.filter(t => t.kind === 'create').length
138
+ const drops = diff.tables.filter(t => t.kind === 'drop').length
139
+ const modifies = diff.tables.filter(t => t.kind === 'modify').length
140
+ console.log(
141
+ chalk.bold('Summary: ') +
142
+ `${creates} table(s) to create, ${drops} to drop, ${modifies} to modify, ` +
143
+ `${diff.enums.length} enum change(s), ${diff.constraints.length} constraint change(s), ${diff.indexes.length} index change(s)`
144
+ )
145
+ console.log(chalk.dim('\nRun "bun strav generate:migration" to create migration files.'))
146
+ } catch (err) {
147
+ console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
148
+ process.exit(1)
149
+ } finally {
150
+ if (db) await shutdown(db)
151
+ }
152
+ })
153
+ }
@@ -0,0 +1,149 @@
1
+ import type { Command } from 'commander'
2
+ import chalk from 'chalk'
3
+ import { createInterface } from 'node:readline'
4
+ import { rmSync } from 'node:fs'
5
+ import { bootstrap, shutdown } from '../cli/bootstrap.ts'
6
+ import type Database from '@stravigor/database/database/database'
7
+ import type SchemaRegistry from '@stravigor/database/schema/registry'
8
+ import type DatabaseIntrospector from '@stravigor/database/database/introspector'
9
+ import SchemaDiffer from '@stravigor/database/database/migration/differ'
10
+ import SqlGenerator from '@stravigor/database/database/migration/sql_generator'
11
+ import MigrationFileGenerator from '@stravigor/database/database/migration/file_generator'
12
+ import MigrationTracker from '@stravigor/database/database/migration/tracker'
13
+ import MigrationRunner from '@stravigor/database/database/migration/runner'
14
+ import { getDatabasePaths } from '../config/loader.ts'
15
+
16
+ /**
17
+ * Drop all tables and enum types, regenerate a single migration from
18
+ * the current schema definitions, and run it.
19
+ *
20
+ * Shared by `fresh` and `seed --fresh`.
21
+ */
22
+ export async function freshDatabase(
23
+ db: Database,
24
+ registry: SchemaRegistry,
25
+ introspector: DatabaseIntrospector,
26
+ migrationsPath: string = 'database/migrations'
27
+ ): Promise<number> {
28
+ // Drop all tables in public schema
29
+ console.log(chalk.cyan('\nDropping all tables and types...'))
30
+
31
+ const tables = await db.sql`
32
+ SELECT table_name FROM information_schema.tables
33
+ WHERE table_schema = 'public' AND table_type = 'BASE TABLE'
34
+ `
35
+ for (const row of tables) {
36
+ await db.sql.unsafe(`DROP TABLE IF EXISTS "${row.table_name}" CASCADE`)
37
+ }
38
+
39
+ // Drop all enum types in public schema
40
+ const types = await db.sql`
41
+ SELECT t.typname
42
+ FROM pg_type t
43
+ JOIN pg_namespace n ON n.oid = t.typnamespace
44
+ WHERE n.nspname = 'public'
45
+ AND t.typtype = 'e'
46
+ `
47
+ for (const row of types) {
48
+ await db.sql.unsafe(`DROP TYPE IF EXISTS "${row.typname}" CASCADE`)
49
+ }
50
+
51
+ // Delete existing migration files
52
+ console.log(chalk.cyan('Clearing migration directory...'))
53
+ rmSync(migrationsPath, { recursive: true, force: true })
54
+
55
+ // Generate new migration
56
+ console.log(chalk.cyan('Generating fresh migration...'))
57
+
58
+ const desired = registry.buildRepresentation()
59
+ const actual = await introspector.introspect()
60
+ const diff = new SchemaDiffer().diff(desired, actual)
61
+
62
+ const sql = new SqlGenerator().generate(diff)
63
+ const version = Date.now().toString()
64
+ const tableOrder = desired.tables.map(t => t.name)
65
+
66
+ const fileGen = new MigrationFileGenerator(migrationsPath)
67
+ await fileGen.generate(version, 'fresh', sql, diff, tableOrder)
68
+
69
+ // Run the migration
70
+ console.log(chalk.cyan('Running migration...'))
71
+
72
+ const tracker = new MigrationTracker(db)
73
+ const runner = new MigrationRunner(db, tracker, migrationsPath)
74
+ const result = await runner.run()
75
+
76
+ return result.applied.length
77
+ }
78
+
79
+ /**
80
+ * Guard that ensures APP_ENV is "local". Exits the process if not.
81
+ */
82
+ export function requireLocalEnv(commandName: string): void {
83
+ const appEnv = process.env.APP_ENV
84
+ if (appEnv !== 'local') {
85
+ console.error(
86
+ chalk.red('REJECTED: ') + `${commandName} can only run when APP_ENV is set to "local".`
87
+ )
88
+ if (!appEnv) {
89
+ console.error(chalk.dim(' APP_ENV is not defined in .env'))
90
+ } else {
91
+ console.error(chalk.dim(` Current APP_ENV: "${appEnv}"`))
92
+ }
93
+ process.exit(1)
94
+ }
95
+ }
96
+
97
+ export function register(program: Command): void {
98
+ program
99
+ .command('fresh')
100
+ .alias('migration:fresh')
101
+ .description('Reset database and migrations, regenerate and run from scratch')
102
+ .action(async () => {
103
+ requireLocalEnv('fresh')
104
+
105
+ // 6-digit challenge
106
+ const challenge = String(Math.floor(100000 + Math.random() * 900000))
107
+ console.log(
108
+ chalk.red('WARNING: ') +
109
+ 'This will ' +
110
+ chalk.red('destroy ALL data') +
111
+ ' in the database and recreate everything from schemas.'
112
+ )
113
+ console.log(`\n Type ${chalk.yellow(challenge)} to confirm:\n`)
114
+
115
+ const answer = await prompt(' > ')
116
+ if (answer.trim() !== challenge) {
117
+ console.error(chalk.red('\nChallenge code does not match. Operation cancelled.'))
118
+ process.exit(1)
119
+ }
120
+
121
+ let db
122
+ try {
123
+ // Get configured database paths
124
+ const dbPaths = await getDatabasePaths()
125
+
126
+ const { db: database, registry, introspector } = await bootstrap()
127
+ db = database
128
+
129
+ const applied = await freshDatabase(db, registry, introspector, dbPaths.migrations)
130
+
131
+ console.log(chalk.green(`\nFresh migration complete. Applied ${applied} migration(s).`))
132
+ } catch (err) {
133
+ console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
134
+ process.exit(1)
135
+ } finally {
136
+ if (db) await shutdown(db)
137
+ }
138
+ })
139
+ }
140
+
141
+ function prompt(question: string): Promise<string> {
142
+ const rl = createInterface({ input: process.stdin, output: process.stdout })
143
+ return new Promise(resolve => {
144
+ rl.question(question, answer => {
145
+ rl.close()
146
+ resolve(answer)
147
+ })
148
+ })
149
+ }
@@ -0,0 +1,93 @@
1
+ import type { Command } from 'commander'
2
+ import chalk from 'chalk'
3
+ import { bootstrap, shutdown } from '../cli/bootstrap.ts'
4
+ import SchemaDiffer from '@stravigor/database/database/migration/differ'
5
+ import SqlGenerator from '@stravigor/database/database/migration/sql_generator'
6
+ import MigrationFileGenerator from '@stravigor/database/database/migration/file_generator'
7
+ import { discoverDomains } from '@stravigor/database'
8
+ import { getDatabasePaths } from '../config/loader.ts'
9
+
10
+ export function register(program: Command): void {
11
+ program
12
+ .command('generate:migration')
13
+ .aliases(['migration:generate', 'g:migration'])
14
+ .description('Generate migration files from schema changes')
15
+ .option('-m, --message <message>', 'Migration message', 'migration')
16
+ .option('-s, --scope <scope>', 'Domain (e.g., public, tenant, factory, marketing)', 'public')
17
+ .action(async (opts: { message: string; scope: string }) => {
18
+ let db
19
+ try {
20
+ // Get configured database paths
21
+ const dbPaths = await getDatabasePaths()
22
+
23
+ // Validate scope against available domains
24
+ const availableDomains = discoverDomains(dbPaths.schemas)
25
+ if (!availableDomains.includes(opts.scope)) {
26
+ throw new Error(`Invalid domain: ${opts.scope}. Available domains: ${availableDomains.join(', ')}`)
27
+ }
28
+ const scope = opts.scope
29
+
30
+ const { db: database, registry, introspector } = await bootstrap(scope)
31
+ db = database
32
+
33
+ console.log(chalk.cyan(`Comparing ${scope} schema with database...`))
34
+
35
+ const desired = registry.buildRepresentation()
36
+ const actual = await introspector.introspect()
37
+ const diff = new SchemaDiffer().diff(desired, actual)
38
+
39
+ const hasChanges =
40
+ diff.enums.length > 0 ||
41
+ diff.tables.length > 0 ||
42
+ diff.constraints.length > 0 ||
43
+ diff.indexes.length > 0
44
+
45
+ if (!hasChanges) {
46
+ console.log(chalk.green('No changes detected. Schema is in sync with the database.'))
47
+ return
48
+ }
49
+
50
+ const sql = new SqlGenerator().generate(diff)
51
+ const version = Date.now().toString()
52
+
53
+ // Use the table order from the desired representation (already dependency-ordered)
54
+ const tableOrder = desired.tables.map(t => t.name)
55
+
56
+ // Use scoped migrations path from configuration
57
+ const scopedPath = `${dbPaths.migrations}/${scope}`
58
+ const fileGen = new MigrationFileGenerator(scopedPath)
59
+ const dir = await fileGen.generate(version, opts.message, sql, diff, tableOrder)
60
+
61
+ // Print summary
62
+ console.log(chalk.green(`\nMigration generated: ${version}`))
63
+ console.log(chalk.dim(` Directory: ${dir}`))
64
+ console.log(chalk.dim(` Message: ${opts.message}\n`))
65
+
66
+ const counts: string[] = []
67
+ const creates = diff.tables.filter(t => t.kind === 'create').length
68
+ const drops = diff.tables.filter(t => t.kind === 'drop').length
69
+ const modifies = diff.tables.filter(t => t.kind === 'modify').length
70
+ if (creates > 0) counts.push(chalk.green(`${creates} table(s) to create`))
71
+ if (drops > 0) counts.push(chalk.red(`${drops} table(s) to drop`))
72
+ if (modifies > 0) counts.push(chalk.yellow(`${modifies} table(s) to modify`))
73
+
74
+ const enumCreates = diff.enums.filter(e => e.kind === 'create').length
75
+ const enumDrops = diff.enums.filter(e => e.kind === 'drop').length
76
+ const enumModifies = diff.enums.filter(e => e.kind === 'modify').length
77
+ if (enumCreates > 0) counts.push(chalk.green(`${enumCreates} enum(s) to create`))
78
+ if (enumDrops > 0) counts.push(chalk.red(`${enumDrops} enum(s) to drop`))
79
+ if (enumModifies > 0) counts.push(chalk.yellow(`${enumModifies} enum(s) to modify`))
80
+
81
+ if (diff.constraints.length > 0)
82
+ counts.push(`${diff.constraints.length} constraint change(s)`)
83
+ if (diff.indexes.length > 0) counts.push(`${diff.indexes.length} index change(s)`)
84
+
85
+ console.log(' Summary: ' + counts.join(', '))
86
+ } catch (err) {
87
+ console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
88
+ process.exit(1)
89
+ } finally {
90
+ if (db) await shutdown(db)
91
+ }
92
+ })
93
+ }
@@ -0,0 +1,66 @@
1
+ import type { Command } from 'commander'
2
+ import chalk from 'chalk'
3
+ import { bootstrap, shutdown } from '../cli/bootstrap.ts'
4
+ import MigrationTracker from '@stravigor/database/database/migration/tracker'
5
+ import MigrationRunner from '@stravigor/database/database/migration/runner'
6
+ import { discoverDomains } from '@stravigor/database'
7
+ import { getDatabasePaths } from '../config/loader.ts'
8
+
9
+ export function register(program: Command): void {
10
+ program
11
+ .command('rollback')
12
+ .alias('migration:rollback')
13
+ .description('Rollback migrations by batch')
14
+ .option('--batch <number>', 'Specific batch number to rollback')
15
+ .option('-s, --scope <scope>', 'Domain (e.g., public, tenant, factory, marketing)', 'public')
16
+ .action(async (opts: { batch?: string; scope: string }) => {
17
+ let db
18
+ try {
19
+ // Get configured database paths
20
+ const dbPaths = await getDatabasePaths()
21
+
22
+ // Validate scope against available domains
23
+ const availableDomains = discoverDomains(dbPaths.schemas)
24
+ if (!availableDomains.includes(opts.scope)) {
25
+ throw new Error(`Invalid domain: ${opts.scope}. Available domains: ${availableDomains.join(', ')}`)
26
+ }
27
+ const scope = opts.scope
28
+
29
+ const { db: database } = await bootstrap()
30
+ db = database
31
+
32
+ const scopedPath = `${dbPaths.migrations}/${scope}`
33
+ const tracker = new MigrationTracker(db, scope)
34
+ const runner = new MigrationRunner(db, tracker, scopedPath, scope)
35
+
36
+ const batchNum = opts.batch ? parseInt(opts.batch, 10) : undefined
37
+
38
+ console.log(
39
+ batchNum
40
+ ? chalk.cyan(`Rolling back batch ${batchNum}...`)
41
+ : chalk.cyan('Rolling back last batch...')
42
+ )
43
+
44
+ const result = await runner.rollback(batchNum)
45
+
46
+ if (result.rolledBack.length === 0) {
47
+ console.log(chalk.yellow('Nothing to rollback.'))
48
+ return
49
+ }
50
+
51
+ console.log(
52
+ chalk.green(
53
+ `\nRolled back ${result.rolledBack.length} migration(s) from batch ${result.batch}:`
54
+ )
55
+ )
56
+ for (const version of result.rolledBack) {
57
+ console.log(chalk.dim(` - ${version}`))
58
+ }
59
+ } catch (err) {
60
+ console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
61
+ process.exit(1)
62
+ } finally {
63
+ if (db) await shutdown(db)
64
+ }
65
+ })
66
+ }
@@ -0,0 +1,57 @@
1
+ import type { Command } from 'commander'
2
+ import chalk from 'chalk'
3
+ import { bootstrap, shutdown } from '../cli/bootstrap.ts'
4
+ import MigrationTracker from '@stravigor/database/database/migration/tracker'
5
+ import MigrationRunner from '@stravigor/database/database/migration/runner'
6
+ import { discoverDomains } from '@stravigor/database'
7
+ import { getDatabasePaths } from '../config/loader.ts'
8
+
9
+ export function register(program: Command): void {
10
+ program
11
+ .command('migrate')
12
+ .alias('migration:run')
13
+ .description('Run pending migrations')
14
+ .option('-s, --scope <scope>', 'Domain (e.g., public, tenant, factory, marketing)', 'public')
15
+ .action(async (opts: { scope: string }) => {
16
+ let db
17
+ try {
18
+ // Get configured database paths
19
+ const dbPaths = await getDatabasePaths()
20
+
21
+ // Validate scope against available domains
22
+ const availableDomains = discoverDomains(dbPaths.schemas)
23
+ if (!availableDomains.includes(opts.scope)) {
24
+ throw new Error(`Invalid domain: ${opts.scope}. Available domains: ${availableDomains.join(', ')}`)
25
+ }
26
+ const scope = opts.scope
27
+
28
+ const { db: database } = await bootstrap()
29
+ db = database
30
+
31
+ const scopedPath = `${dbPaths.migrations}/${scope}`
32
+ const tracker = new MigrationTracker(db, scope)
33
+ const runner = new MigrationRunner(db, tracker, scopedPath, scope)
34
+
35
+ console.log(chalk.cyan('Running pending migrations...'))
36
+
37
+ const result = await runner.run()
38
+
39
+ if (result.applied.length === 0) {
40
+ console.log(chalk.green('Nothing to migrate. All migrations are up to date.'))
41
+ return
42
+ }
43
+
44
+ console.log(
45
+ chalk.green(`\nApplied ${result.applied.length} migration(s) in batch ${result.batch}:`)
46
+ )
47
+ for (const version of result.applied) {
48
+ console.log(chalk.dim(` - ${version}`))
49
+ }
50
+ } catch (err) {
51
+ console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
52
+ process.exit(1)
53
+ } finally {
54
+ if (db) await shutdown(db)
55
+ }
56
+ })
57
+ }