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.
Files changed (78) hide show
  1. package/README.md +48 -7
  2. package/dist/cjs/cli/compile-query.d.ts +22 -2
  3. package/dist/cjs/cli/compile-query.js +49 -5
  4. package/dist/cjs/cli/config.d.ts +2 -0
  5. package/dist/cjs/cli/config.js +1 -1
  6. package/dist/cjs/cli/destructive.js +78 -43
  7. package/dist/cjs/cli/index.d.ts +95 -1
  8. package/dist/cjs/cli/index.js +609 -145
  9. package/dist/cjs/cli/mcp.js +30 -1
  10. package/dist/cjs/cli/pii-predicate-guard.d.ts +25 -0
  11. package/dist/cjs/cli/pii-predicate-guard.js +72 -12
  12. package/dist/cjs/cli/rate-limit.js +38 -1
  13. package/dist/cjs/cli/studio.js +26 -5
  14. package/dist/cjs/cli/ui.d.ts +33 -0
  15. package/dist/cjs/cli/ui.js +53 -7
  16. package/dist/cjs/client.d.ts +13 -1
  17. package/dist/cjs/client.js +1 -1
  18. package/dist/cjs/errors.d.ts +12 -1
  19. package/dist/cjs/errors.js +11 -2
  20. package/dist/cjs/generate.d.ts +26 -0
  21. package/dist/cjs/generate.js +174 -27
  22. package/dist/cjs/index.d.ts +1 -1
  23. package/dist/cjs/index.js +1 -1
  24. package/dist/cjs/introspect.d.ts +17 -0
  25. package/dist/cjs/introspect.js +100 -1
  26. package/dist/cjs/mssql.d.ts +18 -0
  27. package/dist/cjs/mssql.js +20 -1
  28. package/dist/cjs/pipeline.js +44 -6
  29. package/dist/cjs/powql.js +51 -17
  30. package/dist/cjs/query/batched-loader.js +3 -3
  31. package/dist/cjs/query/builder.js +1 -1
  32. package/dist/cjs/query/relations.d.ts +5 -0
  33. package/dist/cjs/query/relations.js +141 -69
  34. package/dist/cjs/query/utils.d.ts +13 -0
  35. package/dist/cjs/query/utils.js +16 -0
  36. package/dist/cjs/serverless.d.ts +1 -1
  37. package/dist/cjs/serverless.js +1 -1
  38. package/dist/cjs/sqlite.d.ts +33 -1
  39. package/dist/cjs/sqlite.js +84 -3
  40. package/dist/cli/compile-query.d.ts +22 -2
  41. package/dist/cli/compile-query.js +50 -6
  42. package/dist/cli/config.d.ts +2 -0
  43. package/dist/cli/config.js +1 -1
  44. package/dist/cli/destructive.js +78 -43
  45. package/dist/cli/index.d.ts +95 -1
  46. package/dist/cli/index.js +604 -147
  47. package/dist/cli/mcp.js +30 -1
  48. package/dist/cli/pii-predicate-guard.d.ts +25 -0
  49. package/dist/cli/pii-predicate-guard.js +73 -13
  50. package/dist/cli/rate-limit.js +38 -1
  51. package/dist/cli/studio.js +27 -6
  52. package/dist/cli/ui.d.ts +33 -0
  53. package/dist/cli/ui.js +51 -7
  54. package/dist/client.d.ts +13 -1
  55. package/dist/client.js +1 -1
  56. package/dist/errors.d.ts +12 -1
  57. package/dist/errors.js +11 -2
  58. package/dist/generate.d.ts +26 -0
  59. package/dist/generate.js +172 -27
  60. package/dist/index.d.ts +1 -1
  61. package/dist/index.js +1 -1
  62. package/dist/introspect.d.ts +17 -0
  63. package/dist/introspect.js +98 -1
  64. package/dist/mssql.d.ts +18 -0
  65. package/dist/mssql.js +20 -1
  66. package/dist/pipeline.js +44 -6
  67. package/dist/powql.js +53 -19
  68. package/dist/query/batched-loader.js +4 -4
  69. package/dist/query/builder.js +2 -2
  70. package/dist/query/relations.d.ts +5 -0
  71. package/dist/query/relations.js +141 -70
  72. package/dist/query/utils.d.ts +13 -0
  73. package/dist/query/utils.js +15 -0
  74. package/dist/serverless.d.ts +1 -1
  75. package/dist/serverless.js +1 -1
  76. package/dist/sqlite.d.ts +33 -1
  77. package/dist/sqlite.js +85 -4
  78. package/package.json +2 -2
@@ -7,7 +7,7 @@
7
7
  * turbine init , Initialize a Turbine project
8
8
  * turbine generate | pull , Introspect database and generate TypeScript types
9
9
  * turbine migrate-from-prisma - Parse a schema.prisma and emit a Prisma->Turbine name map + report
10
- * turbine push - Apply schema-builder definitions to database (destructive ops gated)
10
+ * turbine push - Apply schema-builder definitions to database (--schema-file, destructive ops gated)
11
11
  * turbine migrate create <name> - Create a new SQL migration file (--auto | --from-diff | --recipe <name>)
12
12
  * turbine migrate up , Apply pending migrations
13
13
  * turbine migrate deploy , Apply pending migrations without prompts
@@ -60,6 +60,10 @@ var __importStar = (this && this.__importStar) || (function () {
60
60
  };
61
61
  })();
62
62
  Object.defineProperty(exports, "__esModule", { value: true });
63
+ exports.canonicalCommand = canonicalCommand;
64
+ exports.knownCommands = knownCommands;
65
+ exports.flagsForCommand = flagsForCommand;
66
+ exports.allFlagTokens = allFlagTokens;
63
67
  exports.parseArgs = parseArgs;
64
68
  exports.usesProjectConfig = usesProjectConfig;
65
69
  exports.bootstrapCliConfig = bootstrapCliConfig;
@@ -70,10 +74,13 @@ exports.tsxInstallCommand = tsxInstallCommand;
70
74
  exports.tsxRequiredNotice = tsxRequiredNotice;
71
75
  exports.detectConsumerModuleType = detectConsumerModuleType;
72
76
  exports.planInitSteps = planInitSteps;
77
+ exports.initSchemaTemplate = initSchemaTemplate;
73
78
  exports.gitignoreIgnoresEnv = gitignoreIgnoresEnv;
74
79
  exports.planEnvScaffold = planEnvScaffold;
75
80
  exports.scaffoldEnvForUrl = scaffoldEnvForUrl;
81
+ exports.generatedClientImport = generatedClientImport;
76
82
  exports.initEnvNotice = initEnvNotice;
83
+ exports.refuseSchemaFilePath = refuseSchemaFilePath;
77
84
  exports.resolveMigrateFromPrismaUrl = resolveMigrateFromPrismaUrl;
78
85
  exports.readPrismaSchemaSource = readPrismaSchemaSource;
79
86
  exports.buildMigrateDeployOptions = buildMigrateDeployOptions;
@@ -93,6 +100,7 @@ const introspect_js_1 = require("../introspect.js");
93
100
  const plan_divergence_js_1 = require("../plan-divergence.js");
94
101
  const plan_flip_probe_js_1 = require("../plan-flip-probe.js");
95
102
  const prisma_schema_fingerprint_js_1 = require("../prisma-schema-fingerprint.js");
103
+ const utils_js_1 = require("../query/utils.js");
96
104
  const schema_js_1 = require("../schema.js");
97
105
  const schema_sql_js_1 = require("../schema-sql.js");
98
106
  const config_js_1 = require("./config.js");
@@ -114,11 +122,227 @@ const ui_js_1 = require("./ui.js");
114
122
  * rather than "you forgot the recipe name".
115
123
  */
