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/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 {
|
|
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 =
|
|
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
|
|
301
|
-
|
|
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(
|
|
318
|
+
writeFileSync(initSeedFile, `/**
|
|
306
319
|
* Turbine seed file
|
|
307
320
|
*
|
|
308
321
|
* Run with: npx turbine seed
|
|
309
322
|
*/
|
|
310
323
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
|
|
319
|
-
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
957
|
-
label('Seed file',
|
|
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
|
|
1091
|
+
if (!seedFile || !existsSync(seedFile)) {
|
|
1092
|
+
error(`Seed file not found.`);
|
|
961
1093
|
newline();
|
|
962
|
-
console.log(` ${dim('Create one
|
|
963
|
-
console.log(` ${dim('Or set
|
|
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
|
-
|
|
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
|
-
//
|
|
1153
|
-
//
|
|
1154
|
-
//
|
|
1155
|
-
|
|
1156
|
-
|
|
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
|
-
//
|
|
1229
|
-
//
|
|
1230
|
-
if (host
|
|
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('
|
|
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
|
-
|
|
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>;
|