turbine-orm 0.81.0 → 0.90.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 (295) hide show
  1. package/README.md +26 -25
  2. package/dist/checkout.d.ts +27 -2
  3. package/dist/checkout.js +75 -4
  4. package/dist/cjs/checkout.d.ts +27 -2
  5. package/dist/cjs/checkout.js +76 -3
  6. package/dist/cjs/cli/code-first-tags.d.ts +62 -0
  7. package/dist/cjs/cli/code-first-tags.js +124 -0
  8. package/dist/cjs/cli/compile-query.js +9 -0
  9. package/dist/cjs/cli/config.d.ts +35 -6
  10. package/dist/cjs/cli/config.js +17 -1
  11. package/dist/cjs/cli/destructive.d.ts +7 -4
  12. package/dist/cjs/cli/destructive.js +224 -20
  13. package/dist/cjs/cli/error-catalog.js +36 -0
  14. package/dist/cjs/cli/error-text.d.ts +15 -0
  15. package/dist/cjs/cli/error-text.js +40 -0
  16. package/dist/cjs/cli/index.d.ts +62 -4
  17. package/dist/cjs/cli/index.js +568 -108
  18. package/dist/cjs/cli/local-server-guard.d.ts +53 -0
  19. package/dist/cjs/cli/local-server-guard.js +119 -0
  20. package/dist/cjs/cli/local-server-output.d.ts +41 -0
  21. package/dist/cjs/cli/local-server-output.js +103 -0
  22. package/dist/cjs/cli/mcp.d.ts +8 -0
  23. package/dist/cjs/cli/mcp.js +121 -32
  24. package/dist/cjs/cli/migrate.d.ts +113 -5
  25. package/dist/cjs/cli/migrate.js +270 -21
  26. package/dist/cjs/cli/observe.d.ts +6 -1
  27. package/dist/cjs/cli/observe.js +85 -55
  28. package/dist/cjs/cli/pii-predicate-guard.d.ts +43 -23
  29. package/dist/cjs/cli/pii-predicate-guard.js +98 -37
  30. package/dist/cjs/cli/prisma-resolve.js +17 -13
  31. package/dist/cjs/cli/prisma-schema.js +12 -4
  32. package/dist/cjs/cli/studio-ui.generated.js +1 -1
  33. package/dist/cjs/cli/studio.d.ts +14 -2
  34. package/dist/cjs/cli/studio.js +244 -69
  35. package/dist/cjs/cli/ui.d.ts +1 -1
  36. package/dist/cjs/client.d.ts +558 -85
  37. package/dist/cjs/client.js +1270 -395
  38. package/dist/cjs/connection-guard.d.ts +13 -0
  39. package/dist/cjs/connection-guard.js +28 -0
  40. package/dist/cjs/connection-url.d.ts +51 -16
  41. package/dist/cjs/connection-url.js +67 -34
  42. package/dist/cjs/diagnostics.d.ts +81 -0
  43. package/dist/cjs/diagnostics.js +147 -0
  44. package/dist/cjs/dialect.d.ts +449 -20
  45. package/dist/cjs/dialect.js +117 -76
  46. package/dist/cjs/engine-config.d.ts +7 -6
  47. package/dist/cjs/errors.d.ts +126 -6
  48. package/dist/cjs/errors.js +256 -13
  49. package/dist/cjs/generate.d.ts +57 -7
  50. package/dist/cjs/generate.js +635 -279
  51. package/dist/cjs/global-filters-config.d.ts +59 -0
  52. package/dist/cjs/global-filters-config.js +367 -0
  53. package/dist/cjs/index.d.ts +9 -4
  54. package/dist/cjs/index.js +10 -2
  55. package/dist/cjs/introspect.d.ts +31 -6
  56. package/dist/cjs/introspect.js +108 -50
  57. package/dist/cjs/log-codes.d.ts +12 -0
  58. package/dist/cjs/log-codes.js +77 -0
  59. package/dist/cjs/logger.d.ts +87 -0
  60. package/dist/cjs/logger.js +98 -0
  61. package/dist/cjs/mssql.d.ts +8 -5
  62. package/dist/cjs/mssql.js +352 -65
  63. package/dist/cjs/mysql.d.ts +3 -5
  64. package/dist/cjs/mysql.js +420 -70
  65. package/dist/cjs/nested-write.d.ts +3 -0
  66. package/dist/cjs/nested-write.js +179 -114
  67. package/dist/cjs/pipeline-submittable.d.ts +12 -0
  68. package/dist/cjs/pipeline-submittable.js +118 -44
  69. package/dist/cjs/pipeline.js +21 -26
  70. package/dist/cjs/powdb-introspect.d.ts +5 -2
  71. package/dist/cjs/powdb-introspect.js +20 -15
  72. package/dist/cjs/powdb-shared.d.ts +36 -5
  73. package/dist/cjs/powdb-shared.js +67 -8
  74. package/dist/cjs/powdb.d.ts +4 -0
  75. package/dist/cjs/powdb.js +183 -51
  76. package/dist/cjs/powql.d.ts +394 -51
  77. package/dist/cjs/powql.js +1829 -284
  78. package/dist/cjs/prisma-compat.d.ts +23 -15
  79. package/dist/cjs/prisma-compat.js +507 -372
  80. package/dist/cjs/query/aggregates.d.ts +17 -1
  81. package/dist/cjs/query/aggregates.js +261 -29
  82. package/dist/cjs/query/batched-loader.d.ts +25 -6
  83. package/dist/cjs/query/batched-loader.js +103 -20
  84. package/dist/cjs/query/builder.d.ts +325 -184
  85. package/dist/cjs/query/builder.js +1452 -490
  86. package/dist/cjs/query/column-ref.d.ts +61 -0
  87. package/dist/cjs/query/column-ref.js +73 -0
  88. package/dist/cjs/query/compound-unique.d.ts +21 -0
  89. package/dist/cjs/query/compound-unique.js +98 -18
  90. package/dist/cjs/query/deferred.d.ts +53 -16
  91. package/dist/cjs/query/expr.d.ts +217 -0
  92. package/dist/cjs/query/expr.js +859 -0
  93. package/dist/cjs/query/filters.d.ts +155 -53
  94. package/dist/cjs/query/filters.js +613 -76
  95. package/dist/cjs/query/index.d.ts +2 -0
  96. package/dist/cjs/query/index.js +7 -1
  97. package/dist/cjs/query/json-value.d.ts +43 -0
  98. package/dist/cjs/query/json-value.js +59 -0
  99. package/dist/cjs/query/normalize-args.d.ts +87 -0
  100. package/dist/cjs/query/normalize-args.js +527 -0
  101. package/dist/cjs/query/option-surface.d.ts +16 -4
  102. package/dist/cjs/query/option-surface.js +57 -14
  103. package/dist/cjs/query/relation-names.d.ts +14 -0
  104. package/dist/cjs/query/relation-names.js +77 -18
  105. package/dist/cjs/query/relations.d.ts +30 -3
  106. package/dist/cjs/query/relations.js +219 -63
  107. package/dist/cjs/query/row-lock.d.ts +49 -0
  108. package/dist/cjs/query/row-lock.js +138 -0
  109. package/dist/cjs/query/sql-comment.d.ts +161 -0
  110. package/dist/cjs/query/sql-comment.js +381 -0
  111. package/dist/cjs/query/stream-idle.d.ts +132 -0
  112. package/dist/cjs/query/stream-idle.js +212 -0
  113. package/dist/cjs/query/table-client.d.ts +101 -0
  114. package/dist/cjs/query/table-client.js +74 -0
  115. package/dist/cjs/query/types.d.ts +677 -298
  116. package/dist/cjs/query/utils.d.ts +105 -6
  117. package/dist/cjs/query/utils.js +426 -19
  118. package/dist/cjs/query/warn-registry.d.ts +18 -0
  119. package/dist/cjs/query/warn-registry.js +18 -0
  120. package/dist/cjs/query/where-compile.d.ts +138 -14
  121. package/dist/cjs/query/where-compile.js +513 -29
  122. package/dist/cjs/query/where.d.ts +159 -59
  123. package/dist/cjs/query/where.js +716 -246
  124. package/dist/cjs/query/writes.d.ts +112 -50
  125. package/dist/cjs/query/writes.js +1498 -200
  126. package/dist/cjs/query-events.d.ts +4 -2
  127. package/dist/cjs/query-events.js +3 -1
  128. package/dist/cjs/realtime.d.ts +7 -3
  129. package/dist/cjs/realtime.js +13 -2
  130. package/dist/cjs/schema-builder.d.ts +55 -4
  131. package/dist/cjs/schema-builder.js +382 -18
  132. package/dist/cjs/schema-metadata.d.ts +21 -1
  133. package/dist/cjs/schema-metadata.js +29 -7
  134. package/dist/cjs/schema-sql.d.ts +75 -5
  135. package/dist/cjs/schema-sql.js +497 -45
  136. package/dist/cjs/schema.d.ts +26 -5
  137. package/dist/cjs/schema.js +110 -13
  138. package/dist/cjs/sql-placeholders.d.ts +56 -0
  139. package/dist/cjs/sql-placeholders.js +204 -0
  140. package/dist/cjs/sqlite.d.ts +75 -20
  141. package/dist/cjs/sqlite.js +593 -95
  142. package/dist/cjs/supplied-pool-types.d.ts +48 -0
  143. package/dist/cjs/supplied-pool-types.js +256 -0
  144. package/dist/cjs/transaction-control.d.ts +51 -0
  145. package/dist/cjs/transaction-control.js +316 -0
  146. package/dist/cjs/transaction-state.d.ts +342 -0
  147. package/dist/cjs/transaction-state.js +707 -0
  148. package/dist/cjs/typed-sql.d.ts +37 -3
  149. package/dist/cjs/typed-sql.js +102 -8
  150. package/dist/cli/code-first-tags.d.ts +62 -0
  151. package/dist/cli/code-first-tags.js +120 -0
  152. package/dist/cli/compile-query.js +9 -0
  153. package/dist/cli/config.d.ts +35 -6
  154. package/dist/cli/config.js +16 -1
  155. package/dist/cli/destructive.d.ts +7 -4
  156. package/dist/cli/destructive.js +224 -20
  157. package/dist/cli/error-catalog.js +36 -0
  158. package/dist/cli/error-text.d.ts +15 -0
  159. package/dist/cli/error-text.js +37 -0
  160. package/dist/cli/index.d.ts +62 -4
  161. package/dist/cli/index.js +569 -112
  162. package/dist/cli/local-server-guard.d.ts +53 -0
  163. package/dist/cli/local-server-guard.js +113 -0
  164. package/dist/cli/local-server-output.d.ts +41 -0
  165. package/dist/cli/local-server-output.js +98 -0
  166. package/dist/cli/mcp.d.ts +8 -0
  167. package/dist/cli/mcp.js +121 -34
  168. package/dist/cli/migrate.d.ts +113 -5
  169. package/dist/cli/migrate.js +261 -21
  170. package/dist/cli/observe.d.ts +6 -1
  171. package/dist/cli/observe.js +84 -54
  172. package/dist/cli/pii-predicate-guard.d.ts +43 -23
  173. package/dist/cli/pii-predicate-guard.js +100 -35
  174. package/dist/cli/prisma-resolve.js +15 -11
  175. package/dist/cli/prisma-schema.js +12 -4
  176. package/dist/cli/studio-ui.generated.js +1 -1
  177. package/dist/cli/studio.d.ts +14 -2
  178. package/dist/cli/studio.js +246 -71
  179. package/dist/cli/ui.d.ts +1 -1
  180. package/dist/client.d.ts +558 -85
  181. package/dist/client.js +1275 -400
  182. package/dist/connection-guard.d.ts +13 -0
  183. package/dist/connection-guard.js +27 -0
  184. package/dist/connection-url.d.ts +51 -16
  185. package/dist/connection-url.js +63 -34
  186. package/dist/diagnostics.d.ts +81 -0
  187. package/dist/diagnostics.js +144 -0
  188. package/dist/dialect.d.ts +449 -20
  189. package/dist/dialect.js +115 -43
  190. package/dist/engine-config.d.ts +7 -6
  191. package/dist/errors.d.ts +126 -6
  192. package/dist/errors.js +252 -12
  193. package/dist/generate.d.ts +57 -7
  194. package/dist/generate.js +635 -281
  195. package/dist/global-filters-config.d.ts +59 -0
  196. package/dist/global-filters-config.js +361 -0
  197. package/dist/index.d.ts +9 -4
  198. package/dist/index.js +4 -2
  199. package/dist/introspect.d.ts +31 -6
  200. package/dist/introspect.js +106 -50
  201. package/dist/log-codes.d.ts +12 -0
  202. package/dist/log-codes.js +74 -0
  203. package/dist/logger.d.ts +87 -0
  204. package/dist/logger.js +93 -0
  205. package/dist/mssql.d.ts +8 -5
  206. package/dist/mssql.js +354 -67
  207. package/dist/mysql.d.ts +3 -5
  208. package/dist/mysql.js +423 -73
  209. package/dist/nested-write.d.ts +3 -0
  210. package/dist/nested-write.js +177 -112
  211. package/dist/pipeline-submittable.d.ts +12 -0
  212. package/dist/pipeline-submittable.js +117 -44
  213. package/dist/pipeline.js +21 -26
  214. package/dist/powdb-introspect.d.ts +5 -2
  215. package/dist/powdb-introspect.js +21 -16
  216. package/dist/powdb-shared.d.ts +36 -5
  217. package/dist/powdb-shared.js +65 -8
  218. package/dist/powdb.d.ts +4 -0
  219. package/dist/powdb.js +183 -51
  220. package/dist/powql.d.ts +394 -51
  221. package/dist/powql.js +1836 -291
  222. package/dist/prisma-compat.d.ts +23 -15
  223. package/dist/prisma-compat.js +508 -373
  224. package/dist/query/aggregates.d.ts +17 -1
  225. package/dist/query/aggregates.js +263 -32
  226. package/dist/query/batched-loader.d.ts +25 -6
  227. package/dist/query/batched-loader.js +101 -21
  228. package/dist/query/builder.d.ts +325 -184
  229. package/dist/query/builder.js +1458 -495
  230. package/dist/query/column-ref.d.ts +61 -0
  231. package/dist/query/column-ref.js +66 -0
  232. package/dist/query/compound-unique.d.ts +21 -0
  233. package/dist/query/compound-unique.js +97 -19
  234. package/dist/query/deferred.d.ts +53 -16
  235. package/dist/query/expr.d.ts +217 -0
  236. package/dist/query/expr.js +848 -0
  237. package/dist/query/filters.d.ts +155 -53
  238. package/dist/query/filters.js +597 -72
  239. package/dist/query/index.d.ts +2 -0
  240. package/dist/query/index.js +4 -0
  241. package/dist/query/json-value.d.ts +43 -0
  242. package/dist/query/json-value.js +54 -0
  243. package/dist/query/normalize-args.d.ts +87 -0
  244. package/dist/query/normalize-args.js +517 -0
  245. package/dist/query/option-surface.d.ts +16 -4
  246. package/dist/query/option-surface.js +56 -13
  247. package/dist/query/relation-names.d.ts +14 -0
  248. package/dist/query/relation-names.js +77 -19
  249. package/dist/query/relations.d.ts +30 -3
  250. package/dist/query/relations.js +220 -67
  251. package/dist/query/row-lock.d.ts +49 -0
  252. package/dist/query/row-lock.js +131 -0
  253. package/dist/query/sql-comment.d.ts +161 -0
  254. package/dist/query/sql-comment.js +374 -0
  255. package/dist/query/stream-idle.d.ts +132 -0
  256. package/dist/query/stream-idle.js +203 -0
  257. package/dist/query/table-client.d.ts +101 -0
  258. package/dist/query/table-client.js +70 -0
  259. package/dist/query/types.d.ts +677 -298
  260. package/dist/query/utils.d.ts +105 -6
  261. package/dist/query/utils.js +414 -18
  262. package/dist/query/warn-registry.d.ts +18 -0
  263. package/dist/query/warn-registry.js +18 -0
  264. package/dist/query/where-compile.d.ts +138 -14
  265. package/dist/query/where-compile.js +508 -31
  266. package/dist/query/where.d.ts +159 -59
  267. package/dist/query/where.js +711 -248
  268. package/dist/query/writes.d.ts +112 -50
  269. package/dist/query/writes.js +1496 -202
  270. package/dist/query-events.d.ts +4 -2
  271. package/dist/query-events.js +3 -1
  272. package/dist/realtime.d.ts +7 -3
  273. package/dist/realtime.js +13 -2
  274. package/dist/schema-builder.d.ts +55 -4
  275. package/dist/schema-builder.js +381 -18
  276. package/dist/schema-metadata.d.ts +21 -1
  277. package/dist/schema-metadata.js +28 -7
  278. package/dist/schema-sql.d.ts +75 -5
  279. package/dist/schema-sql.js +494 -44
  280. package/dist/schema.d.ts +26 -5
  281. package/dist/schema.js +109 -13
  282. package/dist/sql-placeholders.d.ts +56 -0
  283. package/dist/sql-placeholders.js +200 -0
  284. package/dist/sqlite.d.ts +75 -20
  285. package/dist/sqlite.js +597 -99
  286. package/dist/supplied-pool-types.d.ts +48 -0
  287. package/dist/supplied-pool-types.js +247 -0
  288. package/dist/transaction-control.d.ts +51 -0
  289. package/dist/transaction-control.js +311 -0
  290. package/dist/transaction-state.d.ts +342 -0
  291. package/dist/transaction-state.js +696 -0
  292. package/dist/typed-sql.d.ts +37 -3
  293. package/dist/typed-sql.js +100 -8
  294. package/package.json +7 -4
  295. package/skills/turbine-orm/SKILL.md +56 -6
