turbine-orm 0.75.0 → 0.76.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 +48 -7
- package/dist/cjs/cli/compile-query.d.ts +22 -2
- package/dist/cjs/cli/compile-query.js +49 -5
- package/dist/cjs/cli/config.d.ts +2 -0
- package/dist/cjs/cli/config.js +1 -1
- package/dist/cjs/cli/destructive.js +78 -43
- package/dist/cjs/cli/index.d.ts +95 -1
- package/dist/cjs/cli/index.js +609 -145
- package/dist/cjs/cli/mcp.js +30 -1
- package/dist/cjs/cli/pii-predicate-guard.d.ts +25 -0
- package/dist/cjs/cli/pii-predicate-guard.js +72 -12
- package/dist/cjs/cli/rate-limit.js +38 -1
- package/dist/cjs/cli/studio.js +26 -5
- package/dist/cjs/cli/ui.d.ts +33 -0
- package/dist/cjs/cli/ui.js +53 -7
- package/dist/cjs/client.d.ts +13 -1
- package/dist/cjs/client.js +1 -1
- package/dist/cjs/errors.d.ts +12 -1
- package/dist/cjs/errors.js +11 -2
- package/dist/cjs/generate.d.ts +26 -0
- package/dist/cjs/generate.js +174 -27
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/introspect.d.ts +17 -0
- package/dist/cjs/introspect.js +100 -1
- package/dist/cjs/mssql.d.ts +18 -0
- package/dist/cjs/mssql.js +20 -1
- package/dist/cjs/pipeline.js +44 -6
- package/dist/cjs/powql.js +51 -17
- package/dist/cjs/query/batched-loader.js +3 -3
- package/dist/cjs/query/builder.js +1 -1
- package/dist/cjs/query/relations.d.ts +5 -0
- package/dist/cjs/query/relations.js +141 -69
- package/dist/cjs/query/utils.d.ts +13 -0
- package/dist/cjs/query/utils.js +16 -0
- package/dist/cjs/serverless.d.ts +1 -1
- package/dist/cjs/serverless.js +1 -1
- package/dist/cjs/sqlite.d.ts +33 -1
- package/dist/cjs/sqlite.js +84 -3
- package/dist/cli/compile-query.d.ts +22 -2
- package/dist/cli/compile-query.js +50 -6
- package/dist/cli/config.d.ts +2 -0
- package/dist/cli/config.js +1 -1
- package/dist/cli/destructive.js +78 -43
- package/dist/cli/index.d.ts +95 -1
- package/dist/cli/index.js +604 -147
- package/dist/cli/mcp.js +30 -1
- package/dist/cli/pii-predicate-guard.d.ts +25 -0
- package/dist/cli/pii-predicate-guard.js +73 -13
- package/dist/cli/rate-limit.js +38 -1
- package/dist/cli/studio.js +27 -6
- package/dist/cli/ui.d.ts +33 -0
- package/dist/cli/ui.js +51 -7
- package/dist/client.d.ts +13 -1
- package/dist/client.js +1 -1
- package/dist/errors.d.ts +12 -1
- package/dist/errors.js +11 -2
- package/dist/generate.d.ts +26 -0
- package/dist/generate.js +172 -27
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/introspect.d.ts +17 -0
- package/dist/introspect.js +98 -1
- package/dist/mssql.d.ts +18 -0
- package/dist/mssql.js +20 -1
- package/dist/pipeline.js +44 -6
- package/dist/powql.js +53 -19
- package/dist/query/batched-loader.js +4 -4
- package/dist/query/builder.js +2 -2
- package/dist/query/relations.d.ts +5 -0
- package/dist/query/relations.js +141 -70
- package/dist/query/utils.d.ts +13 -0
- package/dist/query/utils.js +15 -0
- package/dist/serverless.d.ts +1 -1
- package/dist/serverless.js +1 -1
- package/dist/sqlite.d.ts +33 -1
- package/dist/sqlite.js +85 -4
- package/package.json +2 -2
package/dist/cli/index.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* turbine init , Initialize a Turbine project
|
|
7
7
|
* turbine generate | pull , Introspect database and generate TypeScript types
|
|
8
8
|
* turbine migrate-from-prisma - Parse a schema.prisma and emit a Prisma->Turbine name map + report
|
|
9
|
-
* turbine push - Apply schema-builder definitions to database (destructive ops gated)
|
|
9
|
+
* turbine push - Apply schema-builder definitions to database (--schema-file, destructive ops gated)
|
|
10
10
|
* turbine migrate create <name> - Create a new SQL migration file (--auto | --from-diff | --recipe <name>)
|
|
11
11
|
* turbine migrate up , Apply pending migrations
|
|
12
12
|
* turbine migrate deploy , Apply pending migrations without prompts
|
|
@@ -30,13 +30,14 @@ import { tmpdir } from 'node:os';
|
|
|
30
30
|
import { basename, dirname, extname, join, relative, resolve, sep } from 'node:path';
|
|
31
31
|
import { pathToFileURL } from 'node:url';
|
|
32
32
|
import { detectPooler, poolerRefusalMessage } from '../connection-url.js';
|
|
33
|
-
import { generate, generatePrismaMap } from '../generate.js';
|
|
33
|
+
import { generate, generatePrismaMap, resolveImportExtension } from '../generate.js';
|
|
34
34
|
import { buildCreateIndexSql, buildCreateIndexStatements, buildDropIndexSql, collectDoctorProbeIndexNames, collectRelationProbeColumns, findMissingRelationIndexes, } from '../index-advisor.js';
|
|
35
35
|
import { auditDoctorIndexes, collectStatsSnapshot, collectTableHeat, findInvalidIndexes, findRedundantIndexes, findUnusedIndexes, formatBytes, isSnapshotUsable, STATS_THRESHOLDS, scoreMissingIndex, } from '../index-stats.js';
|
|
36
36
|
import { introspect } from '../introspect.js';
|
|
37
37
|
import { collectDivergenceCandidateColumns, collectDivergenceOrderColumns, findPlanDivergence, PLAN_DIVERGENCE_THRESHOLDS, } from '../plan-divergence.js';
|
|
38
38
|
import { applyFlipVerdicts, emptyFlipProbeResult, needsFlipProbe, probePlanFlips } from '../plan-flip-probe.js';
|
|
39
39
|
import { fingerprintPrismaSchema } from '../prisma-schema-fingerprint.js';
|
|
40
|
+
import { closestName } from '../query/utils.js';
|
|
40
41
|
import { snakeToCamel } from '../schema.js';
|
|
41
42
|
import { DestructivePushRefusal, schemaDiff, schemaPush } from '../schema-sql.js';
|
|
42
43
|
import { configTemplate, connectionStringHasPassword, DEFAULT_INIT_SEED_FILE, findConfigFile, loadConfigResult, looksLikeSchemaFilePath, resolveConfig, resolveSeedFile, unwrapModuleDefault, } from './config.js';
|
|
@@ -49,7 +50,7 @@ import { formatPrismaReport, summaryLines } from './prisma-report.js';
|
|
|
49
50
|
import { DEFAULT_EXCLUDED_TABLES, resolvePrismaSchema } from './prisma-resolve.js';
|
|
50
51
|
import { PrismaParseError, parsePrismaSchema, resolvePrismaDatasourceUrl, } from './prisma-schema.js';
|
|
51
52
|
import { startStudio } from './studio.js';
|
|
52
|
-
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';
|
|
53
|
+
import { banner, blue, bold, box, cyan, dim, divider, elapsed, error, errorBanner, errorLine, table as formatTable, gray, green, header, info, label, magenta, newline, red, redactUrl, Spinner, success, symbols, warn, yellow, } from './ui.js';
|
|
53
54
|
/**
|
|
54
55
|
* Fail an argument-parse error the way every other CLI failure looks: banner,
|
|
55
56
|
* red error line, then indented hint lines. The flag validators used to call
|
|
@@ -58,11 +59,227 @@ import { banner, blue, bold, box, cyan, dim, divider, elapsed, error, table as f
|
|
|
58
59
|
* rather than "you forgot the recipe name".
|
|
59
60
|
*/
|
|
60
61
|
function failArg(message, ...hints) {
|
|
61
|
-
|
|
62
|
+
errorBanner();
|
|
62
63
|
error(message);
|
|
63
64
|
for (const hint of hints)
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
errorLine(dim(hint));
|
|
66
|
+
errorLine();
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// The flag surface, as data
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
/**
|
|
73
|
+
* Long flags accepted by EVERY command.
|
|
74
|
+
*
|
|
75
|
+
* Deliberately generous: these are connection/output overrides and diagnostics,
|
|
76
|
+
* so accepting one where it happens to be inert costs nothing. Nothing that
|
|
77
|
+
* changes what a command DOES to your data belongs here, which is why
|
|
78
|
+
* `--dry-run`, `--allow-destructive`, `--fix` and `--yes` are all per-command
|
|
79
|
+
* below.
|
|
80
|
+
*/
|
|
81
|
+
const GLOBAL_FLAGS = [
|
|
82
|
+
'--url',
|
|
83
|
+
'--out',
|
|
84
|
+
'--schema',
|
|
85
|
+
'--include',
|
|
86
|
+
'--exclude',
|
|
87
|
+
'--verbose',
|
|
88
|
+
'--force',
|
|
89
|
+
'--help',
|
|
90
|
+
];
|
|
91
|
+
/**
|
|
92
|
+
* The flags each command accepts, BEYOND {@link GLOBAL_FLAGS}. Long spellings
|
|
93
|
+
* only; short forms come from {@link FLAG_ALIAS}.
|
|
94
|
+
*
|
|
95
|
+
* This table is the CLI's flag surface as data, for the same reason
|
|
96
|
+
* `query/option-surface.ts` is the query surface as data: an unknown flag used
|
|
97
|
+
* to be DISCARDED by the parser's `default:` branch, so `push --dry-runn`
|
|
98
|
+
* executed for real, `migrate create x --autoo` wrote an empty template and
|
|
99
|
+
* reported success, and `doctor --fixx` wrote nothing and exited 0. A typo in a
|
|
100
|
+
* safety flag silently disarmed the safety. Every entry here is cross-checked
|
|
101
|
+
* against the parser's own `case` labels by
|
|
102
|
+
* `src/test/cli-unknown-flags.test.ts`, so adding a flag to one and not the
|
|
103
|
+
* other fails the build rather than stranding it.
|
|
104
|
+
*/
|
|
105
|
+
const COMMAND_FLAGS = {
|
|
106
|
+
init: ['--yes', '--schema-file', '--skip-schema', '--with-schema', '--skip-seed', '--skip-push', '--skip-generate'],
|
|
107
|
+
generate: [
|
|
108
|
+
'--zod',
|
|
109
|
+
'--include-views',
|
|
110
|
+
'--no-timestamp',
|
|
111
|
+
'--import-ext',
|
|
112
|
+
'--keep-column-names',
|
|
113
|
+
'--legacy-to-many-uniques',
|
|
114
|
+
'--allow-empty',
|
|
115
|
+
],
|
|
116
|
+
'migrate-from-prisma': ['--no-db', '--if-db', '--allow-partial', '--no-timestamp'],
|
|
117
|
+
push: ['--schema-file', '--dry-run', '--allow-destructive'],
|
|
118
|
+
migrate: [
|
|
119
|
+
'--schema-file',
|
|
120
|
+
'--auto',
|
|
121
|
+
'--from-diff',
|
|
122
|
+
'--recipe',
|
|
123
|
+
'--step',
|
|
124
|
+
'--dry-run',
|
|
125
|
+
'--allow-drift',
|
|
126
|
+
'--allow-destructive',
|
|
127
|
+
],
|
|
128
|
+
seed: [],
|
|
129
|
+
status: [],
|
|
130
|
+
doctor: [
|
|
131
|
+
'--fix',
|
|
132
|
+
'--json',
|
|
133
|
+
'--no-concurrently',
|
|
134
|
+
'--unused',
|
|
135
|
+
'--audit',
|
|
136
|
+
'--min-scans',
|
|
137
|
+
'--metrics-url',
|
|
138
|
+
'--no-plan-divergence',
|
|
139
|
+
'--allow-pooler',
|
|
140
|
+
],
|
|
141
|
+
studio: ['--port', '--host', '--no-open', '--allow-remote', '--write', '--show-pii', '--demo'],
|
|
142
|
+
mcp: [],
|
|
143
|
+
observe: ['--port', '--host', '--no-open', '--allow-remote', '--metrics-url'],
|
|
144
|
+
skill: ['--print', '--agents', '--dir'],
|
|
145
|
+
help: [],
|
|
146
|
+
version: [],
|
|
147
|
+
};
|
|
148
|
+
/** Command spellings that dispatch to another command's handler. */
|
|
149
|
+
const COMMAND_ALIASES = {
|
|
150
|
+
gen: 'generate',
|
|
151
|
+
g: 'generate',
|
|
152
|
+
pull: 'generate',
|
|
153
|
+
migration: 'migrate',
|
|
154
|
+
m: 'migrate',
|
|
155
|
+
s: 'seed',
|
|
156
|
+
info: 'status',
|
|
157
|
+
};
|
|
158
|
+
/**
|
|
159
|
+
* Long flag to its alternative spelling, for MATCHING and for display.
|
|
160
|
+
*
|
|
161
|
+
* Kept out of {@link COMMAND_FLAGS} so a command lists each flag once: the
|
|
162
|
+
* "valid flags" block a rejection prints reads as one flag per idea, with its
|
|
163
|
+
* short form attached, rather than as a list twice as long.
|
|
164
|
+
*/
|
|
165
|
+
const FLAG_ALIAS = {
|
|
166
|
+
'--url': '-u',
|
|
167
|
+
'--out': '-o',
|
|
168
|
+
'--schema': '-s',
|
|
169
|
+
'--step': '-n',
|
|
170
|
+
'--yes': '-y',
|
|
171
|
+
'--force': '-f',
|
|
172
|
+
'--verbose': '-v',
|
|
173
|
+
'--help': '-h',
|
|
174
|
+
'--import-ext': '--import-extension',
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* The canonical name of `command`, or undefined when nothing dispatches it.
|
|
178
|
+
*
|
|
179
|
+
* @internal exported for tests.
|
|
180
|
+
*/
|
|
181
|
+
export function canonicalCommand(command) {
|
|
182
|
+
const resolved = COMMAND_ALIASES[command] ?? command;
|
|
183
|
+
return resolved in COMMAND_FLAGS ? resolved : undefined;
|
|
184
|
+
}
|
|
185
|
+
/** Every command name a user could reasonably have meant, canonical spellings only. */
|
|
186
|
+
export function knownCommands() {
|
|
187
|
+
return Object.keys(COMMAND_FLAGS);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* The long flags `command` accepts (its own, then the global ones), or
|
|
191
|
+
* undefined when the command itself is unrecognized.
|
|
192
|
+
*
|
|
193
|
+
* An unknown command returns undefined rather than an empty list on purpose:
|
|
194
|
+
* `turbine genrate --url ...` should be told the COMMAND is misspelled, not
|
|
195
|
+
* handed a flag error for a flag that is perfectly valid on the command it
|
|
196
|
+
* meant.
|
|
197
|
+
*
|
|
198
|
+
* @internal exported for tests.
|
|
199
|
+
*/
|
|
200
|
+
export function flagsForCommand(command) {
|
|
201
|
+
const canonical = canonicalCommand(command);
|
|
202
|
+
if (canonical === undefined)
|
|
203
|
+
return undefined;
|
|
204
|
+
return { own: COMMAND_FLAGS[canonical] ?? [], global: GLOBAL_FLAGS };
|
|
205
|
+
}
|
|
206
|
+
/** Every token (long + alias) `command` accepts, or undefined for an unknown command. */
|
|
207
|
+
function acceptedFlagTokens(command) {
|
|
208
|
+
const flags = flagsForCommand(command);
|
|
209
|
+
if (!flags)
|
|
210
|
+
return undefined;
|
|
211
|
+
const tokens = new Set();
|
|
212
|
+
for (const flag of [...flags.own, ...flags.global]) {
|
|
213
|
+
tokens.add(flag);
|
|
214
|
+
const alias = FLAG_ALIAS[flag];
|
|
215
|
+
if (alias)
|
|
216
|
+
tokens.add(alias);
|
|
217
|
+
}
|
|
218
|
+
return tokens;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Every flag token any command accepts, long spellings and aliases alike.
|
|
222
|
+
*
|
|
223
|
+
* Exists for `src/test/cli-arg-safety.test.ts`, which cross-checks it against
|
|
224
|
+
* the `case` labels in {@link parseArgs} in both directions. A flag in the
|
|
225
|
+
* parser but in no command's list is unreachable (the validator rejects it
|
|
226
|
+
* before the case runs); a flag in a list with no case is accepted and then
|
|
227
|
+
* silently ignored, which is the bug this whole surface exists to end.
|
|
228
|
+
*
|
|
229
|
+
* @internal exported for tests.
|
|
230
|
+
*/
|
|
231
|
+
export function allFlagTokens() {
|
|
232
|
+
const tokens = new Set();
|
|
233
|
+
for (const command of knownCommands()) {
|
|
234
|
+
for (const token of acceptedFlagTokens(command) ?? [])
|
|
235
|
+
tokens.add(token);
|
|
236
|
+
}
|
|
237
|
+
return tokens;
|
|
238
|
+
}
|
|
239
|
+
/** `--url` rendered for the help block: `--url, -u`. */
|
|
240
|
+
function displayFlag(flag) {
|
|
241
|
+
const alias = FLAG_ALIAS[flag];
|
|
242
|
+
return alias ? `${flag}, ${alias}` : flag;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Reject the unknown flags on a recognized command, naming the closest real one.
|
|
246
|
+
*
|
|
247
|
+
* `closestName` rather than `suggestKey`: the second pass `suggestKey` adds is a
|
|
248
|
+
* camelCase-word-subsequence match, which does nothing for kebab-case flags.
|
|
249
|
+
* Long spellings ONLY are offered as candidates, because `nameCloseness` scores
|
|
250
|
+
* substring containment above edit distance and every short flag is a substring
|
|
251
|
+
* of something: `--no-opn` contains `-o`, which would otherwise outrank the
|
|
252
|
+
* `--no-open` the user obviously meant.
|
|
253
|
+
*/
|
|
254
|
+
function failUnknownFlags(command, unknown) {
|
|
255
|
+
const flags = flagsForCommand(command);
|
|
256
|
+
const canonical = canonicalCommand(command) ?? command;
|
|
257
|
+
errorBanner();
|
|
258
|
+
const plural = unknown.length > 1 ? 's' : '';
|
|
259
|
+
error(`Unknown flag${plural} for ${cyan(`turbine ${canonical}`)}: ${unknown.map((f) => bold(f)).join(' ')}`);
|
|
260
|
+
errorLine();
|
|
261
|
+
if (flags) {
|
|
262
|
+
for (const flag of unknown) {
|
|
263
|
+
const suggestion = closestName(flag, [...flags.own, ...flags.global]);
|
|
264
|
+
if (!suggestion)
|
|
265
|
+
continue;
|
|
266
|
+
// With one bad flag the headline already names it; with several, each
|
|
267
|
+
// suggestion has to say which flag it is about.
|
|
268
|
+
const subject = unknown.length > 1 ? `${bold(flag)} ${symbols.arrow} ` : '';
|
|
269
|
+
errorLine(`${subject}${dim('Did you mean')} ${cyan(displayFlag(suggestion))}${dim('?')}`);
|
|
270
|
+
}
|
|
271
|
+
if (flags.own.length > 0) {
|
|
272
|
+
errorLine();
|
|
273
|
+
errorLine(dim(`Flags for ${canonical}:`));
|
|
274
|
+
errorLine(` ${flags.own.map(displayFlag).join(' ')}`);
|
|
275
|
+
}
|
|
276
|
+
errorLine();
|
|
277
|
+
errorLine(dim('Flags for every command:'));
|
|
278
|
+
errorLine(` ${flags.global.map(displayFlag).join(' ')}`);
|
|
279
|
+
}
|
|
280
|
+
errorLine();
|
|
281
|
+
errorLine(`${dim('Run')} ${cyan(`npx turbine ${canonical} --help`)} ${dim('for the full list.')}`);
|
|
282
|
+
errorLine();
|
|
66
283
|
process.exit(1);
|
|
67
284
|
}
|
|
68
285
|
export function parseArgs(argv = process.argv.slice(2)) {
|
|
@@ -77,9 +294,20 @@ export function parseArgs(argv = process.argv.slice(2)) {
|
|
|
77
294
|
result.subcommand = args[i];
|
|
78
295
|
i++;
|
|
79
296
|
}
|
|
297
|
+
// The flags this command accepts. `undefined` means the COMMAND is unknown,
|
|
298
|
+
// in which case nothing here validates flags: main() reports the misspelled
|
|
299
|
+
// command instead, which is the actual problem.
|
|
300
|
+
const accepted = acceptedFlagTokens(result.command);
|
|
301
|
+
const unknownFlags = [];
|
|
80
302
|
for (; i < args.length; i++) {
|
|
81
303
|
const arg = args[i];
|
|
82
304
|
const next = args[i + 1];
|
|
305
|
+
// Checked BEFORE the switch, and on the same cursor, so a flag's VALUE
|
|
306
|
+
// (`--step -1`, `--host -x`) is consumed by its own case and never reaches
|
|
307
|
+
// this test. A bare `-`-prefixed token that is not a flag reaches it and is
|
|
308
|
+
// rejected: no command takes a negative-number positional.
|
|
309
|
+
if (accepted && arg.startsWith('-') && !accepted.has(arg))
|
|
310
|
+
unknownFlags.push(arg);
|
|
83
311
|
switch (arg) {
|
|
84
312
|
case '--url':
|
|
85
313
|
case '-u':
|
|
@@ -96,6 +324,10 @@ export function parseArgs(argv = process.argv.slice(2)) {
|
|
|
96
324
|
result.schema = next;
|
|
97
325
|
i++;
|
|
98
326
|
break;
|
|
327
|
+
case '--schema-file':
|
|
328
|
+
result.schemaFile = next;
|
|
329
|
+
i++;
|
|
330
|
+
break;
|
|
99
331
|
case '--include':
|
|
100
332
|
result.include = next?.split(',');
|
|
101
333
|
i++;
|
|
@@ -267,6 +499,10 @@ export function parseArgs(argv = process.argv.slice(2)) {
|
|
|
267
499
|
break;
|
|
268
500
|
}
|
|
269
501
|
}
|
|
502
|
+
// Reported once, after the whole line is parsed, so two typos on one command
|
|
503
|
+
// are two lines of one error rather than two runs of the CLI.
|
|
504
|
+
if (unknownFlags.length > 0)
|
|
505
|
+
failUnknownFlags(result.command, unknownFlags);
|
|
270
506
|
return result;
|
|
271
507
|
}
|
|
272
508
|
// ---------------------------------------------------------------------------
|
|
@@ -277,35 +513,35 @@ export function parseArgs(argv = process.argv.slice(2)) {
|
|
|
277
513
|
* Called when we know we need to load a `.ts` file but the loader isn't available.
|
|
278
514
|
*/
|
|
279
515
|
function failMissingTsLoader(filePath, reason) {
|
|
280
|
-
|
|
516
|
+
errorLine();
|
|
281
517
|
error(`Cannot load TypeScript file: ${filePath}`);
|
|
282
|
-
|
|
518
|
+
errorLine();
|
|
283
519
|
if (reason === 'unsupported') {
|
|
284
|
-
|
|
285
|
-
|
|
520
|
+
errorLine(`${dim('Your Node.js version does not support')} ${cyan('module.register()')}.`);
|
|
521
|
+
errorLine(`${dim('Upgrade to Node.js')} ${cyan('20.6+')} ${dim('or use a')} ${cyan('.js')} ${dim('/')} ${cyan('.mjs')} ${dim('config file.')}`);
|
|
286
522
|
}
|
|
287
523
|
else if (reason === 'failed') {
|
|
288
524
|
// tsx IS installed but registering its loader threw. Report the real
|
|
289
525
|
// cause, telling the user to install tsx here would be a misdiagnosis.
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
526
|
+
errorLine(`${dim('tsx is installed, but registering its TypeScript loader failed:')}`);
|
|
527
|
+
errorLine();
|
|
528
|
+
errorLine(` ${getTsLoaderError() ?? '(unknown error)'}`);
|
|
529
|
+
errorLine();
|
|
530
|
+
errorLine(`${dim('Try upgrading tsx:')} ${cyan('npm install --save-dev tsx@latest')}${dim(', or rename your file to')} ${cyan('.mjs')}.`);
|
|
295
531
|
}
|
|
296
532
|
else {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
308
|
-
|
|
533
|
+
errorLine(`${dim('Loading .ts config / schema files requires')} ${cyan('tsx')} ${dim('to be installed.')}`);
|
|
534
|
+
errorLine();
|
|
535
|
+
errorLine(`${dim('Install it as a dev dependency:')}`);
|
|
536
|
+
errorLine(` ${cyan('npm install --save-dev tsx')}`);
|
|
537
|
+
errorLine(` ${dim('or')}`);
|
|
538
|
+
errorLine(` ${cyan('pnpm add -D tsx')}`);
|
|
539
|
+
errorLine(` ${dim('or')}`);
|
|
540
|
+
errorLine(` ${cyan('yarn add -D tsx')}`);
|
|
541
|
+
errorLine();
|
|
542
|
+
errorLine(`${dim('Alternatively, rename your file to')} ${cyan('.js')} ${dim('or')} ${cyan('.mjs')}.`);
|
|
543
|
+
}
|
|
544
|
+
errorLine();
|
|
309
545
|
process.exit(1);
|
|
310
546
|
}
|
|
311
547
|
// ---------------------------------------------------------------------------
|
|
@@ -354,20 +590,20 @@ export async function bootstrapCliConfig(args, overrides) {
|
|
|
354
590
|
function requireUrl(config, options = {}) {
|
|
355
591
|
if (!config.url) {
|
|
356
592
|
error('No database URL provided.');
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
593
|
+
errorLine();
|
|
594
|
+
errorLine(`${dim('Set it in one of these ways:')}`);
|
|
595
|
+
errorLine(` ${dim('1.')} Add ${cyan('url')} to ${cyan('turbine.config.ts')}`);
|
|
360
596
|
// .env auto-load needs Node 20.12+ (process.loadEnvFile); be honest below it.
|
|
361
597
|
const envFileNote = typeof process.loadEnvFile === 'function' ? '(auto-loaded)' : '(needs Node 20.12+ to auto-load)';
|
|
362
|
-
|
|
363
|
-
|
|
598
|
+
errorLine(` ${dim('2.')} Set ${cyan('DATABASE_URL')} in your environment or a ${cyan('.env')} file ${dim(envFileNote)}`);
|
|
599
|
+
errorLine(` ${dim('3.')} Pass ${cyan('--url')} flag`);
|
|
364
600
|
const vars = options.datasourceVars ?? [];
|
|
365
601
|
if (vars.length > 0) {
|
|
366
602
|
const list = vars.map((v) => cyan(v)).join(', ');
|
|
367
603
|
const plural = vars.length > 1 ? 'these variables' : 'this variable';
|
|
368
|
-
|
|
604
|
+
errorLine(` ${dim('4.')} Set ${list} ${dim(`(${plural}, declared by your schema.prisma datasource, ${vars.length > 1 ? 'are' : 'is'} unset)`)}`);
|
|
369
605
|
}
|
|
370
|
-
|
|
606
|
+
errorLine();
|
|
371
607
|
process.exit(1);
|
|
372
608
|
}
|
|
373
609
|
return config.url;
|
|
@@ -376,7 +612,7 @@ async function loadSchemaFile(schemaFile) {
|
|
|
376
612
|
const absPath = resolve(schemaFile);
|
|
377
613
|
if (!existsSync(absPath)) {
|
|
378
614
|
error(`Schema file not found: ${schemaFile}`);
|
|
379
|
-
|
|
615
|
+
errorLine(`${dim('Create one with:')} ${cyan('turbine init')}`);
|
|
380
616
|
process.exit(1);
|
|
381
617
|
}
|
|
382
618
|
// If this is a TypeScript file, ensure the tsx ESM loader is registered
|
|
@@ -405,11 +641,11 @@ async function loadSchemaFile(schemaFile) {
|
|
|
405
641
|
catch (err) {
|
|
406
642
|
error(`Failed to load schema file: ${schemaFile}`);
|
|
407
643
|
if (err instanceof Error) {
|
|
408
|
-
|
|
644
|
+
errorLine(`${dim(err.message)}`);
|
|
409
645
|
// If the error is the classic ERR_UNKNOWN_FILE_EXTENSION, give a hint.
|
|
410
646
|
if (err.message.includes('ERR_UNKNOWN_FILE_EXTENSION') || err.message.includes('Unknown file extension')) {
|
|
411
|
-
|
|
412
|
-
|
|
647
|
+
errorLine();
|
|
648
|
+
errorLine(`${dim('Hint: install')} ${cyan('tsx')} ${dim('to load .ts files:')} ${cyan('npm install --save-dev tsx')}`);
|
|
413
649
|
}
|
|
414
650
|
printCjsHintIfApplicable(err);
|
|
415
651
|
}
|
|
@@ -428,9 +664,9 @@ function printCjsHintIfApplicable(err) {
|
|
|
428
664
|
if (msg.includes('ERR_REQUIRE_ESM') ||
|
|
429
665
|
msg.includes('require() of ES Module') ||
|
|
430
666
|
msg.includes('Cannot require() ES Module')) {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
667
|
+
errorLine();
|
|
668
|
+
errorLine(`${dim('Hint: add')} ${cyan('"type": "module"')} ${dim('to your')} ${cyan('package.json')}${dim('.')}`);
|
|
669
|
+
errorLine(`${dim('Turbine is an ESM package; without it, Node/tsx tries to')} ${cyan('require()')} ${dim('it and fails.')}`);
|
|
434
670
|
}
|
|
435
671
|
}
|
|
436
672
|
/**
|
|
@@ -732,7 +968,54 @@ export default defineSeed(async (db) => {
|
|
|
732
968
|
console.log('Done!');
|
|
733
969
|
});
|
|
734
970
|
`;
|
|
735
|
-
|
|
971
|
+
/**
|
|
972
|
+
* The starter schema, with two real tables and a foreign key between them.
|
|
973
|
+
*
|
|
974
|
+
* Scaffolded ONLY when the target database has no tables of its own (see
|
|
975
|
+
* {@link initSchemaTemplate}). An all-commented-out schema next to an empty
|
|
976
|
+
* database is a dead end: `push` reports "Database already in sync",
|
|
977
|
+
* `generate` refuses to emit an empty client, and the next-steps text ends up
|
|
978
|
+
* describing a `db.users` that does not exist. Two tables rather than one
|
|
979
|
+
* because the relation is what the very next query in the quickstart reads.
|
|
980
|
+
*/
|
|
981
|
+
const INIT_SCHEMA_EXAMPLE = `/**
|
|
982
|
+
* Turbine schema definition
|
|
983
|
+
*
|
|
984
|
+
* Define your database schema in TypeScript.
|
|
985
|
+
* Use \`npx turbine push\` to sync it to your database, then
|
|
986
|
+
* \`npx turbine generate\` to emit the typed client.
|
|
987
|
+
*
|
|
988
|
+
* This starter is a working example. Rename it, edit it, or replace it
|
|
989
|
+
* entirely: nothing here is special to Turbine.
|
|
990
|
+
*
|
|
991
|
+
* @see https://turbineorm.dev
|
|
992
|
+
*/
|
|
993
|
+
|
|
994
|
+
import { defineSchema } from 'turbine-orm';
|
|
995
|
+
|
|
996
|
+
export default defineSchema({
|
|
997
|
+
users: {
|
|
998
|
+
id: { type: 'serial', primaryKey: true },
|
|
999
|
+
email: { type: 'text', notNull: true, unique: true },
|
|
1000
|
+
name: { type: 'text', notNull: true },
|
|
1001
|
+
created_at: { type: 'timestamptz', notNull: true, default: 'NOW()' },
|
|
1002
|
+
},
|
|
1003
|
+
posts: {
|
|
1004
|
+
id: { type: 'serial', primaryKey: true },
|
|
1005
|
+
user_id: { type: 'integer', notNull: true, references: 'users.id' },
|
|
1006
|
+
title: { type: 'text', notNull: true },
|
|
1007
|
+
published: { type: 'boolean', notNull: true, default: 'false' },
|
|
1008
|
+
created_at: { type: 'timestamptz', notNull: true, default: 'NOW()' },
|
|
1009
|
+
},
|
|
1010
|
+
});
|
|
1011
|
+
`;
|
|
1012
|
+
/**
|
|
1013
|
+
* The starter schema with nothing defined, for the one case where a real
|
|
1014
|
+
* example could collide: `init --with-schema` beside a database that already
|
|
1015
|
+
* has tables. There, an example `users` table is a schema diff against
|
|
1016
|
+
* somebody's real data, so the file stays a comment.
|
|
1017
|
+
*/
|
|
1018
|
+
const INIT_SCHEMA_PLACEHOLDER = `/**
|
|
736
1019
|
* Turbine schema definition
|
|
737
1020
|
*
|
|
738
1021
|
* Define your database schema in TypeScript.
|
|
@@ -744,15 +1027,27 @@ const INIT_SCHEMA_TEMPLATE = `/**
|
|
|
744
1027
|
import { defineSchema } from 'turbine-orm';
|
|
745
1028
|
|
|
746
1029
|
export default defineSchema({
|
|
747
|
-
//
|
|
1030
|
+
// Your database already has tables, so this file starts empty on purpose:
|
|
1031
|
+
// \`turbine push\` applies exactly what is declared here. Describe the tables
|
|
1032
|
+
// you want Turbine to own, or run \`npx turbine generate\` to work from the
|
|
1033
|
+
// database as it already is.
|
|
1034
|
+
//
|
|
748
1035
|
// users: {
|
|
749
1036
|
// id: { type: 'serial', primaryKey: true },
|
|
750
1037
|
// email: { type: 'text', notNull: true, unique: true },
|
|
751
1038
|
// name: { type: 'text', notNull: true },
|
|
752
|
-
// created_at: { type: '
|
|
1039
|
+
// created_at: { type: 'timestamptz', notNull: true, default: 'NOW()' },
|
|
753
1040
|
// },
|
|
754
1041
|
});
|
|
755
1042
|
`;
|
|
1043
|
+
/**
|
|
1044
|
+
* Which starter schema to scaffold.
|
|
1045
|
+
*
|
|
1046
|
+
* @internal exported for tests.
|
|
1047
|
+
*/
|
|
1048
|
+
export function initSchemaTemplate(dbHasTables) {
|
|
1049
|
+
return dbHasTables ? INIT_SCHEMA_PLACEHOLDER : INIT_SCHEMA_EXAMPLE;
|
|
1050
|
+
}
|
|
756
1051
|
/**
|
|
757
1052
|
* Does this `.gitignore` text already ignore `.env`?
|
|
758
1053
|
*
|
|
@@ -905,12 +1200,15 @@ function ensureInitScaffoldDirs(config) {
|
|
|
905
1200
|
}
|
|
906
1201
|
}
|
|
907
1202
|
}
|
|
908
|
-
function writeInitSchemaTemplate(config) {
|
|
1203
|
+
function writeInitSchemaTemplate(config, dbHasTables) {
|
|
909
1204
|
const schemaDir = dirname(config.schemaFile);
|
|
910
1205
|
if (schemaDir && !existsSync(schemaDir))
|
|
911
1206
|
mkdirSync(schemaDir, { recursive: true });
|
|
912
|
-
writeFileSync(config.schemaFile,
|
|
1207
|
+
writeFileSync(config.schemaFile, initSchemaTemplate(dbHasTables), 'utf-8');
|
|
913
1208
|
success(`Created ${cyan(config.schemaFile)}`);
|
|
1209
|
+
if (!dbHasTables) {
|
|
1210
|
+
console.log(` ${dim('It defines a')} ${cyan('users')} ${dim('and a')} ${cyan('posts')} ${dim('table to start from.')}`);
|
|
1211
|
+
}
|
|
914
1212
|
}
|
|
915
1213
|
function writeInitSeedTemplate(seedFilePath) {
|
|
916
1214
|
const seedDir = dirname(seedFilePath);
|
|
@@ -960,6 +1258,28 @@ async function runInitPush(config, url) {
|
|
|
960
1258
|
success(`Applied ${bold(String(result.statementsExecuted))} statement(s)`);
|
|
961
1259
|
}
|
|
962
1260
|
}
|
|
1261
|
+
/**
|
|
1262
|
+
* The exact import line for the generated client, extension included.
|
|
1263
|
+
*
|
|
1264
|
+
* `import { turbine } from './generated/turbine'` is a hard TypeScript error
|
|
1265
|
+
* (TS2834) under `moduleResolution: NodeNext`, which is what this package ships
|
|
1266
|
+
* and what its own tsconfig uses: a relative import needs an explicit file
|
|
1267
|
+
* extension, and NodeNext does no directory-index resolution either. The
|
|
1268
|
+
* generator has always appended the extension to its OWN sibling imports; the
|
|
1269
|
+
* line printed at the reader was the one place it never reached.
|
|
1270
|
+
*
|
|
1271
|
+
* The extension comes from {@link resolveImportExtension}, the same resolver the
|
|
1272
|
+
* generator runs, so the printed line matches the files just written rather
|
|
1273
|
+
* than a second guess about the consumer's tsconfig. Under bundler resolution
|
|
1274
|
+
* it resolves to `''` and the directory form is correct as-is.
|
|
1275
|
+
*
|
|
1276
|
+
* @internal exported for tests.
|
|
1277
|
+
*/
|
|
1278
|
+
export function generatedClientImport(config) {
|
|
1279
|
+
const dir = `./${config.out.replace(/^\.\//, '').replace(/\/+$/, '')}`;
|
|
1280
|
+
const { ext } = resolveImportExtension(config.out, config.importExtension);
|
|
1281
|
+
return `import { turbine } from '${ext === '' ? dir : `${dir}/index${ext}`}';`;
|
|
1282
|
+
}
|
|
963
1283
|
/** Introspect the database and generate the typed client. */
|
|
964
1284
|
async function runInitGenerate(config, url) {
|
|
965
1285
|
const spinner = new Spinner('Introspecting database').start();
|
|
@@ -971,16 +1291,22 @@ async function runInitGenerate(config, url) {
|
|
|
971
1291
|
exclude: config.exclude.length ? config.exclude : undefined,
|
|
972
1292
|
relationNames: config.relationNames,
|
|
973
1293
|
});
|
|
974
|
-
|
|
1294
|
+
const tableNames = Object.keys(schema.tables);
|
|
1295
|
+
spinner.succeed(`Found ${bold(String(tableNames.length))} tables`);
|
|
975
1296
|
const genSpinner = new Spinner('Generating TypeScript client').start();
|
|
976
1297
|
const result = generate({ schema, outDir: config.out, connectionString: url });
|
|
977
1298
|
genSpinner.succeed(`Generated ${bold(String(result.files.length))} files to ${cyan(`${config.out}/`)}`);
|
|
1299
|
+
// The accessor name the generator emits is snakeToCamel of the table name
|
|
1300
|
+
// (generate.ts, generateIndex), so this is the spelling that autocompletes.
|
|
1301
|
+
const first = tableNames[0];
|
|
1302
|
+
return { tableCount: tableNames.length, sampleAccessor: first ? snakeToCamel(first) : undefined };
|
|
978
1303
|
}
|
|
979
1304
|
catch (err) {
|
|
980
1305
|
spinner.fail('Could not generate client');
|
|
981
1306
|
if (err instanceof Error)
|
|
982
|
-
|
|
1307
|
+
errorLine(dim(redactUrl(err.message)));
|
|
983
1308
|
info(`Run generation later with: ${cyan('npx turbine generate')}`);
|
|
1309
|
+
return { tableCount: null };
|
|
984
1310
|
}
|
|
985
1311
|
}
|
|
986
1312
|
/** Run the seed file. */
|
|
@@ -1204,6 +1530,10 @@ async function cmdInit(args, config) {
|
|
|
1204
1530
|
newline();
|
|
1205
1531
|
// TypeScript files this run actually created: they drive the tsx heads-up below.
|
|
1206
1532
|
const tsFilesWritten = [];
|
|
1533
|
+
// What the generate step produced, if it ran. The next-steps text is derived
|
|
1534
|
+
// from this rather than assumed, so it cannot promise a `db.users` the
|
|
1535
|
+
// generated client does not have.
|
|
1536
|
+
let generated = { tableCount: null };
|
|
1207
1537
|
// Execute the plan in order. `run` proceeds; `prompt` asks; `skip` reports.
|
|
1208
1538
|
for (const step of plan) {
|
|
1209
1539
|
if (step.action === 'skip') {
|
|
@@ -1231,7 +1561,7 @@ async function cmdInit(args, config) {
|
|
|
1231
1561
|
break;
|
|
1232
1562
|
}
|
|
1233
1563
|
case 'schema':
|
|
1234
|
-
writeInitSchemaTemplate(config);
|
|
1564
|
+
writeInitSchemaTemplate(config, state.dbHasTables === true);
|
|
1235
1565
|
if (needsTsLoader(config.schemaFile))
|
|
1236
1566
|
tsFilesWritten.push(config.schemaFile);
|
|
1237
1567
|
break;
|
|
@@ -1244,7 +1574,7 @@ async function cmdInit(args, config) {
|
|
|
1244
1574
|
await runInitPush(config, url);
|
|
1245
1575
|
break;
|
|
1246
1576
|
case 'generate':
|
|
1247
|
-
await runInitGenerate(config, url);
|
|
1577
|
+
generated = await runInitGenerate(config, url);
|
|
1248
1578
|
break;
|
|
1249
1579
|
case 'seed-run':
|
|
1250
1580
|
await runInitSeed(config);
|
|
@@ -1283,20 +1613,84 @@ async function cmdInit(args, config) {
|
|
|
1283
1613
|
if (!tsxMissing && !canResolveTsx()) {
|
|
1284
1614
|
console.log(` ${dim('Note: the TypeScript config requires')} ${cyan('tsx')}: ${cyan(tsxInstallCommand(detectPackageManager()))}`);
|
|
1285
1615
|
}
|
|
1616
|
+
newline();
|
|
1617
|
+
console.log(` ${dim('3.')} Create migrations: ${cyan('npx turbine migrate create <name>')}`);
|
|
1618
|
+
console.log(` ${dim('4.')} Run migrations: ${cyan('npx turbine migrate up')}`);
|
|
1619
|
+
console.log(` ${dim('5.')} Seed your database: ${cyan('npx turbine seed')}`);
|
|
1620
|
+
}
|
|
1621
|
+
else if (generated.tableCount === null || generated.tableCount === 0) {
|
|
1622
|
+
// There is no typed client to import: either generate did not run (push /
|
|
1623
|
+
// generate declined or skipped), or it ran against a database with no
|
|
1624
|
+
// tables and emitted a client with no accessors. Printing
|
|
1625
|
+
// `await db.users.findMany()` here is the single most misleading line the
|
|
1626
|
+
// CLI produces, because the reader has no way to know it cannot work.
|
|
1627
|
+
console.log(` ${dim('1.')} Describe your tables in ${cyan(config.schemaFile)}`);
|
|
1628
|
+
console.log(` ${dim('2.')} Create them: ${cyan('npx turbine push')}`);
|
|
1629
|
+
console.log(` ${dim('3.')} Emit the client: ${cyan('npx turbine generate')}`);
|
|
1630
|
+
newline();
|
|
1631
|
+
console.log(` ${dim('Until a table exists, the generated client has no table accessors, so')}`);
|
|
1632
|
+
console.log(` ${dim('there is nothing to import yet. Working from a database you already')}`);
|
|
1633
|
+
console.log(` ${dim('have? Skip step 1 and run')} ${cyan('npx turbine generate')} ${dim('on its own.')}`);
|
|
1634
|
+
newline();
|
|
1635
|
+
console.log(` ${dim('Nothing to connect to yet?')} ${cyan('npx turbine studio --demo')} ${dim('needs no database.')}`);
|
|
1286
1636
|
}
|
|
1287
1637
|
else {
|
|
1638
|
+
const accessor = generated.sampleAccessor ?? 'users';
|
|
1288
1639
|
console.log(` ${dim('1.')} Import the generated client:`);
|
|
1289
|
-
console.log(` ${cyan(
|
|
1640
|
+
console.log(` ${cyan(generatedClientImport(config))}`);
|
|
1290
1641
|
newline();
|
|
1291
1642
|
console.log(` ${dim('2.')} Create a connection and query:`);
|
|
1292
1643
|
console.log(` ${dim('const db = turbine();')}`);
|
|
1293
|
-
console.log(` ${dim(
|
|
1644
|
+
console.log(` ${dim(`const rows = await db.${accessor}.findMany();`)}`);
|
|
1645
|
+
newline();
|
|
1646
|
+
console.log(` ${dim('3.')} Create migrations: ${cyan('npx turbine migrate create <name>')}`);
|
|
1647
|
+
console.log(` ${dim('4.')} Run migrations: ${cyan('npx turbine migrate up')}`);
|
|
1648
|
+
console.log(` ${dim('5.')} Seed your database: ${cyan('npx turbine seed')}`);
|
|
1294
1649
|
}
|
|
1295
1650
|
newline();
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1651
|
+
}
|
|
1652
|
+
// ---------------------------------------------------------------------------
|
|
1653
|
+
// `--schema` is the namespace, `--schema-file` is the file
|
|
1654
|
+
// ---------------------------------------------------------------------------
|
|
1655
|
+
/**
|
|
1656
|
+
* Refuse a `schema` that is plainly a FILE PATH, on every command that reads it
|
|
1657
|
+
* as a Postgres namespace.
|
|
1658
|
+
*
|
|
1659
|
+
* `--schema` / `-s` sets the namespace to introspect (default `public`). The
|
|
1660
|
+
* defineSchema() file is a different idea entirely, and mistaking the two is
|
|
1661
|
+
* silent on every command that used to accept it: `generate` introspects
|
|
1662
|
+
* `WHERE table_schema = './turbine/schema.ts'` and matches nothing, and `push`
|
|
1663
|
+
* reads `config.schemaFile` instead and reports
|
|
1664
|
+
* "Schema file not found: ./turbine/schema.ts", naming a path the reader never
|
|
1665
|
+
* typed while their schema file sits in the directory they ran it from.
|
|
1666
|
+
*
|
|
1667
|
+
* The check was written for `generate` and wired into `generate` alone, which is
|
|
1668
|
+
* how `push`, the command whose flag name the mistake is actually about, kept
|
|
1669
|
+
* the bad error. It is one function called from every `--schema` command now,
|
|
1670
|
+
* for the same reason `resolveColumnName` is one function: two copies of a rule
|
|
1671
|
+
* is how two commands come to disagree about whether an argument is valid.
|
|
1672
|
+
*
|
|
1673
|
+
* @internal exported for tests.
|
|
1674
|
+
*/
|
|
1675
|
+
export function refuseSchemaFilePath(config, options = {}) {
|
|
1676
|
+
if (!looksLikeSchemaFilePath(config.schema))
|
|
1677
|
+
return;
|
|
1678
|
+
error(`The ${cyan('--schema')} value looks like a file path: ${cyan(config.schema)}`);
|
|
1679
|
+
errorLine();
|
|
1680
|
+
errorLine(`${dim('Did you mean')} ${cyan(`--schema-file ${config.schema}`)}${dim('?')}`);
|
|
1681
|
+
errorLine();
|
|
1682
|
+
errorLine(`${dim('--schema is the Postgres schema NAME to read')} ${dim('(default:')} ${cyan('public')}${dim(').')}`);
|
|
1683
|
+
errorLine(`${dim('--schema-file is the path to your')} ${cyan('defineSchema()')} ${dim('file')} ${dim(`(default: ${config.schemaFile}).`)}`);
|
|
1684
|
+
errorLine();
|
|
1685
|
+
errorLine(`${dim('Or set it once in')} ${cyan('turbine.config.ts')}${dim(':')}`);
|
|
1686
|
+
errorLine(` ${green('schema:')} ${cyan("'public'")}${dim(", // or omit, reads the 'public' schema")}`);
|
|
1687
|
+
errorLine(` ${green('schemaFile:')} ${cyan(`'${config.schema}'`)}${dim(', // your defineSchema() file (used by `turbine push`)')}`);
|
|
1688
|
+
if (options.escapeHatch) {
|
|
1689
|
+
errorLine();
|
|
1690
|
+
errorLine(`${dim('Re-run with')} ${cyan(options.escapeHatch)} ${dim('to use this literal schema name anyway.')}`);
|
|
1691
|
+
}
|
|
1692
|
+
errorLine();
|
|
1693
|
+
process.exit(1);
|
|
1300
1694
|
}
|
|
1301
1695
|
// ---------------------------------------------------------------------------
|
|
1302
1696
|
// Command: generate (pull)
|
|
@@ -1305,24 +1699,11 @@ async function cmdGenerate(args, config) {
|
|
|
1305
1699
|
banner();
|
|
1306
1700
|
const url = requireUrl(config);
|
|
1307
1701
|
const startTime = performance.now();
|
|
1308
|
-
//
|
|
1309
|
-
//
|
|
1310
|
-
//
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
error(`The "schema" option looks like a file path: ${cyan(config.schema)}`);
|
|
1314
|
-
newline();
|
|
1315
|
-
console.log(` ${dim('"schema" is the Postgres schema NAME to introspect')} ${dim('(default:')} ${cyan('public')}${dim(').')}`);
|
|
1316
|
-
console.log(` ${dim('The path to your defineSchema() file belongs in')} ${cyan('schemaFile')}${dim('.')}`);
|
|
1317
|
-
newline();
|
|
1318
|
-
console.log(` ${dim('Fix your')} ${cyan('turbine.config.ts')}${dim(':')}`);
|
|
1319
|
-
console.log(` ${green('schema:')} ${cyan("'public'")}${dim(", // or omit, introspects the 'public' schema")}`);
|
|
1320
|
-
console.log(` ${green('schemaFile:')} ${cyan(`'${config.schema}'`)}${dim(', // your defineSchema() file (used by `turbine push`)')}`);
|
|
1321
|
-
newline();
|
|
1322
|
-
console.log(` ${dim('Re-run with')} ${cyan('--allow-empty')} ${dim('to introspect this literal schema name anyway.')}`);
|
|
1323
|
-
newline();
|
|
1324
|
-
process.exit(1);
|
|
1325
|
-
}
|
|
1702
|
+
// `--allow-empty` is generate's documented escape hatch for introspecting a
|
|
1703
|
+
// literal schema name that happens to look like a path, so it SKIPS the
|
|
1704
|
+
// refusal rather than merely being named by it.
|
|
1705
|
+
if (args.allowEmpty !== true)
|
|
1706
|
+
refuseSchemaFilePath(config, { escapeHatch: '--allow-empty' });
|
|
1326
1707
|
label('Database', redactUrl(url));
|
|
1327
1708
|
label('Schema', config.schema);
|
|
1328
1709
|
label('Output', config.out);
|
|
@@ -1356,15 +1737,15 @@ async function cmdGenerate(args, config) {
|
|
|
1356
1737
|
if (tableNames.length === 0 && !args.allowEmpty) {
|
|
1357
1738
|
newline();
|
|
1358
1739
|
error(`Introspection matched 0 tables in schema ${cyan(config.schema)}, refusing to generate an empty client.`);
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1740
|
+
errorLine();
|
|
1741
|
+
errorLine(`${dim('Common causes:')}`);
|
|
1742
|
+
errorLine(` ${dim('•')} ${cyan('schema')} ${dim('points at the wrong Postgres namespace')} ${dim('(it is the schema NAME, default')} ${cyan('public')}${dim(').')}`);
|
|
1743
|
+
errorLine(` ${dim('•')} ${dim('You meant to set')} ${cyan('schemaFile')} ${dim('(your defineSchema() file), not')} ${cyan('schema')}${dim('.')}`);
|
|
1744
|
+
errorLine(` ${dim('•')} ${cyan('include')}/${cyan('exclude')} ${dim('filtered out every table.')}`);
|
|
1745
|
+
errorLine(` ${dim('•')} ${dim('The database has no tables yet, run')} ${cyan('turbine push')} ${dim('or a migration first.')}`);
|
|
1746
|
+
errorLine();
|
|
1747
|
+
errorLine(`${dim('If an empty client is genuinely what you want, re-run with')} ${cyan('--allow-empty')}${dim('.')}`);
|
|
1748
|
+
errorLine();
|
|
1368
1749
|
process.exit(1);
|
|
1369
1750
|
}
|
|
1370
1751
|
// Print table summary
|
|
@@ -1402,9 +1783,11 @@ async function cmdGenerate(args, config) {
|
|
|
1402
1783
|
newline();
|
|
1403
1784
|
console.log(` ${bold('Usage:')}`);
|
|
1404
1785
|
newline();
|
|
1405
|
-
console.log(` ${cyan(
|
|
1786
|
+
console.log(` ${cyan(generatedClientImport(config))}`);
|
|
1406
1787
|
console.log(` ${dim('const db = turbine({ connectionString: process.env.DATABASE_URL });')}`);
|
|
1407
|
-
|
|
1788
|
+
// Name a table the client just emitted rather than a hardcoded `users`.
|
|
1789
|
+
const sampleAccessor = snakeToCamel(tableNames[0] ?? 'users');
|
|
1790
|
+
console.log(` ${dim(`const row = await db.${sampleAccessor}.findFirst();`)}`);
|
|
1408
1791
|
newline();
|
|
1409
1792
|
}
|
|
1410
1793
|
/**
|
|
@@ -1464,10 +1847,10 @@ async function cmdMigrateFromPrisma(args, config) {
|
|
|
1464
1847
|
}
|
|
1465
1848
|
if (!existsSync(prismaPath)) {
|
|
1466
1849
|
error(`Prisma schema not found: ${cyan(prismaPath)}`);
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1850
|
+
errorLine();
|
|
1851
|
+
errorLine(`${dim('Point at it with')} ${cyan('--schema <path>')} ${dim('(a .prisma file, or a directory of them;')}`);
|
|
1852
|
+
errorLine(`${dim('default: prisma/schema.prisma, then prisma/schema/).')}`);
|
|
1853
|
+
errorLine();
|
|
1471
1854
|
process.exit(1);
|
|
1472
1855
|
}
|
|
1473
1856
|
// Parse (fatal only on a construct we must understand).
|
|
@@ -1478,8 +1861,8 @@ async function cmdMigrateFromPrisma(args, config) {
|
|
|
1478
1861
|
catch (err) {
|
|
1479
1862
|
newline();
|
|
1480
1863
|
error(`Could not read ${cyan(prismaPath)}`);
|
|
1481
|
-
|
|
1482
|
-
|
|
1864
|
+
errorLine(`${red(err instanceof Error ? err.message : String(err))}`);
|
|
1865
|
+
errorLine();
|
|
1483
1866
|
process.exit(1);
|
|
1484
1867
|
}
|
|
1485
1868
|
let ast;
|
|
@@ -1490,8 +1873,8 @@ async function cmdMigrateFromPrisma(args, config) {
|
|
|
1490
1873
|
if (err instanceof PrismaParseError) {
|
|
1491
1874
|
newline();
|
|
1492
1875
|
error(`Could not parse ${cyan(prismaPath)}`);
|
|
1493
|
-
|
|
1494
|
-
|
|
1876
|
+
errorLine(`${red(err.message)}`);
|
|
1877
|
+
errorLine();
|
|
1495
1878
|
process.exit(1);
|
|
1496
1879
|
}
|
|
1497
1880
|
throw err;
|
|
@@ -1570,7 +1953,7 @@ async function cmdMigrateFromPrisma(args, config) {
|
|
|
1570
1953
|
const rel = relative(process.cwd(), outDir);
|
|
1571
1954
|
if (rel.startsWith('..') || resolve(rel) !== outDir) {
|
|
1572
1955
|
error(`Output directory must be within the project root. Got: ${config.out}`);
|
|
1573
|
-
|
|
1956
|
+
errorLine();
|
|
1574
1957
|
process.exit(1);
|
|
1575
1958
|
}
|
|
1576
1959
|
mkdirSync(outDir, { recursive: true });
|
|
@@ -1634,6 +2017,7 @@ async function cmdMigrateFromPrisma(args, config) {
|
|
|
1634
2017
|
// ---------------------------------------------------------------------------
|
|
1635
2018
|
async function cmdPush(args, config) {
|
|
1636
2019
|
banner();
|
|
2020
|
+
refuseSchemaFilePath(config);
|
|
1637
2021
|
const url = requireUrl(config);
|
|
1638
2022
|
label('Database', redactUrl(url));
|
|
1639
2023
|
label('Schema file', config.schemaFile);
|
|
@@ -1814,34 +2198,37 @@ async function cmdMigrate(args, config) {
|
|
|
1814
2198
|
break;
|
|
1815
2199
|
default:
|
|
1816
2200
|
error(`Unknown migrate subcommand: ${sub}`);
|
|
1817
|
-
|
|
2201
|
+
errorLine(`${dim('Run')} ${cyan('npx turbine migrate help')} ${dim('for usage.')}`);
|
|
1818
2202
|
process.exit(1);
|
|
1819
2203
|
}
|
|
1820
2204
|
}
|
|
1821
2205
|
async function cmdMigrateCreate(args, config) {
|
|
1822
2206
|
banner();
|
|
2207
|
+
// `--auto` / `--from-diff` diff the schema FILE against the namespace, so both
|
|
2208
|
+
// halves of the confusion are live on this one command.
|
|
2209
|
+
refuseSchemaFilePath(config);
|
|
1823
2210
|
const name = args.positional[0];
|
|
1824
2211
|
if (!name) {
|
|
1825
2212
|
error('Migration name is required.');
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
2213
|
+
errorLine();
|
|
2214
|
+
errorLine(`${dim('Usage:')} ${cyan('npx turbine migrate create <name>')}`);
|
|
2215
|
+
errorLine(`${dim('Example:')} ${cyan('npx turbine migrate create add_users_table')}`);
|
|
2216
|
+
errorLine(`${dim('Auto:')} ${cyan('npx turbine migrate create my_change --auto')}`);
|
|
2217
|
+
errorLine();
|
|
1831
2218
|
process.exit(1);
|
|
1832
2219
|
}
|
|
1833
2220
|
// The scaffold strategies each own the file body, so combining them is
|
|
1834
2221
|
// ambiguous. Refuse up front (before any of the strategy blocks run).
|
|
1835
2222
|
if (args.fromDiff && args.recipe) {
|
|
1836
2223
|
error('--from-diff cannot be combined with --recipe.');
|
|
1837
|
-
|
|
1838
|
-
|
|
2224
|
+
errorLine(`${dim('Pick one: --from-diff generates from the schema diff, --recipe scaffolds a pattern.')}`);
|
|
2225
|
+
errorLine();
|
|
1839
2226
|
process.exit(1);
|
|
1840
2227
|
}
|
|
1841
2228
|
if (args.fromDiff && args.auto) {
|
|
1842
2229
|
error('--from-diff cannot be combined with --auto.');
|
|
1843
|
-
|
|
1844
|
-
|
|
2230
|
+
errorLine(`${dim('Both generate from the schema diff; --from-diff also flags destructive statements.')}`);
|
|
2231
|
+
errorLine();
|
|
1845
2232
|
process.exit(1);
|
|
1846
2233
|
}
|
|
1847
2234
|
if (args.auto) {
|
|
@@ -1995,12 +2382,12 @@ async function cmdMigrateCreate(args, config) {
|
|
|
1995
2382
|
if (args.recipe) {
|
|
1996
2383
|
if (!MIGRATION_RECIPES[args.recipe]) {
|
|
1997
2384
|
error(`Unknown migration recipe: ${args.recipe}`);
|
|
1998
|
-
|
|
1999
|
-
|
|
2385
|
+
errorLine();
|
|
2386
|
+
errorLine(`${dim('Available recipes:')}`);
|
|
2000
2387
|
for (const [key, recipe] of Object.entries(MIGRATION_RECIPES)) {
|
|
2001
|
-
|
|
2388
|
+
errorLine(` ${cyan(key)} ${dim(recipe.description)}`);
|
|
2002
2389
|
}
|
|
2003
|
-
|
|
2390
|
+
errorLine();
|
|
2004
2391
|
process.exit(1);
|
|
2005
2392
|
}
|
|
2006
2393
|
const file = createMigration(config.migrationsDir, name, undefined, { recipe: args.recipe });
|
|
@@ -2198,11 +2585,11 @@ async function cmdMigrateDeploy(args, config) {
|
|
|
2198
2585
|
if (plan.mismatches.length > 0) {
|
|
2199
2586
|
if (!args.allowDrift) {
|
|
2200
2587
|
error('Deploy blocked by migration drift');
|
|
2201
|
-
|
|
2588
|
+
errorLine();
|
|
2202
2589
|
for (const line of formatChecksumMismatchError(plan.mismatches).split('\n')) {
|
|
2203
|
-
|
|
2590
|
+
errorLine(`${line.replace('[turbine] ', '')}`);
|
|
2204
2591
|
}
|
|
2205
|
-
|
|
2592
|
+
errorLine();
|
|
2206
2593
|
process.exit(1);
|
|
2207
2594
|
}
|
|
2208
2595
|
warn('--allow-drift is set: checksum validation is DISABLED for this deploy.');
|
|
@@ -2533,6 +2920,7 @@ async function cmdSeed(_args, config) {
|
|
|
2533
2920
|
// ---------------------------------------------------------------------------
|
|
2534
2921
|
async function cmdStatus(_args, config) {
|
|
2535
2922
|
banner();
|
|
2923
|
+
refuseSchemaFilePath(config);
|
|
2536
2924
|
const url = requireUrl(config);
|
|
2537
2925
|
label('Database', redactUrl(url));
|
|
2538
2926
|
label('Schema', config.schema);
|
|
@@ -2672,6 +3060,7 @@ export function refusePoolerConnection(url, args) {
|
|
|
2672
3060
|
}
|
|
2673
3061
|
async function cmdDoctor(args, config) {
|
|
2674
3062
|
const jsonMode = args.json === true;
|
|
3063
|
+
refuseSchemaFilePath(config);
|
|
2675
3064
|
const url = requireUrl(config);
|
|
2676
3065
|
// Before the banner, before introspect, before any pool is constructed.
|
|
2677
3066
|
refusePoolerConnection(url, args);
|
|
@@ -3419,6 +3808,9 @@ export function isLoopbackHost(host) {
|
|
|
3419
3808
|
async function cmdStudio(args, config) {
|
|
3420
3809
|
banner();
|
|
3421
3810
|
const demo = args.demo === true;
|
|
3811
|
+
// Demo mode reads no namespace at all, so there is nothing to mistake there.
|
|
3812
|
+
if (!demo)
|
|
3813
|
+
refuseSchemaFilePath(config);
|
|
3422
3814
|
// Demo mode is self-contained (seeded in-memory database), so it never needs
|
|
3423
3815
|
// a DATABASE_URL. The placeholder is only used for display.
|
|
3424
3816
|
const url = demo ? 'demo://in-memory' : requireUrl(config);
|
|
@@ -3435,10 +3827,10 @@ async function cmdStudio(args, config) {
|
|
|
3435
3827
|
if (!isLoopbackHost(host)) {
|
|
3436
3828
|
if (!args.allowRemote) {
|
|
3437
3829
|
error(`Studio refuses to bind to ${yellow(host)} without ${cyan('--allow-remote')}.`);
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3830
|
+
errorLine();
|
|
3831
|
+
errorLine(`${dim('Loopback only by default')} ${dim('(127.0.0.1, localhost, ::1).')}`);
|
|
3832
|
+
errorLine(`${dim('Pass')} ${cyan('--allow-remote')} ${dim('to opt in to network exposure.')}`);
|
|
3833
|
+
errorLine();
|
|
3442
3834
|
process.exit(1);
|
|
3443
3835
|
}
|
|
3444
3836
|
// warn() prints; it returns void. Wrapping it in console.log() printed a
|
|
@@ -3546,6 +3938,7 @@ async function cmdStudio(args, config) {
|
|
|
3546
3938
|
// Command: mcp, read-only JSON-RPC stdio server
|
|
3547
3939
|
// ---------------------------------------------------------------------------
|
|
3548
3940
|
async function cmdMcp(_args, config) {
|
|
3941
|
+
refuseSchemaFilePath(config);
|
|
3549
3942
|
const url = requireUrl(config);
|
|
3550
3943
|
await runMcpServer({
|
|
3551
3944
|
url,
|
|
@@ -3564,10 +3957,10 @@ async function cmdObserve(args) {
|
|
|
3564
3957
|
const url = process.env.TURBINE_OBSERVE_URL;
|
|
3565
3958
|
if (!url) {
|
|
3566
3959
|
error('TURBINE_OBSERVE_URL environment variable is required for the observe command.');
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3960
|
+
errorLine();
|
|
3961
|
+
errorLine(`${dim('Set it to the Postgres connection string where metrics are stored.')}`);
|
|
3962
|
+
errorLine(`${dim('Example:')} ${cyan('TURBINE_OBSERVE_URL=postgres://... npx turbine observe')}`);
|
|
3963
|
+
errorLine();
|
|
3571
3964
|
process.exit(1);
|
|
3572
3965
|
}
|
|
3573
3966
|
const port = args.port ?? 4984;
|
|
@@ -3582,10 +3975,10 @@ async function cmdObserve(args) {
|
|
|
3582
3975
|
if (!isLoopbackHost(host)) {
|
|
3583
3976
|
if (!args.allowRemote) {
|
|
3584
3977
|
error(`Observe refuses to bind to ${yellow(host)} without ${cyan('--allow-remote')}.`);
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3978
|
+
errorLine();
|
|
3979
|
+
errorLine(`${dim('Loopback only by default')} ${dim('(127.0.0.1, localhost, ::1).')}`);
|
|
3980
|
+
errorLine(`${dim('Pass')} ${cyan('--allow-remote')} ${dim('to opt in to network exposure.')}`);
|
|
3981
|
+
errorLine();
|
|
3589
3982
|
process.exit(1);
|
|
3590
3983
|
}
|
|
3591
3984
|
// warn() prints and returns void; see the same guard in cmdStudio.
|
|
@@ -3650,6 +4043,8 @@ export function showSubcommandHelp(command) {
|
|
|
3650
4043
|
doctor: showDoctorHelp,
|
|
3651
4044
|
studio: showStudioHelp,
|
|
3652
4045
|
mcp: showMcpHelp,
|
|
4046
|
+
observe: showObserveHelp,
|
|
4047
|
+
skill: showSkillHelp,
|
|
3653
4048
|
};
|
|
3654
4049
|
const fn = helpMap[command];
|
|
3655
4050
|
if (fn) {
|
|
@@ -3677,6 +4072,7 @@ function showInitHelp() {
|
|
|
3677
4072
|
console.log(` ${cyan('--url, -u')} ${dim('<url>')} Postgres connection string to embed in config`);
|
|
3678
4073
|
console.log(` ${cyan('--force, -f')} Overwrite existing config file`);
|
|
3679
4074
|
console.log(` ${cyan('--yes, -y')} Accept every step's default (non-interactive)`);
|
|
4075
|
+
console.log(` ${cyan('--schema-file')} ${dim('<path>')} Where to scaffold the schema file ${dim('(default: ./turbine/schema.ts)')}`);
|
|
3680
4076
|
console.log(` ${cyan('--skip-schema')} Don't create the starter schema file`);
|
|
3681
4077
|
console.log(` ${cyan('--with-schema')} Create it even if the database already has tables`);
|
|
3682
4078
|
console.log(` ${cyan('--skip-seed')} Don't create the seed file or run the seed`);
|
|
@@ -3757,8 +4153,11 @@ function showPushHelp() {
|
|
|
3757
4153
|
console.log(` Reads your ${cyan('turbine/schema.ts')} file, diffs against the live database,`);
|
|
3758
4154
|
console.log(` and applies CREATE/ALTER statements.`);
|
|
3759
4155
|
newline();
|
|
4156
|
+
console.log(` ${dim('Note:')} the file is ${cyan('--schema-file')}${dim('.')} ${cyan('--schema')} ${dim('is the Postgres namespace to write into.')}`);
|
|
4157
|
+
newline();
|
|
3760
4158
|
console.log(` ${bold('Options:')}`);
|
|
3761
4159
|
console.log(` ${cyan('--url, -u')} ${dim('<url>')} Postgres connection string`);
|
|
4160
|
+
console.log(` ${cyan('--schema-file')} ${dim('<path>')} Your ${cyan('defineSchema()')} file ${dim('(default: ./turbine/schema.ts)')}`);
|
|
3762
4161
|
console.log(` ${cyan('--dry-run')} Show SQL without executing`);
|
|
3763
4162
|
console.log(` ${cyan('--allow-destructive')} Skip the interactive confirmation for data-destroying statements ${dim('(CI)')}`);
|
|
3764
4163
|
console.log(` ${cyan('--verbose, -v')} Show detailed output`);
|
|
@@ -3780,6 +4179,7 @@ function showMigrateHelp() {
|
|
|
3780
4179
|
newline();
|
|
3781
4180
|
console.log(` ${bold('Options:')}`);
|
|
3782
4181
|
console.log(` ${cyan('--url, -u')} ${dim('<url>')} Postgres connection string`);
|
|
4182
|
+
console.log(` ${cyan('--schema-file')} ${dim('<path>')} Your ${cyan('defineSchema()')} file, for ${cyan('--auto')} / ${cyan('--from-diff')}`);
|
|
3783
4183
|
console.log(` ${cyan('--auto')} Auto-generate UP/DOWN SQL from schema diff ${dim('(create only)')}`);
|
|
3784
4184
|
console.log(` ${cyan('--from-diff')} Generate from schema diff, flagging destructive statements ${dim('(create only)')}`);
|
|
3785
4185
|
console.log(` ${cyan('--recipe')} ${dim('<name>')} Scaffold a sanctioned migration pattern ${dim('(create only, e.g. backfill)')}`);
|
|
@@ -3932,6 +4332,51 @@ function showMcpHelp() {
|
|
|
3932
4332
|
console.log(` ${cyan('--exclude')} ${dim('<tables>')} Comma-separated tables to exclude`);
|
|
3933
4333
|
newline();
|
|
3934
4334
|
}
|
|
4335
|
+
function showObserveHelp() {
|
|
4336
|
+
banner();
|
|
4337
|
+
console.log(` ${bold('turbine observe')}, Query metrics dashboard`);
|
|
4338
|
+
newline();
|
|
4339
|
+
console.log(` ${bold('Usage:')}`);
|
|
4340
|
+
console.log(` TURBINE_OBSERVE_URL=postgres://... npx turbine observe ${dim('[options]')}`);
|
|
4341
|
+
newline();
|
|
4342
|
+
console.log(` Reads the ${cyan('_turbine_metrics')} table written by ${cyan('db.$observe()')} and serves`);
|
|
4343
|
+
console.log(` per-minute count / avg / p50 / p95 / p99 / error aggregates. Read-only,`);
|
|
4344
|
+
console.log(` loopback-bound, behind a random per-session token, same model as Studio.`);
|
|
4345
|
+
newline();
|
|
4346
|
+
console.log(` ${dim('The connection string comes from')} ${cyan('TURBINE_OBSERVE_URL')}${dim(', not')} ${cyan('DATABASE_URL')}${dim(':')}`);
|
|
4347
|
+
console.log(` ${dim('metrics are meant to live in a different database from the one they measure.')}`);
|
|
4348
|
+
newline();
|
|
4349
|
+
console.log(` ${bold('Options:')}`);
|
|
4350
|
+
console.log(` ${cyan('--port')} ${dim('<n>')} HTTP port ${dim('(default: 4984)')}`);
|
|
4351
|
+
console.log(` ${cyan('--host')} ${dim('<addr>')} Bind address ${dim('(default: 127.0.0.1)')}`);
|
|
4352
|
+
console.log(` ${cyan('--no-open')} Don't auto-open the browser`);
|
|
4353
|
+
console.log(` ${cyan('--allow-remote')} Allow a non-loopback ${cyan('--host')} ${dim('(refused without it)')}`);
|
|
4354
|
+
newline();
|
|
4355
|
+
}
|
|
4356
|
+
function showSkillHelp() {
|
|
4357
|
+
banner();
|
|
4358
|
+
console.log(` ${bold('turbine skill')}, Install the agent query skill`);
|
|
4359
|
+
newline();
|
|
4360
|
+
console.log(` ${bold('Usage:')}`);
|
|
4361
|
+
console.log(` npx turbine skill ${dim('[options]')}`);
|
|
4362
|
+
newline();
|
|
4363
|
+
console.log(` Writes ${cyan('.claude/skills/turbine-orm/SKILL.md')}: how to write Turbine queries,`);
|
|
4364
|
+
console.log(` for a coding agent working in this project. The file ships in the package`);
|
|
4365
|
+
console.log(` rather than being generated here, and every factual claim in it is executed`);
|
|
4366
|
+
console.log(` against a live database on each release.`);
|
|
4367
|
+
newline();
|
|
4368
|
+
console.log(` ${bold('Options:')}`);
|
|
4369
|
+
console.log(` ${cyan('--print')} Write the skill to stdout instead of installing it`);
|
|
4370
|
+
console.log(` ${cyan('--agents')} Print the AGENTS.md / CLAUDE.md instructions block`);
|
|
4371
|
+
console.log(` ${cyan('--dir')} ${dim('<path>')} Skills root to install into ${dim('(default: .claude/skills)')}`);
|
|
4372
|
+
newline();
|
|
4373
|
+
console.log(` ${bold('Examples:')}`);
|
|
4374
|
+
console.log(` ${dim('$')} npx turbine skill`);
|
|
4375
|
+
console.log(` ${dim('$')} npx turbine skill --agents >> AGENTS.md`);
|
|
4376
|
+
newline();
|
|
4377
|
+
console.log(` ${dim('Also worth connecting:')} ${cyan('npx turbine mcp')}${dim(', the read-only MCP server.')}`);
|
|
4378
|
+
newline();
|
|
4379
|
+
}
|
|
3935
4380
|
// ---------------------------------------------------------------------------
|
|
3936
4381
|
// Help
|
|
3937
4382
|
// ---------------------------------------------------------------------------
|
|
@@ -3962,7 +4407,8 @@ function showHelp() {
|
|
|
3962
4407
|
console.log(` ${bold('Options:')}`);
|
|
3963
4408
|
console.log(` ${cyan('--url, -u')} ${dim('<url>')} Postgres connection string`);
|
|
3964
4409
|
console.log(` ${cyan('--out, -o')} ${dim('<dir>')} Output directory ${dim('(default: ./generated/turbine)')}`);
|
|
3965
|
-
console.log(` ${cyan('--schema, -s')} ${dim('<name>')} Postgres schema ${dim('(default: public)')}`);
|
|
4410
|
+
console.log(` ${cyan('--schema, -s')} ${dim('<name>')} Postgres schema NAME to read ${dim('(default: public)')}`);
|
|
4411
|
+
console.log(` ${cyan('--schema-file')} ${dim('<path>')} Your ${cyan('defineSchema()')} file ${dim('(push / migrate --auto / init)')}`);
|
|
3966
4412
|
console.log(` ${cyan('--include')} ${dim('<tables>')} Comma-separated tables to include`);
|
|
3967
4413
|
console.log(` ${cyan('--exclude')} ${dim('<tables>')} Comma-separated tables to exclude`);
|
|
3968
4414
|
console.log(` ${cyan('--dry-run')} Show SQL without executing`);
|
|
@@ -4080,9 +4526,9 @@ function cmdSkill(args) {
|
|
|
4080
4526
|
const source = root ? resolve(root, 'skills', 'turbine-orm', 'SKILL.md') : undefined;
|
|
4081
4527
|
if (!source || !existsSync(source)) {
|
|
4082
4528
|
error('Could not find the packaged skill inside turbine-orm.');
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4529
|
+
errorLine();
|
|
4530
|
+
errorLine(`${dim('Read it online instead:')} ${cyan('https://turbineorm.dev/ai-agents')}`);
|
|
4531
|
+
errorLine();
|
|
4086
4532
|
process.exit(1);
|
|
4087
4533
|
}
|
|
4088
4534
|
const body = readFileSync(source, 'utf8');
|
|
@@ -4180,6 +4626,7 @@ async function main() {
|
|
|
4180
4626
|
url: args.url,
|
|
4181
4627
|
out: args.out,
|
|
4182
4628
|
schema: args.schema,
|
|
4629
|
+
schemaFile: args.schemaFile,
|
|
4183
4630
|
include: args.include,
|
|
4184
4631
|
exclude: args.exclude,
|
|
4185
4632
|
importExtension: args.importExtension,
|
|
@@ -4256,50 +4703,60 @@ async function main() {
|
|
|
4256
4703
|
case 'skill':
|
|
4257
4704
|
cmdSkill(args);
|
|
4258
4705
|
break;
|
|
4259
|
-
default:
|
|
4706
|
+
default: {
|
|
4260
4707
|
error(`Unknown command: ${bold(args.command)}`);
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4708
|
+
errorLine();
|
|
4709
|
+
// Only canonical spellings are offered. `nameCloseness` scores substring
|
|
4710
|
+
// containment above edit distance, so including the one-letter aliases
|
|
4711
|
+
// would answer "genrate" with "g" (contained, score 501) instead of
|
|
4712
|
+
// "generate" (one edit away, score 99).
|
|
4713
|
+
const suggestion = closestName(args.command, knownCommands());
|
|
4714
|
+
if (suggestion) {
|
|
4715
|
+
errorLine(`${dim('Did you mean')} ${cyan(`turbine ${suggestion}`)}${dim('?')}`);
|
|
4716
|
+
errorLine();
|
|
4717
|
+
}
|
|
4718
|
+
errorLine(`${dim('Run')} ${cyan('npx turbine help')} ${dim('for available commands.')}`);
|
|
4719
|
+
errorLine();
|
|
4264
4720
|
process.exit(1);
|
|
4721
|
+
}
|
|
4265
4722
|
}
|
|
4266
4723
|
}
|
|
4267
4724
|
catch (err) {
|
|
4268
4725
|
if (err instanceof Error) {
|
|
4269
4726
|
if (err.message.includes('ECONNREFUSED') || err.message.includes('connection')) {
|
|
4270
|
-
|
|
4727
|
+
errorLine();
|
|
4271
4728
|
error(`Could not connect to database`);
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4729
|
+
errorLine(`${dim(redactUrl(err.message))}`);
|
|
4730
|
+
errorLine();
|
|
4731
|
+
errorLine(`${dim('Check that:')}`);
|
|
4732
|
+
errorLine(` ${dim('1.')} Your database is running`);
|
|
4733
|
+
errorLine(` ${dim('2.')} The connection string is correct`);
|
|
4734
|
+
errorLine(` ${dim('3.')} Network/firewall allows the connection`);
|
|
4278
4735
|
}
|
|
4279
4736
|
else if (err.message.includes('authentication')) {
|
|
4280
|
-
|
|
4737
|
+
errorLine();
|
|
4281
4738
|
error(`Authentication failed`);
|
|
4282
|
-
|
|
4739
|
+
errorLine(`${dim(redactUrl(err.message))}`);
|
|
4283
4740
|
}
|
|
4284
4741
|
else if (err.message.includes('does not exist')) {
|
|
4285
|
-
|
|
4742
|
+
errorLine();
|
|
4286
4743
|
error(`Database or schema not found`);
|
|
4287
|
-
|
|
4744
|
+
errorLine(`${dim(redactUrl(err.message))}`);
|
|
4288
4745
|
}
|
|
4289
4746
|
else {
|
|
4290
|
-
|
|
4747
|
+
errorLine();
|
|
4291
4748
|
error(redactUrl(err.message));
|
|
4292
4749
|
if (args.verbose && err.stack) {
|
|
4293
|
-
|
|
4294
|
-
console.
|
|
4750
|
+
errorLine();
|
|
4751
|
+
console.error(dim(redactUrl(err.stack)));
|
|
4295
4752
|
}
|
|
4296
4753
|
}
|
|
4297
4754
|
}
|
|
4298
4755
|
else {
|
|
4299
|
-
|
|
4756
|
+
errorLine();
|
|
4300
4757
|
error(`Unexpected error: ${redactUrl(String(err))}`);
|
|
4301
4758
|
}
|
|
4302
|
-
|
|
4759
|
+
errorLine();
|
|
4303
4760
|
process.exit(1);
|
|
4304
4761
|
}
|
|
4305
4762
|
}
|