116
124
  function failArg(message, ...hints) {
117
- (0, ui_js_1.banner)();
125
+ (0, ui_js_1.errorBanner)();
118
126
  (0, ui_js_1.error)(message);
119
127
  for (const hint of hints)
120
- console.log(` ${(0, ui_js_1.dim)(hint)}`);
121
- (0, ui_js_1.newline)();
128
+ (0, ui_js_1.errorLine)((0, ui_js_1.dim)(hint));
129
+ (0, ui_js_1.errorLine)();
130
+ process.exit(1);
131
+ }
132
+ // ---------------------------------------------------------------------------
133
+ // The flag surface, as data
134
+ // ---------------------------------------------------------------------------
135
+ /**
136
+ * Long flags accepted by EVERY command.
137
+ *
138
+ * Deliberately generous: these are connection/output overrides and diagnostics,
139
+ * so accepting one where it happens to be inert costs nothing. Nothing that
140
+ * changes what a command DOES to your data belongs here, which is why
141
+ * `--dry-run`, `--allow-destructive`, `--fix` and `--yes` are all per-command
142
+ * below.
143
+ */
144
+ const GLOBAL_FLAGS = [
145
+ '--url',
146
+ '--out',
147
+ '--schema',
148
+ '--include',
149
+ '--exclude',
150
+ '--verbose',
151
+ '--force',
152
+ '--help',
153
+ ];
154
+ /**
155
+ * The flags each command accepts, BEYOND {@link GLOBAL_FLAGS}. Long spellings
156
+ * only; short forms come from {@link FLAG_ALIAS}.
157
+ *
158
+ * This table is the CLI's flag surface as data, for the same reason
159
+ * `query/option-surface.ts` is the query surface as data: an unknown flag used
160
+ * to be DISCARDED by the parser's `default:` branch, so `push --dry-runn`
161
+ * executed for real, `migrate create x --autoo` wrote an empty template and
162
+ * reported success, and `doctor --fixx` wrote nothing and exited 0. A typo in a
163
+ * safety flag silently disarmed the safety. Every entry here is cross-checked
164
+ * against the parser's own `case` labels by
165
+ * `src/test/cli-unknown-flags.test.ts`, so adding a flag to one and not the
166
+ * other fails the build rather than stranding it.
167
+ */
168
+ const COMMAND_FLAGS = {
169
+ init: ['--yes', '--schema-file', '--skip-schema', '--with-schema', '--skip-seed', '--skip-push', '--skip-generate'],
170
+ generate: [
171
+ '--zod',
172
+ '--include-views',
173
+ '--no-timestamp',
174
+ '--import-ext',
175
+ '--keep-column-names',
176
+ '--legacy-to-many-uniques',
177
+ '--allow-empty',
178
+ ],
179
+ 'migrate-from-prisma': ['--no-db', '--if-db', '--allow-partial', '--no-timestamp'],
180
+ push: ['--schema-file', '--dry-run', '--allow-destructive'],
181
+ migrate: [
182
+ '--schema-file',
183
+ '--auto',
184
+ '--from-diff',
185
+ '--recipe',
186
+ '--step',
187
+ '--dry-run',
188
+ '--allow-drift',
189
+ '--allow-destructive',
190
+ ],
191
+ seed: [],
192
+ status: [],
193
+ doctor: [
194
+ '--fix',
195
+ '--json',
196
+ '--no-concurrently',
197
+ '--unused',
198
+ '--audit',
199
+ '--min-scans',
200
+ '--metrics-url',
201
+ '--no-plan-divergence',
202
+ '--allow-pooler',
203
+ ],
204
+ studio: ['--port', '--host', '--no-open', '--allow-remote', '--write', '--show-pii', '--demo'],
205
+ mcp: [],
206
+ observe: ['--port', '--host', '--no-open', '--allow-remote', '--metrics-url'],
207
+ skill: ['--print', '--agents', '--dir'],
208
+ help: [],
209
+ version: [],
210
+ };
211
+ /** Command spellings that dispatch to another command's handler. */
212
+ const COMMAND_ALIASES = {
213
+ gen: 'generate',
214
+ g: 'generate',
215
+ pull: 'generate',
216
+ migration: 'migrate',
217
+ m: 'migrate',
218
+ s: 'seed',
219
+ info: 'status',
220
+ };
221
+ /**
222
+ * Long flag to its alternative spelling, for MATCHING and for display.
223
+ *
224
+ * Kept out of {@link COMMAND_FLAGS} so a command lists each flag once: the
225
+ * "valid flags" block a rejection prints reads as one flag per idea, with its
226
+ * short form attached, rather than as a list twice as long.
227
+ */
228
+ const FLAG_ALIAS = {
229
+ '--url': '-u',
230
+ '--out': '-o',
231
+ '--schema': '-s',
232
+ '--step': '-n',
233
+ '--yes': '-y',
234
+ '--force': '-f',
235
+ '--verbose': '-v',
236
+ '--help': '-h',
237
+ '--import-ext': '--import-extension',
238
+ };
239
+ /**
240
+ * The canonical name of `command`, or undefined when nothing dispatches it.
241
+ *
242
+ * @internal exported for tests.
243
+ */
244
+ function canonicalCommand(command) {
245
+ const resolved = COMMAND_ALIASES[command] ?? command;
246
+ return resolved in COMMAND_FLAGS ? resolved : undefined;
247
+ }
248
+ /** Every command name a user could reasonably have meant, canonical spellings only. */
249
+ function knownCommands() {
250
+ return Object.keys(COMMAND_FLAGS);
251
+ }
252
+ /**
253
+ * The long flags `command` accepts (its own, then the global ones), or
254
+ * undefined when the command itself is unrecognized.
255
+ *
256
+ * An unknown command returns undefined rather than an empty list on purpose:
257
+ * `turbine genrate --url ...` should be told the COMMAND is misspelled, not
258
+ * handed a flag error for a flag that is perfectly valid on the command it
259
+ * meant.
260
+ *
261
+ * @internal exported for tests.
262
+ */
263
+ function flagsForCommand(command) {
264
+ const canonical = canonicalCommand(command);
265
+ if (canonical === undefined)
266
+ return undefined;
267
+ return { own: COMMAND_FLAGS[canonical] ?? [], global: GLOBAL_FLAGS };
268
+ }
269
+ /** Every token (long + alias) `command` accepts, or undefined for an unknown command. */
270
+ function acceptedFlagTokens(command) {
271
+ const flags = flagsForCommand(command);
272
+ if (!flags)
273
+ return undefined;
274
+ const tokens = new Set();
275
+ for (const flag of [...flags.own, ...flags.global]) {
276
+ tokens.add(flag);
277
+ const alias = FLAG_ALIAS[flag];
278
+ if (alias)
279
+ tokens.add(alias);
280
+ }
281
+ return tokens;
282
+ }
283
+ /**
284
+ * Every flag token any command accepts, long spellings and aliases alike.
285
+ *
286
+ * Exists for `src/test/cli-arg-safety.test.ts`, which cross-checks it against
287
+ * the `case` labels in {@link parseArgs} in both directions. A flag in the
288
+ * parser but in no command's list is unreachable (the validator rejects it
289
+ * before the case runs); a flag in a list with no case is accepted and then
290
+ * silently ignored, which is the bug this whole surface exists to end.
291
+ *
292
+ * @internal exported for tests.
293
+ */
294
+ function allFlagTokens() {
295
+ const tokens = new Set();
296
+ for (const command of knownCommands()) {
297
+ for (const token of acceptedFlagTokens(command) ?? [])
298
+ tokens.add(token);
299
+ }
300
+ return tokens;
301
+ }
302
+ /** `--url` rendered for the help block: `--url, -u`. */
303
+ function displayFlag(flag) {
304
+ const alias = FLAG_ALIAS[flag];
305
+ return alias ? `${flag}, ${alias}` : flag;
306
+ }
307
+ /**
308
+ * Reject the unknown flags on a recognized command, naming the closest real one.
309
+ *
310
+ * `closestName` rather than `suggestKey`: the second pass `suggestKey` adds is a
311
+ * camelCase-word-subsequence match, which does nothing for kebab-case flags.
312
+ * Long spellings ONLY are offered as candidates, because `nameCloseness` scores
313
+ * substring containment above edit distance and every short flag is a substring
314
+ * of something: `--no-opn` contains `-o`, which would otherwise outrank the
315
+ * `--no-open` the user obviously meant.
316
+ */
317
+ function failUnknownFlags(command, unknown) {
318
+ const flags = flagsForCommand(command);
319
+ const canonical = canonicalCommand(command) ?? command;
320
+ (0, ui_js_1.errorBanner)();
321
+ const plural = unknown.length > 1 ? 's' : '';
322
+ (0, ui_js_1.error)(`Unknown flag${plural} for ${(0, ui_js_1.cyan)(`turbine ${canonical}`)}: ${unknown.map((f) => (0, ui_js_1.bold)(f)).join(' ')}`);
323
+ (0, ui_js_1.errorLine)();
324
+ if (flags) {
325
+ for (const flag of unknown) {
326
+ const suggestion = (0, utils_js_1.closestName)(flag, [...flags.own, ...flags.global]);
327
+ if (!suggestion)
328
+ continue;
329
+ // With one bad flag the headline already names it; with several, each
330
+ // suggestion has to say which flag it is about.
331
+ const subject = unknown.length > 1 ? `${(0, ui_js_1.bold)(flag)} ${ui_js_1.symbols.arrow} ` : '';
332
+ (0, ui_js_1.errorLine)(`${subject}${(0, ui_js_1.dim)('Did you mean')} ${(0, ui_js_1.cyan)(displayFlag(suggestion))}${(0, ui_js_1.dim)('?')}`);
333
+ }
334
+ if (flags.own.length > 0) {
335
+ (0, ui_js_1.errorLine)();
336
+ (0, ui_js_1.errorLine)((0, ui_js_1.dim)(`Flags for ${canonical}:`));
337
+ (0, ui_js_1.errorLine)(` ${flags.own.map(displayFlag).join(' ')}`);
338
+ }
339
+ (0, ui_js_1.errorLine)();
340
+ (0, ui_js_1.errorLine)((0, ui_js_1.dim)('Flags for every command:'));
341
+ (0, ui_js_1.errorLine)(` ${flags.global.map(displayFlag).join(' ')}`);
342
+ }
343
+ (0, ui_js_1.errorLine)();
344
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('Run')} ${(0, ui_js_1.cyan)(`npx turbine ${canonical} --help`)} ${(0, ui_js_1.dim)('for the full list.')}`);
345
+ (0, ui_js_1.errorLine)();
122
346
  process.exit(1);
123
347
  }
124
348
  function parseArgs(argv = process.argv.slice(2)) {
@@ -133,9 +357,20 @@ function parseArgs(argv = process.argv.slice(2)) {
133
357
  result.subcommand = args[i];
134
358
  i++;
135
359
  }
360
+ // The flags this command accepts. `undefined` means the COMMAND is unknown,
361
+ // in which case nothing here validates flags: main() reports the misspelled
362
+ // command instead, which is the actual problem.
363
+ const accepted = acceptedFlagTokens(result.command);
364
+ const unknownFlags = [];
136
365
  for (; i < args.length; i++) {
137
366
  const arg = args[i];
138
367
  const next = args[i + 1];
368
+ // Checked BEFORE the switch, and on the same cursor, so a flag's VALUE
369
+ // (`--step -1`, `--host -x`) is consumed by its own case and never reaches
370
+ // this test. A bare `-`-prefixed token that is not a flag reaches it and is
371
+ // rejected: no command takes a negative-number positional.
372
+ if (accepted && arg.startsWith('-') && !accepted.has(arg))
373
+ unknownFlags.push(arg);
139
374
  switch (arg) {
140
375
  case '--url':
141
376
  case '-u':
@@ -152,6 +387,10 @@ function parseArgs(argv = process.argv.slice(2)) {
152
387
  result.schema = next;
153
388
  i++;
154
389
  break;
390
+ case '--schema-file':
391
+ result.schemaFile = next;
392
+ i++;
393
+ break;
155
394
  case '--include':
156
395
  result.include = next?.split(',');
157
396
  i++;
@@ -323,6 +562,10 @@ function parseArgs(argv = process.argv.slice(2)) {
323
562
  break;
324
563
  }
325
564
  }
565
+ // Reported once, after the whole line is parsed, so two typos on one command
566
+ // are two lines of one error rather than two runs of the CLI.
567
+ if (unknownFlags.length > 0)
568
+ failUnknownFlags(result.command, unknownFlags);
326
569
  return result;
327
570
  }
328
571
  // ---------------------------------------------------------------------------
@@ -333,35 +576,35 @@ function parseArgs(argv = process.argv.slice(2)) {
333
576
  * Called when we know we need to load a `.ts` file but the loader isn't available.
334
577
  */
335
578
  function failMissingTsLoader(filePath, reason) {
336
- (0, ui_js_1.newline)();
579
+ (0, ui_js_1.errorLine)();
337
580
  (0, ui_js_1.error)(`Cannot load TypeScript file: ${filePath}`);
338
- (0, ui_js_1.newline)();
581
+ (0, ui_js_1.errorLine)();
339
582
  if (reason === 'unsupported') {
340
- console.log(` ${(0, ui_js_1.dim)('Your Node.js version does not support')} ${(0, ui_js_1.cyan)('module.register()')}.`);
341
- console.log(` ${(0, ui_js_1.dim)('Upgrade to Node.js')} ${(0, ui_js_1.cyan)('20.6+')} ${(0, ui_js_1.dim)('or use a')} ${(0, ui_js_1.cyan)('.js')} ${(0, ui_js_1.dim)('/')} ${(0, ui_js_1.cyan)('.mjs')} ${(0, ui_js_1.dim)('config file.')}`);
583
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('Your Node.js version does not support')} ${(0, ui_js_1.cyan)('module.register()')}.`);
584
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('Upgrade to Node.js')} ${(0, ui_js_1.cyan)('20.6+')} ${(0, ui_js_1.dim)('or use a')} ${(0, ui_js_1.cyan)('.js')} ${(0, ui_js_1.dim)('/')} ${(0, ui_js_1.cyan)('.mjs')} ${(0, ui_js_1.dim)('config file.')}`);
342
585
  }
343
586
  else if (reason === 'failed') {
344
587
  // tsx IS installed but registering its loader threw. Report the real
345
588
  // cause, telling the user to install tsx here would be a misdiagnosis.
346
- console.log(` ${(0, ui_js_1.dim)('tsx is installed, but registering its TypeScript loader failed:')}`);
347
- (0, ui_js_1.newline)();
348
- console.log(` ${(0, loader_js_1.getTsLoaderError)() ?? '(unknown error)'}`);
349
- (0, ui_js_1.newline)();
350
- console.log(` ${(0, ui_js_1.dim)('Try upgrading tsx:')} ${(0, ui_js_1.cyan)('npm install --save-dev tsx@latest')}${(0, ui_js_1.dim)(', or rename your file to')} ${(0, ui_js_1.cyan)('.mjs')}.`);
589
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('tsx is installed, but registering its TypeScript loader failed:')}`);
590
+ (0, ui_js_1.errorLine)();
591
+ (0, ui_js_1.errorLine)(` ${(0, loader_js_1.getTsLoaderError)() ?? '(unknown error)'}`);
592
+ (0, ui_js_1.errorLine)();
593
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('Try upgrading tsx:')} ${(0, ui_js_1.cyan)('npm install --save-dev tsx@latest')}${(0, ui_js_1.dim)(', or rename your file to')} ${(0, ui_js_1.cyan)('.mjs')}.`);
351
594
  }
352
595
  else {
353
- console.log(` ${(0, ui_js_1.dim)('Loading .ts config / schema files requires')} ${(0, ui_js_1.cyan)('tsx')} ${(0, ui_js_1.dim)('to be installed.')}`);
354
- (0, ui_js_1.newline)();
355
- console.log(` ${(0, ui_js_1.dim)('Install it as a dev dependency:')}`);
356
- console.log(` ${(0, ui_js_1.cyan)('npm install --save-dev tsx')}`);
357
- console.log(` ${(0, ui_js_1.dim)('or')}`);
358
- console.log(` ${(0, ui_js_1.cyan)('pnpm add -D tsx')}`);
359
- console.log(` ${(0, ui_js_1.dim)('or')}`);
360
- console.log(` ${(0, ui_js_1.cyan)('yarn add -D tsx')}`);
361
- (0, ui_js_1.newline)();
362
- console.log(` ${(0, ui_js_1.dim)('Alternatively, rename your file to')} ${(0, ui_js_1.cyan)('.js')} ${(0, ui_js_1.dim)('or')} ${(0, ui_js_1.cyan)('.mjs')}.`);
363
- }
364
- (0, ui_js_1.newline)();
596
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('Loading .ts config / schema files requires')} ${(0, ui_js_1.cyan)('tsx')} ${(0, ui_js_1.dim)('to be installed.')}`);
597
+ (0, ui_js_1.errorLine)();
598
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('Install it as a dev dependency:')}`);
599
+ (0, ui_js_1.errorLine)(` ${(0, ui_js_1.cyan)('npm install --save-dev tsx')}`);
600
+ (0, ui_js_1.errorLine)(` ${(0, ui_js_1.dim)('or')}`);
601
+ (0, ui_js_1.errorLine)(` ${(0, ui_js_1.cyan)('pnpm add -D tsx')}`);
602
+ (0, ui_js_1.errorLine)(` ${(0, ui_js_1.dim)('or')}`);
603
+ (0, ui_js_1.errorLine)(` ${(0, ui_js_1.cyan)('yarn add -D tsx')}`);
604
+ (0, ui_js_1.errorLine)();
605
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('Alternatively, rename your file to')} ${(0, ui_js_1.cyan)('.js')} ${(0, ui_js_1.dim)('or')} ${(0, ui_js_1.cyan)('.mjs')}.`);
606
+ }
607
+ (0, ui_js_1.errorLine)();
365
608
  process.exit(1);