@@ -60,6 +60,7 @@ var __importStar = (this && this.__importStar) || (function () {
60
60
  };
61
61
  })();
62
62
  Object.defineProperty(exports, "__esModule", { value: true });
63
+ exports.MIGRATE_POOLER_EXPLANATION = exports.DEFAULT_LOCK_WAIT_SECONDS = void 0;
63
64
  exports.canonicalCommand = canonicalCommand;
64
65
  exports.knownCommands = knownCommands;
65
66
  exports.flagsForCommand = flagsForCommand;
@@ -81,6 +82,8 @@ exports.scaffoldEnvForUrl = scaffoldEnvForUrl;
81
82
  exports.generatedClientImport = generatedClientImport;
82
83
  exports.initEnvNotice = initEnvNotice;
83
84
  exports.refuseSchemaFilePath = refuseSchemaFilePath;
85
+ exports.overlayCodeFirstTags = overlayCodeFirstTags;
86
+ exports.generatorColumnTypes = generatorColumnTypes;
84
87
  exports.resolveMigrateFromPrismaUrl = resolveMigrateFromPrismaUrl;
85
88
  exports.readPrismaSchemaSource = readPrismaSchemaSource;
86
89
  exports.buildMigrateDeployOptions = buildMigrateDeployOptions;
@@ -105,8 +108,10 @@ const prisma_schema_fingerprint_js_1 = require("../prisma-schema-fingerprint.js"
105
108
  const utils_js_1 = require("../query/utils.js");
106
109
  const schema_js_1 = require("../schema.js");
107
110
  const schema_sql_js_1 = require("../schema-sql.js");
111
+ const code_first_tags_js_1 = require("./code-first-tags.js");
108
112
  const config_js_1 = require("./config.js");
109
113
  const destructive_js_1 = require("./destructive.js");
114
+ const error_text_js_1 = require("./error-text.js");
110
115
  const loader_js_1 = require("./loader.js");
111
116
  const mcp_js_1 = require("./mcp.js");
112
117
  const migrate_js_1 = require("./migrate.js");
@@ -116,6 +121,8 @@ const prisma_resolve_js_1 = require("./prisma-resolve.js");
116
121
  const prisma_schema_js_1 = require("./prisma-schema.js");
117
122
  const studio_js_1 = require("./studio.js");
118
123
  const ui_js_1 = require("./ui.js");
124
+ /** How long a bare `migrate deploy --wait` waits for a concurrent deploy to finish. */
125
+ exports.DEFAULT_LOCK_WAIT_SECONDS = 300;
119
126
  /**
120
127
  * Fail an argument-parse error the way every other CLI failure looks: banner,
121
128
  * red error line, then indented hint lines. The flag validators used to call
@@ -170,6 +177,7 @@ const GLOBAL_FLAGS = [
170
177
  const COMMAND_FLAGS = {
171
178
  init: ['--yes', '--schema-file', '--skip-schema', '--with-schema', '--skip-seed', '--skip-push', '--skip-generate'],
172
179
  generate: [
180
+ '--schema-file',
173
181
  '--zod',
174
182
  '--include-views',
175
183
  '--no-timestamp',
@@ -179,7 +187,7 @@ const COMMAND_FLAGS = {
179
187
  '--allow-empty',
180
188
  ],
181
189
  'migrate-from-prisma': ['--no-db', '--if-db', '--allow-partial', '--no-timestamp'],
182
- push: ['--schema-file', '--dry-run', '--allow-destructive'],
190
+ push: ['--schema-file', '--dry-run', '--exit-code', '--allow-destructive'],
183
191
  migrate: [
184
192
  '--schema-file',
185
193
  '--auto',
@@ -187,8 +195,12 @@ const COMMAND_FLAGS = {
187
195
  '--recipe',
188
196
  '--step',
189
197
  '--dry-run',
198
+ '--exit-code',
199
+ '--wait',
190
200
  '--allow-drift',
191
201
  '--allow-destructive',
202
+ '--allow-pending',
203
+ '--allow-pooler',
192
204
  ],
193
205
  seed: [],
194
206
  status: [],
@@ -366,6 +378,8 @@ function parseArgs(argv = process.argv.slice(2)) {
366
378
  const unknownFlags = [];
367
379
  /** The raw `--step` operand, judged after the loop. Absent means unset. */
368
380
  let stepRaw;
381
+ /** The raw `--wait` operand, judged after the loop: undefined absent, null bare `--wait`. */
382
+ let waitRaw;
369
383
  for (; i < args.length; i++) {
370
384
  const arg = args[i];
371
385
  const next = args[i + 1];
@@ -373,8 +387,15 @@ function parseArgs(argv = process.argv.slice(2)) {
373
387
  // (`--step -1`, `--host -x`) is consumed by its own case and never reaches
374
388
  // this test. A bare `-`-prefixed token that is not a flag reaches it and is
375
389
  // rejected: no command takes a negative-number positional.
376
- if (accepted && arg.startsWith('-') && !accepted.has(arg))
390
+ // `--wait=30` is the one flag whose value attaches with `=`, so it is
391
+ // matched by its name and handled here rather than by a `case` label.
392
+ const attachedWait = arg.startsWith('--wait=');
393
+ if (accepted && arg.startsWith('-') && !accepted.has(attachedWait ? '--wait' : arg))
377
394
  unknownFlags.push(arg);
395
+ if (attachedWait) {
396
+ waitRaw = arg.slice('--wait='.length);
397
+ continue;
398
+ }
378
399
  switch (arg) {
379
400
  case '--url':
380
401
  case '-u':
@@ -418,6 +439,23 @@ function parseArgs(argv = process.argv.slice(2)) {
418
439
  case '--dry-run':
419
440
  result.dryRun = true;
420
441
  break;
442
+ case '--exit-code':
443
+ result.exitCode = true;
444
+ break;
445
+ case '--allow-pending':
446
+ result.allowPending = true;
447
+ break;
448
+ case '--wait':
449
+ // `--wait 30` reads the next token only when it is all digits: `deploy`
450
+ // takes no positional, so a bare number after the flag is its value.
451
+ if (next !== undefined && /^\d+$/.test(next)) {
452
+ waitRaw = next;
453
+ i++;
454
+ }
455
+ else {
456
+ waitRaw = null;
457
+ }
458
+ break;
421
459
  case '--auto':
422
460
  result.auto = true;
423
461
  break;
@@ -500,8 +538,8 @@ function parseArgs(argv = process.argv.slice(2)) {
500
538
  break;
501
539
  case '--import-ext':
502
540
  case '--import-extension':
503
- if (next !== 'js' && next !== 'none' && next !== 'auto') {
504
- failArg(`${(0, ui_js_1.cyan)('--import-ext')} requires one of: js, none, auto ${(0, ui_js_1.dim)(`(got ${next ?? 'nothing'})`)}`, 'Example: npx turbine generate --import-ext none');
541
+ if (next !== 'js' && next !== 'ts' && next !== 'none' && next !== 'auto') {
542
+ failArg(`${(0, ui_js_1.cyan)('--import-ext')} requires one of: js, ts, none, auto ${(0, ui_js_1.dim)(`(got ${next ?? 'nothing'})`)}`, 'Example: npx turbine generate --import-ext ts');
505
543
  }
506
544
  result.importExtension = next;
507
545
  i++;
@@ -592,6 +630,18 @@ function parseArgs(argv = process.argv.slice(2)) {
592
630
  }
593
631
  result.step = parsed;
594
632
  }
633
+ if (waitRaw !== undefined && accepted !== undefined) {
634
+ if (waitRaw === null) {
635
+ result.wait = exports.DEFAULT_LOCK_WAIT_SECONDS;
636
+ }
637
+ else {
638
+ const parsed = /^\d+$/.test(waitRaw.trim()) ? Number.parseInt(waitRaw, 10) : Number.NaN;
639
+ if (!Number.isSafeInteger(parsed) || parsed < 1) {
640
+ failArg(`${(0, ui_js_1.cyan)('--wait')} expects a positive whole number of seconds, got ${(0, ui_js_1.bold)(waitRaw === '' ? '(nothing)' : `"${waitRaw}"`)}.`, `Bare ${'--wait'} waits ${exports.DEFAULT_LOCK_WAIT_SECONDS} seconds. To not wait at all, leave the flag off.`, 'Example: npx turbine migrate deploy --wait=120');
641
+ }
642
+ result.wait = parsed;
643
+ }
644
+ }
595
645
  return result;
596
646
  }
597
647
  // ---------------------------------------------------------------------------
@@ -790,6 +840,11 @@ function loadDotEnvForCli(deps = {}) {
790
840
  if (!fileExists(envPath)) {
791
841
  return { fileExists: false, loaded: false, unsupported: false, databaseUrlProvenance: shellOrNone };
792
842
  }
843
+ // An exported-but-empty `DATABASE_URL=` means "not configured", and the loader
844
+ // never overrides a variable that is already set, so it would otherwise block
845
+ // the value in the `.env` for good.
846
+ if (env.DATABASE_URL === '')
847
+ delete env.DATABASE_URL;
793
848
  const loader = deps.loadEnvFile !== undefined
794
849
  ? deps.loadEnvFile
795
850
  : typeof process.loadEnvFile === 'function'
@@ -807,7 +862,7 @@ function loadDotEnvForCli(deps = {}) {
807
862
  loaded: false,
808
863
  unsupported: false,
809
864
  databaseUrlProvenance: shellOrNone,
810
- loadError: err instanceof Error ? err.message : String(err),
865
+ loadError: (0, error_text_js_1.errorText)(err),
811
866
  };
812
867
  }
813
868
  // `.env`-sourced only if DATABASE_URL was absent before and present after.
@@ -1206,42 +1261,94 @@ DATABASE_URL=postgres://user:password@localhost:5432/database
1206
1261
  * @internal exported for tests.
1207
1262
  */
1208
1263
  function scaffoldEnvForUrl(url) {
1264
+ // The plan is read from disk and the writes come after it, so a file can
1265
+ // appear in between. Creation is exclusive (see createFileExclusive); when it
1266
+ // loses, plan again from what is there now. The cap only ends the loop for a
1267
+ // name that cannot be read but cannot be created either, a dangling symlink.
1268
+ for (let attempt = 0; attempt < 3; attempt++) {
1269
+ const plan = applyEnvScaffold(url);
1270
+ if (plan)
1271
+ return plan;
1272
+ }
1273
+ throw new errors_js_1.ValidationError('Could not write .env, .env.example and .gitignore: one of those names is taken by something that cannot be read ' +
1274
+ '(a dangling symlink?). Remove it and run the command again.');
1275
+ }
1276
+ /** One planning pass plus its writes; undefined when a create found its name already taken. */
1277
+ function applyEnvScaffold(url) {
1209
1278
  const envPath = '.env';
1210
1279
  const envExamplePath = '.env.example';
1211
1280
  const gitignorePath = '.gitignore';
1212
- const envExists = (0, node_fs_1.existsSync)(envPath);
1281
+ const env = readIfPresent(envPath);
1282
+ const gitignore = readIfPresent(gitignorePath);
1213
1283
  const plan = planEnvScaffold({
1214
- envExists,
1215
- envHasDatabaseUrl: envExists ? /^\s*(export\s+)?DATABASE_URL\s*=/m.test((0, node_fs_1.readFileSync)(envPath, 'utf-8')) : false,
1216
- envExampleExists: (0, node_fs_1.existsSync)(envExamplePath),
1217
- gitignoreExists: (0, node_fs_1.existsSync)(gitignorePath),
1218
- gitignoreIgnoresEnv: (0, node_fs_1.existsSync)(gitignorePath) ? gitignoreIgnoresEnv((0, node_fs_1.readFileSync)(gitignorePath, 'utf-8')) : false,
1284
+ envExists: env !== undefined,
1285
+ envHasDatabaseUrl: env !== undefined && /^\s*(export\s+)?DATABASE_URL\s*=/m.test(env),
1286
+ envExampleExists: readIfPresent(envExamplePath) !== undefined,
1287
+ gitignoreExists: gitignore !== undefined,
1288
+ gitignoreIgnoresEnv: gitignore !== undefined && gitignoreIgnoresEnv(gitignore),
1219
1289
  });
1220
1290
  // .gitignore FIRST: the ignore rule has to exist before the file holding the
1221
1291
  // password does, or a `git add -A` in between commits it.
1222
- if (plan.gitignore === 'created')
1223
- (0, node_fs_1.writeFileSync)(gitignorePath, '# Local environment (secrets)\n.env\n', 'utf-8');
1224
- else if (plan.gitignore === 'appended')
1292
+ if (plan.gitignore === 'created') {
1293
+ if (!createFileExclusive(gitignorePath, '# Local environment (secrets)\n.env\n'))
1294
+ return undefined;
1295
+ }
1296
+ else if (plan.gitignore === 'appended') {
1225
1297
  (0, node_fs_1.appendFileSync)(gitignorePath, '\n# Local environment (secrets)\n.env\n');
1298
+ }
1299
+ // 0600 on CREATION only. The whole point of this scaffold is moving a live
1300
+ // password out of a committed file, and the default 0644 would leave it
1301
+ // world-readable to every account on a shared box or CI runner. `mode` is
1302
+ // applied by the OS only when the open() actually creates the file, so a
1303
+ // `.env` that exists keeps whatever mode its owner chose.
1226
1304
  if (plan.env === 'created') {
1227
- // 0600 on CREATION only. The whole point of this scaffold is moving a live
1228
- // password out of a committed file, and the default 0644 would leave it
1229
- // world-readable to every account on a shared box or CI runner. `mode` is
1230
- // applied by the OS only when the open() actually creates the file, so if
1231
- // one appeared since the existsSync above we write into it without widening
1232
- // or narrowing whatever mode its owner chose.
1233
- (0, node_fs_1.writeFileSync)(envPath, `# Turbine database connection. Never commit this file.\nDATABASE_URL=${url}\n`, {
1234
- encoding: 'utf-8',
1235
- mode: 0o600,
1236
- });
1305
+ const body = `# Turbine database connection. Never commit this file.\nDATABASE_URL=${url}\n`;
1306
+ if (!createFileExclusive(envPath, body, 0o600))
1307
+ return undefined;
1237
1308
  }
1238
1309
  else if (plan.env === 'appended') {
1239
- (0, node_fs_1.appendFileSync)(envPath, `\n# Added by turbine init\nDATABASE_URL=${url}\n`);
1310
+ (0, node_fs_1.appendFileSync)(envPath, `\n# Added by turbine init\nDATABASE_URL=${url}\n`, { mode: 0o600 });
1240
1311
  }
1241
- if (plan.envExample === 'created')
1242
- (0, node_fs_1.writeFileSync)(envExamplePath, ENV_EXAMPLE_TEMPLATE, 'utf-8');
1312
+ if (plan.envExample === 'created' && !createFileExclusive(envExamplePath, ENV_EXAMPLE_TEMPLATE))
1313
+ return undefined;
1243
1314
  return plan;
1244
1315
  }
1316
+ /**
1317
+ * A file's text, or undefined when there is no such file. One read in place of
1318
+ * an existence check followed by a read, so there is no window between the two.
1319
+ * A directory under that name counts as present: the write step then fails on
1320
+ * it by name, which is the error that was always raised.
1321
+ */
1322
+ function readIfPresent(path) {
1323
+ try {
1324
+ return (0, node_fs_1.readFileSync)(path, 'utf-8');
1325
+ }
1326
+ catch (err) {
1327
+ const code = err.code;
1328
+ if (code === 'ENOENT')
1329
+ return undefined;
1330
+ if (code === 'EISDIR')
1331
+ return '';
1332
+ throw err;
1333
+ }
1334
+ }
1335
+ /**
1336
+ * Create `path` only if nothing is there; false when the name is taken.
1337
+ * `wx` is O_CREAT | O_EXCL: the kernel refuses (EEXIST) any existing name,
1338
+ * a symlink included, so a file that appeared after the plan was made is never
1339
+ * truncated and a link planted at the name is never written through.
1340
+ */
1341
+ function createFileExclusive(path, body, mode) {
1342
+ try {
1343
+ (0, node_fs_1.writeFileSync)(path, body, { encoding: 'utf-8', flag: 'wx', ...(mode === undefined ? {} : { mode }) });
1344
+ return true;
1345
+ }
1346
+ catch (err) {
1347
+ if (err.code === 'EEXIST')
1348
+ return false;
1349
+ throw err;
1350
+ }
1351
+ }
1245
1352
  /** Say exactly what was written where, so nobody has to guess where the password went. */
1246
1353
  function reportEnvScaffold(plan, url) {
1247
1354
  (0, ui_js_1.warn)(`The connection string passed to ${(0, ui_js_1.cyan)('--url')} contains a password.`);
@@ -1277,8 +1384,8 @@ function ensureInitScaffoldDirs(config) {
1277
1384
  (0, ui_js_1.success)(`Created ${(0, ui_js_1.cyan)(`${config.out}/`)}`);
1278
1385
  }
1279
1386
  const gitignorePath = '.gitignore';
1280
- if ((0, node_fs_1.existsSync)(gitignorePath)) {
1281
- const gitignoreContent = (0, node_fs_1.readFileSync)(gitignorePath, 'utf-8');
1387
+ const gitignoreContent = readIfPresent(gitignorePath);
1388
+ if (gitignoreContent !== undefined) {
1282
1389
  const additions = [];
1283
1390
  if (!gitignoreContent.includes('generated/turbine'))
1284
1391
  additions.push('generated/turbine/');
@@ -1384,8 +1491,19 @@ async function runInitGenerate(config, url) {
1384
1491
  });
1385
1492
  const tableNames = Object.keys(schema.tables);
1386
1493
  spinner.succeed(`Found ${(0, ui_js_1.bold)(String(tableNames.length))} tables`);
1494
+ // The schema file init just pushed is where `pii` / `updatedAt` live.
1495
+ const appliedTags = await overlayCodeFirstTags(schema, config);
1496
+ if (appliedTags && describeAppliedTags(appliedTags)) {
1497
+ (0, ui_js_1.info)(`Applied column tags from ${(0, ui_js_1.cyan)(config.schemaFile)}: ${describeAppliedTags(appliedTags)}`);
1498
+ }
1387
1499
  const genSpinner = new ui_js_1.Spinner('Generating TypeScript client').start();
1388
- const result = (0, generate_js_1.generate)({ schema, outDir: config.out, connectionString: url });
1500
+ const result = (0, generate_js_1.generate)({
1501
+ schema,
1502
+ outDir: config.out,
1503
+ connectionString: url,
1504
+ importExtension: config.importExtension,
1505
+ columnTypes: await generatorColumnTypesFor(appliedTags, config, schema, url),
1506
+ });
1389
1507
  genSpinner.succeed(`Generated ${(0, ui_js_1.bold)(String(result.files.length))} files to ${(0, ui_js_1.cyan)(`${config.out}/`)}`);
1390
1508
  // The accessor name the generator emits is snakeToCamel of the table name
1391
1509
  // (generate.ts, generateIndex), so this is the spelling that autocompletes.
@@ -1394,8 +1512,7 @@ async function runInitGenerate(config, url) {
1394
1512
  }
1395
1513
  catch (err) {
1396
1514
  spinner.fail('Could not generate client');
1397
- if (err instanceof Error)
1398
- (0, ui_js_1.errorLine)((0, ui_js_1.dim)((0, ui_js_1.redactUrl)(err.message)));
1515
+ (0, ui_js_1.errorLine)((0, ui_js_1.dim)((0, ui_js_1.redactUrl)((0, error_text_js_1.errorText)(err))));
1399
1516
  (0, ui_js_1.info)(`Run generation later with: ${(0, ui_js_1.cyan)('npx turbine generate')}`);
1400
1517
  return { tableCount: null };
1401
1518
  }
@@ -1409,13 +1526,14 @@ async function runInitSeed(config) {
1409
1526
  }
1410
1527
  const spinner = new ui_js_1.Spinner('Running seed file').start();
1411
1528
  try {
1412
- await runSeedPlan(getSeedExecutionPlan(seedFile), config);
1529
+ const outcome = await runSeedPlan(getSeedExecutionPlan(seedFile), config);
1413
1530
  spinner.succeed('Seed completed');
1531
+ if (!outcome.confirmed)
1532
+ (0, ui_js_1.warn)(UNCONFIRMED_SEED_NOTICE);
1414
1533
  }
1415
1534
  catch (err) {
1416
1535
  spinner.fail('Seed failed');
1417
- if (err instanceof Error)
1418
- console.log(` ${(0, ui_js_1.dim)((0, ui_js_1.redactUrl)(err.message))}`);
1536
+ console.log(` ${(0, ui_js_1.dim)((0, ui_js_1.redactUrl)((0, error_text_js_1.errorText)(err)))}`);
1419
1537
  }
1420
1538
  }
1421
1539
  /** Human label for a step, used in prompts and skip messages. */
@@ -1726,7 +1844,7 @@ async function cmdInit(args, config) {
1726
1844
  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.')}`);
1727
1845
  }
1728
1846
  else {
1729
- const accessor = generated.sampleAccessor ?? 'users';
1847
+ const accessor = shown(generated.sampleAccessor ?? 'users');
1730
1848
  console.log(` ${(0, ui_js_1.dim)('1.')} Import the generated client:`);
1731
1849
  console.log(` ${(0, ui_js_1.cyan)(generatedClientImport(config))}`);
1732
1850
  (0, ui_js_1.newline)();
@@ -1775,7 +1893,7 @@ function refuseSchemaFilePath(config, options = {}) {
1775
1893
  (0, ui_js_1.errorLine)();
1776
1894
  (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)(':')}`);
1777
1895
  (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")}`);
1778
- (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`)')}`);
1896
+ (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 (read by push and generate)')}`);
1779
1897
  if (options.escapeHatch) {
1780
1898
  (0, ui_js_1.errorLine)();
1781
1899
  (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.')}`);
@@ -1784,6 +1902,113 @@ function refuseSchemaFilePath(config, options = {}) {
1784
1902
  process.exit(1);
1785
1903
  }
1786
1904
  // ---------------------------------------------------------------------------
1905
+ // Code-first column tags
1906
+ // ---------------------------------------------------------------------------
1907
+ /**
1908
+ * Overlay the column tags the defineSchema() file declares (`pii`, `updatedAt`,
1909
+ * `maxLength`) onto an introspected schema, before it is generated.
1910
+ *
1911
+ * Introspection reads the database, and the database has no column for these
1912
+ * flags, so a client generated from introspection alone returns PII by default,
1913
+ * never stamps `updatedAt`, and leaves Studio and the MCP server with nothing to
1914
+ * redact. The schema file is the only place the tags exist.
1915
+ *
1916
+ * Loaded when the file was named (`--schema-file` / config `schemaFile`) or sits
1917
+ * at the default path. A named file that is missing, or any file that cannot be
1918
+ * loaded, fails the command: carrying on would generate the unredacted client
1919
+ * this exists to prevent. Only a missing DEFAULT file is skipped, which is a
1920
+ * database-first project with no schema file at all.
1921
+ *
1922
+ * Returns what was applied, or `null` when there was no schema file to read.
1923
+ *
1924
+ * @internal exported for tests.
1925
+ */
1926
+ async function overlayCodeFirstTags(schema, config) {
1927
+ if (config.schemaFileConfigured !== true && !(0, node_fs_1.existsSync)((0, node_path_1.resolve)(config.schemaFile)))
1928
+ return null;
1929
+ const def = await loadSchemaFile(config.schemaFile);
1930
+ const filters = {
1931
+ include: config.include.length ? config.include : undefined,
1932
+ exclude: config.exclude.length ? config.exclude : undefined,
1933
+ };
1934
+ return (0, code_first_tags_js_1.applyCodeFirstColumnTags)(schema, (0, code_first_tags_js_1.extractCodeFirstColumnTags)(def), {
1935
+ source: config.schemaFile,
1936
+ // A table the caller filtered out of the client has no client to tag.
1937
+ inScope: (table) => (0, introspect_js_1.applyTableFilters)([table], filters).length > 0,
1938
+ });
1939
+ }
1940
+ /** "2 pii, 1 updatedAt" for the line that reports an {@link overlayCodeFirstTags} result. */
1941
+ function describeAppliedTags(applied) {
1942
+ const parts = [];
1943
+ if (applied.pii > 0)
1944
+ parts.push(`${applied.pii} pii`);
1945
+ if (applied.updatedAt > 0)
1946
+ parts.push(`${applied.updatedAt} updatedAt`);
1947
+ if (applied.maxLength > 0)
1948
+ parts.push(`${applied.maxLength} maxLength`);
1949
+ const tsTypes = Object.keys(applied.columnTypes).length;
1950
+ if (tsTypes > 0)
1951
+ parts.push(`${tsTypes} tsType`);
1952
+ return parts.join(', ');
1953
+ }
1954
+ /**
1955
+ * The generator's `columnTypes`: the schema file's `tsType` declarations, then
1956
+ * the config file's `columnTypes`, which wins on the same key. A config key
1957
+ * whose table the run's include / exclude filters leave out is dropped, since
1958
+ * that table has no generated type to override; every other key goes to the
1959
+ * generator, which rejects one naming no table or column.
1960
+ *
1961
+ * An `include` list leaves out every table it does not name, so a misspelt
1962
+ * table looks exactly like a real table that was left out. `databaseTables`
1963
+ * (every table the schema holds, before filtering) tells them apart: only a
1964
+ * table the database has is dropped, and a name it does not have goes on to the
1965
+ * generator, which names the closest match. Without it every key outside the
1966
+ * filters is dropped.
1967
+ *
1968
+ * @internal exported for tests.
1969
+ */
1970
+ function generatorColumnTypes(applied, config, schema, databaseTables) {
1971
+ const filters = {
1972
+ include: config.include.length ? config.include : undefined,
1973
+ exclude: config.exclude.length ? config.exclude : undefined,
1974
+ };
1975
+ const merged = { ...applied?.columnTypes };
1976
+ for (const [key, type] of Object.entries(config.columnTypes ?? {})) {
1977
+ const table = key.slice(0, key.lastIndexOf('.'));
1978
+ const leftOut = !Object.hasOwn(schema.tables, table) && (0, introspect_js_1.applyTableFilters)([table], filters).length === 0;
1979
+ if (leftOut && (databaseTables === undefined || databaseTables.has(table)))
1980
+ continue;
1981
+ merged[key] = type;
1982
+ }
1983
+ return Object.keys(merged).length > 0 ? merged : undefined;
1984
+ }
1985
+ /** The names of every table and view in `schema`, before any include / exclude filter. */
1986
+ async function listDatabaseTables(url, schema) {
1987
+ const { default: pg } = await Promise.resolve().then(() => __importStar(require('pg')));
1988
+ const client = new pg.Client({ connectionString: url });
1989
+ (0, connection_guard_js_1.guardConnection)(client);
1990
+ await client.connect();
1991
+ try {
1992
+ const res = await client.query('SELECT table_name FROM information_schema.tables WHERE table_schema = $1', [schema]);
1993
+ return new Set(res.rows.map((r) => r.table_name));
1994
+ }
1995
+ finally {
1996
+ await client.end();
1997
+ }
1998
+ }
1999
+ /**
2000
+ * {@link generatorColumnTypes} for a run against a database. The catalog is read
2001
+ * only when an `include` list is set and a config key names a table outside the
2002
+ * generated set, which is the one case where a typo and a table left out look
2003
+ * alike.
2004
+ */
2005
+ async function generatorColumnTypesFor(applied, config, schema, url, namespace = config.schema) {
2006
+ const keys = Object.keys(config.columnTypes ?? {});
2007
+ const outsideInclude = config.include.length > 0 && keys.some((key) => !Object.hasOwn(schema.tables, key.slice(0, key.lastIndexOf('.'))));
2008
+ const databaseTables = outsideInclude && url ? await listDatabaseTables(url, namespace) : undefined;
2009
+ return generatorColumnTypes(applied, config, schema, databaseTables);
2010
+ }
2011
+ // ---------------------------------------------------------------------------
1787
2012
  // Command: generate (pull)
1788
2013
  // ---------------------------------------------------------------------------
1789
2014
  async function cmdGenerate(args, config) {
@@ -1822,6 +2047,10 @@ async function cmdGenerate(args, config) {
1822
2047
  for (const t of skippedInternalTables) {
1823
2048
  console.log(` ${(0, ui_js_1.dim)(`${ui_js_1.symbols.teeEnd} skipped internal table ${t} (add it to include to keep it)`)}`);
1824
2049
  }
2050
+ const appliedTags = await overlayCodeFirstTags(schema, config);
2051
+ if (appliedTags && describeAppliedTags(appliedTags)) {
2052
+ console.log(` ${(0, ui_js_1.dim)(`${ui_js_1.symbols.teeEnd} applied column tags from ${config.schemaFile}: ${describeAppliedTags(appliedTags)}`)}`);
2053
+ }
1825
2054
  // Guard: zero tables means the generated client would be empty. That is almost
1826
2055
  // always a misconfiguration (wrong `schema`, an include/exclude that filtered
1827
2056
  // everything, or a database with no tables yet) rather than intent. Fail loudly
@@ -1846,12 +2075,12 @@ async function cmdGenerate(args, config) {
1846
2075
  for (const tbl of Object.values(schema.tables)) {
1847
2076
  const relCount = Object.keys(tbl.relations).length;
1848
2077
  const pk = tbl.primaryKey.join(', ') || '(none)';
1849
- console.log(` ${ui_js_1.symbols.tee} ${(0, ui_js_1.bold)(tbl.name)} ${(0, ui_js_1.dim)(`${tbl.columns.length} cols, PK: ${pk}`)}${relCount > 0 ? (0, ui_js_1.dim)(`, ${relCount} rels`) : ''}`);
2078
+ console.log(` ${ui_js_1.symbols.tee} ${(0, ui_js_1.bold)(shown(tbl.name))} ${(0, ui_js_1.dim)(`${tbl.columns.length} cols, PK: ${shown(pk)}`)}${relCount > 0 ? (0, ui_js_1.dim)(`, ${relCount} rels`) : ''}`);
1850
2079
  }
1851
2080
  (0, ui_js_1.newline)();
1852
2081
  }
1853
2082
  if (Object.keys(schema.enums).length > 0) {
1854
- (0, ui_js_1.info)(`Enums: ${Object.keys(schema.enums).join(', ')}`);
2083
+ (0, ui_js_1.info)(`Enums: ${Object.keys(schema.enums).map(shown).join(', ')}`);
1855
2084
  }
1856
2085
  // Generate
1857
2086
  const genSpinner = new ui_js_1.Spinner('Generating TypeScript client').start();
@@ -1863,6 +2092,7 @@ async function cmdGenerate(args, config) {
1863
2092
  noTimestamp: args.noTimestamp,
1864
2093
  importExtension: config.importExtension,
1865
2094
  keepColumnNames: config.keepColumnNames,
2095
+ columnTypes: await generatorColumnTypesFor(appliedTags, config, schema, url),
1866
2096
  });
1867
2097
  genSpinner.succeed(`Generated ${(0, ui_js_1.bold)(String(result.files.length))} files in ${(0, ui_js_1.elapsed)(startTime)}`);
1868
2098
  // List files
@@ -1879,7 +2109,12 @@ async function cmdGenerate(args, config) {
1879
2109
  console.log(` ${(0, ui_js_1.dim)('const db = turbine({ connectionString: process.env.DATABASE_URL });')}`);
1880
2110
  // Name a table the client just emitted rather than a hardcoded `users`.
1881
2111
  const sampleAccessor = (0, schema_js_1.snakeToCamel)(tableNames[0] ?? 'users');
1882
- console.log(` ${(0, ui_js_1.dim)(`const row = await db.${sampleAccessor}.findFirst();`)}`);
2112
+ console.log(` ${(0, ui_js_1.dim)(`const row = await db.${shown(sampleAccessor)}.findFirst();`)}`);
2113
+ if (config.importExtension === 'ts') {
2114
+ (0, ui_js_1.newline)();
2115
+ console.log(` ${(0, ui_js_1.dim)('Run it with Node type stripping:')} ${(0, ui_js_1.cyan)('node --env-file=.env app.ts')} ${(0, ui_js_1.dim)('(Node 22.18+ or 23.6+; earlier 22.x needs --experimental-strip-types)')}`);
2116
+ console.log(` ${(0, ui_js_1.dim)('A type checker needs')} ${(0, ui_js_1.cyan)('allowImportingTsExtensions')} ${(0, ui_js_1.dim)('(or')} ${(0, ui_js_1.cyan)('rewriteRelativeImportExtensions')}${(0, ui_js_1.dim)(') in tsconfig.json.')}`);
2117
+ }
1883
2118
  (0, ui_js_1.newline)();
1884
2119
  }
1885
2120
  /**
@@ -1917,8 +2152,15 @@ function resolveMigrateFromPrismaUrl(configUrl, ast, env) {
1917
2152
  * concatenation, so each file is introduced by a comment naming it.
1918
2153
  */
1919
2154
  function readPrismaSchemaSource(path) {
1920
- if (!(0, node_fs_1.statSync)(path).isDirectory())
2155
+ // Read first, ask what it is only if that fails: a stat followed by a read
2156
+ // can disagree with itself when the path is replaced in between.
2157
+ try {
1921
2158
  return (0, node_fs_1.readFileSync)(path, 'utf-8');
2159
+ }
2160
+ catch (err) {
2161
+ if (err.code !== 'EISDIR')
2162
+ throw err;
2163
+ }
1922
2164
  const files = (0, node_fs_1.readdirSync)(path)
1923
2165
  .filter((f) => f.endsWith('.prisma'))
1924
2166
  .sort();
@@ -1953,7 +2195,7 @@ async function cmdMigrateFromPrisma(args, config) {
1953
2195
  catch (err) {
1954
2196
  (0, ui_js_1.newline)();
1955
2197
  (0, ui_js_1.error)(`Could not read ${(0, ui_js_1.cyan)(prismaPath)}`);
1956
- (0, ui_js_1.errorLine)(`${(0, ui_js_1.red)(err instanceof Error ? err.message : String(err))}`);
2198
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.red)((0, error_text_js_1.errorText)(err))}`);
1957
2199
  (0, ui_js_1.errorLine)();
1958
2200
  process.exit(1);
1959
2201
  }
@@ -2076,6 +2318,8 @@ async function cmdMigrateFromPrisma(args, config) {
2076
2318
  noTimestamp: args.noTimestamp,
2077
2319
  importExtension: config.importExtension,
2078
2320
  keepColumnNames: config.keepColumnNames,
2321
+ // No schema file is read here, so only the config file's overrides apply.
2322
+ columnTypes: await generatorColumnTypesFor(null, config, schemaMeta, url, 'public'),
2079
2323
  });
2080
2324
  for (const file of gen.files) {
2081
2325
  console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.teeEnd)} ${(0, ui_js_1.cyan)((0, node_path_1.join)(outDir, file))} ${(0, ui_js_1.dim)('(client)')}`);
@@ -2108,9 +2352,26 @@ async function cmdMigrateFromPrisma(args, config) {
2108
2352
  // ---------------------------------------------------------------------------
2109
2353
  // Command: push
2110
2354
  // ---------------------------------------------------------------------------
2355
+ /**
2356
+ * Print what a diff did NOT compare, under an "in sync" style message.
2357
+ *
2358
+ * "Already in sync" only means the compared surface matches. The rest is listed
2359
+ * so the sentence is not read as a guarantee about the whole database.
2360
+ */
2361
+ function printNotCompared(items) {
2362
+ if (!items || items.length === 0)
2363
+ return;
2364
+ console.log(` ${(0, ui_js_1.dim)('Not compared, so this says nothing about:')}`);
2365
+ for (const item of items)
2366
+ console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.dot)} ${(0, ui_js_1.dim)(item)}`);
2367
+ (0, ui_js_1.newline)();
2368
+ }
2111
2369
  async function cmdPush(args, config) {
2112
2370
  (0, ui_js_1.banner)();
2113
2371
  refuseSchemaFilePath(config);
2372
+ if (args.exitCode && !args.dryRun) {
2373
+ failArg(`${(0, ui_js_1.cyan)('--exit-code')} needs ${(0, ui_js_1.cyan)('--dry-run')} on push.`, 'It reports whether the database differs from the schema file (exit 2) without changing it.', 'Example: npx turbine push --dry-run --exit-code');
2374
+ }
2114
2375
  const url = requireUrl(config);
2115
2376
  (0, ui_js_1.label)('Database', (0, ui_js_1.redactUrl)(url));
2116
2377
  (0, ui_js_1.label)('Schema file', config.schemaFile);
@@ -2122,8 +2383,32 @@ async function cmdPush(args, config) {
2122
2383
  const diffSpinner = new ui_js_1.Spinner('Computing schema diff').start();
2123
2384
  const diff = await (0, schema_sql_js_1.schemaDiff)(schemaDef, url, { schema: config.schema });
2124
2385
  if (diff.statements.length === 0 && diff.drop.length === 0) {
2125
- diffSpinner.succeed('Database is already in sync');
2386
+ // "In sync" is a claim about everything the diff looked at, so it is made
2387
+ // only when the diff also has nothing to say. A difference it cannot apply
2388
+ // (an undeclared index, a reordered enum) is reported as exactly that.
2389
+ const advisories = diff.warnings ?? [];
2390
+ if (advisories.length === 0) {
2391
+ diffSpinner.succeed('Database is already in sync');
2392
+ }
2393
+ else {
2394
+ diffSpinner.succeed('Nothing to apply, but the database is not fully in sync');
2395
+ }
2126
2396
  (0, ui_js_1.newline)();
2397
+ for (const w of advisories)
2398
+ (0, ui_js_1.warn)(w);
2399
+ if (advisories.length > 0)
2400
+ (0, ui_js_1.newline)();
2401
+ printNotCompared(diff.notCompared);
2402
+ // A warning is a difference the diff found and cannot apply (a unique index
2403
+ // that is not unique, a removed enum label), so a gate fails on it. What the
2404
+ // diff only discloses as "not compared" is not a difference it found, and a
2405
+ // CHECK expression that merely reads differently is a warning it cannot
2406
+ // confirm (driftWarnings leaves it out).
2407
+ if (args.exitCode && (0, schema_sql_js_1.driftWarnings)(diff).length > 0) {
2408
+ (0, ui_js_1.warn)(`The database differs from the schema file ${(0, ui_js_1.dim)('(--exit-code: exiting 2)')}.`);
2409
+ (0, ui_js_1.newline)();
2410
+ process.exit(2);
2411
+ }
2127
2412
  return;
2128
2413
  }
2129
2414
  diffSpinner.succeed('Found changes');
@@ -2180,6 +2465,11 @@ async function cmdPush(args, config) {
2180
2465
  if (args.dryRun) {
2181
2466
  (0, ui_js_1.info)('Dry run, no changes applied.');
2182
2467
  (0, ui_js_1.newline)();
2468
+ if (args.exitCode) {
2469
+ (0, ui_js_1.warn)(`The database differs from the schema file ${(0, ui_js_1.dim)('(--exit-code: exiting 2)')}.`);
2470
+ (0, ui_js_1.newline)();
2471
+ process.exit(2);
2472
+ }
2183
2473
  return;
2184
2474
  }
2185
2475
  if (args.allowDestructive) {
@@ -2253,8 +2543,12 @@ async function cmdMigrate(args, config) {
2253
2543
  console.log(` ${(0, ui_js_1.cyan)('--recipe <name>')} Scaffold a sanctioned migration pattern`);
2254
2544
  console.log(` ${(0, ui_js_1.cyan)('--step, -n')} Number of migrations to apply/rollback`);
2255
2545
  console.log(` ${(0, ui_js_1.cyan)('--dry-run')} Show SQL without executing`);
2546
+ console.log(` ${(0, ui_js_1.cyan)('--exit-code')} Exit 2 when ${(0, ui_js_1.cyan)('status')} finds pending, modified, missing or misnamed migrations`);
2547
+ console.log(` ${(0, ui_js_1.cyan)('--wait')}${(0, ui_js_1.dim)('[=<seconds>]')} Make ${(0, ui_js_1.cyan)('deploy')} wait for a concurrent deploy's lock`);
2548
+ console.log(` ${(0, ui_js_1.cyan)('--allow-pending')} Let ${(0, ui_js_1.cyan)('--auto')} / ${(0, ui_js_1.cyan)('--from-diff')} run with migrations pending`);
2256
2549
  console.log(` ${(0, ui_js_1.cyan)('--allow-destructive')} Run data-destroying statements without prompting`);
2257
2550
  console.log(` ${(0, ui_js_1.cyan)('--allow-drift')} Re-baseline drifted checksums on ${(0, ui_js_1.cyan)('up')} / ${(0, ui_js_1.cyan)('deploy')} / ${(0, ui_js_1.cyan)('down')} ${(0, ui_js_1.dim)('(advanced)')}`);
2551
+ console.log(` ${(0, ui_js_1.cyan)('--allow-pooler')} Run ${(0, ui_js_1.cyan)('up')} / ${(0, ui_js_1.cyan)('deploy')} / ${(0, ui_js_1.cyan)('down')} against a transaction pooler anyway ${(0, ui_js_1.dim)('(refused by default: the migration lock is session-level and a pooler can release it on another backend)')}`);
2258
2552
  (0, ui_js_1.newline)();
2259
2553
  console.log(` ${(0, ui_js_1.bold)('Recipes')} ${(0, ui_js_1.dim)('(--recipe):')}`);
2260
2554
  for (const [key, recipe] of Object.entries(migrate_js_1.MIGRATION_RECIPES)) {
@@ -2272,6 +2566,23 @@ async function cmdMigrate(args, config) {
2272
2566
  (0, ui_js_1.newline)();
2273
2567
  return;
2274
2568
  }
2569
+ // Flags that change what a subcommand does are refused where they do nothing,
2570
+ // rather than accepted and ignored: a CI gate that silently never gates is
2571
+ // worse than one that will not start.
2572
+ if (args.exitCode && sub !== 'status' && sub !== 'list') {
2573
+ failArg(`${(0, ui_js_1.cyan)('--exit-code')} only applies to ${(0, ui_js_1.cyan)('migrate status')} (and ${(0, ui_js_1.cyan)('push --dry-run')}).`, 'Example: npx turbine migrate status --exit-code');
2574
+ }
2575
+ if (args.wait !== undefined && sub !== 'deploy') {
2576
+ failArg(`${(0, ui_js_1.cyan)('--wait')} only applies to ${(0, ui_js_1.cyan)('migrate deploy')}.`, 'Example: npx turbine migrate deploy --wait=120');
2577
+ }
2578
+ // The commands that take the migration lock, before the banner and before any
2579
+ // connection. A dry run takes no lock, so it may use any endpoint.
2580
+ if ((sub === 'up' || sub === 'deploy' || sub === 'down') && args.dryRun !== true && config.url) {
2581
+ refusePoolerConnection(config.url, args, {
2582
+ command: `turbine migrate ${sub}`,
2583
+ explanation: exports.MIGRATE_POOLER_EXPLANATION,
2584
+ });
2585
+ }
2275
2586
  switch (sub) {
2276
2587
  case 'create':
2277
2588
  await cmdMigrateCreate(args, config);
@@ -2295,6 +2606,58 @@ async function cmdMigrate(args, config) {
2295
2606
  process.exit(1);
2296
2607
  }
2297
2608
  }
2609
+ /**
2610
+ * Stop `migrate create --auto` / `--from-diff` while migrations are pending.
2611
+ *
2612
+ * Both diff the schema file against the live database, and a pending migration
2613
+ * is a change the file already describes and the database does not have yet. The
2614
+ * new migration would repeat it and fail on the first database that runs the
2615
+ * pending one first. See {@link formatPendingAutoRefusal}.
2616
+ */
2617
+ async function refuseWhenMigrationsPending(args, config, url, flag) {
2618
+ if ((0, migrate_js_1.listMigrationFiles)(config.migrationsDir).length === 0)
2619
+ return;
2620
+ const pending = (0, migrate_js_1.pendingMigrationFiles)(await (0, migrate_js_1.migrateStatus)(url, config.migrationsDir, { schema: config.schema }));
2621
+ if (pending.length === 0)
2622
+ return;
2623
+ if (args.allowPending) {
2624
+ (0, ui_js_1.warn)(`--allow-pending is set: ${(0, ui_js_1.bold)(String(pending.length))} pending migration(s) are NOT reflected in the diff.`);
2625
+ (0, ui_js_1.newline)();
2626
+ return;
2627
+ }
2628
+ const [first, ...rest] = (0, migrate_js_1.formatPendingAutoRefusal)(pending, flag).split('\n');
2629
+ (0, ui_js_1.error)(first);
2630
+ for (const line of rest)
2631
+ (0, ui_js_1.errorLine)(line);
2632
+ (0, ui_js_1.errorLine)();
2633
+ process.exit(1);
2634
+ }
2635
+ /**
2636
+ * Explain an empty `--auto` / `--from-diff` diff without claiming more than it
2637
+ * knows: "in sync" only when nothing, extra tables and advisories included, was
2638
+ * left over.
2639
+ */
2640
+ function reportNothingToMigrate(diff, spinner) {
2641
+ const advisories = diff.warnings ?? [];
2642
+ if (diff.drop.length === 0 && advisories.length === 0) {
2643
+ spinner.succeed('Database is already in sync: nothing to migrate');
2644
+ (0, ui_js_1.newline)();
2645
+ printNotCompared(diff.notCompared);
2646
+ return;
2647
+ }
2648
+ spinner.succeed('Nothing to migrate: no statement to write, but the database is not fully in sync');
2649
+ (0, ui_js_1.newline)();
2650
+ if (diff.drop.length > 0) {
2651
+ (0, ui_js_1.warn)(`${(0, ui_js_1.bold)(String(diff.drop.length))} table(s) exist in the database but not in the schema file ` +
2652
+ `(${diff.drop.join(', ')}). A migration never drops a table on its own: write that one by hand.`);
2653
+ (0, ui_js_1.newline)();
2654
+ }
2655
+ for (const w of advisories)
2656
+ (0, ui_js_1.warn)(w);
2657
+ if (advisories.length > 0)
2658
+ (0, ui_js_1.newline)();
2659
+ printNotCompared(diff.notCompared);
2660
+ }
2298
2661
  async function cmdMigrateCreate(args, config) {
2299
2662
  (0, ui_js_1.banner)();
2300
2663
  // `--auto` / `--from-diff` diff the schema FILE against the namespace, so both
@@ -2324,18 +2687,21 @@ async function cmdMigrateCreate(args, config) {
2324
2687
  (0, ui_js_1.errorLine)();
2325
2688
  process.exit(1);
2326
2689
  }
2690
+ if (args.allowPending && !args.auto && !args.fromDiff) {
2691
+ failArg(`${(0, ui_js_1.cyan)('--allow-pending')} only applies to ${(0, ui_js_1.cyan)('--auto')} and ${(0, ui_js_1.cyan)('--from-diff')}.`, 'It lets a diff-generated migration proceed while earlier migrations are still pending.');
2692
+ }
2327
2693
  if (args.auto) {
2328
2694
  // Auto-generate migration from schema diff
2329
2695
  const url = requireUrl(config);
2330
2696
  (0, ui_js_1.label)('Database', (0, ui_js_1.redactUrl)(url));
2331
2697
  (0, ui_js_1.label)('Schema file', config.schemaFile);
2332
2698
  (0, ui_js_1.newline)();
2699
+ await refuseWhenMigrationsPending(args, config, url, '--auto');
2333
2700
  const schemaDef = await loadSchemaFile(config.schemaFile);
2334
2701
  const diffSpinner = new ui_js_1.Spinner('Computing schema diff').start();
2335
2702
  const diff = await (0, schema_sql_js_1.schemaDiff)(schemaDef, url, { schema: config.schema });
2336
2703
  if (diff.statements.length === 0) {
2337
- diffSpinner.succeed('Database is already in sync: nothing to migrate');
2338
- (0, ui_js_1.newline)();
2704
+ reportNothingToMigrate(diff, diffSpinner);
2339
2705
  return;
2340
2706
  }
2341
2707
  diffSpinner.succeed(`Found ${(0, ui_js_1.bold)(String(diff.statements.length))} change(s)`);
@@ -2387,7 +2753,9 @@ async function cmdMigrateCreate(args, config) {
2387
2753
  console.log(` ${(0, ui_js_1.red)(ui_js_1.symbols.warning)} ${(0, ui_js_1.dim)('DOWN')} [${h.kind}] ${h.target}`);
2388
2754
  }
2389
2755
  (0, ui_js_1.newline)();
2390
- console.log(` ${(0, ui_js_1.dim)('`migrate up` refuses destructive statements by default: confirm interactively or pass')} ${(0, ui_js_1.cyan)('--allow-destructive')}${(0, ui_js_1.dim)('.')}`);
2756
+ console.log(body.destructiveUp.length > 0
2757
+ ? ` ${(0, ui_js_1.dim)('`migrate up` refuses destructive statements by default: confirm interactively or pass')} ${(0, ui_js_1.cyan)('--allow-destructive')}${(0, ui_js_1.dim)('.')}`
2758
+ : ` ${(0, ui_js_1.dim)('Only the DOWN section is destructive: `migrate up` runs this file without a prompt, `migrate down` asks first.')}`);
2391
2759
  (0, ui_js_1.newline)();
2392
2760
  }
2393
2761
  if (diff.warnings && diff.warnings.length > 0) {
@@ -2407,12 +2775,12 @@ async function cmdMigrateCreate(args, config) {
2407
2775
  (0, ui_js_1.label)('Database', (0, ui_js_1.redactUrl)(url));
2408
2776
  (0, ui_js_1.label)('Schema file', config.schemaFile);
2409
2777
  (0, ui_js_1.newline)();
2778
+ await refuseWhenMigrationsPending(args, config, url, '--from-diff');
2410
2779
  const schemaDef = await loadSchemaFile(config.schemaFile);
2411
2780
  const diffSpinner = new ui_js_1.Spinner('Computing schema diff').start();
2412
2781
  const diff = await (0, schema_sql_js_1.schemaDiff)(schemaDef, url, { schema: config.schema });
2413
2782
  if (diff.statements.length === 0) {
2414
- diffSpinner.succeed('Database is already in sync: nothing to migrate');
2415
- (0, ui_js_1.newline)();
2783
+ reportNothingToMigrate(diff, diffSpinner);
2416
2784
  return;
2417
2785
  }
2418
2786
  diffSpinner.succeed(`Found ${(0, ui_js_1.bold)(String(diff.statements.length))} change(s)`);
@@ -2459,7 +2827,9 @@ async function cmdMigrateCreate(args, config) {
2459
2827
  console.log(` ${(0, ui_js_1.red)(ui_js_1.symbols.warning)} ${(0, ui_js_1.dim)('DOWN')} [${h.kind}] ${h.target}`);
2460
2828
  }
2461
2829
  (0, ui_js_1.newline)();
2462
- console.log(` ${(0, ui_js_1.dim)('`migrate up` refuses destructive statements by default: confirm interactively or pass')} ${(0, ui_js_1.cyan)('--allow-destructive')}${(0, ui_js_1.dim)('.')}`);
2830
+ console.log(body.destructiveUp.length > 0
2831
+ ? ` ${(0, ui_js_1.dim)('`migrate up` refuses destructive statements by default: confirm interactively or pass')} ${(0, ui_js_1.cyan)('--allow-destructive')}${(0, ui_js_1.dim)('.')}`
2832
+ : ` ${(0, ui_js_1.dim)('Only the DOWN section is destructive: `migrate up` runs this file without a prompt, `migrate down` asks first.')}`);
2463
2833
  (0, ui_js_1.newline)();
2464
2834
  }
2465
2835
  if (diff.warnings && diff.warnings.length > 0) {
@@ -2780,7 +3150,18 @@ async function cmdMigrateDeploy(args, config) {
2780
3150
  if (destructive.length > 0)
2781
3151
  printDestructiveNotice(destructive);
2782
3152
  const runSpinner = new ui_js_1.Spinner('Deploying migrations').start();
2783
- const result = await (0, migrate_js_1.migrateDeploy)(url, config.migrationsDir, { allowDrift: args.allowDrift, schema: config.schema });
3153
+ const result = await (0, migrate_js_1.migrateDeploy)(url, config.migrationsDir, {
3154
+ allowDrift: args.allowDrift,
3155
+ schema: config.schema,
3156
+ lockWaitMs: args.wait !== undefined ? args.wait * 1000 : undefined,
3157
+ onLockWait: () => {
3158
+ runSpinner.stop();
3159
+ (0, ui_js_1.warn)(`Another migration holds the lock: waiting up to ${(0, ui_js_1.bold)(String(args.wait))}s for it to finish.`);
3160
+ console.log(` ${(0, ui_js_1.dim)('Whatever it applies is then no longer pending, so this run will skip it.')}`);
3161
+ (0, ui_js_1.newline)();
3162
+ runSpinner.start();
3163
+ },
3164
+ });
2784
3165
  if (result.applied.length > 0) {
2785
3166
  runSpinner.succeed(`${(0, ui_js_1.bold)(String(result.applied.length))} applied`);
2786
3167
  for (const file of result.applied) {
@@ -2907,6 +3288,15 @@ async function cmdMigrateDown(args, config) {
2907
3288
  for (const file of result.rolledBack) {
2908
3289
  console.log(` ${(0, ui_js_1.yellow)(ui_js_1.symbols.arrowRight)} ${file.filename}`);
2909
3290
  }
3291
+ const noSql = (0, migrate_js_1.rollbacksWithoutSql)(result.rolledBack);
3292
+ if (noSql.length > 0) {
3293
+ (0, ui_js_1.newline)();
3294
+ (0, ui_js_1.warn)(`${(0, ui_js_1.bold)(String(noSql.length))} rolled-back migration(s) have a DOWN section that runs no SQL:`);
3295
+ for (const file of noSql)
3296
+ console.log(` ${(0, ui_js_1.yellow)(ui_js_1.symbols.dot)} ${file.filename}`);
3297
+ console.log(` ${(0, ui_js_1.dim)('Their tracking rows are removed, but nothing they changed was undone (a dropped column')}`);
3298
+ console.log(` ${(0, ui_js_1.dim)('cannot be restored automatically). Restore that data from a backup.')}`);
3299
+ }
2910
3300
  }
2911
3301
  else if (result.errors.length === 0) {
2912
3302
  spinner.succeed('No migrations to roll back');
@@ -2930,20 +3320,24 @@ async function cmdMigrateDown(args, config) {
2930
3320
  }
2931
3321
  (0, ui_js_1.newline)();
2932
3322
  }
2933
- async function cmdMigrateStatus(_args, config) {
3323
+ async function cmdMigrateStatus(args, config) {
2934
3324
  (0, ui_js_1.banner)();
2935
3325
  const url = requireUrl(config);
2936
3326
  (0, ui_js_1.label)('Database', (0, ui_js_1.redactUrl)(url));
2937
3327
  (0, ui_js_1.label)('Migrations', config.migrationsDir);
2938
3328
  (0, ui_js_1.newline)();
2939
3329
  const allFiles = (0, migrate_js_1.listMigrationFiles)(config.migrationsDir);
2940
- if (allFiles.length === 0) {
3330
+ // A gate still has to ask the database when there are no files: applied
3331
+ // migrations whose files are gone are exactly the drift it exists to catch.
3332
+ if (allFiles.length === 0 && !args.exitCode) {
2941
3333
  (0, ui_js_1.warn)('No migration files found.');
2942
3334
  console.log(` ${(0, ui_js_1.dim)('Create one with:')} ${(0, ui_js_1.cyan)('npx turbine migrate create <name>')}`);
2943
3335
  (0, ui_js_1.newline)();
2944
3336
  return;
2945
3337
  }
2946
3338
  warnIgnoredMigrationFiles(config.migrationsDir);
3339
+ if (allFiles.length === 0)
3340
+ (0, ui_js_1.warn)('No migration files found.');
2947
3341
  const statuses = await (0, migrate_js_1.migrateStatus)(url, config.migrationsDir, { schema: config.schema });
2948
3342
  const appliedCount = statuses.filter((s) => s.applied).length;
2949
3343
  const pendingCount = statuses.filter((s) => !s.applied).length;
@@ -2998,6 +3392,14 @@ async function cmdMigrateStatus(_args, config) {
2998
3392
  console.log(` ${(0, ui_js_1.dim)('Run')} ${(0, ui_js_1.cyan)('npx turbine migrate up')} ${(0, ui_js_1.dim)('to apply pending migrations.')}`);
2999
3393
  (0, ui_js_1.newline)();
3000
3394
  }
3395
+ if (args.exitCode) {
3396
+ const problems = (0, migrate_js_1.statusGateProblems)((0, migrate_js_1.summarizeMigrationStatus)(statuses), (0, migrate_js_1.listIgnoredSqlFiles)(config.migrationsDir).map(introspect_js_1.printableCatalogName));
3397
+ if (problems.length > 0) {
3398
+ (0, ui_js_1.warn)(`Migration history is not clean: ${problems.join(', ')} ${(0, ui_js_1.dim)('(--exit-code: exiting 2)')}.`);
3399
+ (0, ui_js_1.newline)();
3400
+ process.exit(2);
3401
+ }
3402
+ }
3001
3403
  }
3002
3404
  function getSeedExecutionPlan(seedFile) {
3003
3405
  const ext = (0, node_path_1.extname)(seedFile).toLowerCase();
@@ -3056,6 +3458,9 @@ async function seedConnectionString(config) {
3056
3458
  }
3057
3459
  return (0, migrate_js_1.connectionStringForSchema)(config.url, schema, inherited);
3058
3460
  }
3461
+ /** The notice for a {@link SeedRunOutcome} that could not be confirmed. */
3462
+ const UNCONFIRMED_SEED_NOTICE = 'The seed file ran to completion, but it does not call defineSeed(), so Turbine cannot tell whether it wrote anything. ' +
3463
+ 'Wrap the work in defineSeed(async (db) => { ... }) to have it confirmed.';
3059
3464
  async function runSeedPlan(plan, config) {
3060
3465
  const oldDatabaseUrl = process.env.DATABASE_URL;
3061
3466
  // The seed connects through DATABASE_URL, whichever way it runs: `defineSeed`
@@ -3075,9 +3480,11 @@ async function runSeedPlan(plan, config) {
3075
3480
  }
3076
3481
  // The seed runs in a child process, so we cannot observe its callback
3077
3482
  // directly. Hand it a sentinel path: `defineSeed`'s runner writes the file
3078
- // only after a callback executes to completion. If the child exits cleanly
3079
- // but the sentinel never appears, the seed module loaded without running
3080
- // anything: a silent no-op we must report as a failure, not success.
3483
+ // only after a callback executes to completion. A non-zero exit is a
3484
+ // failure. A clean exit with no sentinel is a script that did its work at
3485
+ // the top level (a typed-client seed), which succeeds and says so, unless
3486
+ // the file names `defineSeed`: then its callback was expected to run and
3487
+ // did not, which is a no-op to report as a failure.
3081
3488
  const sentinelDir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'turbine-seed-'));
3082
3489
  const sentinel = (0, node_path_1.join)(sentinelDir, 'ran');
3083
3490
  const { execFileSync } = await Promise.resolve().then(() => __importStar(require('node:child_process')));
@@ -3090,15 +3497,18 @@ async function runSeedPlan(plan, config) {
3090
3497
  TURBINE_SEED_SENTINEL: sentinel,
3091
3498
  },
3092
3499
  });
3093
- if (!(0, node_fs_1.existsSync)(sentinel)) {
3500
+ if ((0, node_fs_1.existsSync)(sentinel))
3501
+ return { confirmed: true };
3502
+ const seedFile = plan.args[plan.args.length - 1] ?? '';
3503
+ if (/\bdefineSeed\b/.test((0, node_fs_1.readFileSync)(seedFile, 'utf-8'))) {
3094
3504
  throw new Error('The seed file completed without running a seed callback. ' +
3095
3505
  'Make sure it calls defineSeed(async (db) => { ... }) at the top level.');
3096
3506
  }
3507
+ return { confirmed: false };
3097
3508
  }
3098
3509
  finally {
3099
3510
  (0, node_fs_1.rmSync)(sentinelDir, { recursive: true, force: true });
3100
3511
  }
3101
- return;
3102
3512
  }
3103
3513
  if (plan.kind === 'js') {
3104
3514
  const mod = await Promise.resolve(`${(0, node_url_1.pathToFileURL)(plan.file).href}`).then(s => __importStar(require(s)));
@@ -3107,7 +3517,7 @@ async function runSeedPlan(plan, config) {
3107
3517
  'Export your seed with `export default defineSeed(async (db) => { ... })`.');
3108
3518
  }
3109
3519
  await mod.default();
3110
- return;
3520
+ return { confirmed: true };
3111
3521
  }
3112
3522
  // `seedUrl` is already pinned and already schema-checked; `requireUrl` is
3113
3523
  // only reached when no url was configured at all, in which case there is no
@@ -3123,6 +3533,7 @@ async function runSeedPlan(plan, config) {
3123
3533
  finally {
3124
3534
  await client.end();
3125
3535
  }
3536
+ return { confirmed: true };
3126
3537
  }
3127
3538
  finally {
3128
3539
  if (oldDatabaseUrl === undefined) {
@@ -3148,14 +3559,14 @@ async function cmdSeed(_args, config) {
3148
3559
  }
3149
3560
  const spinner = new ui_js_1.Spinner('Running seed file').start();
3150
3561
  try {
3151
- await runSeedPlan(getSeedExecutionPlan(seedFile), config);
3562
+ const outcome = await runSeedPlan(getSeedExecutionPlan(seedFile), config);
3152
3563
  spinner.succeed('Seed completed');
3564
+ if (!outcome.confirmed)
3565
+ (0, ui_js_1.warn)(UNCONFIRMED_SEED_NOTICE);
3153
3566
  }
3154
3567
  catch (err) {
3155
3568
  spinner.fail('Seed failed');
3156
- if (err instanceof Error) {
3157
- console.log(` ${(0, ui_js_1.dim)((0, ui_js_1.redactUrl)(err.message))}`);
3158
- }
3569
+ console.log(` ${(0, ui_js_1.dim)((0, ui_js_1.redactUrl)((0, error_text_js_1.errorText)(err)))}`);
3159
3570
  (0, ui_js_1.newline)();
3160
3571
  process.exit(1);
3161
3572
  }
@@ -3186,7 +3597,7 @@ async function cmdStatus(_args, config) {
3186
3597
  for (const tbl of Object.values(schema.tables)) {
3187
3598
  const relCount = Object.keys(tbl.relations).length;
3188
3599
  const _pk = tbl.primaryKey.join(', ') || (0, ui_js_1.dim)('(none)');
3189
- console.log(` ${(0, ui_js_1.bold)((0, ui_js_1.cyan)(tbl.name))}`);
3600
+ console.log(` ${(0, ui_js_1.bold)((0, ui_js_1.cyan)(shown(tbl.name)))}`);
3190
3601
  for (let i = 0; i < tbl.columns.length; i++) {
3191
3602
  const col = tbl.columns[i];
3192
3603
  const isLast = i === tbl.columns.length - 1 && relCount === 0;
@@ -3194,7 +3605,7 @@ async function cmdStatus(_args, config) {
3194
3605
  const nullable = col.nullable ? (0, ui_js_1.dim)('?') : '';
3195
3606
  const def = col.hasDefault ? (0, ui_js_1.dim)(' (default)') : '';
3196
3607
  const pkLabel = tbl.primaryKey.includes(col.name) ? ` ${(0, ui_js_1.magenta)('PK')}` : '';
3197
- console.log(` ${(0, ui_js_1.dim)(prefix)} ${col.field}${nullable}: ${(0, ui_js_1.green)(col.tsType)}${pkLabel}${def} ${(0, ui_js_1.gray)(`${ui_js_1.symbols.arrow} ${col.pgType}`)}`);
3608
+ console.log(` ${(0, ui_js_1.dim)(prefix)} ${shown(col.field)}${nullable}: ${(0, ui_js_1.green)(shown(col.tsType))}${pkLabel}${def} ${(0, ui_js_1.gray)(`${ui_js_1.symbols.arrow} ${shown(col.pgType)}`)}`);
3198
3609
  }
3199
3610
  const rels = Object.entries(tbl.relations);
3200
3611
  if (rels.length > 0) {
@@ -3204,7 +3615,7 @@ async function cmdStatus(_args, config) {
3204
3615
  const prefix = isLast ? ui_js_1.symbols.teeEnd : ui_js_1.symbols.tee;
3205
3616
  const relColor = rel.type === 'hasMany' ? ui_js_1.blue : ui_js_1.yellow;
3206
3617
  const fkDisplay = Array.isArray(rel.foreignKey) ? rel.foreignKey.join(', ') : rel.foreignKey;
3207
- console.log(` ${(0, ui_js_1.dim)(prefix)} ${relColor(relName)} ${(0, ui_js_1.dim)(ui_js_1.symbols.arrow)} ${rel.to} ${(0, ui_js_1.dim)(`(${rel.type}, FK: ${fkDisplay})`)}`);
3618
+ console.log(` ${(0, ui_js_1.dim)(prefix)} ${relColor(shown(relName))} ${(0, ui_js_1.dim)(ui_js_1.symbols.arrow)} ${shown(rel.to)} ${(0, ui_js_1.dim)(`(${rel.type}, FK: ${shown(fkDisplay)})`)}`);
3208
3619
  }
3209
3620
  }
3210
3621
  (0, ui_js_1.newline)();
@@ -3212,7 +3623,7 @@ async function cmdStatus(_args, config) {
3212
3623
  if (Object.keys(schema.enums).length > 0) {
3213
3624
  console.log(` ${(0, ui_js_1.bold)('Enums:')}`);
3214
3625
  for (const [enumName, labels] of Object.entries(schema.enums)) {
3215
- console.log(` ${(0, ui_js_1.cyan)(enumName)}: ${labels.map((l) => (0, ui_js_1.green)(`'${l}'`)).join((0, ui_js_1.dim)(' | '))}`);
3626
+ console.log(` ${(0, ui_js_1.cyan)(shown(enumName))}: ${labels.map((l) => (0, ui_js_1.green)(`'${shown(l)}'`)).join((0, ui_js_1.dim)(' | '))}`);
3216
3627
  }
3217
3628
  (0, ui_js_1.newline)();
3218
3629
  }
@@ -3270,6 +3681,23 @@ const CONCURRENTLY_RECIPE_COMMENT = [
3270
3681
  '-- makes "migrate up" look hung. For bounded waits, SET lock_timeout /',
3271
3682
  '-- statement_timeout in a psql session.',
3272
3683
  ].join('\n');
3684
+ /**
3685
+ * Why `migrate up`, `deploy` and `down` refuse a pooled endpoint. The reason is
3686
+ * the migration lock, not the doctor's session-state argument.
3687
+ */
3688
+ exports.MIGRATE_POOLER_EXPLANATION = [
3689
+ 'turbine migrate serializes runs with a session-level advisory lock (pg_try_advisory_lock).',
3690
+ 'That lock belongs to one server backend and only means something while the same backend',
3691
+ 'serves every statement of the run. A transaction pooler (PgBouncer, a Neon "-pooler"',
3692
+ "endpoint, Supabase's pooler) hands each transaction to whichever backend is free, so:",
3693
+ '',
3694
+ ' 1. The lock is not tied to this run. It can be taken on one backend and released on',
3695
+ ' another, which leaves it held on a shared backend (later runs are told another',
3696
+ ' migration is in progress) and lets two runs apply the same file.',
3697
+ ' 2. Session state a migration file sets (SET lock_timeout, SET search_path) is left on',
3698
+ ' a shared backend for other clients, and state an earlier client left there applies',
3699
+ ' to this run.',
3700
+ ];
3273
3701
  /**
3274
3702
  * Refuse a `doctor` run whose connection string points at a transaction pooler.
3275
3703
  *
@@ -3291,13 +3719,13 @@ const CONCURRENTLY_RECIPE_COMMENT = [
3291
3719
  * Exported for the unit test only (like {@link isLoopbackHost}): a gate whose
3292
3720
  * failure path is `process.exit` is untestable through the command itself.
3293
3721
  */
3294
- function refusePoolerConnection(url, args) {
3722
+ function refusePoolerConnection(url, args, refusal = { command: 'turbine doctor' }) {
3295
3723
  if (args.allowPooler === true)
3296
3724
  return;
3297
3725
  const detection = (0, connection_url_js_1.detectPooler)(url);
3298
3726
  if (!detection.pooled)
3299
3727
  return;
3300
- const lines = (0, connection_url_js_1.poolerRefusalMessage)(detection, { command: 'turbine doctor', allowFlag: '--allow-pooler' });
3728
+ const lines = (0, connection_url_js_1.poolerRefusalMessage)(detection, { ...refusal, allowFlag: '--allow-pooler' });
3301
3729
  console.error('');
3302
3730
  console.error(` ${(0, ui_js_1.red)(ui_js_1.symbols.cross)} ${lines[0]}`);
3303
3731
  for (const line of lines.slice(1))
@@ -3365,7 +3793,7 @@ async function cmdDoctor(args, config) {
3365
3793
  tables: {},
3366
3794
  indexes: [],
3367
3795
  nullFrac: {},
3368
- notices: [`statistics collection failed: ${err instanceof Error ? err.message : String(err)}`],
3796
+ notices: [`statistics collection failed: ${(0, error_text_js_1.errorText)(err)}`],
3369
3797
  };
3370
3798
  }
3371
3799
  // Table-heat boost: read _turbine_metrics (app DB or --metrics-url) and use
@@ -3590,6 +4018,12 @@ function renderUnusedCaveats(minScans, snapshot) {
3590
4018
  }
3591
4019
  (0, ui_js_1.newline)();
3592
4020
  }
4021
+ /**
4022
+ * Catalog names go to the terminal through this. A database lets its creator
4023
+ * pick any characters for a table or index name, and a raw ESC or bidi control
4024
+ * in one rewrites the line it is printed on.
4025
+ */
4026
+ const shown = introspect_js_1.printableCatalogName;
3593
4027
  /** doctor --unused: never-scanned indexes with DROP suggestions (report-only). */
3594
4028
  function renderUnusedIndexes(unused, minScans, snapshot) {
3595
4029
  if (unused.length === 0)
@@ -3598,12 +4032,12 @@ function renderUnusedIndexes(unused, minScans, snapshot) {
3598
4032
  (0, ui_js_1.warn)(`Found ${(0, ui_js_1.bold)(String(unused.length))} never-scanned index(es) (${(0, index_stats_js_1.formatBytes)(total)} reclaimable).`);
3599
4033
  renderUnusedCaveats(minScans, snapshot);
3600
4034
  for (const u of unused) {
3601
- console.log(` ${(0, ui_js_1.yellow)(ui_js_1.symbols.warning)} ${(0, ui_js_1.bold)((0, ui_js_1.cyan)(u.table))} ${(0, ui_js_1.dim)(`(${u.columns.join(', ') || '?'})`)} ${(0, ui_js_1.gray)(`${u.indexName} · ${u.idxScan} scans · ${(0, index_stats_js_1.formatBytes)(u.sizeBytes)}`)}`);
4035
+ console.log(` ${(0, ui_js_1.yellow)(ui_js_1.symbols.warning)} ${(0, ui_js_1.bold)((0, ui_js_1.cyan)(shown(u.table)))} ${(0, ui_js_1.dim)(`(${shown(u.columns.join(', ') || '?')})`)} ${(0, ui_js_1.gray)(`${shown(u.indexName)} · ${u.idxScan} scans · ${(0, index_stats_js_1.formatBytes)(u.sizeBytes)}`)}`);
3602
4036
  // A functional / partial / non-btree index is not a plain `(col)` rebuild:
3603
4037
  // say what it actually is before anyone runs the DROP.
3604
4038
  if (u.caveat)
3605
4039
  console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.tee)} ${(0, ui_js_1.yellow)(u.caveat)}`);
3606
- console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.teeEnd)} ${(0, ui_js_1.green)(u.dropSql)}`);
4040
+ console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.teeEnd)} ${(0, ui_js_1.green)(shown(u.dropSql))}`);
3607
4041
  (0, ui_js_1.newline)();
3608
4042
  }
3609
4043
  }
@@ -3616,9 +4050,9 @@ function renderRedundantIndexes(redundant) {
3616
4050
  console.log(` ${(0, ui_js_1.dim)('Each is a leading prefix of a wider index that already serves the same lookups. Report-only, never auto-dropped.')}`);
3617
4051
  (0, ui_js_1.newline)();
3618
4052
  for (const r of redundant) {
3619
- console.log(` ${(0, ui_js_1.yellow)(ui_js_1.symbols.warning)} ${(0, ui_js_1.bold)((0, ui_js_1.cyan)(r.table))} ${(0, ui_js_1.dim)(`(${r.columns.join(', ')})`)} ${(0, ui_js_1.gray)(`${r.indexName} · ${(0, index_stats_js_1.formatBytes)(r.sizeBytes)}`)}`);
3620
- console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.tee)} covered by ${(0, ui_js_1.blue)(r.coveredBy)} ${(0, ui_js_1.dim)(`(${r.coveredByColumns.join(', ')})`)}`);
3621
- console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.teeEnd)} ${(0, ui_js_1.green)(r.dropSql)}`);
4053
+ console.log(` ${(0, ui_js_1.yellow)(ui_js_1.symbols.warning)} ${(0, ui_js_1.bold)((0, ui_js_1.cyan)(shown(r.table)))} ${(0, ui_js_1.dim)(`(${shown(r.columns.join(', '))})`)} ${(0, ui_js_1.gray)(`${shown(r.indexName)} · ${(0, index_stats_js_1.formatBytes)(r.sizeBytes)}`)}`);
4054
+ console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.tee)} covered by ${(0, ui_js_1.blue)(shown(r.coveredBy))} ${(0, ui_js_1.dim)(`(${shown(r.coveredByColumns.join(', '))})`)}`);
4055
+ console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.teeEnd)} ${(0, ui_js_1.green)(shown(r.dropSql))}`);
3622
4056
  (0, ui_js_1.newline)();
3623
4057
  }
3624
4058
  }
@@ -3633,7 +4067,7 @@ function renderDoctorAudit(audit, minScans, snapshot) {
3633
4067
  renderUnusedCaveats(minScans, snapshot);
3634
4068
  for (const a of audit) {
3635
4069
  const tag = a.ambiguous ? (0, ui_js_1.red)(' [ambiguous: truncated name collides with another column set]') : '';
3636
- console.log(` ${(0, ui_js_1.yellow)(ui_js_1.symbols.warning)} ${(0, ui_js_1.bold)((0, ui_js_1.cyan)(a.table))} ${(0, ui_js_1.dim)(`(${a.columns.join(', ') || '?'})`)} ${(0, ui_js_1.gray)(`${a.indexName} · ${a.idxScan} scans · ${(0, index_stats_js_1.formatBytes)(a.sizeBytes)}`)}${tag}`);
4070
+ console.log(` ${(0, ui_js_1.yellow)(ui_js_1.symbols.warning)} ${(0, ui_js_1.bold)((0, ui_js_1.cyan)(shown(a.table)))} ${(0, ui_js_1.dim)(`(${shown(a.columns.join(', ') || '?')})`)} ${(0, ui_js_1.gray)(`${shown(a.indexName)} · ${a.idxScan} scans · ${(0, index_stats_js_1.formatBytes)(a.sizeBytes)}`)}${tag}`);
3637
4071
  if (a.ambiguous) {
3638
4072
  console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.tee)} ${(0, ui_js_1.dim)('the 63-byte name maps to more than one probe column set; confirm before dropping')}`);
3639
4073
  }
@@ -3642,7 +4076,7 @@ function renderDoctorAudit(audit, minScans, snapshot) {
3642
4076
  console.log(` ${(0, ui_js_1.dim)('reappear as a missing-index finding in this same report.')}`);
3643
4077
  }
3644
4078
  else if (a.dropSql) {
3645
- console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.teeEnd)} ${(0, ui_js_1.green)(a.dropSql)}`);
4079
+ console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.teeEnd)} ${(0, ui_js_1.green)(shown(a.dropSql))}`);
3646
4080
  }
3647
4081
  (0, ui_js_1.newline)();
3648
4082
  }
@@ -3696,12 +4130,12 @@ function divergenceAmplificationLines(d) {
3696
4130
  const amp = divInt(d.worstCaseAmplification ?? 0);
3697
4131
  const corr = d.orderColumnCorrelation;
3698
4132
  const corrLabel = corr === null || corr === undefined
3699
- ? `no pg_stats correlation available for "${d.orderColumn}"`
3700
- : `correlation ${corr.toFixed(5)} on "${d.orderColumn}"`;
4133
+ ? `no pg_stats correlation available for "${shown(d.orderColumn)}"`
4134
+ : `correlation ${corr.toFixed(5)} on "${shown(d.orderColumn)}"`;
3701
4135
  if (d.heapNearlyOrdered === true) {
3702
4136
  return [
3703
4137
  `The size of that flip turns on the heap's physical order, and THIS heap is in near-exact`,
3704
- `"${d.orderColumn}" order (${corrLabel}), so consecutive index entries hit the`,
4138
+ `"${shown(d.orderColumn)}" order (${corrLabel}), so consecutive index entries hit the`,
3705
4139
  `same pinned page: measured ~1x, not the ~${amp}x an unordered heap reads. Most likely this`,
3706
4140
  `one is not costing you anything today. It is also one sampled statistic away from the`,
3707
4141
  `much worse reading, so measure rather than assume in either direction.`,
@@ -3710,7 +4144,7 @@ function divergenceAmplificationLines(d) {
3710
4144
  return [
3711
4145
  `That costs ~${amp}x the buffers of the seq scan, because each index entry is its own heap`,
3712
4146
  `fetch (${corrLabel}). The one shape that reads ~1x instead is a heap`,
3713
- `in near-exact "${d.orderColumn}" order; two pages of local disorder already reads ~41x.`,
4147
+ `in near-exact "${shown(d.orderColumn)}" order; two pages of local disorder already reads ~41x.`,
3714
4148
  ];
3715
4149
  }
3716
4150
  function renderDivergenceEvidence(d) {
@@ -3718,7 +4152,7 @@ function renderDivergenceEvidence(d) {
3718
4152
  console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.tee)} ${(0, ui_js_1.yellow)('cached-plan risk:')} this unindexed filter column can also flip a cached plan.`);
3719
4153
  console.log(` ${(0, ui_js_1.dim)(`${divInt(d.rows)} rows in ${divInt(d.pages)} pages, rarest value ~${divInt(d.rarestBucket)} rows, below the assumed LIMIT ${d.assumedLimit}.`)}`);
3720
4154
  console.log(` ${(0, ui_js_1.dim)(`Without the index the good plan is a seq scan (${divInt(d.pages)} pages); a promoted generic`)}`);
3721
- console.log(` ${(0, ui_js_1.dim)(`plan cannot see the value is rare, keeps the ordered "${d.orderColumn}" walk, and reads`)}`);
4155
+ console.log(` ${(0, ui_js_1.dim)(`plan cannot see the value is rare, keeps the ordered "${shown(d.orderColumn)}" walk, and reads`)}`);
3722
4156
  console.log(` ${(0, ui_js_1.dim)(`up to ~${tuples} tuples before it fills the LIMIT.`)}`);
3723
4157
  console.log(` ${(0, ui_js_1.dim)('Postgres promotes this shape exactly when the workload keeps asking for the rare')}`);
3724
4158
  console.log(` ${(0, ui_js_1.dim)('value: that is the case where the custom plan is expensive enough for the generic')}`);
@@ -3727,7 +4161,7 @@ function renderDivergenceEvidence(d) {
3727
4161
  console.log(` ${(0, ui_js_1.dim)(line)}`);
3728
4162
  console.log(` ${(0, ui_js_1.dim)('Adding the index above is the fix. Confirm first if you want to:')}`);
3729
4163
  for (const line of d.diagnosticSql.split('\n')) {
3730
- console.log(` ${(0, ui_js_1.green)(line)}`);
4164
+ console.log(` ${(0, ui_js_1.green)(shown(line))}`);
3731
4165
  }
3732
4166
  console.log(` ${(0, ui_js_1.dim)('After adding this index, re-run doctor. This column is expected to reappear as a')}`);
3733
4167
  console.log(` ${(0, ui_js_1.dim)('sparse-value finding in the cached-plan section. That later finding is exposure, not')}`);
@@ -3781,9 +4215,9 @@ function renderFinding(f, opts) {
3781
4215
  const rows = f.score.metrics.rows;
3782
4216
  const rowsLabel = rows !== null ? `~${rows.toLocaleString()} rows` : 'row count unknown';
3783
4217
  const sizeLabel = f.score.metrics.sizeBytes !== null ? `, ${(0, index_stats_js_1.formatBytes)(f.score.metrics.sizeBytes)}` : '';
3784
- console.log(` ${(0, ui_js_1.yellow)(ui_js_1.symbols.warning)} ${(0, ui_js_1.bold)((0, ui_js_1.cyan)(m.table))} ${(0, ui_js_1.dim)(`(${m.columns.join(', ')})`)} ${(0, ui_js_1.gray)(`${rowsLabel}${sizeLabel}`)}`);
4218
+ console.log(` ${(0, ui_js_1.yellow)(ui_js_1.symbols.warning)} ${(0, ui_js_1.bold)((0, ui_js_1.cyan)(shown(m.table)))} ${(0, ui_js_1.dim)(`(${shown(m.columns.join(', '))})`)} ${(0, ui_js_1.gray)(`${rowsLabel}${sizeLabel}`)}`);
3785
4219
  for (const p of m.probes) {
3786
- console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.tee)} probed by ${p.from}.${(0, ui_js_1.blue)(p.relation)} ${(0, ui_js_1.dim)(`(${p.type})`)}`);
4220
+ console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.tee)} probed by ${shown(p.from)}.${(0, ui_js_1.blue)(shown(p.relation))} ${(0, ui_js_1.dim)(`(${p.type})`)}`);
3787
4221
  }
3788
4222
  if (opts.withReasons) {
3789
4223
  for (const reason of f.score.reasons) {
@@ -3791,7 +4225,7 @@ function renderFinding(f, opts) {
3791
4225
  }
3792
4226
  }
3793
4227
  const last = opts.divergence ? ui_js_1.symbols.tee : ui_js_1.symbols.teeEnd;
3794
- console.log(` ${(0, ui_js_1.dim)(last)} ${(0, ui_js_1.green)(doctorCreateSql(f, { concurrently: opts.concurrently }))}`);
4228
+ console.log(` ${(0, ui_js_1.dim)(last)} ${(0, ui_js_1.green)(shown(doctorCreateSql(f, { concurrently: opts.concurrently })))}`);
3795
4229
  if (opts.divergence)
3796
4230
  renderDivergenceEvidence(opts.divergence);
3797
4231
  (0, ui_js_1.newline)();
@@ -3804,8 +4238,8 @@ function renderInvalidIndexes(invalid) {
3804
4238
  console.log(` ${(0, ui_js_1.dim)('IF NOT EXISTS skips an invalid index on rerun, so it never rebuilds. Drop it, then rerun:')}`);
3805
4239
  (0, ui_js_1.newline)();
3806
4240
  for (const idx of invalid) {
3807
- console.log(` ${(0, ui_js_1.red)(ui_js_1.symbols.warning)} ${(0, ui_js_1.bold)((0, ui_js_1.cyan)(idx.table))} ${(0, ui_js_1.dim)(`(${idx.columns.join(', ') || '?'})`)} ${(0, ui_js_1.gray)(idx.indexName)}`);
3808
- console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.teeEnd)} ${(0, ui_js_1.green)(idx.dropSql)}`);
4241
+ console.log(` ${(0, ui_js_1.red)(ui_js_1.symbols.warning)} ${(0, ui_js_1.bold)((0, ui_js_1.cyan)(shown(idx.table)))} ${(0, ui_js_1.dim)(`(${shown(idx.columns.join(', ') || '?')})`)} ${(0, ui_js_1.gray)(shown(idx.indexName))}`);
4242
+ console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.teeEnd)} ${(0, ui_js_1.green)(shown(idx.dropSql))}`);
3809
4243
  (0, ui_js_1.newline)();
3810
4244
  }
3811
4245
  }
@@ -3898,10 +4332,10 @@ function renderPlanDivergence(divergence, attached) {
3898
4332
  // on (an index that exists but cannot serve the equality: partial,
3899
4333
  // expression, or non-btree). The remedy is still an index, not a
3900
4334
  // plan-cache setting, so this entry never suggests forceCustomPlan.
3901
- console.log(` ${(0, ui_js_1.yellow)(ui_js_1.symbols.warning)} ${(0, ui_js_1.bold)((0, ui_js_1.cyan)(`${f.table}.${f.column}`))} ${(0, ui_js_1.gray)('UNINDEXED-FILTER FLIP')}`);
4335
+ console.log(` ${(0, ui_js_1.yellow)(ui_js_1.symbols.warning)} ${(0, ui_js_1.bold)((0, ui_js_1.cyan)(`${shown(f.table)}.${shown(f.column)}`))} ${(0, ui_js_1.gray)('UNINDEXED-FILTER FLIP')}`);
3902
4336
  console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.tee)} ${divInt(f.rows)} rows in ${divInt(f.pages)} pages, rarest value ~${(0, ui_js_1.bold)(divInt(f.rarestBucket))} rows, below the assumed LIMIT ${f.assumedLimit}`);
3903
- console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.tee)} no index serves ${f.column} = $1, so the good plan is a seq scan (${divInt(f.pages)} pages);`);
3904
- console.log(` ${(0, ui_js_1.dim)(`a promoted generic plan keeps the ordered "${f.orderColumn}" walk and reads up to ~${divInt(f.tuplesWalked ?? f.rows)} tuples`)}`);
4337
+ console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.tee)} no index serves ${shown(f.column)} = $1, so the good plan is a seq scan (${divInt(f.pages)} pages);`);
4338
+ console.log(` ${(0, ui_js_1.dim)(`a promoted generic plan keeps the ordered "${shown(f.orderColumn)}" walk and reads up to ~${divInt(f.tuplesWalked ?? f.rows)} tuples`)}`);
3905
4339
  console.log(` ${(0, ui_js_1.dim)('before it fills the LIMIT.')}`);
