@drzl/cli 4.19.0 → 4.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,21 +1,290 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ CONFIG_FILE_NAMES,
4
+ ConfigValidationError,
5
+ GENERATOR_KINDS,
6
+ addressableName,
7
+ ambiguousPatternWarnings,
3
8
  computeGeneratorOutputDirs,
4
9
  computeWatchTargets,
10
+ configFromKinds,
11
+ displayTableName,
12
+ expressOutDir,
13
+ fastifyOutDir,
5
14
  filterTables,
15
+ graphqlOutDir,
16
+ hasNamedSchemas,
17
+ honoOutDir,
18
+ importFreshConfigModule,
6
19
  loadConfig,
20
+ matchesAny,
21
+ matchesTable,
22
+ nearestKey,
23
+ nestjsOutDir,
24
+ tableAliases,
25
+ tableFilterWarnings,
7
26
  trpcOutDir
8
- } from "./chunk-V2IXXAC2.js";
27
+ } from "./chunk-54E2IO7N.js";
9
28
 
10
29
  // src/cli.ts
11
- import { SchemaAnalyzer } from "@drzl/analyzer";
12
- import { ORPCGenerator } from "@drzl/generator-orpc";
13
- import chalk3 from "chalk";
30
+ import { qualifiedTableName as qualifiedTableName2, SchemaAnalyzer as SchemaAnalyzer2 } from "@drzl/analyzer";
14
31
  import chokidar from "chokidar";
15
- import cliProgress from "cli-progress";
16
32
  import { Command } from "commander";
17
- import * as path4 from "path";
33
+ import * as path7 from "path";
34
+
35
+ // src/output.ts
36
+ import { Chalk } from "chalk";
37
+ import cliProgress from "cli-progress";
18
38
  import ora from "ora";
39
+ var EXIT_OK = 0;
40
+ var EXIT_FAILED = 1;
41
+ var EXIT_FINDINGS = 2;
42
+ var PROGRESS_MIN_TABLES = 25;
43
+ function colorLevelFor(stream, env) {
44
+ if (env.NO_COLOR !== void 0 && env.NO_COLOR !== "") return 0;
45
+ if (env.TERM === "dumb") return 0;
46
+ const forced = env.FORCE_COLOR;
47
+ if (forced !== void 0) {
48
+ if (forced === "false" || forced === "0") return 0;
49
+ if (forced === "" || forced === "true") return 1;
50
+ const n = Number.parseInt(forced, 10);
51
+ if (Number.isInteger(n)) return Math.min(Math.max(n, 0), 3);
52
+ return 1;
53
+ }
54
+ if (!stream.isTTY) return 0;
55
+ if (env.COLORTERM === "truecolor" || env.COLORTERM === "24bit") return 3;
56
+ if (env.TERM?.includes("256")) return 2;
57
+ return 1;
58
+ }
59
+ function shouldShowProgress(opts) {
60
+ if (opts.quiet || opts.json) return false;
61
+ if (!opts.stderr.isTTY) return false;
62
+ return opts.tables >= PROGRESS_MIN_TABLES;
63
+ }
64
+ function createProgress(enabled, total, stream) {
65
+ if (!enabled) {
66
+ return { start() {
67
+ }, update() {
68
+ }, stop() {
69
+ } };
70
+ }
71
+ const bar = new cliProgress.SingleBar(
72
+ { hideCursor: true, stream },
73
+ cliProgress.Presets.shades_classic
74
+ );
75
+ let running = false;
76
+ return {
77
+ start() {
78
+ if (running) return;
79
+ bar.start(total, 0);
80
+ running = true;
81
+ },
82
+ update(value) {
83
+ if (running) bar.update(value);
84
+ },
85
+ stop() {
86
+ if (!running) return;
87
+ bar.stop();
88
+ running = false;
89
+ }
90
+ };
91
+ }
92
+ var Output = class {
93
+ constructor(options = {}) {
94
+ this.stdout = options.stdout ?? process.stdout;
95
+ this.stderr = options.stderr ?? process.stderr;
96
+ this.env = options.env ?? process.env;
97
+ this.quiet = options.quiet ?? false;
98
+ this.json = options.json ?? false;
99
+ this.outStyle = new Chalk({ level: colorLevelFor(this.stdout, this.env) });
100
+ this.errStyle = new Chalk({ level: colorLevelFor(this.stderr, this.env) });
101
+ }
102
+ /** The command's answer. Never suppressed by `--quiet`, because then nothing would be left. */
103
+ data(text) {
104
+ this.stdout.write(text.endsWith("\n") ? text : text + "\n");
105
+ }
106
+ /**
107
+ * The one JSON document `--json` promises, and the reason nothing else may touch stdout.
108
+ *
109
+ * Stringified without indentation on purpose: this is a machine's copy, `jq` formats it for a
110
+ * human, and the two commands that already print an indented document (`analyze`, `doctor`) keep
111
+ * doing so through `data` because their shape is a published contract.
112
+ */
113
+ jsonData(payload) {
114
+ this.data(JSON.stringify(payload));
115
+ }
116
+ /** Narration. Dropped by `--quiet` and by `--json`. */
117
+ note(text) {
118
+ if (this.quiet || this.json) return;
119
+ this.stderr.write(text + "\n");
120
+ }
121
+ /** A warning: narration a user asked to be quiet still does not need. */
122
+ warn(text) {
123
+ if (this.quiet || this.json) return;
124
+ this.stderr.write(this.errStyle.yellow(text) + "\n");
125
+ }
126
+ /**
127
+ * A failure. Never suppressed by anything, because a script that cannot tell a success from a
128
+ * swallowed failure is worse off than one with no `--quiet` at all.
129
+ *
130
+ * Under `--json` the machine-readable failure goes to stdout as the document, so this stays
131
+ * quiet there rather than printing the same fact twice in two shapes.
132
+ */
133
+ error(text, detail) {
134
+ if (this.json) return;
135
+ const line = this.errStyle.red(text) + (detail ? " " + detail : "");
136
+ this.stderr.write(line + "\n");
137
+ }
138
+ /** A hint under an error. Suppressed by `--quiet`: the error above it already said what broke. */
139
+ hint(text) {
140
+ if (this.quiet || this.json) return;
141
+ this.stderr.write(this.errStyle.dim(text) + "\n");
142
+ }
143
+ /**
144
+ * A spinner on stderr, or nothing.
145
+ *
146
+ * `ora` is constructed only when stderr is a terminal. Given a pipe it still writes its text
147
+ * once as `- Analyzing...`, which is a line nobody reading a log wants, and given `NO_COLOR` it
148
+ * writes a coloured symbol anyway. Both are avoided by not building it.
149
+ */
150
+ spinner(text) {
151
+ const live = !this.quiet && !this.json && this.stderr.isTTY ? ora({
152
+ text,
153
+ stream: this.stderr,
154
+ // ora paints its own frame cyan through its own chalk, which is a second colour
155
+ // decision beside this one and does not read `NO_COLOR` either. Measured with the
156
+ // variable set: everything else on the line went plain and the spinner frame arrived
157
+ // as `[36m⠋[39m`. `false` is ora's documented way to turn that off.
158
+ color: this.errStyle.level > 0 ? "cyan" : false
159
+ }).start() : null;
160
+ return {
161
+ succeed: (done) => {
162
+ live?.stop();
163
+ this.succeed(done);
164
+ },
165
+ fail: (done) => {
166
+ live?.stop();
167
+ this.error(done);
168
+ },
169
+ stop: () => live?.stop()
170
+ };
171
+ }
172
+ /**
173
+ * A completed step.
174
+ *
175
+ * The tick is rendered here rather than by `ora.succeed`, which is the fix for the escape that
176
+ * reached piped output: `log-symbols` colours the symbol from the environment alone and never
177
+ * looks at the stream, so `drzl analyze 2> log` used to write `✔` into
178
+ * the file. Here the symbol goes through the same per-stream decision as everything else.
179
+ */
180
+ succeed(text) {
181
+ if (this.quiet || this.json) return;
182
+ this.stderr.write(this.errStyle.green("\u2714") + " " + text + "\n");
183
+ }
184
+ /** A progress bar for `tables` items, or a no-op. See `shouldShowProgress` for the four gates. */
185
+ progress(tables) {
186
+ return createProgress(
187
+ shouldShowProgress({
188
+ tables,
189
+ stderr: this.stderr,
190
+ quiet: this.quiet,
191
+ json: this.json
192
+ }),
193
+ tables,
194
+ this.stderr
195
+ );
196
+ }
197
+ /**
198
+ * Whether an unrequested extra, such as the sponsor tip, should be shown at all.
199
+ *
200
+ * A terminal is the only place an aside has a reader. Piped into a file it is noise in someone's
201
+ * log, and under `--json` it would be noise in the middle of a document.
202
+ */
203
+ get wantsAsides() {
204
+ return !this.quiet && !this.json && Boolean(this.stderr.isTTY);
205
+ }
206
+ };
207
+ function jsonFailure(command, code, message, exitCode = EXIT_FAILED) {
208
+ return { ok: false, command, code, message, exitCode };
209
+ }
210
+ function messageOf(value) {
211
+ const message = value?.message;
212
+ return String(message ?? value);
213
+ }
214
+
215
+ // src/express-options.ts
216
+ function expressOptions(g, cfg) {
217
+ return {
218
+ outputDir: expressOutDir(g, cfg),
219
+ includeRelations: g.includeRelations,
220
+ naming: g.naming,
221
+ outputHeader: g.outputHeader,
222
+ format: g.format,
223
+ importExtension: g.importExtension,
224
+ validation: g.validation
225
+ };
226
+ }
227
+
228
+ // src/fastify-options.ts
229
+ function fastifyOptions(g, cfg) {
230
+ return {
231
+ outputDir: fastifyOutDir(g, cfg),
232
+ includeRelations: g.includeRelations,
233
+ naming: g.naming,
234
+ outputHeader: g.outputHeader,
235
+ format: g.format,
236
+ importExtension: g.importExtension
237
+ };
238
+ }
239
+
240
+ // src/generator-loader.ts
241
+ var GeneratorNotInstalledError = class extends Error {
242
+ constructor(specifier, reason) {
243
+ super(`${specifier} is not installed`);
244
+ this.specifier = specifier;
245
+ this.reason = reason;
246
+ this.name = "GeneratorNotInstalledError";
247
+ }
248
+ };
249
+ function isPackageMissing(err, specifier) {
250
+ const code = err?.code;
251
+ if (code !== "ERR_MODULE_NOT_FOUND") return false;
252
+ const message = err?.message;
253
+ return typeof message === "string" && message.includes(`'${specifier}'`);
254
+ }
255
+ async function loadGenerator(specifier, load) {
256
+ try {
257
+ return await load();
258
+ } catch (e) {
259
+ if (isPackageMissing(e, specifier)) throw new GeneratorNotInstalledError(specifier, e);
260
+ throw e;
261
+ }
262
+ }
263
+
264
+ // src/graphql-options.ts
265
+ function graphqlOptions(g, cfg) {
266
+ return {
267
+ outputDir: graphqlOutDir(g, cfg),
268
+ naming: g.naming,
269
+ outputHeader: g.outputHeader,
270
+ format: g.format,
271
+ importExtension: g.importExtension
272
+ };
273
+ }
274
+
275
+ // src/hono-options.ts
276
+ function honoOptions(g, cfg) {
277
+ return {
278
+ outputDir: honoOutDir(g, cfg),
279
+ includeRelations: g.includeRelations,
280
+ naming: g.naming,
281
+ validator: g.validator,
282
+ outputHeader: g.outputHeader,
283
+ format: g.format,
284
+ importExtension: g.importExtension,
285
+ validation: g.validation
286
+ };
287
+ }
19
288
 
20
289
  // src/validation-options.ts
21
290
  function validationOptions(g, cfg, outDir, caps = {}) {
@@ -32,6 +301,11 @@ function validationOptions(g, cfg, outDir, caps = {}) {
32
301
  duplicateFinder: g.duplicateFinder,
33
302
  nestedSchemas: g.nestedSchemas,
34
303
  nestedDepth: g.nestedDepth,
304
+ // Every validation generator can express a brand, including TypeBox, which has no brand
305
+ // helper and gets one from `TUnsafe` instead. So this needs no capability flag: an option
306
+ // that reached only four of the five would be the class of defect this file exists to
307
+ // remove.
308
+ branded: g.branded,
35
309
  // Only where the generator can act on them, so an unsupported option is absent rather than
36
310
  // present and ignored.
37
311
  ...caps.schemaTypes ? {
@@ -39,7 +313,10 @@ function validationOptions(g, cfg, outDir, caps = {}) {
39
313
  schemaPath: cfg.schema,
40
314
  typedJson: g.typedJson,
41
315
  typedColumns: g.typedColumns
42
- } : {}
316
+ } : {},
317
+ ...caps.standardSchema ? { standardSchema: g.standardSchema } : {},
318
+ ...caps.meta ? { meta: g.meta } : {},
319
+ ...caps.constraints ? { constraints: g.constraints } : {}
43
320
  };
44
321
  }
45
322
 
@@ -53,7 +330,57 @@ function jsonSchemaOptions(g, cfg, outDir) {
53
330
  document: g.document,
54
331
  // Read only while emitting a document, where it adds `/users/{id}/posts`. The per-table
55
332
  // schemas are flat whatever it says.
56
- includeRelations: g.includeRelations
333
+ includeRelations: g.includeRelations,
334
+ // The mirror image: read only for the per-table modules, since the document shares regardless.
335
+ sharedEnums: g.sharedEnums
336
+ };
337
+ }
338
+
339
+ // src/nestjs-options.ts
340
+ function nestjsOptions(g, cfg) {
341
+ return {
342
+ outputDir: nestjsOutDir(g, cfg),
343
+ naming: g.naming,
344
+ outputHeader: g.outputHeader,
345
+ format: g.format,
346
+ importExtension: g.importExtension,
347
+ validation: g.validation
348
+ };
349
+ }
350
+
351
+ // src/orpc-options.ts
352
+ function orpcOptions(g, cfg, servicesDir) {
353
+ return {
354
+ outputDir: cfg.outDir,
355
+ template: g.template,
356
+ includeRelations: g.includeRelations,
357
+ naming: g.naming,
358
+ outputHeader: g.outputHeader,
359
+ format: g.format,
360
+ templateOptions: g.templateOptions,
361
+ importExtension: g.importExtension,
362
+ validation: g.validation,
363
+ // Documented on this generator since it was added and unreachable from a config file for most
364
+ // of that time, because the config schema had no such key and zod stripped it in silence.
365
+ databaseInjection: g.databaseInjection,
366
+ // Where the service generator is actually writing, so a router template that imports services
367
+ // spells a path that exists. The templates default this to `src/services`, which is right only
368
+ // by coincidence for a config that puts them elsewhere.
369
+ servicesDir
370
+ };
371
+ }
372
+
373
+ // src/service-options.ts
374
+ function serviceOptions(g, outDir) {
375
+ return {
376
+ outDir,
377
+ outputHeader: g.outputHeader,
378
+ format: g.format,
379
+ dataAccess: g.dataAccess,
380
+ dbImportPath: g.dbImportPath,
381
+ schemaImportPath: g.schemaImportPath,
382
+ importExtension: g.importExtension,
383
+ databaseInjection: g.databaseInjection
57
384
  };
58
385
  }
59
386
 
@@ -76,15 +403,335 @@ function trpcOptions(g, cfg, servicesDir) {
76
403
  };
77
404
  }
78
405
 
406
+ // src/generator-registry.ts
407
+ var VALIDATOR_DEFAULT_DIRS = {
408
+ zod: "src/validators/zod",
409
+ valibot: "src/validators/valibot",
410
+ arktype: "src/validators/arktype",
411
+ typebox: "src/validators/typebox",
412
+ effect: "src/validators/effect",
413
+ "json-schema": "src/validators/json-schema"
414
+ };
415
+ var SERVICES_DEFAULT_DIR = "src/services";
416
+ function resolveServicesDir(cfg) {
417
+ return cfg.generators.find((g) => g.kind === "service")?.path ?? SERVICES_DEFAULT_DIR;
418
+ }
419
+ var GENERATORS = [
420
+ {
421
+ kind: "orpc",
422
+ specifier: "@drzl/generator-orpc",
423
+ load: () => import("@drzl/generator-orpc"),
424
+ construct: (m, analysis) => new m.ORPCGenerator(analysis),
425
+ // `cfg.outDir` and never `g.path`: see `orpcOptions` for why that is this generator's own
426
+ // arrangement rather than an oversight to correct here.
427
+ outputDir: (_g, cfg) => cfg.outDir,
428
+ options: (g, cfg, ctx) => orpcOptions(g, cfg, ctx.servicesDir)
429
+ },
430
+ {
431
+ kind: "trpc",
432
+ // An optional dependency, like seven others below. A package that has never been published
433
+ // cannot publish through npm's trusted-publisher OIDC flow, so its first version has to go out
434
+ // by hand, and naming it as a hard dependency of the CLI in the same release breaks
435
+ // `npm i @drzl/cli` for everyone until it exists. A missing optional dependency is skipped by
436
+ // the installer rather than failing it, which is why these really can be absent on an ordinary
437
+ // install, and why `loadGenerator` tells absence apart from failure.
438
+ specifier: "@drzl/generator-trpc",
439
+ load: () => import("./dist-ZIHNXQ7U.js"),
440
+ construct: (m, analysis) => new m.TRPCGenerator(analysis),
441
+ outputDir: (g, cfg) => trpcOutDir(g, cfg),
442
+ options: (g, cfg, ctx) => trpcOptions(g, cfg, ctx.servicesDir)
443
+ },
444
+ {
445
+ kind: "hono",
446
+ specifier: "@drzl/generator-hono",
447
+ load: () => import("./dist-P24ILO5N.js"),
448
+ construct: (m, analysis) => new m.HonoGenerator(analysis),
449
+ outputDir: (g, cfg) => honoOutDir(g, cfg),
450
+ options: (g, cfg) => honoOptions(g, cfg)
451
+ },
452
+ {
453
+ kind: "express",
454
+ specifier: "@drzl/generator-express",
455
+ load: () => import("./dist-T5376MW7.js"),
456
+ construct: (m, analysis) => new m.ExpressGenerator(analysis),
457
+ outputDir: (g, cfg) => expressOutDir(g, cfg),
458
+ options: (g, cfg) => expressOptions(g, cfg)
459
+ },
460
+ {
461
+ kind: "fastify",
462
+ specifier: "@drzl/generator-fastify",
463
+ load: () => import("./dist-KQMPKOFK.js"),
464
+ construct: (m, analysis) => new m.FastifyGenerator(analysis),
465
+ outputDir: (g, cfg) => fastifyOutDir(g, cfg),
466
+ options: (g, cfg) => fastifyOptions(g, cfg)
467
+ },
468
+ {
469
+ kind: "nestjs",
470
+ specifier: "@drzl/generator-nestjs",
471
+ load: () => import("./dist-SGI2I53L.js"),
472
+ construct: (m, analysis) => new m.NestJSGenerator(analysis),
473
+ outputDir: (g, cfg) => nestjsOutDir(g, cfg),
474
+ options: (g, cfg) => nestjsOptions(g, cfg)
475
+ },
476
+ {
477
+ kind: "graphql",
478
+ specifier: "@drzl/generator-graphql",
479
+ load: () => import("./dist-UVP6B4XJ.js"),
480
+ construct: (m, analysis) => new m.GraphQLGenerator(analysis),
481
+ outputDir: (g, cfg) => graphqlOutDir(g, cfg),
482
+ options: (g, cfg) => graphqlOptions(g, cfg)
483
+ },
484
+ {
485
+ kind: "service",
486
+ specifier: "@drzl/generator-service",
487
+ load: () => import("@drzl/generator-service"),
488
+ construct: (m, analysis) => new m.ServiceGenerator(analysis),
489
+ outputDir: (g) => g.path ?? SERVICES_DEFAULT_DIR,
490
+ options: (g, _cfg, ctx) => serviceOptions(g, ctx.outDir)
491
+ },
492
+ {
493
+ kind: "zod",
494
+ specifier: "@drzl/generator-zod",
495
+ load: () => import("@drzl/generator-zod"),
496
+ construct: (m, analysis) => new m.ZodGenerator(analysis),
497
+ outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS.zod,
498
+ // `meta` is zod-only; see `GeneratorCapabilities.meta` for why it is not passed to the other
499
+ // four rather than being passed and ignored.
500
+ options: (g, cfg, ctx) => validationOptions(g, cfg, ctx.outDir, {
501
+ schemaTypes: true,
502
+ meta: true,
503
+ constraints: true
504
+ })
505
+ },
506
+ {
507
+ kind: "valibot",
508
+ specifier: "@drzl/generator-valibot",
509
+ load: () => import("@drzl/generator-valibot"),
510
+ construct: (m, analysis) => new m.ValibotGenerator(analysis),
511
+ outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS.valibot,
512
+ options: (g, cfg, ctx) => validationOptions(g, cfg, ctx.outDir, { schemaTypes: true, constraints: true })
513
+ },
514
+ {
515
+ kind: "arktype",
516
+ specifier: "@drzl/generator-arktype",
517
+ load: () => import("@drzl/generator-arktype"),
518
+ construct: (m, analysis) => new m.ArkTypeGenerator(analysis),
519
+ outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS.arktype,
520
+ options: (g, cfg, ctx) => validationOptions(g, cfg, ctx.outDir, { schemaTypes: false })
521
+ },
522
+ {
523
+ kind: "typebox",
524
+ specifier: "@drzl/generator-typebox",
525
+ load: () => import("@drzl/generator-typebox"),
526
+ construct: (m, analysis) => new m.TypeBoxGenerator(analysis),
527
+ outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS.typebox,
528
+ options: (g, cfg, ctx) => validationOptions(g, cfg, ctx.outDir, { schemaTypes: true, standardSchema: true })
529
+ },
530
+ {
531
+ kind: "effect",
532
+ specifier: "@drzl/generator-effect",
533
+ load: () => import("./dist-KX62ETKK.js"),
534
+ construct: (m, analysis) => new m.EffectGenerator(analysis),
535
+ outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS.effect,
536
+ options: (g, cfg, ctx) => validationOptions(g, cfg, ctx.outDir, { schemaTypes: true })
537
+ },
538
+ {
539
+ kind: "json-schema",
540
+ specifier: "@drzl/generator-json-schema",
541
+ load: () => import("./dist-QYH7DRFY.js"),
542
+ construct: (m, analysis) => new m.JsonSchemaGenerator(analysis),
543
+ outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS["json-schema"],
544
+ options: (g, cfg, ctx) => jsonSchemaOptions(g, cfg, ctx.outDir)
545
+ }
546
+ ];
547
+ var GENERATOR_BY_KIND = new Map(
548
+ GENERATORS.map((entry) => [entry.kind, entry])
549
+ );
550
+ function entryFor(kind) {
551
+ const entry = GENERATOR_BY_KIND.get(kind);
552
+ if (!entry) throw new Error(`No generator is registered for kind "${kind}".`);
553
+ return entry;
554
+ }
555
+ function filesOf(result) {
556
+ return Array.isArray(result) ? result : result.files;
557
+ }
558
+ async function runGenerator(entry, g, cfg, ctx) {
559
+ return runGeneratorWithOptions(entry, ctx.analysis, {
560
+ ...entry.options(g, cfg, {
561
+ outDir: entry.outputDir(g, cfg),
562
+ servicesDir: ctx.servicesDir
563
+ }),
564
+ ...ctx.fileSink ? { fileSink: ctx.fileSink } : {},
565
+ ...ctx.onProgress ? { onProgress: ctx.onProgress } : {}
566
+ });
567
+ }
568
+ async function runGeneratorWithOptions(entry, analysis, options) {
569
+ const module = await loadGenerator(entry.specifier, entry.load);
570
+ return filesOf(await entry.construct(module, analysis).generate(options));
571
+ }
572
+
573
+ // src/kind-selection.ts
574
+ var PIPELINE_PREFIX = "generate-";
575
+ var KindSelectionError = class extends Error {
576
+ constructor(code, message, hint) {
577
+ super(message);
578
+ this.code = code;
579
+ this.hint = hint;
580
+ this.name = "KindSelectionError";
581
+ }
582
+ };
583
+ function kindList() {
584
+ return GENERATOR_KINDS.join(", ");
585
+ }
586
+ function isKind(value) {
587
+ return GENERATOR_KINDS.includes(value);
588
+ }
589
+ function parseOnly(value, flag = "--only") {
590
+ if (value === void 0 || value === null) return void 0;
591
+ const requested = String(value).split(",").map((part) => part.trim()).filter(Boolean);
592
+ if (!requested.length) {
593
+ throw new KindSelectionError(
594
+ "DRZL_CLI_ONLY",
595
+ `${flag} was given no kind. Pass one or more of: ${kindList()}.`
596
+ );
597
+ }
598
+ const kinds = /* @__PURE__ */ new Set();
599
+ for (const name of requested) {
600
+ if (isKind(name)) {
601
+ kinds.add(name);
602
+ continue;
603
+ }
604
+ const bare = name.startsWith(PIPELINE_PREFIX) ? name.slice(PIPELINE_PREFIX.length) : "";
605
+ throw new KindSelectionError(
606
+ "DRZL_CLI_ONLY",
607
+ `${flag}: there is no generator kind "${name}".`,
608
+ isKind(bare) ? `Write it the way the config does: ${flag} ${bare}.` : `Valid kinds are: ${kindList()}.`
609
+ );
610
+ }
611
+ return kinds;
612
+ }
613
+ function resolveWatchSelection(opts) {
614
+ const only = parseOnly(opts.only);
615
+ const pipeline = opts.pipeline === void 0 || opts.pipeline === null ? "all" : String(opts.pipeline);
616
+ if (pipeline === "analyze") {
617
+ if (only) {
618
+ throw new KindSelectionError(
619
+ "DRZL_CLI_ONLY",
620
+ "--pipeline analyze runs no generator, so it cannot be combined with --only.",
621
+ "Drop one of the two."
622
+ );
623
+ }
624
+ return { analyzeOnly: true };
625
+ }
626
+ if (pipeline === "all") return { analyzeOnly: false, kinds: only };
627
+ if (only) {
628
+ throw new KindSelectionError(
629
+ "DRZL_CLI_ONLY",
630
+ "--pipeline and --only say the same thing, so passing both is ambiguous.",
631
+ `Use --only ${[...only].join(",")} on its own; --pipeline is the older spelling.`
632
+ );
633
+ }
634
+ const bare = pipeline.startsWith(PIPELINE_PREFIX) ? pipeline.slice(PIPELINE_PREFIX.length) : "";
635
+ if (!isKind(bare)) {
636
+ throw new KindSelectionError(
637
+ "DRZL_CLI_ONLY",
638
+ `--pipeline: there is no pipeline called "${pipeline}".`,
639
+ // A bare kind is the mirror image of the mistake `parseOnly` names, and the answer is the
640
+ // flag that takes bare kinds rather than the list of sixteen values this one takes.
641
+ isKind(pipeline) ? `That is a generator kind, so it goes to the newer flag: --only ${pipeline}.` : `Use --only <kind>, or one of: all, analyze, ${GENERATOR_KINDS.map(
642
+ (k) => PIPELINE_PREFIX + k
643
+ ).join(", ")}.`
644
+ );
645
+ }
646
+ return { analyzeOnly: false, kinds: /* @__PURE__ */ new Set([bare]) };
647
+ }
648
+ function selectGenerators(generators, kinds) {
649
+ if (!kinds) return [...generators];
650
+ return generators.filter((g) => kinds.has(g.kind));
651
+ }
652
+ function emptySelectionMessage(kinds, configured, flag = "--only") {
653
+ if (!kinds || selectGenerators(configured, kinds).length) return void 0;
654
+ const asked = [...kinds].join(", ");
655
+ const names = [...new Set(configured.map((g) => g.kind))];
656
+ return `${flag} ${asked} matched no generator in this config, which names: ${names.join(", ") || "none"}.`;
657
+ }
658
+
659
+ // src/schema-outcome.ts
660
+ var SCHEMA_UNREADABLE_CODE = "DRZL_SCHEMA_001";
661
+ var SCHEMA_EMPTY_CODE = "DRZL_SCHEMA_002";
662
+ var SCHEMA_FILTERED_CODE = "DRZL_SCHEMA_003";
663
+ var NOTHING_GENERATED = "Nothing was generated.";
664
+ var UNREADABLE_CODES = /* @__PURE__ */ new Set(["DRZL_ANL_NOFILE", "DRZL_ANL_IMPORT"]);
665
+ function firstLine(message) {
666
+ return String(message).split("\n")[0].trim();
667
+ }
668
+ function describeSchemaTarget(schema) {
669
+ if (typeof schema === "string") return schema;
670
+ if (schema.length === 1) return schema[0];
671
+ return `${schema.length} schema files`;
672
+ }
673
+ function schemaLoadFailure(issues, schema, consequence = NOTHING_GENERATED) {
674
+ const blocking = issues.filter(
675
+ (issue) => issue.level === "error" && issue.code && UNREADABLE_CODES.has(issue.code)
676
+ );
677
+ if (!blocking.length) return void 0;
678
+ const first = blocking[0];
679
+ const more = blocking.length > 1 ? ` (and ${blocking.length - 1} more)` : "";
680
+ const single = typeof schema === "string" ? schema : schema.length === 1 ? schema[0] : void 0;
681
+ if (first.code === "DRZL_ANL_NOFILE") {
682
+ const named = single ?? afterPrefix(first.message, "Schema file not found:");
683
+ return {
684
+ code: SCHEMA_UNREADABLE_CODE,
685
+ message: `Schema file not found (${SCHEMA_UNREADABLE_CODE}): ${named}${more}`,
686
+ hint: 'Check the "schema" path in your drzl config, or point --config at another one. ' + consequence
687
+ };
688
+ }
689
+ const reason = single ? firstLine(afterPrefix(first.message, "Failed to import schema:")) : firstLine(String(first.message ?? ""));
690
+ const message = single ? `Could not load the schema module ${single} (${SCHEMA_UNREADABLE_CODE}): ${reason}${more}` : `Could not load a schema module (${SCHEMA_UNREADABLE_CODE}): ${reason}${more}`;
691
+ return {
692
+ code: SCHEMA_UNREADABLE_CODE,
693
+ message,
694
+ hint: single ? `Fix that error and run again. \`drzl analyze ${single}\` prints it in full. ${consequence}` : `Fix that error and run again. ${consequence}`
695
+ };
696
+ }
697
+ function afterPrefix(message, prefix) {
698
+ const text = String(message ?? "");
699
+ return text.startsWith(prefix) ? text.slice(prefix.length).trim() : text;
700
+ }
701
+ function nothingToGenerate(opts) {
702
+ if (opts.remaining.length > 0) return void 0;
703
+ const target = describeSchemaTarget(opts.schema);
704
+ const consequence = opts.consequence ?? NOTHING_GENERATED;
705
+ if (!opts.analyzed.length) {
706
+ return {
707
+ code: SCHEMA_EMPTY_CODE,
708
+ message: `No Drizzle tables found in ${target} (${SCHEMA_EMPTY_CODE}).`,
709
+ hint: "That module imported cleanly and exported no tables, so every generator would write an empty barrel. Export them from it, for example: export const users = pgTable(...). " + consequence
710
+ };
711
+ }
712
+ const names = opts.analyzed.map((table) => table.name);
713
+ const shown = names.slice(0, 6).join(", ");
714
+ const rest = names.length > 6 ? `, and ${names.length - 6} more` : "";
715
+ return {
716
+ code: SCHEMA_FILTERED_CODE,
717
+ message: `Every table was removed by this config's filters (${SCHEMA_FILTERED_CODE}). ${target} declares ${names.length} table${names.length === 1 ? "" : "s"}: ${shown}${rest}.`,
718
+ hint: 'Check "include" and "exclude" in your drzl config. A pattern is matched against the whole database table name, with * as the only metacharacter. ' + consequence
719
+ };
720
+ }
721
+
722
+ // src/column-filter.ts
723
+ import { parseCheck as parseCheck2 } from "@drzl/validation-core";
724
+
79
725
  // src/doctor.ts
