turbine-orm 0.4.0 → 0.7.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.
Files changed (57) hide show
  1. package/README.md +243 -26
  2. package/dist/cjs/cli/config.js +151 -0
  3. package/dist/cjs/cli/index.js +1176 -0
  4. package/dist/cjs/cli/migrate.js +446 -0
  5. package/dist/cjs/cli/ui.js +233 -0
  6. package/dist/cjs/client.js +512 -0
  7. package/dist/cjs/errors.js +293 -0
  8. package/dist/cjs/generate.js +321 -0
  9. package/dist/cjs/index.js +94 -0
  10. package/dist/cjs/introspect.js +287 -0
  11. package/dist/cjs/package.json +1 -0
  12. package/dist/cjs/pipeline.js +78 -0
  13. package/dist/cjs/query.js +1891 -0
  14. package/dist/cjs/schema-builder.js +238 -0
  15. package/dist/cjs/schema-sql.js +509 -0
  16. package/dist/cjs/schema.js +140 -0
  17. package/dist/cjs/serverless.js +110 -0
  18. package/dist/cli/config.js +6 -16
  19. package/dist/cli/index.js +256 -49
  20. package/dist/cli/migrate.d.ts +35 -6
  21. package/dist/cli/migrate.js +124 -76
  22. package/dist/cli/ui.js +5 -9
  23. package/dist/client.d.ts +87 -3
  24. package/dist/client.js +122 -46
  25. package/dist/errors.d.ts +138 -0
  26. package/dist/errors.js +278 -0
  27. package/dist/generate.js +37 -11
  28. package/dist/index.d.ts +10 -8
  29. package/dist/index.js +15 -11
  30. package/dist/introspect.js +3 -5
  31. package/dist/pipeline.js +8 -1
  32. package/dist/query.d.ts +310 -45
  33. package/dist/query.js +565 -237
  34. package/dist/schema-builder.js +91 -23
  35. package/dist/schema-sql.d.ts +6 -2
  36. package/dist/schema-sql.js +180 -26
  37. package/dist/schema.js +4 -1
  38. package/dist/serverless.d.ts +91 -139
  39. package/dist/serverless.js +86 -173
  40. package/package.json +44 -21
  41. package/dist/cli/config.d.ts.map +0 -1
  42. package/dist/cli/index.d.ts.map +0 -1
  43. package/dist/cli/migrate.d.ts.map +0 -1
  44. package/dist/cli/ui.d.ts.map +0 -1
  45. package/dist/client.d.ts.map +0 -1
  46. package/dist/generate.d.ts.map +0 -1
  47. package/dist/index.d.ts.map +0 -1
  48. package/dist/introspect.d.ts.map +0 -1
  49. package/dist/pipeline.d.ts.map +0 -1
  50. package/dist/query.d.ts.map +0 -1
  51. package/dist/schema-builder.d.ts.map +0 -1
  52. package/dist/schema-sql.d.ts.map +0 -1
  53. package/dist/schema.d.ts.map +0 -1
  54. package/dist/serverless.d.ts.map +0 -1
  55. package/dist/types.d.ts +0 -93
  56. package/dist/types.d.ts.map +0 -1
  57. package/dist/types.js +0 -126
package/dist/cli/index.js CHANGED
@@ -19,15 +19,15 @@
19
19
  * npx turbine init --url postgres://...
20
20
  * npx turbine migrate create add_users_table
21
21
  */
22
- import { existsSync, mkdirSync, writeFileSync, readFileSync, appendFileSync } from 'node:fs';
23
- import { resolve, relative } from 'node:path';
24
- import { introspect } from '../introspect.js';
22
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
23
+ import { relative, resolve } from 'node:path';
24
+ import { pathToFileURL } from 'node:url';
25
25
  import { generate } from '../generate.js';
26
+ import { introspect } from '../introspect.js';
26
27
  import { schemaDiff, schemaPush } from '../schema-sql.js';