3906
4340
  for (const line of divergenceAmplificationLines(f))
3907
4341
  console.log(` ${(0, ui_js_1.dim)(line)}`);
@@ -3916,13 +4350,13 @@ function renderPlanDivergence(divergence, attached) {
3916
4350
  (0, ui_js_1.newline)();
3917
4351
  continue;
3918
4352
  }
3919
- console.log(` ${(0, ui_js_1.yellow)(ui_js_1.symbols.warning)} ${(0, ui_js_1.bold)((0, ui_js_1.cyan)(`${f.table}.${f.column}`))} ${(0, ui_js_1.gray)('SPARSE-VALUE FLIP')}`);
4353
+ console.log(` ${(0, ui_js_1.yellow)(ui_js_1.symbols.warning)} ${(0, ui_js_1.bold)((0, ui_js_1.cyan)(`${shown(f.table)}.${shown(f.column)}`))} ${(0, ui_js_1.gray)('SPARSE-VALUE FLIP')}`);
3920
4354
  console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.tee)} generic estimate ${(0, ui_js_1.bold)(divInt(f.genericEstimate))} rows ${(0, ui_js_1.dim)(`(${divInt(f.rows)} rows / ${divInt(f.distinctValues)} distinct values)`)}`);
3921
4355
  console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.tee)} rarest value bucket ${(0, ui_js_1.bold)(divInt(f.rarestBucket))} rows ${(0, ui_js_1.dim)('(pg_stats most_common_freqs / residual bucket)')}`);