80
- import { parseCheck } from "@drzl/validation-core";
81
- import chalk from "chalk";
726
+ import { lengthMeasure, parseCheck } from "@drzl/validation-core";
727
+ import { Chalk as Chalk2 } from "chalk";
728
+ var PLAIN = new Chalk2({ level: 0 });
82
729
  var HANDLED_CODES = /* @__PURE__ */ new Set(["DRZL_ANL_UNKNOWN_COLUMN"]);
83
- function splitPath(path5) {
84
- if (!path5) return {};
85
- const dot = path5.lastIndexOf(".");
86
- if (dot <= 0) return { table: path5 };
87
- return { table: path5.slice(0, dot), column: path5.slice(dot + 1) };
730
+ function splitPath(path8) {
731
+ if (!path8) return {};
732
+ const dot = path8.lastIndexOf(".");
733
+ if (dot <= 0) return { table: path8 };
734
+ return { table: path8.slice(0, dot), column: path8.slice(dot + 1) };
88
735
  }
89
736
  function namedColumns(parsed) {
90
737
  const out = [];
@@ -92,6 +739,7 @@ function namedColumns(parsed) {
92
739
  for (const s of parsed.sets ?? []) out.push({ column: s.column, scalar: true });
93
740
  for (const l of parsed.lengths ?? []) out.push({ column: l.column, scalar: false });
94
741
  for (const c of parsed.cardinalities ?? []) out.push({ column: c.column, scalar: false });
742
+ for (const n of parsed.nulls ?? []) out.push({ column: n.column, scalar: false });
95
743
  for (const r of parsed.rows ?? []) {
96
744
  out.push({ column: r.left, scalar: false });
97
745
  out.push({ column: r.right, scalar: false });
@@ -120,6 +768,19 @@ function describeShape(c) {
120
768
  return "a structured";
121
769
  }
122
770
  }
771
+ var countNoun = (l) => l.unit === "bytes" ? "byte count" : "character count";
772
+ function countHint(c) {
773
+ if (c.shape?.kind === "byteString")
774
+ return "A binary(n)/varbinary(n) column hands the caller a string produced by a lossy decode, so its width is code points coming out and bytes going in and neither is a count of the value in hand. The column already caps itself at n bytes; a second bound stated here would be a different measurement. Leave this one to the database.";
775
+ return "Only constraints whose meaning is unambiguous are translated, because a validator enforcing a guess rejects rows the database accepts. Your database still enforces this one; nothing DRZL emits does.";
776
+ }
777
+ function declineHint(reason) {
778
+ if (/combined with/.test(reason))
779
+ return "Postgres computes numeric arithmetic exactly and JavaScript computes it in binary floating point, so `x + y <= 0.3` accepts (0.1, 0.2) in the database and rejects it in JavaScript. The right translation depends on whether the columns are numeric, double precision or bigint, and the expression does not say. Put the result in a generated column and constrain that, or leave this one to the database.";
780
+ if (/\bOR\b/.test(reason))
781
+ return "A disjunction is read only where the whole of it pins one column to a set of values, such as `status = 'a' OR status = 'b'`, which becomes the same enum an IN list does. Anything else is refused whole rather than in part: a row satisfying the other branch is one the database accepts, and enforcing one branch would turn it away.";
782
+ return "Only constraints whose meaning is unambiguous are translated, because a validator enforcing a guess rejects rows the database accepts. Your database still enforces this one; nothing DRZL emits does.";
783
+ }
123
784
  function checkFindings(table) {
124
785
  const out = [];
125
786
  const byName = new Map(table.columns.map((c) => [c.name, c]));
@@ -135,10 +796,34 @@ function checkFindings(table) {
135
796
  table: table.tsName,
136
797
  constraint: k.name,
137
798
  message: `CHECK ${label} on "${table.tsName}" is not translated: ${parsed.reason}. Expression: ${expr}`,
138
- hint: "Only constraints whose meaning is unambiguous are translated, because a validator enforcing a guess rejects rows the database accepts. Your database still enforces this one; nothing DRZL emits does."
799
+ hint: declineHint(parsed.reason)
139
800
  });
140
801
  continue;
141
802
  }
803
+ for (const n of parsed.nulls ?? []) {
804
+ if (n.notNull) continue;
805
+ out.push({
806
+ kind: "check-declined",
807
+ level: "warn",
808
+ table: table.tsName,
809
+ constraint: k.name,
810
+ message: `CHECK ${label} on "${table.tsName}" holds "${n.column} IS NULL", which narrows the column to NULL alone and no generated schema states. Expression: ${expr}`,
811
+ hint: "A column that may only ever be NULL is usually a constraint written the wrong way round. Drop the column, or state the rule as a CHECK on the column that decides it."
812
+ });
813
+ }
814
+ for (const l of parsed.lengths ?? []) {
815
+ const col = byName.get(l.column);
816
+ if (!col || lengthMeasure(col, l)) continue;
817
+ out.push({
818
+ kind: "check-uncountable",
819
+ level: "warn",
820
+ table: table.tsName,
821
+ column: l.column,
822
+ constraint: k.name,
823
+ message: `CHECK ${label} on "${table.tsName}" counts ${describeShape(col)} column "${l.column}", whose ${countNoun(l)} in JavaScript is not the one the database took, so it is not translated. Expression: ${expr}`,
824
+ hint: countHint(col)
825
+ });
826
+ }
142
827
  const seen = /* @__PURE__ */ new Set();
143
828
  for (const { column, scalar } of namedColumns(parsed)) {
144
829
  if (seen.has(column)) continue;
@@ -250,7 +935,7 @@ var SECTIONS = [
250
935
  why: "These get a validator that accepts any value."
251
936
  },
252
937
  {
253
- kinds: ["check-declined", "check-unknown-column", "check-not-scalar"],
938
+ kinds: ["check-declined", "check-unknown-column", "check-not-scalar", "check-uncountable"],
254
939
  title: "CHECK constraints DRZL does not enforce",
255
940
  why: "Your database still enforces these. Nothing DRZL generates does."
256
941
  },
@@ -279,7 +964,8 @@ function wrap(text, indent, first = indent, width = 96) {
279
964
  if (line) lines.push(line);
280
965
  return lines.map((l, i) => (i === 0 ? first : indent) + l).join("\n");
281
966
  }
282
- function renderDoctorReport(report) {
967
+ function renderDoctorReport(report, style = PLAIN) {
968
+ const chalk = style;
283
969
  const out = [];
284
970
  const plural = (n, one) => `${n} ${one}${n === 1 ? "" : "s"}`;
285
971
  out.push(chalk.bold(`DRZL doctor ${report.schema}`));
@@ -334,87 +1020,1520 @@ function renderDoctorReport(report) {
334
1020
  return out.join("\n");
335
1021
  }
336
1022
 
337
- // src/drift.ts
338
- import { promises as fs } from "fs";
339
- import path from "path";
340
- async function snapshotDir(dir) {
341
- const out = /* @__PURE__ */ new Map();
342
- async function walk(current) {
343
- let entries;
344
- try {
345
- entries = await fs.readdir(current, { withFileTypes: true });
346
- } catch {
347
- return;
1023
+ // src/column-filter.ts
1024
+ function checkedColumns(expression, name) {
1025
+ const parsed = parseCheck2(expression, name);
1026
+ if (!parsed.ok) return [];
1027
+ return [...new Set(namedColumns(parsed).map((n) => n.column))];
1028
+ }
1029
+ function filterColumns(tables, spec) {
1030
+ const entries = Object.entries(spec ?? {});
1031
+ if (!entries.length) return { tables, warnings: [] };
1032
+ const errors = [];
1033
+ const warnings = [];
1034
+ const nameForConfig = hasNamedSchemas(tables) ? addressableName : displayTableName;
1035
+ for (const [tablePattern, rules] of entries) {
1036
+ const matched = tables.filter((t) => matchesTable([tablePattern], t));
1037
+ if (!matched.length) {
1038
+ errors.push(
1039
+ `columns[${JSON.stringify(tablePattern)}] matches no table. The schema declares: ${tables.map(nameForConfig).join(", ") || "(no tables)"}.`
1040
+ );
1041
+ continue;
348
1042
  }
349
- for (const e of entries) {
350
- const full = path.join(current, e.name);
351
- if (e.isDirectory()) await walk(full);
352
- else out.set(path.relative(dir, full), await fs.readFile(full, "utf8"));
1043
+ const available = [...new Set(matched.flatMap((t) => t.columns.map((c) => c.name)))];
1044
+ for (const which of ["pick", "omit"]) {
1045
+ for (const pattern of rules[which] ?? []) {
1046
+ if (available.some((name) => matchesAny([pattern], name))) continue;
1047
+ errors.push(
1048
+ `columns[${JSON.stringify(tablePattern)}].${which} names ${JSON.stringify(pattern)}, which matches no column of ${matched.map(nameForConfig).join(", ")}. Available: ${available.join(", ")}.`
1049
+ );
1050
+ }
353
1051
  }
354
1052
  }
355
- await walk(dir);
356
- return out;
357
- }
358
- async function snapshotAll(dirs) {
359
- const all = /* @__PURE__ */ new Map();
360
- for (const dir of dirs) {
361
- for (const [rel, content] of await snapshotDir(dir)) {
362
- all.set(path.join(dir, rel), content);
1053
+ warnings.push(
1054
+ ...ambiguousPatternWarnings(
1055
+ entries.map(([p]) => p),
1056
+ tables,
1057
+ "columns"
1058
+ )
1059
+ );
1060
+ const out = tables.map((table) => {
1061
+ const mine = entries.filter(([pattern]) => matchesTable([pattern], table));
1062
+ if (!mine.length) return table;
1063
+ let keep = table.columns;
1064
+ for (const [, rules] of mine) {
1065
+ if (rules.pick?.length) keep = keep.filter((c) => matchesAny(rules.pick, c.name));
1066
+ if (rules.omit?.length) keep = keep.filter((c) => !matchesAny(rules.omit, c.name));
1067
+ }
1068
+ if (keep.length === table.columns.length) return table;
1069
+ const kept = new Set(keep.map((c) => c.name));
1070
+ const dropped = table.columns.filter((c) => !kept.has(c.name));
1071
+ if (!keep.length) {
1072
+ errors.push(
1073
+ `columns leaves table "${displayTableName(table)}" with no columns at all. An empty schema describes no row, so this is never a narrower API. Exclude the table instead, with the top-level "exclude" option.`
1074
+ );
1075
+ return table;
363
1076
  }
1077
+ const lostKey = (table.primaryKey?.columns ?? []).filter((n) => !kept.has(n));
1078
+ if (lostKey.length) {
1079
+ errors.push(
1080
+ `columns drops ${lostKey.map((n) => JSON.stringify(n)).join(", ")} from table "${displayTableName(table)}", which is part of its primary key (${table.primaryKey?.columns.join(", ")}). The generated getById, update and delete address rows by that key, so the emitted schemas would describe a row nothing can address. Keep the key, or leave the whole table out with the top-level "exclude" option.`
1081
+ );
1082
+ return table;
1083
+ }
1084
+ for (const c of dropped) {
1085
+ if (c.nullable || c.hasDefault || c.isGenerated || table.readOnly) continue;
1086
+ warnings.push(
1087
+ `drzl config: the "columns" option drops "${c.name}" from table "${displayTableName(table)}", and the database requires it: NOT NULL with no default. The emitted insert schema therefore describes a payload that is not a complete row, so whatever calls db.insert has to supply "${c.name}" itself.`
1088
+ );
1089
+ }
1090
+ for (const k of table.checks ?? []) {
1091
+ const lost = checkedColumns(k.expression, k.name).filter(
1092
+ (n) => !kept.has(n) && table.columns.some((c) => c.name === n)
1093
+ );
1094
+ if (!lost.length) continue;
1095
+ warnings.push(
1096
+ `drzl config: CHECK ${k.name ? `"${k.name}"` : "(unnamed)"} on table "${displayTableName(table)}" names ${lost.map((n) => JSON.stringify(n)).join(", ")}, which the "columns" option drops, so nothing DRZL emits enforces it. Your database still does.`
1097
+ );
1098
+ }
1099
+ return {
1100
+ ...table,
1101
+ columns: keep,
1102
+ unique: (table.unique ?? []).filter((k) => k.columns.every((n) => kept.has(n))),
1103
+ indexes: (table.indexes ?? []).filter((i) => i.columns.every((n) => kept.has(n))),
1104
+ ...table.foreignKeys ? { foreignKeys: table.foreignKeys.filter((f) => f.columns.every((n) => kept.has(n))) } : {}
1105
+ };
1106
+ });
1107
+ if (errors.length) {
1108
+ throw new Error(
1109
+ `drzl config: the "columns" option cannot be honoured.
1110
+ ` + errors.map((e) => ` - ${e}`).join("\n")
1111
+ );
364
1112
  }
365
- return all;
1113
+ return { tables: out, warnings };
366
1114
  }
367
- function diffSnapshots(before, after) {
368
- const out = [];
369
- for (const [file, content] of after) {
370
- if (!before.has(file)) out.push({ file, status: "added" });
371
- else if (before.get(file) !== content) out.push({ file, status: "changed" });
1115
+
1116
+ // src/explain.ts
1117
+ import { qualifiedForeignTable, qualifiedTableName } from "@drzl/analyzer";
1118
+ import { tableConstraints } from "@drzl/validation-core";
1119
+ import { Chalk as Chalk3 } from "chalk";
1120
+ var PLAIN2 = new Chalk3({ level: 0 });
1121
+ function namesOf(table) {
1122
+ const [bare, qualified] = tableAliases(table);
1123
+ return { qualified, name: bare, tsName: table.tsName };
1124
+ }
1125
+ var MATCH_ORDER = ["qualified", "name", "tsName"];
1126
+ var MATCH_LABELS = {
1127
+ qualified: "the schema-qualified name",
1128
+ name: "the database name",
1129
+ tsName: "the export name"
1130
+ };
1131
+ function hitsFor(tables, query, fold) {
1132
+ const wanted = fold(query);
1133
+ const hits = [];
1134
+ for (const table of tables) {
1135
+ const names = namesOf(table);
1136
+ const matchedOn = MATCH_ORDER.find((key) => fold(names[key]) === wanted);
1137
+ if (matchedOn) hits.push({ table, matchedOn });
372
1138
  }
373
- for (const file of before.keys()) {
374
- if (!after.has(file)) out.push({ file, status: "removed" });
1139
+ return hits;
1140
+ }
1141
+ var same = (s) => s;
1142
+ var folded = (s) => s.toLowerCase();
1143
+ function matchTable(tables, query) {
1144
+ for (const [exact, fold] of [
1145
+ [true, same],
1146
+ [false, folded]
1147
+ ]) {
1148
+ const hits = hitsFor(tables, query, fold);
1149
+ if (hits.length === 1) return { kind: "found", exact, ...hits[0] };
1150
+ if (hits.length > 1) return { kind: "ambiguous", exact, hits };
375
1151
  }
376
- return out.sort((a, b) => a.file.localeCompare(b.file));
1152
+ const known = tables.flatMap((t) => {
1153
+ const names = namesOf(t);
1154
+ return t.tsName === names.name ? [names.name] : [names.name, names.tsName];
1155
+ });
1156
+ return { kind: "none", suggestion: nearestKey(query, known) };
1157
+ }
1158
+ function renderLiteral(value) {
1159
+ if (typeof value === "string") return `'${value.replace(/'/g, "\\'")}'`;
1160
+ if (value === null) return "null";
1161
+ if (typeof value === "bigint") return `${value}n`;
1162
+ if (value instanceof Date) return value.toISOString();
1163
+ if (typeof value === "object") return JSON.stringify(value);
1164
+ return String(value);
1165
+ }
1166
+ function defaultOf(column) {
1167
+ if (column.defaultValue !== void 0) return { kind: "literal", value: column.defaultValue };
1168
+ if (column.defaultExpression) return { kind: "expression", text: column.defaultExpression };
1169
+ return column.hasDefault ? { kind: "runtime" } : null;
1170
+ }
1171
+ function describeDefault(value) {
1172
+ if (!value) return "";
1173
+ if (value.kind === "literal") return `default ${renderLiteral(value.value)}`;
1174
+ if (value.kind === "expression") return `default ${value.text}`;
1175
+ return "has default";
1176
+ }
1177
+ function describeShape2(shape) {
1178
+ switch (shape.kind) {
1179
+ case "buffer":
1180
+ return "binary payload, carried as a Uint8Array";
1181
+ case "json":
1182
+ return "any JSON value, checked recursively";
1183
+ case "tuple":
1184
+ return `tuple of ${shape.length} numbers`;
1185
+ case "numberObject":
1186
+ return `object of numbers: ${shape.fields.join(", ")}`;
1187
+ case "numberVector":
1188
+ return shape.length ? `numeric vector of ${shape.length}` : "numeric vector";
1189
+ case "custom":
1190
+ return shape.sqlType ? `customType, declared ${shape.sqlType}, with no runtime shape to read` : "customType, with no runtime shape to read";
1191
+ case "bitstring":
1192
+ if (shape.length === void 0) return "string of 0 and 1";
1193
+ return shape.exact ? `string of ${shape.length} digits, each 0 or 1` : `string of at most ${shape.length} digits, each 0 or 1`;
1194
+ case "byteString":
1195
+ return shape.length ? `bytes, declared width ${shape.length}` : "bytes";
1196
+ }
1197
+ }
1198
+ function capReason(column, narrowedBySet) {
1199
+ if (column.shape)
1200
+ return `"${column.name}" is a structured column, whose value space is not stated as a width`;
1201
+ if (narrowedBySet)
1202
+ return `a CHECK narrows "${column.name}" to a set of literals, which states its value space instead`;
1203
+ if (column.enumValues?.length)
1204
+ return `"${column.name}" is an enum, and its members state its value space instead`;
1205
+ if (column.tsType !== "string")
1206
+ return `"${column.name}" does not arrive as a string, so there is nothing to measure`;
1207
+ if (column.format)
1208
+ return `the ${column.format} format replaces the width on "${column.name}" rather than adding to it`;
1209
+ return `the generated schemas state "${column.name}" some other way`;
1210
+ }
1211
+ function factsFor(column, opts) {
1212
+ const facts = [];
1213
+ const state = (text) => facts.push({ text, stated: true });
1214
+ if (column.arrayDimensions) {
1215
+ state(
1216
+ column.arrayDimensions === 1 ? "an array of the type above" : `an array of ${column.arrayDimensions} dimensions`
1217
+ );
1218
+ }
1219
+ if (column.shape) state(describeShape2(column.shape));
1220
+ if (column.enumValues?.length) {
1221
+ state(`one of ${column.enumValues.map((v) => `'${v}'`).join(", ")}`);
1222
+ }
1223
+ if (column.format) state(`text in the ${column.format} format the database parses`);
1224
+ if (column.min !== void 0 && column.max !== void 0) {
1225
+ state(`${column.min} to ${column.max}`);
1226
+ } else if (column.min !== void 0) state(`at least ${column.min}`);
1227
+ else if (column.max !== void 0) state(`at most ${column.max}`);
1228
+ if (column.integer === true) state("whole numbers only");
1229
+ if (column.integer === false) state("fractions allowed");
1230
+ if (column.allowsNaN !== void 0) {
1231
+ state(column.allowsNaN ? "NaN is stored and returned" : "NaN is refused");
1232
+ }
1233
+ if (column.allowsInfinity !== void 0) {
1234
+ state(column.allowsInfinity ? "Infinity is stored and returned" : "Infinity is refused");
1235
+ }
1236
+ for (const [value2, text] of [
1237
+ [column.maxLength, `at most ${column.maxLength} characters`],
1238
+ [column.maxBytes, `at most ${column.maxBytes} bytes`]
1239
+ ]) {
1240
+ if (value2 === void 0) continue;
1241
+ facts.push(
1242
+ opts.capStated ? { text, stated: true } : { text, stated: false, reason: capReason(column, opts.narrowedBySet) }
1243
+ );
1244
+ }
1245
+ const value = defaultOf(column);
1246
+ if (value?.kind === "literal") state(`defaults to ${renderLiteral(value.value)}`);
1247
+ else if (value?.kind === "expression") state(`defaults to ${value.text}, evaluated by the database`);
1248
+ else if (value?.kind === "runtime") {
1249
+ facts.push({
1250
+ text: "has a default",
1251
+ stated: false,
1252
+ reason: "the value is produced at insert time, by the database or by a Drizzle function, so the field is optional on insert and no schema states what it becomes"
1253
+ });
1254
+ }
1255
+ if (column.isGenerated) {
1256
+ state("generated by the database, so it is left out of insert and update schemas");
1257
+ }
1258
+ return facts;
1259
+ }
1260
+ function issueTouches(issue, table) {
1261
+ if (!issue.path) return false;
1262
+ const names = namesOf(table);
1263
+ const own = [names.qualified, names.name, names.tsName, table.name];
1264
+ if (own.includes(issue.path)) return true;
1265
+ const dot = issue.path.lastIndexOf(".");
1266
+ return dot > 0 && own.includes(issue.path.slice(0, dot));
1267
+ }
1268
+ function issueColumn(issue, table) {
1269
+ const path8 = issue.path ?? "";
1270
+ const names = namesOf(table);
1271
+ for (const prefix of [names.qualified, names.tsName, names.name, table.name]) {
1272
+ if (path8.startsWith(`${prefix}.`)) {
1273
+ const rest = path8.slice(prefix.length + 1);
1274
+ if (table.columns.some((c) => c.name === rest)) return rest;
1275
+ }
1276
+ }
1277
+ return void 0;
1278
+ }
1279
+ var RELATION_CODES = /* @__PURE__ */ new Set(["DRZL_ANL_RELATIONS", "DRZL_ANL_REL_V2"]);
1280
+ function gapsFor(table, constraints, issues) {
1281
+ const gaps = [];
1282
+ for (const constraint of constraints) {
1283
+ for (const part of constraint.unenforced ?? []) {
1284
+ gaps.push({
1285
+ kind: "check",
1286
+ subject: constraint.name ?? constraint.id,
1287
+ // `part.part` already carries the constraint name where the declaration had one, because
1288
+ // that is the text an emitted schema would have attached. The renderer prefixes `subject`
1289
+ // only when it is not already there, so a named CHECK is not announced twice.
1290
+ message: `${part.part} is not enforced: ${part.reason}.`,
1291
+ hint: "Your database still enforces it. Nothing DRZL generates does."
1292
+ });
1293
+ }
1294
+ }
1295
+ for (const issue of issues) {
1296
+ if (issue.level === "info") continue;
1297
+ if (!issueTouches(issue, table)) continue;
1298
+ const subject = issueColumn(issue, table);
1299
+ gaps.push({
1300
+ kind: RELATION_CODES.has(issue.code) ? "relation" : subject ? "column" : "analyzer",
1301
+ ...subject ? { subject } : {},
1302
+ message: issue.message,
1303
+ ...issue.hint ? { hint: issue.hint } : {}
1304
+ });
1305
+ }
1306
+ return gaps;
1307
+ }
1308
+ function explainTable(analysis, match, options = {}) {
1309
+ const table = match.table;
1310
+ const qualified = qualifiedTableName(table);
1311
+ const constraints = tableConstraints(table).constraints;
1312
+ const capped = new Set(
1313
+ constraints.filter((c) => c.kind === "maxLength" || c.kind === "maxBytes").flatMap((c) => c.columns)
1314
+ );
1315
+ const narrowedBySet = new Set(
1316
+ constraints.filter((c) => c.values).map((c) => c.values.column)
1317
+ );
1318
+ const primaryKeyColumns = new Set(table.primaryKey?.columns ?? []);
1319
+ const singleColumnUnique = new Set(
1320
+ (table.unique ?? []).filter((u) => u.columns.length === 1).map((u) => u.columns[0])
1321
+ );
1322
+ const columns = table.columns.map((column) => ({
1323
+ name: column.name,
1324
+ tsType: column.tsType,
1325
+ dbType: column.dbType,
1326
+ ...column.sqlType ? { sqlType: column.sqlType } : {},
1327
+ nullable: column.nullable,
1328
+ hasDefault: column.hasDefault,
1329
+ default: defaultOf(column),
1330
+ isGenerated: column.isGenerated,
1331
+ inPrimaryKey: primaryKeyColumns.has(column.name),
1332
+ unique: singleColumnUnique.has(column.name),
1333
+ ...column.references ? { references: column.references } : {},
1334
+ ...column.enumValues ? { enumValues: column.enumValues } : {},
1335
+ ...column.arrayDimensions ? { arrayDimensions: column.arrayDimensions } : {},
1336
+ ...column.shape ? { shape: column.shape } : {},
1337
+ facts: factsFor(column, {
1338
+ capStated: capped.has(column.name),
1339
+ narrowedBySet: narrowedBySet.has(column.name)
1340
+ })
1341
+ }));
1342
+ const relations = analysis.relations.filter((r) => r.from === qualified || r.to === qualified || r.via === qualified).map((r) => ({ ...r, outgoing: r.from === qualified }));
1343
+ const keyColumns = table.columns.filter((c) => primaryKeyColumns.has(c.name));
1344
+ const primaryKey = table.primaryKey?.columns.length ? {
1345
+ ...table.primaryKey.name ? { name: table.primaryKey.name } : {},
1346
+ columns: [...table.primaryKey.columns],
1347
+ generated: keyColumns.length > 0 && keyColumns.every((c) => c.isGenerated || c.hasDefault)
1348
+ } : null;
1349
+ const removed = options.keptColumns ? table.columns.map((c) => c.name).filter((name) => !options.keptColumns.includes(name)) : [];
1350
+ return {
1351
+ name: table.name,
1352
+ tsName: table.tsName,
1353
+ ...table.schema ? { schema: table.schema } : {},
1354
+ qualified,
1355
+ addressable: addressableName(table),
1356
+ readOnly: !!table.readOnly,
1357
+ matchedOn: match.matchedOn,
1358
+ matchedExactly: match.exact,
1359
+ ...options.keptTables && !options.keptTables.includes(qualified) ? { excludedByConfig: true } : {},
1360
+ ...removed.length ? { columnsRemovedByConfig: removed } : {},
1361
+ columns,
1362
+ primaryKey,
1363
+ unique: (table.unique ?? []).map((u) => ({
1364
+ ...u.name ? { name: u.name } : {},
1365
+ columns: [...u.columns]
1366
+ })),
1367
+ indexes: (table.indexes ?? []).map((i) => ({
1368
+ ...i.name ? { name: i.name } : {},
1369
+ columns: [...i.columns]
1370
+ })),
1371
+ foreignKeys: (table.foreignKeys ?? []).map((fk) => ({
1372
+ ...fk.name ? { name: fk.name } : {},
1373
+ columns: [...fk.columns],
1374
+ references: { table: qualifiedForeignTable(fk), columns: [...fk.foreignColumns] },
1375
+ ...fk.onDelete ? { onDelete: fk.onDelete } : {},
1376
+ ...fk.onUpdate ? { onUpdate: fk.onUpdate } : {}
1377
+ })),
1378
+ relations,
1379
+ constraints,
1380
+ gaps: gapsFor(table, constraints, analysis.issues)
1381
+ };
1382
+ }
1383
+ function summarize(analysis) {
1384
+ return analysis.tables.map((table) => ({
1385
+ name: table.name,
1386
+ tsName: table.tsName,
1387
+ ...table.schema ? { schema: table.schema } : {},
1388
+ qualified: qualifiedTableName(table),
1389
+ columns: table.columns.length,
1390
+ checks: table.checks?.length ?? 0,
1391
+ gaps: gapsFor(table, tableConstraints(table).constraints, analysis.issues).length
1392
+ }));
1393
+ }
1394
+ var WIDTH = 80;
1395
+ var pad = (text, width) => text + " ".repeat(Math.max(0, width - text.length));
1396
+ var widest = (values) => values.reduce((n, v) => Math.max(n, v.length), 0);
1397
+ function wrap2(text, indent, first = indent) {
1398
+ const lines = [];
1399
+ let line = "";
1400
+ for (const word of String(text).split(/\s+/)) {
1401
+ if (line && `${line} ${word}`.length + indent.length > WIDTH) {
1402
+ lines.push(line);
1403
+ line = word;
1404
+ } else {
1405
+ line = line ? `${line} ${word}` : word;
1406
+ }
1407
+ }
1408
+ if (line) lines.push(line);
1409
+ return lines.map((l, i) => (i === 0 ? first : indent) + l).join("\n");
1410
+ }
1411
+ function renderTsType(column) {
1412
+ return column.tsType + "[]".repeat(column.arrayDimensions ?? 0);
1413
+ }
1414
+ function columnNotes(column) {
1415
+ const notes = [];
1416
+ if (column.inPrimaryKey) notes.push("pk");
1417
+ if (column.unique) notes.push("unique");
1418
+ if (column.references) {
1419
+ notes.push(`fk -> ${column.references.table}.${column.references.column}`);
1420
+ }
1421
+ if (column.isGenerated) notes.push("generated");
1422
+ const value = describeDefault(column.default);
1423
+ if (value && !column.isGenerated) notes.push(value);
1424
+ return notes.join(", ");
1425
+ }
1426
+ function constraintLines(constraint, style, labelWidth) {
1427
+ const label = constraint.name ?? "";
1428
+ const verdict = constraint.enforced ? style.green("enforced") : style.yellow("not enforced by any generated schema");
1429
+ const out = [` ${pad(label, labelWidth)} ${constraint.rule}`];
1430
+ out.push(` ${" ".repeat(labelWidth)} ${verdict}`);
1431
+ for (const part of constraint.unenforced ?? []) {
1432
+ out.push(style.dim(wrap2(part.reason, " ".repeat(labelWidth + 4))));
1433
+ }
1434
+ return out;
1435
+ }
1436
+ function renderExplanation(explanation, context, style = PLAIN2) {
1437
+ const out = [];
1438
+ const plural = (n, one) => `${n} ${one}${n === 1 ? "" : "s"}`;
1439
+ out.push(style.bold(explanation.qualified) + style.dim(` ${context.schema}`));
1440
+ const identity = [
1441
+ context.dialect,
1442
+ `table "${explanation.name}"`,
1443
+ `export "${explanation.tsName}"`,
1444
+ plural(explanation.columns.length, "column")
1445
+ ];
1446
+ if (explanation.readOnly) identity.push("read-only, so no insert or update schema is emitted");
1447
+ out.push(style.dim(" " + identity.join(", ")));
1448
+ if (!explanation.matchedExactly) {
1449
+ out.push(style.dim(` matched on ${MATCH_LABELS[explanation.matchedOn]}, ignoring case`));
1450
+ }
1451
+ if (explanation.excludedByConfig) {
1452
+ out.push("");
1453
+ out.push(style.yellow(" This config's include/exclude removes this table."));
1454
+ out.push(style.dim(" No generator sees it, so nothing below reaches any emitted file."));
1455
+ }
1456
+ if (explanation.columnsRemovedByConfig?.length) {
1457
+ out.push("");
1458
+ out.push(
1459
+ style.yellow(
1460
+ ` This config's columns filter removes ${explanation.columnsRemovedByConfig.length} of these columns: ${explanation.columnsRemovedByConfig.join(", ")}.`
1461
+ )
1462
+ );
1463
+ }
1464
+ out.push("");
1465
+ out.push(style.bold("Columns"));
1466
+ const tsTypes = explanation.columns.map(renderTsType);
1467
+ const nameWidth = widest(["COLUMN", ...explanation.columns.map((c) => c.name)]);
1468
+ const tsWidth = widest(["TS TYPE", ...tsTypes]);
1469
+ const sqlWidth = widest(["SQL TYPE", ...explanation.columns.map((c) => c.sqlType ?? c.dbType)]);
1470
+ out.push(
1471
+ style.dim(
1472
+ ` ${pad("COLUMN", nameWidth)} ${pad("TS TYPE", tsWidth)} ${pad("SQL TYPE", sqlWidth)} NULL`
1473
+ )
1474
+ );
1475
+ explanation.columns.forEach((column, i) => {
1476
+ const sql = column.sqlType ?? column.dbType;
1477
+ const notes = columnNotes(column);
1478
+ const nullable = column.nullable ? "yes" : "no";
1479
+ out.push(
1480
+ ` ${pad(column.name, nameWidth)} ${pad(tsTypes[i], tsWidth)} ${pad(sql, sqlWidth)} ` + // Padded only when something follows it: a trailing run of spaces on every second row is
1481
+ // invisible in a terminal and is the first thing a test diff shows.
1482
+ (notes ? `${pad(nullable, 4)} ${style.dim(notes)}` : nullable)
1483
+ );
1484
+ });
1485
+ const withFacts = explanation.columns.filter((c) => c.facts.length);
1486
+ if (withFacts.length) {
1487
+ out.push("");
1488
+ out.push(style.bold("What the generators read off each column"));
1489
+ const factWidth = widest(withFacts.map((c) => c.name));
1490
+ for (const column of withFacts) {
1491
+ let first = true;
1492
+ for (const fact of column.facts) {
1493
+ const label = first ? pad(column.name, factWidth) : " ".repeat(factWidth);
1494
+ first = false;
1495
+ out.push(` ${label} ${fact.stated ? fact.text : style.yellow(fact.text)}`);
1496
+ if (fact.stated) continue;
1497
+ out.push(
1498
+ style.dim(
1499
+ wrap2(
1500
+ `not stated by any generated schema: ${fact.reason}`,
1501
+ " ".repeat(factWidth + 4)
1502
+ )
1503
+ )
1504
+ );
1505
+ }
1506
+ }
1507
+ }
1508
+ out.push("");
1509
+ out.push(style.bold("Keys"));
1510
+ if (explanation.primaryKey) {
1511
+ const pk = explanation.primaryKey;
1512
+ out.push(
1513
+ ` PRIMARY KEY (${pk.columns.join(", ")})` + (pk.generated ? style.dim(" filled in by the database") : "")
1514
+ );
1515
+ if (pk.columns.length > 1) {
1516
+ out.push(
1517
+ style.dim(
1518
+ wrap2(
1519
+ `The service and router generators key getById, update and delete on "${pk.columns[0]}" alone, so those operations match on part of this key.`,
1520
+ " "
1521
+ )
1522
+ )
1523
+ );
1524
+ }
1525
+ } else {
1526
+ out.push(style.yellow(" No primary key."));
1527
+ out.push(
1528
+ style.dim(
1529
+ wrap2(
1530
+ 'The service and router generators fall back to a column named "id".',
1531
+ " "
1532
+ )
1533
+ )
1534
+ );
1535
+ }
1536
+ for (const unique of explanation.unique) {
1537
+ out.push(` UNIQUE (${unique.columns.join(", ")})` + (unique.name ? style.dim(` ${unique.name}`) : ""));
1538
+ }
1539
+ for (const index of explanation.indexes) {
1540
+ out.push(style.dim(` INDEX (${index.columns.join(", ")})${index.name ? ` ${index.name}` : ""}`));
1541
+ }
1542
+ if (explanation.foreignKeys.length) {
1543
+ out.push("");
1544
+ out.push(style.bold("Foreign keys"));
1545
+ for (const fk of explanation.foreignKeys) {
1546
+ const actions = [
1547
+ fk.onDelete ? `ON DELETE ${fk.onDelete}` : "",
1548
+ fk.onUpdate ? `ON UPDATE ${fk.onUpdate}` : ""
1549
+ ].filter(Boolean).join(" ");
1550
+ out.push(
1551
+ ` (${fk.columns.join(", ")}) -> ${fk.references.table} (${fk.references.columns.join(", ")})` + (actions ? style.dim(` ${actions}`) : "")
1552
+ );
1553
+ }
1554
+ }
1555
+ if (explanation.relations.length) {
1556
+ out.push("");
1557
+ out.push(style.bold("Relations"));
1558
+ for (const relation of explanation.relations) {
1559
+ const via = relation.via ? ` through ${relation.via}` : "";
1560
+ out.push(
1561
+ ` ${relation.from} -> ${relation.to}${via}` + style.dim(` ${relation.kind}`)
1562
+ );
1563
+ }
1564
+ }
1565
+ const checks = explanation.constraints.filter((c) => c.kind === "check");
1566
+ if (checks.length) {
1567
+ out.push("");
1568
+ out.push(style.bold("CHECK constraints, as DRZL parsed them"));
1569
+ const labelWidth = widest(checks.map((c) => c.name ?? ""));
1570
+ for (const check of checks) out.push(...constraintLines(check, style, labelWidth));
1571
+ }
1572
+ out.push("");
1573
+ if (!explanation.gaps.length) {
1574
+ out.push(style.green("Nothing about this table was dropped or left unrecognised."));
1575
+ return out.join("\n");
1576
+ }
1577
+ out.push(style.yellow(`Not understood (${explanation.gaps.length})`));
1578
+ out.push(style.dim(" These are in your schema and are not in anything DRZL generates."));
1579
+ out.push("");
1580
+ const groups = /* @__PURE__ */ new Map();
1581
+ for (const gap of explanation.gaps) {
1582
+ const key = gap.hint ?? "";
1583
+ groups.set(key, [...groups.get(key) ?? [], gap]);
1584
+ }
1585
+ for (const [hint, items] of groups) {
1586
+ for (const gap of items) {
1587
+ const named = gap.subject && !gap.message.startsWith(gap.subject);
1588
+ out.push(wrap2((named ? `${gap.subject}: ` : "") + gap.message, " ", ` ${style.dim("-")} `));
1589
+ }
1590
+ if (hint) out.push(style.dim(wrap2(hint, " ")));
1591
+ out.push("");
1592
+ }
1593
+ return out.join("\n").replace(/\n+$/, "");
1594
+ }
1595
+ function renderIndex(tables, context, style = PLAIN2) {
1596
+ const out = [];
1597
+ const plural = (n, one) => `${n} ${one}${n === 1 ? "" : "s"}`;
1598
+ out.push(style.bold(context.schema) + style.dim(` ${context.dialect}`));
1599
+ out.push(style.dim(` ${plural(tables.length, "table")}`));
1600
+ out.push("");
1601
+ const nameWidth = widest(["TABLE", ...tables.map((t) => t.qualified)]);
1602
+ const tsWidth = widest(["EXPORT", ...tables.map((t) => t.tsName)]);
1603
+ out.push(style.dim(` ${pad("TABLE", nameWidth)} ${pad("EXPORT", tsWidth)} COLUMNS`));
1604
+ for (const table of tables) {
1605
+ const columns = String(table.columns);
1606
+ out.push(
1607
+ ` ${pad(table.qualified, nameWidth)} ${pad(table.tsName, tsWidth)} ` + (table.gaps ? `${pad(columns, 7)} ` + style.yellow(`${plural(table.gaps, "thing")} not understood`) : columns)
1608
+ );
1609
+ }
1610
+ out.push("");
1611
+ out.push(style.dim(" drzl explain <table> for one of them in full"));
1612
+ return out.join("\n");
1613
+ }
1614
+ var NO_SUCH_TABLE_CODE = "DRZL_EXPLAIN_001";
1615
+ var AMBIGUOUS_TABLE_CODE = "DRZL_EXPLAIN_002";
1616
+ var NAME_CAP = 12;
1617
+ function noSuchTableProblem(query, tables, suggestion) {
1618
+ const names = tables.map((t) => displayTableName(t));
1619
+ const shown = names.slice(0, NAME_CAP).join(", ");
1620
+ const rest = names.length > NAME_CAP ? `, and ${names.length - NAME_CAP} more` : "";
1621
+ return {
1622
+ code: NO_SUCH_TABLE_CODE,
1623
+ message: `No table called "${query}" (${NO_SUCH_TABLE_CODE}). ` + (names.length ? `This schema declares ${names.length} table${names.length === 1 ? "" : "s"}: ${shown}${rest}.` : "This schema declares no tables."),
1624
+ hint: suggestion ? `Did you mean "${suggestion}"?` : "A table is matched by its database name, by its schema-qualified name, or by the name it is exported under, ignoring case where nothing matches exactly."
1625
+ };
1626
+ }
1627
+ function ambiguousTableProblem(query, hits) {
1628
+ const named = hits.map((hit) => `${addressableName(hit.table)} (exported as ${hit.table.tsName})`).join(", ");
1629
+ return {
1630
+ code: AMBIGUOUS_TABLE_CODE,
1631
+ message: `"${query}" names ${hits.length} tables (${AMBIGUOUS_TABLE_CODE}): ${named}.`,
1632
+ hint: `Name one of them exactly, for example "${addressableName(hits[0].table)}".`
1633
+ };
1634
+ }
1635
+
1636
+ // src/drizzle-kit.ts
1637
+ import * as fs from "fs";
1638
+ import * as path from "path";
1639
+ var DRIZZLE_KIT_CONFIG_CANDIDATES = [
1640
+ "drizzle.config.ts",
1641
+ "drizzle.config.js",
1642
+ "drizzle.config.json"
1643
+ ];
1644
+ var CODE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".js", ".cjs", ".mjs", ".mts", ".cts"]);
1645
+ function findDrizzleKitConfig(cwd) {
1646
+ for (const name of DRIZZLE_KIT_CONFIG_CANDIDATES) {
1647
+ const p = path.join(cwd, name);
1648
+ if (fs.existsSync(p)) return p;
1649
+ }
1650
+ return null;
1651
+ }
1652
+ async function loadDrizzleKitConfig(p) {
1653
+ let raw;
1654
+ try {
1655
+ raw = await importFreshConfigModule(p);
1656
+ } catch (e) {
1657
+ throw new Error(
1658
+ `drzl config: failed to load the drizzle-kit config at ${p}: ${e?.message ?? e}`
1659
+ );
1660
+ }
1661
+ if (!raw || typeof raw !== "object") {
1662
+ throw new Error(`drzl config: ${p} did not export a drizzle-kit config object.`);
1663
+ }
1664
+ const record = raw;
1665
+ const schema = record.schema;
1666
+ if (schema !== void 0 && typeof schema !== "string" && !(Array.isArray(schema) && schema.every((s) => typeof s === "string"))) {
1667
+ throw new Error(
1668
+ `drzl config: "schema" in ${p} must be a string or an array of strings, matching drizzle-kit's own Config type.`
1669
+ );
1670
+ }
1671
+ return {
1672
+ path: p,
1673
+ schema,
1674
+ dialect: typeof record.dialect === "string" ? record.dialect : void 0,
1675
+ casing: typeof record.casing === "string" ? record.casing : void 0
1676
+ };
1677
+ }
1678
+ function hasGlobMagic(entry) {
1679
+ return /[*?{}[\]]/.test(entry) || /[!@+]\(/.test(entry);
1680
+ }
1681
+ function staticGlobBase(entry, cwd) {
1682
+ const segments = entry.split("/");
1683
+ const kept = [];
1684
+ for (const s of segments) {
1685
+ if (hasGlobMagic(s)) break;
1686
+ kept.push(s);
1687
+ }
1688
+ const joined = kept.join("/");
1689
+ const base = path.resolve(cwd, joined || ".");
1690
+ if (fs.existsSync(base) && fs.statSync(base).isDirectory()) return base;
1691
+ return path.dirname(base);
1692
+ }
1693
+ function filesOneLevel(dir) {
1694
+ const out = [];
1695
+ for (const name of fs.readdirSync(dir)) {
1696
+ const full = path.join(dir, name);
1697
+ if (!fs.lstatSync(full).isDirectory()) out.push(full);
1698
+ }
1699
+ return out;
1700
+ }
1701
+ function expandSchemaPaths(entries, cwd) {
1702
+ const list = typeof entries === "string" ? [entries] : entries;
1703
+ const files = /* @__PURE__ */ new Set();
1704
+ const watchDirs = /* @__PURE__ */ new Set();
1705
+ for (const entry of list) {
1706
+ if (hasGlobMagic(entry)) {
1707
+ watchDirs.add(staticGlobBase(entry, cwd));
1708
+ for (const match of fs.globSync(entry, { cwd })) {
1709
+ const full2 = path.resolve(cwd, match);
1710
+ if (fs.existsSync(full2) && fs.statSync(full2).isDirectory()) {
1711
+ for (const f of filesOneLevel(full2)) files.add(f);
1712
+ } else {
1713
+ files.add(full2);
1714
+ }
1715
+ }
1716
+ continue;
1717
+ }
1718
+ const full = path.resolve(cwd, entry);
1719
+ let stat = null;
1720
+ try {
1721
+ stat = fs.statSync(full);
1722
+ } catch {
1723
+ watchDirs.add(path.dirname(full));
1724
+ continue;
1725
+ }
1726
+ if (stat.isDirectory()) {
1727
+ watchDirs.add(full);
1728
+ for (const f of filesOneLevel(full)) files.add(f);
1729
+ } else {
1730
+ watchDirs.add(path.dirname(full));
1731
+ files.add(full);
1732
+ }
1733
+ }
1734
+ const kept = [...files].filter((f) => CODE_EXTENSIONS.has(path.extname(f).toLowerCase()));
1735
+ return { files: kept.sort(), watchDirs: [...watchDirs] };
1736
+ }
1737
+ function mapDrizzleKitDialect(declared) {
1738
+ if (!declared) return null;
1739
+ const map = {
1740
+ postgresql: "postgres",
1741
+ mysql: "mysql",
1742
+ sqlite: "sqlite",
1743
+ turso: "sqlite",
1744
+ singlestore: "singlestore",
1745
+ gel: "gel"
1746
+ };
1747
+ if (declared in map) return map[declared];
1748
+ const analyzerDialects = [
1749
+ "sqlite",
1750
+ "postgres",
1751
+ "mysql",
1752
+ "singlestore",
1753
+ "mssql",
1754
+ "cockroach",
1755
+ "gel"
1756
+ ];
1757
+ return analyzerDialects.includes(declared) ? declared : null;
1758
+ }
1759
+ function dialectMismatchWarning(args) {
1760
+ const expected = mapDrizzleKitDialect(args.declared);
1761
+ if (!expected) return null;
1762
+ if (args.analyzed === "unknown") return null;
1763
+ if (args.analyzed === expected) return null;
1764
+ return `drzl: ${args.configPath} declares dialect "${args.declared}", but the schema analyzed as "${args.analyzed}". DRZL follows the schema; if the schema files are the right ones, the dialect in that config is stale.`;
1765
+ }
1766
+ async function resolveSchemaSource(cfg, cwd = process.cwd()) {
1767
+ if (cfg.schema) {
1768
+ const warnings = [];
1769
+ if (cfg.drizzleKit === true || typeof cfg.drizzleKit === "string") {
1770
+ warnings.push(
1771
+ `drzl config: both "schema" and "drizzleKit" are set. "schema" wins, so the drizzle-kit config was not read; remove one of the two to silence this.`
1772
+ );
1773
+ }
1774
+ return {
1775
+ source: "drzl",
1776
+ schema: cfg.schema,
1777
+ watchDirs: [path.dirname(path.resolve(cwd, cfg.schema))],
1778
+ warnings
1779
+ };
1780
+ }
1781
+ if (cfg.drizzleKit === false) {
1782
+ throw new Error(
1783
+ `drzl config: no "schema" is set and "drizzleKit" is false, so the drizzle-kit fallback is disabled. Set "schema".`
1784
+ );
1785
+ }
1786
+ let configPath;
1787
+ if (typeof cfg.drizzleKit === "string") {
1788
+ configPath = path.resolve(cwd, cfg.drizzleKit);
1789
+ if (!fs.existsSync(configPath)) {
1790
+ throw new Error(`drzl config: "drizzleKit" points at ${configPath}, which does not exist.`);
1791
+ }
1792
+ } else {
1793
+ const found = findDrizzleKitConfig(cwd);
1794
+ if (!found) {
1795
+ const looked = DRIZZLE_KIT_CONFIG_CANDIDATES.join(", ");
1796
+ throw new Error(
1797
+ cfg.drizzleKit === true ? `drzl config: "drizzleKit" is set, but no drizzle-kit config was found (looked for ${looked} in ${cwd}). Create one, or point "drizzleKit" at its path.` : `drzl config: no "schema" is set and no drizzle-kit config was found (looked for ${looked} in ${cwd}). Set "schema" in your drzl config, or add "drizzleKit" naming your drizzle-kit config file.`
1798
+ );
1799
+ }
1800
+ configPath = found;
1801
+ }
1802
+ const kit = await loadDrizzleKitConfig(configPath);
1803
+ if (kit.schema === void 0) {
1804
+ throw new Error(
1805
+ `drzl config: ${configPath} has no "schema" entry, so there is nothing to analyze. Set "schema" there, or set "schema" in your drzl config.`
1806
+ );
1807
+ }
1808
+ const { files, watchDirs } = expandSchemaPaths(kit.schema, cwd);
1809
+ if (!files.length) {
1810
+ const shown = (typeof kit.schema === "string" ? [kit.schema] : kit.schema).map((s) => JSON.stringify(s)).join(", ");
1811
+ throw new Error(
1812
+ `drzl config: the "schema" patterns in ${configPath} matched no schema files: ${shown}. DRZL expands them the way drizzle-kit does; check them against your tree.`
1813
+ );
1814
+ }
1815
+ return {
1816
+ source: "drizzle-kit",
1817
+ schema: files,
1818
+ watchDirs,
1819
+ drizzleKitConfigPath: configPath,
1820
+ drizzleKitDialect: kit.dialect,
1821
+ warnings: []
1822
+ };
1823
+ }
1824
+
1825
+ // src/drift.ts
1826
+ import { promises as fs2 } from "fs";
1827
+ import path2 from "path";
1828
+ async function snapshotDir(dir) {
1829
+ const out = /* @__PURE__ */ new Map();
1830
+ async function walk(current) {
1831
+ let entries;
1832
+ try {
1833
+ entries = await fs2.readdir(current, { withFileTypes: true });
1834
+ } catch {
1835
+ return;
1836
+ }
1837
+ for (const e of entries) {
1838
+ const full = path2.join(current, e.name);
1839
+ if (e.isDirectory()) await walk(full);
1840
+ else out.set(path2.relative(dir, full), await fs2.readFile(full, "utf8"));
1841
+ }
1842
+ }
1843
+ await walk(dir);
1844
+ return out;
1845
+ }
1846
+ async function snapshotAll(dirs) {
1847
+ const all = /* @__PURE__ */ new Map();
1848
+ for (const dir of dirs) {
1849
+ for (const [rel, content] of await snapshotDir(dir)) {
1850
+ all.set(path2.join(dir, rel), content);
1851
+ }
1852
+ }
1853
+ return all;
1854
+ }
1855
+ function diffSnapshots(before, after) {
1856
+ const out = [];
1857
+ for (const [file, content] of after) {
1858
+ if (!before.has(file)) out.push({ file, status: "added" });
1859
+ else if (before.get(file) !== content) out.push({ file, status: "changed" });
1860
+ }
1861
+ for (const file of before.keys()) {
1862
+ if (!after.has(file)) out.push({ file, status: "removed" });
1863
+ }
1864
+ return out.sort((a, b) => a.file.localeCompare(b.file));
377
1865
  }
378
1866
  async function restoreSnapshot(before, after) {
379
1867
  for (const [file, content] of before) {
380
- await fs.mkdir(path.dirname(file), { recursive: true });
381
- await fs.writeFile(file, content, "utf8");
1868
+ await fs2.mkdir(path2.dirname(file), { recursive: true });
1869
+ await fs2.writeFile(file, content, "utf8");
382
1870
  }
383
1871
  for (const file of after.keys()) {
384
- if (!before.has(file)) await fs.rm(file, { force: true });
1872
+ if (!before.has(file)) await fs2.rm(file, { force: true });
385
1873
  }
386
1874
  }
387
1875
 
388
- // src/generator-loader.ts
389
- var GeneratorNotInstalledError = class extends Error {
390
- constructor(specifier, reason) {
391
- super(`${specifier} is not installed`);
392
- this.specifier = specifier;
393
- this.reason = reason;
394
- this.name = "GeneratorNotInstalledError";
1876
+ // src/emit-plan.ts
1877
+ import { promises as fs3 } from "fs";
1878
+ import path3 from "path";
1879
+ var EmitPlan = class {
1880
+ constructor(options) {
1881
+ this.byFile = /* @__PURE__ */ new Map();
1882
+ this.dirs = /* @__PURE__ */ new Set();
1883
+ this.writes = options.write;
1884
+ this.existing = options.existing;
1885
+ }
1886
+ async mkdir(dir) {
1887
+ this.dirs.add(dir);
1888
+ if (this.writes) await fs3.mkdir(dir, { recursive: true });
1889
+ }
1890
+ async writeFile(file, contents) {
1891
+ const prior = this.byFile.get(file);
1892
+ const before = prior ? prior.before : await this.read(file);
1893
+ this.byFile.set(file, {
1894
+ file,
1895
+ before,
1896
+ after: contents,
1897
+ verdict: before === null ? "created" : before === contents ? "unchanged" : "changed"
1898
+ });
1899
+ if (this.writes) await fs3.writeFile(file, contents, "utf8");
1900
+ }
1901
+ async read(file) {
1902
+ if (this.existing) return this.existing.get(file) ?? null;
1903
+ try {
1904
+ return await fs3.readFile(file, "utf8");
1905
+ } catch {
1906
+ return null;
1907
+ }
1908
+ }
1909
+ /** Every directory a generator asked for, whether or not it was created. */
1910
+ get directories() {
1911
+ return [...this.dirs];
1912
+ }
1913
+ /** Every recorded file, in the order it was first written. */
1914
+ get files() {
1915
+ return [...this.byFile.values()];
1916
+ }
1917
+ /**
1918
+ * The verdicts for a list of paths, in the order given.
1919
+ *
1920
+ * A path with no verdict is a path the generator reported writing without routing it through
1921
+ * the sink, which is the one shape a version mismatch takes: a `@drzl/cli` that knows about
1922
+ * `fileSink` beside a generator package that predates it. It is returned rather than thrown on
1923
+ * so the caller can name the generator; see `unrecorded`.
1924
+ */
1925
+ verdictsFor(paths) {
1926
+ return paths.map((p) => this.byFile.get(p));
1927
+ }
1928
+ /** The paths a generator claims to have written that never reached this sink. */
1929
+ unrecorded(paths) {
1930
+ return paths.filter((p) => !this.byFile.has(p));
1931
+ }
1932
+ counts(paths) {
1933
+ const entries = paths ? this.verdictsFor(paths).filter(Boolean) : this.files;
1934
+ const counts = { total: entries.length, created: 0, changed: 0, unchanged: 0 };
1935
+ for (const e of entries) counts[e.verdict]++;
1936
+ return counts;
1937
+ }
1938
+ };
1939
+ function describeCounts(counts) {
1940
+ const parts = [];
1941
+ if (counts.created) parts.push(`${counts.created} created`);
1942
+ if (counts.changed) parts.push(`${counts.changed} changed`);
1943
+ if (counts.unchanged) parts.push(`${counts.unchanged} unchanged`);
1944
+ return parts.join(", ") || "nothing to write";
1945
+ }
1946
+ function pendingChanges(plan) {
1947
+ return plan.files.filter((f) => f.verdict !== "unchanged").sort((a, b) => a.file.localeCompare(b.file));
1948
+ }
1949
+ function driftStatusOf(verdict) {
1950
+ return verdict === "created" ? "added" : "changed";
1951
+ }
1952
+ async function verifyNothingWasWritten(dirs, before) {
1953
+ const after = await snapshotAll(dirs);
1954
+ const drift = diffSnapshots(before, after);
1955
+ if (!drift.length) return [];
1956
+ await restoreSnapshot(before, after);
1957
+ return drift.map((d) => d.file).sort();
1958
+ }
1959
+ function displayPath(file, cwd = process.cwd()) {
1960
+ const rel = path3.relative(cwd, file);
1961
+ return rel && !rel.startsWith("..") ? rel : file;
1962
+ }
1963
+
1964
+ // src/unified-diff.ts
1965
+ var DEFAULT_DIFF_LIMITS = {
1966
+ maxLines: 4e3,
1967
+ maxEdits: 1500,
1968
+ context: 3
1969
+ };
1970
+ function toLines(text) {
1971
+ if (text === "") return { lines: [], newlineAtEnd: true };
1972
+ const newlineAtEnd = text.endsWith("\n");
1973
+ const lines = text.split("\n");
1974
+ if (newlineAtEnd) lines.pop();
1975
+ return { lines, newlineAtEnd };
1976
+ }
1977
+ function shortestEdit(a, b, maxEdits) {
1978
+ const n = a.length;
1979
+ const m = b.length;
1980
+ const max = n + m;
1981
+ const offset = max;
1982
+ const v = new Int32Array(2 * max + 1);
1983
+ const trace = [];
1984
+ const limit = Math.min(max, maxEdits);
1985
+ for (let d = 0; d <= limit; d++) {
1986
+ trace.push(Int32Array.prototype.slice.call(v));
1987
+ for (let k = -d; k <= d; k += 2) {
1988
+ let x;
1989
+ if (k === -d || k !== d && v[k - 1 + offset] < v[k + 1 + offset]) x = v[k + 1 + offset];
1990
+ else x = v[k - 1 + offset] + 1;
1991
+ let y = x - k;
1992
+ while (x < n && y < m && a[x] === b[y]) {
1993
+ x++;
1994
+ y++;
1995
+ }
1996
+ v[k + offset] = x;
1997
+ if (x >= n && y >= m) return trace;
1998
+ }
1999
+ }
2000
+ return null;
2001
+ }
2002
+ function backtrack(trace, a, b) {
2003
+ const max = a.length + b.length;
2004
+ const offset = max;
2005
+ let x = a.length;
2006
+ let y = b.length;
2007
+ const ops = [];
2008
+ for (let d = trace.length - 1; d >= 0; d--) {
2009
+ const v = trace[d];
2010
+ const k = x - y;
2011
+ let prevK;
2012
+ if (k === -d || k !== d && v[k - 1 + offset] < v[k + 1 + offset]) prevK = k + 1;
2013
+ else prevK = k - 1;
2014
+ const prevX = v[prevK + offset];
2015
+ const prevY = prevX - prevK;
2016
+ while (x > prevX && y > prevY) {
2017
+ x--;
2018
+ y--;
2019
+ ops.push({ kind: "equal", a: x, b: y });
2020
+ }
2021
+ if (d > 0) {
2022
+ if (x === prevX) {
2023
+ y--;
2024
+ ops.push({ kind: "insert", a: x, b: y });
2025
+ } else {
2026
+ x--;
2027
+ ops.push({ kind: "delete", a: x, b: y });
2028
+ }
2029
+ }
2030
+ }
2031
+ ops.reverse();
2032
+ return ops;
2033
+ }
2034
+ function diffLines(a, b, maxEdits) {
2035
+ let head = 0;
2036
+ while (head < a.length && head < b.length && a[head] === b[head]) head++;
2037
+ let tail = 0;
2038
+ while (tail < a.length - head && tail < b.length - head && a[a.length - 1 - tail] === b[b.length - 1 - tail]) {
2039
+ tail++;
2040
+ }
2041
+ const midA = a.slice(head, a.length - tail);
2042
+ const midB = b.slice(head, b.length - tail);
2043
+ let mid = [];
2044
+ if (midA.length || midB.length) {
2045
+ const trace = shortestEdit(midA, midB, maxEdits);
2046
+ if (!trace) return null;
2047
+ mid = backtrack(trace, midA, midB);
2048
+ }
2049
+ const ops = [];
2050
+ for (let i = 0; i < head; i++) ops.push({ kind: "equal", a: i, b: i });
2051
+ for (const op of mid) ops.push({ kind: op.kind, a: op.a + head, b: op.b + head });
2052
+ for (let i = 0; i < tail; i++) {
2053
+ ops.push({ kind: "equal", a: a.length - tail + i, b: b.length - tail + i });
2054
+ }
2055
+ return ops;
2056
+ }
2057
+ function unifiedDiff(before, after, opts) {
2058
+ if (before === after) return "";
2059
+ const limits = { ...DEFAULT_DIFF_LIMITS, ...opts.limits ?? {} };
2060
+ const from = toLines(before);
2061
+ const to = toLines(after);
2062
+ const beforeLines = withNoNewlineMark(from);
2063
+ const afterLines = withNoNewlineMark(to);
2064
+ if (from.lines.length > limits.maxLines || to.lines.length > limits.maxLines) {
2065
+ return `--- ${opts.fromLabel}
2066
+ +++ ${opts.toLabel}
2067
+ @@ no line diff @@
2068
+ ${from.lines.length} lines on disk, ${to.lines.length} lines regenerated. Not diffed: the file is longer than the ${limits.maxLines}-line cap.
2069
+ `;
2070
+ }
2071
+ const ops = diffLines(beforeLines, afterLines, limits.maxEdits);
2072
+ if (!ops) {
2073
+ return `--- ${opts.fromLabel}
2074
+ +++ ${opts.toLabel}
2075
+ @@ no line diff @@
2076
+ ${from.lines.length} lines on disk, ${to.lines.length} lines regenerated. Not diffed: the two differ by more than the ${limits.maxEdits}-edit cap, so the whole file is effectively new.
2077
+ `;
2078
+ }
2079
+ const hunks = buildHunks(ops, beforeLines, afterLines, limits.context);
2080
+ if (!hunks.length) return "";
2081
+ return `--- ${opts.fromLabel}
2082
+ +++ ${opts.toLabel}
2083
+ ${hunks.join("")}`;
2084
+ }
2085
+ var NO_NEWLINE = "\";
2086
+ var NO_NEWLINE_MARK = "\0\0drzl:no-newline";
2087
+ function withNoNewlineMark(side) {
2088
+ if (side.newlineAtEnd || !side.lines.length) return side.lines;
2089
+ const marked = side.lines.slice();
2090
+ marked[marked.length - 1] += NO_NEWLINE_MARK;
2091
+ return marked;
2092
+ }
2093
+ function renderLine(prefix, line, into) {
2094
+ if (line.endsWith(NO_NEWLINE_MARK)) {
2095
+ into.push(prefix + line.slice(0, -NO_NEWLINE_MARK.length));
2096
+ into.push(NO_NEWLINE);
2097
+ return;
2098
+ }
2099
+ into.push(prefix + line);
2100
+ }
2101
+ function buildHunks(ops, beforeLines, afterLines, context) {
2102
+ const changed = [];
2103
+ ops.forEach((op, i) => {
2104
+ if (op.kind !== "equal") changed.push(i);
2105
+ });
2106
+ if (!changed.length) return [];
2107
+ const ranges = [];
2108
+ for (const i of changed) {
2109
+ const start = Math.max(0, i - context);
2110
+ const end = Math.min(ops.length - 1, i + context);
2111
+ const last = ranges[ranges.length - 1];
2112
+ if (last && start <= last[1] + 1) last[1] = Math.max(last[1], end);
2113
+ else ranges.push([start, end]);
2114
+ }
2115
+ const hunks = [];
2116
+ for (const [start, end] of ranges) {
2117
+ let aStart = -1;
2118
+ let bStart = -1;
2119
+ let aCount = 0;
2120
+ let bCount = 0;
2121
+ const body = [];
2122
+ for (let i = start; i <= end; i++) {
2123
+ const op = ops[i];
2124
+ if (op.kind === "equal" || op.kind === "delete") {
2125
+ if (aStart < 0) aStart = op.a;
2126
+ aCount++;
2127
+ }
2128
+ if (op.kind === "equal" || op.kind === "insert") {
2129
+ if (bStart < 0) bStart = op.b;
2130
+ bCount++;
2131
+ }
2132
+ if (op.kind === "equal") renderLine(" ", beforeLines[op.a], body);
2133
+ else if (op.kind === "delete") renderLine("-", beforeLines[op.a], body);
2134
+ else renderLine("+", afterLines[op.b], body);
2135
+ }
2136
+ const aFrom = aCount === 0 ? 0 : aStart + 1;
2137
+ const bFrom = bCount === 0 ? 0 : bStart + 1;
2138
+ hunks.push(`@@ -${aFrom},${aCount} +${bFrom},${bCount} @@
2139
+ ${body.join("\n")}
2140
+ `);
2141
+ }
2142
+ return hunks;
2143
+ }
2144
+
2145
+ // src/watch-loop.ts
2146
+ var DEFAULT_WATCH_DEBOUNCE_MS = 200;
2147
+ function resolveDebounce(value, warn) {
2148
+ if (value === void 0 || value === null || value === "") return DEFAULT_WATCH_DEBOUNCE_MS;
2149
+ const ms = Number(value);
2150
+ if (!Number.isFinite(ms) || ms < 0) {
2151
+ warn(
2152
+ `--debounce ${String(value)} is not a number of milliseconds. Using ${DEFAULT_WATCH_DEBOUNCE_MS}ms.`
2153
+ );
2154
+ return DEFAULT_WATCH_DEBOUNCE_MS;
2155
+ }
2156
+ return ms;
2157
+ }
2158
+ function createRebuildScheduler(options) {
2159
+ const timers = options.timers ?? {
2160
+ setTimeout: (fn, ms) => setTimeout(fn, ms),
2161
+ clearTimeout: (handle2) => clearTimeout(handle2)
2162
+ };
2163
+ let handle = null;
2164
+ let running = false;
2165
+ let pending = false;
2166
+ const drain = async () => {
2167
+ if (running) {
2168
+ pending = true;
2169
+ return;
2170
+ }
2171
+ running = true;
2172
+ try {
2173
+ await options.run();
2174
+ while (pending) {
2175
+ pending = false;
2176
+ await options.run();
2177
+ }
2178
+ } finally {
2179
+ running = false;
2180
+ pending = false;
2181
+ }
2182
+ };
2183
+ return {
2184
+ trigger() {
2185
+ if (handle !== null) timers.clearTimeout(handle);
2186
+ handle = timers.setTimeout(() => {
2187
+ handle = null;
2188
+ void drain();
2189
+ }, options.debounceMs);
2190
+ },
2191
+ runNow() {
2192
+ return drain();
2193
+ },
2194
+ cancel() {
2195
+ if (handle !== null) timers.clearTimeout(handle);
2196
+ handle = null;
2197
+ },
2198
+ get busy() {
2199
+ return running;
2200
+ }
2201
+ };
2202
+ }
2203
+
2204
+ // src/init.ts
2205
+ import { SchemaAnalyzer } from "@drzl/analyzer";
2206
+ import * as fs4 from "fs";
2207
+ import * as path4 from "path";
2208
+ var INIT_GENERATOR_CHOICES = [
2209
+ { kind: "zod", packageName: "@drzl/generator-zod", label: "Zod validators" },
2210
+ { kind: "valibot", packageName: "@drzl/generator-valibot", label: "Valibot validators" },
2211
+ { kind: "arktype", packageName: "@drzl/generator-arktype", label: "ArkType validators" },
2212
+ { kind: "typebox", packageName: "@drzl/generator-typebox", label: "TypeBox validators" },
2213
+ { kind: "orpc", packageName: "@drzl/generator-orpc", label: "oRPC router" }
2214
+ ];
2215
+ var DEFAULT_GENERATOR_KIND = INIT_GENERATOR_CHOICES[0].kind;
2216
+ var ROUTER_KINDS = /* @__PURE__ */ new Set(["orpc"]);
2217
+ var SCHEMA_CANDIDATE_STEMS = [
2218
+ "src/db/schema",
2219
+ "src/db/schema/index",
2220
+ "src/db/schemas/index",
2221
+ "src/lib/db/schema",
2222
+ "src/lib/db/schema/index",
2223
+ "src/schema",
2224
+ "src/schema/index",
2225
+ "src/schemas/index",
2226
+ "app/db/schema",
2227
+ "lib/db/schema",
2228
+ "db/schema",
2229
+ "db/schema/index",
2230
+ "drizzle/schema",
2231
+ "schema"
2232
+ ];
2233
+ var CANDIDATE_EXTENSIONS = [".ts", ".js"];
2234
+ function schemaCandidates() {
2235
+ const out = [];
2236
+ for (const stem of SCHEMA_CANDIDATE_STEMS) {
2237
+ for (const ext of CANDIDATE_EXTENSIONS) out.push(`${stem}${ext}`);
2238
+ }
2239
+ return out;
2240
+ }
2241
+ async function classifySchemaCandidate(target) {
2242
+ let analysis;
2243
+ try {
2244
+ analysis = await new SchemaAnalyzer(target).analyze({
2245
+ includeRelations: false,
2246
+ validateConstraints: false
2247
+ });
2248
+ } catch (e) {
2249
+ return { verdict: "unverified", tables: 0, reason: firstLine2(String(e?.message ?? e)) };
2250
+ }
2251
+ if (analysis.tables.length > 0) {
2252
+ return { verdict: "confirmed", tables: analysis.tables.length };
2253
+ }
2254
+ const importError = analysis.issues.find(
2255
+ (i) => i.level === "error" && i.code === "DRZL_ANL_IMPORT"
2256
+ );
2257
+ if (importError)
2258
+ return { verdict: "unverified", tables: 0, reason: firstLine2(importError.message) };
2259
+ return { verdict: "rejected", tables: 0 };
2260
+ }
2261
+ function firstLine2(message) {
2262
+ return String(message).split("\n")[0].trim();
2263
+ }
2264
+ async function detectSchema(cwd) {
2265
+ const notes = [];
2266
+ let kitFiles = null;
2267
+ let kitPath;
2268
+ try {
2269
+ const source = await resolveSchemaSource({}, cwd);
2270
+ if (source.source === "drizzle-kit") {
2271
+ kitFiles = source.schema;
2272
+ kitPath = source.drizzleKitConfigPath;
2273
+ }
2274
+ } catch {
2275
+ kitFiles = null;
2276
+ }
2277
+ if (kitFiles && kitPath) {
2278
+ const rel = path4.relative(cwd, kitPath) || path4.basename(kitPath);
2279
+ const report = await classifySchemaCandidate(kitFiles);
2280
+ if (report.verdict === "confirmed" || report.verdict === "unverified") {
2281
+ notes.push(
2282
+ report.verdict === "confirmed" ? `Schema from ${rel} (${kitFiles.length} file${kitFiles.length === 1 ? "" : "s"}, ${report.tables} table${report.tables === 1 ? "" : "s"})` : `Schema from ${rel}, which DRZL could not import yet: ${report.reason}`
2283
+ );
2284
+ return {
2285
+ source: "drizzle-kit",
2286
+ drizzleKitConfig: rel,
2287
+ verdict: report.verdict,
2288
+ tables: report.tables,
2289
+ notes
2290
+ };
2291
+ }
2292
+ notes.push(`${rel} names schema files that declare no Drizzle tables; looking elsewhere.`);
2293
+ }
2294
+ const present = schemaCandidates().filter((c) => fs4.existsSync(path4.resolve(cwd, c)));
2295
+ const unverified = [];
2296
+ for (const file of present) {
2297
+ const report = await classifySchemaCandidate(path4.resolve(cwd, file));
2298
+ if (report.verdict === "confirmed") {
2299
+ notes.push(
2300
+ `Schema found at ${file} (${report.tables} table${report.tables === 1 ? "" : "s"})`
2301
+ );
2302
+ return {
2303
+ source: "convention",
2304
+ schema: file,
2305
+ verdict: "confirmed",
2306
+ tables: report.tables,
2307
+ notes
2308
+ };
2309
+ }
2310
+ if (report.verdict === "unverified") unverified.push({ file, report });
2311
+ else notes.push(`${file} exists but declares no Drizzle tables; not using it.`);
2312
+ }
2313
+ if (unverified.length) {
2314
+ const { file, report } = unverified[0];
2315
+ notes.push(`Schema assumed to be ${file}; DRZL could not import it: ${report.reason}`);
2316
+ return { source: "convention", schema: file, verdict: "unverified", tables: 0, notes };
2317
+ }
2318
+ notes.push(
2319
+ present.length ? "No file DRZL looked at declares any Drizzle tables." : "No drizzle-kit config and no schema in the usual locations."
2320
+ );
2321
+ return { source: "none", tables: 0, notes };
2322
+ }
2323
+ function generatorLine(kind) {
2324
+ if (kind === "orpc") return `{ kind: 'orpc', template: 'standard', includeRelations: true }`;
2325
+ return `{ kind: '${kind}', path: 'src/validators/${kind}' }`;
2326
+ }
2327
+ function renderInitConfig(plan) {
2328
+ const lines = [];
2329
+ lines.push(`import type { DrzlConfigInput } from '@drzl/cli/config';`);
2330
+ lines.push("");
2331
+ lines.push("export default {");
2332
+ if (plan.schema) {
2333
+ lines.push(` schema: '${plan.schema}',`);
2334
+ } else if (plan.schemaSource === "drizzle-kit") {
2335
+ lines.push(` // No "schema" here on purpose: DRZL reads it from your drizzle-kit config, so`);
2336
+ lines.push(
2337
+ ` // the path is written once. Set "schema" to override it, or "drizzleKit": false`
2338
+ );
2339
+ lines.push(` // to refuse the fallback.`);
2340
+ } else {
2341
+ lines.push(` // Set this to your Drizzle schema file, for example 'src/db/schema.ts'. DRZL`);
2342
+ lines.push(` // found no drizzle-kit config and no schema declaring tables in the usual`);
2343
+ lines.push(` // locations, and will not name a file that is not there.`);
2344
+ lines.push(` // schema: 'src/db/schema.ts',`);
2345
+ }
2346
+ const hasRouter = plan.generators.some((k) => ROUTER_KINDS.has(k));
2347
+ if (hasRouter) lines.push(` outDir: 'src/api',`);
2348
+ lines.push(` analyzer: { includeRelations: true, validateConstraints: true },`);
2349
+ lines.push(" generators: [");
2350
+ const others = INIT_GENERATOR_CHOICES.filter((c) => !plan.generators.includes(c.kind)).map((c) => `'${c.kind}'`).join(", ");
2351
+ if (others) lines.push(` // Other kinds this CLI already has installed: ${others}.`);
2352
+ if (hasRouter) {
2353
+ lines.push(' // A second router generator needs its own "path"; they share "outDir".');
2354
+ }
2355
+ for (const kind of plan.generators) lines.push(` ${generatorLine(kind)},`);
2356
+ lines.push(" ],");
2357
+ lines.push("} satisfies DrzlConfigInput;");
2358
+ return lines.join("\n") + "\n";
2359
+ }
2360
+ function isInteractive(ctx) {
2361
+ if (ctx.env.CI) return false;
2362
+ return Boolean(ctx.stdin.isTTY) && Boolean(ctx.stdout.isTTY);
2363
+ }
2364
+ async function ask(rl, question) {
2365
+ const closed = new Promise((resolve4) => rl.once("close", () => resolve4(null)));
2366
+ try {
2367
+ return await Promise.race([rl.question(question), closed]);
2368
+ } catch {
2369
+ return null;
2370
+ }
2371
+ }
2372
+ async function promptForPlan(args) {
2373
+ const { input, output, detection, cwd } = args;
2374
+ const write = (s) => output.write(s + "\n");
2375
+ let schema = args.schemaFromFlag ?? detection.schema;
2376
+ let schemaSource = args.schemaFromFlag ? "convention" : detection.source;
2377
+ let generators = args.generatorsFromFlag;
2378
+ let endedEarly = false;
2379
+ let readlineModule;
2380
+ try {
2381
+ readlineModule = await import("readline/promises");
2382
+ } catch {
2383
+ return {
2384
+ schema,
2385
+ schemaSource,
2386
+ generators: normalizeGenerators(generators) ?? [DEFAULT_GENERATOR_KIND],
2387
+ endedEarly: true
2388
+ };
2389
+ }
2390
+ const rl = readlineModule.createInterface({ input, output });
2391
+ try {
2392
+ if (args.schemaFromFlag === void 0) {
2393
+ for (const note of detection.notes) write(note);
2394
+ const prompt = detection.source === "drizzle-kit" ? "Schema file, or Enter to keep reading it from your drizzle-kit config: " : detection.schema ? `Schema file [${detection.schema}]: ` : "Schema file (Enter to leave it unset): ";
2395
+ const answer = await ask(rl, prompt);
2396
+ if (answer === null) endedEarly = true;
2397
+ else if (answer.trim()) {
2398
+ const typed = answer.trim();
2399
+ const report = await classifySchemaCandidate(path4.resolve(cwd, typed));
2400
+ if (report.verdict === "confirmed") {
2401
+ write(` ${typed}: ${report.tables} table${report.tables === 1 ? "" : "s"}`);
2402
+ } else if (report.verdict === "unverified") {
2403
+ write(` ${typed}: DRZL could not import it (${report.reason}). Using it anyway.`);
2404
+ } else {
2405
+ write(` ${typed}: no Drizzle tables found in it. Using it anyway.`);
2406
+ }
2407
+ schema = typed;
2408
+ schemaSource = "convention";
2409
+ }
2410
+ }
2411
+ if (generators === void 0 && !endedEarly) {
2412
+ write("What should DRZL generate?");
2413
+ INIT_GENERATOR_CHOICES.forEach((c, i) => write(` ${i + 1}) ${c.label}`));
2414
+ for (let attempt = 0; attempt < 3; attempt++) {
2415
+ const answer = await ask(rl, `Choice [1, ${INIT_GENERATOR_CHOICES[0].label}]: `);
2416
+ if (answer === null) {
2417
+ endedEarly = true;
2418
+ break;
2419
+ }
2420
+ const raw = answer.trim().toLowerCase();
2421
+ if (!raw) break;
2422
+ const byIndex = Number(raw);
2423
+ const picked = Number.isInteger(byIndex) && byIndex >= 1 && byIndex <= INIT_GENERATOR_CHOICES.length ? INIT_GENERATOR_CHOICES[byIndex - 1] : INIT_GENERATOR_CHOICES.find((c) => c.kind === raw);
2424
+ if (picked) {
2425
+ generators = [picked.kind];
2426
+ break;
2427
+ }
2428
+ write(` "${answer.trim()}" is not one of the choices.`);
2429
+ }
2430
+ }
2431
+ } finally {
2432
+ rl.close();
2433
+ }
2434
+ return {
2435
+ schema,
2436
+ schemaSource,
2437
+ generators: normalizeGenerators(generators) ?? [DEFAULT_GENERATOR_KIND],
2438
+ endedEarly
2439
+ };
2440
+ }
2441
+ function normalizeGenerators(kinds) {
2442
+ if (kinds === void 0) return void 0;
2443
+ const known = new Set(INIT_GENERATOR_CHOICES.map((c) => c.kind));
2444
+ for (const k of kinds) {
2445
+ if (!known.has(k)) {
2446
+ throw new Error(
2447
+ `drzl init: "${k}" is not a generator init can scaffold. Choose from ${[...known].join(", ")}. Every other kind is documented in drzl.config; the route generators are optional dependencies and may not be installed.`
2448
+ );
2449
+ }
395
2450
  }
396
- };
397
- function isPackageMissing(err, specifier) {
398
- const code = err?.code;
399
- if (code !== "ERR_MODULE_NOT_FOUND") return false;
400
- const message = err?.message;
401
- return typeof message === "string" && message.includes(`'${specifier}'`);
2451
+ const picked = INIT_GENERATOR_CHOICES.filter((c) => kinds.includes(c.kind)).map((c) => c.kind);
2452
+ return picked.length ? picked : void 0;
402
2453
  }
403
- async function loadGenerator(specifier, load) {
2454
+ function parseGeneratorsFlag(value) {
2455
+ if (value === void 0) return void 0;
2456
+ const parts = value.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
2457
+ if (!parts.length) throw new Error("drzl init: --generators was given no kinds.");
2458
+ return parts;
2459
+ }
2460
+ async function runInit(args) {
2461
+ const target = path4.resolve(args.cwd, "drzl.config.ts");
2462
+ const existing = CONFIG_FILE_NAMES.find((name) => fs4.existsSync(path4.resolve(args.cwd, name)));
2463
+ if (existing) {
2464
+ args.error(
2465
+ `drzl init: ${existing} already exists, so nothing was written. Delete it, or edit it by hand; init never overwrites a config, and will not write one that shadows it either.`
2466
+ );
2467
+ return { code: 1 };
2468
+ }
2469
+ let fromFlag;
404
2470
  try {
405
- return await load();
2471
+ fromFlag = normalizeGenerators(parseGeneratorsFlag(args.generatorsFlag));
406
2472
  } catch (e) {
407
- if (isPackageMissing(e, specifier)) throw new GeneratorNotInstalledError(specifier, e);
408
- throw e;
2473
+ args.error(String(e?.message ?? e));
2474
+ return { code: 1 };
2475
+ }
2476
+ const detection = await detectSchema(args.cwd);
2477
+ let plan;
2478
+ const interactive = !args.yes && isInteractive({ stdin: args.stdin, stdout: args.stdout, env: args.env });
2479
+ if (interactive) {
2480
+ const result = await promptForPlan({
2481
+ input: args.stdin,
2482
+ output: args.stdout,
2483
+ detection,
2484
+ cwd: args.cwd,
2485
+ schemaFromFlag: args.schemaFlag,
2486
+ generatorsFromFlag: fromFlag
2487
+ });
2488
+ plan = {
2489
+ schema: result.schema,
2490
+ schemaSource: result.schemaSource,
2491
+ generators: result.generators
2492
+ };
2493
+ } else {
2494
+ for (const note of detection.notes) args.log(note);
2495
+ plan = {
2496
+ schema: args.schemaFlag ?? detection.schema,
2497
+ schemaSource: args.schemaFlag ? "convention" : detection.source,
2498
+ generators: fromFlag ?? [DEFAULT_GENERATOR_KIND]
2499
+ };
2500
+ if (args.schemaFlag) {
2501
+ const full = path4.resolve(args.cwd, args.schemaFlag);
2502
+ if (!fs4.existsSync(full)) {
2503
+ args.log(`--schema ${args.schemaFlag} is not there yet. Writing it anyway.`);
2504
+ } else if ((await classifySchemaCandidate(full)).verdict === "rejected") {
2505
+ args.log(`--schema ${args.schemaFlag} declares no Drizzle tables. Writing it anyway.`);
2506
+ }
2507
+ }
2508
+ }
2509
+ try {
2510
+ fs4.writeFileSync(target, renderInitConfig(plan), { flag: "wx" });
2511
+ } catch (e) {
2512
+ if (e?.code === "EEXIST") {
2513
+ args.error(
2514
+ `drzl init: drzl.config.ts already exists, so nothing was written. init never overwrites a config.`
2515
+ );
2516
+ return { code: 1 };
2517
+ }
2518
+ args.error(`drzl init: could not write ${target}: ${e?.message ?? e}`);
2519
+ return { code: 1 };
409
2520
  }
2521
+ args.log(`Created ${target}`);
2522
+ args.log(` generators: ${plan.generators.join(", ")}`);
2523
+ if (plan.schema) args.log(` schema: ${plan.schema}`);
2524
+ else if (plan.schemaSource === "drizzle-kit") args.log(" schema: from your drizzle-kit config");
2525
+ else
2526
+ args.log(
2527
+ ' schema: not set. Fill in "schema" before running `drzl generate`, or add a drizzle-kit config.'
2528
+ );
2529
+ return { code: 0, written: target, plan };
410
2530
  }
411
2531
 
412
2532
  // src/sponsor.ts
413
- import chalk2 from "chalk";
414
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
415
- import path2 from "path";
416
- var CACHE_DIR = path2.join(process.cwd(), "node_modules", ".cache", "@drzl");
417
- var CACHE_FILE = path2.join(CACHE_DIR, "sponsor-message.json");
2533
+ import { existsSync as existsSync3, mkdirSync, readFileSync, writeFileSync as writeFileSync2 } from "fs";
2534
+ import path5 from "path";
2535
+ var CACHE_DIR = path5.join(process.cwd(), "node_modules", ".cache", "@drzl");
2536
+ var CACHE_FILE = path5.join(CACHE_DIR, "sponsor-message.json");
418
2537
  var DEFAULT_INTERVAL_MS = 1e3 * 60 * 15;
419
2538
  var shownThisProcess = false;
420
2539
  var tips = [
@@ -424,17 +2543,19 @@ var tips = [
424
2543
  "Need JSON Schema or OpenAPI? The json-schema generator emits both, with no runtime dependency.",
425
2544
  "Use output headers to track generated files and trim noisy diffs."
426
2545
  ];
427
- var green = (msg) => chalk2.hex("#6ee7b7")(msg);
428
- var cyan = (msg) => chalk2.cyan(msg);
429
- var gray = (msg) => chalk2.gray(msg);
430
2546
  function maybeShowSponsorMessage({
431
2547
  reason = "generate",
432
2548
  minIntervalMs = DEFAULT_INTERVAL_MS,
433
- force = false
2549
+ force = false,
2550
+ out = new Output()
434
2551
  } = {}) {
2552
+ const green = (msg) => out.errStyle.hex("#6ee7b7")(msg);
2553
+ const cyan = (msg) => out.errStyle.cyan(msg);
2554
+ const gray = (msg) => out.errStyle.gray(msg);
435
2555
  const hideViaEnv = process.env.DRZL_HIDE_SPONSOR?.toLowerCase();
436
2556
  const hideRequested = hideViaEnv === "1" || hideViaEnv === "true";
437
2557
  if (hideRequested || process.env.CI && !force || shownThisProcess && !force) return;
2558
+ if (!out.wantsAsides && !force) return;
438
2559
  try {
439
2560
  mkdirSync(CACHE_DIR, { recursive: true });
440
2561
  const payload = readCache();
@@ -449,7 +2570,7 @@ function maybeShowSponsorMessage({
449
2570
  if (!shouldShow) return;
450
2571
  shownThisProcess = true;
451
2572
  const tip = tips[payload.runs % tips.length];
452
- console.log(
2573
+ out.stderr.write(
453
2574
  `
454
2575
  ${cyan(`\u{1F680} DRZL finished a ${reason} run (#${payload.runs.toLocaleString()}).`)}
455
2576
 
@@ -457,13 +2578,14 @@ ${green("\u2728 Sponsors keep DRZL shipping. Consider supporting ongoing dev:")}
457
2578
  ${green("GitHub Sponsors")} ${gray("\u2192 https://github.com/sponsors/omar-dulaimi")}
458
2579
 
459
2580
  ${green("Pro tip:")} ${tip}
2581
+
460
2582
  `
461
2583
  );
462
2584
  } catch {
463
2585
  }
464
2586
  }
465
2587
  function readCache() {
466
- if (!existsSync(CACHE_FILE)) {
2588
+ if (!existsSync3(CACHE_FILE)) {
467
2589
  return { runs: 0 };
468
2590
  }
469
2591
  try {
@@ -475,16 +2597,16 @@ function readCache() {
475
2597
  }
476
2598
  }
477
2599
  function writeCache(payload) {
478
- writeFileSync(CACHE_FILE, JSON.stringify(payload, null, 2), "utf8");
2600
+ writeFileSync2(CACHE_FILE, JSON.stringify(payload, null, 2), "utf8");
479
2601
  }
480
2602
 
481
2603
  // src/version.ts
482
2604
  import { readFileSync as readFileSync2 } from "fs";
483
- import * as path3 from "path";
2605
+ import * as path6 from "path";
484
2606
  import { fileURLToPath } from "url";
485
2607
  var PACKAGE_NAME = "@drzl/cli";
486
2608
  function moduleDir() {
487
- return path3.dirname(fileURLToPath(import.meta.url));
2609
+ return path6.dirname(fileURLToPath(import.meta.url));
488
2610
  }
489
2611
  function readVersionFrom(manifestPath) {
490
2612
  let raw;
@@ -507,21 +2629,66 @@ function readVersionFrom(manifestPath) {
507
2629
  return manifest.version;
508
2630
  }
509
2631
  function readCliVersion() {
510
- return readVersionFrom(path3.join(moduleDir(), "..", "package.json"));
2632
+ return readVersionFrom(path6.join(moduleDir(), "..", "package.json"));
511
2633
  }
512
2634
  var CLI_VERSION = readCliVersion();
513
2635
 
514
2636
  // src/cli.ts
515
- function reportGeneratorFailure(kind, e) {
2637
+ function reportGeneratorFailure(out, kind, e) {
516
2638
  if (e instanceof GeneratorNotInstalledError) {
517
- console.error(
518
- chalk3.red(`The ${kind} generator is not installed.`),
519
- chalk3.yellow(`
520
- Install with: npm install ${e.specifier}`)
2639
+ out.error(`The ${kind} generator is not installed.`);
2640
+ out.hint(`Install with: npm install ${e.specifier}`);
2641
+ return `The ${kind} generator is not installed. Install with: npm install ${e.specifier}`;
2642
+ }
2643
+ const detail = messageOf(e);
2644
+ out.error(`The ${kind} generator failed:`, detail);
2645
+ return `The ${kind} generator failed: ${detail}`;
2646
+ }
2647
+ function outputFor(opts) {
2648
+ return new Output({ quiet: !!opts.quiet, json: !!opts.json });
2649
+ }
2650
+ function drzlErrorCode(error, fallback) {
2651
+ return error instanceof ConfigValidationError ? error.code : fallback;
2652
+ }
2653
+ function reportSchemaProblem(out, command, problem) {
2654
+ if (out.json) out.jsonData(jsonFailure(command, problem.code, problem.message));
2655
+ else {
2656
+ out.error(problem.message);
2657
+ out.hint(problem.hint);
2658
+ }
2659
+ process.exit(EXIT_FAILED);
2660
+ }
2661
+ var DIFF_FILE_CAP = 20;
2662
+ function printCheckDiffs(out, drift) {
2663
+ const shown = drift.slice(0, DIFF_FILE_CAP);
2664
+ for (const d of shown) {
2665
+ const label = displayPath(d.file);
2666
+ const text = unifiedDiff(d.before ?? "", d.after, {
2667
+ fromLabel: `a/${label}`,
2668
+ toLabel: `b/${label}`
2669
+ });
2670
+ if (!text) continue;
2671
+ out.note("");
2672
+ for (const line of text.split("\n")) {
2673
+ if (!line) continue;
2674
+ if (line.startsWith("+++") || line.startsWith("---")) out.note(out.errStyle.bold(line));
2675
+ else if (line.startsWith("@@")) out.note(out.errStyle.cyan(line));
2676
+ else if (line.startsWith("+")) out.note(out.errStyle.green(line));
2677
+ else if (line.startsWith("-")) out.note(out.errStyle.red(line));
2678
+ else out.note(out.errStyle.gray(line));
2679
+ }
2680
+ }
2681
+ if (drift.length > shown.length) {
2682
+ out.note("");
2683
+ out.note(
2684
+ out.errStyle.gray(
2685
+ `${drift.length - shown.length} more file(s) differ. Diffs are capped at ${DIFF_FILE_CAP} files; every drifted file is named in the list above.`
2686
+ )
521
2687
  );
522
- return;
523
2688
  }
524
- console.error(chalk3.red(`The ${kind} generator failed:`), e?.message ?? e);
2689
+ }
2690
+ function withOutputFlags(command) {
2691
+ return command.option("--json", "write one JSON document to stdout and nothing else", false).option("-q, --quiet", "drop the progress narration on stderr; errors still print", false);
525
2692
  }
526
2693
  var program = new Command();
527
2694
  program.name("drzl").description("DRZL - Drizzle Developer Toolkit").version(CLI_VERSION);
@@ -532,368 +2699,531 @@ Need a template, adapter, or generator DRZL doesn't ship yet?
532
2699
  \u2192 DM @omardulaimidev on X: https://x.com/omardulaimidev
533
2700
  `
534
2701
  );
535
- program.command("analyze").argument("<schema>", "path to drizzle schema (TS)").option("--relations", "include relations", true).option("--validate", "validate constraints", true).option("--out <file>", "write analysis JSON to file").option("--json", "print JSON to stdout (overrides --out)", false).action(async (schema, opts) => {
2702
+ withOutputFlags(
2703
+ program.command("analyze").argument("<schema>", "path to drizzle schema (TS)").option("--relations", "include relations", true).option("--validate", "validate constraints", true).option("--out <file>", "write analysis JSON to file")
2704
+ ).action(async (schema, opts) => {
2705
+ const out = outputFor(opts);
536
2706
  try {
537
- const analyzer = new SchemaAnalyzer(schema);
538
- const spinner = !opts.json ? ora("Analyzing schema...").start() : null;
2707
+ const analyzer = new SchemaAnalyzer2(schema);
2708
+ const spinner = out.spinner("Analyzing schema...");
539
2709
  const start = Date.now();
540
2710
  const res = await analyzer.analyze({
541
2711
  includeRelations: !!opts.relations,
542
2712
  validateConstraints: !!opts.validate
543
2713
  });
544
2714
  const ms = Date.now() - start;
545
- const json = JSON.stringify(res, null, 2);
546
- if (opts.json) {
547
- console.log(json);
548
- } else if (opts.out) {
549
- const fs2 = await import("fs/promises");
550
- await fs2.writeFile(opts.out, json, "utf8");
551
- spinner?.succeed(chalk3.green(`Analysis written to ${opts.out} in ${ms}ms`));
2715
+ const unreadable = res.issues.some(
2716
+ (i) => i.level === "error" && (i.code === "DRZL_ANL_NOFILE" || i.code === "DRZL_ANL_IMPORT")
2717
+ );
2718
+ const errors = res.issues.some((i) => i.level === "error");
2719
+ const code = unreadable ? EXIT_FAILED : errors ? EXIT_FINDINGS : EXIT_OK;
2720
+ if (opts.out && !opts.json) {
2721
+ const fs5 = await import("fs/promises");
2722
+ await fs5.writeFile(opts.out, JSON.stringify(res, null, 2), "utf8");
2723
+ spinner.succeed(`Analysis written to ${opts.out} in ${ms}ms`);
552
2724
  } else {
553
- spinner?.succeed(chalk3.green(`Analyzed in ${ms}ms`));
554
- console.log(json);
2725
+ spinner.succeed(`Analyzed in ${ms}ms`);
2726
+ const document = opts.json ? { command: "analyze", exitCode: code, ...res } : res;
2727
+ out.data(JSON.stringify(document, null, 2));
555
2728
  }
556
- process.exit(res.issues.some((i) => i.level === "error") ? 2 : 0);
2729
+ process.exit(code);
557
2730
  } catch (e) {
558
- const msg = e?.message ?? String(e);
559
- if (opts.json)
560
- console.log(JSON.stringify({ event: "error", code: "DRZL_CLI_ANALYZE", message: msg }));
561
- else
562
- console.error(
563
- chalk3.red("Analyze failed (DRZL_CLI_ANALYZE):"),
564
- msg,
565
- "\nTip: run with --json for structured output."
2731
+ const msg = messageOf(e);
2732
+ if (opts.json) out.jsonData(jsonFailure("analyze", "DRZL_CLI_ANALYZE", msg));
2733
+ else {
2734
+ out.error("Analyze failed (DRZL_CLI_ANALYZE):", msg);
2735
+ out.hint("Tip: run with --json for structured output.");
2736
+ }
2737
+ process.exit(EXIT_FAILED);
2738
+ }
2739
+ });
2740
+ withOutputFlags(
2741
+ program.command("doctor").description("Report what DRZL cannot type or enforce in your schema, and why").argument("[schema]", "path to drizzle schema (TS); defaults to the schema in drzl.config").option("-c, --config <path>", "path to drzl.config, read when no schema argument is given").option("--strict", "exit 2 when anything is reported", false)
2742
+ ).action(async (schema, opts) => {
2743
+ const out = outputFor(opts);
2744
+ {
2745
+ try {
2746
+ let target = schema;
2747
+ if (!target) {
2748
+ const cfg = await loadConfig(opts.config, (w) => out.warn(w));
2749
+ if (cfg) target = (await resolveSchemaSource(cfg)).schema;
2750
+ }
2751
+ if (!target) {
2752
+ const msg = "No schema given. Pass a path, or run from a directory with a drzl.config.";
2753
+ if (opts.json) out.jsonData(jsonFailure("doctor", "DRZL_CLI_DOCTOR", msg));
2754
+ else out.error("Doctor failed (DRZL_CLI_DOCTOR):", msg);
2755
+ process.exit(EXIT_FAILED);
2756
+ return;
2757
+ }
2758
+ const analyzer = new SchemaAnalyzer2(target);
2759
+ const analysis = await analyzer.analyze({
2760
+ includeRelations: true,
2761
+ validateConstraints: true
2762
+ });
2763
+ const report = buildDoctorReport(
2764
+ analysis,
2765
+ Array.isArray(target) ? target.join(", ") : target
566
2766
  );
567
- process.exit(1);
2767
+ const unreadable = report.findings.some((f) => f.level === "error");
2768
+ const code = unreadable ? EXIT_FAILED : opts.strict && report.findings.length ? EXIT_FINDINGS : EXIT_OK;
2769
+ if (opts.json)
2770
+ out.data(JSON.stringify({ command: "doctor", exitCode: code, ...report }, null, 2));
2771
+ else out.data(renderDoctorReport(report, out.outStyle));
2772
+ process.exit(code);
2773
+ } catch (e) {
2774
+ const msg = messageOf(e);
2775
+ const code = drzlErrorCode(e, "DRZL_CLI_DOCTOR");
2776
+ if (opts.json) out.jsonData(jsonFailure("doctor", code, msg));
2777
+ else if (e instanceof ConfigValidationError) {
2778
+ out.error(msg);
2779
+ } else {
2780
+ out.error("Doctor failed (DRZL_CLI_DOCTOR):", msg);
2781
+ out.hint("Tip: run with --json for structured output.");
2782
+ }
2783
+ process.exit(EXIT_FAILED);
2784
+ }
568
2785
  }
569
2786
  });
570
- program.command("doctor").description("Report what DRZL cannot type or enforce in your schema, and why").argument("[schema]", "path to drizzle schema (TS); defaults to the schema in drzl.config").option("-c, --config <path>", "path to drzl.config, read when no schema argument is given").option("--json", "print the report as JSON instead of prose", false).option("--strict", "exit 2 when anything is reported", false).action(async (schema, opts) => {
571
- try {
572
- let target = schema;
573
- if (!target) {
574
- const cfg = await loadConfig(opts.config);
575
- target = cfg?.schema;
2787
+ async function explainSchemaSource(opts, out) {
2788
+ if (opts.schema) return { schema: opts.schema, label: opts.schema };
2789
+ const cfg = await loadConfig(opts.config, (w) => out.warn(w));
2790
+ if (cfg) {
2791
+ const source = await resolveSchemaSource(cfg);
2792
+ for (const w of source.warnings) out.warn(w);
2793
+ return {
2794
+ schema: source.schema,
2795
+ label: describeSchemaTarget(source.schema),
2796
+ config: cfg,
2797
+ ...source.source === "drizzle-kit" && source.drizzleKitConfigPath ? { note: `Schema from ${path7.relative(process.cwd(), source.drizzleKitConfigPath)}` } : {}
2798
+ };
2799
+ }
2800
+ const detected = await detectSchema(process.cwd());
2801
+ if (!detected.schema) return void 0;
2802
+ return {
2803
+ schema: detected.schema,
2804
+ label: detected.schema,
2805
+ note: detected.notes[detected.notes.length - 1]
2806
+ };
2807
+ }
2808
+ withOutputFlags(
2809
+ program.command("explain").description("Show what DRZL understood about one table, and what it did not").argument(
2810
+ "[table]",
2811
+ "the table to explain, by database name, qualified name or export name; omit for the list"
2812
+ ).option("-c, --config <path>", "path to drzl.config, read when --schema is not given").option("-s, --schema <path>", "path to the schema, overriding the config")
2813
+ ).action(async (tableName, opts) => {
2814
+ const out = outputFor(opts);
2815
+ const fail = (problem) => {
2816
+ if (out.json) out.jsonData(jsonFailure("explain", problem.code, problem.message));
2817
+ else {
2818
+ out.error(problem.message);
2819
+ out.hint(problem.hint);
576
2820
  }
577
- if (!target) {
578
- const msg = "No schema given. Pass a path, or run from a directory with a drzl.config.";
579
- if (opts.json)
580
- console.log(JSON.stringify({ event: "error", code: "DRZL_CLI_DOCTOR", message: msg }));
581
- else console.error(chalk3.red("Doctor failed (DRZL_CLI_DOCTOR):"), msg);
582
- process.exit(1);
2821
+ process.exit(EXIT_FAILED);
2822
+ };
2823
+ try {
2824
+ const source = await explainSchemaSource(opts, out);
2825
+ if (!source) {
2826
+ fail({
2827
+ code: "DRZL_CFG_001",
2828
+ message: "No schema found (DRZL_CFG_001). There is no drzl.config, no drizzle-kit config, and no schema in the usual locations.",
2829
+ hint: "Pass --schema <path>, or run `drzl init` to write a config."
2830
+ });
583
2831
  return;
584
2832
  }
585
- const analyzer = new SchemaAnalyzer(target);
586
- const analysis = await analyzer.analyze({
2833
+ if (source.note) out.note(out.errStyle.gray(source.note));
2834
+ const spinner = out.spinner("Reading the schema...");
2835
+ const analysis = await new SchemaAnalyzer2(source.schema).analyze({
2836
+ // Both on, for the reason `doctor` turns both on: this command's job is to say everything
2837
+ // that is known, and a relation that appears only under a flag is one a reader would be
2838
+ // told is absent.
587
2839
  includeRelations: true,
588
2840
  validateConstraints: true
589
2841
  });
590
- const report = buildDoctorReport(analysis, target);
591
- if (opts.json) console.log(JSON.stringify(report, null, 2));
592
- else console.log(renderDoctorReport(report));
593
- if (report.findings.some((f) => f.level === "error")) {
594
- process.exit(1);
595
- return;
2842
+ spinner.stop();
2843
+ const problem = schemaLoadFailure(analysis.issues, source.schema, "There is nothing to explain.") ?? nothingToGenerate({
2844
+ schema: source.schema,
2845
+ analyzed: analysis.tables,
2846
+ remaining: analysis.tables,
2847
+ consequence: "There is nothing to explain."
2848
+ });
2849
+ if (problem) reportSchemaProblem(out, "explain", problem);
2850
+ const context = { schema: source.label, dialect: analysis.dialect };
2851
+ if (!tableName) {
2852
+ const tables = summarize(analysis);
2853
+ if (out.json) out.jsonData({ command: "explain", exitCode: EXIT_OK, ...context, tables });
2854
+ else out.data(renderIndex(tables, context, out.outStyle));
2855
+ process.exit(EXIT_OK);
2856
+ }
2857
+ const match = matchTable(analysis.tables, tableName);
2858
+ if (match.kind === "ambiguous") fail(ambiguousTableProblem(tableName, match.hits));
2859
+ if (match.kind === "none") {
2860
+ fail(noSuchTableProblem(tableName, analysis.tables, match.suggestion));
2861
+ }
2862
+ const cfg = source.config;
2863
+ let keptTables;
2864
+ let keptColumns;
2865
+ if (cfg) {
2866
+ keptTables = filterTables(analysis.tables, cfg).map((t) => qualifiedTableName2(t));
2867
+ try {
2868
+ const narrowed = filterColumns(
2869
+ [match.table],
2870
+ cfg.columns
2871
+ );
2872
+ keptColumns = narrowed.tables[0]?.columns.map((c) => c.name);
2873
+ } catch {
2874
+ keptColumns = void 0;
2875
+ }
596
2876
  }
597
- process.exit(opts.strict && report.findings.length ? 2 : 0);
2877
+ const explanation = explainTable(
2878
+ analysis,
2879
+ match,
2880
+ { keptTables, keptColumns }
2881
+ );
2882
+ if (out.json) {
2883
+ out.jsonData({ command: "explain", exitCode: EXIT_OK, ...context, table: explanation });
2884
+ } else {
2885
+ out.data(renderExplanation(explanation, context, out.outStyle));
2886
+ }
2887
+ process.exit(EXIT_OK);
598
2888
  } catch (e) {
599
- const msg = e?.message ?? String(e);
600
- if (opts.json)
601
- console.log(JSON.stringify({ event: "error", code: "DRZL_CLI_DOCTOR", message: msg }));
602
- else
603
- console.error(
604
- chalk3.red("Doctor failed (DRZL_CLI_DOCTOR):"),
605
- msg,
606
- "\nTip: run with --json for structured output."
607
- );
608
- process.exit(1);
2889
+ const msg = messageOf(e);
2890
+ const code = drzlErrorCode(e, "DRZL_CLI_EXPLAIN");
2891
+ if (opts.json) out.jsonData(jsonFailure("explain", code, msg));
2892
+ else if (e instanceof ConfigValidationError) out.error(msg);
2893
+ else {
2894
+ out.error("Explain failed (DRZL_CLI_EXPLAIN):", msg);
2895
+ out.hint("Tip: run with --json for structured output.");
2896
+ }
2897
+ process.exit(EXIT_FAILED);
609
2898
  }
610
2899
  });
611
- program.command("generate").description("Run configured generators (drzl.config.*)").option("-c, --config <path>", "path to drzl.config").option(
612
- "--check",
613
- "regenerate and fail if the result differs from what is on disk, without changing it"
2900
+ withOutputFlags(
2901
+ program.command("generate").description("Run configured generators (drzl.config.*)").option("-c, --config <path>", "path to drzl.config").option("-s, --schema <path>", "path to the schema, overriding the config").option(
2902
+ "--only <kinds>",
2903
+ `run only these generator kinds, comma separated: ${kindList()}`
2904
+ ).option(
2905
+ "--check",
2906
+ "regenerate and fail if the result differs from what is on disk, without changing it"
2907
+ ).option("--dry-run", "report what would be written, and write nothing", false)
614
2908
  ).action(async (opts) => {
615
- try {
616
- const cfg = await loadConfig(opts.config);
617
- if (!cfg) {
618
- console.error(
619
- chalk3.red("No config found (DRZL_CFG_001). Create drzl.config.ts or pass --config.")
620
- );
621
- process.exit(2);
622
- return;
623
- }
624
- const analyzer = new SchemaAnalyzer(cfg.schema);
625
- const spinner = ora("Analyzing...").start();
626
- const t0 = Date.now();
627
- const analysis = await analyzer.analyze({
628
- includeRelations: cfg.analyzer.includeRelations,
629
- validateConstraints: cfg.analyzer.validateConstraints,
630
- includeHeuristicRelations: cfg.analyzer.includeHeuristicRelations
631
- });
632
- analysis.tables = filterTables(analysis.tables, cfg);
633
- spinner.succeed(`Analysis complete in ${Date.now() - t0}ms`);
634
- reportWideColumns(analysis.issues);
635
- const driftDirs = computeGeneratorOutputDirs(cfg);
636
- const driftBefore = opts.check ? await snapshotAll(driftDirs) : null;
637
- const progress = new cliProgress.SingleBar(
638
- { hideCursor: true },
639
- cliProgress.Presets.shades_classic
640
- );
641
- const total = analysis.tables.length || 1;
642
- progress.start(total, 0);
643
- const servicesDir = cfg.generators.find((x) => x.kind === "service")?.path ?? "src/services";
644
- for (const g of cfg.generators) {
645
- if (g.kind === "orpc") {
646
- const gen = new ORPCGenerator(analysis);
647
- const { files } = await gen.generate({
648
- outputDir: cfg.outDir,
649
- template: g.template,
650
- includeRelations: g.includeRelations,
651
- naming: g.naming,
652
- outputHeader: g.outputHeader,
653
- format: g.format,
654
- templateOptions: g.templateOptions,
655
- importExtension: g.importExtension,
656
- validation: g.validation,
657
- // Documented on this generator since it was added and never reachable from a config
658
- // file, because the config schema had no such key and zod stripped it in silence.
659
- databaseInjection: g.databaseInjection,
660
- servicesDir,
661
- onProgress: ({ index }) => progress.update(index)
662
- });
663
- progress.stop();
664
- ora().succeed(chalk3.green(`Generated (${g.kind}): ${files.length} files`));
665
- files.forEach((f) => console.log(" -", chalk3.cyan(f)));
666
- } else if (g.kind === "trpc") {
667
- try {
668
- const { TRPCGenerator } = await loadGenerator(
669
- "@drzl/generator-trpc",
670
- () => import("./dist-XBGVORL3.js")
671
- );
672
- const gen = new TRPCGenerator(analysis);
673
- const { files } = await gen.generate({
674
- ...trpcOptions(g, cfg, servicesDir),
675
- onProgress: ({ index }) => progress.update(index)
676
- });
677
- progress.stop();
678
- ora().succeed(chalk3.green(`Generated (trpc): ${files.length} files`));
679
- files.forEach((f) => console.log(" -", chalk3.cyan(f)));
680
- } catch (e) {
681
- progress.stop();
682
- reportGeneratorFailure(g.kind, e);
683
- process.exit(1);
2909
+ const out = outputFor(opts);
2910
+ const planning = !!opts.check || !!opts.dryRun;
2911
+ const emitted = [];
2912
+ const warnings = [];
2913
+ const warn = (text) => {
2914
+ warnings.push(text);
2915
+ out.warn(text);
2916
+ };
2917
+ {
2918
+ try {
2919
+ const only = parseOnly(opts.only);
2920
+ let cfg = await loadConfig(opts.config, warn);
2921
+ if (!cfg && only) {
2922
+ cfg = configFromKinds([...only], opts.schema, warn);
2923
+ }
2924
+ if (!cfg) {
2925
+ const msg = "No config found (DRZL_CFG_001). Create drzl.config.ts or pass --config.";
2926
+ if (opts.json) out.jsonData(jsonFailure("generate", "DRZL_CFG_001", msg));
2927
+ else {
2928
+ out.error(msg);
2929
+ out.hint("Or run one generator with no config: drzl generate --schema <path> --only <kind>.");
684
2930
  }
685
- } else if (g.kind === "service") {
686
- try {
687
- const { ServiceGenerator } = await loadGenerator(
688
- "@drzl/generator-service",
689
- () => import("@drzl/generator-service")
690
- );
691
- const gen = new ServiceGenerator(analysis);
692
- const target = g.path ?? "src/services";
693
- const files = await gen.generate({
694
- outDir: target,
695
- outputHeader: g.outputHeader,
696
- format: g.format,
697
- dataAccess: g.dataAccess,
698
- dbImportPath: g.dbImportPath,
699
- schemaImportPath: g.schemaImportPath,
700
- importExtension: g.importExtension,
701
- // The other half of `databaseInjection`. A router generator in injection mode
702
- // emits `Service.getById(ctx.db, id)`, and only a service generated in the same
703
- // mode has a `db` parameter to receive it. This branch never passed the option, so
704
- // the two halves of one generated project disagreed about the signature.
705
- databaseInjection: g.databaseInjection
706
- });
707
- progress.stop();
708
- ora().succeed(chalk3.green(`Generated (service): ${files.length} files`));
709
- files.forEach((f) => console.log(" -", chalk3.cyan(f)));
710
- } catch (e) {
711
- progress.stop();
712
- reportGeneratorFailure(g.kind, e);
713
- process.exit(1);
2931
+ process.exit(EXIT_FAILED);
2932
+ return;
2933
+ }
2934
+ if (opts.schema) {
2935
+ const { drizzleKit: _fromConfig, ...rest } = cfg;
2936
+ cfg = { ...rest, schema: opts.schema };
2937
+ }
2938
+ const nothingSelected = emptySelectionMessage(only, cfg.generators);
2939
+ if (nothingSelected) {
2940
+ if (opts.json) out.jsonData(jsonFailure("generate", "DRZL_CLI_ONLY", nothingSelected));
2941
+ else {
2942
+ out.error(nothingSelected);
2943
+ out.hint('Add it to "generators" in your config, or name a kind that is already there.');
714
2944
  }
715
- } else if (g.kind === "zod") {
716
- try {
717
- const { ZodGenerator } = await loadGenerator(
718
- "@drzl/generator-zod",
719
- () => import("@drzl/generator-zod")
720
- );
721
- const gen = new ZodGenerator(analysis);
722
- const target = g.path ?? "src/validators/zod";
723
- const files = await gen.generate(
724
- validationOptions(g, cfg, target, { schemaTypes: true })
725
- );
726
- progress.stop();
727
- ora().succeed(chalk3.green(`Generated (zod): ${files.length} files`));
728
- files.forEach((f) => console.log(" -", chalk3.cyan(f)));
729
- } catch (e) {
730
- progress.stop();
731
- reportGeneratorFailure(g.kind, e);
732
- process.exit(1);
2945
+ process.exit(EXIT_FAILED);
2946
+ return;
2947
+ }
2948
+ const source = await resolveSchemaSource(cfg);
2949
+ for (const w of source.warnings) warn(w);
2950
+ if (!cfg.schema && Array.isArray(source.schema) && source.schema.length === 1) {
2951
+ cfg = { ...cfg, schema: source.schema[0] };
2952
+ }
2953
+ if (source.source === "drizzle-kit") {
2954
+ const n = source.schema.length;
2955
+ out.note(
2956
+ out.errStyle.gray(
2957
+ `Schema from ${path7.relative(process.cwd(), source.drizzleKitConfigPath)} (${n} file${n === 1 ? "" : "s"})`
2958
+ )
2959
+ );
2960
+ }
2961
+ const analyzer = new SchemaAnalyzer2(source.schema);
2962
+ const spinner = out.spinner("Analyzing...");
2963
+ const t0 = Date.now();
2964
+ const analysis = await analyzer.analyze({
2965
+ includeRelations: cfg.analyzer.includeRelations,
2966
+ validateConstraints: cfg.analyzer.validateConstraints,
2967
+ includeHeuristicRelations: cfg.analyzer.includeHeuristicRelations
2968
+ });
2969
+ const loadFailure = schemaLoadFailure(analysis.issues, source.schema);
2970
+ if (loadFailure) {
2971
+ spinner.stop();
2972
+ reportSchemaProblem(out, "generate", loadFailure);
2973
+ }
2974
+ spinner.succeed(`Analysis complete in ${Date.now() - t0}ms`);
2975
+ const dialectWarning = dialectMismatchWarning({
2976
+ configPath: source.drizzleKitConfigPath ?? "",
2977
+ declared: source.drizzleKitDialect,
2978
+ analyzed: analysis.dialect
2979
+ });
2980
+ if (dialectWarning) warn(dialectWarning);
2981
+ const narrowed = filterColumns(analysis.tables, cfg.columns);
2982
+ const filterWarnings = tableFilterWarnings(narrowed.tables, cfg);
2983
+ analysis.tables = filterTables(narrowed.tables, cfg);
2984
+ for (const w of [...narrowed.warnings, ...filterWarnings]) warn(w);
2985
+ for (const w of wideColumnWarning(analysis.issues)) warn(w);
2986
+ const empty = nothingToGenerate({
2987
+ schema: source.schema,
2988
+ analyzed: narrowed.tables,
2989
+ remaining: analysis.tables
2990
+ });
2991
+ if (empty) reportSchemaProblem(out, "generate", empty);
2992
+ const outputDirs = computeGeneratorOutputDirs(cfg);
2993
+ const existing = planning ? await snapshotAll(outputDirs) : void 0;
2994
+ const plan = new EmitPlan({ write: !planning, existing });
2995
+ const total = analysis.tables.length || 1;
2996
+ const progress = out.progress(total);
2997
+ const generated = (kind, files) => {
2998
+ progress.stop();
2999
+ const missed = plan.unrecorded(files);
3000
+ if (missed.length && planning) {
3001
+ const message = `The ${kind} generator wrote ${missed.length} file(s) directly instead of reporting them, so this run could not be a preview. Update @drzl/generator-${kind} to a version that supports --dry-run. First file: ${displayPath(missed[0])}`;
3002
+ if (opts.json) out.jsonData(jsonFailure("generate", "DRZL_GEN_003", message));
3003
+ else out.error(message);
3004
+ process.exit(EXIT_FAILED);
733
3005
  }
734
- } else if (g.kind === "valibot") {
735
- try {
736
- const { ValibotGenerator } = await loadGenerator(
737
- "@drzl/generator-valibot",
738
- () => import("@drzl/generator-valibot")
739
- );
740
- const gen = new ValibotGenerator(analysis);
741
- const target = g.path ?? "src/validators/valibot";
742
- const files = await gen.generate(
743
- validationOptions(g, cfg, target, { schemaTypes: true })
744
- );
745
- progress.stop();
746
- ora().succeed(chalk3.green(`Generated (valibot): ${files.length} files`));
747
- files.forEach((f) => console.log(" -", chalk3.cyan(f)));
748
- } catch (e) {
749
- progress.stop();
750
- reportGeneratorFailure(g.kind, e);
751
- process.exit(1);
3006
+ const verdicts = plan.verdictsFor(files).filter(Boolean);
3007
+ emitted.push({
3008
+ kind,
3009
+ files,
3010
+ changes: verdicts.map((v) => ({ file: v.file, status: v.verdict }))
3011
+ });
3012
+ if (opts.json) return;
3013
+ if (out.quiet) return;
3014
+ out.succeed(
3015
+ out.errStyle.green(
3016
+ `${planning ? "Would write" : "Generated"} (${kind}): ${files.length} files`
3017
+ ) + out.errStyle.gray(` (${describeCounts(plan.counts(files))})`)
3018
+ );
3019
+ for (const v of verdicts) {
3020
+ if (opts.check) break;
3021
+ if (v.verdict === "unchanged") continue;
3022
+ const mark = v.verdict === "created" ? "+" : "~";
3023
+ out.note(" " + out.errStyle.cyan(mark + " " + displayPath(v.file)));
752
3024
  }
753
- } else if (g.kind === "arktype") {
3025
+ for (const f of files) out.data(" - " + out.outStyle.cyan(f));
3026
+ };
3027
+ const failGenerator = (kind, e) => {
3028
+ progress.stop();
3029
+ const message = reportGeneratorFailure(out, kind, e);
3030
+ if (opts.json) out.jsonData(jsonFailure("generate", "DRZL_GEN_002", message));
3031
+ process.exit(EXIT_FAILED);
3032
+ };
3033
+ const servicesDir = resolveServicesDir(cfg);
3034
+ for (const g of selectGenerators(cfg.generators, only)) {
3035
+ progress.start();
3036
+ const entry = GENERATOR_BY_KIND.get(g.kind);
3037
+ if (!entry) continue;
754
3038
  try {
755
- const { ArkTypeGenerator } = await loadGenerator(
756
- "@drzl/generator-arktype",
757
- () => import("@drzl/generator-arktype")
758
- );
759
- const gen = new ArkTypeGenerator(analysis);
760
- const target = g.path ?? "src/validators/arktype";
761
- const files = await gen.generate(
762
- validationOptions(g, cfg, target, { schemaTypes: false })
763
- );
764
- progress.stop();
765
- ora().succeed(chalk3.green(`Generated (arktype): ${files.length} files`));
766
- files.forEach((f) => console.log(" -", chalk3.cyan(f)));
3039
+ const files = await runGenerator(entry, g, cfg, {
3040
+ analysis,
3041
+ servicesDir,
3042
+ fileSink: plan,
3043
+ onProgress: ({ index }) => progress.update(index)
3044
+ });
3045
+ generated(g.kind, files);
767
3046
  } catch (e) {
768
- progress.stop();
769
- reportGeneratorFailure(g.kind, e);
770
- process.exit(1);
3047
+ failGenerator(g.kind, e);
771
3048
  }
772
- } else if (g.kind === "json-schema") {
773
- try {
774
- const { JsonSchemaGenerator } = await loadGenerator(
775
- "@drzl/generator-json-schema",
776
- () => import("./dist-TRJLPIWT.js")
777
- );
778
- const gen = new JsonSchemaGenerator(analysis);
779
- const target = g.path ?? "src/validators/json-schema";
780
- const files = await gen.generate(jsonSchemaOptions(g, cfg, target));
781
- progress.stop();
782
- ora().succeed(chalk3.green(`Generated (json-schema): ${files.length} files`));
783
- files.forEach((f) => console.log(" -", chalk3.cyan(f)));
784
- } catch (e) {
785
- progress.stop();
786
- reportGeneratorFailure(g.kind, e);
787
- process.exit(1);
3049
+ }
3050
+ const generatorsDocument = () => emitted.map((e) => ({
3051
+ kind: e.kind,
3052
+ files: e.files,
3053
+ changes: e.changes.map((c) => ({ file: displayPath(c.file), status: c.status }))
3054
+ }));
3055
+ if (planning) {
3056
+ const wrote = await verifyNothingWasWritten(outputDirs, existing);
3057
+ if (wrote.length) {
3058
+ const message = `${wrote.length} file(s) were written by a run that promised to write none, and have been restored. This means an installed generator package is older than this CLI. Update your @drzl/generator-* packages. First file: ${displayPath(wrote[0])}`;
3059
+ if (opts.json) out.jsonData(jsonFailure("generate", "DRZL_GEN_003", message));
3060
+ else out.error(message);
3061
+ process.exit(EXIT_FAILED);
788
3062
  }
789
- } else if (g.kind === "typebox") {
790
- try {
791
- const { TypeBoxGenerator } = await loadGenerator(
792
- "@drzl/generator-typebox",
793
- () => import("@drzl/generator-typebox")
794
- );
795
- const gen = new TypeBoxGenerator(analysis);
796
- const target = g.path ?? "src/validators/typebox";
797
- const files = await gen.generate(
798
- validationOptions(g, cfg, target, { schemaTypes: true })
799
- );
800
- progress.stop();
801
- ora().succeed(chalk3.green(`Generated (typebox): ${files.length} files`));
802
- files.forEach((f) => console.log(" -", chalk3.cyan(f)));
803
- } catch (e) {
804
- progress.stop();
805
- reportGeneratorFailure(g.kind, e);
806
- process.exit(1);
3063
+ }
3064
+ if (opts.check) {
3065
+ const drift = pendingChanges(plan);
3066
+ const upToDate = drift.length === 0;
3067
+ const code = upToDate ? EXIT_OK : EXIT_FINDINGS;
3068
+ if (opts.json) {
3069
+ out.jsonData({
3070
+ ok: true,
3071
+ command: "generate",
3072
+ exitCode: code,
3073
+ check: {
3074
+ upToDate,
3075
+ drift: drift.map((d, i) => ({
3076
+ file: displayPath(d.file),
3077
+ status: driftStatusOf(d.verdict),
3078
+ // Item 81. Beyond the cap the entry is still here with its status, and only the
3079
+ // diff is absent, so a machine reading this never loses a file.
3080
+ diff: i < DIFF_FILE_CAP ? unifiedDiff(d.before ?? "", d.after, {
3081
+ fromLabel: `a/${displayPath(d.file)}`,
3082
+ toLabel: `b/${displayPath(d.file)}`
3083
+ }) : null
3084
+ })),
3085
+ diffFileCap: DIFF_FILE_CAP
3086
+ },
3087
+ generators: generatorsDocument(),
3088
+ warnings
3089
+ });
3090
+ process.exit(code);
807
3091
  }
808
- } else if (g.kind === "effect") {
809
- try {
810
- const { EffectGenerator } = await loadGenerator(
811
- "@drzl/generator-effect",
812
- () => import("./dist-CZDVFYFW.js")
813
- );
814
- const gen = new EffectGenerator(analysis);
815
- const target = g.path ?? "src/validators/effect";
816
- const files = await gen.generate(
817
- validationOptions(g, cfg, target, { schemaTypes: true })
818
- );
819
- progress.stop();
820
- ora().succeed(chalk3.green(`Generated (effect): ${files.length} files`));
821
- files.forEach((f) => console.log(" -", chalk3.cyan(f)));
822
- } catch (e) {
823
- progress.stop();
824
- reportGeneratorFailure(g.kind, e);
825
- process.exit(1);
3092
+ if (!upToDate) {
3093
+ out.error(`
3094
+ Generated output is out of date (${drift.length} file(s)):`);
3095
+ for (const d of drift) {
3096
+ const status = driftStatusOf(d.verdict);
3097
+ const mark = status === "added" ? "+" : "~";
3098
+ out.error(
3099
+ ` ${mark} ${out.errStyle.yellow(status.padEnd(8))} ${displayPath(d.file)}`
3100
+ );
3101
+ }
3102
+ printCheckDiffs(out, drift);
3103
+ out.hint("\nRun `drzl generate` and commit the result. Nothing was written by this check.");
3104
+ process.exit(code);
826
3105
  }
3106
+ out.succeed(out.errStyle.green("Generated output is up to date."));
3107
+ process.exit(code);
827
3108
  }
828
- }
829
- if (driftBefore) {
830
- const after = await snapshotAll(driftDirs);
831
- const drift = diffSnapshots(driftBefore, after);
832
- await restoreSnapshot(driftBefore, after);
833
- if (drift.length) {
834
- console.error(chalk3.red(`
835
- Generated output is out of date (${drift.length} file(s)):`));
836
- for (const d of drift) {
837
- const mark = d.status === "added" ? "+" : d.status === "removed" ? "-" : "~";
838
- console.error(
839
- ` ${mark} ${chalk3.yellow(d.status.padEnd(8))} ${path4.relative(process.cwd(), d.file)}`
840
- );
841
- }
842
- console.error(
843
- chalk3.dim(
844
- "\nRun `drzl generate` and commit the result. Nothing was written by this check."
845
- )
3109
+ if (opts.json) {
3110
+ out.jsonData({
3111
+ ok: true,
3112
+ command: "generate",
3113
+ exitCode: EXIT_OK,
3114
+ check: null,
3115
+ dryRun: !!opts.dryRun,
3116
+ generators: generatorsDocument(),
3117
+ warnings
3118
+ });
3119
+ return;
3120
+ }
3121
+ if (opts.dryRun) {
3122
+ const counts = plan.counts();
3123
+ out.succeed(
3124
+ out.errStyle.green(`Dry run: ${counts.total} file(s) would be written`) + out.errStyle.gray(` (${describeCounts(counts)}). Nothing was written.`)
846
3125
  );
847
- process.exit(1);
3126
+ process.exit(EXIT_OK);
848
3127
  }
849
- console.log(chalk3.green("Generated output is up to date."));
850
- return;
851
- }
852
- if (cfg.generators.length) {
853
- maybeShowSponsorMessage({ reason: "generate" });
3128
+ if (cfg.generators.length) {
3129
+ maybeShowSponsorMessage({ reason: "generate", out });
3130
+ }
3131
+ } catch (e) {
3132
+ const msg = messageOf(e);
3133
+ const code = drzlErrorCode(e, "DRZL_GEN_001");
3134
+ if (e instanceof KindSelectionError) {
3135
+ if (opts.json) out.jsonData(jsonFailure("generate", e.code, msg));
3136
+ else {
3137
+ out.error(msg);
3138
+ if (e.hint) out.hint(e.hint);
3139
+ }
3140
+ process.exit(EXIT_FAILED);
3141
+ }
3142
+ if (opts.json) out.jsonData(jsonFailure("generate", code, msg));
3143
+ else if (e instanceof ConfigValidationError) {
3144
+ out.error(msg);
3145
+ } else {
3146
+ out.error("Generate failed (DRZL_GEN_001):", msg);
3147
+ out.hint("Tip: check your drzl.config.ts and template path.");
3148
+ }
3149
+ process.exit(EXIT_FAILED);
854
3150
  }
855
- } catch (e) {
856
- console.error(
857
- chalk3.red("Generate failed (DRZL_GEN_001):"),
858
- e?.message ?? e,
859
- "\nTip: check your drzl.config.ts and template path."
860
- );
861
- process.exit(1);
862
3151
  }
863
3152
  });
864
- program.command("generate:orpc").argument("<schema>", "path to drizzle schema (TS)").option("-o, --outDir <dir>", "output directory", "src/api").option("--template <name>", "template name", "standard").option("--includeRelations", "include relation endpoints").action(async (schema, opts) => {
3153
+ function schemaProblemFor(analysis, schema) {
3154
+ return schemaLoadFailure(analysis.issues, schema) ?? nothingToGenerate({ schema, analyzed: analysis.tables, remaining: analysis.tables });
3155
+ }
3156
+ function deprecationNotice(command, kind, schema, cmd) {
3157
+ const replacement = `drzl generate --schema ${schema} --only ${kind}`;
3158
+ const CONFIG_KEYS = {
3159
+ outDir: "outDir",
3160
+ template: "template",
3161
+ includeRelations: "includeRelations",
3162
+ servicesDir: "the service generator's path"
3163
+ };
3164
+ const moved = Object.keys(CONFIG_KEYS).filter(
3165
+ (name) => cmd.getOptionValueSource(name) === "cli"
3166
+ );
3167
+ const tail = moved.length ? ` (${moved.map((name) => CONFIG_KEYS[name]).join(", ")} ${moved.length === 1 ? "moves" : "move"} into drzl.config.ts)` : "";
3168
+ return `${command} is deprecated and will be removed in 5.0. Run this instead: ${replacement}${tail}`;
3169
+ }
3170
+ withOutputFlags(
3171
+ program.command("generate:orpc").description("Deprecated. Use `drzl generate --schema <path> --only orpc`").argument("<schema>", "path to drizzle schema (TS)").option("-o, --outDir <dir>", "output directory", "src/api").option("--template <name>", "template name", "standard").option("--includeRelations", "include relation endpoints")
3172
+ ).action(async (schema, opts, cmd) => {
3173
+ const out = outputFor(opts);
3174
+ out.warn(deprecationNotice("generate:orpc", "orpc", schema, cmd));
865
3175
  try {
866
- const analyzer = new SchemaAnalyzer(schema);
3176
+ const analyzer = new SchemaAnalyzer2(schema);
867
3177
  const analysis = await analyzer.analyze({
868
3178
  includeRelations: !!opts.includeRelations,
869
3179
  validateConstraints: true
870
3180
  });
871
- const gen = new ORPCGenerator(analysis);
872
- const { files } = await gen.generate({
3181
+ const problem = schemaProblemFor(analysis, schema);
3182
+ if (problem) reportSchemaProblem(out, "generate:orpc", problem);
3183
+ const files = await runGeneratorWithOptions(entryFor("orpc"), analysis, {
873
3184
  outputDir: opts.outDir,
874
3185
  template: opts.template,
875
3186
  includeRelations: !!opts.includeRelations
876
3187
  });
877
- console.log(chalk3.green(`Generated:`), files.map((f) => chalk3.cyan(f)).join(", "));
878
- maybeShowSponsorMessage({ reason: "generate:orpc" });
3188
+ if (opts.json) {
3189
+ out.jsonData({
3190
+ ok: true,
3191
+ command: "generate:orpc",
3192
+ exitCode: EXIT_OK,
3193
+ generators: [{ kind: "orpc", files }]
3194
+ });
3195
+ return;
3196
+ }
3197
+ if (!out.quiet) {
3198
+ out.data(out.outStyle.green("Generated:") + " " + files.map((f) => out.outStyle.cyan(f)).join(", "));
3199
+ }
3200
+ maybeShowSponsorMessage({ reason: "generate:orpc", out });
879
3201
  } catch (e) {
880
- console.error(chalk3.red("Generate orpc failed:"), e?.message ?? e);
881
- process.exit(1);
3202
+ let message;
3203
+ if (e instanceof GeneratorNotInstalledError) {
3204
+ message = reportGeneratorFailure(out, "orpc", e);
3205
+ } else {
3206
+ message = messageOf(e);
3207
+ out.error("Generate orpc failed:", message);
3208
+ }
3209
+ if (opts.json) out.jsonData(jsonFailure("generate:orpc", "DRZL_CLI_ORPC", message));
3210
+ process.exit(EXIT_FAILED);
882
3211
  }
883
3212
  });
884
- program.command("generate:trpc").argument("<schema>", "path to drizzle schema (TS)").option("-o, --outDir <dir>", "output directory", "src/api").option("--template <name>", "standard | service", "standard").option("--includeRelations", "include relation endpoints").option("--servicesDir <dir>", "where the service generator writes", "src/services").action(async (schema, opts) => {
3213
+ withOutputFlags(
3214
+ program.command("generate:trpc").description("Deprecated. Use `drzl generate --schema <path> --only trpc`").argument("<schema>", "path to drizzle schema (TS)").option("-o, --outDir <dir>", "output directory", "src/api").option("--template <name>", "standard | service", "standard").option("--includeRelations", "include relation endpoints").option("--servicesDir <dir>", "where the service generator writes", "src/services")
3215
+ ).action(async (schema, opts, cmd) => {
3216
+ const out = outputFor(opts);
3217
+ out.warn(deprecationNotice("generate:trpc", "trpc", schema, cmd));
885
3218
  try {
886
- const analyzer = new SchemaAnalyzer(schema);
3219
+ const analyzer = new SchemaAnalyzer2(schema);
887
3220
  const analysis = await analyzer.analyze({
888
3221
  includeRelations: !!opts.includeRelations,
889
3222
  validateConstraints: true
890
3223
  });
891
- const { TRPCGenerator } = await loadGenerator(
892
- "@drzl/generator-trpc",
893
- () => import("./dist-XBGVORL3.js")
894
- );
895
- const gen = new TRPCGenerator(analysis);
896
- const { files } = await gen.generate({
3224
+ const problem = schemaProblemFor(analysis, schema);
3225
+ if (problem) reportSchemaProblem(out, "generate:trpc", problem);
3226
+ const files = await runGeneratorWithOptions(entryFor("trpc"), analysis, {
897
3227
  outputDir: opts.outDir,
898
3228
  template: opts.template,
899
3229
  includeRelations: !!opts.includeRelations,
@@ -901,27 +3231,95 @@ program.command("generate:trpc").argument("<schema>", "path to drizzle schema (T
901
3231
  // cannot become the branch that forgets it.
902
3232
  servicesDir: opts.servicesDir
903
3233
  });
904
- console.log(chalk3.green(`Generated:`), files.map((f) => chalk3.cyan(f)).join(", "));
905
- maybeShowSponsorMessage({ reason: "generate:trpc" });
3234
+ if (opts.json) {
3235
+ out.jsonData({
3236
+ ok: true,
3237
+ command: "generate:trpc",
3238
+ exitCode: EXIT_OK,
3239
+ generators: [{ kind: "trpc", files }]
3240
+ });
3241
+ return;
3242
+ }
3243
+ if (!out.quiet) {
3244
+ out.data(
3245
+ out.outStyle.green("Generated:") + " " + files.map((f) => out.outStyle.cyan(f)).join(", ")
3246
+ );
3247
+ }
3248
+ maybeShowSponsorMessage({ reason: "generate:trpc", out });
906
3249
  } catch (e) {
907
- reportGeneratorFailure("trpc", e);
908
- process.exit(1);
3250
+ const message = reportGeneratorFailure(out, "trpc", e);
3251
+ if (opts.json) out.jsonData(jsonFailure("generate:trpc", "DRZL_CLI_TRPC", message));
3252
+ process.exit(EXIT_FAILED);
909
3253
  }
910
3254
  });
911
- program.command("watch").description("Watch schema and regenerate on changes").option("-c, --config <path>", "path to drzl.config").option("--pipeline <name>", "all | analyze | generate-orpc | generate-trpc", "all").option("--debounce <ms>", "debounce ms", "200").option("--json", "emit JSON logs", false).option("--poll", "force polling (helps WSL/Docker/remote FS)", false).action(async (opts) => {
912
- let cfg = await loadConfig(opts.config);
913
- if (!cfg) {
914
- console.error(chalk3.red("No config found. Create drzl.config.ts or pass --config."));
915
- process.exit(2);
3255
+ program.command("watch").description("Watch schema and regenerate on changes").option("-c, --config <path>", "path to drzl.config").option(
3256
+ "--only <kinds>",
3257
+ `rebuild only these generator kinds, comma separated: ${kindList()}`
3258
+ ).option(
3259
+ "--pipeline <name>",
3260
+ "all | analyze | generate-<kind>, the older spelling of --only",
3261
+ "all"
3262
+ ).option("--debounce <ms>", "wait this long after the last change before rebuilding", "200").option("--clear", "clear the terminal before each rebuild", false).option("--json", "emit JSON logs", false).option("-q, --quiet", "drop the progress narration on stderr; errors still print", false).option("--poll", "force polling (helps WSL/Docker/remote FS)", false).action(async (opts) => {
3263
+ const out = outputFor(opts);
3264
+ let selection;
3265
+ try {
3266
+ selection = resolveWatchSelection(opts);
3267
+ } catch (e) {
3268
+ if (e instanceof KindSelectionError) {
3269
+ if (opts.json) out.jsonData(jsonFailure("watch", e.code, e.message));
3270
+ else {
3271
+ out.error(e.message);
3272
+ if (e.hint) out.hint(e.hint);
3273
+ }
3274
+ } else out.error(messageOf(e));
3275
+ process.exit(EXIT_FAILED);
3276
+ return;
3277
+ }
3278
+ const reportWatchProblem = (problem) => {
3279
+ if (opts.json) {
3280
+ out.jsonData({ event: "error", code: problem.code, message: problem.message });
3281
+ return;
3282
+ }
3283
+ out.error(problem.message);
3284
+ out.hint(problem.hint);
3285
+ };
3286
+ const clearScreen = () => {
3287
+ if (!opts.clear || opts.json || out.quiet) return;
3288
+ if (!out.stderr.isTTY) return;
3289
+ out.stderr.write("\x1B[2J\x1B[3J\x1B[H");
3290
+ };
3291
+ let loaded;
3292
+ try {
3293
+ loaded = await loadConfig(opts.config, (w) => out.warn(w));
3294
+ } catch (e) {
3295
+ out.error(messageOf(e));
3296
+ process.exit(EXIT_FAILED);
916
3297
  return;
917
3298
  }
918
- const abs = (p) => path4.resolve(process.cwd(), p);
3299
+ if (!loaded) {
3300
+ out.error("No config found (DRZL_CFG_001). Create drzl.config.ts or pass --config.");
3301
+ process.exit(EXIT_FAILED);
3302
+ return;
3303
+ }
3304
+ let cfg = loaded;
3305
+ const abs = (p) => path7.resolve(process.cwd(), p);
919
3306
  const isInside = (child, parent) => {
920
- const rel = path4.relative(parent, child);
921
- return !!rel && !rel.startsWith("..") && !path4.isAbsolute(rel);
3307
+ const rel = path7.relative(parent, child);
3308
+ return !!rel && !rel.startsWith("..") && !path7.isAbsolute(rel);
922
3309
  };
3310
+ let source;
3311
+ try {
3312
+ source = await resolveSchemaSource(cfg);
3313
+ } catch (e) {
3314
+ out.error(messageOf(e));
3315
+ process.exit(EXIT_FAILED);
3316
+ return;
3317
+ }
3318
+ for (const w of source.warnings) out.warn(w);
923
3319
  const ignoredOutDirs = new Set(computeGeneratorOutputDirs(cfg).map(abs));
924
- const currentTargets = new Set(computeWatchTargets(cfg).map(abs));
3320
+ const currentTargets = new Set(
3321
+ computeWatchTargets(cfg, process.cwd(), source).map(abs)
3322
+ );
925
3323
  const syncWatcherTargets = (watcher2, next) => {
926
3324
  const add = [];
927
3325
  const del = [];
@@ -943,7 +3341,7 @@ program.command("watch").description("Watch schema and regenerate on changes").o
943
3341
  if (full === dir || isInside(full, dir)) return true;
944
3342
  }
945
3343
  if (stats?.isDirectory()) return false;
946
- const ext = path4.extname(full);
3344
+ const ext = path7.extname(full);
947
3345
  if (!ext) return false;
948
3346
  return !WATCHED_EXTENSIONS.has(ext);
949
3347
  };
@@ -954,7 +3352,7 @@ program.command("watch").description("Watch schema and regenerate on changes").o
954
3352
  ignored: ignoredFn
955
3353
  });
956
3354
  const logTrigger = (type, file) => {
957
- if (opts.json) console.log(JSON.stringify({ event: "trigger", type, file }));
3355
+ if (opts.json) out.jsonData({ event: "trigger", type, file });
958
3356
  };
959
3357
  watcher.on("add", (p) => {
960
3358
  logTrigger("add", p);
@@ -967,257 +3365,127 @@ program.command("watch").description("Watch schema and regenerate on changes").o
967
3365
  trigger(p);
968
3366
  });
969
3367
  let lastFiles = [];
3368
+ const watchGenerated = (kind, files) => {
3369
+ if (opts.json) {
3370
+ out.jsonData({ event: "generate_complete", kind, files });
3371
+ return;
3372
+ }
3373
+ out.succeed(
3374
+ out.errStyle.green(`Generated (${kind}): ${files.length} files`) + (files.length ? " " + files.map((f) => out.errStyle.cyan(f)).join(", ") : "")
3375
+ );
3376
+ };
970
3377
  const run = async () => {
971
3378
  try {
972
- const reloaded = await loadConfig(opts.config);
3379
+ const reloaded = await loadConfig(opts.config, (w) => out.warn(w));
973
3380
  if (!reloaded) throw new Error("Config disappeared during watch.");
974
3381
  cfg = reloaded;
3382
+ source = await resolveSchemaSource(cfg);
3383
+ if (!cfg.schema && Array.isArray(source.schema) && source.schema.length === 1) {
3384
+ cfg = { ...cfg, schema: source.schema[0] };
3385
+ }
975
3386
  rebuildIgnoreDirsFrom(cfg);
976
- const nextTargets = new Set(computeWatchTargets(cfg).map(abs));
3387
+ const nextTargets = new Set(
3388
+ computeWatchTargets(cfg, process.cwd(), source).map(abs)
3389
+ );
977
3390
  syncWatcherTargets(watcher, nextTargets);
978
- if (!opts.json) console.clear();
3391
+ clearScreen();
979
3392
  if (opts.json) {
980
- console.log(
981
- JSON.stringify({
982
- event: "watch_config_applied",
983
- targets: Array.from(currentTargets),
984
- ignored: Array.from(ignoredOutDirs)
985
- })
986
- );
3393
+ out.jsonData({
3394
+ event: "watch_config_applied",
3395
+ targets: Array.from(currentTargets),
3396
+ ignored: Array.from(ignoredOutDirs)
3397
+ });
987
3398
  }
988
- const analyzer = new SchemaAnalyzer(cfg.schema);
3399
+ for (const w of source.warnings) out.warn(w);
3400
+ const analyzer = new SchemaAnalyzer2(source.schema);
989
3401
  const analysis = await analyzer.analyze({
990
3402
  includeRelations: cfg.analyzer.includeRelations,
991
3403
  validateConstraints: cfg.analyzer.validateConstraints,
992
3404
  includeHeuristicRelations: cfg.analyzer.includeHeuristicRelations
993
3405
  });
994
- analysis.tables = filterTables(analysis.tables, cfg);
995
- if (!opts.json) reportWideColumns(analysis.issues);
996
- if (opts.pipeline === "analyze") {
3406
+ const dialectWarning = dialectMismatchWarning({
3407
+ configPath: source.drizzleKitConfigPath ?? "",
3408
+ declared: source.drizzleKitDialect,
3409
+ analyzed: analysis.dialect
3410
+ });
3411
+ if (dialectWarning) out.warn(dialectWarning);
3412
+ const loadFailure = schemaLoadFailure(analysis.issues, source.schema);
3413
+ if (loadFailure) {
3414
+ reportWatchProblem(loadFailure);
3415
+ return;
3416
+ }
3417
+ const narrowed = filterColumns(analysis.tables, cfg.columns);
3418
+ const filterWarnings = tableFilterWarnings(narrowed.tables, cfg);
3419
+ analysis.tables = filterTables(narrowed.tables, cfg);
3420
+ for (const w of [...narrowed.warnings, ...filterWarnings]) out.warn(w);
3421
+ for (const w of wideColumnWarning(analysis.issues)) out.warn(w);
3422
+ if (selection.analyzeOnly) {
997
3423
  if (opts.json) {
998
- console.log(
999
- JSON.stringify({
1000
- event: "analyze_complete",
1001
- issues: analysis.issues,
1002
- tables: analysis.tables.length
1003
- })
1004
- );
3424
+ out.jsonData({
3425
+ event: "analyze_complete",
3426
+ issues: analysis.issues,
3427
+ tables: analysis.tables.length
3428
+ });
1005
3429
  } else {
1006
- console.log(chalk3.green("Analyze complete."));
3430
+ out.succeed("Analyze complete.");
1007
3431
  }
1008
3432
  return;
1009
3433
  }
3434
+ const empty = nothingToGenerate({
3435
+ schema: source.schema,
3436
+ analyzed: narrowed.tables,
3437
+ remaining: analysis.tables
3438
+ });
3439
+ if (empty) {
3440
+ reportWatchProblem(empty);
3441
+ return;
3442
+ }
1010
3443
  const newFiles = [];
1011
- const servicesDir = cfg.generators.find((x) => x.kind === "service")?.path ?? "src/services";
1012
- const PIPELINE_KINDS = {
1013
- "generate-orpc": "orpc",
1014
- "generate-trpc": "trpc"
1015
- };
1016
- for (const g of cfg.generators) {
1017
- if (opts.pipeline !== "all" && PIPELINE_KINDS[opts.pipeline] !== g.kind) {
1018
- continue;
3444
+ const servicesDir = resolveServicesDir(cfg);
3445
+ const unmatched = emptySelectionMessage(selection.kinds, cfg.generators);
3446
+ if (unmatched) {
3447
+ if (opts.json) out.jsonData({ event: "error", code: "DRZL_CLI_ONLY", message: unmatched });
3448
+ else {
3449
+ out.error(unmatched);
3450
+ out.hint('Add it to "generators" in your config, or name a kind that is already there.');
1019
3451
  }
1020
- if (g.kind === "orpc") {
1021
- const gen = new ORPCGenerator(analysis);
1022
- const { files } = await gen.generate({
1023
- outputDir: cfg.outDir,
1024
- template: g.template,
1025
- includeRelations: g.includeRelations,
1026
- naming: g.naming,
1027
- outputHeader: g.outputHeader,
1028
- format: g.format,
1029
- templateOptions: g.templateOptions,
1030
- importExtension: g.importExtension,
1031
- validation: g.validation,
1032
- databaseInjection: g.databaseInjection,
1033
- servicesDir
1034
- });
1035
- opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
1036
- chalk3.green(`Generated (${g.kind}):`),
1037
- files.map((f) => chalk3.cyan(f)).join(", ")
1038
- );
3452
+ return;
3453
+ }
3454
+ for (const g of selectGenerators(cfg.generators, selection.kinds)) {
3455
+ const entry = GENERATOR_BY_KIND.get(g.kind);
3456
+ if (!entry) continue;
3457
+ try {
3458
+ const files = await runGenerator(entry, g, cfg, { analysis, servicesDir });
3459
+ watchGenerated(g.kind, files);
1039
3460
  newFiles.push(...files);
1040
- } else if (g.kind === "trpc") {
1041
- try {
1042
- const { TRPCGenerator } = await loadGenerator(
1043
- "@drzl/generator-trpc",
1044
- () => import("./dist-XBGVORL3.js")
1045
- );
1046
- const gen = new TRPCGenerator(analysis);
1047
- const { files } = await gen.generate(trpcOptions(g, cfg, servicesDir));
1048
- opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
1049
- chalk3.green(`Generated (trpc): ${files.length} files`),
1050
- files.map((f) => chalk3.cyan(f)).join(", ")
1051
- );
1052
- newFiles.push(...files);
1053
- } catch (e) {
1054
- reportGeneratorFailure(g.kind, e);
1055
- return;
1056
- }
1057
- } else if (g.kind === "service") {
1058
- try {
1059
- const { ServiceGenerator } = await loadGenerator(
1060
- "@drzl/generator-service",
1061
- () => import("@drzl/generator-service")
1062
- );
1063
- const gen = new ServiceGenerator(analysis);
1064
- const target = g.path ?? "src/services";
1065
- const files = await gen.generate({
1066
- outDir: target,
1067
- outputHeader: g.outputHeader,
1068
- format: g.format,
1069
- dataAccess: g.dataAccess,
1070
- dbImportPath: g.dbImportPath,
1071
- schemaImportPath: g.schemaImportPath,
1072
- importExtension: g.importExtension,
1073
- databaseInjection: g.databaseInjection
1074
- });
1075
- opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
1076
- chalk3.green(`Generated (service): ${files.length} files`),
1077
- files.map((f) => chalk3.cyan(f)).join(", ")
1078
- );
1079
- newFiles.push(...files);
1080
- } catch (e) {
1081
- reportGeneratorFailure(g.kind, e);
1082
- return;
1083
- }
1084
- } else if (g.kind === "zod") {
1085
- try {
1086
- const { ZodGenerator } = await loadGenerator(
1087
- "@drzl/generator-zod",
1088
- () => import("@drzl/generator-zod")
1089
- );
1090
- const gen = new ZodGenerator(analysis);
1091
- const target = g.path ?? "src/validators/zod";
1092
- const files = await gen.generate(
1093
- validationOptions(g, cfg, target, { schemaTypes: true })
1094
- );
1095
- opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
1096
- chalk3.green(`Generated (zod): ${files.length} files`),
1097
- files.map((f) => chalk3.cyan(f)).join(", ")
1098
- );
1099
- newFiles.push(...files);
1100
- } catch (e) {
1101
- reportGeneratorFailure(g.kind, e);
1102
- return;
1103
- }
1104
- } else if (g.kind === "valibot") {
1105
- try {
1106
- const { ValibotGenerator } = await loadGenerator(
1107
- "@drzl/generator-valibot",
1108
- () => import("@drzl/generator-valibot")
1109
- );
1110
- const gen = new ValibotGenerator(analysis);
1111
- const target = g.path ?? "src/validators/valibot";
1112
- const files = await gen.generate(
1113
- validationOptions(g, cfg, target, { schemaTypes: true })
1114
- );
1115
- opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
1116
- chalk3.green(`Generated (valibot): ${files.length} files`),
1117
- files.map((f) => chalk3.cyan(f)).join(", ")
1118
- );
1119
- newFiles.push(...files);
1120
- } catch (e) {
1121
- reportGeneratorFailure(g.kind, e);
1122
- return;
1123
- }
1124
- } else if (g.kind === "arktype") {
1125
- try {
1126
- const { ArkTypeGenerator } = await loadGenerator(
1127
- "@drzl/generator-arktype",
1128
- () => import("@drzl/generator-arktype")
1129
- );
1130
- const gen = new ArkTypeGenerator(analysis);
1131
- const target = g.path ?? "src/validators/arktype";
1132
- const files = await gen.generate(
1133
- validationOptions(g, cfg, target, { schemaTypes: false })
1134
- );
1135
- opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
1136
- chalk3.green(`Generated (arktype): ${files.length} files`),
1137
- files.map((f) => chalk3.cyan(f)).join(", ")
1138
- );
1139
- newFiles.push(...files);
1140
- } catch (e) {
1141
- reportGeneratorFailure(g.kind, e);
1142
- return;
1143
- }
1144
- } else if (g.kind === "typebox") {
1145
- try {
1146
- const { TypeBoxGenerator } = await loadGenerator(
1147
- "@drzl/generator-typebox",
1148
- () => import("@drzl/generator-typebox")
1149
- );
1150
- const gen = new TypeBoxGenerator(analysis);
1151
- const target = g.path ?? "src/validators/typebox";
1152
- const files = await gen.generate(
1153
- validationOptions(g, cfg, target, { schemaTypes: true })
1154
- );
1155
- opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
1156
- chalk3.green(`Generated (typebox): ${files.length} files`),
1157
- files.map((f) => chalk3.cyan(f)).join(", ")
1158
- );
1159
- newFiles.push(...files);
1160
- } catch (e) {
1161
- reportGeneratorFailure(g.kind, e);
1162
- return;
1163
- }
1164
- } else if (g.kind === "effect") {
1165
- try {
1166
- const { EffectGenerator } = await loadGenerator(
1167
- "@drzl/generator-effect",
1168
- () => import("./dist-CZDVFYFW.js")
1169
- );
1170
- const gen = new EffectGenerator(analysis);
1171
- const target = g.path ?? "src/validators/effect";
1172
- const files = await gen.generate(
1173
- validationOptions(g, cfg, target, { schemaTypes: true })
1174
- );
1175
- opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
1176
- chalk3.green(`Generated (effect): ${files.length} files`),
1177
- files.map((f) => chalk3.cyan(f)).join(", ")
1178
- );
1179
- newFiles.push(...files);
1180
- } catch (e) {
1181
- reportGeneratorFailure(g.kind, e);
1182
- return;
1183
- }
1184
- } else if (g.kind === "json-schema") {
1185
- try {
1186
- const { JsonSchemaGenerator } = await loadGenerator(
1187
- "@drzl/generator-json-schema",
1188
- () => import("./dist-TRJLPIWT.js")
1189
- );
1190
- const gen = new JsonSchemaGenerator(analysis);
1191
- const target = g.path ?? "src/validators/json-schema";
1192
- const files = await gen.generate(jsonSchemaOptions(g, cfg, target));
1193
- opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
1194
- chalk3.green(`Generated (json-schema): ${files.length} files`),
1195
- files.map((f) => chalk3.cyan(f)).join(", ")
1196
- );
1197
- newFiles.push(...files);
1198
- } catch (e) {
1199
- reportGeneratorFailure(g.kind, e);
1200
- return;
1201
- }
3461
+ } catch (e) {
3462
+ reportGeneratorFailure(out, g.kind, e);
3463
+ return;
1202
3464
  }
1203
3465
  }
1204
3466
  const added = newFiles.filter((f) => !lastFiles.includes(f));
1205
3467
  const removed = lastFiles.filter((f) => !newFiles.includes(f));
1206
- opts.json ? console.log(JSON.stringify({ event: "diff", added, removed })) : (() => {
1207
- if (added.length) console.log(chalk3.blue(`Added: ${added.join(", ")}`));
1208
- if (removed.length) console.log(chalk3.yellow(`Removed: ${removed.join(", ")}`));
1209
- })();
1210
- if (newFiles.length && !opts.json) {
1211
- const reason = opts.pipeline && opts.pipeline !== "all" ? `watch:${opts.pipeline}` : "watch";
1212
- maybeShowSponsorMessage({ reason });
3468
+ if (opts.json) {
3469
+ out.jsonData({ event: "diff", added, removed });
3470
+ } else {
3471
+ if (added.length) out.note(out.errStyle.blue(`Added: ${added.join(", ")}`));
3472
+ if (removed.length) out.warn(`Removed: ${removed.join(", ")}`);
3473
+ }
3474
+ if (newFiles.length) {
3475
+ const reason = selection.kinds ? `watch:${[...selection.kinds].join(",")}` : "watch";
3476
+ maybeShowSponsorMessage({ reason, out });
1213
3477
  }
1214
3478
  lastFiles = newFiles;
1215
3479
  } catch (e) {
1216
- opts.json ? console.log(JSON.stringify({ event: "error", message: String(e?.message ?? e) })) : console.error(chalk3.red("Watch pipeline failed:"), e?.message ?? e);
3480
+ const msg = messageOf(e);
3481
+ if (opts.json) out.jsonData({ event: "error", message: msg });
3482
+ else out.error("Watch pipeline failed:", msg);
1217
3483
  }
1218
3484
  };
1219
- const debounced = Number(opts.debounce) || 200;
1220
- let timer = null;
3485
+ const scheduler = createRebuildScheduler({
3486
+ run,
3487
+ debounceMs: resolveDebounce(opts.debounce, (w) => out.warn(w))
3488
+ });
1221
3489
  const trigger = (file) => {
1222
3490
  if (file) {
1223
3491
  const full = abs(file);
@@ -1225,62 +3493,73 @@ program.command("watch").description("Watch schema and regenerate on changes").o
1225
3493
  if (full === dir || isInside(full, dir)) return;
1226
3494
  }
1227
3495
  }
1228
- if (timer) clearTimeout(timer);
1229
- timer = setTimeout(run, debounced);
3496
+ scheduler.trigger();
1230
3497
  };
1231
3498
  if (opts.json) {
1232
- console.log(
1233
- JSON.stringify({
1234
- event: "watching",
1235
- targets: Array.from(currentTargets),
1236
- ignored: Array.from(ignoredOutDirs)
1237
- })
1238
- );
3499
+ out.jsonData({
3500
+ event: "watching",
3501
+ targets: Array.from(currentTargets),
3502
+ ignored: Array.from(ignoredOutDirs)
3503
+ });
1239
3504
  } else {
1240
- console.log(
1241
- chalk3.gray(
1242
- "Watching:\n " + Array.from(currentTargets).map((p) => path4.relative(process.cwd(), p)).join("\n ")
3505
+ out.note(
3506
+ out.errStyle.gray(
3507
+ "Watching:\n " + Array.from(currentTargets).map((p) => path7.relative(process.cwd(), p)).join("\n ")
1243
3508
  )
1244
3509
  );
1245
3510
  }
1246
- watcher.on("add", (p) => trigger(p)).on("change", (p) => trigger(p)).on("unlink", (p) => trigger(p)).on("error", (err) => console.error(chalk3.red("Watcher error:"), err));
1247
- await run();
3511
+ watcher.on("add", (p) => trigger(p)).on("change", (p) => trigger(p)).on("unlink", (p) => trigger(p)).on("error", (err) => out.error("Watcher error:", messageOf(err)));
3512
+ await scheduler.runNow();
1248
3513
  });
1249
- program.command("init").description("Scaffold a drzl.config.ts").option("-y, --yes", "accept defaults").action(async (_opts) => {
1250
- const fs2 = await import("fs/promises");
1251
- const path5 = await import("path");
1252
- const target = path5.resolve(process.cwd(), "drzl.config.ts");
1253
- const template = `export default {
1254
- schema: 'src/db/schema.ts',
1255
- outDir: 'src/api',
1256
- analyzer: { includeRelations: true, validateConstraints: true },
1257
- generators: [
1258
- // For tRPC instead: { kind: 'trpc', template: 'standard', includeRelations: true }
1259
- // To run both, give one of them its own \`path\`; they share \`outDir\` otherwise.
1260
- { kind: 'orpc', template: 'standard', includeRelations: true }
1261
- ]
1262
- } as const
1263
- `;
1264
- try {
1265
- await fs2.writeFile(target, template, { flag: "wx" });
1266
- console.log(chalk3.green(`Created ${target}`));
1267
- } catch (e) {
1268
- console.error(chalk3.red("Init failed:"), e?.message ?? e);
1269
- process.exit(1);
3514
+ withOutputFlags(
3515
+ program.command("init").description("Scaffold a drzl.config.ts, finding your schema and asking what to generate").option("-y, --yes", "take the defaults and ask nothing").option("--schema <path>", "the schema file to write into the config, skipping detection").option(
3516
+ "--generators <list>",
3517
+ `comma-separated: ${INIT_GENERATOR_CHOICES.map((c) => c.kind).join(", ")}`
3518
+ )
3519
+ ).action(async (opts) => {
3520
+ const out = outputFor(opts);
3521
+ const failures = [];
3522
+ const outcome = await runInit({
3523
+ cwd: process.cwd(),
3524
+ yes: !!opts.yes || !!opts.json,
3525
+ schemaFlag: opts.schema,
3526
+ generatorsFlag: opts.generators,
3527
+ stdin: process.stdin,
3528
+ stdout: process.stdout,
3529
+ env: process.env,
3530
+ // Narration on stderr, all of it: what `init` produces is a file on disk, and the lines it
3531
+ // prints are a report about that.
3532
+ log: (s) => out.note(s.startsWith("Created ") ? out.errStyle.green(s) : out.errStyle.gray(s)),
3533
+ error: (s) => {
3534
+ failures.push(s);
3535
+ out.error(s);
3536
+ }
3537
+ });
3538
+ if (opts.json) {
3539
+ out.jsonData(
3540
+ outcome.code === 0 ? {
3541
+ ok: true,
3542
+ command: "init",
3543
+ exitCode: EXIT_OK,
3544
+ written: outcome.written,
3545
+ schema: outcome.plan?.schema ?? null,
3546
+ schemaSource: outcome.plan?.schemaSource ?? null,
3547
+ generators: outcome.plan?.generators ?? []
3548
+ } : jsonFailure("init", "DRZL_CLI_INIT", failures.join(" ") || "init did not write a config")
3549
+ );
1270
3550
  }
3551
+ process.exit(outcome.code === 0 ? EXIT_OK : EXIT_FAILED);
1271
3552
  });
1272
- function reportWideColumns(issues) {
3553
+ function wideColumnWarning(issues) {
1273
3554
  const wide = issues.filter((i) => i.code === "DRZL_ANL_UNKNOWN_COLUMN");
1274
- if (!wide.length) return;
1275
- console.warn(
1276
- chalk3.yellow(`
1277
- ${wide.length} column${wide.length === 1 ? "" : "s"} could not be typed:`)
1278
- );
1279
- for (const i of wide.slice(0, 10)) console.warn(chalk3.gray(` - ${i.message}`));
1280
- if (wide.length > 10) console.warn(chalk3.gray(` ... and ${wide.length - 10} more`));
1281
- const hints = [...new Set(wide.map((i) => i.hint).filter(Boolean))];
1282
- for (const h of hints) console.warn(chalk3.gray(` ${h}`));
1283
- console.warn(chalk3.gray(" Run `drzl doctor` for the full report."));
3555
+ if (!wide.length) return [];
3556
+ const lines = [`
3557
+ ${wide.length} column${wide.length === 1 ? "" : "s"} could not be typed:`];
3558
+ for (const i of wide.slice(0, 10)) lines.push(` - ${i.message}`);
3559
+ if (wide.length > 10) lines.push(` ... and ${wide.length - 10} more`);
3560
+ for (const h of [...new Set(wide.map((i) => i.hint).filter(Boolean))]) lines.push(` ${h}`);
3561
+ lines.push(" Run `drzl doctor` for the full report.");
3562
+ return [lines.join("\n")];
1284
3563
  }
1285
3564
  program.parseAsync(process.argv);
1286
3565
  //# sourceMappingURL=cli.js.map