@strav/cli 0.4.30 → 1.0.0-alpha.4

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.
Files changed (63) hide show
  1. package/package.json +17 -41
  2. package/src/binder.ts +88 -0
  3. package/src/command.ts +297 -0
  4. package/src/config_list.ts +42 -0
  5. package/src/config_show.ts +50 -0
  6. package/src/console_provider.ts +46 -0
  7. package/src/exit_codes.ts +26 -0
  8. package/src/index.ts +60 -2
  9. package/src/key_generate.ts +66 -0
  10. package/src/make/index.ts +17 -0
  11. package/src/make/make_command_file.ts +27 -0
  12. package/src/make/make_controller.ts +24 -0
  13. package/src/make/make_factory.ts +25 -0
  14. package/src/make/make_job.ts +25 -0
  15. package/src/make/make_mail.ts +27 -0
  16. package/src/make/make_middleware.ts +23 -0
  17. package/src/make/make_migration.ts +48 -0
  18. package/src/make/make_model.ts +91 -0
  19. package/src/make/make_notification.ts +23 -0
  20. package/src/make/make_policy.ts +24 -0
  21. package/src/make/make_provider.ts +29 -0
  22. package/src/make/make_repository.ts +30 -0
  23. package/src/make/make_request.ts +24 -0
  24. package/src/make/make_seeder.ts +23 -0
  25. package/src/make/make_test.ts +22 -0
  26. package/src/make_command.ts +69 -0
  27. package/src/run_cli.ts +121 -0
  28. package/src/scaffold_console_provider.ts +45 -0
  29. package/src/signature.ts +171 -0
  30. package/src/subset_boot.ts +51 -0
  31. package/src/util_console_provider.ts +18 -0
  32. package/src/cli/bootstrap.ts +0 -77
  33. package/src/cli/command_loader.ts +0 -180
  34. package/src/cli/index.ts +0 -3
  35. package/src/cli/strav.ts +0 -13
  36. package/src/commands/db_seed.ts +0 -77
  37. package/src/commands/db_setup_roles.ts +0 -101
  38. package/src/commands/generate_api.ts +0 -93
  39. package/src/commands/generate_key.ts +0 -47
  40. package/src/commands/generate_models.ts +0 -49
  41. package/src/commands/generate_seeder.ts +0 -68
  42. package/src/commands/migration_compare.ts +0 -167
  43. package/src/commands/migration_fresh.ts +0 -148
  44. package/src/commands/migration_generate.ts +0 -84
  45. package/src/commands/migration_rollback.ts +0 -54
  46. package/src/commands/migration_run.ts +0 -45
  47. package/src/commands/package_install.ts +0 -161
  48. package/src/commands/queue_flush.ts +0 -35
  49. package/src/commands/queue_retry.ts +0 -34
  50. package/src/commands/queue_work.ts +0 -101
  51. package/src/commands/scheduler_work.ts +0 -46
  52. package/src/commands/tenant_create.ts +0 -35
  53. package/src/commands/tenant_delete.ts +0 -64
  54. package/src/commands/tenant_list.ts +0 -39
  55. package/src/config/loader.ts +0 -50
  56. package/src/generators/api_generator.ts +0 -1035
  57. package/src/generators/config.ts +0 -113
  58. package/src/generators/doc_generator.ts +0 -996
  59. package/src/generators/index.ts +0 -11
  60. package/src/generators/model_generator.ts +0 -596
  61. package/src/generators/route_generator.ts +0 -187
  62. package/src/generators/test_generator.ts +0 -1667
  63. package/tsconfig.json +0 -5