366
609
  }
367
610
  // ---------------------------------------------------------------------------
@@ -410,20 +653,20 @@ async function bootstrapCliConfig(args, overrides) {
410
653
  function requireUrl(config, options = {}) {
411
654
  if (!config.url) {
412
655
  (0, ui_js_1.error)('No database URL provided.');
413
- (0, ui_js_1.newline)();
414
- console.log(` ${(0, ui_js_1.dim)('Set it in one of these ways:')}`);
415
- console.log(` ${(0, ui_js_1.dim)('1.')} Add ${(0, ui_js_1.cyan)('url')} to ${(0, ui_js_1.cyan)('turbine.config.ts')}`);
656
+ (0, ui_js_1.errorLine)();
657
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('Set it in one of these ways:')}`);
658
+ (0, ui_js_1.errorLine)(` ${(0, ui_js_1.dim)('1.')} Add ${(0, ui_js_1.cyan)('url')} to ${(0, ui_js_1.cyan)('turbine.config.ts')}`);
416
659
  // .env auto-load needs Node 20.12+ (process.loadEnvFile); be honest below it.
417
660
  const envFileNote = typeof process.loadEnvFile === 'function' ? '(auto-loaded)' : '(needs Node 20.12+ to auto-load)';
418
- console.log(` ${(0, ui_js_1.dim)('2.')} Set ${(0, ui_js_1.cyan)('DATABASE_URL')} in your environment or a ${(0, ui_js_1.cyan)('.env')} file ${(0, ui_js_1.dim)(envFileNote)}`);
419
- console.log(` ${(0, ui_js_1.dim)('3.')} Pass ${(0, ui_js_1.cyan)('--url')} flag`);
661
+ (0, ui_js_1.errorLine)(` ${(0, ui_js_1.dim)('2.')} Set ${(0, ui_js_1.cyan)('DATABASE_URL')} in your environment or a ${(0, ui_js_1.cyan)('.env')} file ${(0, ui_js_1.dim)(envFileNote)}`);
662
+ (0, ui_js_1.errorLine)(` ${(0, ui_js_1.dim)('3.')} Pass ${(0, ui_js_1.cyan)('--url')} flag`);
420
663
  const vars = options.datasourceVars ?? [];
421
664
  if (vars.length > 0) {
422
665
  const list = vars.map((v) => (0, ui_js_1.cyan)(v)).join(', ');
423
666
  const plural = vars.length > 1 ? 'these variables' : 'this variable';
424
- console.log(` ${(0, ui_js_1.dim)('4.')} Set ${list} ${(0, ui_js_1.dim)(`(${plural}, declared by your schema.prisma datasource, ${vars.length > 1 ? 'are' : 'is'} unset)`)}`);
667
+ (0, ui_js_1.errorLine)(` ${(0, ui_js_1.dim)('4.')} Set ${list} ${(0, ui_js_1.dim)(`(${plural}, declared by your schema.prisma datasource, ${vars.length > 1 ? 'are' : 'is'} unset)`)}`);
425
668
  }
426
- (0, ui_js_1.newline)();
669
+ (0, ui_js_1.errorLine)();
427
670
  process.exit(1);
428
671
  }
429
672
  return config.url;
@@ -432,7 +675,7 @@ async function loadSchemaFile(schemaFile) {
432
675
  const absPath = (0, node_path_1.resolve)(schemaFile);
433
676
  if (!(0, node_fs_1.existsSync)(absPath)) {
434
677
  (0, ui_js_1.error)(`Schema file not found: ${schemaFile}`);
435
- console.log(` ${(0, ui_js_1.dim)('Create one with:')} ${(0, ui_js_1.cyan)('turbine init')}`);
678
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('Create one with:')} ${(0, ui_js_1.cyan)('turbine init')}`);
436
679
  process.exit(1);
437
680
  }
438
681
  // If this is a TypeScript file, ensure the tsx ESM loader is registered
@@ -461,11 +704,11 @@ async function loadSchemaFile(schemaFile) {
461
704
  catch (err) {
462
705
  (0, ui_js_1.error)(`Failed to load schema file: ${schemaFile}`);
463
706
  if (err instanceof Error) {
464
- console.log(` ${(0, ui_js_1.dim)(err.message)}`);
707
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)(err.message)}`);
465
708
  // If the error is the classic ERR_UNKNOWN_FILE_EXTENSION, give a hint.
466
709
  if (err.message.includes('ERR_UNKNOWN_FILE_EXTENSION') || err.message.includes('Unknown file extension')) {
467
- (0, ui_js_1.newline)();
468
- console.log(` ${(0, ui_js_1.dim)('Hint: install')} ${(0, ui_js_1.cyan)('tsx')} ${(0, ui_js_1.dim)('to load .ts files:')} ${(0, ui_js_1.cyan)('npm install --save-dev tsx')}`);
710
+ (0, ui_js_1.errorLine)();
711
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('Hint: install')} ${(0, ui_js_1.cyan)('tsx')} ${(0, ui_js_1.dim)('to load .ts files:')} ${(0, ui_js_1.cyan)('npm install --save-dev tsx')}`);
469
712
  }
470
713
  printCjsHintIfApplicable(err);
471
714
  }
@@ -484,9 +727,9 @@ function printCjsHintIfApplicable(err) {
484
727
  if (msg.includes('ERR_REQUIRE_ESM') ||
485
728
  msg.includes('require() of ES Module') ||
486
729
  msg.includes('Cannot require() ES Module')) {
487
- (0, ui_js_1.newline)();
488
- console.log(` ${(0, ui_js_1.dim)('Hint: add')} ${(0, ui_js_1.cyan)('"type": "module"')} ${(0, ui_js_1.dim)('to your')} ${(0, ui_js_1.cyan)('package.json')}${(0, ui_js_1.dim)('.')}`);
489
- console.log(` ${(0, ui_js_1.dim)('Turbine is an ESM package; without it, Node/tsx tries to')} ${(0, ui_js_1.cyan)('require()')} ${(0, ui_js_1.dim)('it and fails.')}`);
730
+ (0, ui_js_1.errorLine)();
731
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('Hint: add')} ${(0, ui_js_1.cyan)('"type": "module"')} ${(0, ui_js_1.dim)('to your')} ${(0, ui_js_1.cyan)('package.json')}${(0, ui_js_1.dim)('.')}`);
732
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('Turbine is an ESM package; without it, Node/tsx tries to')} ${(0, ui_js_1.cyan)('require()')} ${(0, ui_js_1.dim)('it and fails.')}`);
490
733
  }
491
734
  }
