turbine-orm 0.27.0 → 0.28.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 (52) hide show
  1. package/README.md +17 -13
  2. package/dist/cjs/cli/config.js +20 -3
  3. package/dist/cjs/cli/destructive.js +47 -31
  4. package/dist/cjs/cli/index.js +273 -71
  5. package/dist/cjs/cli/mcp.js +788 -0
  6. package/dist/cjs/cli/migrate.js +95 -20
  7. package/dist/cjs/cli/studio.js +3 -2
  8. package/dist/cjs/client.js +267 -34
  9. package/dist/cjs/dialect.js +2 -0
  10. package/dist/cjs/generate.js +171 -7
  11. package/dist/cjs/index.js +4 -1
  12. package/dist/cjs/introspect.js +177 -4
  13. package/dist/cjs/query/batched-loader.js +148 -0
  14. package/dist/cjs/query/builder.js +714 -133
  15. package/dist/cjs/schema-builder.js +59 -4
  16. package/dist/cjs/schema-sql.js +315 -6
  17. package/dist/cjs/seed.js +66 -0
  18. package/dist/cli/config.d.ts +9 -2
  19. package/dist/cli/config.js +19 -3
  20. package/dist/cli/destructive.js +47 -31
  21. package/dist/cli/index.d.ts +52 -1
  22. package/dist/cli/index.js +272 -74
  23. package/dist/cli/mcp.d.ts +17 -0
  24. package/dist/cli/mcp.js +781 -0
  25. package/dist/cli/migrate.d.ts +37 -0
  26. package/dist/cli/migrate.js +92 -20
  27. package/dist/cli/studio.d.ts +3 -2
  28. package/dist/cli/studio.js +3 -2
  29. package/dist/client.d.ts +136 -1
  30. package/dist/client.js +267 -34
  31. package/dist/dialect.d.ts +17 -0
  32. package/dist/dialect.js +2 -0
  33. package/dist/generate.d.ts +17 -0
  34. package/dist/generate.js +171 -10
  35. package/dist/index.d.ts +4 -3
  36. package/dist/index.js +2 -0
  37. package/dist/introspect.d.ts +20 -1
  38. package/dist/introspect.js +175 -4
  39. package/dist/query/batched-loader.d.ts +29 -2
  40. package/dist/query/batched-loader.js +148 -1
  41. package/dist/query/builder.d.ts +156 -8
  42. package/dist/query/builder.js +715 -134
  43. package/dist/query/index.d.ts +1 -1
  44. package/dist/query/types.d.ts +113 -8
  45. package/dist/schema-builder.d.ts +73 -8
  46. package/dist/schema-builder.js +59 -4
  47. package/dist/schema-sql.d.ts +67 -0
  48. package/dist/schema-sql.js +310 -6
  49. package/dist/schema.d.ts +53 -0
  50. package/dist/seed.d.ts +4 -0
  51. package/dist/seed.js +63 -0
  52. package/package.json +2 -3
package/dist/cli/index.js CHANGED
@@ -8,12 +8,14 @@
8
8
  * turbine push — Apply schema-builder definitions to database
9
9
  * turbine migrate create <name> — Create a new SQL migration file
10
10
  * turbine migrate up — Apply pending migrations
11
+ * turbine migrate deploy — Apply pending migrations without prompts
11
12
  * turbine migrate down — Rollback last migration
12
13
  * turbine migrate status — Show migration status
13
14
  * turbine seed — Run seed file
14
15
  * turbine status — Show schema summary
15
16
  * turbine doctor — Check relations for missing FK indexes (--fix emits migration)
16
17
  * turbine studio — Launch local read-only web UI
18
+ * turbine mcp — Start read-only MCP server over JSON-RPC stdio
17
19
  * turbine observe — Launch metrics dashboard (requires TURBINE_OBSERVE_URL)
18
20
  *
19
21
  * Usage:
@@ -22,20 +24,21 @@
22
24
  * npx turbine migrate create add_users_table
23
25
  */