27
- import { loadConfig, resolveConfig, findConfigFile, configTemplate } from './config.js';
28
- import { createMigration, migrateUp, migrateDown, migrateStatus, listMigrationFiles, } from './migrate.js';
29
- import { bold, dim, red, green, yellow, blue, cyan, gray, magenta, greenBright, cyanBright, yellowBright, symbols, box, table as formatTable, Spinner, header, success, error, warn, info, label, newline, divider, banner, elapsed, redactUrl, } from './ui.js';
30
- import { pathToFileURL } from 'node:url';
28
+ import { configTemplate, findConfigFile, loadConfig, resolveConfig } from './config.js';
29
+ import { createMigration, listMigrationFiles, migrateDown, migrateStatus, migrateUp } from './migrate.js';
30
+ import { banner, blue, bold, box, cyan, dim, divider, elapsed, error, table as formatTable, gray, green, header, info, label, magenta, newline, red, redactUrl, Spinner, success, symbols, warn, yellow, } from './ui.js';
31
31
  function parseArgs() {
32
32
  const args = process.argv.slice(2);
33
33
  const result = {
@@ -75,6 +75,9 @@ function parseArgs() {
75
75
  case '--dry-run':
76
76
  result.dryRun = true;
77
77
  break;
78
+ case '--auto':
79
+ result.auto = true;
80
+ break;
78
81
  case '--force':
79
82
  case '-f':
80
83
  result.force = true;
@@ -83,6 +86,10 @@ function parseArgs() {
83
86
  case '-v':
84
87
  result.verbose = true;
85
88
  break;
89
+ case '--help':
90
+ case '-h':
91
+ result.help = true;
92
+ break;
86
93
  default:
87
94
  if (!arg.startsWith('-')) {
88
95
  result.positional.push(arg);
@@ -140,7 +147,7 @@ async function cmdInit(args, config) {
140
147
  banner();
141
148
  header('Initializing Turbine project');
142
149
  // Detect environment
143
- const envUrl = process.env['DATABASE_URL'];
150
+ const envUrl = process.env.DATABASE_URL;
144
151
  const hasEnvFile = existsSync('.env');
145
152
  const hasEnvLocal = existsSync('.env.local');
146
153
  if (envUrl) {
@@ -179,7 +186,7 @@ async function cmdInit(args, config) {
179
186
  mkdirSync(migrDir, { recursive: true });
180
187
  // Create .gitkeep
181
188
  writeFileSync(`${migrDir}/.gitkeep`, '', 'utf-8');
182
- success(`Created ${cyan(migrDir + '/')}`);
189
+ success(`Created ${cyan(`${migrDir}/`)}`);
183
190
  }
184
191
  else {
185
192
  info(`Migrations dir already exists: ${dim(migrDir)}`);
@@ -187,7 +194,7 @@ async function cmdInit(args, config) {
187
194
  // Create output directory
188
195
  if (!existsSync(config.out)) {
189
196
  mkdirSync(config.out, { recursive: true });
190
- success(`Created ${cyan(config.out + '/')}`);
197
+ success(`Created ${cyan(`${config.out}/`)}`);
191
198
  }
192
199
  // Create seed file template
193
200
  const seedDir = config.seedFile.substring(0, config.seedFile.lastIndexOf('/'));
@@ -231,7 +238,7 @@ async function cmdInit(args, config) {
231
238
  * Define your database schema in TypeScript.
232
239
  * Use \`npx turbine push\` to sync it to your database.
233
240
  *
234
- * @see https://github.com/zvndev/turbine-orm
241
+ * @see https://turbineorm.dev
235
242
  */
236
243
 
237
244
  import { defineSchema } from 'turbine-orm';
@@ -248,13 +255,20 @@ export default defineSchema({
248
255
  `, 'utf-8');
249
256
  success(`Created ${cyan(config.schemaFile)}`);
250
257
  }
251
- // Add .gitignore entry for generated output
258
+ // Add .gitignore entries for generated output and config (may contain connection strings)
252
259
  const gitignorePath = '.gitignore';
253
260
  if (existsSync(gitignorePath)) {
254
261
  const gitignoreContent = readFileSync(gitignorePath, 'utf-8');
262
+ const additions = [];
255
263
  if (!gitignoreContent.includes('generated/turbine')) {
256
- appendFileSync(gitignorePath, '\n# Turbine generated client\ngenerated/turbine/\n');
257
- success(`Added ${cyan('generated/turbine/')} to ${cyan('.gitignore')}`);
264
+ additions.push('generated/turbine/');
265
+ }
266
+ if (!gitignoreContent.includes('turbine.config.ts')) {
267
+ additions.push('turbine.config.ts');
268
+ }
269
+ if (additions.length > 0) {
270
+ appendFileSync(gitignorePath, `\n# Turbine generated client & config\n${additions.join('\n')}\n`);
271
+ success(`Added ${cyan(additions.join(', '))} to ${cyan('.gitignore')}`);
258
272
  }
259
273
  }
260
274
  // If we have a URL, run initial generate
@@ -275,15 +289,15 @@ export default defineSchema({
275
289
  spinner.succeed(`Found ${bold(String(tableCount))} tables`);
276
290
  const genSpinner = new Spinner('Generating TypeScript client').start();
277
291
  const result = generate({ schema, outDir: config.out, connectionString: url });
278
- genSpinner.succeed(`Generated ${bold(String(result.files.length))} files to ${cyan(config.out + '/')}`);
292
+ genSpinner.succeed(`Generated ${bold(String(result.files.length))} files to ${cyan(`${config.out}/`)}`);
279
293
  }
280
294
  catch (err) {
281
295
  spinner.fail('Could not connect to database');
282
296
  if (err instanceof Error) {
283
- console.log(` ${dim(err.message)}`);
297
+ console.log(` ${dim(redactUrl(err.message))}`);
284
298
  }
285
299
  newline();
286
- info('You can run generation later with: ' + cyan('npx turbine generate'));
300
+ info(`You can run generation later with: ${cyan('npx turbine generate')}`);
287
301
  }
288
302
  }
289
303
  // Next steps
@@ -359,7 +373,7 @@ async function cmdGenerate(args, config) {
359
373
  genSpinner.succeed(`Generated ${bold(String(result.files.length))} files in ${elapsed(startTime)}`);
360
374
  // List files
361
375
  for (const file of result.files) {
362
- console.log(` ${dim(symbols.teeEnd)} ${cyan(result.outDir + '/' + file)}`);
376
+ console.log(` ${dim(symbols.teeEnd)} ${cyan(`${result.outDir}/${file}`)}`);
363
377
  }
364
378
  // Usage hint
365
379
  newline();
@@ -407,9 +421,11 @@ async function cmdPush(args, config) {
407
421
  for (const a of diff.alter) {
408
422
  console.log(` ${yellow(symbols.arrowRight)} ${a.table}`);
409
423
  for (const col of a.columns) {
410
- const actionLabel = col.action === 'add' ? green('+ add') :
411
- col.action === 'drop' ? red('- drop') :
412
- yellow('~ ' + col.action.replace('_', ' '));
424
+ const actionLabel = col.action === 'add'
425
+ ? green('+ add')
426
+ : col.action === 'drop'
427
+ ? red('- drop')
428
+ : yellow(`~ ${col.action.replace('_', ' ')}`);
413
429
  console.log(` ${actionLabel} ${col.column}`);
414
430
  }
415
431
  }
@@ -463,17 +479,20 @@ async function cmdMigrate(args, config) {
463
479
  console.log(` ${bold('turbine migrate')} ${dim('— SQL-first migration system')}`);
464
480
  newline();
465
481
  console.log(` ${bold('Commands:')}`);
466
- console.log(` ${cyan('create <name>')} Create a new migration file`);
467
- console.log(` ${cyan('up')} Apply pending migrations`);
468
- console.log(` ${cyan('down')} Rollback last migration`);
469
- console.log(` ${cyan('status')} Show migration status`);
482
+ console.log(` ${cyan('create <name>')} Create a new migration file`);
483
+ console.log(` ${cyan('create <name> --auto')} Auto-generate from schema diff`);
484
+ console.log(` ${cyan('up')} Apply pending migrations`);
485
+ console.log(` ${cyan('down')} Rollback last migration`);
486
+ console.log(` ${cyan('status')} Show migration status`);
470
487
  newline();
471
488
  console.log(` ${bold('Options:')}`);
489
+ console.log(` ${cyan('--auto')} Auto-generate UP/DOWN SQL from schema diff`);
472
490
  console.log(` ${cyan('--step, -n')} Number of migrations to apply/rollback`);
473
491
  console.log(` ${cyan('--dry-run')} Show SQL without executing`);
474
492
  newline();
475
493
  console.log(` ${bold('Examples:')}`);
476
494
  console.log(` ${dim('npx turbine migrate create add_users_table')}`);
495
+ console.log(` ${dim('npx turbine migrate create add_email_index --auto')}`);
477
496
  console.log(` ${dim('npx turbine migrate up')}`);
478
497
  console.log(` ${dim('npx turbine migrate down --step 2')}`);
479
498
  newline();
@@ -507,9 +526,61 @@ async function cmdMigrateCreate(args, config) {
507
526
  newline();
508
527
  console.log(` ${dim('Usage:')} ${cyan('npx turbine migrate create <name>')}`);
509
528
  console.log(` ${dim('Example:')} ${cyan('npx turbine migrate create add_users_table')}`);
529
+ console.log(` ${dim('Auto:')} ${cyan('npx turbine migrate create my_change --auto')}`);
510
530
  newline();
511
531
  process.exit(1);
512
532
  }
533
+ if (args.auto) {
534
+ // Auto-generate migration from schema diff
535
+ const url = requireUrl(config);
536
+ label('Database', redactUrl(url));
537
+ label('Schema file', config.schemaFile);
538
+ newline();
539
+ const schemaDef = await loadSchemaFile(config.schemaFile);
540
+ const diffSpinner = new Spinner('Computing schema diff').start();
541
+ const diff = await schemaDiff(schemaDef, url);
542
+ if (diff.statements.length === 0) {
543
+ diffSpinner.succeed('Database is already in sync — nothing to migrate');
544
+ newline();
545
+ return;
546
+ }
547
+ diffSpinner.succeed(`Found ${bold(String(diff.statements.length))} change(s)`);
548
+ newline();
549
+ const upSQL = diff.statements.join('\n');
550
+ const downSQL = diff.reverseStatements.join('\n');
551
+ const file = createMigration(config.migrationsDir, name, { up: upSQL, down: downSQL });
552
+ const relPath = relative(process.cwd(), file.path);
553
+ success(`Created auto-migration: ${bold(file.filename)}`);
554
+ newline();
555
+ console.log(` ${dim('File:')} ${cyan(relPath)}`);
556
+ newline();
557
+ // Show summary of changes
558
+ if (diff.create.length > 0) {
559
+ console.log(` ${green('+ Create')} ${diff.create.length} table(s): ${diff.create.map((t) => t.name).join(', ')}`);
560
+ }
561
+ if (diff.alter.length > 0) {
562
+ console.log(` ${yellow('~ Alter')} ${diff.alter.length} table(s):`);
563
+ for (const a of diff.alter) {
564
+ for (const col of a.columns) {
565
+ const actionLabel = col.action === 'add'
566
+ ? green('+ add')
567
+ : col.action === 'drop'
568
+ ? red('- drop')
569
+ : col.action === 'add_unique'
570
+ ? green('+ unique')
571
+ : col.action === 'drop_unique'
572
+ ? red('- unique')
573
+ : yellow(`~ ${col.action.replace(/_/g, ' ')}`);
574
+ console.log(` ${actionLabel} ${a.table}.${col.column}`);
575
+ }
576
+ }
577
+ }
578
+ newline();
579
+ console.log(` ${dim('Review the migration, then run:')}`);
580
+ console.log(` ${cyan('npx turbine migrate up')}`);
581
+ newline();
582
+ return;
583
+ }
513
584
  const file = createMigration(config.migrationsDir, name);
514
585
  const relPath = relative(process.cwd(), file.path);
515
586
  success(`Created migration: ${bold(file.filename)}`);
@@ -591,7 +662,7 @@ async function cmdMigrateDown(args, config) {
591
662
  }
592
663
  newline();
593
664
  }
594
- async function cmdMigrateStatus(args, config) {
665
+ async function cmdMigrateStatus(_args, config) {
595
666
  banner();
596
667
  const url = requireUrl(config);
597
668
  label('Database', redactUrl(url));
@@ -621,19 +692,22 @@ async function cmdMigrateStatus(args, config) {
621
692
  const rows = statuses.map((s) => {
622
693
  let status;
623
694
  if (s.applied && s.checksumValid === false) {
624
- status = red(symbols.warning + ' Drifted');
695
+ status = red(`${symbols.warning} Drifted`);
625
696
  }
626
697
  else if (s.applied) {
627
- status = green(symbols.check + ' Applied');
698
+ status = green(`${symbols.check} Applied`);
628
699
  }
629
700
  else {
630
- status = yellow(symbols.dot + ' Pending');
701
+ status = yellow(`${symbols.dot} Pending`);
631
702
  }
632
703
  return [
633
704
  status,
634
705
  s.file.filename,
635
706
  s.appliedAt
636
- ? dim(s.appliedAt.toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC'))
707
+ ? dim(s.appliedAt
708
+ .toISOString()
709
+ .replace('T', ' ')
710
+ .replace(/\.\d+Z$/, ' UTC'))
637
711
  : dim('—'),
638
712
  ];
639
713
  });
@@ -647,7 +721,7 @@ async function cmdMigrateStatus(args, config) {
647
721
  // ---------------------------------------------------------------------------
648
722
  // Command: seed
649
723
  // ---------------------------------------------------------------------------
650
- async function cmdSeed(args, config) {
724
+ async function cmdSeed(_args, config) {
651
725
  banner();
652
726
  const seedFile = resolve(config.seedFile);
653
727
  label('Seed file', config.seedFile);
@@ -663,20 +737,20 @@ async function cmdSeed(args, config) {
663
737
  const spinner = new Spinner('Running seed file').start();
664
738
  try {
665
739
  // Use child_process to run the seed file via tsx or node
666
- const { execSync } = await import('node:child_process');
740
+ const { execFileSync } = await import('node:child_process');
667
741
  // Try tsx first (most compatible with .ts files), fall back to node --experimental-strip-types
668
742
  const runners = [
669
- { cmd: 'npx tsx', name: 'tsx' },
670
- { cmd: 'node --experimental-strip-types', name: 'node' },
743
+ { cmd: 'npx', args: ['tsx', seedFile], name: 'tsx' },
744
+ { cmd: 'node', args: ['--experimental-strip-types', seedFile], name: 'node' },
671
745
  ];
672
746
  let ran = false;
673
747
  for (const runner of runners) {
674
748
  try {
675
- execSync(`${runner.cmd} ${seedFile}`, {
749
+ execFileSync(runner.cmd, runner.args, {
676
750
  stdio: 'inherit',
677
751
  env: {
678
752
  ...process.env,
679
- DATABASE_URL: config.url || process.env['DATABASE_URL'],
753
+ DATABASE_URL: config.url || process.env.DATABASE_URL,
680
754
  },
681
755
  });
682
756
  ran = true;
@@ -698,7 +772,7 @@ async function cmdSeed(args, config) {
698
772
  catch (err) {
699
773
  spinner.fail('Seed failed');
700
774
  if (err instanceof Error) {
701
- console.log(` ${dim(err.message)}`);
775
+ console.log(` ${dim(redactUrl(err.message))}`);
702
776
  }
703
777
  newline();
704
778
  process.exit(1);
@@ -708,7 +782,7 @@ async function cmdSeed(args, config) {
708
782
  // ---------------------------------------------------------------------------
709
783
  // Command: status
710
784
  // ---------------------------------------------------------------------------
711
- async function cmdStatus(args, config) {
785
+ async function cmdStatus(_args, config) {
712
786
  banner();
713
787
  const url = requireUrl(config);
714
788
  label('Database', redactUrl(url));
@@ -726,7 +800,7 @@ async function cmdStatus(args, config) {
726
800
  newline();
727
801
  for (const tbl of Object.values(schema.tables)) {
728
802
  const relCount = Object.keys(tbl.relations).length;
729
- const pk = tbl.primaryKey.join(', ') || dim('(none)');
803
+ const _pk = tbl.primaryKey.join(', ') || dim('(none)');
730
804
  console.log(` ${bold(cyan(tbl.name))}`);
731
805
  for (let i = 0; i < tbl.columns.length; i++) {
732
806
  const col = tbl.columns[i];
@@ -735,7 +809,7 @@ async function cmdStatus(args, config) {
735
809
  const nullable = col.nullable ? dim('?') : '';
736
810
  const def = col.hasDefault ? dim(' (default)') : '';
737
811
  const pkLabel = tbl.primaryKey.includes(col.name) ? ` ${magenta('PK')}` : '';
738
- console.log(` ${dim(prefix)} ${col.field}${nullable}: ${green(col.tsType)}${pkLabel}${def} ${gray(symbols.arrow + ' ' + col.pgType)}`);
812
+ console.log(` ${dim(prefix)} ${col.field}${nullable}: ${green(col.tsType)}${pkLabel}${def} ${gray(`${symbols.arrow} ${col.pgType}`)}`);
739
813
  }
740
814
  const rels = Object.entries(tbl.relations);
741
815
  if (rels.length > 0) {
@@ -768,11 +842,138 @@ async function cmdStudio(_args, _config) {
768
842
  'A local web UI for browsing your database,',
769
843
  'exploring relations, and managing data.',
770
844
  '',
771
- `Follow ${cyan('@zvndev')} for updates.`,
845
+ `Follow ${cyan('@turbineorm')} for updates.`,
772
846
  ].join('\n'), { title: bold(cyan('Studio')), padding: 2 }));
773
847
  newline();
774
848
  }
775
849
  // ---------------------------------------------------------------------------
850
+ // Subcommand help
851
+ // ---------------------------------------------------------------------------
852
+ function showSubcommandHelp(command) {
853
+ const helpMap = {
854
+ init: showInitHelp,
855
+ generate: showGenerateHelp,
856
+ pull: showGenerateHelp,
857
+ push: showPushHelp,
858
+ migrate: showMigrateHelp,
859
+ migration: showMigrateHelp,
860
+ seed: showSeedHelp,
861
+ status: showStatusHelp,
862
+ };
863
+ const fn = helpMap[command];
864
+ if (fn) {
865
+ fn();
866
+ return true;
867
+ }
868
+ return false;
869
+ }
870
+ function showInitHelp() {
871
+ banner();
872
+ console.log(` ${bold('turbine init')} — Initialize a Turbine project`);
873
+ newline();
874
+ console.log(` ${bold('Usage:')}`);
875
+ console.log(` npx turbine init ${dim('[options]')}`);
876
+ newline();
877
+ console.log(` Creates ${cyan('turbine.config.ts')}, migrations directory, seed file template,`);
878
+ console.log(` and schema file template.`);
879
+ newline();
880
+ console.log(` ${bold('Options:')}`);
881
+ console.log(` ${cyan('--url, -u')} ${dim('<url>')} Postgres connection string to embed in config`);
882
+ console.log(` ${cyan('--force, -f')} Overwrite existing config file`);
883
+ newline();
884
+ }
885
+ function showGenerateHelp() {
886
+ banner();
887
+ console.log(` ${bold('turbine generate')} — Introspect database and generate TypeScript types`);
888
+ newline();
889
+ console.log(` ${bold('Usage:')}`);
890
+ console.log(` npx turbine generate ${dim('[options]')}`);
891
+ newline();
892
+ console.log(` Connects to your database, reads the schema, and generates:`);
893
+ console.log(` ${dim('•')} ${cyan('types.ts')} — Entity interfaces, Create/Update input types`);
894
+ console.log(` ${dim('•')} ${cyan('metadata.ts')} — Runtime schema metadata`);
895
+ console.log(` ${dim('•')} ${cyan('index.ts')} — Configured client with typed table accessors`);
896
+ newline();
897
+ console.log(` ${bold('Options:')}`);
898
+ console.log(` ${cyan('--url, -u')} ${dim('<url>')} Postgres connection string`);
899
+ console.log(` ${cyan('--out, -o')} ${dim('<dir>')} Output directory ${dim('(default: ./generated/turbine)')}`);
900
+ console.log(` ${cyan('--schema, -s')} ${dim('<name>')} Postgres schema ${dim('(default: public)')}`);
901
+ console.log(` ${cyan('--include')} ${dim('<tables>')} Comma-separated tables to include`);
902
+ console.log(` ${cyan('--exclude')} ${dim('<tables>')} Comma-separated tables to exclude`);
903
+ newline();
904
+ }
905
+ function showPushHelp() {
906
+ banner();
907
+ console.log(` ${bold('turbine push')} — Apply schema-builder definitions to database`);
908
+ newline();
909
+ console.log(` ${bold('Usage:')}`);
910
+ console.log(` npx turbine push ${dim('[options]')}`);
911
+ newline();
912
+ console.log(` Reads your ${cyan('turbine/schema.ts')} file, diffs against the live database,`);
913
+ console.log(` and applies CREATE/ALTER statements.`);
914
+ newline();
915
+ console.log(` ${bold('Options:')}`);
916
+ console.log(` ${cyan('--url, -u')} ${dim('<url>')} Postgres connection string`);
917
+ console.log(` ${cyan('--dry-run')} Show SQL without executing`);
918
+ console.log(` ${cyan('--verbose, -v')} Show detailed output`);
919
+ newline();
920
+ }
921
+ function showMigrateHelp() {
922
+ banner();
923
+ console.log(` ${bold('turbine migrate')} — SQL migration management`);
924
+ newline();
925
+ console.log(` ${bold('Usage:')}`);
926
+ console.log(` npx turbine migrate ${cyan('<subcommand>')} ${dim('[options]')}`);
927
+ newline();
928
+ console.log(` ${bold('Subcommands:')}`);
929
+ console.log(` ${cyan('create')} ${dim('<name>')} Create a new migration file`);
930
+ console.log(` ${cyan('up')} Apply pending migrations`);
931
+ console.log(` ${cyan('down')} Rollback last migration`);
932
+ console.log(` ${cyan('status')} Show applied/pending migrations`);
933
+ newline();
934
+ console.log(` ${bold('Options:')}`);
935
+ console.log(` ${cyan('--url, -u')} ${dim('<url>')} Postgres connection string`);
936
+ console.log(` ${cyan('--step, -n')} ${dim('<N>')} Number of migrations to apply/rollback`);
937
+ console.log(` ${cyan('--dry-run')} Show SQL without executing`);
938
+ console.log(` ${cyan('--verbose, -v')} Show detailed output`);
939
+ newline();
940
+ console.log(` ${bold('Examples:')}`);
941
+ console.log(` ${dim('$')} npx turbine migrate create add_users_table`);
942
+ console.log(` ${dim('$')} npx turbine migrate up`);
943
+ console.log(` ${dim('$')} npx turbine migrate down --step 2`);
944
+ console.log(` ${dim('$')} npx turbine migrate status`);
945
+ newline();
946
+ }
947
+ function showSeedHelp() {
948
+ banner();
949
+ console.log(` ${bold('turbine seed')} — Run seed file`);
950
+ newline();
951
+ console.log(` ${bold('Usage:')}`);
952
+ console.log(` npx turbine seed ${dim('[options]')}`);
953
+ newline();
954
+ console.log(` Runs the seed file specified in ${cyan('turbine.config.ts')}`);
955
+ console.log(` ${dim('(default: ./turbine/seed.ts)')}`);
956
+ newline();
957
+ console.log(` ${bold('Options:')}`);
958
+ console.log(` ${cyan('--url, -u')} ${dim('<url>')} Postgres connection string`);
959
+ newline();
960
+ }
961
+ function showStatusHelp() {
962
+ banner();
963
+ console.log(` ${bold('turbine status')} — Show database schema summary`);
964
+ newline();
965
+ console.log(` ${bold('Usage:')}`);
966
+ console.log(` npx turbine status ${dim('[options]')}`);
967
+ newline();
968
+ console.log(` Introspects your database and displays tables, columns,`);
969
+ console.log(` types, relations, and indexes.`);
970
+ newline();
971
+ console.log(` ${bold('Options:')}`);
972
+ console.log(` ${cyan('--url, -u')} ${dim('<url>')} Postgres connection string`);
973
+ console.log(` ${cyan('--schema, -s')} ${dim('<name>')} Postgres schema ${dim('(default: public)')}`);
974
+ newline();
975
+ }
976
+ // ---------------------------------------------------------------------------
776
977
  // Help
777
978
  // ---------------------------------------------------------------------------
778
979
  function showHelp() {
@@ -819,8 +1020,7 @@ function showHelp() {
819
1020
  // Version
820
1021
  // ---------------------------------------------------------------------------
821
1022
  function showVersion() {
822
- // Read version from package.json at build time
823
- console.log(`turbine-orm v0.3.0`);
1023
+ console.log(`turbine-orm v0.5.0`);
824
1024
  }
825
1025
  // ---------------------------------------------------------------------------
826
1026
  // Main
@@ -832,6 +1032,13 @@ async function main() {
832
1032
  showHelp();
833
1033
  return;
834
1034
  }
1035
+ // Subcommand help: e.g. `turbine migrate --help`
1036
+ if (args.help) {
1037
+ if (showSubcommandHelp(args.command))
1038
+ return;
1039
+ showHelp();
1040
+ return;
1041
+ }
835
1042
  if (args.command === 'version' || args.command === '--version' || args.command === '-V') {
836
1043
  showVersion();
837
1044
  return;
@@ -897,7 +1104,7 @@ async function main() {
897
1104
  if (err.message.includes('ECONNREFUSED') || err.message.includes('connection')) {
898
1105
  newline();
899
1106
  error(`Could not connect to database`);
900
- console.log(` ${dim(err.message)}`);
1107
+ console.log(` ${dim(redactUrl(err.message))}`);
901
1108
  newline();
902
1109
  console.log(` ${dim('Check that:')}`);
903
1110
  console.log(` ${dim('1.')} Your database is running`);
@@ -907,25 +1114,25 @@ async function main() {
907
1114
  else if (err.message.includes('authentication')) {
908
1115
  newline();
909
1116
  error(`Authentication failed`);
910
- console.log(` ${dim(err.message)}`);
1117
+ console.log(` ${dim(redactUrl(err.message))}`);
911
1118
  }
912
1119
  else if (err.message.includes('does not exist')) {
913
1120
  newline();
914
1121
  error(`Database or schema not found`);
915
- console.log(` ${dim(err.message)}`);
1122
+ console.log(` ${dim(redactUrl(err.message))}`);
916
1123
  }
917
1124
  else {
918
1125
  newline();
919
- error(err.message);
1126
+ error(redactUrl(err.message));
920
1127
  if (args.verbose && err.stack) {
921
1128
  newline();
922
- console.log(dim(err.stack));
1129
+ console.log(dim(redactUrl(err.stack)));
923
1130
  }
924
1131
  }
925
1132
  }
926
1133
  else {
927
1134
  newline();
928
- error(`Unexpected error: ${String(err)}`);
1135
+ error(`Unexpected error: ${redactUrl(String(err))}`);
929
1136
  }
930
1137
  newline();
931
1138
  process.exit(1);
@@ -12,16 +12,14 @@
12
12
  * DROP TABLE users;
13
13
  */
14
14
  export interface MigrationFile {
15
- /** Full filename (e.g. "20260325_001_create_users.sql") */
15
+ /** Full filename (e.g. "20260325120000_create_users.sql") */
16
16
  filename: string;
17
17
  /** Absolute path to the file */
18
18
  path: string;
19
- /** Extracted name portion (e.g. "create_users") */
19
+ /** Extracted name portion (e.g. "20260325120000_create_users") */
20
20
  name: string;
21
- /** Timestamp prefix (e.g. "20260325") */
21
+ /** Timestamp prefix (e.g. "20260325120000") — YYYYMMDDHHMMSS */
22
22
  timestamp: string;
23
- /** Sequence number (e.g. "001") */
24
- sequence: string;
25
23
  }
26
24
  export interface AppliedMigration {
27
25
  id: number;
@@ -36,10 +34,36 @@ export interface MigrationStatus {
36
34
  /** True if the file checksum matches the stored checksum (only set for applied migrations) */
37
35
  checksumValid?: boolean;
38
36
  }
37
+ /**
38
+ * Parse a migration filename into its components.
39
+ * Expected format: YYYYMMDDHHMMSS_description.sql
40
+ */
41
+ export declare function parseMigrationFilename(filename: string): MigrationFile | null;
42
+ /**
43
+ * Sanitize a migration name: lowercase, replace non-alnum with _, collapse duplicates, trim.
44
+ */
45
+ export declare function sanitizeName(name: string): string;
46
+ /**
47
+ * Generate a YYYYMMDDHHMMSS timestamp string from a Date.
48
+ */
49
+ export declare function formatTimestamp(date: Date): string;
50
+ /**
51
+ * Get pending migration files — those not yet applied.
52
+ * Returns files sorted by timestamp (ascending).
53
+ */
54
+ export declare function getPendingMigrations(migrationsDir: string, applied: string[]): MigrationFile[];
39
55
  /**
40
56
  * List all migration files in the migrations directory, sorted by name.
41
57
  */
42
58
  export declare function listMigrationFiles(migrationsDir: string): MigrationFile[];
59
+ /**
60
+ * Parse migration content string into UP and DOWN sections.
61
+ * Exported for unit testing.
62
+ */
63
+ export declare function parseMigrationContent(content: string): {
64
+ up: string;
65
+ down: string;
66
+ };
43
67
  /**
44
68
  * Parse a migration file into UP and DOWN sections.
45
69
  */
@@ -49,8 +73,12 @@ export declare function parseMigrationSQL(filePath: string): {
49
73
  };
50
74
  /**
51
75
  * Create a new migration file.
76
+ * If `autoContent` is provided, the UP/DOWN sections are pre-populated with the given SQL.
52
77
  */
53
- export declare function createMigration(migrationsDir: string, name: string): MigrationFile;
78
+ export declare function createMigration(migrationsDir: string, name: string, autoContent?: {
79
+ up: string;
80
+ down: string;
81
+ }): MigrationFile;
54
82
  /**
55
83
  * Apply all pending migrations (UP).
56
84
  *
@@ -62,6 +90,7 @@ export declare function createMigration(migrationsDir: string, name: string): Mi
62
90
  */
63
91
  export declare function migrateUp(connectionString: string, migrationsDir: string, options?: {
64
92
  step?: number;
93
+ force?: boolean;
65
94
  }): Promise<{
66
95
  applied: MigrationFile[];
67
96
  errors: Array<{