492
735
  /**
@@ -788,7 +1031,54 @@ export default defineSeed(async (db) => {
788
1031
  console.log('Done!');
789
1032
  });
790
1033
  `;
791
- const INIT_SCHEMA_TEMPLATE = `/**
1034
+ /**
1035
+ * The starter schema, with two real tables and a foreign key between them.
1036
+ *
1037
+ * Scaffolded ONLY when the target database has no tables of its own (see
1038
+ * {@link initSchemaTemplate}). An all-commented-out schema next to an empty
1039
+ * database is a dead end: `push` reports "Database already in sync",
1040
+ * `generate` refuses to emit an empty client, and the next-steps text ends up
1041
+ * describing a `db.users` that does not exist. Two tables rather than one
1042
+ * because the relation is what the very next query in the quickstart reads.
1043
+ */
1044
+ const INIT_SCHEMA_EXAMPLE = `/**
1045
+ * Turbine schema definition
1046
+ *
1047
+ * Define your database schema in TypeScript.
1048
+ * Use \`npx turbine push\` to sync it to your database, then
1049
+ * \`npx turbine generate\` to emit the typed client.
1050
+ *
1051
+ * This starter is a working example. Rename it, edit it, or replace it
1052
+ * entirely: nothing here is special to Turbine.
1053
+ *
1054
+ * @see https://turbineorm.dev
1055
+ */
1056
+
1057
+ import { defineSchema } from 'turbine-orm';
1058
+
1059
+ export default defineSchema({
1060
+ users: {
1061
+ id: { type: 'serial', primaryKey: true },
1062
+ email: { type: 'text', notNull: true, unique: true },
1063
+ name: { type: 'text', notNull: true },
1064
+ created_at: { type: 'timestamptz', notNull: true, default: 'NOW()' },
1065
+ },
1066
+ posts: {
1067
+ id: { type: 'serial', primaryKey: true },
1068
+ user_id: { type: 'integer', notNull: true, references: 'users.id' },
1069
+ title: { type: 'text', notNull: true },
1070
+ published: { type: 'boolean', notNull: true, default: 'false' },
1071
+ created_at: { type: 'timestamptz', notNull: true, default: 'NOW()' },
1072
+ },
1073
+ });
1074
+ `;
1075
+ /**
1076
+ * The starter schema with nothing defined, for the one case where a real
1077
+ * example could collide: `init --with-schema` beside a database that already
1078
+ * has tables. There, an example `users` table is a schema diff against
1079
+ * somebody's real data, so the file stays a comment.
1080
+ */
1081
+ const INIT_SCHEMA_PLACEHOLDER = `/**
792
1082
  * Turbine schema definition
793
1083
  *
794
1084
  * Define your database schema in TypeScript.
@@ -800,15 +1090,27 @@ const INIT_SCHEMA_TEMPLATE = `/**
800
1090
  import { defineSchema } from 'turbine-orm';
801
1091
 
802
1092
  export default defineSchema({
803
- // Example:
1093
+ // Your database already has tables, so this file starts empty on purpose:
1094
+ // \`turbine push\` applies exactly what is declared here. Describe the tables
1095
+ // you want Turbine to own, or run \`npx turbine generate\` to work from the
1096
+ // database as it already is.
1097
+ //
804
1098
  // users: {
805
1099
  // id: { type: 'serial', primaryKey: true },
806
1100
  // email: { type: 'text', notNull: true, unique: true },
807
1101
  // name: { type: 'text', notNull: true },
808
- // created_at: { type: 'timestamp', default: 'NOW()' },
1102
+ // created_at: { type: 'timestamptz', notNull: true, default: 'NOW()' },
809
1103
  // },
810
1104
  });
811
1105
  `;
1106
+ /**
1107
+ * Which starter schema to scaffold.
1108
+ *
1109
+ * @internal exported for tests.
1110
+ */
1111
+ function initSchemaTemplate(dbHasTables) {
1112
+ return dbHasTables ? INIT_SCHEMA_PLACEHOLDER : INIT_SCHEMA_EXAMPLE;
1113
+ }
812
1114
  /**
813
1115
  * Does this `.gitignore` text already ignore `.env`?
814
1116
  *
@@ -961,12 +1263,15 @@ function ensureInitScaffoldDirs(config) {
961
1263
  }
962
1264
  }
963
1265
  }
964
- function writeInitSchemaTemplate(config) {
1266
+ function writeInitSchemaTemplate(config, dbHasTables) {
965
1267
  const schemaDir = (0, node_path_1.dirname)(config.schemaFile);
966
1268
  if (schemaDir && !(0, node_fs_1.existsSync)(schemaDir))
967
1269
  (0, node_fs_1.mkdirSync)(schemaDir, { recursive: true });
968
- (0, node_fs_1.writeFileSync)(config.schemaFile, INIT_SCHEMA_TEMPLATE, 'utf-8');
1270
+ (0, node_fs_1.writeFileSync)(config.schemaFile, initSchemaTemplate(dbHasTables), 'utf-8');
969
1271
  (0, ui_js_1.success)(`Created ${(0, ui_js_1.cyan)(config.schemaFile)}`);
1272
+ if (!dbHasTables) {
1273
+ console.log(` ${(0, ui_js_1.dim)('It defines a')} ${(0, ui_js_1.cyan)('users')} ${(0, ui_js_1.dim)('and a')} ${(0, ui_js_1.cyan)('posts')} ${(0, ui_js_1.dim)('table to start from.')}`);
1274
+ }
970
1275
  }
971
1276
  function writeInitSeedTemplate(seedFilePath) {
972
1277
  const seedDir = (0, node_path_1.dirname)(seedFilePath);
@@ -1016,6 +1321,28 @@ async function runInitPush(config, url) {
1016
1321
  (0, ui_js_1.success)(`Applied ${(0, ui_js_1.bold)(String(result.statementsExecuted))} statement(s)`);
1017
1322
  }
1018
1323
  }
1324
+ /**
1325
+ * The exact import line for the generated client, extension included.
1326
+ *
1327
+ * `import { turbine } from './generated/turbine'` is a hard TypeScript error
1328
+ * (TS2834) under `moduleResolution: NodeNext`, which is what this package ships
1329
+ * and what its own tsconfig uses: a relative import needs an explicit file
1330
+ * extension, and NodeNext does no directory-index resolution either. The
1331
+ * generator has always appended the extension to its OWN sibling imports; the
1332
+ * line printed at the reader was the one place it never reached.
1333
+ *
1334
+ * The extension comes from {@link resolveImportExtension}, the same resolver the
1335
+ * generator runs, so the printed line matches the files just written rather
1336
+ * than a second guess about the consumer's tsconfig. Under bundler resolution
1337
+ * it resolves to `''` and the directory form is correct as-is.
1338
+ *
1339
+ * @internal exported for tests.
1340
+ */
1341
+ function generatedClientImport(config) {
1342
+ const dir = `./${config.out.replace(/^\.\//, '').replace(/\/+$/, '')}`;
1343
+ const { ext } = (0, generate_js_1.resolveImportExtension)(config.out, config.importExtension);
1344
+ return `import { turbine } from '${ext === '' ? dir : `${dir}/index${ext}`}';`;
1345
+ }
1019
1346
  /** Introspect the database and generate the typed client. */
1020
1347
  async function runInitGenerate(config, url) {
1021
1348
  const spinner = new ui_js_1.Spinner('Introspecting database').start();
@@ -1027,16 +1354,22 @@ async function runInitGenerate(config, url) {
1027
1354
  exclude: config.exclude.length ? config.exclude : undefined,
1028
1355
  relationNames: config.relationNames,
1029
1356
  });
1030
- spinner.succeed(`Found ${(0, ui_js_1.bold)(String(Object.keys(schema.tables).length))} tables`);
1357
+ const tableNames = Object.keys(schema.tables);
1358
+ spinner.succeed(`Found ${(0, ui_js_1.bold)(String(tableNames.length))} tables`);
1031
1359
  const genSpinner = new ui_js_1.Spinner('Generating TypeScript client').start();
1032
1360
  const result = (0, generate_js_1.generate)({ schema, outDir: config.out, connectionString: url });
1033
1361
  genSpinner.succeed(`Generated ${(0, ui_js_1.bold)(String(result.files.length))} files to ${(0, ui_js_1.cyan)(`${config.out}/`)}`);
1362
+ // The accessor name the generator emits is snakeToCamel of the table name
1363
+ // (generate.ts, generateIndex), so this is the spelling that autocompletes.
1364
+ const first = tableNames[0];
1365
+ return { tableCount: tableNames.length, sampleAccessor: first ? (0, schema_js_1.snakeToCamel)(first) : undefined };
1034
1366
  }
1035
1367
  catch (err) {
1036
1368
  spinner.fail('Could not generate client');
1037
1369
  if (err instanceof Error)
1038
- console.log(` ${(0, ui_js_1.dim)((0, ui_js_1.redactUrl)(err.message))}`);
1370
+ (0, ui_js_1.errorLine)((0, ui_js_1.dim)((0, ui_js_1.redactUrl)(err.message)));
1039
1371
  (0, ui_js_1.info)(`Run generation later with: ${(0, ui_js_1.cyan)('npx turbine generate')}`);
1372
+ return { tableCount: null };
1040
1373
  }
1041
1374
  }
1042
1375
  /** Run the seed file. */
