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
@@ -9,12 +9,14 @@
9
9
  * turbine push — Apply schema-builder definitions to database
10
10
  * turbine migrate create <name> — Create a new SQL migration file
11
11
  * turbine migrate up — Apply pending migrations
12
+ * turbine migrate deploy — Apply pending migrations without prompts
12
13
  * turbine migrate down — Rollback last migration
13
14
  * turbine migrate status — Show migration status
14
15
  * turbine seed — Run seed file
15
16
  * turbine status — Show schema summary
16
17
  * turbine doctor — Check relations for missing FK indexes (--fix emits migration)
17
18
  * turbine studio — Launch local read-only web UI
19
+ * turbine mcp — Start read-only MCP server over JSON-RPC stdio
18
20
  * turbine observe — Launch metrics dashboard (requires TURBINE_OBSERVE_URL)
19
21
  *
20
22
  * Usage:
@@ -56,6 +58,10 @@ var __importStar = (this && this.__importStar) || (function () {
56
58
  };
57
59
  })();
58
60
  Object.defineProperty(exports, "__esModule", { value: true });
61
+ exports.parseArgs = parseArgs;
62
+ exports.buildMigrateDeployOptions = buildMigrateDeployOptions;
63
+ exports.getSeedExecutionPlan = getSeedExecutionPlan;
64
+ exports.isLoopbackHost = isLoopbackHost;
59
65
  const node_fs_1 = require("node:fs");
60
66
  const node_path_1 = require("node:path");
61
67
  const node_url_1 = require("node:url");
@@ -65,12 +71,13 @@ const introspect_js_1 = require("../introspect.js");
65
71
  const schema_sql_js_1 = require("../schema-sql.js");
66
72
  const config_js_1 = require("./config.js");
67
73
  const loader_js_1 = require("./loader.js");
74
+ const mcp_js_1 = require("./mcp.js");
68
75
  const migrate_js_1 = require("./migrate.js");
69
76
  const observe_js_1 = require("./observe.js");
70
77
  const studio_js_1 = require("./studio.js");
71
78
  const ui_js_1 = require("./ui.js");
