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.
- package/README.md +17 -13
- package/dist/cjs/cli/config.js +20 -3
- package/dist/cjs/cli/destructive.js +47 -31
- package/dist/cjs/cli/index.js +273 -71
- package/dist/cjs/cli/mcp.js +788 -0
- package/dist/cjs/cli/migrate.js +95 -20
- package/dist/cjs/cli/studio.js +3 -2
- package/dist/cjs/client.js +267 -34
- package/dist/cjs/dialect.js +2 -0
- package/dist/cjs/generate.js +171 -7
- package/dist/cjs/index.js +4 -1
- package/dist/cjs/introspect.js +177 -4
- package/dist/cjs/query/batched-loader.js +148 -0
- package/dist/cjs/query/builder.js +714 -133
- package/dist/cjs/schema-builder.js +59 -4
- package/dist/cjs/schema-sql.js +315 -6
- package/dist/cjs/seed.js +66 -0
- package/dist/cli/config.d.ts +9 -2
- package/dist/cli/config.js +19 -3
- package/dist/cli/destructive.js +47 -31
- package/dist/cli/index.d.ts +52 -1
- package/dist/cli/index.js +272 -74
- package/dist/cli/mcp.d.ts +17 -0
- package/dist/cli/mcp.js +781 -0
- package/dist/cli/migrate.d.ts +37 -0
- package/dist/cli/migrate.js +92 -20
- package/dist/cli/studio.d.ts +3 -2
- package/dist/cli/studio.js +3 -2
- package/dist/client.d.ts +136 -1
- package/dist/client.js +267 -34
- package/dist/dialect.d.ts +17 -0
- package/dist/dialect.js +2 -0
- package/dist/generate.d.ts +17 -0
- package/dist/generate.js +171 -10
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -0
- package/dist/introspect.d.ts +20 -1
- package/dist/introspect.js +175 -4
- package/dist/query/batched-loader.d.ts +29 -2
- package/dist/query/batched-loader.js +148 -1
- package/dist/query/builder.d.ts +156 -8
- package/dist/query/builder.js +715 -134
- package/dist/query/index.d.ts +1 -1
- package/dist/query/types.d.ts +113 -8
- package/dist/schema-builder.d.ts +73 -8
- package/dist/schema-builder.js +59 -4
- package/dist/schema-sql.d.ts +67 -0
- package/dist/schema-sql.js +310 -6
- package/dist/schema.d.ts +53 -0
- package/dist/seed.d.ts +4 -0
- package/dist/seed.js +63 -0
- package/package.json +2 -3
package/dist/cjs/cli/index.js
CHANGED
|
@@ -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 =
|
|
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
|
|
336
|
-
|
|
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)(
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
|
|
354
|
-
|
|
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)(
|
|
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
|
-
|
|
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,
|
|
992
|
-
(0, ui_js_1.label)('Seed file',
|
|
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
|
|
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
|
|
998
|
-
console.log(` ${(0, ui_js_1.dim)('Or set
|
|
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
|
-
|
|
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
|
-
//
|
|
1188
|
-
//
|
|
1189
|
-
//
|
|
1190
|
-
|
|
1191
|
-
|
|
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
|
-
//
|
|
1264
|
-
//
|
|
1265
|
-
if (host
|
|
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)('
|
|
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
|
-
|
|
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
|
+
}
|