@@ -1260,6 +1593,10 @@ async function cmdInit(args, config) {
1260
1593
  (0, ui_js_1.newline)();
1261
1594
  // TypeScript files this run actually created: they drive the tsx heads-up below.
1262
1595
  const tsFilesWritten = [];
1596
+ // What the generate step produced, if it ran. The next-steps text is derived
1597
+ // from this rather than assumed, so it cannot promise a `db.users` the
1598
+ // generated client does not have.
1599
+ let generated = { tableCount: null };
1263
1600
  // Execute the plan in order. `run` proceeds; `prompt` asks; `skip` reports.
1264
1601
  for (const step of plan) {
1265
1602
  if (step.action === 'skip') {
@@ -1287,7 +1624,7 @@ async function cmdInit(args, config) {
1287
1624
  break;
1288
1625
  }
1289
1626
  case 'schema':
1290
- writeInitSchemaTemplate(config);
1627
+ writeInitSchemaTemplate(config, state.dbHasTables === true);
1291
1628
  if ((0, loader_js_1.needsTsLoader)(config.schemaFile))
1292
1629
  tsFilesWritten.push(config.schemaFile);
1293
1630
  break;
@@ -1300,7 +1637,7 @@ async function cmdInit(args, config) {
1300
1637
  await runInitPush(config, url);
1301
1638
  break;
1302
1639
  case 'generate':
1303
- await runInitGenerate(config, url);
1640
+ generated = await runInitGenerate(config, url);
1304
1641
  break;
1305
1642
  case 'seed-run':
1306
1643
  await runInitSeed(config);
@@ -1339,20 +1676,84 @@ async function cmdInit(args, config) {
1339
1676
  if (!tsxMissing && !(0, loader_js_1.canResolveTsx)()) {
1340
1677
  console.log(` ${(0, ui_js_1.dim)('Note: the TypeScript config requires')} ${(0, ui_js_1.cyan)('tsx')}: ${(0, ui_js_1.cyan)(tsxInstallCommand(detectPackageManager()))}`);
1341
1678
  }
1679
+ (0, ui_js_1.newline)();
1680
+ console.log(` ${(0, ui_js_1.dim)('3.')} Create migrations: ${(0, ui_js_1.cyan)('npx turbine migrate create <name>')}`);
1681
+ console.log(` ${(0, ui_js_1.dim)('4.')} Run migrations: ${(0, ui_js_1.cyan)('npx turbine migrate up')}`);
1682
+ console.log(` ${(0, ui_js_1.dim)('5.')} Seed your database: ${(0, ui_js_1.cyan)('npx turbine seed')}`);
1683
+ }
1684
+ else if (generated.tableCount === null || generated.tableCount === 0) {
1685
+ // There is no typed client to import: either generate did not run (push /
1686
+ // generate declined or skipped), or it ran against a database with no
1687
+ // tables and emitted a client with no accessors. Printing
1688
+ // `await db.users.findMany()` here is the single most misleading line the
1689
+ // CLI produces, because the reader has no way to know it cannot work.
1690
+ console.log(` ${(0, ui_js_1.dim)('1.')} Describe your tables in ${(0, ui_js_1.cyan)(config.schemaFile)}`);
1691
+ console.log(` ${(0, ui_js_1.dim)('2.')} Create them: ${(0, ui_js_1.cyan)('npx turbine push')}`);
1692
+ console.log(` ${(0, ui_js_1.dim)('3.')} Emit the client: ${(0, ui_js_1.cyan)('npx turbine generate')}`);
1693
+ (0, ui_js_1.newline)();
1694
+ console.log(` ${(0, ui_js_1.dim)('Until a table exists, the generated client has no table accessors, so')}`);
1695
+ console.log(` ${(0, ui_js_1.dim)('there is nothing to import yet. Working from a database you already')}`);
1696
+ console.log(` ${(0, ui_js_1.dim)('have? Skip step 1 and run')} ${(0, ui_js_1.cyan)('npx turbine generate')} ${(0, ui_js_1.dim)('on its own.')}`);
1697
+ (0, ui_js_1.newline)();
1698
+ console.log(` ${(0, ui_js_1.dim)('Nothing to connect to yet?')} ${(0, ui_js_1.cyan)('npx turbine studio --demo')} ${(0, ui_js_1.dim)('needs no database.')}`);
1342
1699
  }
1343
1700
  else {
1701
+ const accessor = generated.sampleAccessor ?? 'users';
1344
1702
  console.log(` ${(0, ui_js_1.dim)('1.')} Import the generated client:`);
1345
- console.log(` ${(0, ui_js_1.cyan)(`import { turbine } from './${config.out.replace('./', '')}';`)}`);
1703
+ console.log(` ${(0, ui_js_1.cyan)(generatedClientImport(config))}`);
1346
1704
  (0, ui_js_1.newline)();
1347
1705
  console.log(` ${(0, ui_js_1.dim)('2.')} Create a connection and query:`);
1348
1706
  console.log(` ${(0, ui_js_1.dim)('const db = turbine();')}`);
1349
- console.log(` ${(0, ui_js_1.dim)('const users = await db.users.findMany();')}`);
1707
+ console.log(` ${(0, ui_js_1.dim)(`const rows = await db.${accessor}.findMany();`)}`);
1708
+ (0, ui_js_1.newline)();
1709
+ console.log(` ${(0, ui_js_1.dim)('3.')} Create migrations: ${(0, ui_js_1.cyan)('npx turbine migrate create <name>')}`);
1710
+ console.log(` ${(0, ui_js_1.dim)('4.')} Run migrations: ${(0, ui_js_1.cyan)('npx turbine migrate up')}`);
1711
+ console.log(` ${(0, ui_js_1.dim)('5.')} Seed your database: ${(0, ui_js_1.cyan)('npx turbine seed')}`);
1350
1712
  }
1351
1713
  (0, ui_js_1.newline)();
1352
- console.log(` ${(0, ui_js_1.dim)('3.')} Create migrations: ${(0, ui_js_1.cyan)('npx turbine migrate create <name>')}`);
1353
- console.log(` ${(0, ui_js_1.dim)('4.')} Run migrations: ${(0, ui_js_1.cyan)('npx turbine migrate up')}`);
1354
- console.log(` ${(0, ui_js_1.dim)('5.')} Seed your database: ${(0, ui_js_1.cyan)('npx turbine seed')}`);
1355
- (0, ui_js_1.newline)();
1714
+ }
1715
+ // ---------------------------------------------------------------------------
1716
+ // `--schema` is the namespace, `--schema-file` is the file
1717
+ // ---------------------------------------------------------------------------
1718
+ /**
1719
+ * Refuse a `schema` that is plainly a FILE PATH, on every command that reads it
1720
+ * as a Postgres namespace.
1721
+ *
1722
+ * `--schema` / `-s` sets the namespace to introspect (default `public`). The
1723
+ * defineSchema() file is a different idea entirely, and mistaking the two is
1724
+ * silent on every command that used to accept it: `generate` introspects
1725
+ * `WHERE table_schema = './turbine/schema.ts'` and matches nothing, and `push`
1726
+ * reads `config.schemaFile` instead and reports
1727
+ * "Schema file not found: ./turbine/schema.ts", naming a path the reader never
1728
+ * typed while their schema file sits in the directory they ran it from.
1729
+ *
1730
+ * The check was written for `generate` and wired into `generate` alone, which is
1731
+ * how `push`, the command whose flag name the mistake is actually about, kept
1732
+ * the bad error. It is one function called from every `--schema` command now,
1733
+ * for the same reason `resolveColumnName` is one function: two copies of a rule
1734
+ * is how two commands come to disagree about whether an argument is valid.
1735
+ *
1736
+ * @internal exported for tests.
1737
+ */
1738
+ function refuseSchemaFilePath(config, options = {}) {
1739
+ if (!(0, config_js_1.looksLikeSchemaFilePath)(config.schema))
1740
+ return;
1741
+ (0, ui_js_1.error)(`The ${(0, ui_js_1.cyan)('--schema')} value looks like a file path: ${(0, ui_js_1.cyan)(config.schema)}`);
1742
+ (0, ui_js_1.errorLine)();
1743
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('Did you mean')} ${(0, ui_js_1.cyan)(`--schema-file ${config.schema}`)}${(0, ui_js_1.dim)('?')}`);
1744
+ (0, ui_js_1.errorLine)();
1745
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('--schema is the Postgres schema NAME to read')} ${(0, ui_js_1.dim)('(default:')} ${(0, ui_js_1.cyan)('public')}${(0, ui_js_1.dim)(').')}`);
1746
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('--schema-file is the path to your')} ${(0, ui_js_1.cyan)('defineSchema()')} ${(0, ui_js_1.dim)('file')} ${(0, ui_js_1.dim)(`(default: ${config.schemaFile}).`)}`);
1747
+ (0, ui_js_1.errorLine)();
1748
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('Or set it once in')} ${(0, ui_js_1.cyan)('turbine.config.ts')}${(0, ui_js_1.dim)(':')}`);
1749
+ (0, ui_js_1.errorLine)(` ${(0, ui_js_1.green)('schema:')} ${(0, ui_js_1.cyan)("'public'")}${(0, ui_js_1.dim)(", // or omit, reads the 'public' schema")}`);
1750
+ (0, ui_js_1.errorLine)(` ${(0, ui_js_1.green)('schemaFile:')} ${(0, ui_js_1.cyan)(`'${config.schema}'`)}${(0, ui_js_1.dim)(', // your defineSchema() file (used by `turbine push`)')}`);
1751
+ if (options.escapeHatch) {
1752
+ (0, ui_js_1.errorLine)();
1753
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('Re-run with')} ${(0, ui_js_1.cyan)(options.escapeHatch)} ${(0, ui_js_1.dim)('to use this literal schema name anyway.')}`);
1754
+ }
1755
+ (0, ui_js_1.errorLine)();
1756
+ process.exit(1);
1356
1757
  }
1357
1758
  // ---------------------------------------------------------------------------
1358
1759
  // Command: generate (pull)
@@ -1361,24 +1762,11 @@ async function cmdGenerate(args, config) {
1361
1762
  (0, ui_js_1.banner)();
1362
1763
  const url = requireUrl(config);
1363
1764
  const startTime = performance.now();
1364
- // Guard: `schema` is the Postgres NAMESPACE to introspect (default `public`),
1365
- // NOT the path to your schema-builder file, that goes in `schemaFile`. If the
1366
- // configured `schema` looks like a file path, introspection would silently
1367
- // match zero tables and emit an empty client. Fail loudly instead.
1368
- if (!args.allowEmpty && (0, config_js_1.looksLikeSchemaFilePath)(config.schema)) {
1369
- (0, ui_js_1.error)(`The "schema" option looks like a file path: ${(0, ui_js_1.cyan)(config.schema)}`);
1370
- (0, ui_js_1.newline)();
1371
- console.log(` ${(0, ui_js_1.dim)('"schema" is the Postgres schema NAME to introspect')} ${(0, ui_js_1.dim)('(default:')} ${(0, ui_js_1.cyan)('public')}${(0, ui_js_1.dim)(').')}`);
1372
- console.log(` ${(0, ui_js_1.dim)('The path to your defineSchema() file belongs in')} ${(0, ui_js_1.cyan)('schemaFile')}${(0, ui_js_1.dim)('.')}`);
1373
- (0, ui_js_1.newline)();
1374
- console.log(` ${(0, ui_js_1.dim)('Fix your')} ${(0, ui_js_1.cyan)('turbine.config.ts')}${(0, ui_js_1.dim)(':')}`);
1375
- console.log(` ${(0, ui_js_1.green)('schema:')} ${(0, ui_js_1.cyan)("'public'")}${(0, ui_js_1.dim)(", // or omit, introspects the 'public' schema")}`);
1376
- console.log(` ${(0, ui_js_1.green)('schemaFile:')} ${(0, ui_js_1.cyan)(`'${config.schema}'`)}${(0, ui_js_1.dim)(', // your defineSchema() file (used by `turbine push`)')}`);
1377
- (0, ui_js_1.newline)();
1378
- console.log(` ${(0, ui_js_1.dim)('Re-run with')} ${(0, ui_js_1.cyan)('--allow-empty')} ${(0, ui_js_1.dim)('to introspect this literal schema name anyway.')}`);
1379
- (0, ui_js_1.newline)();
1380
- process.exit(1);
1381
- }
1765
+ // `--allow-empty` is generate's documented escape hatch for introspecting a
1766
+ // literal schema name that happens to look like a path, so it SKIPS the
1767
+ // refusal rather than merely being named by it.
1768
+ if (args.allowEmpty !== true)
1769
+ refuseSchemaFilePath(config, { escapeHatch: '--allow-empty' });
1382
1770
  (0, ui_js_1.label)('Database', (0, ui_js_1.redactUrl)(url));
1383
1771
  (0, ui_js_1.label)('Schema', config.schema);
1384
1772
  (0, ui_js_1.label)('Output', config.out);
@@ -1412,15 +1800,15 @@ async function cmdGenerate(args, config) {
1412
1800
  if (tableNames.length === 0 && !args.allowEmpty) {
1413
1801
  (0, ui_js_1.newline)();
1414
1802
  (0, ui_js_1.error)(`Introspection matched 0 tables in schema ${(0, ui_js_1.cyan)(config.schema)}, refusing to generate an empty client.`);
1415
- (0, ui_js_1.newline)();
1416
- console.log(` ${(0, ui_js_1.dim)('Common causes:')}`);
1417
- console.log(` ${(0, ui_js_1.dim)('•')} ${(0, ui_js_1.cyan)('schema')} ${(0, ui_js_1.dim)('points at the wrong Postgres namespace')} ${(0, ui_js_1.dim)('(it is the schema NAME, default')} ${(0, ui_js_1.cyan)('public')}${(0, ui_js_1.dim)(').')}`);
1418
- console.log(` ${(0, ui_js_1.dim)('•')} ${(0, ui_js_1.dim)('You meant to set')} ${(0, ui_js_1.cyan)('schemaFile')} ${(0, ui_js_1.dim)('(your defineSchema() file), not')} ${(0, ui_js_1.cyan)('schema')}${(0, ui_js_1.dim)('.')}`);
1419
- console.log(` ${(0, ui_js_1.dim)('•')} ${(0, ui_js_1.cyan)('include')}/${(0, ui_js_1.cyan)('exclude')} ${(0, ui_js_1.dim)('filtered out every table.')}`);
1420
- console.log(` ${(0, ui_js_1.dim)('•')} ${(0, ui_js_1.dim)('The database has no tables yet, run')} ${(0, ui_js_1.cyan)('turbine push')} ${(0, ui_js_1.dim)('or a migration first.')}`);
1421
- (0, ui_js_1.newline)();
1422
- console.log(` ${(0, ui_js_1.dim)('If an empty client is genuinely what you want, re-run with')} ${(0, ui_js_1.cyan)('--allow-empty')}${(0, ui_js_1.dim)('.')}`);
1423
- (0, ui_js_1.newline)();
1803
+ (0, ui_js_1.errorLine)();
1804
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('Common causes:')}`);
1805
+ (0, ui_js_1.errorLine)(` ${(0, ui_js_1.dim)('•')} ${(0, ui_js_1.cyan)('schema')} ${(0, ui_js_1.dim)('points at the wrong Postgres namespace')} ${(0, ui_js_1.dim)('(it is the schema NAME, default')} ${(0, ui_js_1.cyan)('public')}${(0, ui_js_1.dim)(').')}`);
1806
+ (0, ui_js_1.errorLine)(` ${(0, ui_js_1.dim)('•')} ${(0, ui_js_1.dim)('You meant to set')} ${(0, ui_js_1.cyan)('schemaFile')} ${(0, ui_js_1.dim)('(your defineSchema() file), not')} ${(0, ui_js_1.cyan)('schema')}${(0, ui_js_1.dim)('.')}`);
1807
+ (0, ui_js_1.errorLine)(` ${(0, ui_js_1.dim)('•')} ${(0, ui_js_1.cyan)('include')}/${(0, ui_js_1.cyan)('exclude')} ${(0, ui_js_1.dim)('filtered out every table.')}`);
1808
+ (0, ui_js_1.errorLine)(` ${(0, ui_js_1.dim)('•')} ${(0, ui_js_1.dim)('The database has no tables yet, run')} ${(0, ui_js_1.cyan)('turbine push')} ${(0, ui_js_1.dim)('or a migration first.')}`);
1809
+ (0, ui_js_1.errorLine)();
1810
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('If an empty client is genuinely what you want, re-run with')} ${(0, ui_js_1.cyan)('--allow-empty')}${(0, ui_js_1.dim)('.')}`);
1811
+ (0, ui_js_1.errorLine)();
1424
1812
  process.exit(1);
1425
1813
  }
1426
1814
  // Print table summary
@@ -1458,9 +1846,11 @@ async function cmdGenerate(args, config) {
1458
1846
  (0, ui_js_1.newline)();
1459
1847
  console.log(` ${(0, ui_js_1.bold)('Usage:')}`);
1460
1848
  (0, ui_js_1.newline)();
1461
- console.log(` ${(0, ui_js_1.cyan)(`import { turbine } from './${config.out.replace('./', '')}';`)}`);
1849
+ console.log(` ${(0, ui_js_1.cyan)(generatedClientImport(config))}`);
1462
1850
  console.log(` ${(0, ui_js_1.dim)('const db = turbine({ connectionString: process.env.DATABASE_URL });')}`);
1463
- console.log(` ${(0, ui_js_1.dim)('const user = await db.users.findUnique({ where: { id: 1 } });')}`);
1851
+ // Name a table the client just emitted rather than a hardcoded `users`.
1852
+ const sampleAccessor = (0, schema_js_1.snakeToCamel)(tableNames[0] ?? 'users');
1853
+ console.log(` ${(0, ui_js_1.dim)(`const row = await db.${sampleAccessor}.findFirst();`)}`);
1464
1854
  (0, ui_js_1.newline)();
1465
1855
  }
1466
1856
  /**
@@ -1520,10 +1910,10 @@ async function cmdMigrateFromPrisma(args, config) {
1520
1910
  }
1521
1911
  if (!(0, node_fs_1.existsSync)(prismaPath)) {
1522
1912
  (0, ui_js_1.error)(`Prisma schema not found: ${(0, ui_js_1.cyan)(prismaPath)}`);
1523
- (0, ui_js_1.newline)();
1524
- console.log(` ${(0, ui_js_1.dim)('Point at it with')} ${(0, ui_js_1.cyan)('--schema <path>')} ${(0, ui_js_1.dim)('(a .prisma file, or a directory of them;')}`);
1525
- console.log(` ${(0, ui_js_1.dim)('default: prisma/schema.prisma, then prisma/schema/).')}`);
1526
- (0, ui_js_1.newline)();
1913
+ (0, ui_js_1.errorLine)();
1914
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('Point at it with')} ${(0, ui_js_1.cyan)('--schema <path>')} ${(0, ui_js_1.dim)('(a .prisma file, or a directory of them;')}`);
1915
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('default: prisma/schema.prisma, then prisma/schema/).')}`);
1916
+ (0, ui_js_1.errorLine)();
1527
1917
  process.exit(1);
1528
1918
  }
1529
1919
  // Parse (fatal only on a construct we must understand).
@@ -1534,8 +1924,8 @@ async function cmdMigrateFromPrisma(args, config) {
1534
1924
  catch (err) {
1535
1925
  (0, ui_js_1.newline)();
1536
1926
  (0, ui_js_1.error)(`Could not read ${(0, ui_js_1.cyan)(prismaPath)}`);
1537
- console.log(` ${(0, ui_js_1.red)(err instanceof Error ? err.message : String(err))}`);
1538
- (0, ui_js_1.newline)();
1927
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.red)(err instanceof Error ? err.message : String(err))}`);
1928
+ (0, ui_js_1.errorLine)();
1539
1929
  process.exit(1);