72
- function parseArgs() {
73
- const args = process.argv.slice(2);
79
+ function parseArgs(argv = process.argv.slice(2)) {
80
+ const args = argv;
74
81
  const result = {
75
82
  command: args[0] ?? 'help',
76
83
  positional: [],
@@ -128,6 +135,12 @@ function parseArgs() {
128
135
  case '--fix':
129
136
  result.fix = true;
130
137
  break;
138
+ case '--zod':
139
+ result.zod = true;
140
+ break;
141
+ case '--include-views':
142
+ result.includeViews = true;
143
+ break;
131
144
  case '--allow-destructive':
132
145
  result.allowDestructive = true;
133
146
  break;
@@ -154,6 +167,9 @@ function parseArgs() {
154
167
  case '--no-open':
155
168
  result.noOpen = true;
156
169
  break;
170
+ case '--allow-remote':
171
+ result.allowRemote = true;
172
+ break;
157
173
  default:
158
174
  if (!arg.startsWith('-')) {
159
175
  result.positional.push(arg);
@@ -332,34 +348,30 @@ async function cmdInit(args, config) {
332
348
  (0, ui_js_1.success)(`Created ${(0, ui_js_1.cyan)(`${config.out}/`)}`);
333
349
  }
334
350
  // Create seed file template
335
- const seedDir = config.seedFile.substring(0, config.seedFile.lastIndexOf('/'));
336
- if (!(0, node_fs_1.existsSync)(config.seedFile)) {
351
+ const initSeedFile = config.seedFile ?? './seed.ts';
352
+ const seedDir = (0, node_path_1.dirname)(initSeedFile);
353
+ if (!(0, node_fs_1.existsSync)(initSeedFile)) {
337
354
  if (!(0, node_fs_1.existsSync)(seedDir)) {
338
355
  (0, node_fs_1.mkdirSync)(seedDir, { recursive: true });
339
356
  }
340
- (0, node_fs_1.writeFileSync)(config.seedFile, `/**
357
+ (0, node_fs_1.writeFileSync)(initSeedFile, `/**
341
358
  * Turbine seed file
342
359
  *
343
360
  * Run with: npx turbine seed
344
361
  */
345
362
 
346
- // import { turbine } from '${config.out.replace('./', '')}';
347
- //
348
- // const db = turbine({ connectionString: process.env.DATABASE_URL });
349
- //
350
- // async function seed() {
351
- // console.log('Seeding database...');
352
- //
353
- // // Add your seed data here:
354
- // // await db.users.create({ data: { email: 'admin@example.com', name: 'Admin' } });
355
- //
356
- // console.log('Done!');
357
- // await db.disconnect();
358
- // }
359
- //
360
- // seed();
363
+ import { defineSeed } from 'turbine-orm';
364
+
365
+ export default defineSeed(async (db) => {
366
+ console.log('Seeding database...');
367
+
368
+ // Add your seed data here:
369
+ // await db.raw\`INSERT INTO users (email, name) VALUES (\${'admin@example.com'}, \${'Admin'})\`;
370
+
371
+ console.log('Done!');
372
+ });
361
373
  `, 'utf-8');
362
- (0, ui_js_1.success)(`Created ${(0, ui_js_1.cyan)(config.seedFile)}`);
374
+ (0, ui_js_1.success)(`Created ${(0, ui_js_1.cyan)(initSeedFile)}`);
363
375
  }
364
376
  // Create schema builder template
365
377
  if (!(0, node_fs_1.existsSync)(config.schemaFile)) {
@@ -501,6 +513,7 @@ async function cmdGenerate(args, config) {
501
513
  schema: config.schema,
502
514
  include: config.include.length ? config.include : undefined,
503
515
  exclude: config.exclude.length ? config.exclude : undefined,
516
+ includeViews: args.includeViews,
504
517
  });
505
518
  const tableNames = Object.keys(schema.tables);
506
519
  const totalColumns = Object.values(schema.tables).reduce((sum, t) => sum + t.columns.length, 0);
@@ -543,6 +556,7 @@ async function cmdGenerate(args, config) {
543
556
  schema,
544
557
  outDir: config.out,
545
558
  connectionString: url,
559
+ zod: args.zod,
546
560
  });
547
561
  genSpinner.succeed(`Generated ${(0, ui_js_1.bold)(String(result.files.length))} files in ${(0, ui_js_1.elapsed)(startTime)}`);
548
562
  // List files
@@ -656,6 +670,7 @@ async function cmdMigrate(args, config) {
656
670
  console.log(` ${(0, ui_js_1.cyan)('create <name>')} Create a new migration file`);
657
671
  console.log(` ${(0, ui_js_1.cyan)('create <name> --auto')} Auto-generate from schema diff`);
658
672
  console.log(` ${(0, ui_js_1.cyan)('up')} Apply pending migrations`);
673
+ console.log(` ${(0, ui_js_1.cyan)('deploy')} Apply pending migrations without prompts`);
659
674
  console.log(` ${(0, ui_js_1.cyan)('down')} Rollback last migration`);
660
675
  console.log(` ${(0, ui_js_1.cyan)('status')} Show migration status`);
661
676
  (0, ui_js_1.newline)();
@@ -669,6 +684,7 @@ async function cmdMigrate(args, config) {
669
684
  console.log(` ${(0, ui_js_1.dim)('npx turbine migrate create add_users_table')}`);
670
685
  console.log(` ${(0, ui_js_1.dim)('npx turbine migrate create add_email_index --auto')}`);
671
686
  console.log(` ${(0, ui_js_1.dim)('npx turbine migrate up')}`);
687
+ console.log(` ${(0, ui_js_1.dim)('npx turbine migrate deploy --dry-run')}`);
672
688
  console.log(` ${(0, ui_js_1.dim)('npx turbine migrate down --step 2')}`);
673
689
  (0, ui_js_1.newline)();
674
690
  return;
@@ -680,6 +696,9 @@ async function cmdMigrate(args, config) {
680
696
  case 'up':
681
697
  await cmdMigrateUp(args, config);
682
698
  break;
699
+ case 'deploy':
700
+ await cmdMigrateDeploy(args, config);
701
+ break;
683
702
  case 'down':
684
703
  await cmdMigrateDown(args, config);
685
704
  break;
@@ -838,6 +857,67 @@ async function cmdMigrateUp(args, config) {
838
857
  }
839
858
  (0, ui_js_1.newline)();
840
859
  }
860
+ function buildMigrateDeployOptions(_args) {
861
+ return {
862
+ allowDrift: false,
863
+ allowDestructive: true,
864
+ step: undefined,
865
+ };
866
+ }
867
+ async function cmdMigrateDeploy(args, config) {
868
+ (0, ui_js_1.banner)();
869
+ const url = requireUrl(config);
870
+ (0, ui_js_1.label)('Database', (0, ui_js_1.redactUrl)(url));
871
+ (0, ui_js_1.label)('Migrations', config.migrationsDir);
872
+ (0, ui_js_1.newline)();
873
+ if (args.dryRun) {
874
+ const spinner = new ui_js_1.Spinner('Checking pending migrations').start();
875
+ const plan = await (0, migrate_js_1.inspectMigrationDeploy)(url, config.migrationsDir);
876
+ if (plan.mismatches.length > 0) {
877
+ spinner.fail('Deploy blocked by migration drift');
878
+ for (const mismatch of plan.mismatches) {
879
+ const reason = mismatch.type === 'missing' ? 'deleted from disk' : 'modified on disk';
880
+ console.log(` ${(0, ui_js_1.red)(ui_js_1.symbols.cross)} ${mismatch.name}.sql ${(0, ui_js_1.dim)(`(${reason})`)}`);
881
+ }
882
+ (0, ui_js_1.newline)();
883
+ process.exit(1);
884
+ }
885
+ if (plan.pending.length === 0) {
886
+ spinner.succeed('No pending migrations');
887
+ (0, ui_js_1.newline)();
888
+ return;
889
+ }
890
+ spinner.succeed(`${(0, ui_js_1.bold)(String(plan.pending.length))} pending migration(s)`);
891
+ for (const file of plan.pending) {
892
+ console.log(` ${(0, ui_js_1.yellow)(ui_js_1.symbols.dot)} ${file.filename}`);
893
+ }
894
+ (0, ui_js_1.newline)();
895
+ return;
896
+ }
897
+ const spinner = new ui_js_1.Spinner('Deploying migrations').start();
898
+ const result = await (0, migrate_js_1.migrateDeploy)(url, config.migrationsDir);
899
+ if (result.applied.length === 0 && result.errors.length === 0) {
900
+ spinner.succeed('0 applied — all migrations are up to date');
901
+ (0, ui_js_1.newline)();
902
+ return;
903
+ }
904
+ if (result.applied.length > 0) {
905
+ spinner.succeed(`${(0, ui_js_1.bold)(String(result.applied.length))} applied`);
906
+ for (const file of result.applied) {
907
+ console.log(` ${(0, ui_js_1.green)(ui_js_1.symbols.check)} ${file.filename}`);
908
+ }
909
+ }
910
+ if (result.errors.length > 0) {
911
+ spinner.fail('Deploy failed');
912
+ for (const { file, error: msg } of result.errors) {
913
+ console.log(` ${(0, ui_js_1.red)(ui_js_1.symbols.cross)} ${file.filename}`);
914
+ console.log(` ${(0, ui_js_1.dim)(msg)}`);
915
+ }
916
+ (0, ui_js_1.newline)();
917
+ process.exit(1);
918
+ }
919
+ (0, ui_js_1.newline)();
920
+ }
841
921
  /** True when the error is migrate up/down's destructive-statement refusal. */
842
922
  function isDestructiveRefusal(err) {
843
923
  return err instanceof Error && err.message.includes('DESTRUCTIVE');
@@ -983,55 +1063,81 @@ async function cmdMigrateStatus(_args, config) {
983
1063
  (0, ui_js_1.newline)();
984
1064
  }
985
1065
  }
986
- // ---------------------------------------------------------------------------
987
- // Command: seed
988
- // ---------------------------------------------------------------------------
1066
+ function getSeedExecutionPlan(seedFile) {
1067
+ const ext = (0, node_path_1.extname)(seedFile).toLowerCase();
1068
+ if (ext === '.ts' || ext === '.mts' || ext === '.cts') {
1069
+ return { kind: 'tsx', command: 'npx', args: ['tsx', seedFile] };
1070
+ }
1071
+ if (ext === '.js' || ext === '.mjs' || ext === '.cjs') {
1072
+ return { kind: 'js', file: seedFile };
1073
+ }
1074
+ if (ext === '.sql') {
1075
+ return { kind: 'sql', file: seedFile };
1076
+ }
1077
+ throw new Error(`Unsupported seed file extension: ${ext || '(none)'}. Use seed.ts, seed.js, or seed.sql.`);
1078
+ }
1079
+ async function runSeedPlan(plan, config) {
1080
+ const oldDatabaseUrl = process.env.DATABASE_URL;
1081
+ if (config.url)
1082
+ process.env.DATABASE_URL = config.url;
1083
+ try {
1084
+ if (plan.kind === 'tsx') {
1085
+ if (!(0, loader_js_1.canResolveTsx)()) {
1086
+ throw new Error('TypeScript seed files require tsx — install tsx or use seed.js/seed.sql.');
1087
+ }
1088
+ const { execFileSync } = await Promise.resolve().then(() => __importStar(require('node:child_process')));
1089
+ execFileSync(plan.command, plan.args, {
1090
+ stdio: 'inherit',
1091
+ env: {
1092
+ ...process.env,
1093
+ DATABASE_URL: config.url || process.env.DATABASE_URL,
1094
+ },
1095
+ });
1096
+ return;
1097
+ }
1098
+ if (plan.kind === 'js') {
1099
+ const mod = await Promise.resolve(`${(0, node_url_1.pathToFileURL)(plan.file).href}`).then(s => __importStar(require(s)));
1100
+ if (typeof mod.default === 'function') {
1101
+ await mod.default();
1102
+ }
1103
+ return;
1104
+ }
1105
+ const url = requireUrl(config);
1106
+ const { default: pg } = await Promise.resolve().then(() => __importStar(require('pg')));
1107
+ const client = new pg.Client({ connectionString: url });
1108
+ await client.connect();
1109
+ try {
1110
+ await client.query((0, node_fs_1.readFileSync)(plan.file, 'utf-8'));
1111
+ }
1112
+ finally {
1113
+ await client.end();
1114
+ }
1115
+ }
1116
+ finally {
1117
+ if (oldDatabaseUrl === undefined) {
1118
+ delete process.env.DATABASE_URL;
1119
+ }
1120
+ else {
1121
+ process.env.DATABASE_URL = oldDatabaseUrl;
1122
+ }
1123
+ }
1124
+ }
989
1125
  async function cmdSeed(_args, config) {
990
1126
  (0, ui_js_1.banner)();
991
- const seedFile = (0, node_path_1.resolve)(config.seedFile);
992
- (0, ui_js_1.label)('Seed file', config.seedFile);
1127
+ const seedFile = (0, config_js_1.resolveSeedFile)(config);
1128
+ (0, ui_js_1.label)('Seed file', seedFile ? (0, node_path_1.relative)(process.cwd(), seedFile) || seedFile : '(not found)');
993
1129
  (0, ui_js_1.newline)();
994
- if (!(0, node_fs_1.existsSync)(seedFile)) {
995
- (0, ui_js_1.error)(`Seed file not found: ${config.seedFile}`);
1130
+ if (!seedFile || !(0, node_fs_1.existsSync)(seedFile)) {
1131
+ (0, ui_js_1.error)(`Seed file not found.`);
996
1132
  (0, ui_js_1.newline)();
997
- console.log(` ${(0, ui_js_1.dim)('Create one with:')} ${(0, ui_js_1.cyan)('npx turbine init')}`);
998
- console.log(` ${(0, ui_js_1.dim)('Or set a custom path in')} ${(0, ui_js_1.cyan)('turbine.config.ts')}`);
1133
+ console.log(` ${(0, ui_js_1.dim)('Create one of:')} ${(0, ui_js_1.cyan)('seed.ts')}${(0, ui_js_1.dim)(',')} ${(0, ui_js_1.cyan)('seed.js')}${(0, ui_js_1.dim)(',')} ${(0, ui_js_1.cyan)('seed.sql')}`);
1134
+ console.log(` ${(0, ui_js_1.dim)('Or set')} ${(0, ui_js_1.cyan)('seed')} ${(0, ui_js_1.dim)('in')} ${(0, ui_js_1.cyan)('turbine.config.ts')}`);
999
1135
  (0, ui_js_1.newline)();
1000
1136
  process.exit(1);
1001
1137
  }
1002
1138
  const spinner = new ui_js_1.Spinner('Running seed file').start();
1003
1139
  try {
1004
- // Use child_process to run the seed file via tsx or node
1005
- const { execFileSync } = await Promise.resolve().then(() => __importStar(require('node:child_process')));
1006
- // Try tsx first (most compatible with .ts files), fall back to node --experimental-strip-types
1007
- const runners = [
1008
- { cmd: 'npx', args: ['tsx', seedFile], name: 'tsx' },
1009
- { cmd: 'node', args: ['--experimental-strip-types', seedFile], name: 'node' },
1010
- ];
1011
- let ran = false;
1012
- for (const runner of runners) {
1013
- try {
1014
- execFileSync(runner.cmd, runner.args, {
1015
- stdio: 'inherit',
1016
- env: {
1017
- ...process.env,
1018
- DATABASE_URL: config.url || process.env.DATABASE_URL,
1019
- },
1020
- });
1021
- ran = true;
1022
- break;
1023
- }
1024
- catch (err) {
1025
- // If tsx not found, try next runner
1026
- if (err instanceof Error && 'status' in err && err.status === null) {
1027
- continue;
1028
- }
1029
- throw err;
1030
- }
1031
- }
1032
- if (!ran) {
1033
- throw new Error('Could not find tsx or compatible Node.js version to run .ts files');
1034
- }
1140
+ await runSeedPlan(getSeedExecutionPlan(seedFile), config);
1035
1141
  spinner.succeed('Seed completed');
1036
1142
  }
1037
1143
  catch (err) {
@@ -1172,6 +1278,17 @@ async function cmdDoctor(args, config) {
1172
1278
  }
1173
1279
  }
1174
1280
  // ---------------------------------------------------------------------------
1281
+ // Loopback host gate (Studio / Observe)
1282
+ // ---------------------------------------------------------------------------
1283
+ /**
1284
+ * True when `host` is a loopback address Studio/Observe may bind without
1285
+ * `--allow-remote`. Accepts IPv4, IPv6, and the common bracket form.
1286
+ */
1287
+ function isLoopbackHost(host) {
1288
+ const h = host.trim().toLowerCase();
1289
+ return h === '127.0.0.1' || h === 'localhost' || h === '::1' || h === '[::1]';
1290
+ }
1291
+ // ---------------------------------------------------------------------------
1175
1292
  // Command: studio — local read-only web UI
1176
1293
  // ---------------------------------------------------------------------------
1177
1294
  async function cmdStudio(args, config) {
@@ -1184,11 +1301,18 @@ async function cmdStudio(args, config) {
1184
1301
  console.log((0, ui_js_1.red)(`✗ invalid port: ${args.port}`));
1185
1302
  process.exit(1);
1186
1303
  }
1187
- // Warn loudly when an explicit non-loopback --host is used — the user is
1188
- // opting in, so we proceed rather than refuse. Studio has no real
1189
- // authentication beyond a random session token, so exposing it on a LAN
1190
- // interface is foot-gun territory.
1191
- if (host !== '127.0.0.1' && host !== 'localhost' && host !== '::1') {
1304
+ // Non-loopback binds require an explicit --allow-remote opt-in. Studio has
1305
+ // only a random session token exposing it on a LAN interface is foot-gun
1306
+ // territory, so we refuse rather than warn-and-proceed.
1307
+ if (!isLoopbackHost(host)) {
1308
+ if (!args.allowRemote) {
1309
+ (0, ui_js_1.error)(`Studio refuses to bind to ${(0, ui_js_1.yellow)(host)} without ${(0, ui_js_1.cyan)('--allow-remote')}.`);
1310
+ (0, ui_js_1.newline)();
1311
+ console.log(` ${(0, ui_js_1.dim)('Loopback only by default')} ${(0, ui_js_1.dim)('(127.0.0.1, localhost, ::1).')}`);
1312
+ console.log(` ${(0, ui_js_1.dim)('Pass')} ${(0, ui_js_1.cyan)('--allow-remote')} ${(0, ui_js_1.dim)('to opt in to network exposure.')}`);
1313
+ (0, ui_js_1.newline)();
1314
+ process.exit(1);
1315
+ }
1192
1316
  console.log((0, ui_js_1.warn)(`Studio is binding to ${(0, ui_js_1.yellow)(host)} — this is NOT loopback. ` +
1193
1317
  `Anyone on your network who can reach this port + guess the session token can read your database.`));
1194
1318
  }
@@ -1240,6 +1364,19 @@ async function cmdStudio(args, config) {
1240
1364
  });
1241
1365
  }
1242
1366
  // ---------------------------------------------------------------------------
1367
+ // Command: mcp — read-only JSON-RPC stdio server
1368
+ // ---------------------------------------------------------------------------
1369
+ async function cmdMcp(_args, config) {
1370
+ const url = requireUrl(config);
1371
+ await (0, mcp_js_1.runMcpServer)({
1372
+ url,
1373
+ schema: config.schema,
1374
+ migrationsDir: config.migrationsDir,
1375
+ include: config.include.length ? config.include : undefined,
1376
+ exclude: config.exclude.length ? config.exclude : undefined,
1377
+ });
1378
+ }
1379
+ // ---------------------------------------------------------------------------
1243
1380
  // Command: observe
1244
1381
  // ---------------------------------------------------------------------------
1245
1382
  async function cmdObserve(args) {
@@ -1260,9 +1397,17 @@ async function cmdObserve(args) {
1260
1397
  console.log((0, ui_js_1.red)(`✗ invalid port: ${args.port}`));
1261
1398
  process.exit(1);
1262
1399
  }
1263
- // Warn loudly when an explicit non-loopback --host is used — the user is
1264
- // opting in, so we proceed rather than refuse.
1265
- if (host !== '127.0.0.1' && host !== 'localhost' && host !== '::1') {
1400
+ // Non-loopback binds require an explicit --allow-remote opt-in (same model
1401
+ // as Studio). Refuse without the flag; warn loudly when opted in.
1402
+ if (!isLoopbackHost(host)) {
1403
+ if (!args.allowRemote) {
1404
+ (0, ui_js_1.error)(`Observe refuses to bind to ${(0, ui_js_1.yellow)(host)} without ${(0, ui_js_1.cyan)('--allow-remote')}.`);
1405
+ (0, ui_js_1.newline)();
1406
+ console.log(` ${(0, ui_js_1.dim)('Loopback only by default')} ${(0, ui_js_1.dim)('(127.0.0.1, localhost, ::1).')}`);
1407
+ console.log(` ${(0, ui_js_1.dim)('Pass')} ${(0, ui_js_1.cyan)('--allow-remote')} ${(0, ui_js_1.dim)('to opt in to network exposure.')}`);
1408
+ (0, ui_js_1.newline)();
1409
+ process.exit(1);
1410
+ }
1266
1411
  console.log((0, ui_js_1.warn)(`Observe is binding to ${(0, ui_js_1.yellow)(host)} — this is NOT loopback. ` +
1267
1412
  `Anyone on your network who can reach this port + guess the session token can read your metrics.`));
1268
1413
  }
@@ -1313,6 +1458,7 @@ function showSubcommandHelp(command) {
1313
1458
  migration: showMigrateHelp,
1314
1459
  seed: showSeedHelp,
1315
1460
  status: showStatusHelp,
1461
+ mcp: showMcpHelp,
1316
1462
  };
1317
1463
  const fn = helpMap[command];
1318
1464
  if (fn) {
@@ -1347,6 +1493,7 @@ function showGenerateHelp() {
1347
1493
  console.log(` ${(0, ui_js_1.dim)('•')} ${(0, ui_js_1.cyan)('types.ts')} — Entity interfaces, Create/Update input types`);
1348
1494
  console.log(` ${(0, ui_js_1.dim)('•')} ${(0, ui_js_1.cyan)('metadata.ts')} — Runtime schema metadata`);
1349
1495
  console.log(` ${(0, ui_js_1.dim)('•')} ${(0, ui_js_1.cyan)('index.ts')} — Configured client with typed table accessors`);
1496
+ console.log(` ${(0, ui_js_1.dim)('•')} ${(0, ui_js_1.cyan)('zod.ts')} — Zod schemas ${(0, ui_js_1.dim)('(with --zod)')}`);
1350
1497
  (0, ui_js_1.newline)();
1351
1498
  console.log(` ${(0, ui_js_1.bold)('Options:')}`);
1352
1499
  console.log(` ${(0, ui_js_1.cyan)('--url, -u')} ${(0, ui_js_1.dim)('<url>')} Postgres connection string`);
@@ -1354,6 +1501,8 @@ function showGenerateHelp() {
1354
1501
  console.log(` ${(0, ui_js_1.cyan)('--schema, -s')} ${(0, ui_js_1.dim)('<name>')} Postgres schema ${(0, ui_js_1.dim)('(default: public)')}`);
1355
1502
  console.log(` ${(0, ui_js_1.cyan)('--include')} ${(0, ui_js_1.dim)('<tables>')} Comma-separated tables to include`);
1356
1503
  console.log(` ${(0, ui_js_1.cyan)('--exclude')} ${(0, ui_js_1.dim)('<tables>')} Comma-separated tables to exclude`);
1504
+ console.log(` ${(0, ui_js_1.cyan)('--zod')} Also emit ${(0, ui_js_1.cyan)('zod.ts')} validation schemas ${(0, ui_js_1.dim)('(needs the zod dep)')}`);
1505
+ console.log(` ${(0, ui_js_1.cyan)('--include-views')} Include views + materialized views as read-only entities`);
1357
1506
  console.log(` ${(0, ui_js_1.cyan)('--allow-empty')} Generate even when introspection matches 0 tables`);
1358
1507
  (0, ui_js_1.newline)();
1359
1508
  }
@@ -1383,6 +1532,7 @@ function showMigrateHelp() {
1383
1532
  console.log(` ${(0, ui_js_1.bold)('Subcommands:')}`);
1384
1533
  console.log(` ${(0, ui_js_1.cyan)('create')} ${(0, ui_js_1.dim)('<name>')} Create a new migration file`);
1385
1534
  console.log(` ${(0, ui_js_1.cyan)('up')} Apply pending migrations`);
1535
+ console.log(` ${(0, ui_js_1.cyan)('deploy')} Apply pending migrations without prompts`);
1386
1536
  console.log(` ${(0, ui_js_1.cyan)('down')} Rollback last migration`);
1387
1537
  console.log(` ${(0, ui_js_1.cyan)('status')} Show applied/pending migrations`);
1388
1538
  (0, ui_js_1.newline)();
@@ -1399,6 +1549,7 @@ function showMigrateHelp() {
1399
1549
  console.log(` ${(0, ui_js_1.dim)('$')} npx turbine migrate create add_users_table`);
1400
1550
  console.log(` ${(0, ui_js_1.dim)('$')} npx turbine migrate create add_email_index --auto`);
1401
1551
  console.log(` ${(0, ui_js_1.dim)('$')} npx turbine migrate up`);
1552
+ console.log(` ${(0, ui_js_1.dim)('$')} npx turbine migrate deploy --dry-run`);
1402
1553
  console.log(` ${(0, ui_js_1.dim)('$')} npx turbine migrate down --step 2`);
1403
1554
  console.log(` ${(0, ui_js_1.dim)('$')} npx turbine migrate status`);
1404
1555
  (0, ui_js_1.newline)();
@@ -1411,7 +1562,9 @@ function showSeedHelp() {
1411
1562
  console.log(` npx turbine seed ${(0, ui_js_1.dim)('[options]')}`);
1412
1563
  (0, ui_js_1.newline)();
1413
1564
  console.log(` Runs the seed file specified in ${(0, ui_js_1.cyan)('turbine.config.ts')}`);
1414
- console.log(` ${(0, ui_js_1.dim)('(default: ./turbine/seed.ts)')}`);
1565
+ console.log(` ${(0, ui_js_1.dim)('or the first default candidate: ./seed.ts, ./seed.js, ./seed.sql')}`);
1566
+ (0, ui_js_1.newline)();
1567
+ console.log(` ${(0, ui_js_1.dim)('TypeScript seeds run with')} ${(0, ui_js_1.cyan)('npx tsx')} ${(0, ui_js_1.dim)('and can export')} ${(0, ui_js_1.cyan)('defineSeed(fn)')}${(0, ui_js_1.dim)('.')}`);
1415
1568
  (0, ui_js_1.newline)();
1416
1569
  console.log(` ${(0, ui_js_1.bold)('Options:')}`);
1417
1570
  console.log(` ${(0, ui_js_1.cyan)('--url, -u')} ${(0, ui_js_1.dim)('<url>')} Postgres connection string`);
@@ -1432,6 +1585,23 @@ function showStatusHelp() {
1432
1585
  console.log(` ${(0, ui_js_1.cyan)('--schema, -s')} ${(0, ui_js_1.dim)('<name>')} Postgres schema ${(0, ui_js_1.dim)('(default: public)')}`);
1433
1586
  (0, ui_js_1.newline)();
1434
1587
  }
1588
+ function showMcpHelp() {
1589
+ (0, ui_js_1.banner)();
1590
+ console.log(` ${(0, ui_js_1.bold)('turbine mcp')} — Start read-only MCP server over stdio`);
1591
+ (0, ui_js_1.newline)();
1592
+ console.log(` ${(0, ui_js_1.bold)('Usage:')}`);
1593
+ console.log(` npx turbine mcp ${(0, ui_js_1.dim)('[options]')}`);
1594
+ (0, ui_js_1.newline)();
1595
+ console.log(` Speaks newline-delimited JSON-RPC 2.0 on stdin/stdout and exposes`);
1596
+ console.log(` schema, migration status, doctor, EXPLAIN, and sample-row tools.`);
1597
+ (0, ui_js_1.newline)();
1598
+ console.log(` ${(0, ui_js_1.bold)('Options:')}`);
1599
+ console.log(` ${(0, ui_js_1.cyan)('--url, -u')} ${(0, ui_js_1.dim)('<url>')} Postgres connection string`);
1600
+ console.log(` ${(0, ui_js_1.cyan)('--schema, -s')} ${(0, ui_js_1.dim)('<name>')} Postgres schema ${(0, ui_js_1.dim)('(default: public)')}`);
1601
+ console.log(` ${(0, ui_js_1.cyan)('--include')} ${(0, ui_js_1.dim)('<tables>')} Comma-separated tables to include`);
1602
+ console.log(` ${(0, ui_js_1.cyan)('--exclude')} ${(0, ui_js_1.dim)('<tables>')} Comma-separated tables to exclude`);
1603
+ (0, ui_js_1.newline)();
1604
+ }
1435
1605
  // ---------------------------------------------------------------------------
1436
1606
  // Help
1437
1607
  // ---------------------------------------------------------------------------
@@ -1447,12 +1617,14 @@ function showHelp() {
1447
1617
  console.log(` ${(0, ui_js_1.cyan)('migrate')} ${(0, ui_js_1.dim)('<sub>')} SQL migration management`);
1448
1618
  console.log(` ${(0, ui_js_1.dim)('create <name>')} Create a new migration file`);
1449
1619
  console.log(` ${(0, ui_js_1.dim)('up')} Apply pending migrations`);
1620
+ console.log(` ${(0, ui_js_1.dim)('deploy')} Apply pending migrations without prompts`);
1450
1621
  console.log(` ${(0, ui_js_1.dim)('down')} Rollback last migration`);
1451
1622
  console.log(` ${(0, ui_js_1.dim)('status')} Show applied/pending migrations`);
1452
1623
  console.log(` ${(0, ui_js_1.cyan)('seed')} Run seed file`);
1453
1624
  console.log(` ${(0, ui_js_1.cyan)('status')} ${(0, ui_js_1.dim)('| info')} Show schema summary`);
1454
1625
  console.log(` ${(0, ui_js_1.cyan)('doctor')} Check relations for missing FK indexes ${(0, ui_js_1.dim)('(--fix emits migration)')}`);
1455
1626
  console.log(` ${(0, ui_js_1.cyan)('studio')} Launch local read-only web UI`);
1627
+ console.log(` ${(0, ui_js_1.cyan)('mcp')} Start read-only MCP server over stdio`);
1456
1628
  console.log(` ${(0, ui_js_1.cyan)('observe')} Launch metrics dashboard ${(0, ui_js_1.dim)('(requires TURBINE_OBSERVE_URL)')}`);
1457
1629
  (0, ui_js_1.newline)();
1458
1630
  console.log(` ${(0, ui_js_1.bold)('Options:')}`);
@@ -1474,6 +1646,7 @@ function showHelp() {
1474
1646
  console.log(` ${(0, ui_js_1.cyan)('--port')} ${(0, ui_js_1.dim)('<n>')} HTTP port ${(0, ui_js_1.dim)('(default: 4983 studio, 4984 observe)')}`);
1475
1647
  console.log(` ${(0, ui_js_1.cyan)('--host')} ${(0, ui_js_1.dim)('<addr>')} Bind address ${(0, ui_js_1.dim)('(default: 127.0.0.1)')}`);
1476
1648
  console.log(` ${(0, ui_js_1.cyan)('--no-open')} Don't auto-open the browser`);
1649
+ console.log(` ${(0, ui_js_1.cyan)('--allow-remote')} Allow non-loopback --host ${(0, ui_js_1.dim)('(refused without this flag)')}`);
1477
1650
  (0, ui_js_1.newline)();
1478
1651
  console.log(` ${(0, ui_js_1.bold)('Config file:')}`);
1479
1652
  console.log(` ${(0, ui_js_1.dim)('Create')} ${(0, ui_js_1.cyan)('turbine.config.ts')} ${(0, ui_js_1.dim)('with')} ${(0, ui_js_1.cyan)('npx turbine init')}`);
@@ -1484,6 +1657,7 @@ function showHelp() {
1484
1657
  console.log(` ${(0, ui_js_1.dim)('$')} DATABASE_URL=postgres://... npx turbine generate`);
1485
1658
  console.log(` ${(0, ui_js_1.dim)('$')} npx turbine migrate create add_users_table`);
1486
1659
  console.log(` ${(0, ui_js_1.dim)('$')} npx turbine migrate up`);
1660
+ console.log(` ${(0, ui_js_1.dim)('$')} npx turbine migrate deploy --dry-run`);
1487
1661
  console.log(` ${(0, ui_js_1.dim)('$')} npx turbine push --dry-run`);
1488
1662
  (0, ui_js_1.newline)();
1489
1663
  }
@@ -1610,6 +1784,9 @@ async function main() {
1610
1784
  case 'studio':
1611
1785
  await cmdStudio(args, config);
1612
1786
  break;
1787
+ case 'mcp':
1788
+ await cmdMcp(args, config);
1789
+ break;
1613
1790
  case 'observe':
1614
1791
  await cmdObserve(args);
1615
1792
  break;
@@ -1660,4 +1837,29 @@ async function main() {
1660
1837
  process.exit(1);
1661
1838
  }
1662
1839
  }
1663
- main();
1840
+ function isCliEntry() {
1841
+ // Decide from process.argv[1] instead of import.meta.url so the same code
1842
+ // compiles cleanly for both the ESM and CJS builds (see showVersion above).
1843
+ // The CLI runs via the bin shim ("turbine"), the built output
1844
+ // (dist/[cjs/]cli/index.{js,cjs}), or tsx on the source (src/cli/index.ts).
1845
+ // Test files import this module with their own path in argv[1], which never
1846
+ // matches these shapes.
1847
+ const entry = process.argv[1];
1848
+ if (!entry)
1849
+ return false;
1850
+ let real = entry;
1851
+ try {
1852
+ real = (0, node_fs_1.realpathSync)(entry);
1853
+ }
1854
+ catch {
1855
+ real = (0, node_path_1.resolve)(entry);
1856
+ }
1857
+ const base = (0, node_path_1.basename)(real);
1858
+ if (base === 'turbine' || base === 'turbine-orm')
1859
+ return true;
1860
+ const isIndexFile = base === 'index.js' || base === 'index.cjs' || base === 'index.ts';
1861
+ return isIndexFile && (0, node_path_1.basename)((0, node_path_1.dirname)(real)) === 'cli';
1862
+ }
1863
+ if (isCliEntry()) {
1864
+ void main();
1865
+ }