3922
4356
  console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.tee)} crossover ${(0, ui_js_1.bold)(divInt(f.crossoverRows ?? 0))} rows ${(0, ui_js_1.dim)(`(sqrt(limit ${f.assumedLimit} x ${divInt(f.pages)} pages); ${divInt(f.crossoverRowsWide ?? 0)} at limit ${f.thresholds.wideLimit})`)}`);
3923
4357
  console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.tee)} ${(0, ui_js_1.dim)(`values below the crossover: ${divInt(f.valuesBelowCrossover ?? 0)} of ${divInt(f.distinctValues)}, filter-column correlation ${f.correlation.toFixed(2)}, ${analyzedLabel(f)}`)}`);
3924
4358
  console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.tee)} for such a value the generic plan walks ~${(0, ui_js_1.bold)(divInt(f.walkPages ?? 0))} of ${divInt(f.pages)} pages ${(0, ui_js_1.dim)(`(${Math.round((f.walkFraction ?? 0) * 100)}% of the table)`)}`);
3925
- console.log(` ${(0, ui_js_1.dim)(`for reads shaped WHERE ${f.column} = $1 ORDER BY ${f.orderColumn} LIMIT $n,`)}`);
4359
+ console.log(` ${(0, ui_js_1.dim)(`for reads shaped WHERE ${shown(f.column)} = $1 ORDER BY ${shown(f.orderColumn)} LIMIT $n,`)}`);
3926
4360
  console.log(` ${(0, ui_js_1.dim)("where the custom plan reads only that value's own rows.")}`);
3927
4361
  console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.tee)} ${(0, ui_js_1.dim)('No amplification figure is printed, deliberately. This models how many rows a')}`);