24
26
  import { appendFileSync, existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
25
- import { dirname, relative, resolve } from 'node:path';
27
+ import { basename, dirname, extname, relative, resolve } from 'node:path';
26
28
  import { pathToFileURL } from 'node:url';
27
29
  import { generate } from '../generate.js';
28
30
  import { findMissingRelationIndexes } from '../index-advisor.js';
29
31
  import { introspect } from '../introspect.js';
30
32
  import { schemaDiff, schemaPush } from '../schema-sql.js';
31
- import { configTemplate, findConfigFile, loadConfig, looksLikeSchemaFilePath, resolveConfig } from './config.js';
33
+ import { configTemplate, findConfigFile, loadConfig, looksLikeSchemaFilePath, resolveConfig, resolveSeedFile, } from './config.js';
32
34
  import { canResolveTsx, getTsLoaderError, needsTsLoader, registerTsLoader } from './loader.js';
33
- import { createMigration, listMigrationFiles, migrateDown, migrateStatus, migrateUp } from './migrate.js';
35
+ import { runMcpServer } from './mcp.js';
36
+ import { createMigration, inspectMigrationDeploy, listMigrationFiles, migrateDeploy, migrateDown, migrateStatus, migrateUp, } from './migrate.js';
34
37
  import { startObserve } from './observe.js';
35
38
  import { startStudio } from './studio.js';
36
39
  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';
37
- function parseArgs() {
38
- const args = process.argv.slice(2);
40
+ export function parseArgs(argv = process.argv.slice(2)) {
41
+ const args = argv;
39
42
  const result = {
40
43
  command: args[0] ?? 'help',
41
44
  positional: [],
@@ -93,6 +96,12 @@ function parseArgs() {
93
96
  case '--fix':
94
97
  result.fix = true;
95
98
  break;
99
+ case '--zod':
100
+ result.zod = true;
101
+ break;
102
+ case '--include-views':
103
+ result.includeViews = true;
104
+ break;
96
105
  case '--allow-destructive':
97
106
  result.allowDestructive = true;
98
107
  break;
@@ -119,6 +128,9 @@ function parseArgs() {
119
128
  case '--no-open':
120
129
  result.noOpen = true;
121
130
  break;
131
+ case '--allow-remote':
132
+ result.allowRemote = true;
133
+ break;
122
134
  default:
123
135
  if (!arg.startsWith('-')) {
124
136
  result.positional.push(arg);
@@ -297,34 +309,30 @@ async function cmdInit(args, config) {
297
309
  success(`Created ${cyan(`${config.out}/`)}`);
298
310
  }
299
311
  // Create seed file template
300
- const seedDir = config.seedFile.substring(0, config.seedFile.lastIndexOf('/'));
301
- if (!existsSync(config.seedFile)) {
312
+ const initSeedFile = config.seedFile ?? './seed.ts';
313
+ const seedDir = dirname(initSeedFile);
314
+ if (!existsSync(initSeedFile)) {
302
315
  if (!existsSync(seedDir)) {
303
316
  mkdirSync(seedDir, { recursive: true });
304
317
  }
305
- writeFileSync(config.seedFile, `/**
318
+ writeFileSync(initSeedFile, `/**
306
319
  * Turbine seed file
307
320
  *
308
321
  * Run with: npx turbine seed
309
322
  */
310
323
 
311
- // import { turbine } from '${config.out.replace('./', '')}';
312
- //
313
- // const db = turbine({ connectionString: process.env.DATABASE_URL });
314
- //
315
- // async function seed() {
316
- // console.log('Seeding database...');
317
- //
318
- // // Add your seed data here:
319
- // // await db.users.create({ data: { email: 'admin@example.com', name: 'Admin' } });
320
- //
321
- // console.log('Done!');
322
- // await db.disconnect();
323
- // }
324
- //
325
- // seed();
324
+ import { defineSeed } from 'turbine-orm';
325
+
326
+ export default defineSeed(async (db) => {
327
+ console.log('Seeding database...');
328
+
329
+ // Add your seed data here:
330
+ // await db.raw\`INSERT INTO users (email, name) VALUES (\${'admin@example.com'}, \${'Admin'})\`;
331
+
332
+ console.log('Done!');
333
+ });
326
334
  `, 'utf-8');
327
- success(`Created ${cyan(config.seedFile)}`);
335
+ success(`Created ${cyan(initSeedFile)}`);
328
336
  }
329
337
  // Create schema builder template
330
338
  if (!existsSync(config.schemaFile)) {
@@ -466,6 +474,7 @@ async function cmdGenerate(args, config) {
466
474
  schema: config.schema,
467
475
  include: config.include.length ? config.include : undefined,
468
476
  exclude: config.exclude.length ? config.exclude : undefined,
477
+ includeViews: args.includeViews,
469
478
  });
470
479
  const tableNames = Object.keys(schema.tables);
471
480
  const totalColumns = Object.values(schema.tables).reduce((sum, t) => sum + t.columns.length, 0);
@@ -508,6 +517,7 @@ async function cmdGenerate(args, config) {
508
517
  schema,
509
518
  outDir: config.out,
510
519
  connectionString: url,
520
+ zod: args.zod,
511
521
  });
512
522
  genSpinner.succeed(`Generated ${bold(String(result.files.length))} files in ${elapsed(startTime)}`);
513
523
  // List files
@@ -621,6 +631,7 @@ async function cmdMigrate(args, config) {
621
631
  console.log(` ${cyan('create <name>')} Create a new migration file`);
622
632
  console.log(` ${cyan('create <name> --auto')} Auto-generate from schema diff`);
623
633
  console.log(` ${cyan('up')} Apply pending migrations`);
634
+ console.log(` ${cyan('deploy')} Apply pending migrations without prompts`);
624
635
  console.log(` ${cyan('down')} Rollback last migration`);
625
636
  console.log(` ${cyan('status')} Show migration status`);
626
637
  newline();
@@ -634,6 +645,7 @@ async function cmdMigrate(args, config) {
634
645
  console.log(` ${dim('npx turbine migrate create add_users_table')}`);
635
646
  console.log(` ${dim('npx turbine migrate create add_email_index --auto')}`);
636
647
  console.log(` ${dim('npx turbine migrate up')}`);
648
+ console.log(` ${dim('npx turbine migrate deploy --dry-run')}`);
637
649
  console.log(` ${dim('npx turbine migrate down --step 2')}`);
638
650
  newline();
639
651
  return;
@@ -645,6 +657,9 @@ async function cmdMigrate(args, config) {
645
657
  case 'up':
646
658
  await cmdMigrateUp(args, config);
647
659
  break;
660
+ case 'deploy':
661
+ await cmdMigrateDeploy(args, config);
662
+ break;
648
663
  case 'down':
649
664
  await cmdMigrateDown(args, config);
650
665
  break;
@@ -803,6 +818,67 @@ async function cmdMigrateUp(args, config) {
803
818
  }
804
819
  newline();
805
820
  }
821
+ export function buildMigrateDeployOptions(_args) {
822
+ return {
823
+ allowDrift: false,
824
+ allowDestructive: true,
825
+ step: undefined,
826
+ };
827
+ }
828
+ async function cmdMigrateDeploy(args, config) {
829
+ banner();
830
+ const url = requireUrl(config);
831
+ label('Database', redactUrl(url));
832
+ label('Migrations', config.migrationsDir);
833
+ newline();
834
+ if (args.dryRun) {
835
+ const spinner = new Spinner('Checking pending migrations').start();
836
+ const plan = await inspectMigrationDeploy(url, config.migrationsDir);
837
+ if (plan.mismatches.length > 0) {
838
+ spinner.fail('Deploy blocked by migration drift');
839
+ for (const mismatch of plan.mismatches) {
840
+ const reason = mismatch.type === 'missing' ? 'deleted from disk' : 'modified on disk';
841
+ console.log(` ${red(symbols.cross)} ${mismatch.name}.sql ${dim(`(${reason})`)}`);
842
+ }
843
+ newline();
844
+ process.exit(1);
845
+ }
846
+ if (plan.pending.length === 0) {
847
+ spinner.succeed('No pending migrations');
848
+ newline();
849
+ return;
850
+ }
851
+ spinner.succeed(`${bold(String(plan.pending.length))} pending migration(s)`);
852
+ for (const file of plan.pending) {
853
+ console.log(` ${yellow(symbols.dot)} ${file.filename}`);
854
+ }
855
+ newline();
856
+ return;
857
+ }
858
+ const spinner = new Spinner('Deploying migrations').start();
859
+ const result = await migrateDeploy(url, config.migrationsDir);
860
+ if (result.applied.length === 0 && result.errors.length === 0) {
861
+ spinner.succeed('0 applied — all migrations are up to date');
862
+ newline();
863
+ return;
864
+ }
865
+ if (result.applied.length > 0) {
866
+ spinner.succeed(`${bold(String(result.applied.length))} applied`);
867
+ for (const file of result.applied) {
868
+ console.log(` ${green(symbols.check)} ${file.filename}`);
869
+ }
870
+ }
871
+ if (result.errors.length > 0) {
872
+ spinner.fail('Deploy failed');
873
+ for (const { file, error: msg } of result.errors) {
874
+ console.log(` ${red(symbols.cross)} ${file.filename}`);
875
+ console.log(` ${dim(msg)}`);
876
+ }
877
+ newline();
878
+ process.exit(1);
879
+ }
880
+ newline();
881
+ }
806
882
  /** True when the error is migrate up/down's destructive-statement refusal. */
807
883
  function isDestructiveRefusal(err) {
808
884
  return err instanceof Error && err.message.includes('DESTRUCTIVE');
@@ -948,55 +1024,81 @@ async function cmdMigrateStatus(_args, config) {
948
1024
  newline();
949
1025
  }
950
1026
  }
951
- // ---------------------------------------------------------------------------
952
- // Command: seed
953
- // ---------------------------------------------------------------------------
1027
+ export function getSeedExecutionPlan(seedFile) {
1028
+ const ext = extname(seedFile).toLowerCase();
1029
+ if (ext === '.ts' || ext === '.mts' || ext === '.cts') {
1030
+ return { kind: 'tsx', command: 'npx', args: ['tsx', seedFile] };
1031
+ }
1032
+ if (ext === '.js' || ext === '.mjs' || ext === '.cjs') {
1033
+ return { kind: 'js', file: seedFile };
1034
+ }
1035
+ if (ext === '.sql') {
1036
+ return { kind: 'sql', file: seedFile };
1037
+ }
1038
+ throw new Error(`Unsupported seed file extension: ${ext || '(none)'}. Use seed.ts, seed.js, or seed.sql.`);
1039
+ }
1040
+ async function runSeedPlan(plan, config) {
1041
+ const oldDatabaseUrl = process.env.DATABASE_URL;
1042
+ if (config.url)
1043
+ process.env.DATABASE_URL = config.url;
1044
+ try {
1045
+ if (plan.kind === 'tsx') {
1046
+ if (!canResolveTsx()) {
1047
+ throw new Error('TypeScript seed files require tsx — install tsx or use seed.js/seed.sql.');
1048
+ }
1049
+ const { execFileSync } = await import('node:child_process');
1050
+ execFileSync(plan.command, plan.args, {
1051
+ stdio: 'inherit',
1052
+ env: {
1053
+ ...process.env,
1054
+ DATABASE_URL: config.url || process.env.DATABASE_URL,
1055
+ },
1056
+ });
1057
+ return;
1058
+ }
1059
+ if (plan.kind === 'js') {
1060
+ const mod = await import(pathToFileURL(plan.file).href);
1061
+ if (typeof mod.default === 'function') {
1062
+ await mod.default();
1063
+ }
1064
+ return;
1065
+ }
1066
+ const url = requireUrl(config);
1067
+ const { default: pg } = await import('pg');
1068
+ const client = new pg.Client({ connectionString: url });
1069
+ await client.connect();
1070
+ try {
1071
+ await client.query(readFileSync(plan.file, 'utf-8'));
1072
+ }
1073
+ finally {
1074
+ await client.end();
1075
+ }
1076
+ }
1077
+ finally {
1078
+ if (oldDatabaseUrl === undefined) {
1079
+ delete process.env.DATABASE_URL;
1080
+ }
1081
+ else {
1082
+ process.env.DATABASE_URL = oldDatabaseUrl;
1083
+ }
1084
+ }
1085
+ }
954
1086
  async function cmdSeed(_args, config) {
955
1087
  banner();
956
- const seedFile = resolve(config.seedFile);
957
- label('Seed file', config.seedFile);
1088
+ const seedFile = resolveSeedFile(config);
1089
+ label('Seed file', seedFile ? relative(process.cwd(), seedFile) || seedFile : '(not found)');
958
1090
  newline();
959
- if (!existsSync(seedFile)) {
960
- error(`Seed file not found: ${config.seedFile}`);
1091
+ if (!seedFile || !existsSync(seedFile)) {
1092
+ error(`Seed file not found.`);
961
1093
  newline();
962
- console.log(` ${dim('Create one with:')} ${cyan('npx turbine init')}`);
963
- console.log(` ${dim('Or set a custom path in')} ${cyan('turbine.config.ts')}`);
1094
+ console.log(` ${dim('Create one of:')} ${cyan('seed.ts')}${dim(',')} ${cyan('seed.js')}${dim(',')} ${cyan('seed.sql')}`);
1095
+ console.log(` ${dim('Or set')} ${cyan('seed')} ${dim('in')} ${cyan('turbine.config.ts')}`);
964
1096
  newline();
965
1097
  process.exit(1);
966
1098
  }
967
1099
  const spinner = new Spinner('Running seed file').start();
968
1100
  try {
969
- // Use child_process to run the seed file via tsx or node
970
- const { execFileSync } = await import('node:child_process');
971
- // Try tsx first (most compatible with .ts files), fall back to node --experimental-strip-types
972
- const runners = [
973
- { cmd: 'npx', args: ['tsx', seedFile], name: 'tsx' },
974
- { cmd: 'node', args: ['--experimental-strip-types', seedFile], name: 'node' },
975
- ];
976
- let ran = false;
977
- for (const runner of runners) {
978
- try {
979
- execFileSync(runner.cmd, runner.args, {
980
- stdio: 'inherit',
981
- env: {
982
- ...process.env,
983
- DATABASE_URL: config.url || process.env.DATABASE_URL,
984
- },
985
- });
986
- ran = true;
987
- break;
988
- }
989
- catch (err) {
990
- // If tsx not found, try next runner
991
- if (err instanceof Error && 'status' in err && err.status === null) {
992
- continue;
993
- }
994
- throw err;
995
- }
996
- }
997
- if (!ran) {
998
- throw new Error('Could not find tsx or compatible Node.js version to run .ts files');
999
- }
1101
+ await runSeedPlan(getSeedExecutionPlan(seedFile), config);
1000
1102
  spinner.succeed('Seed completed');
1001
1103
  }
1002
1104
  catch (err) {
@@ -1137,6 +1239,17 @@ async function cmdDoctor(args, config) {
1137
1239
  }
1138
1240
  }
1139
1241
  // ---------------------------------------------------------------------------
1242
+ // Loopback host gate (Studio / Observe)
1243
+ // ---------------------------------------------------------------------------
1244
+ /**
1245
+ * True when `host` is a loopback address Studio/Observe may bind without
1246
+ * `--allow-remote`. Accepts IPv4, IPv6, and the common bracket form.
1247
+ */
1248
+ export function isLoopbackHost(host) {
1249
+ const h = host.trim().toLowerCase();
1250
+ return h === '127.0.0.1' || h === 'localhost' || h === '::1' || h === '[::1]';
1251
+ }
1252
+ // ---------------------------------------------------------------------------
1140
1253
  // Command: studio — local read-only web UI
1141
1254
  // ---------------------------------------------------------------------------
1142
1255
  async function cmdStudio(args, config) {
@@ -1149,11 +1262,18 @@ async function cmdStudio(args, config) {
1149
1262
  console.log(red(`✗ invalid port: ${args.port}`));
1150
1263
  process.exit(1);
1151
1264
  }
1152
- // Warn loudly when an explicit non-loopback --host is used — the user is
1153
- // opting in, so we proceed rather than refuse. Studio has no real
1154
- // authentication beyond a random session token, so exposing it on a LAN
1155
- // interface is foot-gun territory.
1156
- if (host !== '127.0.0.1' && host !== 'localhost' && host !== '::1') {
1265
+ // Non-loopback binds require an explicit --allow-remote opt-in. Studio has
1266
+ // only a random session token exposing it on a LAN interface is foot-gun
1267
+ // territory, so we refuse rather than warn-and-proceed.
1268
+ if (!isLoopbackHost(host)) {
1269
+ if (!args.allowRemote) {
1270
+ error(`Studio refuses to bind to ${yellow(host)} without ${cyan('--allow-remote')}.`);
1271
+ newline();
1272
+ console.log(` ${dim('Loopback only by default')} ${dim('(127.0.0.1, localhost, ::1).')}`);
1273
+ console.log(` ${dim('Pass')} ${cyan('--allow-remote')} ${dim('to opt in to network exposure.')}`);
1274
+ newline();
1275
+ process.exit(1);
1276
+ }
1157
1277
  console.log(warn(`Studio is binding to ${yellow(host)} — this is NOT loopback. ` +
1158
1278
  `Anyone on your network who can reach this port + guess the session token can read your database.`));
1159
1279
  }
@@ -1205,6 +1325,19 @@ async function cmdStudio(args, config) {
1205
1325
  });
1206
1326
  }
1207
1327
  // ---------------------------------------------------------------------------
1328
+ // Command: mcp — read-only JSON-RPC stdio server
1329
+ // ---------------------------------------------------------------------------
1330
+ async function cmdMcp(_args, config) {
1331
+ const url = requireUrl(config);
1332
+ await runMcpServer({
1333
+ url,
1334
+ schema: config.schema,
1335
+ migrationsDir: config.migrationsDir,
1336
+ include: config.include.length ? config.include : undefined,
1337
+ exclude: config.exclude.length ? config.exclude : undefined,
1338
+ });
1339
+ }
1340
+ // ---------------------------------------------------------------------------
1208
1341
  // Command: observe
1209
1342
  // ---------------------------------------------------------------------------
1210
1343
  async function cmdObserve(args) {
@@ -1225,9 +1358,17 @@ async function cmdObserve(args) {
1225
1358
  console.log(red(`✗ invalid port: ${args.port}`));
1226
1359
  process.exit(1);
1227
1360
  }
1228
- // Warn loudly when an explicit non-loopback --host is used — the user is
1229
- // opting in, so we proceed rather than refuse.
1230
- if (host !== '127.0.0.1' && host !== 'localhost' && host !== '::1') {
1361
+ // Non-loopback binds require an explicit --allow-remote opt-in (same model
1362
+ // as Studio). Refuse without the flag; warn loudly when opted in.
1363
+ if (!isLoopbackHost(host)) {
1364
+ if (!args.allowRemote) {
1365
+ error(`Observe refuses to bind to ${yellow(host)} without ${cyan('--allow-remote')}.`);
1366
+ newline();
1367
+ console.log(` ${dim('Loopback only by default')} ${dim('(127.0.0.1, localhost, ::1).')}`);
1368
+ console.log(` ${dim('Pass')} ${cyan('--allow-remote')} ${dim('to opt in to network exposure.')}`);
1369
+ newline();
1370
+ process.exit(1);
1371
+ }
1231
1372
  console.log(warn(`Observe is binding to ${yellow(host)} — this is NOT loopback. ` +
1232
1373
  `Anyone on your network who can reach this port + guess the session token can read your metrics.`));
1233
1374
  }
@@ -1278,6 +1419,7 @@ function showSubcommandHelp(command) {
1278
1419
  migration: showMigrateHelp,
1279
1420
  seed: showSeedHelp,
1280
1421
  status: showStatusHelp,
1422
+ mcp: showMcpHelp,
1281
1423
  };
1282
1424
  const fn = helpMap[command];
1283
1425
  if (fn) {
@@ -1312,6 +1454,7 @@ function showGenerateHelp() {
1312
1454
  console.log(` ${dim('•')} ${cyan('types.ts')} — Entity interfaces, Create/Update input types`);
1313
1455
  console.log(` ${dim('•')} ${cyan('metadata.ts')} — Runtime schema metadata`);
1314
1456
  console.log(` ${dim('•')} ${cyan('index.ts')} — Configured client with typed table accessors`);
1457
+ console.log(` ${dim('•')} ${cyan('zod.ts')} — Zod schemas ${dim('(with --zod)')}`);
1315
1458
  newline();
1316
1459
  console.log(` ${bold('Options:')}`);
1317
1460
  console.log(` ${cyan('--url, -u')} ${dim('<url>')} Postgres connection string`);
@@ -1319,6 +1462,8 @@ function showGenerateHelp() {
1319
1462
  console.log(` ${cyan('--schema, -s')} ${dim('<name>')} Postgres schema ${dim('(default: public)')}`);
1320
1463
  console.log(` ${cyan('--include')} ${dim('<tables>')} Comma-separated tables to include`);
1321
1464
  console.log(` ${cyan('--exclude')} ${dim('<tables>')} Comma-separated tables to exclude`);
1465
+ console.log(` ${cyan('--zod')} Also emit ${cyan('zod.ts')} validation schemas ${dim('(needs the zod dep)')}`);
1466
+ console.log(` ${cyan('--include-views')} Include views + materialized views as read-only entities`);
1322
1467
  console.log(` ${cyan('--allow-empty')} Generate even when introspection matches 0 tables`);
1323
1468
  newline();
1324
1469
  }
@@ -1348,6 +1493,7 @@ function showMigrateHelp() {
1348
1493
  console.log(` ${bold('Subcommands:')}`);
1349
1494
  console.log(` ${cyan('create')} ${dim('<name>')} Create a new migration file`);
1350
1495
  console.log(` ${cyan('up')} Apply pending migrations`);
1496
+ console.log(` ${cyan('deploy')} Apply pending migrations without prompts`);
1351
1497
  console.log(` ${cyan('down')} Rollback last migration`);
1352
1498
  console.log(` ${cyan('status')} Show applied/pending migrations`);
1353
1499
  newline();
@@ -1364,6 +1510,7 @@ function showMigrateHelp() {
1364
1510
  console.log(` ${dim('$')} npx turbine migrate create add_users_table`);
1365
1511
  console.log(` ${dim('$')} npx turbine migrate create add_email_index --auto`);
1366
1512
  console.log(` ${dim('$')} npx turbine migrate up`);
1513
+ console.log(` ${dim('$')} npx turbine migrate deploy --dry-run`);
1367
1514
  console.log(` ${dim('$')} npx turbine migrate down --step 2`);
1368
1515
  console.log(` ${dim('$')} npx turbine migrate status`);
1369
1516
  newline();
@@ -1376,7 +1523,9 @@ function showSeedHelp() {
1376
1523
  console.log(` npx turbine seed ${dim('[options]')}`);
1377
1524
  newline();
1378
1525
  console.log(` Runs the seed file specified in ${cyan('turbine.config.ts')}`);
1379
- console.log(` ${dim('(default: ./turbine/seed.ts)')}`);
1526
+ console.log(` ${dim('or the first default candidate: ./seed.ts, ./seed.js, ./seed.sql')}`);
1527
+ newline();
1528
+ console.log(` ${dim('TypeScript seeds run with')} ${cyan('npx tsx')} ${dim('and can export')} ${cyan('defineSeed(fn)')}${dim('.')}`);
1380
1529
  newline();
1381
1530
  console.log(` ${bold('Options:')}`);
1382
1531
  console.log(` ${cyan('--url, -u')} ${dim('<url>')} Postgres connection string`);
@@ -1397,6 +1546,23 @@ function showStatusHelp() {
1397
1546
  console.log(` ${cyan('--schema, -s')} ${dim('<name>')} Postgres schema ${dim('(default: public)')}`);
1398
1547
  newline();
1399
1548
  }
1549
+ function showMcpHelp() {
1550
+ banner();
1551
+ console.log(` ${bold('turbine mcp')} — Start read-only MCP server over stdio`);
1552
+ newline();
1553
+ console.log(` ${bold('Usage:')}`);
1554
+ console.log(` npx turbine mcp ${dim('[options]')}`);
1555
+ newline();
1556
+ console.log(` Speaks newline-delimited JSON-RPC 2.0 on stdin/stdout and exposes`);
1557
+ console.log(` schema, migration status, doctor, EXPLAIN, and sample-row tools.`);
1558
+ newline();
1559
+ console.log(` ${bold('Options:')}`);
1560
+ console.log(` ${cyan('--url, -u')} ${dim('<url>')} Postgres connection string`);
1561
+ console.log(` ${cyan('--schema, -s')} ${dim('<name>')} Postgres schema ${dim('(default: public)')}`);
1562
+ console.log(` ${cyan('--include')} ${dim('<tables>')} Comma-separated tables to include`);
1563
+ console.log(` ${cyan('--exclude')} ${dim('<tables>')} Comma-separated tables to exclude`);
1564
+ newline();
1565
+ }
1400
1566
  // ---------------------------------------------------------------------------
1401
1567
  // Help
1402
1568
  // ---------------------------------------------------------------------------
@@ -1412,12 +1578,14 @@ function showHelp() {
1412
1578
  console.log(` ${cyan('migrate')} ${dim('<sub>')} SQL migration management`);
1413
1579
  console.log(` ${dim('create <name>')} Create a new migration file`);
1414
1580
  console.log(` ${dim('up')} Apply pending migrations`);
1581
+ console.log(` ${dim('deploy')} Apply pending migrations without prompts`);
1415
1582
  console.log(` ${dim('down')} Rollback last migration`);
1416
1583
  console.log(` ${dim('status')} Show applied/pending migrations`);
1417
1584
  console.log(` ${cyan('seed')} Run seed file`);
1418
1585
  console.log(` ${cyan('status')} ${dim('| info')} Show schema summary`);
1419
1586
  console.log(` ${cyan('doctor')} Check relations for missing FK indexes ${dim('(--fix emits migration)')}`);
1420
1587
  console.log(` ${cyan('studio')} Launch local read-only web UI`);
1588
+ console.log(` ${cyan('mcp')} Start read-only MCP server over stdio`);
1421
1589
  console.log(` ${cyan('observe')} Launch metrics dashboard ${dim('(requires TURBINE_OBSERVE_URL)')}`);
1422
1590
  newline();
1423
1591
  console.log(` ${bold('Options:')}`);
@@ -1439,6 +1607,7 @@ function showHelp() {
1439
1607
  console.log(` ${cyan('--port')} ${dim('<n>')} HTTP port ${dim('(default: 4983 studio, 4984 observe)')}`);
1440
1608
  console.log(` ${cyan('--host')} ${dim('<addr>')} Bind address ${dim('(default: 127.0.0.1)')}`);
1441
1609
  console.log(` ${cyan('--no-open')} Don't auto-open the browser`);
1610
+ console.log(` ${cyan('--allow-remote')} Allow non-loopback --host ${dim('(refused without this flag)')}`);
1442
1611
  newline();
1443
1612
  console.log(` ${bold('Config file:')}`);
1444
1613
  console.log(` ${dim('Create')} ${cyan('turbine.config.ts')} ${dim('with')} ${cyan('npx turbine init')}`);
@@ -1449,6 +1618,7 @@ function showHelp() {
1449
1618
  console.log(` ${dim('$')} DATABASE_URL=postgres://... npx turbine generate`);
1450
1619
  console.log(` ${dim('$')} npx turbine migrate create add_users_table`);
1451
1620
  console.log(` ${dim('$')} npx turbine migrate up`);
1621
+ console.log(` ${dim('$')} npx turbine migrate deploy --dry-run`);
1452
1622
  console.log(` ${dim('$')} npx turbine push --dry-run`);
1453
1623
  newline();
1454
1624
  }
@@ -1575,6 +1745,9 @@ async function main() {
1575
1745
  case 'studio':
1576
1746
  await cmdStudio(args, config);
1577
1747
  break;
1748
+ case 'mcp':
1749
+ await cmdMcp(args, config);
1750
+ break;
1578
1751
  case 'observe':
1579
1752
  await cmdObserve(args);
1580
1753
  break;
@@ -1625,4 +1798,29 @@ async function main() {
1625
1798
  process.exit(1);
1626
1799
  }
1627
1800
  }
1628
- main();
1801
+ function isCliEntry() {
1802
+ // Decide from process.argv[1] instead of import.meta.url so the same code
1803
+ // compiles cleanly for both the ESM and CJS builds (see showVersion above).
1804
+ // The CLI runs via the bin shim ("turbine"), the built output
1805
+ // (dist/[cjs/]cli/index.{js,cjs}), or tsx on the source (src/cli/index.ts).
1806
+ // Test files import this module with their own path in argv[1], which never
1807
+ // matches these shapes.
1808
+ const entry = process.argv[1];
1809
+ if (!entry)
1810
+ return false;
1811
+ let real = entry;
1812
+ try {
1813
+ real = realpathSync(entry);
1814
+ }
1815
+ catch {
1816
+ real = resolve(entry);
1817
+ }
1818
+ const base = basename(real);
1819
+ if (base === 'turbine' || base === 'turbine-orm')
1820
+ return true;
1821
+ const isIndexFile = base === 'index.js' || base === 'index.cjs' || base === 'index.ts';
1822
+ return isIndexFile && basename(dirname(real)) === 'cli';
1823
+ }
1824
+ if (isCliEntry()) {
1825
+ void main();
1826
+ }
@@ -0,0 +1,17 @@
1
+ import type { Readable, Writable } from 'node:stream';
2
+ export interface McpServerOptions {
3
+ url: string;
4
+ schema: string;
5
+ migrationsDir: string;
6
+ include?: string[];
7
+ exclude?: string[];
8
+ }
9
+ export interface McpTransport {
10
+ input?: Readable;
11
+ output?: Writable;
12
+ }
13
+ export interface McpServerHandle {
14
+ dispose(): Promise<void>;
15
+ }
16
+ export declare function startMcpServer(options: McpServerOptions, transport?: McpTransport): McpServerHandle;
17
+ export declare function runMcpServer(options: McpServerOptions): Promise<void>;