turbine-orm 0.3.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 (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +295 -0
  3. package/dist/cli/config.d.ts +58 -0
  4. package/dist/cli/config.d.ts.map +1 -0
  5. package/dist/cli/config.js +123 -0
  6. package/dist/cli/config.js.map +1 -0
  7. package/dist/cli/index.d.ts +23 -0
  8. package/dist/cli/index.d.ts.map +1 -0
  9. package/dist/cli/index.js +935 -0
  10. package/dist/cli/index.js.map +1 -0
  11. package/dist/cli/migrate.d.ts +94 -0
  12. package/dist/cli/migrate.d.ts.map +1 -0
  13. package/dist/cli/migrate.js +383 -0
  14. package/dist/cli/migrate.js.map +1 -0
  15. package/dist/cli/ui.d.ts +74 -0
  16. package/dist/cli/ui.d.ts.map +1 -0
  17. package/dist/cli/ui.js +220 -0
  18. package/dist/cli/ui.js.map +1 -0
  19. package/dist/client.d.ts +212 -0
  20. package/dist/client.d.ts.map +1 -0
  21. package/dist/client.js +423 -0
  22. package/dist/client.js.map +1 -0
  23. package/dist/generate.d.ts +24 -0
  24. package/dist/generate.d.ts.map +1 -0
  25. package/dist/generate.js +289 -0
  26. package/dist/generate.js.map +1 -0
  27. package/dist/index.d.ts +44 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +53 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/introspect.d.ts +22 -0
  32. package/dist/introspect.d.ts.map +1 -0
  33. package/dist/introspect.js +284 -0
  34. package/dist/introspect.js.map +1 -0
  35. package/dist/pipeline.d.ts +44 -0
  36. package/dist/pipeline.d.ts.map +1 -0
  37. package/dist/pipeline.js +69 -0
  38. package/dist/pipeline.js.map +1 -0
  39. package/dist/query.d.ts +342 -0
  40. package/dist/query.d.ts.map +1 -0
  41. package/dist/query.js +1396 -0
  42. package/dist/query.js.map +1 -0
  43. package/dist/schema-builder.d.ts +127 -0
  44. package/dist/schema-builder.d.ts.map +1 -0
  45. package/dist/schema-builder.js +164 -0
  46. package/dist/schema-builder.js.map +1 -0
  47. package/dist/schema-sql.d.ts +71 -0
  48. package/dist/schema-sql.d.ts.map +1 -0
  49. package/dist/schema-sql.js +347 -0
  50. package/dist/schema-sql.js.map +1 -0
  51. package/dist/schema.d.ts +90 -0
  52. package/dist/schema.d.ts.map +1 -0
  53. package/dist/schema.js +129 -0
  54. package/dist/schema.js.map +1 -0
  55. package/dist/serverless.d.ts +162 -0
  56. package/dist/serverless.d.ts.map +1 -0
  57. package/dist/serverless.js +195 -0
  58. package/dist/serverless.js.map +1 -0
  59. package/dist/types.d.ts +93 -0
  60. package/dist/types.d.ts.map +1 -0
  61. package/dist/types.js +126 -0
  62. package/dist/types.js.map +1 -0
  63. package/package.json +74 -0
@@ -0,0 +1,935 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @batadata/turbine CLI
4
+ *
5
+ * Commands:
6
+ * turbine init — Initialize a Turbine project
7
+ * turbine generate | pull — Introspect database and generate TypeScript types
8
+ * turbine push — Apply schema-builder definitions to database
9
+ * turbine migrate create <name> — Create a new SQL migration file
10
+ * turbine migrate up — Apply pending migrations
11
+ * turbine migrate down — Rollback last migration
12
+ * turbine migrate status — Show migration status
13
+ * turbine seed — Run seed file
14
+ * turbine status — Show schema summary
15
+ * turbine studio — Launch web UI (coming soon)
16
+ *
17
+ * Usage:
18
+ * DATABASE_URL=postgres://... npx turbine generate
19
+ * npx turbine init --url postgres://...
20
+ * npx turbine migrate create add_users_table
21
+ */
22
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, appendFileSync } from 'node:fs';
23
+ import { resolve, relative } from 'node:path';
24
+ import { introspect } from '../introspect.js';
25
+ import { generate } from '../generate.js';
26
+ 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';
31
+ function parseArgs() {
32
+ const args = process.argv.slice(2);
33
+ const result = {
34
+ command: args[0] ?? 'help',
35
+ positional: [],
36
+ };
37
+ let i = 1;
38
+ // Check for subcommand (e.g. "migrate create")
39
+ if (i < args.length && args[i] && !args[i].startsWith('-')) {
40
+ result.subcommand = args[i];
41
+ i++;
42
+ }
43
+ for (; i < args.length; i++) {
44
+ const arg = args[i];
45
+ const next = args[i + 1];
46
+ switch (arg) {
47
+ case '--url':
48
+ case '-u':
49
+ result.url = next;
50
+ i++;
51
+ break;
52
+ case '--out':
53
+ case '-o':
54
+ result.out = next;
55
+ i++;
56
+ break;
57
+ case '--schema':
58
+ case '-s':
59
+ result.schema = next;
60
+ i++;
61
+ break;
62
+ case '--include':
63
+ result.include = next?.split(',');
64
+ i++;
65
+ break;
66
+ case '--exclude':
67
+ result.exclude = next?.split(',');
68
+ i++;
69
+ break;
70
+ case '--step':
71
+ case '-n':
72
+ result.step = next ? parseInt(next, 10) : undefined;
73
+ i++;
74
+ break;
75
+ case '--dry-run':
76
+ result.dryRun = true;
77
+ break;
78
+ case '--force':
79
+ case '-f':
80
+ result.force = true;
81
+ break;
82
+ case '--verbose':
83
+ case '-v':
84
+ result.verbose = true;
85
+ break;
86
+ default:
87
+ if (!arg.startsWith('-')) {
88
+ result.positional.push(arg);
89
+ }
90
+ break;
91
+ }
92
+ }
93
+ return result;
94
+ }
95
+ // ---------------------------------------------------------------------------
96
+ // Helpers
97
+ // ---------------------------------------------------------------------------
98
+ function requireUrl(config) {
99
+ if (!config.url) {
100
+ error('No database URL provided.');
101
+ newline();
102
+ console.log(` ${dim('Set it in one of these ways:')}`);
103
+ console.log(` ${dim('1.')} Add ${cyan('url')} to ${cyan('turbine.config.ts')}`);
104
+ console.log(` ${dim('2.')} Set ${cyan('DATABASE_URL')} environment variable`);
105
+ console.log(` ${dim('3.')} Pass ${cyan('--url')} flag`);
106
+ newline();
107
+ process.exit(1);
108
+ }
109
+ return config.url;
110
+ }
111
+ async function loadSchemaFile(schemaFile) {
112
+ const absPath = resolve(schemaFile);
113
+ if (!existsSync(absPath)) {
114
+ error(`Schema file not found: ${schemaFile}`);
115
+ console.log(` ${dim('Create one with:')} ${cyan('turbine init')}`);
116
+ process.exit(1);
117
+ }
118
+ try {
119
+ const fileUrl = pathToFileURL(absPath).href;
120
+ const mod = await import(fileUrl);
121
+ const schema = mod.default ?? mod;
122
+ if (!schema.tables) {
123
+ error('Schema file must export a SchemaDef with a "tables" property.');
124
+ process.exit(1);
125
+ }
126
+ return schema;
127
+ }
128
+ catch (err) {
129
+ error(`Failed to load schema file: ${schemaFile}`);
130
+ if (err instanceof Error) {
131
+ console.log(` ${dim(err.message)}`);
132
+ }
133
+ process.exit(1);
134
+ }
135
+ }
136
+ // ---------------------------------------------------------------------------
137
+ // Command: init
138
+ // ---------------------------------------------------------------------------
139
+ async function cmdInit(args, config) {
140
+ banner();
141
+ header('Initializing Turbine project');
142
+ // Detect environment
143
+ const envUrl = process.env['DATABASE_URL'];
144
+ const hasEnvFile = existsSync('.env');
145
+ const hasEnvLocal = existsSync('.env.local');
146
+ if (envUrl) {
147
+ success(`Detected ${cyan('DATABASE_URL')} in environment`);
148
+ }
149
+ else if (hasEnvLocal) {
150
+ info(`Found ${cyan('.env.local')} — Turbine will use ${cyan('DATABASE_URL')} from it if set`);
151
+ }
152
+ else if (hasEnvFile) {
153
+ info(`Found ${cyan('.env')} — Turbine will use ${cyan('DATABASE_URL')} from it if set`);
154
+ }
155
+ else {
156
+ info(`No ${cyan('DATABASE_URL')} found in environment`);
157
+ }
158
+ newline();
159
+ const configPath = findConfigFile();
160
+ // Create config file
161
+ if (configPath && !args.force) {
162
+ warn(`Config file already exists: ${dim(configPath)}`);
163
+ console.log(` ${dim('Run with')} ${cyan('--force')} ${dim('to overwrite')}`);
164
+ }
165
+ else {
166
+ const urlForConfig = args.url ?? undefined;
167
+ const configContent = configTemplate(urlForConfig);
168
+ writeFileSync('turbine.config.ts', configContent, 'utf-8');
169
+ if (configPath) {
170
+ success(`Overwrote ${cyan('turbine.config.ts')}`);
171
+ }
172
+ else {
173
+ success(`Created ${cyan('turbine.config.ts')}`);
174
+ }
175
+ }
176
+ // Create migrations directory
177
+ const migrDir = config.migrationsDir;
178
+ if (!existsSync(migrDir)) {
179
+ mkdirSync(migrDir, { recursive: true });
180
+ // Create .gitkeep
181
+ writeFileSync(`${migrDir}/.gitkeep`, '', 'utf-8');
182
+ success(`Created ${cyan(migrDir + '/')}`);
183
+ }
184
+ else {
185
+ info(`Migrations dir already exists: ${dim(migrDir)}`);
186
+ }
187
+ // Create output directory
188
+ if (!existsSync(config.out)) {
189
+ mkdirSync(config.out, { recursive: true });
190
+ success(`Created ${cyan(config.out + '/')}`);
191
+ }
192
+ // Create seed file template
193
+ const seedDir = config.seedFile.substring(0, config.seedFile.lastIndexOf('/'));
194
+ if (!existsSync(config.seedFile)) {
195
+ if (!existsSync(seedDir)) {
196
+ mkdirSync(seedDir, { recursive: true });
197
+ }
198
+ writeFileSync(config.seedFile, `/**
199
+ * Turbine seed file
200
+ *
201
+ * Run with: npx turbine seed
202
+ */
203
+
204
+ // import { turbine } from '${config.out.replace('./', '')}';
205
+ //
206
+ // const db = turbine({ connectionString: process.env.DATABASE_URL });
207
+ //
208
+ // async function seed() {
209
+ // console.log('Seeding database...');
210
+ //
211
+ // // Add your seed data here:
212
+ // // await db.users.create({ data: { email: 'admin@example.com', name: 'Admin' } });
213
+ //
214
+ // console.log('Done!');
215
+ // await db.disconnect();
216
+ // }
217
+ //
218
+ // seed();
219
+ `, 'utf-8');
220
+ success(`Created ${cyan(config.seedFile)}`);
221
+ }
222
+ // Create schema builder template
223
+ if (!existsSync(config.schemaFile)) {
224
+ const schemaDir = config.schemaFile.substring(0, config.schemaFile.lastIndexOf('/'));
225
+ if (!existsSync(schemaDir)) {
226
+ mkdirSync(schemaDir, { recursive: true });
227
+ }
228
+ writeFileSync(config.schemaFile, `/**
229
+ * Turbine schema definition
230
+ *
231
+ * Define your database schema in TypeScript.
232
+ * Use \`npx turbine push\` to sync it to your database.
233
+ *
234
+ * @see https://batadata.com/docs/turbine/schema
235
+ */
236
+
237
+ import { defineSchema, table, column } from '@batadata/turbine';
238
+
239
+ export default defineSchema({
240
+ // Example:
241
+ // users: table({
242
+ // id: column.serial().primaryKey(),
243
+ // email: column.text().unique().notNull(),
244
+ // name: column.text().notNull(),
245
+ // createdAt: column.timestamp().default('now()'),
246
+ // }),
247
+ });
248
+ `, 'utf-8');
249
+ success(`Created ${cyan(config.schemaFile)}`);
250
+ }
251
+ // Add .gitignore entry for generated output
252
+ const gitignorePath = '.gitignore';
253
+ if (existsSync(gitignorePath)) {
254
+ const gitignoreContent = readFileSync(gitignorePath, 'utf-8');
255
+ if (!gitignoreContent.includes('generated/turbine')) {
256
+ appendFileSync(gitignorePath, '\n# Turbine generated client\ngenerated/turbine/\n');
257
+ success(`Added ${cyan('generated/turbine/')} to ${cyan('.gitignore')}`);
258
+ }
259
+ }
260
+ // If we have a URL, run initial generate
261
+ const url = args.url ?? envUrl ?? config.url;
262
+ if (url) {
263
+ newline();
264
+ divider();
265
+ newline();
266
+ const spinner = new Spinner('Introspecting database').start();
267
+ try {
268
+ const schema = await introspect({
269
+ connectionString: url,
270
+ schema: config.schema,
271
+ include: config.include.length ? config.include : undefined,
272
+ exclude: config.exclude.length ? config.exclude : undefined,
273
+ });
274
+ const tableCount = Object.keys(schema.tables).length;
275
+ spinner.succeed(`Found ${bold(String(tableCount))} tables`);
276
+ const genSpinner = new Spinner('Generating TypeScript client').start();
277
+ const result = generate({ schema, outDir: config.out, connectionString: url });
278
+ genSpinner.succeed(`Generated ${bold(String(result.files.length))} files to ${cyan(config.out + '/')}`);
279
+ }
280
+ catch (err) {
281
+ spinner.fail('Could not connect to database');
282
+ if (err instanceof Error) {
283
+ console.log(` ${dim(err.message)}`);
284
+ }
285
+ newline();
286
+ info('You can run generation later with: ' + cyan('npx turbine generate'));
287
+ }
288
+ }
289
+ // Next steps
290
+ newline();
291
+ divider();
292
+ newline();
293
+ console.log(` ${bold('Next steps:')}`);
294
+ newline();
295
+ if (!url) {
296
+ console.log(` ${dim('1.')} Set your database URL in ${cyan('turbine.config.ts')}`);
297
+ if (!hasEnvFile && !hasEnvLocal) {
298
+ console.log(` ${dim('or create a')} ${cyan('.env')} ${dim('file with')} ${cyan('DATABASE_URL=postgres://...')}`);
299
+ }
300
+ console.log(` ${dim('2.')} Run ${cyan('npx turbine generate')} to introspect your DB`);
301
+ }
302
+ else {
303
+ console.log(` ${dim('1.')} Import the generated client:`);
304
+ console.log(` ${cyan(`import { turbine } from './${config.out.replace('./', '')}';`)}`);
305
+ newline();
306
+ console.log(` ${dim('2.')} Create a connection and query:`);
307
+ console.log(` ${dim('const db = turbine();')}`);
308
+ console.log(` ${dim('const users = await db.users.findMany();')}`);
309
+ }
310
+ newline();
311
+ console.log(` ${dim('3.')} Create migrations: ${cyan('npx turbine migrate create <name>')}`);
312
+ console.log(` ${dim('4.')} Run migrations: ${cyan('npx turbine migrate up')}`);
313
+ console.log(` ${dim('5.')} Seed your database: ${cyan('npx turbine seed')}`);
314
+ newline();
315
+ }
316
+ // ---------------------------------------------------------------------------
317
+ // Command: generate (pull)
318
+ // ---------------------------------------------------------------------------
319
+ async function cmdGenerate(args, config) {
320
+ banner();
321
+ const url = requireUrl(config);
322
+ const startTime = performance.now();
323
+ label('Database', redactUrl(url));
324
+ label('Schema', config.schema);
325
+ label('Output', config.out);
326
+ newline();
327
+ // Introspect
328
+ const spinner = new Spinner('Introspecting database schema').start();
329
+ const schema = await introspect({
330
+ connectionString: url,
331
+ schema: config.schema,
332
+ include: config.include.length ? config.include : undefined,
333
+ exclude: config.exclude.length ? config.exclude : undefined,
334
+ });
335
+ const tableNames = Object.keys(schema.tables);
336
+ const totalColumns = Object.values(schema.tables).reduce((sum, t) => sum + t.columns.length, 0);
337
+ const totalRelations = Object.values(schema.tables).reduce((sum, t) => sum + Object.keys(t.relations).length, 0);
338
+ spinner.succeed(`Found ${bold(String(tableNames.length))} tables, ${bold(String(totalColumns))} columns, ${bold(String(totalRelations))} relations`);
339
+ // Print table summary
340
+ if (args.verbose) {
341
+ newline();
342
+ for (const tbl of Object.values(schema.tables)) {
343
+ const relCount = Object.keys(tbl.relations).length;
344
+ const pk = tbl.primaryKey.join(', ') || '(none)';
345
+ console.log(` ${symbols.tee} ${bold(tbl.name)} ${dim(`${tbl.columns.length} cols, PK: ${pk}`)}${relCount > 0 ? dim(`, ${relCount} rels`) : ''}`);
346
+ }
347
+ newline();
348
+ }
349
+ if (Object.keys(schema.enums).length > 0) {
350
+ info(`Enums: ${Object.keys(schema.enums).join(', ')}`);
351
+ }
352
+ // Generate
353
+ const genSpinner = new Spinner('Generating TypeScript client').start();
354
+ const result = generate({
355
+ schema,
356
+ outDir: config.out,
357
+ connectionString: url,
358
+ });
359
+ genSpinner.succeed(`Generated ${bold(String(result.files.length))} files in ${elapsed(startTime)}`);
360
+ // List files
361
+ for (const file of result.files) {
362
+ console.log(` ${dim(symbols.teeEnd)} ${cyan(result.outDir + '/' + file)}`);
363
+ }
364
+ // Usage hint
365
+ newline();
366
+ divider();
367
+ newline();
368
+ console.log(` ${bold('Usage:')}`);
369
+ newline();
370
+ console.log(` ${cyan(`import { turbine } from './${config.out.replace('./', '')}';`)}`);
371
+ console.log(` ${dim('const db = turbine({ connectionString: process.env.DATABASE_URL });')}`);
372
+ console.log(` ${dim('const user = await db.users.findUnique({ where: { id: 1 } });')}`);
373
+ newline();
374
+ }
375
+ // ---------------------------------------------------------------------------
376
+ // Command: push
377
+ // ---------------------------------------------------------------------------
378
+ async function cmdPush(args, config) {
379
+ banner();
380
+ const url = requireUrl(config);
381
+ label('Database', redactUrl(url));
382
+ label('Schema file', config.schemaFile);
383
+ newline();
384
+ const schemaDef = await loadSchemaFile(config.schemaFile);
385
+ const tableCount = Object.keys(schemaDef.tables).length;
386
+ info(`Schema defines ${bold(String(tableCount))} tables`);
387
+ // Compute diff
388
+ const diffSpinner = new Spinner('Computing schema diff').start();
389
+ const diff = await schemaDiff(schemaDef, url);
390
+ if (diff.statements.length === 0 && diff.drop.length === 0) {
391
+ diffSpinner.succeed('Database is already in sync');
392
+ newline();
393
+ return;
394
+ }
395
+ diffSpinner.succeed('Found changes');
396
+ newline();
397
+ // Show what will happen
398
+ if (diff.create.length > 0) {
399
+ console.log(` ${green('+ Create')} ${bold(String(diff.create.length))} table(s):`);
400
+ for (const t of diff.create) {
401
+ console.log(` ${green(symbols.arrowRight)} ${t.name}`);
402
+ }
403
+ newline();
404
+ }
405
+ if (diff.alter.length > 0) {
406
+ console.log(` ${yellow('~ Alter')} ${bold(String(diff.alter.length))} table(s):`);
407
+ for (const a of diff.alter) {
408
+ console.log(` ${yellow(symbols.arrowRight)} ${a.table}`);
409
+ 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('_', ' '));
413
+ console.log(` ${actionLabel} ${col.column}`);
414
+ }
415
+ }
416
+ newline();
417
+ }
418
+ if (diff.drop.length > 0) {
419
+ console.log(` ${red('- Extra tables')} in database (not in schema):`);
420
+ for (const t of diff.drop) {
421
+ console.log(` ${dim(symbols.arrowRight)} ${t} ${dim('(not dropped automatically)')}`);
422
+ }
423
+ newline();
424
+ }
425
+ // Show SQL
426
+ if (diff.statements.length > 0) {
427
+ console.log(` ${bold('SQL to execute:')}`);
428
+ newline();
429
+ for (const stmt of diff.statements) {
430
+ for (const line of stmt.split('\n')) {
431
+ console.log(` ${dim(symbols.vertLine)} ${cyan(line)}`);
432
+ }
433
+ console.log(` ${dim(symbols.vertLine)}`);
434
+ }
435
+ newline();
436
+ }
437
+ if (args.dryRun) {
438
+ info('Dry run — no changes applied.');
439
+ newline();
440
+ return;
441
+ }
442
+ // Execute
443
+ const pushSpinner = new Spinner('Applying changes').start();
444
+ const result = await schemaPush(schemaDef, url);
445
+ pushSpinner.succeed(`Applied ${bold(String(result.statementsExecuted))} statement(s)`);
446
+ if (result.tablesCreated.length > 0) {
447
+ success(`Created: ${result.tablesCreated.join(', ')}`);
448
+ }
449
+ if (result.tablesAltered.length > 0) {
450
+ success(`Altered: ${result.tablesAltered.join(', ')}`);
451
+ }
452
+ newline();
453
+ info(`Run ${cyan('npx turbine generate')} to update your TypeScript types.`);
454
+ newline();
455
+ }
456
+ // ---------------------------------------------------------------------------
457
+ // Command: migrate
458
+ // ---------------------------------------------------------------------------
459
+ async function cmdMigrate(args, config) {
460
+ const sub = args.subcommand;
461
+ if (!sub || sub === 'help') {
462
+ banner();
463
+ console.log(` ${bold('turbine migrate')} ${dim('— SQL-first migration system')}`);
464
+ newline();
465
+ 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`);
470
+ newline();
471
+ console.log(` ${bold('Options:')}`);
472
+ console.log(` ${cyan('--step, -n')} Number of migrations to apply/rollback`);
473
+ console.log(` ${cyan('--dry-run')} Show SQL without executing`);
474
+ newline();
475
+ console.log(` ${bold('Examples:')}`);
476
+ console.log(` ${dim('npx turbine migrate create add_users_table')}`);
477
+ console.log(` ${dim('npx turbine migrate up')}`);
478
+ console.log(` ${dim('npx turbine migrate down --step 2')}`);
479
+ newline();
480
+ return;
481
+ }
482
+ switch (sub) {
483
+ case 'create':
484
+ await cmdMigrateCreate(args, config);
485
+ break;
486
+ case 'up':
487
+ await cmdMigrateUp(args, config);
488
+ break;
489
+ case 'down':
490
+ await cmdMigrateDown(args, config);
491
+ break;
492
+ case 'status':
493
+ case 'list':
494
+ await cmdMigrateStatus(args, config);
495
+ break;
496
+ default:
497
+ error(`Unknown migrate subcommand: ${sub}`);
498
+ console.log(` ${dim('Run')} ${cyan('npx turbine migrate help')} ${dim('for usage.')}`);
499
+ process.exit(1);
500
+ }
501
+ }
502
+ async function cmdMigrateCreate(args, config) {
503
+ banner();
504
+ const name = args.positional[0];
505
+ if (!name) {
506
+ error('Migration name is required.');
507
+ newline();
508
+ console.log(` ${dim('Usage:')} ${cyan('npx turbine migrate create <name>')}`);
509
+ console.log(` ${dim('Example:')} ${cyan('npx turbine migrate create add_users_table')}`);
510
+ newline();
511
+ process.exit(1);
512
+ }
513
+ const file = createMigration(config.migrationsDir, name);
514
+ const relPath = relative(process.cwd(), file.path);
515
+ success(`Created migration: ${bold(file.filename)}`);
516
+ newline();
517
+ console.log(` ${dim('File:')} ${cyan(relPath)}`);
518
+ newline();
519
+ console.log(` ${dim('Edit the file to add your SQL, then run:')}`);
520
+ console.log(` ${cyan('npx turbine migrate up')}`);
521
+ newline();
522
+ }
523
+ async function cmdMigrateUp(args, config) {
524
+ banner();
525
+ const url = requireUrl(config);
526
+ label('Database', redactUrl(url));
527
+ label('Migrations', config.migrationsDir);
528
+ newline();
529
+ const allFiles = listMigrationFiles(config.migrationsDir);
530
+ if (allFiles.length === 0) {
531
+ warn('No migration files found.');
532
+ console.log(` ${dim('Create one with:')} ${cyan('npx turbine migrate create <name>')}`);
533
+ newline();
534
+ return;
535
+ }
536
+ const spinner = new Spinner('Applying migrations').start();
537
+ const result = await migrateUp(url, config.migrationsDir, {
538
+ step: args.step,
539
+ });
540
+ if (result.applied.length === 0 && result.errors.length === 0) {
541
+ spinner.succeed('All migrations are up to date');
542
+ newline();
543
+ return;
544
+ }
545
+ if (result.applied.length > 0) {
546
+ spinner.succeed(`Applied ${bold(String(result.applied.length))} migration(s)`);
547
+ for (const file of result.applied) {
548
+ console.log(` ${green(symbols.check)} ${file.filename}`);
549
+ }
550
+ }
551
+ if (result.errors.length > 0) {
552
+ spinner.fail('Migration failed');
553
+ for (const { file, error: msg } of result.errors) {
554
+ console.log(` ${red(symbols.cross)} ${file.filename}`);
555
+ console.log(` ${dim(msg)}`);
556
+ }
557
+ newline();
558
+ process.exit(1);
559
+ }
560
+ newline();
561
+ }
562
+ async function cmdMigrateDown(args, config) {
563
+ banner();
564
+ const url = requireUrl(config);
565
+ label('Database', redactUrl(url));
566
+ label('Migrations', config.migrationsDir);
567
+ newline();
568
+ const spinner = new Spinner('Rolling back migration(s)').start();
569
+ const result = await migrateDown(url, config.migrationsDir, {
570
+ step: args.step ?? 1,
571
+ });
572
+ if (result.rolledBack.length === 0 && result.errors.length === 0) {
573
+ spinner.succeed('No migrations to roll back');
574
+ newline();
575
+ return;
576
+ }
577
+ if (result.rolledBack.length > 0) {
578
+ spinner.succeed(`Rolled back ${bold(String(result.rolledBack.length))} migration(s)`);
579
+ for (const file of result.rolledBack) {
580
+ console.log(` ${yellow(symbols.arrowRight)} ${file.filename}`);
581
+ }
582
+ }
583
+ if (result.errors.length > 0) {
584
+ spinner.fail('Rollback failed');
585
+ for (const { file, error: msg } of result.errors) {
586
+ console.log(` ${red(symbols.cross)} ${file.filename}`);
587
+ console.log(` ${dim(msg)}`);
588
+ }
589
+ newline();
590
+ process.exit(1);
591
+ }
592
+ newline();
593
+ }
594
+ async function cmdMigrateStatus(args, config) {
595
+ banner();
596
+ const url = requireUrl(config);
597
+ label('Database', redactUrl(url));
598
+ label('Migrations', config.migrationsDir);
599
+ newline();
600
+ const allFiles = listMigrationFiles(config.migrationsDir);
601
+ if (allFiles.length === 0) {
602
+ warn('No migration files found.');
603
+ console.log(` ${dim('Create one with:')} ${cyan('npx turbine migrate create <name>')}`);
604
+ newline();
605
+ return;
606
+ }
607
+ const statuses = await migrateStatus(url, config.migrationsDir);
608
+ const appliedCount = statuses.filter((s) => s.applied).length;
609
+ const pendingCount = statuses.filter((s) => !s.applied).length;
610
+ info(`${bold(String(appliedCount))} applied, ${pendingCount > 0 ? yellow(bold(String(pendingCount))) : bold(String(pendingCount))} pending`);
611
+ newline();
612
+ // Check for checksum mismatches
613
+ const driftCount = statuses.filter((s) => s.checksumValid === false).length;
614
+ if (driftCount > 0) {
615
+ warn(`${bold(String(driftCount))} migration(s) have been modified after application!`);
616
+ console.log(` ${dim('Applied migrations should be immutable. Modifying them can cause drift.')}`);
617
+ newline();
618
+ }
619
+ // Format as table
620
+ const headers = ['Status', 'Migration', 'Applied at'];
621
+ const rows = statuses.map((s) => {
622
+ let status;
623
+ if (s.applied && s.checksumValid === false) {
624
+ status = red(symbols.warning + ' Drifted');
625
+ }
626
+ else if (s.applied) {
627
+ status = green(symbols.check + ' Applied');
628
+ }
629
+ else {
630
+ status = yellow(symbols.dot + ' Pending');
631
+ }
632
+ return [
633
+ status,
634
+ s.file.filename,
635
+ s.appliedAt
636
+ ? dim(s.appliedAt.toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC'))
637
+ : dim('—'),
638
+ ];
639
+ });
640
+ console.log(formatTable(headers, rows));
641
+ newline();
642
+ if (pendingCount > 0) {
643
+ console.log(` ${dim('Run')} ${cyan('npx turbine migrate up')} ${dim('to apply pending migrations.')}`);
644
+ newline();
645
+ }
646
+ }
647
+ // ---------------------------------------------------------------------------
648
+ // Command: seed
649
+ // ---------------------------------------------------------------------------
650
+ async function cmdSeed(args, config) {
651
+ banner();
652
+ const seedFile = resolve(config.seedFile);
653
+ label('Seed file', config.seedFile);
654
+ newline();
655
+ if (!existsSync(seedFile)) {
656
+ error(`Seed file not found: ${config.seedFile}`);
657
+ newline();
658
+ console.log(` ${dim('Create one with:')} ${cyan('npx turbine init')}`);
659
+ console.log(` ${dim('Or set a custom path in')} ${cyan('turbine.config.ts')}`);
660
+ newline();
661
+ process.exit(1);
662
+ }
663
+ const spinner = new Spinner('Running seed file').start();
664
+ try {
665
+ // Use child_process to run the seed file via tsx or node
666
+ const { execSync } = await import('node:child_process');
667
+ // Try tsx first (most compatible with .ts files), fall back to node --experimental-strip-types
668
+ const runners = [
669
+ { cmd: 'npx tsx', name: 'tsx' },
670
+ { cmd: 'node --experimental-strip-types', name: 'node' },
671
+ ];
672
+ let ran = false;
673
+ for (const runner of runners) {
674
+ try {
675
+ execSync(`${runner.cmd} ${seedFile}`, {
676
+ stdio: 'inherit',
677
+ env: {
678
+ ...process.env,
679
+ DATABASE_URL: config.url || process.env['DATABASE_URL'],
680
+ },
681
+ });
682
+ ran = true;
683
+ break;
684
+ }
685
+ catch (err) {
686
+ // If tsx not found, try next runner
687
+ if (err instanceof Error && 'status' in err && err.status === null) {
688
+ continue;
689
+ }
690
+ throw err;
691
+ }
692
+ }
693
+ if (!ran) {
694
+ throw new Error('Could not find tsx or compatible Node.js version to run .ts files');
695
+ }
696
+ spinner.succeed('Seed completed');
697
+ }
698
+ catch (err) {
699
+ spinner.fail('Seed failed');
700
+ if (err instanceof Error) {
701
+ console.log(` ${dim(err.message)}`);
702
+ }
703
+ newline();
704
+ process.exit(1);
705
+ }
706
+ newline();
707
+ }
708
+ // ---------------------------------------------------------------------------
709
+ // Command: status
710
+ // ---------------------------------------------------------------------------
711
+ async function cmdStatus(args, config) {
712
+ banner();
713
+ const url = requireUrl(config);
714
+ label('Database', redactUrl(url));
715
+ label('Schema', config.schema);
716
+ newline();
717
+ const spinner = new Spinner('Introspecting database').start();
718
+ const schema = await introspect({
719
+ connectionString: url,
720
+ schema: config.schema,
721
+ include: config.include.length ? config.include : undefined,
722
+ exclude: config.exclude.length ? config.exclude : undefined,
723
+ });
724
+ const tableNames = Object.keys(schema.tables);
725
+ spinner.succeed(`Found ${bold(String(tableNames.length))} tables`);
726
+ newline();
727
+ for (const tbl of Object.values(schema.tables)) {
728
+ const relCount = Object.keys(tbl.relations).length;
729
+ const pk = tbl.primaryKey.join(', ') || dim('(none)');
730
+ console.log(` ${bold(cyan(tbl.name))}`);
731
+ for (let i = 0; i < tbl.columns.length; i++) {
732
+ const col = tbl.columns[i];
733
+ const isLast = i === tbl.columns.length - 1 && relCount === 0;
734
+ const prefix = isLast ? symbols.teeEnd : symbols.tee;
735
+ const nullable = col.nullable ? dim('?') : '';
736
+ const def = col.hasDefault ? dim(' (default)') : '';
737
+ 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)}`);
739
+ }
740
+ const rels = Object.entries(tbl.relations);
741
+ if (rels.length > 0) {
742
+ for (let i = 0; i < rels.length; i++) {
743
+ const [relName, rel] = rels[i];
744
+ const isLast = i === rels.length - 1;
745
+ const prefix = isLast ? symbols.teeEnd : symbols.tee;
746
+ const relColor = rel.type === 'hasMany' ? blue : yellow;
747
+ console.log(` ${dim(prefix)} ${relColor(relName)} ${dim(symbols.arrow)} ${rel.to} ${dim(`(${rel.type}, FK: ${rel.foreignKey})`)}`);
748
+ }
749
+ }
750
+ newline();
751
+ }
752
+ if (Object.keys(schema.enums).length > 0) {
753
+ console.log(` ${bold('Enums:')}`);
754
+ for (const [enumName, labels] of Object.entries(schema.enums)) {
755
+ console.log(` ${cyan(enumName)}: ${labels.map((l) => green(`'${l}'`)).join(dim(' | '))}`);
756
+ }
757
+ newline();
758
+ }
759
+ }
760
+ // ---------------------------------------------------------------------------
761
+ // Command: studio (scaffold)
762
+ // ---------------------------------------------------------------------------
763
+ async function cmdStudio(_args, _config) {
764
+ banner();
765
+ console.log(box([
766
+ `${bold('Turbine Studio')} ${dim('— coming soon')}`,
767
+ '',
768
+ 'A local web UI for browsing your database,',
769
+ 'exploring relations, and managing data.',
770
+ '',
771
+ `Follow ${cyan('@batadata')} for updates.`,
772
+ ].join('\n'), { title: bold(cyan('Studio')), padding: 2 }));
773
+ newline();
774
+ }
775
+ // ---------------------------------------------------------------------------
776
+ // Help
777
+ // ---------------------------------------------------------------------------
778
+ function showHelp() {
779
+ banner();
780
+ console.log(` ${bold('Usage:')}`);
781
+ console.log(` npx turbine ${cyan('<command>')} ${dim('[options]')}`);
782
+ newline();
783
+ console.log(` ${bold('Commands:')}`);
784
+ console.log(` ${cyan('init')} Initialize a Turbine project`);
785
+ console.log(` ${cyan('generate')} ${dim('| pull')} Introspect database ${symbols.arrow} generate types`);
786
+ console.log(` ${cyan('push')} Apply schema definitions to database`);
787
+ console.log(` ${cyan('migrate')} ${dim('<sub>')} SQL migration management`);
788
+ console.log(` ${dim('create <name>')} Create a new migration file`);
789
+ console.log(` ${dim('up')} Apply pending migrations`);
790
+ console.log(` ${dim('down')} Rollback last migration`);
791
+ console.log(` ${dim('status')} Show applied/pending migrations`);
792
+ console.log(` ${cyan('seed')} Run seed file`);
793
+ console.log(` ${cyan('status')} ${dim('| info')} Show schema summary`);
794
+ console.log(` ${cyan('studio')} Launch web UI (coming soon)`);
795
+ newline();
796
+ console.log(` ${bold('Options:')}`);
797
+ console.log(` ${cyan('--url, -u')} ${dim('<url>')} Postgres connection string`);
798
+ console.log(` ${cyan('--out, -o')} ${dim('<dir>')} Output directory ${dim('(default: ./generated/turbine)')}`);
799
+ console.log(` ${cyan('--schema, -s')} ${dim('<name>')} Postgres schema ${dim('(default: public)')}`);
800
+ console.log(` ${cyan('--include')} ${dim('<tables>')} Comma-separated tables to include`);
801
+ console.log(` ${cyan('--exclude')} ${dim('<tables>')} Comma-separated tables to exclude`);
802
+ console.log(` ${cyan('--dry-run')} Show SQL without executing`);
803
+ console.log(` ${cyan('--verbose, -v')} Show detailed output`);
804
+ console.log(` ${cyan('--force, -f')} Overwrite existing files`);
805
+ newline();
806
+ console.log(` ${bold('Config file:')}`);
807
+ console.log(` ${dim('Create')} ${cyan('turbine.config.ts')} ${dim('with')} ${cyan('npx turbine init')}`);
808
+ console.log(` ${dim('CLI flags override config file values.')}`);
809
+ newline();
810
+ console.log(` ${bold('Examples:')}`);
811
+ console.log(` ${dim('$')} npx turbine init --url postgres://user:pass@host/db`);
812
+ console.log(` ${dim('$')} DATABASE_URL=postgres://... npx turbine generate`);
813
+ console.log(` ${dim('$')} npx turbine migrate create add_users_table`);
814
+ console.log(` ${dim('$')} npx turbine migrate up`);
815
+ console.log(` ${dim('$')} npx turbine push --dry-run`);
816
+ newline();
817
+ }
818
+ // ---------------------------------------------------------------------------
819
+ // Version
820
+ // ---------------------------------------------------------------------------
821
+ function showVersion() {
822
+ // Read version from package.json at build time
823
+ console.log(`@batadata/turbine v0.2.0`);
824
+ }
825
+ // ---------------------------------------------------------------------------
826
+ // Main
827
+ // ---------------------------------------------------------------------------
828
+ async function main() {
829
+ const args = parseArgs();
830
+ // Quick exits that don't need config
831
+ if (args.command === 'help' || args.command === '--help' || args.command === '-h') {
832
+ showHelp();
833
+ return;
834
+ }
835
+ if (args.command === 'version' || args.command === '--version' || args.command === '-V') {
836
+ showVersion();
837
+ return;
838
+ }
839
+ // Load config file
840
+ let fileConfig = {};
841
+ try {
842
+ fileConfig = await loadConfig();
843
+ }
844
+ catch (err) {
845
+ if (args.command !== 'init') {
846
+ warn(`Could not load config: ${err instanceof Error ? err.message : String(err)}`);
847
+ }
848
+ }
849
+ const overrides = {
850
+ url: args.url,
851
+ out: args.out,
852
+ schema: args.schema,
853
+ include: args.include,
854
+ exclude: args.exclude,
855
+ };
856
+ const config = resolveConfig(fileConfig, overrides);
857
+ try {
858
+ switch (args.command) {
859
+ case 'init':
860
+ await cmdInit(args, config);
861
+ break;
862
+ case 'generate':
863
+ case 'gen':
864
+ case 'g':
865
+ case 'pull':
866
+ await cmdGenerate(args, config);
867
+ break;
868
+ case 'push':
869
+ await cmdPush(args, config);
870
+ break;
871
+ case 'migrate':
872
+ case 'migration':
873
+ case 'm':
874
+ await cmdMigrate(args, config);
875
+ break;
876
+ case 'seed':
877
+ case 's':
878
+ await cmdSeed(args, config);
879
+ break;
880
+ case 'status':
881
+ case 'info':
882
+ await cmdStatus(args, config);
883
+ break;
884
+ case 'studio':
885
+ await cmdStudio(args, config);
886
+ break;
887
+ default:
888
+ error(`Unknown command: ${bold(args.command)}`);
889
+ newline();
890
+ console.log(` ${dim('Run')} ${cyan('npx turbine help')} ${dim('for available commands.')}`);
891
+ newline();
892
+ process.exit(1);
893
+ }
894
+ }
895
+ catch (err) {
896
+ if (err instanceof Error) {
897
+ if (err.message.includes('ECONNREFUSED') || err.message.includes('connection')) {
898
+ newline();
899
+ error(`Could not connect to database`);
900
+ console.log(` ${dim(err.message)}`);
901
+ newline();
902
+ console.log(` ${dim('Check that:')}`);
903
+ console.log(` ${dim('1.')} Your database is running`);
904
+ console.log(` ${dim('2.')} The connection string is correct`);
905
+ console.log(` ${dim('3.')} Network/firewall allows the connection`);
906
+ }
907
+ else if (err.message.includes('authentication')) {
908
+ newline();
909
+ error(`Authentication failed`);
910
+ console.log(` ${dim(err.message)}`);
911
+ }
912
+ else if (err.message.includes('does not exist')) {
913
+ newline();
914
+ error(`Database or schema not found`);
915
+ console.log(` ${dim(err.message)}`);
916
+ }
917
+ else {
918
+ newline();
919
+ error(err.message);
920
+ if (args.verbose && err.stack) {
921
+ newline();
922
+ console.log(dim(err.stack));
923
+ }
924
+ }
925
+ }
926
+ else {
927
+ newline();
928
+ error(`Unexpected error: ${String(err)}`);
929
+ }
930
+ newline();
931
+ process.exit(1);
932
+ }
933
+ }
934
+ main();
935
+ //# sourceMappingURL=index.js.map