1540
1930
  }
1541
1931
  let ast;
@@ -1546,8 +1936,8 @@ async function cmdMigrateFromPrisma(args, config) {
1546
1936
  if (err instanceof prisma_schema_js_1.PrismaParseError) {
1547
1937
  (0, ui_js_1.newline)();
1548
1938
  (0, ui_js_1.error)(`Could not parse ${(0, ui_js_1.cyan)(prismaPath)}`);
1549
- console.log(` ${(0, ui_js_1.red)(err.message)}`);
1550
- (0, ui_js_1.newline)();
1939
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.red)(err.message)}`);
1940
+ (0, ui_js_1.errorLine)();
1551
1941
  process.exit(1);
1552
1942
  }
1553
1943
  throw err;
@@ -1626,7 +2016,7 @@ async function cmdMigrateFromPrisma(args, config) {
1626
2016
  const rel = (0, node_path_1.relative)(process.cwd(), outDir);
1627
2017
  if (rel.startsWith('..') || (0, node_path_1.resolve)(rel) !== outDir) {
1628
2018
  (0, ui_js_1.error)(`Output directory must be within the project root. Got: ${config.out}`);
1629
- (0, ui_js_1.newline)();
2019
+ (0, ui_js_1.errorLine)();
1630
2020
  process.exit(1);
1631
2021
  }
1632
2022
  (0, node_fs_1.mkdirSync)(outDir, { recursive: true });
@@ -1690,6 +2080,7 @@ async function cmdMigrateFromPrisma(args, config) {
1690
2080
  // ---------------------------------------------------------------------------
1691
2081
  async function cmdPush(args, config) {
1692
2082
  (0, ui_js_1.banner)();
2083
+ refuseSchemaFilePath(config);
1693
2084
  const url = requireUrl(config);
1694
2085
  (0, ui_js_1.label)('Database', (0, ui_js_1.redactUrl)(url));
1695
2086
  (0, ui_js_1.label)('Schema file', config.schemaFile);
@@ -1870,34 +2261,37 @@ async function cmdMigrate(args, config) {
1870
2261
  break;
1871
2262
  default:
1872
2263
  (0, ui_js_1.error)(`Unknown migrate subcommand: ${sub}`);
1873
- console.log(` ${(0, ui_js_1.dim)('Run')} ${(0, ui_js_1.cyan)('npx turbine migrate help')} ${(0, ui_js_1.dim)('for usage.')}`);
2264
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('Run')} ${(0, ui_js_1.cyan)('npx turbine migrate help')} ${(0, ui_js_1.dim)('for usage.')}`);
1874
2265
  process.exit(1);
1875
2266
  }
1876
2267
  }
1877
2268
  async function cmdMigrateCreate(args, config) {
1878
2269
  (0, ui_js_1.banner)();
2270
+ // `--auto` / `--from-diff` diff the schema FILE against the namespace, so both
2271
+ // halves of the confusion are live on this one command.
2272
+ refuseSchemaFilePath(config);
1879
2273
  const name = args.positional[0];
1880
2274
  if (!name) {
1881
2275
  (0, ui_js_1.error)('Migration name is required.');
1882
- (0, ui_js_1.newline)();
1883
- console.log(` ${(0, ui_js_1.dim)('Usage:')} ${(0, ui_js_1.cyan)('npx turbine migrate create <name>')}`);
1884
- console.log(` ${(0, ui_js_1.dim)('Example:')} ${(0, ui_js_1.cyan)('npx turbine migrate create add_users_table')}`);
1885
- console.log(` ${(0, ui_js_1.dim)('Auto:')} ${(0, ui_js_1.cyan)('npx turbine migrate create my_change --auto')}`);
1886
- (0, ui_js_1.newline)();
2276
+ (0, ui_js_1.errorLine)();
2277
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('Usage:')} ${(0, ui_js_1.cyan)('npx turbine migrate create <name>')}`);
2278
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('Example:')} ${(0, ui_js_1.cyan)('npx turbine migrate create add_users_table')}`);
2279
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('Auto:')} ${(0, ui_js_1.cyan)('npx turbine migrate create my_change --auto')}`);
2280
+ (0, ui_js_1.errorLine)();
1887
2281
  process.exit(1);
1888
2282
  }
1889
2283
  // The scaffold strategies each own the file body, so combining them is
1890
2284
  // ambiguous. Refuse up front (before any of the strategy blocks run).
1891
2285
  if (args.fromDiff && args.recipe) {
1892
2286
  (0, ui_js_1.error)('--from-diff cannot be combined with --recipe.');
1893
- console.log(` ${(0, ui_js_1.dim)('Pick one: --from-diff generates from the schema diff, --recipe scaffolds a pattern.')}`);
1894
- (0, ui_js_1.newline)();
2287
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('Pick one: --from-diff generates from the schema diff, --recipe scaffolds a pattern.')}`);
2288
+ (0, ui_js_1.errorLine)();
1895
2289
  process.exit(1);
1896
2290
  }
1897
2291
  if (args.fromDiff && args.auto) {
1898
2292
  (0, ui_js_1.error)('--from-diff cannot be combined with --auto.');
1899
- console.log(` ${(0, ui_js_1.dim)('Both generate from the schema diff; --from-diff also flags destructive statements.')}`);
1900
- (0, ui_js_1.newline)();
2293
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('Both generate from the schema diff; --from-diff also flags destructive statements.')}`);
2294
+ (0, ui_js_1.errorLine)();
1901
2295
  process.exit(1);
1902
2296
  }
1903
2297
  if (args.auto) {
@@ -2051,12 +2445,12 @@ async function cmdMigrateCreate(args, config) {
2051
2445
  if (args.recipe) {
2052
2446
  if (!migrate_js_1.MIGRATION_RECIPES[args.recipe]) {
2053
2447
  (0, ui_js_1.error)(`Unknown migration recipe: ${args.recipe}`);
2054
- (0, ui_js_1.newline)();
2055
- console.log(` ${(0, ui_js_1.dim)('Available recipes:')}`);
2448
+ (0, ui_js_1.errorLine)();
2449
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('Available recipes:')}`);
2056
2450
  for (const [key, recipe] of Object.entries(migrate_js_1.MIGRATION_RECIPES)) {
2057
- console.log(` ${(0, ui_js_1.cyan)(key)} ${(0, ui_js_1.dim)(recipe.description)}`);
2451
+ (0, ui_js_1.errorLine)(` ${(0, ui_js_1.cyan)(key)} ${(0, ui_js_1.dim)(recipe.description)}`);
2058
2452
  }
2059
- (0, ui_js_1.newline)();
2453
+ (0, ui_js_1.errorLine)();
2060
2454
  process.exit(1);
2061
2455
  }
2062
2456
  const file = (0, migrate_js_1.createMigration)(config.migrationsDir, name, undefined, { recipe: args.recipe });
