turbine-orm 0.60.1 → 0.62.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 +71 -27
- package/dist/cjs/cli/config.d.ts +40 -0
- package/dist/cjs/cli/config.js +74 -2
- package/dist/cjs/cli/index.d.ts +85 -1
- package/dist/cjs/cli/index.js +374 -24
- package/dist/cjs/cli/mcp.d.ts +8 -0
- package/dist/cjs/cli/mcp.js +448 -29
- package/dist/cjs/cli/pii-tags.d.ts +64 -9
- package/dist/cjs/cli/pii-tags.js +218 -39
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/studio.d.ts +23 -0
- package/dist/cjs/cli/studio.js +126 -53
- package/dist/cjs/cli/ui.d.ts +15 -1
- package/dist/cjs/cli/ui.js +19 -5
- package/dist/cjs/client.js +248 -11
- package/dist/cjs/errors.d.ts +38 -1
- package/dist/cjs/errors.js +235 -24
- package/dist/cjs/index.d.ts +2 -2
- package/dist/cjs/index.js +7 -2
- package/dist/cjs/pipeline-submittable.js +26 -3
- package/dist/cjs/pipeline.js +15 -2
- package/dist/cjs/powql.d.ts +12 -0
- package/dist/cjs/powql.js +46 -21
- package/dist/cjs/prisma-compat.d.ts +15 -5
- package/dist/cjs/prisma-compat.js +273 -78
- package/dist/cjs/query/aggregates.d.ts +1 -1
- package/dist/cjs/query/aggregates.js +24 -10
- package/dist/cjs/query/batched-loader.d.ts +9 -4
- package/dist/cjs/query/batched-loader.js +4 -1
- package/dist/cjs/query/builder.d.ts +47 -0
- package/dist/cjs/query/builder.js +149 -21
- package/dist/cjs/query/index.d.ts +3 -1
- package/dist/cjs/query/index.js +7 -1
- package/dist/cjs/query/option-surface.d.ts +11 -0
- package/dist/cjs/query/option-surface.js +13 -0
- package/dist/cjs/query/relations.d.ts +8 -0
- package/dist/cjs/query/relations.js +21 -1
- package/dist/cjs/query/types.d.ts +152 -18
- package/dist/cjs/query/types.js +212 -1
- package/dist/cjs/query/where.d.ts +3 -3
- package/dist/cjs/query/where.js +8 -2
- package/dist/cjs/query/writes.js +10 -9
- package/dist/cli/config.d.ts +40 -0
- package/dist/cli/config.js +73 -2
- package/dist/cli/index.d.ts +85 -1
- package/dist/cli/index.js +373 -27
- package/dist/cli/mcp.d.ts +8 -0
- package/dist/cli/mcp.js +448 -29
- package/dist/cli/pii-tags.d.ts +64 -9
- package/dist/cli/pii-tags.js +217 -39
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/studio.d.ts +23 -0
- package/dist/cli/studio.js +125 -53
- package/dist/cli/ui.d.ts +15 -1
- package/dist/cli/ui.js +18 -4
- package/dist/client.js +250 -13
- package/dist/errors.d.ts +38 -1
- package/dist/errors.js +234 -23
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -2
- package/dist/pipeline-submittable.js +26 -3
- package/dist/pipeline.js +15 -2
- package/dist/powql.d.ts +12 -0
- package/dist/powql.js +46 -21
- package/dist/prisma-compat.d.ts +15 -5
- package/dist/prisma-compat.js +274 -79
- package/dist/query/aggregates.d.ts +1 -1
- package/dist/query/aggregates.js +24 -10
- package/dist/query/batched-loader.d.ts +9 -4
- package/dist/query/batched-loader.js +4 -1
- package/dist/query/builder.d.ts +47 -0
- package/dist/query/builder.js +148 -21
- package/dist/query/index.d.ts +3 -1
- package/dist/query/index.js +2 -0
- package/dist/query/option-surface.d.ts +11 -0
- package/dist/query/option-surface.js +13 -0
- package/dist/query/relations.d.ts +8 -0
- package/dist/query/relations.js +21 -1
- package/dist/query/types.d.ts +152 -18
- package/dist/query/types.js +207 -2
- package/dist/query/where.d.ts +3 -3
- package/dist/query/where.js +8 -2
- package/dist/query/writes.js +10 -9
- package/package.json +13 -3
package/dist/cjs/cli/index.js
CHANGED
|
@@ -69,11 +69,15 @@ exports.tsxInstallCommand = tsxInstallCommand;
|
|
|
69
69
|
exports.tsxRequiredNotice = tsxRequiredNotice;
|
|
70
70
|
exports.detectConsumerModuleType = detectConsumerModuleType;
|
|
71
71
|
exports.planInitSteps = planInitSteps;
|
|
72
|
+
exports.gitignoreIgnoresEnv = gitignoreIgnoresEnv;
|
|
73
|
+
exports.planEnvScaffold = planEnvScaffold;
|
|
74
|
+
exports.scaffoldEnvForUrl = scaffoldEnvForUrl;
|
|
72
75
|
exports.initEnvNotice = initEnvNotice;
|
|
73
76
|
exports.resolveMigrateFromPrismaUrl = resolveMigrateFromPrismaUrl;
|
|
74
77
|
exports.buildMigrateDeployOptions = buildMigrateDeployOptions;
|
|
75
78
|
exports.getSeedExecutionPlan = getSeedExecutionPlan;
|
|
76
79
|
exports.isLoopbackHost = isLoopbackHost;
|
|
80
|
+
exports.showSubcommandHelp = showSubcommandHelp;
|
|
77
81
|
const node_fs_1 = require("node:fs");
|
|
78
82
|
const node_os_1 = require("node:os");
|
|
79
83
|
const node_path_1 = require("node:path");
|
|
@@ -98,6 +102,21 @@ const prisma_resolve_js_1 = require("./prisma-resolve.js");
|
|
|
98
102
|
const prisma_schema_js_1 = require("./prisma-schema.js");
|
|
99
103
|
const studio_js_1 = require("./studio.js");
|
|
100
104
|
const ui_js_1 = require("./ui.js");
|
|
105
|
+
/**
|
|
106
|
+
* Fail an argument-parse error the way every other CLI failure looks: banner,
|
|
107
|
+
* red error line, then indented hint lines. The flag validators used to call
|
|
108
|
+
* `console.error` directly, so a typo in `--recipe` printed one bare unstyled
|
|
109
|
+
* sentence with no banner and no hint, which reads like an internal crash
|
|
110
|
+
* rather than "you forgot the recipe name".
|
|
111
|
+
*/
|
|
112
|
+
function failArg(message, ...hints) {
|
|
113
|
+
(0, ui_js_1.banner)();
|
|
114
|
+
(0, ui_js_1.error)(message);
|
|
115
|
+
for (const hint of hints)
|
|
116
|
+
console.log(` ${(0, ui_js_1.dim)(hint)}`);
|
|
117
|
+
(0, ui_js_1.newline)();
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
101
120
|
function parseArgs(argv = process.argv.slice(2)) {
|
|
102
121
|
const args = argv;
|
|
103
122
|
const result = {
|
|
@@ -158,6 +177,9 @@ function parseArgs(argv = process.argv.slice(2)) {
|
|
|
158
177
|
case '--skip-schema':
|
|
159
178
|
result.skipSchema = true;
|
|
160
179
|
break;
|
|
180
|
+
case '--with-schema':
|
|
181
|
+
result.withSchema = true;
|
|
182
|
+
break;
|
|
161
183
|
case '--skip-seed':
|
|
162
184
|
result.skipSeed = true;
|
|
163
185
|
break;
|
|
@@ -211,8 +233,7 @@ function parseArgs(argv = process.argv.slice(2)) {
|
|
|
211
233
|
case '--import-ext':
|
|
212
234
|
case '--import-extension':
|
|
213
235
|
if (next !== 'js' && next !== 'none' && next !== 'auto') {
|
|
214
|
-
|
|
215
|
-
process.exit(1);
|
|
236
|
+
failArg(`${(0, ui_js_1.cyan)('--import-ext')} requires one of: js, none, auto ${(0, ui_js_1.dim)(`(got ${next ?? 'nothing'})`)}`, 'Example: npx turbine generate --import-ext none');
|
|
216
237
|
}
|
|
217
238
|
result.importExtension = next;
|
|
218
239
|
i++;
|
|
@@ -228,8 +249,7 @@ function parseArgs(argv = process.argv.slice(2)) {
|
|
|
228
249
|
break;
|
|
229
250
|
case '--recipe':
|
|
230
251
|
if (next === undefined || next.startsWith('-')) {
|
|
231
|
-
|
|
232
|
-
process.exit(1);
|
|
252
|
+
failArg(`${(0, ui_js_1.cyan)('--recipe')} requires a recipe name.`, `Known recipes: ${Object.keys(migrate_js_1.MIGRATION_RECIPES).join(', ') || '(none)'}`, 'Example: npx turbine migrate create backfill_full_name --recipe backfill');
|
|
233
253
|
}
|
|
234
254
|
result.recipe = next;
|
|
235
255
|
i++;
|
|
@@ -633,6 +653,23 @@ function planInitSteps(state, flags) {
|
|
|
633
653
|
return { id, action: 'skip', defaultYes: true, skipReason: 'flag' };
|
|
634
654
|
if (exists)
|
|
635
655
|
return { id, action: 'skip', defaultYes: true, skipReason: 'exists' };
|
|
656
|
+
// An EMPTY code-first schema file written next to a database that ALREADY
|
|
657
|
+
// has tables is what makes the very next `turbine push` announce "Schema
|
|
658
|
+
// defines 0 tables" and list every real table under "Extra tables in
|
|
659
|
+
// database (not dropped automatically)", which reads like imminent data
|
|
660
|
+
// loss. For these projects the database is the source of truth
|
|
661
|
+
// (`turbine generate`), so the default flips to "don't scaffold it":
|
|
662
|
+
// prompted (default no) on a TTY, skipped with an explanation otherwise.
|
|
663
|
+
//
|
|
664
|
+
// `--with-schema` opts back in and returns this step to the normal path. A
|
|
665
|
+
// code-first project bootstrapping in CI against a populated database is a
|
|
666
|
+
// real case, and a default that no flag can override is a behavior change
|
|
667
|
+
// with no way out, not a safety rail.
|
|
668
|
+
if (id === 'schema' && state.dbHasTables === true && !flags.withSchema) {
|
|
669
|
+
return mode === 'prompt'
|
|
670
|
+
? { id, action: 'prompt', defaultYes: false }
|
|
671
|
+
: { id, action: 'skip', defaultYes: false, skipReason: 'db-has-tables' };
|
|
672
|
+
}
|
|
636
673
|
return { id, action: mode === 'prompt' ? 'prompt' : 'run', defaultYes: true };
|
|
637
674
|
};
|
|
638
675
|
steps.push(scaffold('schema', state.schemaExists, flags.skipSchema));
|
|
@@ -687,17 +724,33 @@ async function promptYesNo(question, defaultYes) {
|
|
|
687
724
|
rl.close();
|
|
688
725
|
}
|
|
689
726
|
}
|
|
690
|
-
/**
|
|
691
|
-
|
|
727
|
+
/**
|
|
728
|
+
* Probe database reachability (and how populated it is) with a short-lived
|
|
729
|
+
* connection. Never throws: an unreachable database is a normal init state.
|
|
730
|
+
*/
|
|
731
|
+
async function probeDatabase(url, schema) {
|
|
692
732
|
try {
|
|
693
733
|
const { default: pg } = await Promise.resolve().then(() => __importStar(require('pg')));
|
|
694
734
|
const client = new pg.Client({ connectionString: url });
|
|
695
735
|
await client.connect();
|
|
736
|
+
let tableCount = 0;
|
|
737
|
+
try {
|
|
738
|
+
const res = await client.query(`SELECT count(*)::text AS count
|
|
739
|
+
FROM information_schema.tables
|
|
740
|
+
WHERE table_schema = $1
|
|
741
|
+
AND table_type = 'BASE TABLE'
|
|
742
|
+
AND table_name NOT LIKE '\\_turbine\\_%'`, [schema]);
|
|
743
|
+
tableCount = Number.parseInt(res.rows[0]?.count ?? '0', 10) || 0;
|
|
744
|
+
}
|
|
745
|
+
catch {
|
|
746
|
+
// Reachable but the catalog read failed (permissions, odd engine). Treat
|
|
747
|
+
// it as "unknown", not "empty": tableCount 0 only suppresses a scaffold.
|
|
748
|
+
}
|
|
696
749
|
await client.end();
|
|
697
|
-
return true;
|
|
750
|
+
return { reachable: true, tableCount };
|
|
698
751
|
}
|
|
699
752
|
catch {
|
|
700
|
-
return false;
|
|
753
|
+
return { reachable: false, tableCount: 0 };
|
|
701
754
|
}
|
|
702
755
|
}
|
|
703
756
|
const INIT_SEED_TEMPLATE = `/**
|
|
@@ -738,6 +791,132 @@ export default defineSchema({
|
|
|
738
791
|
// },
|
|
739
792
|
});
|
|
740
793
|
`;
|
|
794
|
+
/**
|
|
795
|
+
* Does this `.gitignore` text already ignore `.env`?
|
|
796
|
+
*
|
|
797
|
+
* Line-based rather than a substring search: a `.gitignore` mentioning
|
|
798
|
+
* `.env.example` (a very common line, since the example file is the one you DO
|
|
799
|
+
* commit) contains the text `.env` while ignoring nothing of the kind, and
|
|
800
|
+
* treating that as covered is how the password would stay committable.
|
|
801
|
+
* Negations (`!.env`) are honored as a later line overriding an earlier one,
|
|
802
|
+
* exactly as git resolves them.
|
|
803
|
+
*
|
|
804
|
+
* @internal exported for tests.
|
|
805
|
+
*/
|
|
806
|
+
function gitignoreIgnoresEnv(content) {
|
|
807
|
+
let ignored = false;
|
|
808
|
+
for (const raw of content.split(/\r?\n/)) {
|
|
809
|
+
const line = raw.trim();
|
|
810
|
+
if (line === '' || line.startsWith('#'))
|
|
811
|
+
continue;
|
|
812
|
+
const negated = line.startsWith('!');
|
|
813
|
+
// A leading `/` anchors to the repo root, a trailing `/` means directory.
|
|
814
|
+
const pattern = (negated ? line.slice(1) : line).replace(/^\//, '').replace(/\/$/, '');
|
|
815
|
+
// The patterns that actually match a root-level `.env` file.
|
|
816
|
+
if (pattern === '.env' || pattern === '.env*' || pattern === '*.env' || pattern === '**/.env') {
|
|
817
|
+
ignored = !negated;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return ignored;
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Decide what the `--url`-carries-a-password scaffold writes. Pure, so the whole
|
|
824
|
+
* matrix (fresh project, existing `.env`, existing `.gitignore`, re-run) is
|
|
825
|
+
* testable without a filesystem.
|
|
826
|
+
*
|
|
827
|
+
* An existing `DATABASE_URL` in `.env` is NEVER rewritten: it is the value the
|
|
828
|
+
* project already runs against, and silently repointing it at the `--url` from
|
|
829
|
+
* one command line is a worse failure than printing a notice.
|
|
830
|
+
*
|
|
831
|
+
* @internal exported for tests.
|
|
832
|
+
*/
|
|
833
|
+
function planEnvScaffold(state) {
|
|
834
|
+
return {
|
|
835
|
+
env: !state.envExists ? 'created' : state.envHasDatabaseUrl ? 'unchanged' : 'appended',
|
|
836
|
+
envExample: state.envExampleExists ? 'unchanged' : 'created',
|
|
837
|
+
// No `.gitignore` at all means nothing is ignored, so the `.env` we just
|
|
838
|
+
// wrote is committable. Create one rather than warn about it.
|
|
839
|
+
gitignore: !state.gitignoreExists ? 'created' : state.gitignoreIgnoresEnv ? 'unchanged' : 'appended',
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
const ENV_EXAMPLE_TEMPLATE = `# Copy this file to .env and fill in your own values.
|
|
843
|
+
# This file is committed; .env is not.
|
|
844
|
+
DATABASE_URL=postgres://user:password@localhost:5432/database
|
|
845
|
+
`;
|
|
846
|
+
/**
|
|
847
|
+
* Move a password-bearing `--url` out of `turbine.config.ts` and into `.env`.
|
|
848
|
+
*
|
|
849
|
+
* `turbine init --url postgres://user:PASSWORD@host/db` is the documented
|
|
850
|
+
* one-liner, and it used to inline that string verbatim into a file projects
|
|
851
|
+
* commit, with no `.gitignore` written at all. The config template now refuses
|
|
852
|
+
* to inline a secret (see `configTemplate`), so the real value has to land
|
|
853
|
+
* somewhere the config can read it from: this writes `.env`, scaffolds the
|
|
854
|
+
* committable `.env.example` next to it, and makes sure `.gitignore` covers
|
|
855
|
+
* `.env` BEFORE the secret is on disk long enough to be staged.
|
|
856
|
+
*
|
|
857
|
+
* Paths are cwd-relative, exactly like the rest of the init scaffold.
|
|
858
|
+
*
|
|
859
|
+
* @internal exported for tests.
|
|
860
|
+
*/
|
|
861
|
+
function scaffoldEnvForUrl(url) {
|
|
862
|
+
const envPath = '.env';
|
|
863
|
+
const envExamplePath = '.env.example';
|
|
864
|
+
const gitignorePath = '.gitignore';
|
|
865
|
+
const envExists = (0, node_fs_1.existsSync)(envPath);
|
|
866
|
+
const plan = planEnvScaffold({
|
|
867
|
+
envExists,
|
|
868
|
+
envHasDatabaseUrl: envExists ? /^\s*(export\s+)?DATABASE_URL\s*=/m.test((0, node_fs_1.readFileSync)(envPath, 'utf-8')) : false,
|
|
869
|
+
envExampleExists: (0, node_fs_1.existsSync)(envExamplePath),
|
|
870
|
+
gitignoreExists: (0, node_fs_1.existsSync)(gitignorePath),
|
|
871
|
+
gitignoreIgnoresEnv: (0, node_fs_1.existsSync)(gitignorePath) ? gitignoreIgnoresEnv((0, node_fs_1.readFileSync)(gitignorePath, 'utf-8')) : false,
|
|
872
|
+
});
|
|
873
|
+
// .gitignore FIRST: the ignore rule has to exist before the file holding the
|
|
874
|
+
// password does, or a `git add -A` in between commits it.
|
|
875
|
+
if (plan.gitignore === 'created')
|
|
876
|
+
(0, node_fs_1.writeFileSync)(gitignorePath, '# Local environment (secrets)\n.env\n', 'utf-8');
|
|
877
|
+
else if (plan.gitignore === 'appended')
|
|
878
|
+
(0, node_fs_1.appendFileSync)(gitignorePath, '\n# Local environment (secrets)\n.env\n');
|
|
879
|
+
if (plan.env === 'created') {
|
|
880
|
+
// 0600 on CREATION only. The whole point of this scaffold is moving a live
|
|
881
|
+
// password out of a committed file, and the default 0644 would leave it
|
|
882
|
+
// world-readable to every account on a shared box or CI runner. `mode` is
|
|
883
|
+
// applied by the OS only when the open() actually creates the file, so if
|
|
884
|
+
// one appeared since the existsSync above we write into it without widening
|
|
885
|
+
// or narrowing whatever mode its owner chose.
|
|
886
|
+
(0, node_fs_1.writeFileSync)(envPath, `# Turbine database connection. Never commit this file.\nDATABASE_URL=${url}\n`, {
|
|
887
|
+
encoding: 'utf-8',
|
|
888
|
+
mode: 0o600,
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
else if (plan.env === 'appended') {
|
|
892
|
+
(0, node_fs_1.appendFileSync)(envPath, `\n# Added by turbine init\nDATABASE_URL=${url}\n`);
|
|
893
|
+
}
|
|
894
|
+
if (plan.envExample === 'created')
|
|
895
|
+
(0, node_fs_1.writeFileSync)(envExamplePath, ENV_EXAMPLE_TEMPLATE, 'utf-8');
|
|
896
|
+
return plan;
|
|
897
|
+
}
|
|
898
|
+
/** Say exactly what was written where, so nobody has to guess where the password went. */
|
|
899
|
+
function reportEnvScaffold(plan, url) {
|
|
900
|
+
(0, ui_js_1.warn)(`The connection string passed to ${(0, ui_js_1.cyan)('--url')} contains a password.`);
|
|
901
|
+
console.log(` ${(0, ui_js_1.dim)('It was NOT written into')} ${(0, ui_js_1.cyan)('turbine.config.ts')}${(0, ui_js_1.dim)(': that file reads')} ${(0, ui_js_1.cyan)('process.env.DATABASE_URL')}${(0, ui_js_1.dim)('.')}`);
|
|
902
|
+
if (plan.env === 'created')
|
|
903
|
+
(0, ui_js_1.success)(`Wrote the connection string to ${(0, ui_js_1.cyan)('.env')} ${(0, ui_js_1.dim)(`(${(0, ui_js_1.redactUrl)(url)})`)}`);
|
|
904
|
+
else if (plan.env === 'appended')
|
|
905
|
+
(0, ui_js_1.success)(`Appended ${(0, ui_js_1.cyan)('DATABASE_URL')} to your existing ${(0, ui_js_1.cyan)('.env')} ${(0, ui_js_1.dim)(`(${(0, ui_js_1.redactUrl)(url)})`)}`);
|
|
906
|
+
else
|
|
907
|
+
(0, ui_js_1.info)(`Your ${(0, ui_js_1.cyan)('.env')} already sets ${(0, ui_js_1.cyan)('DATABASE_URL')}: left untouched. The ${(0, ui_js_1.cyan)('--url')} value was not saved anywhere.`);
|
|
908
|
+
if (plan.envExample === 'created')
|
|
909
|
+
(0, ui_js_1.success)(`Created ${(0, ui_js_1.cyan)('.env.example')} ${(0, ui_js_1.dim)('(safe to commit)')}`);
|
|
910
|
+
if (plan.gitignore === 'created')
|
|
911
|
+
(0, ui_js_1.success)(`Created ${(0, ui_js_1.cyan)('.gitignore')} ignoring ${(0, ui_js_1.cyan)('.env')}`);
|
|
912
|
+
else if (plan.gitignore === 'appended')
|
|
913
|
+
(0, ui_js_1.success)(`Added ${(0, ui_js_1.cyan)('.env')} to ${(0, ui_js_1.cyan)('.gitignore')}`);
|
|
914
|
+
else
|
|
915
|
+
(0, ui_js_1.info)(`${(0, ui_js_1.cyan)('.gitignore')} already ignores ${(0, ui_js_1.cyan)('.env')}`);
|
|
916
|
+
if (plan.env !== 'unchanged') {
|
|
917
|
+
console.log(` ${(0, ui_js_1.dim)('The password is now in')} ${(0, ui_js_1.cyan)('.env')}${(0, ui_js_1.dim)('. If it was ever pushed to a shared history, rotate it.')}`);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
741
920
|
/** Create supporting directories + .gitignore entries (unconditional, unprompted). */
|
|
742
921
|
function ensureInitScaffoldDirs(config) {
|
|
743
922
|
const migrDir = config.migrationsDir;
|
|
@@ -892,15 +1071,27 @@ function reportInitSkip(step) {
|
|
|
892
1071
|
case 'default-no':
|
|
893
1072
|
console.log(` ${(0, ui_js_1.dim)(`${ui_js_1.symbols.dot} ${label} skipped (default no)`)}`);
|
|
894
1073
|
break;
|
|
1074
|
+
case 'db-has-tables':
|
|
1075
|
+
(0, ui_js_1.info)(`${label} not created: your database already has tables ${(0, ui_js_1.dim)('- skipped')}`);
|
|
1076
|
+
console.log(` ${(0, ui_js_1.dim)('An empty')} ${(0, ui_js_1.cyan)('defineSchema()')} ${(0, ui_js_1.dim)('file would make')} ${(0, ui_js_1.cyan)('turbine push')} ${(0, ui_js_1.dim)('report every existing')}`);
|
|
1077
|
+
console.log(` ${(0, ui_js_1.dim)('table as "extra". Your database is the source of truth here: run')} ${(0, ui_js_1.cyan)('turbine generate')}`);
|
|
1078
|
+
console.log(` ${(0, ui_js_1.dim)('to produce the typed client from it. For a code-first schema instead, re-run')}`);
|
|
1079
|
+
console.log(` ${(0, ui_js_1.dim)('with')} ${(0, ui_js_1.cyan)('--with-schema')} ${(0, ui_js_1.dim)('to scaffold')} ${(0, ui_js_1.cyan)('turbine/schema.ts')} ${(0, ui_js_1.dim)('anyway, then use')} ${(0, ui_js_1.cyan)('turbine push')}${(0, ui_js_1.dim)('.')}`);
|
|
1080
|
+
break;
|
|
895
1081
|
default:
|
|
896
1082
|
console.log(` ${(0, ui_js_1.dim)(`${ui_js_1.symbols.dot} ${label} skipped`)}`);
|
|
897
1083
|
}
|
|
898
1084
|
}
|
|
899
1085
|
/** The interactive prompt question for a promptable step. */
|
|
900
|
-
function initPromptQuestion(step, config, seedFilePath) {
|
|
1086
|
+
function initPromptQuestion(step, config, seedFilePath, dbTableCount = 0) {
|
|
901
1087
|
switch (step.id) {
|
|
902
1088
|
case 'schema':
|
|
903
|
-
|
|
1089
|
+
// Ask the honest question when the database is already populated: the
|
|
1090
|
+
// file would be EMPTY, and `turbine push` reports every existing table as
|
|
1091
|
+
// extra until it is filled in by hand.
|
|
1092
|
+
return dbTableCount > 0
|
|
1093
|
+
? `Your database already has ${dbTableCount} table(s). Create an EMPTY code-first schema file anyway (${config.schemaFile})?`
|
|
1094
|
+
: `Create a starter schema file (${config.schemaFile})?`;
|
|
904
1095
|
case 'seed-file':
|
|
905
1096
|
return `Create a starter seed file (${seedFilePath})?`;
|
|
906
1097
|
case 'push':
|
|
@@ -1009,11 +1200,18 @@ async function cmdInit(args, config) {
|
|
|
1009
1200
|
// Probe the database once, up front, so the planner can decide the DB steps.
|
|
1010
1201
|
// Skip the probe entirely when every DB step is already flag-skipped.
|
|
1011
1202
|
const anyDbStepPossible = !(args.skipPush && args.skipGenerate && args.skipSeed);
|
|
1203
|
+
let dbTableCount = 0;
|
|
1012
1204
|
if (hasUrl && anyDbStepPossible) {
|
|
1013
1205
|
const probe = new ui_js_1.Spinner('Checking database connection').start();
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1206
|
+
const probed = await probeDatabase(url, config.schema);
|
|
1207
|
+
state.dbReachable = probed.reachable;
|
|
1208
|
+
dbTableCount = probed.tableCount;
|
|
1209
|
+
state.dbHasTables = probed.tableCount > 0;
|
|
1210
|
+
if (state.dbReachable) {
|
|
1211
|
+
probe.succeed(dbTableCount > 0
|
|
1212
|
+
? `Database is reachable ${(0, ui_js_1.dim)(`(${dbTableCount} table(s) in schema "${config.schema}")`)}`
|
|
1213
|
+
: 'Database is reachable');
|
|
1214
|
+
}
|
|
1017
1215
|
else
|
|
1018
1216
|
probe.info('Database not reachable: push / generate / seed steps will be skipped');
|
|
1019
1217
|
}
|
|
@@ -1022,6 +1220,7 @@ async function cmdInit(args, config) {
|
|
|
1022
1220
|
force: args.force === true,
|
|
1023
1221
|
interactive,
|
|
1024
1222
|
skipSchema: args.skipSchema === true,
|
|
1223
|
+
withSchema: args.withSchema === true,
|
|
1025
1224
|
skipSeed: args.skipSeed === true,
|
|
1026
1225
|
skipPush: args.skipPush === true,
|
|
1027
1226
|
skipGenerate: args.skipGenerate === true,
|
|
@@ -1039,17 +1238,26 @@ async function cmdInit(args, config) {
|
|
|
1039
1238
|
reportInitSkip(step);
|
|
1040
1239
|
continue;
|
|
1041
1240
|
}
|
|
1042
|
-
const shouldRun = step.action === 'run'
|
|
1241
|
+
const shouldRun = step.action === 'run'
|
|
1242
|
+
? true
|
|
1243
|
+
: await promptYesNo(initPromptQuestion(step, config, seedFilePath, dbTableCount), step.defaultYes);
|
|
1043
1244
|
if (!shouldRun) {
|
|
1044
1245
|
(0, ui_js_1.info)(`Skipped ${initStepLabel(step.id)}`);
|
|
1045
1246
|
continue;
|
|
1046
1247
|
}
|
|
1047
1248
|
switch (step.id) {
|
|
1048
|
-
case 'config':
|
|
1249
|
+
case 'config': {
|
|
1049
1250
|
(0, node_fs_1.writeFileSync)('turbine.config.ts', (0, config_js_1.configTemplate)(args.url ?? undefined), 'utf-8');
|
|
1050
1251
|
(0, ui_js_1.success)(state.configExists ? `Overwrote ${(0, ui_js_1.cyan)('turbine.config.ts')}` : `Created ${(0, ui_js_1.cyan)('turbine.config.ts')}`);
|
|
1051
1252
|
tsFilesWritten.push('turbine.config.ts');
|
|
1253
|
+
// A `--url` with a password is never inlined by configTemplate, so the
|
|
1254
|
+
// real value has to be put somewhere the config can read it from.
|
|
1255
|
+
if (args.url && (0, config_js_1.connectionStringHasPassword)(args.url)) {
|
|
1256
|
+
(0, ui_js_1.newline)();
|
|
1257
|
+
reportEnvScaffold(scaffoldEnvForUrl(args.url), args.url);
|
|
1258
|
+
}
|
|
1052
1259
|
break;
|
|
1260
|
+
}
|
|
1053
1261
|
case 'schema':
|
|
1054
1262
|
writeInitSchemaTemplate(config);
|
|
1055
1263
|
if ((0, loader_js_1.needsTsLoader)(config.schemaFile))
|
|
@@ -1777,6 +1985,41 @@ async function cmdMigrateCreate(args, config) {
|
|
|
1777
1985
|
console.log(` ${(0, ui_js_1.cyan)('npx turbine migrate up')}`);
|
|
1778
1986
|
(0, ui_js_1.newline)();
|
|
1779
1987
|
}
|
|
1988
|
+
/**
|
|
1989
|
+
* Print the SQL a `migrate up` / `migrate down` WOULD run, and execute nothing.
|
|
1990
|
+
*
|
|
1991
|
+
* `--dry-run` was parsed and then read by `push` and `deploy` only, so on
|
|
1992
|
+
* `migrate up` it was silently inert: the flag was accepted, no warning was
|
|
1993
|
+
* printed, and the migrations were applied. A flag whose whole purpose is
|
|
1994
|
+
* "show me what this would do to the database" that instead does it, against
|
|
1995
|
+
* whatever `--url` was passed, is the most dangerous shape a CLI bug can take.
|
|
1996
|
+
*
|
|
1997
|
+
* Reads through the same `parseMigrationContent` the executor uses, so what is
|
|
1998
|
+
* printed is what would run, rather than a second rendering that can drift.
|
|
1999
|
+
*/
|
|
2000
|
+
function printMigrationDryRun(files, section) {
|
|
2001
|
+
const verb = section === 'up' ? 'apply' : 'roll back';
|
|
2002
|
+
if (files.length === 0) {
|
|
2003
|
+
(0, ui_js_1.info)(`Dry run: nothing to ${verb}.`);
|
|
2004
|
+
(0, ui_js_1.newline)();
|
|
2005
|
+
return;
|
|
2006
|
+
}
|
|
2007
|
+
(0, ui_js_1.info)(`Dry run: would ${verb} ${(0, ui_js_1.bold)(String(files.length))} migration(s). Nothing was executed.`);
|
|
2008
|
+
(0, ui_js_1.newline)();
|
|
2009
|
+
for (const file of files) {
|
|
2010
|
+
const parsed = (0, migrate_js_1.parseMigrationContent)((0, node_fs_1.readFileSync)(file.path, 'utf-8'));
|
|
2011
|
+
const sql = section === 'up' ? parsed.up : parsed.down;
|
|
2012
|
+
console.log(` ${(0, ui_js_1.cyan)(file.filename)}${parsed.noTransaction ? (0, ui_js_1.dim)(' (no transaction)') : ''}`);
|
|
2013
|
+
if (sql.trim().length === 0) {
|
|
2014
|
+
console.log(` ${(0, ui_js_1.dim)(`(empty ${section.toUpperCase()} section)`)}`);
|
|
2015
|
+
}
|
|
2016
|
+
else {
|
|
2017
|
+
for (const line of sql.split('\n'))
|
|
2018
|
+
console.log(` ${(0, ui_js_1.dim)(line)}`);
|
|
2019
|
+
}
|
|
2020
|
+
(0, ui_js_1.newline)();
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
1780
2023
|
async function cmdMigrateUp(args, config) {
|
|
1781
2024
|
(0, ui_js_1.banner)();
|
|
1782
2025
|
const url = requireUrl(config);
|
|
@@ -1790,6 +2033,14 @@ async function cmdMigrateUp(args, config) {
|
|
|
1790
2033
|
(0, ui_js_1.newline)();
|
|
1791
2034
|
return;
|
|
1792
2035
|
}
|
|
2036
|
+
if (args.dryRun) {
|
|
2037
|
+
const status = await (0, migrate_js_1.migrateStatus)(url, config.migrationsDir);
|
|
2038
|
+
let pending = status.filter((st) => !st.applied).map((st) => st.file);
|
|
2039
|
+
if (args.step != null && args.step > 0)
|
|
2040
|
+
pending = pending.slice(0, args.step);
|
|
2041
|
+
printMigrationDryRun(pending, 'up');
|
|
2042
|
+
return;
|
|
2043
|
+
}
|
|
1793
2044
|
// Big, loud warning when bypassing drift detection, this is a deliberately
|
|
1794
2045
|
// dangerous operation and the user should see it on every invocation.
|
|
1795
2046
|
if (args.allowDrift) {
|
|
@@ -2011,6 +2262,14 @@ async function cmdMigrateDown(args, config) {
|
|
|
2011
2262
|
(0, ui_js_1.label)('Database', (0, ui_js_1.redactUrl)(url));
|
|
2012
2263
|
(0, ui_js_1.label)('Migrations', config.migrationsDir);
|
|
2013
2264
|
(0, ui_js_1.newline)();
|
|
2265
|
+
if (args.dryRun) {
|
|
2266
|
+
const status = await (0, migrate_js_1.migrateStatus)(url, config.migrationsDir);
|
|
2267
|
+
// Rollback order is newest-applied first, and a migration whose file is
|
|
2268
|
+
// missing has no DOWN section to show.
|
|
2269
|
+
const applied = status.filter((st) => st.applied && !st.missingFile).map((st) => st.file);
|
|
2270
|
+
printMigrationDryRun(applied.reverse().slice(0, args.step ?? 1), 'down');
|
|
2271
|
+
return;
|
|
2272
|
+
}
|
|
2014
2273
|
const spinner = new ui_js_1.Spinner('Rolling back migration(s)').start();
|
|
2015
2274
|
let result;
|
|
2016
2275
|
try {
|
|
@@ -3087,8 +3346,10 @@ async function cmdStudio(args, config) {
|
|
|
3087
3346
|
(0, ui_js_1.newline)();
|
|
3088
3347
|
process.exit(1);
|
|
3089
3348
|
}
|
|
3090
|
-
|
|
3091
|
-
|
|
3349
|
+
// warn() prints; it returns void. Wrapping it in console.log() printed a
|
|
3350
|
+
// literal grey "undefined" line under every one of these SAFETY warnings.
|
|
3351
|
+
(0, ui_js_1.warn)(`Studio is binding to ${(0, ui_js_1.yellow)(host)}, this is NOT loopback. ` +
|
|
3352
|
+
`Anyone on your network who can reach this port + guess the session token can read your database.`);
|
|
3092
3353
|
}
|
|
3093
3354
|
const spinner = new ui_js_1.Spinner(demo ? 'Seeding demo dataset' : 'Introspecting database').start();
|
|
3094
3355
|
let studio;
|
|
@@ -3134,12 +3395,12 @@ async function cmdStudio(args, config) {
|
|
|
3134
3395
|
// Loud startup warnings for the opt-in modes that widen Studio's surface.
|
|
3135
3396
|
if (args.write) {
|
|
3136
3397
|
(0, ui_js_1.newline)();
|
|
3137
|
-
|
|
3138
|
-
`${(0, ui_js_1.redactUrl)(url)}. Every change is committed directly to your database.`)
|
|
3398
|
+
(0, ui_js_1.warn)('WRITE MODE is ON. Studio can update, insert, and delete single rows in ' +
|
|
3399
|
+
`${(0, ui_js_1.redactUrl)(url)}. Every change is committed directly to your database.`);
|
|
3139
3400
|
}
|
|
3140
3401
|
if (args.showPii) {
|
|
3141
3402
|
(0, ui_js_1.newline)();
|
|
3142
|
-
|
|
3403
|
+
(0, ui_js_1.warn)('--show-pii is ON. PII-tagged column values are shown UNREDACTED in Studio.');
|
|
3143
3404
|
}
|
|
3144
3405
|
// PII tags are a code-first declaration; introspection never infers them.
|
|
3145
3406
|
// Say plainly whether any reached this session, so nobody assumes a
|
|
@@ -3150,9 +3411,9 @@ async function cmdStudio(args, config) {
|
|
|
3150
3411
|
console.log(` ${(0, ui_js_1.dim)('PII redaction:')} ${studio.piiTags.applied} tagged column(s) from ${(0, ui_js_1.dim)(studio.piiTags.path)}`);
|
|
3151
3412
|
}
|
|
3152
3413
|
else {
|
|
3153
|
-
|
|
3414
|
+
(0, ui_js_1.warn)('No PII-tagged columns found, so nothing will be redacted. Tags are declared in code ' +
|
|
3154
3415
|
`(defineSchema \`pii: true\`) and read from generated metadata in ${config.out}; ` +
|
|
3155
|
-
'introspection alone never infers them. Run `turbine generate` after tagging.')
|
|
3416
|
+
'introspection alone never infers them. Run `turbine generate` after tagging.');
|
|
3156
3417
|
}
|
|
3157
3418
|
}
|
|
3158
3419
|
(0, ui_js_1.newline)();
|
|
@@ -3232,8 +3493,9 @@ async function cmdObserve(args) {
|
|
|
3232
3493
|
(0, ui_js_1.newline)();
|
|
3233
3494
|
process.exit(1);
|
|
3234
3495
|
}
|
|
3235
|
-
|
|
3236
|
-
|
|
3496
|
+
// warn() prints and returns void; see the same guard in cmdStudio.
|
|
3497
|
+
(0, ui_js_1.warn)(`Observe is binding to ${(0, ui_js_1.yellow)(host)}, this is NOT loopback. ` +
|
|
3498
|
+
`Anyone on your network who can reach this port + guess the session token can read your metrics.`);
|
|
3237
3499
|
}
|
|
3238
3500
|
const spinner = new ui_js_1.Spinner('Connecting to metrics database').start();
|
|
3239
3501
|
let handle;
|
|
@@ -3272,6 +3534,13 @@ async function cmdObserve(args) {
|
|
|
3272
3534
|
// ---------------------------------------------------------------------------
|
|
3273
3535
|
// Subcommand help
|
|
3274
3536
|
// ---------------------------------------------------------------------------
|
|
3537
|
+
/**
|
|
3538
|
+
* Print `<command> --help` for a command that has real help, returning whether
|
|
3539
|
+
* one existed. Falling through to the GLOBAL help is the failure mode this map
|
|
3540
|
+
* guards against, and it is silent, so the coverage is asserted in tests.
|
|
3541
|
+
*
|
|
3542
|
+
* @internal exported for tests.
|
|
3543
|
+
*/
|
|
3275
3544
|
function showSubcommandHelp(command) {
|
|
3276
3545
|
const helpMap = {
|
|
3277
3546
|
init: showInitHelp,
|
|
@@ -3283,6 +3552,8 @@ function showSubcommandHelp(command) {
|
|
|
3283
3552
|
migration: showMigrateHelp,
|
|
3284
3553
|
seed: showSeedHelp,
|
|
3285
3554
|
status: showStatusHelp,
|
|
3555
|
+
doctor: showDoctorHelp,
|
|
3556
|
+
studio: showStudioHelp,
|
|
3286
3557
|
mcp: showMcpHelp,
|
|
3287
3558
|
};
|
|
3288
3559
|
const fn = helpMap[command];
|
|
@@ -3312,6 +3583,7 @@ function showInitHelp() {
|
|
|
3312
3583
|
console.log(` ${(0, ui_js_1.cyan)('--force, -f')} Overwrite existing config file`);
|
|
3313
3584
|
console.log(` ${(0, ui_js_1.cyan)('--yes, -y')} Accept every step's default (non-interactive)`);
|
|
3314
3585
|
console.log(` ${(0, ui_js_1.cyan)('--skip-schema')} Don't create the starter schema file`);
|
|
3586
|
+
console.log(` ${(0, ui_js_1.cyan)('--with-schema')} Create it even if the database already has tables`);
|
|
3315
3587
|
console.log(` ${(0, ui_js_1.cyan)('--skip-seed')} Don't create the seed file or run the seed`);
|
|
3316
3588
|
console.log(` ${(0, ui_js_1.cyan)('--skip-push')} Don't push the schema to the database`);
|
|
3317
3589
|
console.log(` ${(0, ui_js_1.cyan)('--skip-generate')} Don't generate the typed client`);
|
|
@@ -3464,6 +3736,83 @@ function showStatusHelp() {
|
|
|
3464
3736
|
console.log(` ${(0, ui_js_1.cyan)('--schema, -s')} ${(0, ui_js_1.dim)('<name>')} Postgres schema ${(0, ui_js_1.dim)('(default: public)')}`);
|
|
3465
3737
|
(0, ui_js_1.newline)();
|
|
3466
3738
|
}
|
|
3739
|
+
function showDoctorHelp() {
|
|
3740
|
+
(0, ui_js_1.banner)();
|
|
3741
|
+
console.log(` ${(0, ui_js_1.bold)('turbine doctor')}, Index + cached-plan triage`);
|
|
3742
|
+
(0, ui_js_1.newline)();
|
|
3743
|
+
console.log(` ${(0, ui_js_1.bold)('Usage:')}`);
|
|
3744
|
+
console.log(` npx turbine doctor ${(0, ui_js_1.dim)('[options]')}`);
|
|
3745
|
+
(0, ui_js_1.newline)();
|
|
3746
|
+
console.log(` Introspects your schema and live statistics, then reports:`);
|
|
3747
|
+
console.log(` ${(0, ui_js_1.dim)('•')} relation probes with no usable index ${(0, ui_js_1.dim)('(ranked by estimated cost)')}`);
|
|
3748
|
+
console.log(` ${(0, ui_js_1.dim)('•')} INVALID indexes ${(0, ui_js_1.dim)('(left behind by a failed CREATE INDEX CONCURRENTLY)')}`);
|
|
3749
|
+
console.log(` ${(0, ui_js_1.dim)('•')} columns whose value distribution can flip a cached plan`);
|
|
3750
|
+
console.log(` ${(0, ui_js_1.dim)('•')} with ${(0, ui_js_1.cyan)('--unused')}: never-scanned and redundant indexes, with DROP suggestions`);
|
|
3751
|
+
(0, ui_js_1.newline)();
|
|
3752
|
+
console.log(` ${(0, ui_js_1.dim)('Read-only: doctor never writes to your database. Only')} ${(0, ui_js_1.cyan)('--fix')} ${(0, ui_js_1.dim)('writes')}`);
|
|
3753
|
+
console.log(` ${(0, ui_js_1.dim)('anything at all, and only a migration FILE you review and run yourself.')}`);
|
|
3754
|
+
(0, ui_js_1.newline)();
|
|
3755
|
+
console.log(` ${(0, ui_js_1.bold)('Options:')}`);
|
|
3756
|
+
console.log(` ${(0, ui_js_1.cyan)('--url, -u')} ${(0, ui_js_1.dim)('<url>')} Postgres connection string`);
|
|
3757
|
+
console.log(` ${(0, ui_js_1.cyan)('--schema, -s')} ${(0, ui_js_1.dim)('<name>')} Postgres schema ${(0, ui_js_1.dim)('(default: public)')}`);
|
|
3758
|
+
console.log(` ${(0, ui_js_1.cyan)('--include')} ${(0, ui_js_1.dim)('<tables>')} Comma-separated tables to include`);
|
|
3759
|
+
console.log(` ${(0, ui_js_1.cyan)('--exclude')} ${(0, ui_js_1.dim)('<tables>')} Comma-separated tables to exclude`);
|
|
3760
|
+
console.log(` ${(0, ui_js_1.cyan)('--fix')} Write an add-index migration for the missing-index findings`);
|
|
3761
|
+
console.log(` ${(0, ui_js_1.cyan)('--no-concurrently')} With ${(0, ui_js_1.cyan)('--fix')}: emit plain CREATE INDEX ${(0, ui_js_1.dim)('(default: CONCURRENTLY, no transaction)')}`);
|
|
3762
|
+
console.log(` ${(0, ui_js_1.cyan)('--json')} Emit the versioned machine-readable report and nothing else`);
|
|
3763
|
+
console.log(` ${(0, ui_js_1.cyan)('--unused')} Also report never-scanned / redundant indexes ${(0, ui_js_1.dim)('(no --fix)')}`);
|
|
3764
|
+
console.log(` ${(0, ui_js_1.cyan)('--audit')} Scope the unused report to doctor's own suggested index names`);
|
|
3765
|
+
console.log(` ${(0, ui_js_1.cyan)('--min-scans')} ${(0, ui_js_1.dim)('<n>')} idx_scan below this counts as never-scanned ${(0, ui_js_1.dim)('(default: 1)')}`);
|
|
3766
|
+
console.log(` ${(0, ui_js_1.cyan)('--metrics-url')} ${(0, ui_js_1.dim)('<url>')} Read ${(0, ui_js_1.cyan)('_turbine_metrics')} for the table-heat boost from a separate DB`);
|
|
3767
|
+
console.log(` ${(0, ui_js_1.cyan)('--no-plan-divergence')} Skip the cached-plan divergence section ${(0, ui_js_1.dim)('(and its pg_stats read)')}`);
|
|
3768
|
+
(0, ui_js_1.newline)();
|
|
3769
|
+
console.log(` ${(0, ui_js_1.bold)('Examples:')}`);
|
|
3770
|
+
console.log(` ${(0, ui_js_1.dim)('$')} npx turbine doctor`);
|
|
3771
|
+
console.log(` ${(0, ui_js_1.dim)('$')} npx turbine doctor --fix`);
|
|
3772
|
+
console.log(` ${(0, ui_js_1.dim)('$')} npx turbine doctor --unused --min-scans 5`);
|
|
3773
|
+
console.log(` ${(0, ui_js_1.dim)('$')} npx turbine doctor --json > doctor.json`);
|
|
3774
|
+
(0, ui_js_1.newline)();
|
|
3775
|
+
}
|
|
3776
|
+
function showStudioHelp() {
|
|
3777
|
+
(0, ui_js_1.banner)();
|
|
3778
|
+
console.log(` ${(0, ui_js_1.bold)('turbine studio')}, Launch the local database UI`);
|
|
3779
|
+
(0, ui_js_1.newline)();
|
|
3780
|
+
console.log(` ${(0, ui_js_1.bold)('Usage:')}`);
|
|
3781
|
+
console.log(` npx turbine studio ${(0, ui_js_1.dim)('[options]')}`);
|
|
3782
|
+
(0, ui_js_1.newline)();
|
|
3783
|
+
console.log(` A local web UI with Query / Data / Schema tabs. There is no raw-SQL`);
|
|
3784
|
+
console.log(` surface: the Query tab is a visual ${(0, ui_js_1.cyan)('findMany')} builder and every`);
|
|
3785
|
+
console.log(` identifier is validated against the introspected schema.`);
|
|
3786
|
+
(0, ui_js_1.newline)();
|
|
3787
|
+
console.log(` ${(0, ui_js_1.bold)('Defaults are the safe ones:')}`);
|
|
3788
|
+
console.log(` ${(0, ui_js_1.dim)('•')} read-only ${(0, ui_js_1.dim)('(reads run inside BEGIN READ ONLY)')}`);
|
|
3789
|
+
console.log(` ${(0, ui_js_1.dim)('•')} PII-tagged column values redacted server-side`);
|
|
3790
|
+
console.log(` ${(0, ui_js_1.dim)('•')} bound to 127.0.0.1, behind a random per-session token`);
|
|
3791
|
+
(0, ui_js_1.newline)();
|
|
3792
|
+
console.log(` ${(0, ui_js_1.bold)('Options:')}`);
|
|
3793
|
+
console.log(` ${(0, ui_js_1.cyan)('--url, -u')} ${(0, ui_js_1.dim)('<url>')} Postgres connection string`);
|
|
3794
|
+
console.log(` ${(0, ui_js_1.cyan)('--schema, -s')} ${(0, ui_js_1.dim)('<name>')} Postgres schema ${(0, ui_js_1.dim)('(default: public)')}`);
|
|
3795
|
+
console.log(` ${(0, ui_js_1.cyan)('--out, -o')} ${(0, ui_js_1.dim)('<dir>')} Generated-metadata dir, source of the PII tags`);
|
|
3796
|
+
console.log(` ${(0, ui_js_1.cyan)('--include')} ${(0, ui_js_1.dim)('<tables>')} Comma-separated tables to include`);
|
|
3797
|
+
console.log(` ${(0, ui_js_1.cyan)('--exclude')} ${(0, ui_js_1.dim)('<tables>')} Comma-separated tables to exclude`);
|
|
3798
|
+
console.log(` ${(0, ui_js_1.cyan)('--port')} ${(0, ui_js_1.dim)('<n>')} HTTP port ${(0, ui_js_1.dim)('(default: 4983)')}`);
|
|
3799
|
+
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)')}`);
|
|
3800
|
+
console.log(` ${(0, ui_js_1.cyan)('--no-open')} Don't auto-open the browser`);
|
|
3801
|
+
console.log(` ${(0, ui_js_1.cyan)('--allow-remote')} Allow a non-loopback ${(0, ui_js_1.cyan)('--host')} ${(0, ui_js_1.dim)('(refused without it)')}`);
|
|
3802
|
+
console.log(` ${(0, ui_js_1.cyan)('--write')} Enable single-row update / insert / delete`);
|
|
3803
|
+
console.log(` ${(0, ui_js_1.cyan)('--show-pii')} Show PII-tagged values unredacted`);
|
|
3804
|
+
console.log(` ${(0, ui_js_1.cyan)('--demo')} Seeded in-memory sample database ${(0, ui_js_1.dim)('(no DATABASE_URL)')}`);
|
|
3805
|
+
(0, ui_js_1.newline)();
|
|
3806
|
+
console.log(` ${(0, ui_js_1.bold)('Examples:')}`);
|
|
3807
|
+
console.log(` ${(0, ui_js_1.dim)('$')} npx turbine studio`);
|
|
3808
|
+
console.log(` ${(0, ui_js_1.dim)('$')} npx turbine studio --demo`);
|
|
3809
|
+
console.log(` ${(0, ui_js_1.dim)('$')} npx turbine studio --write --port 5000`);
|
|
3810
|
+
(0, ui_js_1.newline)();
|
|
3811
|
+
console.log(` ${(0, ui_js_1.dim)('--write commits every change directly to the database, and')} ${(0, ui_js_1.cyan)('--show-pii')}`);
|
|
3812
|
+
console.log(` ${(0, ui_js_1.dim)('reveals values Studio otherwise never sends to the browser. Both print a')}`);
|
|
3813
|
+
console.log(` ${(0, ui_js_1.dim)('startup warning and a persistent in-UI banner. Demo mode saves nothing.')}`);
|
|
3814
|
+
(0, ui_js_1.newline)();
|
|
3815
|
+
}
|
|
3467
3816
|
function showMcpHelp() {
|
|
3468
3817
|
(0, ui_js_1.banner)();
|
|
3469
3818
|
console.log(` ${(0, ui_js_1.bold)('turbine mcp')}, Start read-only MCP server over stdio`);
|
|
@@ -3528,6 +3877,7 @@ function showHelp() {
|
|
|
3528
3877
|
console.log(` ${(0, ui_js_1.bold)('Init options:')}`);
|
|
3529
3878
|
console.log(` ${(0, ui_js_1.cyan)('--yes, -y')} Accept every step's default (non-interactive)`);
|
|
3530
3879
|
console.log(` ${(0, ui_js_1.cyan)('--skip-schema')} Don't scaffold the schema file`);
|
|
3880
|
+
console.log(` ${(0, ui_js_1.cyan)('--with-schema')} Scaffold it even if the database already has tables`);
|
|
3531
3881
|
console.log(` ${(0, ui_js_1.cyan)('--skip-seed')} Don't scaffold or run the seed file`);
|
|
3532
3882
|
console.log(` ${(0, ui_js_1.cyan)('--skip-push')} Don't offer to push the schema to the database`);
|
|
3533
3883
|
console.log(` ${(0, ui_js_1.cyan)('--skip-generate')} Don't offer to generate the typed client`);
|
package/dist/cjs/cli/mcp.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Readable, Writable } from 'node:stream';
|
|
2
|
+
import pg from 'pg';
|
|
2
3
|
import { type ColumnMetadata, type RelationDef } from '../schema.js';
|
|
3
4
|
export interface McpServerOptions {
|
|
4
5
|
url: string;
|
|
@@ -17,6 +18,13 @@ export interface McpServerOptions {
|
|
|
17
18
|
export interface McpTransport {
|
|
18
19
|
input?: Readable;
|
|
19
20
|
output?: Writable;
|
|
21
|
+
/**
|
|
22
|
+
* Pre-built pool, used ONLY by the perimeter tests so they can drive the real
|
|
23
|
+
* JSON-RPC line handler and the real tool handlers with no database (the same
|
|
24
|
+
* reason Studio exports `handleRequest`). Production never sets it: the
|
|
25
|
+
* server builds its own pool from `options.url`.
|
|
26
|
+
*/
|
|
27
|
+
pool?: pg.Pool;
|
|
20
28
|
}
|
|
21
29
|
export interface McpServerHandle {
|
|
22
30
|
dispose(): Promise<void>;
|