@@ -1,77 +0,0 @@
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 { bootstrap, shutdown } from '../cli/bootstrap.ts'
6
- import { freshDatabase, requireLocalEnv } from './migration_fresh.ts'
7
- import { toSnakeCase } from '@strav/kernel/helpers/strings'
8
- import BaseModel from '@strav/database/orm/base_model'
9
- import { Seeder } from '@strav/database/database/seeder'
10
-
11
- const SEEDERS_PATH = 'database/seeders'
12
-
13
- export function register(program: Command): void {
14
- program
15
- .command('seed')
16
- .alias('db:seed')
17
- .description('Seed the database with records')
18
- .option('-c, --class <name>', 'Run a specific seeder class')
19
- .option('--fresh', 'Drop all tables and re-migrate before seeding')
20
- .action(async ({ class: className, fresh }: { class?: string; fresh?: boolean }) => {
21
- let db
22
- try {
23
- const { db: database, registry, introspector } = await bootstrap()
24
- db = database
25
-
26
- // Wire BaseModel so factories / seeders can use the ORM
27
- new BaseModel(db)
28
-
29
- // --fresh: reset the database first
30
- if (fresh) {
31
- requireLocalEnv('seed --fresh')
32
-
33
- const applied = await freshDatabase(db, registry, introspector)
34
- console.log(chalk.green(`\nFresh migration complete. Applied ${applied} migration(s).`))
35
- }
36
-
37
- // Resolve the seeder file
38
- const fileName = className
39
- ? toSnakeCase(className.replace(/Seeder$/i, '') || 'database') + '_seeder'
40
- : 'database_seeder'
41
-
42
- const seederPath = join(process.cwd(), SEEDERS_PATH, `${fileName}.ts`)
43
-
44
- if (!existsSync(seederPath)) {
45
- console.error(chalk.red(`Seeder not found: `) + chalk.dim(seederPath))
46
- console.error(
47
- chalk.dim(
48
- ` Run ${chalk.cyan(`strav generate:seeder ${className ?? 'DatabaseSeeder'}`)} to create it.`
49
- )
50
- )
51
- process.exit(1)
52
- }
53
-
54
- console.log(chalk.cyan(`\nSeeding database...`))
55
-
56
- const mod = await import(seederPath)
57
- const SeederClass = mod.default
58
-
59
- if (!SeederClass || !(SeederClass.prototype instanceof Seeder)) {
60
- console.error(
61
- chalk.red(`Error: `) + `Default export of ${fileName}.ts must extend Seeder.`
62
- )
63
- process.exit(1)
64
- }
65
-
66
- const seeder = new SeederClass(db) as Seeder
67
- await seeder.run()
68
-
69
- console.log(chalk.green('Database seeding complete.'))
70
- } catch (err) {
71
- console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
72
- process.exit(1)
73
- } finally {
74
- if (db) await shutdown(db)
75
- }
76
- })
77
- }
@@ -1,101 +0,0 @@
1
- import type { Command } from 'commander'
2
- import chalk from 'chalk'
3
- import Configuration from '@strav/kernel/config/configuration'
4
- import { SQL } from 'bun'
5
- import { env } from '@strav/kernel/helpers/env'
6
-
7
- /**
8
- * Emit (or apply) the SQL needed to set up the two PostgreSQL roles
9
- * required by the multi-tenant RLS workflow:
10
- *
11
- * - app role: NOBYPASSRLS — used by the application; RLS policies apply.
12
- * - bypass role: BYPASSRLS — used by migrations, TenantManager, withoutTenant().
13
- *
14
- * Reads the desired role names/passwords from the loaded Configuration
15
- * (`database.username`, `database.password`, `database.tenant.bypass.username`,
16
- * `database.tenant.bypass.password`).
17
- *
18
- * Must be run by a PostgreSQL superuser (only the superuser can grant the
19
- * BYPASSRLS attribute). Pass --apply to execute against the configured
20
- * database; otherwise the SQL is printed for manual review.
21
- */
22
- export function register(program: Command): void {
23
- program
24
- .command('db:setup-roles')
25
- .description('Print or apply the SQL to create the app + bypass PostgreSQL roles')
26
- .option('--apply', 'Execute the SQL against the database (requires superuser credentials)')
27
- .option('--superuser <name>', 'Superuser to connect as when --apply is set', 'postgres')
28
- .option('--superuser-password <password>', 'Superuser password (or read from $DB_SUPERUSER_PASSWORD)')
29
- .action(async (opts: { apply?: boolean; superuser: string; superuserPassword?: string }) => {
30
- try {
31
- const config = new Configuration('./config')
32
- await config.load()
33
-
34
- const dbName = config.get('database.database') ?? env('DB_DATABASE', 'strav')
35
- const appUser = config.get('database.username') ?? env('DB_USER', 'strav_app')
36
- const appPassword = config.get('database.password') ?? env('DB_PASSWORD', 'changeme')
37
- const bypassUser =
38
- config.get('database.tenant.bypass.username') ?? env('DB_BYPASS_USER', 'strav_admin')
39
- const bypassPassword =
40
- config.get('database.tenant.bypass.password') ?? env('DB_BYPASS_PASSWORD', 'changeme')
41
-
42
- const stmts = [
43
- `CREATE ROLE "${appUser}" LOGIN PASSWORD '${appPassword}' NOBYPASSRLS;`,
44
- `CREATE ROLE "${bypassUser}" LOGIN PASSWORD '${bypassPassword}' BYPASSRLS;`,
45
- `GRANT ALL ON DATABASE "${dbName}" TO "${appUser}", "${bypassUser}";`,
46
- `GRANT ALL ON SCHEMA public TO "${appUser}", "${bypassUser}";`,
47
- // Default privileges for objects the *current connection* will create.
48
- `ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO "${appUser}", "${bypassUser}";`,
49
- `ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO "${appUser}", "${bypassUser}";`,
50
- // ALTER DEFAULT PRIVILEGES is per-role-of-the-creator. Migrations
51
- // run as the bypass role, so without these the app role never
52
- // sees grants on tables/sequences the bypass role creates and
53
- // any non-bypass query (seeders, request-time inserts) fails
54
- // with `permission denied for table …`. Setting them here means
55
- // the demo flow `db:setup-roles → fresh → seed` works without
56
- // any extra GRANT plumbing.
57
- `ALTER DEFAULT PRIVILEGES FOR ROLE "${bypassUser}" IN SCHEMA public GRANT ALL ON TABLES TO "${appUser}";`,
58
- `ALTER DEFAULT PRIVILEGES FOR ROLE "${bypassUser}" IN SCHEMA public GRANT ALL ON SEQUENCES TO "${appUser}";`,
59
- ]
60
-
61
- if (!opts.apply) {
62
- console.log(chalk.cyan('\n-- Run as a PostgreSQL superuser:\n'))
63
- for (const s of stmts) console.log(s)
64
- console.log(
65
- chalk.dim('\n(Use --apply to execute these against the configured database.)')
66
- )
67
- return
68
- }
69
-
70
- const password = opts.superuserPassword ?? env('DB_SUPERUSER_PASSWORD', '')
71
- const sql = new SQL({
72
- hostname: config.get('database.host') ?? env('DB_HOST', '127.0.0.1'),
73
- port: config.get('database.port') ?? env.int('DB_PORT', 5432),
74
- username: opts.superuser,
75
- password,
76
- database: dbName,
77
- max: 1,
78
- })
79
-
80
- console.log(chalk.cyan(`Applying role setup as superuser "${opts.superuser}"...`))
81
- for (const stmt of stmts) {
82
- try {
83
- await sql.unsafe(stmt)
84
- console.log(chalk.dim(` ok: ${stmt}`))
85
- } catch (err) {
86
- const msg = err instanceof Error ? err.message : String(err)
87
- if (/already exists/i.test(msg)) {
88
- console.log(chalk.dim(` skip (exists): ${stmt}`))
89
- } else {
90
- throw err
91
- }
92
- }
93
- }
94
- await sql.close()
95
- console.log(chalk.green('\nRoles set up.'))
96
- } catch (err) {
97
- console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
98
- process.exit(1)
99
- }
100
- })
101
- }
@@ -1,93 +0,0 @@
1
- import { join } from 'node:path'
2
- import type { Command } from 'commander'
3
- import chalk from 'chalk'
4
- import SchemaRegistry from '@strav/database/schema/registry'
5
- import ApiGenerator from '../generators/api_generator.ts'
6
- import RouteGenerator from '../generators/route_generator.ts'
7
- import TestGenerator from '../generators/test_generator.ts'
8
- import DocGenerator from '../generators/doc_generator.ts'
9
- import type { ApiRoutingConfig } from '../generators/route_generator.ts'
10
- import type { GeneratorConfig } from '../generators/config.ts'
11
- import { loadGeneratorConfig, getDatabasePaths, loadTenantIdType } from '../config/loader.ts'
12
-
13
- export function register(program: Command): void {
14
- program
15
- .command('generate:api')
16
- .alias('g:api')
17
- .description(
18
- 'Generate services, controllers, policies, validators, events, and routes from schemas'
19
- )
20
- .option('-f, --force', 'Overwrite existing generated files')
21
- .action(async ({ force }: { force?: boolean }) => {
22
- try {
23
- console.log(chalk.cyan('Generating API layer from schemas...'))
24
-
25
- // Get configured database paths
26
- const dbPaths = await getDatabasePaths()
27
-
28
- const registry = new SchemaRegistry()
29
- await registry.discover(dbPaths.schemas)
30
- registry.validate()
31
-
32
- const schemas = registry.resolve()
33
- const tenantIdType = await loadTenantIdType()
34
- const representation = registry.buildRepresentation(tenantIdType)
35
-
36
- // Load generator config (if available)
37
- const config = await loadGeneratorConfig()
38
-
39
- const apiGen = new ApiGenerator(schemas, representation, config)
40
- const apiResult = await apiGen.writeAll(force)
41
-
42
- // Load API routing config from config/http.ts (if available)
43
- let apiConfig: Partial<ApiRoutingConfig> | undefined
44
- try {
45
- const httpConfig = (await import(join(process.cwd(), 'config/http.ts'))).default
46
- apiConfig = httpConfig.api
47
- } catch {
48
- // No config/http.ts or no api section — use defaults
49
- }
50
-
51
- const routeGen = new RouteGenerator(schemas, config, apiConfig)
52
- const routeResult = await routeGen.writeAll(force)
53
-
54
- const testGen = new TestGenerator(schemas, representation, config, apiConfig)
55
- const testResult = await testGen.writeAll(force)
56
-
57
- const docGen = new DocGenerator(schemas, representation, config, apiConfig)
58
- const docResult = await docGen.writeAll(force)
59
-
60
- const written = [
61
- ...apiResult.written,
62
- ...routeResult.written,
63
- ...testResult.written,
64
- ...docResult.written,
65
- ]
66
- const skipped = [
67
- ...apiResult.skipped,
68
- ...routeResult.skipped,
69
- ...testResult.skipped,
70
- ...docResult.skipped,
71
- ]
72
-
73
- if (written.length === 0 && skipped.length === 0) {
74
- console.log(chalk.yellow('No API files to generate.'))
75
- return
76
- }
77
-
78
- for (const file of written) {
79
- console.log(chalk.green(` CREATE `) + chalk.dim(file.path))
80
- }
81
- for (const file of skipped) {
82
- console.log(chalk.yellow(` SKIP `) + chalk.dim(file.path) + chalk.dim(' (already exists)'))
83
- }
84
-
85
- if (skipped.length > 0) {
86
- console.log(chalk.dim(`\nSkipped ${skipped.length} existing file(s). Use --force to overwrite.`))
87
- }
88
- } catch (err) {
89
- console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
90
- process.exit(1)
91
- }
92
- })
93
- }
@@ -1,47 +0,0 @@
1
- import { join } from 'node:path'
2
- import type { Command } from 'commander'
3
- import chalk from 'chalk'
4
-
5
- export function register(program: Command): void {
6
- program
7
- .command('generate:key')
8
- .alias('g:key')
9
- .description('Generate an APP_KEY and write it to the .env file')
10
- .option('-f, --force', 'Overwrite existing APP_KEY if present')
11
- .action(async ({ force }: { force?: boolean }) => {
12
- try {
13
- const key = crypto.randomUUID()
14
- const envPath = join(process.cwd(), '.env')
15
- const file = Bun.file(envPath)
16
-
17
- if (await file.exists()) {
18
- const contents = await file.text()
19
- const hasKey = /^APP_KEY\s*=/m.test(contents)
20
-
21
- if (hasKey && !force) {
22
- const current = contents.match(/^APP_KEY\s*=\s*(.*)$/m)?.[1] ?? ''
23
- if (current) {
24
- console.log(chalk.yellow('APP_KEY already exists in .env. Use --force to overwrite.'))
25
- return
26
- }
27
- }
28
-
29
- if (hasKey) {
30
- const updated = contents.replace(/^APP_KEY\s*=.*$/m, `APP_KEY=${key}`)
31
- await Bun.write(envPath, updated)
32
- } else {
33
- const separator = contents.endsWith('\n') ? '' : '\n'
34
- await Bun.write(envPath, contents + separator + `APP_KEY=${key}\n`)
35
- }
36
- } else {
37
- await Bun.write(envPath, `APP_KEY=${key}\n`)
38
- }
39
-
40
- console.log(chalk.green('APP_KEY generated successfully.'))
41
- console.log(chalk.dim(` ${key}`))
42
- } catch (err) {
43
- console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
44
- process.exit(1)
45
- }
46
- })
47
- }
@@ -1,49 +0,0 @@
1
- import type { Command } from 'commander'
2
- import chalk from 'chalk'
3
- import SchemaRegistry from '@strav/database/schema/registry'
4
- import ModelGenerator from '../generators/model_generator.ts'
5
- import { loadGeneratorConfig, getDatabasePaths, loadTenantIdType } from '../config/loader.ts'
6
-
7
- export function register(program: Command): void {
8
- program
9
- .command('generate:models')
10
- .alias('g:models')
11
- .description('Generate model classes and enums from schema definitions')
12
- .option('-f, --force', 'Overwrite existing generated files')
13
- .action(async ({ force }: { force?: boolean }) => {
14
- try {
15
- const dbPaths = await getDatabasePaths()
16
- const config = await loadGeneratorConfig()
17
-
18
- console.log(chalk.cyan('Generating models from schemas...'))
19
-
20
- const registry = new SchemaRegistry()
21
- await registry.discover(dbPaths.schemas)
22
- registry.validate()
23
-
24
- const tenantIdType = await loadTenantIdType()
25
- const representation = registry.buildRepresentation(tenantIdType)
26
- const generator = new ModelGenerator(registry.all(), representation, config)
27
- const { written, skipped } = await generator.writeAll(force)
28
-
29
- if (written.length === 0 && skipped.length === 0) {
30
- console.log(chalk.yellow('No models to generate.'))
31
- return
32
- }
33
-
34
- for (const file of written) {
35
- console.log(chalk.green(` CREATE `) + chalk.dim(file.path))
36
- }
37
- for (const file of skipped) {
38
- console.log(chalk.yellow(` SKIP `) + chalk.dim(file.path) + chalk.dim(' (already exists)'))
39
- }
40
-
41
- if (skipped.length > 0) {
42
- console.log(chalk.dim(`\nSkipped ${skipped.length} existing file(s). Use --force to overwrite.`))
43
- }
44
- } catch (err) {
45
- console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
46
- process.exit(1)
47
- }
48
- })
49
- }
@@ -1,68 +0,0 @@
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 '@strav/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 '@strav/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 '@strav/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
- }
@@ -1,167 +0,0 @@
1
- import type { Command } from 'commander'
2
- import chalk from 'chalk'
3
- import { bootstrap, shutdown } from '../cli/bootstrap.ts'
4
- import SchemaDiffer from '@strav/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(database.tenantIdType)
20
- const actual = await introspector.introspect()
21
- const diff = new SchemaDiffer().diff(desired, actual)
22
-
23
- const hasChanges =
24
- diff.extensions.length > 0 ||
25
- diff.enums.length > 0 ||
26
- diff.tables.length > 0 ||
27
- diff.constraints.length > 0 ||
28
- diff.indexes.length > 0
29
-
30
- if (!hasChanges) {
31
- console.log(chalk.green('Schema is in sync with the database.'))
32
- return
33
- }
34
-
35
- // --- Extension changes ---
36
- if (diff.extensions.length > 0) {
37
- console.log(chalk.bold('Extension changes:'))
38
- for (const x of diff.extensions) {
39
- if (x.kind === 'create') {
40
- console.log(chalk.green(` + CREATE EXTENSION ${x.name}`))
41
- } else if (x.kind === 'drop') {
42
- console.log(chalk.red(` - DROP EXTENSION ${x.name}`))
43
- }
44
- }
45
- console.log()
46
- }
47
-
48
- // --- Enum changes ---
49
- if (diff.enums.length > 0) {
50
- console.log(chalk.bold('Enum changes:'))
51
- for (const e of diff.enums) {
52
- if (e.kind === 'create') {
53
- console.log(chalk.green(` + CREATE ${e.name} (${e.values.join(', ')})`))
54
- } else if (e.kind === 'drop') {
55
- console.log(chalk.red(` - DROP ${e.name}`))
56
- } else if (e.kind === 'modify') {
57
- console.log(chalk.yellow(` ~ MODIFY ${e.name} (add: ${e.addedValues.join(', ')})`))
58
- }
59
- }
60
- console.log()
61
- }
62
-
63
- // --- Table changes ---
64
- if (diff.tables.length > 0) {
65
- console.log(chalk.bold('Table changes:'))
66
- for (const t of diff.tables) {
67
- if (t.kind === 'create') {
68
- console.log(
69
- chalk.green(` + CREATE ${t.table.name}`) +
70
- chalk.dim(` (${t.table.columns.length} columns)`)
71
- )
72
- } else if (t.kind === 'drop') {
73
- console.log(chalk.red(` - DROP ${t.table.name}`))
74
- } else if (t.kind === 'modify') {
75
- console.log(chalk.yellow(` ~ MODIFY ${t.tableName}`))
76
- for (const c of t.columns) {
77
- if (c.kind === 'add') {
78
- console.log(
79
- chalk.green(
80
- ` + ADD COLUMN ${c.column.name} (${typeof c.column.pgType === 'string' ? c.column.pgType : 'custom'})`
81
- )
82
- )
83
- } else if (c.kind === 'drop') {
84
- console.log(chalk.red(` - DROP COLUMN ${c.column.name}`))
85
- } else if (c.kind === 'alter') {
86
- const changes: string[] = []
87
- if (c.typeChange) changes.push(`type: ${c.typeChange.from} -> ${c.typeChange.to}`)
88
- if (c.nullableChange)
89
- changes.push(c.nullableChange.to ? 'set NOT NULL' : 'drop NOT NULL')
90
- if (c.defaultChange) changes.push('default changed')
91
- console.log(
92
- chalk.yellow(` ~ ALTER COLUMN ${c.columnName} (${changes.join(', ')})`)
93
- )
94
- }
95
- }
96
- }
97
- }
98
- console.log()
99
- }
100
-
101
- // --- Constraint changes ---
102
- if (diff.constraints.length > 0) {
103
- console.log(chalk.bold('Constraint changes:'))
104
- for (const c of diff.constraints) {
105
- if (c.kind === 'add_fk') {
106
- console.log(
107
- chalk.green(
108
- ` + ADD FK ${c.tableName}(${c.constraint.columns.join(',')}) -> ${c.constraint.referencedTable}`
109
- )
110
- )
111
- } else if (c.kind === 'drop_fk') {
112
- console.log(
113
- chalk.red(
114
- ` - DROP FK ${c.tableName}(${c.constraint.columns.join(',')}) -> ${c.constraint.referencedTable}`
115
- )
116
- )
117
- } else if (c.kind === 'add_unique') {
118
- console.log(
119
- chalk.green(` + ADD UQ ${c.tableName}(${c.constraint.columns.join(',')})`)
120
- )
121
- } else if (c.kind === 'drop_unique') {
122
- console.log(
123
- chalk.red(` - DROP UQ ${c.tableName}(${c.constraint.columns.join(',')})`)
124
- )
125
- }
126
- }
127
- console.log()
128
- }
129
-
130
- // --- Index changes ---
131
- if (diff.indexes.length > 0) {
132
- console.log(chalk.bold('Index changes:'))
133
- for (const i of diff.indexes) {
134
- if (i.kind === 'add') {
135
- const unique = i.index.unique ? 'UNIQUE ' : ''
136
- console.log(
137
- chalk.green(
138
- ` + CREATE ${unique}INDEX ${i.tableName}(${i.index.columns.join(',')})`
139
- )
140
- )
141
- } else if (i.kind === 'drop') {
142
- console.log(
143
- chalk.red(` - DROP INDEX ${i.tableName}(${i.index.columns.join(',')})`)
144
- )
145
- }
146
- }
147
- console.log()
148
- }
149
-
150
- // --- Summary ---
151
- const creates = diff.tables.filter(t => t.kind === 'create').length
152
- const drops = diff.tables.filter(t => t.kind === 'drop').length
153
- const modifies = diff.tables.filter(t => t.kind === 'modify').length
154
- console.log(
155
- chalk.bold('Summary: ') +
156
- `${creates} table(s) to create, ${drops} to drop, ${modifies} to modify, ` +
157
- `${diff.enums.length} enum change(s), ${diff.constraints.length} constraint change(s), ${diff.indexes.length} index change(s), ${diff.extensions.length} extension change(s)`
158
- )
159
- console.log(chalk.dim('\nRun "bun strav generate:migration" to create migration files.'))
160
- } catch (err) {
161
- console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
162
- process.exit(1)
163
- } finally {
164
- if (db) await shutdown(db)
165
- }
166
- })
167
- }