@@ -2254,11 +2648,11 @@ async function cmdMigrateDeploy(args, config) {
2254
2648
  if (plan.mismatches.length > 0) {
2255
2649
  if (!args.allowDrift) {
2256
2650
  (0, ui_js_1.error)('Deploy blocked by migration drift');
2257
- (0, ui_js_1.newline)();
2651
+ (0, ui_js_1.errorLine)();
2258
2652
  for (const line of (0, migrate_js_1.formatChecksumMismatchError)(plan.mismatches).split('\n')) {
2259
- console.log(` ${line.replace('[turbine] ', '')}`);
2653
+ (0, ui_js_1.errorLine)(`${line.replace('[turbine] ', '')}`);
2260
2654
  }
2261
- (0, ui_js_1.newline)();
2655
+ (0, ui_js_1.errorLine)();
2262
2656
  process.exit(1);
2263
2657
  }
2264
2658
  (0, ui_js_1.warn)('--allow-drift is set: checksum validation is DISABLED for this deploy.');
@@ -2589,6 +2983,7 @@ async function cmdSeed(_args, config) {
2589
2983
  // ---------------------------------------------------------------------------
2590
2984
  async function cmdStatus(_args, config) {
2591
2985
  (0, ui_js_1.banner)();
2986
+ refuseSchemaFilePath(config);
2592
2987
  const url = requireUrl(config);
2593
2988
  (0, ui_js_1.label)('Database', (0, ui_js_1.redactUrl)(url));
2594
2989
  (0, ui_js_1.label)('Schema', config.schema);
@@ -2728,6 +3123,7 @@ function refusePoolerConnection(url, args) {
2728
3123
  }
2729
3124
  async function cmdDoctor(args, config) {
2730
3125
  const jsonMode = args.json === true;
3126
+ refuseSchemaFilePath(config);
2731
3127
  const url = requireUrl(config);
2732
3128
  // Before the banner, before introspect, before any pool is constructed.
2733
3129
  refusePoolerConnection(url, args);
@@ -3475,6 +3871,9 @@ function isLoopbackHost(host) {
3475
3871
  async function cmdStudio(args, config) {
3476
3872
  (0, ui_js_1.banner)();
3477
3873
  const demo = args.demo === true;
3874
+ // Demo mode reads no namespace at all, so there is nothing to mistake there.
3875
+ if (!demo)
3876
+ refuseSchemaFilePath(config);
3478
3877
  // Demo mode is self-contained (seeded in-memory database), so it never needs
3479
3878
  // a DATABASE_URL. The placeholder is only used for display.
3480
3879
  const url = demo ? 'demo://in-memory' : requireUrl(config);
@@ -3491,10 +3890,10 @@ async function cmdStudio(args, config) {
3491
3890
  if (!isLoopbackHost(host)) {
3492
3891
  if (!args.allowRemote) {
3493
3892
  (0, ui_js_1.error)(`Studio refuses to bind to ${(0, ui_js_1.yellow)(host)} without ${(0, ui_js_1.cyan)('--allow-remote')}.`);
3494
- (0, ui_js_1.newline)();
3495
- console.log(` ${(0, ui_js_1.dim)('Loopback only by default')} ${(0, ui_js_1.dim)('(127.0.0.1, localhost, ::1).')}`);
3496
- console.log(` ${(0, ui_js_1.dim)('Pass')} ${(0, ui_js_1.cyan)('--allow-remote')} ${(0, ui_js_1.dim)('to opt in to network exposure.')}`);
3497
- (0, ui_js_1.newline)();
3893
+ (0, ui_js_1.errorLine)();
3894
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('Loopback only by default')} ${(0, ui_js_1.dim)('(127.0.0.1, localhost, ::1).')}`);
3895
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('Pass')} ${(0, ui_js_1.cyan)('--allow-remote')} ${(0, ui_js_1.dim)('to opt in to network exposure.')}`);
3896
+ (0, ui_js_1.errorLine)();
3498
3897
  process.exit(1);
3499
3898
  }
3500
3899
  // warn() prints; it returns void. Wrapping it in console.log() printed a
@@ -3602,6 +4001,7 @@ async function cmdStudio(args, config) {
3602
4001
  // Command: mcp, read-only JSON-RPC stdio server
3603
4002
  // ---------------------------------------------------------------------------
3604
4003
  async function cmdMcp(_args, config) {
4004
+ refuseSchemaFilePath(config);
3605
4005
  const url = requireUrl(config);
3606
4006
  await (0, mcp_js_1.runMcpServer)({
3607
4007
  url,
@@ -3620,10 +4020,10 @@ async function cmdObserve(args) {
3620
4020
  const url = process.env.TURBINE_OBSERVE_URL;
3621
4021
  if (!url) {
3622
4022
  (0, ui_js_1.error)('TURBINE_OBSERVE_URL environment variable is required for the observe command.');
3623
- (0, ui_js_1.newline)();
3624
- console.log(` ${(0, ui_js_1.dim)('Set it to the Postgres connection string where metrics are stored.')}`);
3625
- console.log(` ${(0, ui_js_1.dim)('Example:')} ${(0, ui_js_1.cyan)('TURBINE_OBSERVE_URL=postgres://... npx turbine observe')}`);
3626
- (0, ui_js_1.newline)();
4023
+ (0, ui_js_1.errorLine)();
4024
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('Set it to the Postgres connection string where metrics are stored.')}`);
4025
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('Example:')} ${(0, ui_js_1.cyan)('TURBINE_OBSERVE_URL=postgres://... npx turbine observe')}`);
4026
+ (0, ui_js_1.errorLine)();
3627
4027
  process.exit(1);
3628
4028
  }
3629
4029
  const port = args.port ?? 4984;
@@ -3638,10 +4038,10 @@ async function cmdObserve(args) {
3638
4038
  if (!isLoopbackHost(host)) {
3639
4039
  if (!args.allowRemote) {
3640
4040
  (0, ui_js_1.error)(`Observe refuses to bind to ${(0, ui_js_1.yellow)(host)} without ${(0, ui_js_1.cyan)('--allow-remote')}.`);
3641
- (0, ui_js_1.newline)();
3642
- console.log(` ${(0, ui_js_1.dim)('Loopback only by default')} ${(0, ui_js_1.dim)('(127.0.0.1, localhost, ::1).')}`);
3643
- console.log(` ${(0, ui_js_1.dim)('Pass')} ${(0, ui_js_1.cyan)('--allow-remote')} ${(0, ui_js_1.dim)('to opt in to network exposure.')}`);
3644
- (0, ui_js_1.newline)();
4041
+ (0, ui_js_1.errorLine)();
4042
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('Loopback only by default')} ${(0, ui_js_1.dim)('(127.0.0.1, localhost, ::1).')}`);
4043
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('Pass')} ${(0, ui_js_1.cyan)('--allow-remote')} ${(0, ui_js_1.dim)('to opt in to network exposure.')}`);
4044
+ (0, ui_js_1.errorLine)();
3645
4045
  process.exit(1);
3646
4046
  }
3647
4047
  // warn() prints and returns void; see the same guard in cmdStudio.
@@ -3706,6 +4106,8 @@ function showSubcommandHelp(command) {
3706
4106
  doctor: showDoctorHelp,
3707
4107
  studio: showStudioHelp,
3708
4108
  mcp: showMcpHelp,
4109
+ observe: showObserveHelp,
4110
+ skill: showSkillHelp,
3709
4111
  };
3710
4112
  const fn = helpMap[command];
3711
4113
  if (fn) {
@@ -3733,6 +4135,7 @@ function showInitHelp() {
3733
4135
  console.log(` ${(0, ui_js_1.cyan)('--url, -u')} ${(0, ui_js_1.dim)('<url>')} Postgres connection string to embed in config`);
3734
4136
  console.log(` ${(0, ui_js_1.cyan)('--force, -f')} Overwrite existing config file`);
3735
4137
  console.log(` ${(0, ui_js_1.cyan)('--yes, -y')} Accept every step's default (non-interactive)`);
4138
+ console.log(` ${(0, ui_js_1.cyan)('--schema-file')} ${(0, ui_js_1.dim)('<path>')} Where to scaffold the schema file ${(0, ui_js_1.dim)('(default: ./turbine/schema.ts)')}`);
3736
4139
  console.log(` ${(0, ui_js_1.cyan)('--skip-schema')} Don't create the starter schema file`);
3737
4140
  console.log(` ${(0, ui_js_1.cyan)('--with-schema')} Create it even if the database already has tables`);
3738
4141
  console.log(` ${(0, ui_js_1.cyan)('--skip-seed')} Don't create the seed file or run the seed`);
@@ -3813,8 +4216,11 @@ function showPushHelp() {
3813
4216
  console.log(` Reads your ${(0, ui_js_1.cyan)('turbine/schema.ts')} file, diffs against the live database,`);
3814
4217
  console.log(` and applies CREATE/ALTER statements.`);
3815
4218
  (0, ui_js_1.newline)();
4219
+ console.log(` ${(0, ui_js_1.dim)('Note:')} the file is ${(0, ui_js_1.cyan)('--schema-file')}${(0, ui_js_1.dim)('.')} ${(0, ui_js_1.cyan)('--schema')} ${(0, ui_js_1.dim)('is the Postgres namespace to write into.')}`);
4220
+ (0, ui_js_1.newline)();
3816
4221
  console.log(` ${(0, ui_js_1.bold)('Options:')}`);
3817
4222
  console.log(` ${(0, ui_js_1.cyan)('--url, -u')} ${(0, ui_js_1.dim)('<url>')} Postgres connection string`);
4223
+ console.log(` ${(0, ui_js_1.cyan)('--schema-file')} ${(0, ui_js_1.dim)('<path>')} Your ${(0, ui_js_1.cyan)('defineSchema()')} file ${(0, ui_js_1.dim)('(default: ./turbine/schema.ts)')}`);
3818
4224
  console.log(` ${(0, ui_js_1.cyan)('--dry-run')} Show SQL without executing`);
3819
4225
  console.log(` ${(0, ui_js_1.cyan)('--allow-destructive')} Skip the interactive confirmation for data-destroying statements ${(0, ui_js_1.dim)('(CI)')}`);
3820
4226
  console.log(` ${(0, ui_js_1.cyan)('--verbose, -v')} Show detailed output`);
@@ -3836,6 +4242,7 @@ function showMigrateHelp() {
3836
4242
  (0, ui_js_1.newline)();
3837
4243
  console.log(` ${(0, ui_js_1.bold)('Options:')}`);
3838
4244
  console.log(` ${(0, ui_js_1.cyan)('--url, -u')} ${(0, ui_js_1.dim)('<url>')} Postgres connection string`);
4245
+ console.log(` ${(0, ui_js_1.cyan)('--schema-file')} ${(0, ui_js_1.dim)('<path>')} Your ${(0, ui_js_1.cyan)('defineSchema()')} file, for ${(0, ui_js_1.cyan)('--auto')} / ${(0, ui_js_1.cyan)('--from-diff')}`);
3839
4246
  console.log(` ${(0, ui_js_1.cyan)('--auto')} Auto-generate UP/DOWN SQL from schema diff ${(0, ui_js_1.dim)('(create only)')}`);
3840
4247
  console.log(` ${(0, ui_js_1.cyan)('--from-diff')} Generate from schema diff, flagging destructive statements ${(0, ui_js_1.dim)('(create only)')}`);
3841
4248
  console.log(` ${(0, ui_js_1.cyan)('--recipe')} ${(0, ui_js_1.dim)('<name>')} Scaffold a sanctioned migration pattern ${(0, ui_js_1.dim)('(create only, e.g. backfill)')}`);
@@ -3988,6 +4395,51 @@ function showMcpHelp() {
3988
4395
  console.log(` ${(0, ui_js_1.cyan)('--exclude')} ${(0, ui_js_1.dim)('<tables>')} Comma-separated tables to exclude`);
3989
4396
  (0, ui_js_1.newline)();
3990
4397
  }
4398
+ function showObserveHelp() {
4399
+ (0, ui_js_1.banner)();
4400
+ console.log(` ${(0, ui_js_1.bold)('turbine observe')}, Query metrics dashboard`);
4401
+ (0, ui_js_1.newline)();
4402
+ console.log(` ${(0, ui_js_1.bold)('Usage:')}`);
4403
+ console.log(` TURBINE_OBSERVE_URL=postgres://... npx turbine observe ${(0, ui_js_1.dim)('[options]')}`);
4404
+ (0, ui_js_1.newline)();
4405
+ console.log(` Reads the ${(0, ui_js_1.cyan)('_turbine_metrics')} table written by ${(0, ui_js_1.cyan)('db.$observe()')} and serves`);
4406
+ console.log(` per-minute count / avg / p50 / p95 / p99 / error aggregates. Read-only,`);
4407
+ console.log(` loopback-bound, behind a random per-session token, same model as Studio.`);
4408
+ (0, ui_js_1.newline)();
4409
+ console.log(` ${(0, ui_js_1.dim)('The connection string comes from')} ${(0, ui_js_1.cyan)('TURBINE_OBSERVE_URL')}${(0, ui_js_1.dim)(', not')} ${(0, ui_js_1.cyan)('DATABASE_URL')}${(0, ui_js_1.dim)(':')}`);
4410
+ console.log(` ${(0, ui_js_1.dim)('metrics are meant to live in a different database from the one they measure.')}`);
4411
+ (0, ui_js_1.newline)();
4412
+ console.log(` ${(0, ui_js_1.bold)('Options:')}`);
4413
+ console.log(` ${(0, ui_js_1.cyan)('--port')} ${(0, ui_js_1.dim)('<n>')} HTTP port ${(0, ui_js_1.dim)('(default: 4984)')}`);
4414
+ 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)')}`);
4415
+ console.log(` ${(0, ui_js_1.cyan)('--no-open')} Don't auto-open the browser`);
4416
+ 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)')}`);
4417
+ (0, ui_js_1.newline)();
4418
+ }
4419
+ function showSkillHelp() {
4420
+ (0, ui_js_1.banner)();
4421
+ console.log(` ${(0, ui_js_1.bold)('turbine skill')}, Install the agent query skill`);
4422
+ (0, ui_js_1.newline)();
4423
+ console.log(` ${(0, ui_js_1.bold)('Usage:')}`);
4424
+ console.log(` npx turbine skill ${(0, ui_js_1.dim)('[options]')}`);
4425
+ (0, ui_js_1.newline)();
4426
+ console.log(` Writes ${(0, ui_js_1.cyan)('.claude/skills/turbine-orm/SKILL.md')}: how to write Turbine queries,`);
4427
+ console.log(` for a coding agent working in this project. The file ships in the package`);
4428
+ console.log(` rather than being generated here, and every factual claim in it is executed`);
4429
+ console.log(` against a live database on each release.`);
4430
+ (0, ui_js_1.newline)();
4431
+ console.log(` ${(0, ui_js_1.bold)('Options:')}`);
4432
+ console.log(` ${(0, ui_js_1.cyan)('--print')} Write the skill to stdout instead of installing it`);
4433
+ console.log(` ${(0, ui_js_1.cyan)('--agents')} Print the AGENTS.md / CLAUDE.md instructions block`);
4434
+ console.log(` ${(0, ui_js_1.cyan)('--dir')} ${(0, ui_js_1.dim)('<path>')} Skills root to install into ${(0, ui_js_1.dim)('(default: .claude/skills)')}`);
4435
+ (0, ui_js_1.newline)();
4436
+ console.log(` ${(0, ui_js_1.bold)('Examples:')}`);
4437
+ console.log(` ${(0, ui_js_1.dim)('$')} npx turbine skill`);
4438
+ console.log(` ${(0, ui_js_1.dim)('$')} npx turbine skill --agents >> AGENTS.md`);
4439
+ (0, ui_js_1.newline)();
4440
+ console.log(` ${(0, ui_js_1.dim)('Also worth connecting:')} ${(0, ui_js_1.cyan)('npx turbine mcp')}${(0, ui_js_1.dim)(', the read-only MCP server.')}`);
4441
+ (0, ui_js_1.newline)();
4442
+ }
3991
4443
  // ---------------------------------------------------------------------------
3992
4444
  // Help
3993
4445
  // ---------------------------------------------------------------------------
@@ -4018,7 +4470,8 @@ function showHelp() {
4018
4470
  console.log(` ${(0, ui_js_1.bold)('Options:')}`);
4019
4471
  console.log(` ${(0, ui_js_1.cyan)('--url, -u')} ${(0, ui_js_1.dim)('<url>')} Postgres connection string`);
4020
4472
  console.log(` ${(0, ui_js_1.cyan)('--out, -o')} ${(0, ui_js_1.dim)('<dir>')} Output directory ${(0, ui_js_1.dim)('(default: ./generated/turbine)')}`);
4021
- console.log(` ${(0, ui_js_1.cyan)('--schema, -s')} ${(0, ui_js_1.dim)('<name>')} Postgres schema ${(0, ui_js_1.dim)('(default: public)')}`);
4473
+ console.log(` ${(0, ui_js_1.cyan)('--schema, -s')} ${(0, ui_js_1.dim)('<name>')} Postgres schema NAME to read ${(0, ui_js_1.dim)('(default: public)')}`);
4474
+ console.log(` ${(0, ui_js_1.cyan)('--schema-file')} ${(0, ui_js_1.dim)('<path>')} Your ${(0, ui_js_1.cyan)('defineSchema()')} file ${(0, ui_js_1.dim)('(push / migrate --auto / init)')}`);
4022
4475
  console.log(` ${(0, ui_js_1.cyan)('--include')} ${(0, ui_js_1.dim)('<tables>')} Comma-separated tables to include`);
4023
4476
  console.log(` ${(0, ui_js_1.cyan)('--exclude')} ${(0, ui_js_1.dim)('<tables>')} Comma-separated tables to exclude`);
4024
4477
  console.log(` ${(0, ui_js_1.cyan)('--dry-run')} Show SQL without executing`);
@@ -4136,9 +4589,9 @@ function cmdSkill(args) {
4136
4589
  const source = root ? (0, node_path_1.resolve)(root, 'skills', 'turbine-orm', 'SKILL.md') : undefined;
4137
4590
  if (!source || !(0, node_fs_1.existsSync)(source)) {
4138
4591
  (0, ui_js_1.error)('Could not find the packaged skill inside turbine-orm.');
4139
- (0, ui_js_1.newline)();
4140
- console.log(` ${(0, ui_js_1.dim)('Read it online instead:')} ${(0, ui_js_1.cyan)('https://turbineorm.dev/ai-agents')}`);
4141
- (0, ui_js_1.newline)();
4592
+ (0, ui_js_1.errorLine)();
4593
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('Read it online instead:')} ${(0, ui_js_1.cyan)('https://turbineorm.dev/ai-agents')}`);
4594
+ (0, ui_js_1.errorLine)();
4142
4595
  process.exit(1);
4143
4596
  }
4144
4597
  const body = (0, node_fs_1.readFileSync)(source, 'utf8');
@@ -4236,6 +4689,7 @@ async function main() {
4236
4689
  url: args.url,
4237
4690
  out: args.out,
4238
4691
  schema: args.schema,
4692
+ schemaFile: args.schemaFile,
4239
4693
  include: args.include,
4240
4694
  exclude: args.exclude,
4241
4695
  importExtension: args.importExtension,
@@ -4312,50 +4766,60 @@ async function main() {
4312
4766
  case 'skill':
4313
4767
  cmdSkill(args);
4314
4768
  break;
4315
- default:
4769
+ default: {
4316
4770
  (0, ui_js_1.error)(`Unknown command: ${(0, ui_js_1.bold)(args.command)}`);
4317
- (0, ui_js_1.newline)();
4318
- console.log(` ${(0, ui_js_1.dim)('Run')} ${(0, ui_js_1.cyan)('npx turbine help')} ${(0, ui_js_1.dim)('for available commands.')}`);
4319
- (0, ui_js_1.newline)();
4771
+ (0, ui_js_1.errorLine)();
4772
+ // Only canonical spellings are offered. `nameCloseness` scores substring
4773
+ // containment above edit distance, so including the one-letter aliases
4774
+ // would answer "genrate" with "g" (contained, score 501) instead of
4775
+ // "generate" (one edit away, score 99).
4776
+ const suggestion = (0, utils_js_1.closestName)(args.command, knownCommands());
4777
+ if (suggestion) {
4778
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('Did you mean')} ${(0, ui_js_1.cyan)(`turbine ${suggestion}`)}${(0, ui_js_1.dim)('?')}`);
4779
+ (0, ui_js_1.errorLine)();
4780
+ }
4781
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('Run')} ${(0, ui_js_1.cyan)('npx turbine help')} ${(0, ui_js_1.dim)('for available commands.')}`);
4782
+ (0, ui_js_1.errorLine)();
4320
4783
  process.exit(1);
4784
+ }
4321
4785
  }
4322
4786
  }
4323
4787
  catch (err) {
4324
4788
  if (err instanceof Error) {
4325
4789
  if (err.message.includes('ECONNREFUSED') || err.message.includes('connection')) {
4326
- (0, ui_js_1.newline)();
4790
+ (0, ui_js_1.errorLine)();
4327
4791
  (0, ui_js_1.error)(`Could not connect to database`);
4328
- console.log(` ${(0, ui_js_1.dim)((0, ui_js_1.redactUrl)(err.message))}`);
4329
- (0, ui_js_1.newline)();
4330
- console.log(` ${(0, ui_js_1.dim)('Check that:')}`);
4331
- console.log(` ${(0, ui_js_1.dim)('1.')} Your database is running`);
4332
- console.log(` ${(0, ui_js_1.dim)('2.')} The connection string is correct`);
4333
- console.log(` ${(0, ui_js_1.dim)('3.')} Network/firewall allows the connection`);
4792
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)((0, ui_js_1.redactUrl)(err.message))}`);
4793
+ (0, ui_js_1.errorLine)();
4794
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('Check that:')}`);
4795
+ (0, ui_js_1.errorLine)(` ${(0, ui_js_1.dim)('1.')} Your database is running`);
4796
+ (0, ui_js_1.errorLine)(` ${(0, ui_js_1.dim)('2.')} The connection string is correct`);
4797
+ (0, ui_js_1.errorLine)(` ${(0, ui_js_1.dim)('3.')} Network/firewall allows the connection`);
4334
4798
  }
4335
4799
  else if (err.message.includes('authentication')) {
4336
- (0, ui_js_1.newline)();
4800
+ (0, ui_js_1.errorLine)();
4337
4801
  (0, ui_js_1.error)(`Authentication failed`);
4338
- console.log(` ${(0, ui_js_1.dim)((0, ui_js_1.redactUrl)(err.message))}`);
4802
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)((0, ui_js_1.redactUrl)(err.message))}`);
4339
4803
  }
4340
4804
  else if (err.message.includes('does not exist')) {
4341
- (0, ui_js_1.newline)();
4805
+ (0, ui_js_1.errorLine)();
4342
4806
  (0, ui_js_1.error)(`Database or schema not found`);
4343
- console.log(` ${(0, ui_js_1.dim)((0, ui_js_1.redactUrl)(err.message))}`);
4807
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)((0, ui_js_1.redactUrl)(err.message))}`);
4344
4808
  }
4345
4809
  else {
4346
- (0, ui_js_1.newline)();
4810
+ (0, ui_js_1.errorLine)();
4347
4811
  (0, ui_js_1.error)((0, ui_js_1.redactUrl)(err.message));
4348
4812
  if (args.verbose && err.stack) {
4349
- (0, ui_js_1.newline)();
4350
- console.log((0, ui_js_1.dim)((0, ui_js_1.redactUrl)(err.stack)));
4813
+ (0, ui_js_1.errorLine)();
4814
+ console.error((0, ui_js_1.dim)((0, ui_js_1.redactUrl)(err.stack)));
4351
4815
  }
4352
4816
  }
4353
4817
  }
4354
4818
  else {
4355
- (0, ui_js_1.newline)();
4819
+ (0, ui_js_1.errorLine)();
4356
4820
  (0, ui_js_1.error)(`Unexpected error: ${(0, ui_js_1.redactUrl)(String(err))}`);
4357
4821
  }
4358
- (0, ui_js_1.newline)();
4822
+ (0, ui_js_1.errorLine)();
4359
4823
  process.exit(1);
4360
4824
  }
4361
4825
  }