3928
4362
  console.log(` ${(0, ui_js_1.dim)('value has, not WHERE they sit in the heap, and the second half can move the')}`);
@@ -3984,7 +4418,7 @@ function renderPlanDivergence(divergence, attached) {
3984
4418
  console.log(` ${(0, ui_js_1.dim)('in one direction trades this finding for its mirror image. That applies equally to')}`);
3985
4419
  console.log(` ${(0, ui_js_1.dim)("Turbine's client-level `planCacheMode` and to a SET or ALTER ROLE applied outside")}`);
3986
4420
  console.log(` ${(0, ui_js_1.dim)('Turbine.')}`);
3987
- console.log(` 5. A composite index on (${first.column}, ${first.orderColumn}) makes the GOOD plan better. It`);
4421
+ console.log(` 5. A composite index on (${shown(first.column)}, ${shown(first.orderColumn)}) makes the GOOD plan better. It`);
3988
4422
  console.log(` ${(0, ui_js_1.dim)('does NOT stop the generic plan from choosing the other one, and it can widen the gap.')}`);
3989
4423
  console.log(` ${(0, ui_js_1.dim)('Add it for the custom-plan win, not as a fix for this finding.')}`);
3990
4424
  }
@@ -4009,7 +4443,7 @@ function renderPlanDivergence(divergence, attached) {
4009
4443
  if (notices.length > 0) {
4010
4444
  console.log(` ${(0, ui_js_1.dim)('Not scored for cached-plan divergence (statistics missing):')}`);
4011
4445
  for (const n of notices) {
4012
- console.log(` ${(0, ui_js_1.dim)(`- ${n.table}.${n.column}: ${n.reason}`)}`);
4446
+ console.log(` ${(0, ui_js_1.dim)(`- ${shown(n.table)}.${shown(n.column)}: ${n.reason}`)}`);
4013
4447
  }
4014
4448
  (0, ui_js_1.newline)();
4015
4449
  }
@@ -4107,7 +4541,7 @@ async function cmdStudio(args, config) {
4107
4541
  spinner.succeed(demo ? 'Demo Studio is running' : 'Studio is running');
4108
4542
  }
4109
4543
  catch (err) {
4110
- spinner.fail(`Failed to start Studio: ${err instanceof Error ? err.message : String(err)}`);
4544
+ spinner.fail(`Failed to start Studio: ${(0, error_text_js_1.errorText)(err)}`);
4111
4545
  process.exit(1);
4112
4546
  }
4113
4547
  if (demo) {
@@ -4240,7 +4674,7 @@ async function cmdObserve(args) {
4240
4674
  spinner.succeed('Observe dashboard is running');
4241
4675
  }
4242
4676
  catch (err) {
4243
- spinner.fail(`Failed to start Observe: ${err instanceof Error ? err.message : String(err)}`);
4677
+ spinner.fail(`Failed to start Observe: ${(0, error_text_js_1.errorText)(err)}`);
4244
4678
  process.exit(1);
4245
4679
  }
4246
4680
  (0, ui_js_1.newline)();
@@ -4348,10 +4782,12 @@ function showGenerateHelp() {
4348
4782
  console.log(` ${(0, ui_js_1.cyan)('--schema, -s')} ${(0, ui_js_1.dim)('<name>')} Postgres schema ${(0, ui_js_1.dim)('(default: public)')}`);
4349
4783
  console.log(` ${(0, ui_js_1.cyan)('--include')} ${(0, ui_js_1.dim)('<tables>')} Comma-separated tables to include`);
4350
4784
  console.log(` ${(0, ui_js_1.cyan)('--exclude')} ${(0, ui_js_1.dim)('<tables>')} Comma-separated tables to exclude`);
4785
+ console.log(` ${(0, ui_js_1.cyan)('--schema-file')} ${(0, ui_js_1.dim)('<path>')} Your ${(0, ui_js_1.cyan)('defineSchema()')} file, whose ${(0, ui_js_1.cyan)('pii')} / ${(0, ui_js_1.cyan)('updatedAt')} tags are applied ${(0, ui_js_1.dim)('(default: ./turbine/schema.ts)')}`);
4351
4786
  console.log(` ${(0, ui_js_1.cyan)('--zod')} Also emit ${(0, ui_js_1.cyan)('zod.ts')} validation schemas ${(0, ui_js_1.dim)('(needs the zod dep)')}`);
4352
4787
  console.log(` ${(0, ui_js_1.cyan)('--include-views')} Include views + materialized views as read-only entities`);
4353
4788
  console.log(` ${(0, ui_js_1.cyan)('--no-timestamp')} Omit the ${(0, ui_js_1.dim)('Generated at:')} header line ${(0, ui_js_1.dim)('(reproducible, diff-stable output)')}`);
4354
- console.log(` ${(0, ui_js_1.cyan)('--import-ext')} ${(0, ui_js_1.dim)('<mode>')} Sibling-import extension: ${(0, ui_js_1.cyan)('js')} / ${(0, ui_js_1.cyan)('none')} / ${(0, ui_js_1.cyan)('auto')} ${(0, ui_js_1.dim)('(default: auto)')}`);
4789
+ console.log(` ${(0, ui_js_1.cyan)('--import-ext')} ${(0, ui_js_1.dim)('<mode>')} Sibling-import extension: ${(0, ui_js_1.cyan)('js')} / ${(0, ui_js_1.cyan)('ts')} / ${(0, ui_js_1.cyan)('none')} / ${(0, ui_js_1.cyan)('auto')} ${(0, ui_js_1.dim)('(default: auto)')}`);
4790
+ console.log(` ${(0, ui_js_1.dim)('ts emits ./types.ts so')} ${(0, ui_js_1.cyan)('node app.ts')} ${(0, ui_js_1.dim)('can run the client with native type stripping')}`);
4355
4791
  console.log(` ${(0, ui_js_1.cyan)('--keep-column-names')} Keep raw DB column names as field names ${(0, ui_js_1.dim)('(snake_case, not camelCase)')}`);
4356
4792
  console.log(` ${(0, ui_js_1.cyan)('--legacy-to-many-uniques')} Emit ${(0, ui_js_1.cyan)('hasMany')} for unique-FK children ${(0, ui_js_1.dim)('(pre-0.41 shape; default flips to hasOne)')}`);
4357
4793
  console.log(` ${(0, ui_js_1.cyan)('--allow-empty')} Generate even when introspection matches 0 tables`);
@@ -4408,6 +4844,7 @@ function showPushHelp() {
4408
4844
  console.log(` ${(0, ui_js_1.cyan)('--url, -u')} ${(0, ui_js_1.dim)('<url>')} Postgres connection string`);
4409
4845
  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)')}`);
4410
4846
  console.log(` ${(0, ui_js_1.cyan)('--dry-run')} Show SQL without executing`);
4847
+ console.log(` ${(0, ui_js_1.cyan)('--exit-code')} With ${(0, ui_js_1.cyan)('--dry-run')}: exit 2 when the database differs from the schema file, 0 when in sync ${(0, ui_js_1.dim)('(CI)')}`);
4411
4848
  console.log(` ${(0, ui_js_1.cyan)('--allow-destructive')} Skip the interactive confirmation for data-destroying statements ${(0, ui_js_1.dim)('(CI)')}`);
4412
4849
  console.log(` ${(0, ui_js_1.cyan)('--verbose, -v')} Show detailed output`);
4413
4850
  (0, ui_js_1.newline)();
@@ -4434,7 +4871,11 @@ function showMigrateHelp() {
4434
4871
  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)')}`);
4435
4872
  console.log(` ${(0, ui_js_1.cyan)('--step, -n')} ${(0, ui_js_1.dim)('<N>')} Number of migrations to apply/rollback`);
4436
4873
  console.log(` ${(0, ui_js_1.cyan)('--dry-run')} Show SQL without executing`);
4874
+ console.log(` ${(0, ui_js_1.cyan)('--exit-code')} Exit 2 when migrations are pending, modified, missing or misnamed ${(0, ui_js_1.dim)('(status only, CI)')}`);
4875
+ console.log(` ${(0, ui_js_1.cyan)('--wait')}${(0, ui_js_1.dim)('[=<seconds>]')} Wait for a concurrent deploy to release the lock, default ${exports.DEFAULT_LOCK_WAIT_SECONDS}s ${(0, ui_js_1.dim)('(deploy only)')}`);
4876
+ console.log(` ${(0, ui_js_1.cyan)('--allow-pending')} Let ${(0, ui_js_1.cyan)('--auto')} / ${(0, ui_js_1.cyan)('--from-diff')} run while earlier migrations are still pending ${(0, ui_js_1.dim)('(create only)')}`);
4437
4877
  console.log(` ${(0, ui_js_1.cyan)('--allow-drift')} Re-baseline drifted checksums, REWRITING history ${(0, ui_js_1.dim)('(up / deploy / down)')}`);
4878
+ console.log(` ${(0, ui_js_1.cyan)('--allow-pooler')} Run ${(0, ui_js_1.cyan)('up')} / ${(0, ui_js_1.cyan)('deploy')} / ${(0, ui_js_1.cyan)('down')} through a transaction pooler ${(0, ui_js_1.dim)('(refused: the migration lock is session-level)')}`);
4438
4879
  console.log(` ${(0, ui_js_1.cyan)('--allow-destructive')} Run data-destroying migration statements without the interactive confirm`);
4439
4880
  console.log(` ${(0, ui_js_1.cyan)('--schema, -s')} ${(0, ui_js_1.dim)('<name>')} Postgres schema the migrations run in ${(0, ui_js_1.dim)('(default: public)')}`);
4440
4881
  console.log(` ${(0, ui_js_1.cyan)('--verbose, -v')} Show detailed output`);
@@ -4446,8 +4887,10 @@ function showMigrateHelp() {
4446
4887
  console.log(` ${(0, ui_js_1.dim)('$')} npx turbine migrate create backfill_full_name --recipe backfill`);
4447
4888
  console.log(` ${(0, ui_js_1.dim)('$')} npx turbine migrate up`);
4448
4889
  console.log(` ${(0, ui_js_1.dim)('$')} npx turbine migrate deploy --dry-run`);
4890
+ console.log(` ${(0, ui_js_1.dim)('$')} npx turbine migrate deploy --wait=120`);
4449
4891
  console.log(` ${(0, ui_js_1.dim)('$')} npx turbine migrate down --step 2`);
4450
4892
  console.log(` ${(0, ui_js_1.dim)('$')} npx turbine migrate status`);
4893
+ console.log(` ${(0, ui_js_1.dim)('$')} npx turbine migrate status --exit-code`);
4451
4894
  (0, ui_js_1.newline)();
4452
4895
  }
4453
4896
  function showSeedHelp() {
@@ -4671,8 +5114,12 @@ function showHelp() {
4671
5114
  console.log(` ${(0, ui_js_1.cyan)('--from-diff')} Generate from schema diff, destructive statements flagged ${(0, ui_js_1.dim)('(create)')}`);
4672
5115
  console.log(` ${(0, ui_js_1.cyan)('--recipe')} ${(0, ui_js_1.dim)('<name>')} Scaffold a named migration recipe, e.g. backfill ${(0, ui_js_1.dim)('(create)')}`);
4673
5116
  console.log(` ${(0, ui_js_1.cyan)('--step, -n')} ${(0, ui_js_1.dim)('<N>')} Number of migrations to apply/rollback`);
5117
+ console.log(` ${(0, ui_js_1.cyan)('--exit-code')} Exit 2 on drift ${(0, ui_js_1.dim)('(push --dry-run, migrate status)')}`);
5118
+ console.log(` ${(0, ui_js_1.cyan)('--wait')}${(0, ui_js_1.dim)('[=<seconds>]')} Wait for a concurrent deploy's lock instead of failing ${(0, ui_js_1.dim)('(deploy)')}`);
5119
+ console.log(` ${(0, ui_js_1.cyan)('--allow-pending')} Diff even while migrations are pending ${(0, ui_js_1.dim)('(create --auto / --from-diff)')}`);
4674
5120
  console.log(` ${(0, ui_js_1.cyan)('--allow-destructive')} Run data-destroying statements without prompting ${(0, ui_js_1.dim)('(up/down/push)')}`);
4675
5121
  console.log(` ${(0, ui_js_1.cyan)('--allow-drift')} Re-baseline drifted checksums on ${(0, ui_js_1.cyan)('migrate up')} / ${(0, ui_js_1.cyan)('deploy')} / ${(0, ui_js_1.cyan)('down')} ${(0, ui_js_1.dim)('(advanced)')}`);
5122
+ console.log(` ${(0, ui_js_1.cyan)('--allow-pooler')} Let ${(0, ui_js_1.cyan)('migrate up')} / ${(0, ui_js_1.cyan)('deploy')} / ${(0, ui_js_1.cyan)('down')} and ${(0, ui_js_1.cyan)('doctor')} use a transaction pooler ${(0, ui_js_1.dim)('(refused by default)')}`);
4676
5123
  (0, ui_js_1.newline)();
4677
5124
  console.log(` ${(0, ui_js_1.bold)('Init options:')}`);
4678
5125
  console.log(` ${(0, ui_js_1.cyan)('--yes, -y')} Accept every step's default (non-interactive)`);
@@ -4789,9 +5236,19 @@ function cmdSkill(args) {
4789
5236
  }
4790
5237
  const skillsDir = args.dir ?? (0, node_path_1.join)('.claude', 'skills');
4791
5238
  const target = (0, node_path_1.resolve)(process.cwd(), skillsDir, 'turbine-orm', 'SKILL.md');
4792
- const existed = (0, node_fs_1.existsSync)(target);
4793
5239
  (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(target), { recursive: true });
4794
- (0, node_fs_1.writeFileSync)(target, body);
5240
+ // Exclusive create first: only the wording depends on whether the file was
5241
+ // there, and asking separately would let the answer go stale before the write.
5242
+ let existed = false;
5243
+ try {
5244
+ (0, node_fs_1.writeFileSync)(target, body, { flag: 'wx' });
5245
+ }
5246
+ catch (err) {
5247
+ if (err.code !== 'EEXIST')
5248
+ throw err;
5249
+ existed = true;
5250
+ (0, node_fs_1.writeFileSync)(target, body);
5251
+ }
4795
5252
  (0, ui_js_1.newline)();
4796
5253
  (0, ui_js_1.success)(`${existed ? 'Updated' : 'Installed'} the Turbine query skill`);
4797
5254
  console.log(` ${(0, ui_js_1.dim)((0, node_path_1.relative)(process.cwd(), target))}`);
@@ -4854,7 +5311,7 @@ async function main() {
4854
5311
  // expected here; anything that does arrive is a bug in the parser rather
4855
5312
  // than in the caller's command line, and printing it beats swallowing it.
4856
5313
  (0, ui_js_1.errorLine)();
4857
- (0, ui_js_1.error)(err instanceof Error ? err.message : String(err));
5314
+ (0, ui_js_1.error)((0, error_text_js_1.errorText)(err));
4858
5315
  (0, ui_js_1.errorLine)();
4859
5316
  process.exit(1);
4860
5317
  }
@@ -4987,6 +5444,9 @@ async function main() {
4987
5444
  }
4988
5445
  catch (err) {
4989
5446
  if (err instanceof Error) {
5447
+ // `errorText`, not `message`: a refused `localhost` is an
5448
+ // AggregateError with an empty message (see cli/error-text.ts).
5449
+ const message = (0, error_text_js_1.errorText)(err);
4990
5450
  // TYPE first, message second. These branches sniff for substrings, and
4991
5451
  // `ValidationError` beat them by accident: a refused `--schema` value says
4992
5452
  // "used as a connection parameter", which contains "connection", so a bad
@@ -4994,35 +5454,35 @@ async function main() {
4994
5454
  // firewall hints. A typed error already knows what it is.
4995
5455
  if (err instanceof errors_js_1.ValidationError) {
4996
5456
  (0, ui_js_1.errorLine)();
4997
- (0, ui_js_1.error)((0, ui_js_1.redactUrl)(err.message));
5457
+ (0, ui_js_1.error)((0, ui_js_1.redactUrl)(message));
4998
5458
  if (args.verbose && err.stack) {
4999
5459
  (0, ui_js_1.errorLine)();
5000
5460
  console.error((0, ui_js_1.dim)((0, ui_js_1.redactUrl)(err.stack)));
5001
5461
  }
5002
5462
  }
5003
- else if (err.message.includes('ECONNREFUSED') || err.message.includes('connection')) {
5463
+ else if (message.includes('ECONNREFUSED') || message.includes('connection')) {
5004
5464
  (0, ui_js_1.errorLine)();
5005
5465
  (0, ui_js_1.error)(`Could not connect to database`);
5006
- (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)((0, ui_js_1.redactUrl)(err.message))}`);
5466
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)((0, ui_js_1.redactUrl)(message))}`);
5007
5467
  (0, ui_js_1.errorLine)();
5008
5468
  (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('Check that:')}`);
5009
5469
  (0, ui_js_1.errorLine)(` ${(0, ui_js_1.dim)('1.')} Your database is running`);
5010
5470
  (0, ui_js_1.errorLine)(` ${(0, ui_js_1.dim)('2.')} The connection string is correct`);
5011
5471
  (0, ui_js_1.errorLine)(` ${(0, ui_js_1.dim)('3.')} Network/firewall allows the connection`);
5012
5472
  }
5013
- else if (err.message.includes('authentication')) {
5473
+ else if (message.includes('authentication')) {
5014
5474
  (0, ui_js_1.errorLine)();
5015
5475
  (0, ui_js_1.error)(`Authentication failed`);
5016
- (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)((0, ui_js_1.redactUrl)(err.message))}`);
5476
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)((0, ui_js_1.redactUrl)(message))}`);
5017
5477
  }
5018
- else if (err.message.includes('does not exist')) {
5478
+ else if (message.includes('does not exist')) {
5019
5479
  (0, ui_js_1.errorLine)();
5020
5480
  (0, ui_js_1.error)(`Database or schema not found`);
5021
- (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)((0, ui_js_1.redactUrl)(err.message))}`);
5481
+ (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)((0, ui_js_1.redactUrl)(message))}`);
5022
5482
  }
5023
5483
  else {
5024
5484
  (0, ui_js_1.errorLine)();
5025
- (0, ui_js_1.error)((0, ui_js_1.redactUrl)(err.message));
5485
+ (0, ui_js_1.error)((0, ui_js_1.redactUrl)(message));
5026
5486
  if (args.verbose && err.stack) {
5027
5487
  (0, ui_js_1.errorLine)();
5028
5488
  console.error((0, ui_js_1.dim)((0, ui_js_1.redactUrl)(err.stack)));