@drzl/cli 4.21.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,28 +1,290 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ CONFIG_FILE_NAMES,
4
+ ConfigValidationError,
5
+ GENERATOR_KINDS,
3
6
  addressableName,
4
7
  ambiguousPatternWarnings,
5
8
  computeGeneratorOutputDirs,
6
9
  computeWatchTargets,
10
+ configFromKinds,
7
11
  displayTableName,
12
+ expressOutDir,
13
+ fastifyOutDir,
8
14
  filterTables,
15
+ graphqlOutDir,
9
16
  hasNamedSchemas,
17
+ honoOutDir,
18
+ importFreshConfigModule,
10
19
  loadConfig,
11
20
  matchesAny,
12
21
  matchesTable,
22
+ nearestKey,
23
+ nestjsOutDir,
24
+ tableAliases,
13
25
  tableFilterWarnings,
14
26
  trpcOutDir
15
- } from "./chunk-XNNKHBGV.js";
27
+ } from "./chunk-54E2IO7N.js";
16
28
 
17
29
  // src/cli.ts
18
- import { SchemaAnalyzer } from "@drzl/analyzer";
19
- import { ORPCGenerator } from "@drzl/generator-orpc";
20
- import chalk3 from "chalk";
30
+ import { qualifiedTableName as qualifiedTableName2, SchemaAnalyzer as SchemaAnalyzer2 } from "@drzl/analyzer";
21
31
  import chokidar from "chokidar";
22
- import cliProgress from "cli-progress";
23
32
  import { Command } from "commander";
24
- 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";
25
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
+ }
26
288
 
27
289
  // src/validation-options.ts
28
290
  function validationOptions(g, cfg, outDir, caps = {}) {
@@ -53,7 +315,8 @@ function validationOptions(g, cfg, outDir, caps = {}) {
53
315
  typedColumns: g.typedColumns
54
316
  } : {},
55
317
  ...caps.standardSchema ? { standardSchema: g.standardSchema } : {},
56
- ...caps.meta ? { meta: g.meta } : {}
318
+ ...caps.meta ? { meta: g.meta } : {},
319
+ ...caps.constraints ? { constraints: g.constraints } : {}
57
320
  };
58
321
  }
59
322
 
@@ -67,7 +330,57 @@ function jsonSchemaOptions(g, cfg, outDir) {
67
330
  document: g.document,
68
331
  // Read only while emitting a document, where it adds `/users/{id}/posts`. The per-table
69
332
  // schemas are flat whatever it says.
70
- 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
71
384
  };
72
385
  }
73
386
 
@@ -90,18 +403,335 @@ function trpcOptions(g, cfg, servicesDir) {
90
403
  };
91
404
  }
92
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
+
93
722
  // src/column-filter.ts
94
723
  import { parseCheck as parseCheck2 } from "@drzl/validation-core";
95
724
 
96
725
  // src/doctor.ts
97
- import { parseCheck } from "@drzl/validation-core";
98
- 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 });
99
729
  var HANDLED_CODES = /* @__PURE__ */ new Set(["DRZL_ANL_UNKNOWN_COLUMN"]);
100
- function splitPath(path5) {
101
- if (!path5) return {};
102
- const dot = path5.lastIndexOf(".");
103
- if (dot <= 0) return { table: path5 };
104
- 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) };
105
735
  }
106
736
  function namedColumns(parsed) {
107
737
  const out = [];
@@ -109,6 +739,7 @@ function namedColumns(parsed) {
109
739
  for (const s of parsed.sets ?? []) out.push({ column: s.column, scalar: true });
110
740
  for (const l of parsed.lengths ?? []) out.push({ column: l.column, scalar: false });
111
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 });
112
743
  for (const r of parsed.rows ?? []) {
113
744
  out.push({ column: r.left, scalar: false });
114
745
  out.push({ column: r.right, scalar: false });
@@ -137,6 +768,19 @@ function describeShape(c) {
137
768
  return "a structured";
138
769
  }
139
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
+ }
140
784
  function checkFindings(table) {
141
785
  const out = [];
142
786
  const byName = new Map(table.columns.map((c) => [c.name, c]));
@@ -152,10 +796,34 @@ function checkFindings(table) {
152
796
  table: table.tsName,
153
797
  constraint: k.name,
154
798
  message: `CHECK ${label} on "${table.tsName}" is not translated: ${parsed.reason}. Expression: ${expr}`,
155
- 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)
156
800
  });
157
801
  continue;
158
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
+ }
159
827
  const seen = /* @__PURE__ */ new Set();
160
828
  for (const { column, scalar } of namedColumns(parsed)) {
161
829
  if (seen.has(column)) continue;
@@ -267,7 +935,7 @@ var SECTIONS = [
267
935
  why: "These get a validator that accepts any value."
268
936
  },
269
937
  {
270
- kinds: ["check-declined", "check-unknown-column", "check-not-scalar"],
938
+ kinds: ["check-declined", "check-unknown-column", "check-not-scalar", "check-uncountable"],
271
939
  title: "CHECK constraints DRZL does not enforce",
272
940
  why: "Your database still enforces these. Nothing DRZL generates does."
273
941
  },
@@ -296,7 +964,8 @@ function wrap(text, indent, first = indent, width = 96) {
296
964
  if (line) lines.push(line);
297
965
  return lines.map((l, i) => (i === 0 ? first : indent) + l).join("\n");
298
966
  }
299
- function renderDoctorReport(report) {
967
+ function renderDoctorReport(report, style = PLAIN) {
968
+ const chalk = style;
300
969
  const out = [];
301
970
  const plural = (n, one) => `${n} ${one}${n === 1 ? "" : "s"}`;
302
971
  out.push(chalk.bold(`DRZL doctor ${report.schema}`));
@@ -444,87 +1113,1427 @@ function filterColumns(tables, spec) {
444
1113
  return { tables: out, warnings };
445
1114
  }
446
1115
 
447
- // src/drift.ts
448
- import { promises as fs } from "fs";
449
- import path from "path";
450
- async function snapshotDir(dir) {
451
- const out = /* @__PURE__ */ new Map();
452
- async function walk(current) {
453
- let entries;
454
- try {
455
- entries = await fs.readdir(current, { withFileTypes: true });
456
- } catch {
457
- return;
458
- }
459
- for (const e of entries) {
460
- const full = path.join(current, e.name);
461
- if (e.isDirectory()) await walk(full);
462
- else out.set(path.relative(dir, full), await fs.readFile(full, "utf8"));
463
- }
464
- }
465
- await walk(dir);
466
- return out;
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 };
467
1124
  }
468
- async function snapshotAll(dirs) {
469
- const all = /* @__PURE__ */ new Map();
470
- for (const dir of dirs) {
471
- for (const [rel, content] of await snapshotDir(dir)) {
472
- all.set(path.join(dir, rel), content);
473
- }
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 });
474
1138
  }
475
- return all;
1139
+ return hits;
476
1140
  }
477
- function diffSnapshots(before, after) {
478
- const out = [];
479
- for (const [file, content] of after) {
480
- if (!before.has(file)) out.push({ file, status: "added" });
481
- else if (before.get(file) !== content) out.push({ file, status: "changed" });
482
- }
483
- for (const file of before.keys()) {
484
- if (!after.has(file)) out.push({ file, status: "removed" });
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 };
485
1151
  }
486
- 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) };
487
1157
  }
488
- async function restoreSnapshot(before, after) {
489
- for (const [file, content] of before) {
490
- await fs.mkdir(path.dirname(file), { recursive: true });
491
- await fs.writeFile(file, content, "utf8");
492
- }
493
- for (const file of after.keys()) {
494
- if (!before.has(file)) await fs.rm(file, { force: true });
495
- }
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);
496
1165
  }
497
-
498
- // src/generator-loader.ts
499
- var GeneratorNotInstalledError = class extends Error {
500
- constructor(specifier, reason) {
501
- super(`${specifier} is not installed`);
502
- this.specifier = specifier;
503
- this.reason = reason;
504
- this.name = "GeneratorNotInstalledError";
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));
1865
+ }
1866
+ async function restoreSnapshot(before, after) {
1867
+ for (const [file, content] of before) {
1868
+ await fs2.mkdir(path2.dirname(file), { recursive: true });
1869
+ await fs2.writeFile(file, content, "utf8");
1870
+ }
1871
+ for (const file of after.keys()) {
1872
+ if (!before.has(file)) await fs2.rm(file, { force: true });
1873
+ }
1874
+ }
1875
+
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;
505
1937
  }
506
1938
  };
507
- function isPackageMissing(err, specifier) {
508
- const code = err?.code;
509
- if (code !== "ERR_MODULE_NOT_FOUND") return false;
510
- const message = err?.message;
511
- return typeof message === "string" && message.includes(`'${specifier}'`);
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
+ }
2450
+ }
2451
+ const picked = INIT_GENERATOR_CHOICES.filter((c) => kinds.includes(c.kind)).map((c) => c.kind);
2452
+ return picked.length ? picked : void 0;
512
2453
  }
513
- 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;
514
2470
  try {
515
- return await load();
2471
+ fromFlag = normalizeGenerators(parseGeneratorsFlag(args.generatorsFlag));
516
2472
  } catch (e) {
517
- if (isPackageMissing(e, specifier)) throw new GeneratorNotInstalledError(specifier, e);
518
- 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 };
519
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 };
520
2530
  }
521
2531
 
522
2532
  // src/sponsor.ts
523
- import chalk2 from "chalk";
524
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
525
- import path2 from "path";
526
- var CACHE_DIR = path2.join(process.cwd(), "node_modules", ".cache", "@drzl");
527
- 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");
528
2537
  var DEFAULT_INTERVAL_MS = 1e3 * 60 * 15;
529
2538
  var shownThisProcess = false;
530
2539
  var tips = [
@@ -534,17 +2543,19 @@ var tips = [
534
2543
  "Need JSON Schema or OpenAPI? The json-schema generator emits both, with no runtime dependency.",
535
2544
  "Use output headers to track generated files and trim noisy diffs."
536
2545
  ];
537
- var green = (msg) => chalk2.hex("#6ee7b7")(msg);
538
- var cyan = (msg) => chalk2.cyan(msg);
539
- var gray = (msg) => chalk2.gray(msg);
540
2546
  function maybeShowSponsorMessage({
541
2547
  reason = "generate",
542
2548
  minIntervalMs = DEFAULT_INTERVAL_MS,
543
- force = false
2549
+ force = false,
2550
+ out = new Output()
544
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);
545
2555
  const hideViaEnv = process.env.DRZL_HIDE_SPONSOR?.toLowerCase();
546
2556
  const hideRequested = hideViaEnv === "1" || hideViaEnv === "true";
547
2557
  if (hideRequested || process.env.CI && !force || shownThisProcess && !force) return;
2558
+ if (!out.wantsAsides && !force) return;
548
2559
  try {
549
2560
  mkdirSync(CACHE_DIR, { recursive: true });
550
2561
  const payload = readCache();
@@ -559,7 +2570,7 @@ function maybeShowSponsorMessage({
559
2570
  if (!shouldShow) return;
560
2571
  shownThisProcess = true;
561
2572
  const tip = tips[payload.runs % tips.length];
562
- console.log(
2573
+ out.stderr.write(
563
2574
  `
564
2575
  ${cyan(`\u{1F680} DRZL finished a ${reason} run (#${payload.runs.toLocaleString()}).`)}
565
2576
 
@@ -567,13 +2578,14 @@ ${green("\u2728 Sponsors keep DRZL shipping. Consider supporting ongoing dev:")}
567
2578
  ${green("GitHub Sponsors")} ${gray("\u2192 https://github.com/sponsors/omar-dulaimi")}
568
2579
 
569
2580
  ${green("Pro tip:")} ${tip}
2581
+
570
2582
  `
571
2583
  );
572
2584
  } catch {
573
2585
  }
574
2586
  }
575
2587
  function readCache() {
576
- if (!existsSync(CACHE_FILE)) {
2588
+ if (!existsSync3(CACHE_FILE)) {
577
2589
  return { runs: 0 };
578
2590
  }
579
2591
  try {
@@ -585,16 +2597,16 @@ function readCache() {
585
2597
  }
586
2598
  }
587
2599
  function writeCache(payload) {
588
- writeFileSync(CACHE_FILE, JSON.stringify(payload, null, 2), "utf8");
2600
+ writeFileSync2(CACHE_FILE, JSON.stringify(payload, null, 2), "utf8");
589
2601
  }
590
2602
 
591
2603
  // src/version.ts
592
2604
  import { readFileSync as readFileSync2 } from "fs";
593
- import * as path3 from "path";
2605
+ import * as path6 from "path";
594
2606
  import { fileURLToPath } from "url";
595
2607
  var PACKAGE_NAME = "@drzl/cli";
596
2608
  function moduleDir() {
597
- return path3.dirname(fileURLToPath(import.meta.url));
2609
+ return path6.dirname(fileURLToPath(import.meta.url));
598
2610
  }
599
2611
  function readVersionFrom(manifestPath) {
600
2612
  let raw;
@@ -617,21 +2629,66 @@ function readVersionFrom(manifestPath) {
617
2629
  return manifest.version;
618
2630
  }
619
2631
  function readCliVersion() {
620
- return readVersionFrom(path3.join(moduleDir(), "..", "package.json"));
2632
+ return readVersionFrom(path6.join(moduleDir(), "..", "package.json"));
621
2633
  }
622
2634
  var CLI_VERSION = readCliVersion();
623
2635
 
624
2636
  // src/cli.ts
625
- function reportGeneratorFailure(kind, e) {
2637
+ function reportGeneratorFailure(out, kind, e) {
626
2638
  if (e instanceof GeneratorNotInstalledError) {
627
- console.error(
628
- chalk3.red(`The ${kind} generator is not installed.`),
629
- chalk3.yellow(`
630
- 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
+ )
631
2687
  );
632
- return;
633
2688
  }
634
- 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);
635
2692
  }
636
2693
  var program = new Command();
637
2694
  program.name("drzl").description("DRZL - Drizzle Developer Toolkit").version(CLI_VERSION);
@@ -642,374 +2699,531 @@ Need a template, adapter, or generator DRZL doesn't ship yet?
642
2699
  \u2192 DM @omardulaimidev on X: https://x.com/omardulaimidev
643
2700
  `
644
2701
  );
645
- 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);
646
2706
  try {
647
- const analyzer = new SchemaAnalyzer(schema);
648
- const spinner = !opts.json ? ora("Analyzing schema...").start() : null;
2707
+ const analyzer = new SchemaAnalyzer2(schema);
2708
+ const spinner = out.spinner("Analyzing schema...");
649
2709
  const start = Date.now();
650
2710
  const res = await analyzer.analyze({
651
2711
  includeRelations: !!opts.relations,
652
2712
  validateConstraints: !!opts.validate
653
2713
  });
654
2714
  const ms = Date.now() - start;
655
- const json = JSON.stringify(res, null, 2);
656
- if (opts.json) {
657
- console.log(json);
658
- } else if (opts.out) {
659
- const fs2 = await import("fs/promises");
660
- await fs2.writeFile(opts.out, json, "utf8");
661
- 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`);
662
2724
  } else {
663
- spinner?.succeed(chalk3.green(`Analyzed in ${ms}ms`));
664
- 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));
665
2728
  }
666
- process.exit(res.issues.some((i) => i.level === "error") ? 2 : 0);
2729
+ process.exit(code);
667
2730
  } catch (e) {
668
- const msg = e?.message ?? String(e);
669
- if (opts.json)
670
- console.log(JSON.stringify({ event: "error", code: "DRZL_CLI_ANALYZE", message: msg }));
671
- else
672
- console.error(
673
- chalk3.red("Analyze failed (DRZL_CLI_ANALYZE):"),
674
- msg,
675
- "\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
676
2766
  );
677
- 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
+ }
678
2785
  }
679
2786
  });
680
- 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) => {
681
- try {
682
- let target = schema;
683
- if (!target) {
684
- const cfg = await loadConfig(opts.config);
685
- 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);
686
2820
  }
687
- if (!target) {
688
- const msg = "No schema given. Pass a path, or run from a directory with a drzl.config.";
689
- if (opts.json)
690
- console.log(JSON.stringify({ event: "error", code: "DRZL_CLI_DOCTOR", message: msg }));
691
- else console.error(chalk3.red("Doctor failed (DRZL_CLI_DOCTOR):"), msg);
692
- 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
+ });
693
2831
  return;
694
2832
  }
695
- const analyzer = new SchemaAnalyzer(target);
696
- 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.
697
2839
  includeRelations: true,
698
2840
  validateConstraints: true
699
2841
  });
700
- const report = buildDoctorReport(analysis, target);
701
- if (opts.json) console.log(JSON.stringify(report, null, 2));
702
- else console.log(renderDoctorReport(report));
703
- if (report.findings.some((f) => f.level === "error")) {
704
- process.exit(1);
705
- 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);
706
2856
  }
707
- process.exit(opts.strict && report.findings.length ? 2 : 0);
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
+ }
2876
+ }
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);
708
2888
  } catch (e) {
709
- const msg = e?.message ?? String(e);
710
- if (opts.json)
711
- console.log(JSON.stringify({ event: "error", code: "DRZL_CLI_DOCTOR", message: msg }));
712
- else
713
- console.error(
714
- chalk3.red("Doctor failed (DRZL_CLI_DOCTOR):"),
715
- msg,
716
- "\nTip: run with --json for structured output."
717
- );
718
- 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);
719
2898
  }
720
2899
  });
721
- program.command("generate").description("Run configured generators (drzl.config.*)").option("-c, --config <path>", "path to drzl.config").option(
722
- "--check",
723
- "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)
724
2908
  ).action(async (opts) => {
725
- try {
726
- const cfg = await loadConfig(opts.config);
727
- if (!cfg) {
728
- console.error(
729
- chalk3.red("No config found (DRZL_CFG_001). Create drzl.config.ts or pass --config.")
730
- );
731
- process.exit(2);
732
- return;
733
- }
734
- const analyzer = new SchemaAnalyzer(cfg.schema);
735
- const spinner = ora("Analyzing...").start();
736
- const t0 = Date.now();
737
- const analysis = await analyzer.analyze({
738
- includeRelations: cfg.analyzer.includeRelations,
739
- validateConstraints: cfg.analyzer.validateConstraints,
740
- includeHeuristicRelations: cfg.analyzer.includeHeuristicRelations
741
- });
742
- spinner.succeed(`Analysis complete in ${Date.now() - t0}ms`);
743
- const narrowed = filterColumns(analysis.tables, cfg.columns);
744
- const filterWarnings = tableFilterWarnings(narrowed.tables, cfg);
745
- analysis.tables = filterTables(narrowed.tables, cfg);
746
- for (const w of [...narrowed.warnings, ...filterWarnings]) console.warn(chalk3.yellow(w));
747
- reportWideColumns(analysis.issues);
748
- const driftDirs = computeGeneratorOutputDirs(cfg);
749
- const driftBefore = opts.check ? await snapshotAll(driftDirs) : null;
750
- const progress = new cliProgress.SingleBar(
751
- { hideCursor: true },
752
- cliProgress.Presets.shades_classic
753
- );
754
- const total = analysis.tables.length || 1;
755
- progress.start(total, 0);
756
- const servicesDir = cfg.generators.find((x) => x.kind === "service")?.path ?? "src/services";
757
- for (const g of cfg.generators) {
758
- if (g.kind === "orpc") {
759
- const gen = new ORPCGenerator(analysis);
760
- const { files } = await gen.generate({
761
- outputDir: cfg.outDir,
762
- template: g.template,
763
- includeRelations: g.includeRelations,
764
- naming: g.naming,
765
- outputHeader: g.outputHeader,
766
- format: g.format,
767
- templateOptions: g.templateOptions,
768
- importExtension: g.importExtension,
769
- validation: g.validation,
770
- // Documented on this generator since it was added and never reachable from a config
771
- // file, because the config schema had no such key and zod stripped it in silence.
772
- databaseInjection: g.databaseInjection,
773
- servicesDir,
774
- onProgress: ({ index }) => progress.update(index)
775
- });
776
- progress.stop();
777
- ora().succeed(chalk3.green(`Generated (${g.kind}): ${files.length} files`));
778
- files.forEach((f) => console.log(" -", chalk3.cyan(f)));
779
- } else if (g.kind === "trpc") {
780
- try {
781
- const { TRPCGenerator } = await loadGenerator(
782
- "@drzl/generator-trpc",
783
- () => import("./dist-XBGVORL3.js")
784
- );
785
- const gen = new TRPCGenerator(analysis);
786
- const { files } = await gen.generate({
787
- ...trpcOptions(g, cfg, servicesDir),
788
- onProgress: ({ index }) => progress.update(index)
789
- });
790
- progress.stop();
791
- ora().succeed(chalk3.green(`Generated (trpc): ${files.length} files`));
792
- files.forEach((f) => console.log(" -", chalk3.cyan(f)));
793
- } catch (e) {
794
- progress.stop();
795
- reportGeneratorFailure(g.kind, e);
796
- 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>.");
797
2930
  }
798
- } else if (g.kind === "service") {
799
- try {
800
- const { ServiceGenerator } = await loadGenerator(
801
- "@drzl/generator-service",
802
- () => import("@drzl/generator-service")
803
- );
804
- const gen = new ServiceGenerator(analysis);
805
- const target = g.path ?? "src/services";
806
- const files = await gen.generate({
807
- outDir: target,
808
- outputHeader: g.outputHeader,
809
- format: g.format,
810
- dataAccess: g.dataAccess,
811
- dbImportPath: g.dbImportPath,
812
- schemaImportPath: g.schemaImportPath,
813
- importExtension: g.importExtension,
814
- // The other half of `databaseInjection`. A router generator in injection mode
815
- // emits `Service.getById(ctx.db, id)`, and only a service generated in the same
816
- // mode has a `db` parameter to receive it. This branch never passed the option, so
817
- // the two halves of one generated project disagreed about the signature.
818
- databaseInjection: g.databaseInjection
819
- });
820
- progress.stop();
821
- ora().succeed(chalk3.green(`Generated (service): ${files.length} files`));
822
- files.forEach((f) => console.log(" -", chalk3.cyan(f)));
823
- } catch (e) {
824
- progress.stop();
825
- reportGeneratorFailure(g.kind, e);
826
- 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.');
827
2944
  }
828
- } else if (g.kind === "zod") {
829
- try {
830
- const { ZodGenerator } = await loadGenerator(
831
- "@drzl/generator-zod",
832
- () => import("@drzl/generator-zod")
833
- );
834
- const gen = new ZodGenerator(analysis);
835
- const target = g.path ?? "src/validators/zod";
836
- const files = await gen.generate(
837
- validationOptions(g, cfg, target, { schemaTypes: true, meta: true })
838
- );
839
- progress.stop();
840
- ora().succeed(chalk3.green(`Generated (zod): ${files.length} files`));
841
- files.forEach((f) => console.log(" -", chalk3.cyan(f)));
842
- } catch (e) {
843
- progress.stop();
844
- reportGeneratorFailure(g.kind, e);
845
- 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);
846
3005
  }
847
- } else if (g.kind === "valibot") {
848
- try {
849
- const { ValibotGenerator } = await loadGenerator(
850
- "@drzl/generator-valibot",
851
- () => import("@drzl/generator-valibot")
852
- );
853
- const gen = new ValibotGenerator(analysis);
854
- const target = g.path ?? "src/validators/valibot";
855
- const files = await gen.generate(
856
- validationOptions(g, cfg, target, { schemaTypes: true })
857
- );
858
- progress.stop();
859
- ora().succeed(chalk3.green(`Generated (valibot): ${files.length} files`));
860
- files.forEach((f) => console.log(" -", chalk3.cyan(f)));
861
- } catch (e) {
862
- progress.stop();
863
- reportGeneratorFailure(g.kind, e);
864
- 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)));
865
3024
  }
866
- } 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;
867
3038
  try {
868
- const { ArkTypeGenerator } = await loadGenerator(
869
- "@drzl/generator-arktype",
870
- () => import("@drzl/generator-arktype")
871
- );
872
- const gen = new ArkTypeGenerator(analysis);
873
- const target = g.path ?? "src/validators/arktype";
874
- const files = await gen.generate(
875
- validationOptions(g, cfg, target, { schemaTypes: false })
876
- );
877
- progress.stop();
878
- ora().succeed(chalk3.green(`Generated (arktype): ${files.length} files`));
879
- 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);
880
3046
  } catch (e) {
881
- progress.stop();
882
- reportGeneratorFailure(g.kind, e);
883
- process.exit(1);
3047
+ failGenerator(g.kind, e);
884
3048
  }
885
- } else if (g.kind === "json-schema") {
886
- try {
887
- const { JsonSchemaGenerator } = await loadGenerator(
888
- "@drzl/generator-json-schema",
889
- () => import("./dist-K6N4F3XW.js")
890
- );
891
- const gen = new JsonSchemaGenerator(analysis);
892
- const target = g.path ?? "src/validators/json-schema";
893
- const files = await gen.generate(jsonSchemaOptions(g, cfg, target));
894
- progress.stop();
895
- ora().succeed(chalk3.green(`Generated (json-schema): ${files.length} files`));
896
- files.forEach((f) => console.log(" -", chalk3.cyan(f)));
897
- } catch (e) {
898
- progress.stop();
899
- reportGeneratorFailure(g.kind, e);
900
- 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);
901
3062
  }
902
- } else if (g.kind === "typebox") {
903
- try {
904
- const { TypeBoxGenerator } = await loadGenerator(
905
- "@drzl/generator-typebox",
906
- () => import("@drzl/generator-typebox")
907
- );
908
- const gen = new TypeBoxGenerator(analysis);
909
- const target = g.path ?? "src/validators/typebox";
910
- const files = await gen.generate(
911
- validationOptions(g, cfg, target, {
912
- schemaTypes: true,
913
- standardSchema: true
914
- })
915
- );
916
- progress.stop();
917
- ora().succeed(chalk3.green(`Generated (typebox): ${files.length} files`));
918
- files.forEach((f) => console.log(" -", chalk3.cyan(f)));
919
- } catch (e) {
920
- progress.stop();
921
- reportGeneratorFailure(g.kind, e);
922
- 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);
923
3091
  }
924
- } else if (g.kind === "effect") {
925
- try {
926
- const { EffectGenerator } = await loadGenerator(
927
- "@drzl/generator-effect",
928
- () => import("./dist-WHAW3AMQ.js")
929
- );
930
- const gen = new EffectGenerator(analysis);
931
- const target = g.path ?? "src/validators/effect";
932
- const files = await gen.generate(
933
- validationOptions(g, cfg, target, { schemaTypes: true })
934
- );
935
- progress.stop();
936
- ora().succeed(chalk3.green(`Generated (effect): ${files.length} files`));
937
- files.forEach((f) => console.log(" -", chalk3.cyan(f)));
938
- } catch (e) {
939
- progress.stop();
940
- reportGeneratorFailure(g.kind, e);
941
- 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);
942
3105
  }
3106
+ out.succeed(out.errStyle.green("Generated output is up to date."));
3107
+ process.exit(code);
943
3108
  }
944
- }
945
- if (driftBefore) {
946
- const after = await snapshotAll(driftDirs);
947
- const drift = diffSnapshots(driftBefore, after);
948
- await restoreSnapshot(driftBefore, after);
949
- if (drift.length) {
950
- console.error(chalk3.red(`
951
- Generated output is out of date (${drift.length} file(s)):`));
952
- for (const d of drift) {
953
- const mark = d.status === "added" ? "+" : d.status === "removed" ? "-" : "~";
954
- console.error(
955
- ` ${mark} ${chalk3.yellow(d.status.padEnd(8))} ${path4.relative(process.cwd(), d.file)}`
956
- );
957
- }
958
- console.error(
959
- chalk3.dim(
960
- "\nRun `drzl generate` and commit the result. Nothing was written by this check."
961
- )
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.`)
962
3125
  );
963
- process.exit(1);
3126
+ process.exit(EXIT_OK);
964
3127
  }
965
- console.log(chalk3.green("Generated output is up to date."));
966
- return;
967
- }
968
- if (cfg.generators.length) {
969
- 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);
970
3150
  }
971
- } catch (e) {
972
- console.error(
973
- chalk3.red("Generate failed (DRZL_GEN_001):"),
974
- e?.message ?? e,
975
- "\nTip: check your drzl.config.ts and template path."
976
- );
977
- process.exit(1);
978
3151
  }
979
3152
  });
980
- 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));
981
3175
  try {
982
- const analyzer = new SchemaAnalyzer(schema);
3176
+ const analyzer = new SchemaAnalyzer2(schema);
983
3177
  const analysis = await analyzer.analyze({
984
3178
  includeRelations: !!opts.includeRelations,
985
3179
  validateConstraints: true
986
3180
  });
987
- const gen = new ORPCGenerator(analysis);
988
- 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, {
989
3184
  outputDir: opts.outDir,
990
3185
  template: opts.template,
991
3186
  includeRelations: !!opts.includeRelations
992
3187
  });
993
- console.log(chalk3.green(`Generated:`), files.map((f) => chalk3.cyan(f)).join(", "));
994
- 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 });
995
3201
  } catch (e) {
996
- console.error(chalk3.red("Generate orpc failed:"), e?.message ?? e);
997
- 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);
998
3211
  }
999
3212
  });
1000
- 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));
1001
3218
  try {
1002
- const analyzer = new SchemaAnalyzer(schema);
3219
+ const analyzer = new SchemaAnalyzer2(schema);
1003
3220
  const analysis = await analyzer.analyze({
1004
3221
  includeRelations: !!opts.includeRelations,
1005
3222
  validateConstraints: true
1006
3223
  });
1007
- const { TRPCGenerator } = await loadGenerator(
1008
- "@drzl/generator-trpc",
1009
- () => import("./dist-XBGVORL3.js")
1010
- );
1011
- const gen = new TRPCGenerator(analysis);
1012
- 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, {
1013
3227
  outputDir: opts.outDir,
1014
3228
  template: opts.template,
1015
3229
  includeRelations: !!opts.includeRelations,
@@ -1017,27 +3231,95 @@ program.command("generate:trpc").argument("<schema>", "path to drizzle schema (T
1017
3231
  // cannot become the branch that forgets it.
1018
3232
  servicesDir: opts.servicesDir
1019
3233
  });
1020
- console.log(chalk3.green(`Generated:`), files.map((f) => chalk3.cyan(f)).join(", "));
1021
- 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 });
1022
3249
  } catch (e) {
1023
- reportGeneratorFailure("trpc", e);
1024
- 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);
1025
3253
  }
1026
3254
  });
1027
- 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) => {
1028
- let cfg = await loadConfig(opts.config);
1029
- if (!cfg) {
1030
- console.error(chalk3.red("No config found. Create drzl.config.ts or pass --config."));
1031
- 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);
1032
3297
  return;
1033
3298
  }
1034
- 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);
1035
3306
  const isInside = (child, parent) => {
1036
- const rel = path4.relative(parent, child);
1037
- return !!rel && !rel.startsWith("..") && !path4.isAbsolute(rel);
3307
+ const rel = path7.relative(parent, child);
3308
+ return !!rel && !rel.startsWith("..") && !path7.isAbsolute(rel);
1038
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);
1039
3319
  const ignoredOutDirs = new Set(computeGeneratorOutputDirs(cfg).map(abs));
1040
- const currentTargets = new Set(computeWatchTargets(cfg).map(abs));
3320
+ const currentTargets = new Set(
3321
+ computeWatchTargets(cfg, process.cwd(), source).map(abs)
3322
+ );
1041
3323
  const syncWatcherTargets = (watcher2, next) => {
1042
3324
  const add = [];
1043
3325
  const del = [];
@@ -1059,7 +3341,7 @@ program.command("watch").description("Watch schema and regenerate on changes").o
1059
3341
  if (full === dir || isInside(full, dir)) return true;
1060
3342
  }
1061
3343
  if (stats?.isDirectory()) return false;
1062
- const ext = path4.extname(full);
3344
+ const ext = path7.extname(full);
1063
3345
  if (!ext) return false;
1064
3346
  return !WATCHED_EXTENSIONS.has(ext);
1065
3347
  };
@@ -1070,7 +3352,7 @@ program.command("watch").description("Watch schema and regenerate on changes").o
1070
3352
  ignored: ignoredFn
1071
3353
  });
1072
3354
  const logTrigger = (type, file) => {
1073
- if (opts.json) console.log(JSON.stringify({ event: "trigger", type, file }));
3355
+ if (opts.json) out.jsonData({ event: "trigger", type, file });
1074
3356
  };
1075
3357
  watcher.on("add", (p) => {
1076
3358
  logTrigger("add", p);
@@ -1083,264 +3365,127 @@ program.command("watch").description("Watch schema and regenerate on changes").o
1083
3365
  trigger(p);
1084
3366
  });
1085
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
+ };
1086
3377
  const run = async () => {
1087
3378
  try {
1088
- const reloaded = await loadConfig(opts.config);
3379
+ const reloaded = await loadConfig(opts.config, (w) => out.warn(w));
1089
3380
  if (!reloaded) throw new Error("Config disappeared during watch.");
1090
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
+ }
1091
3386
  rebuildIgnoreDirsFrom(cfg);
1092
- const nextTargets = new Set(computeWatchTargets(cfg).map(abs));
3387
+ const nextTargets = new Set(
3388
+ computeWatchTargets(cfg, process.cwd(), source).map(abs)
3389
+ );
1093
3390
  syncWatcherTargets(watcher, nextTargets);
1094
- if (!opts.json) console.clear();
3391
+ clearScreen();
1095
3392
  if (opts.json) {
1096
- console.log(
1097
- JSON.stringify({
1098
- event: "watch_config_applied",
1099
- targets: Array.from(currentTargets),
1100
- ignored: Array.from(ignoredOutDirs)
1101
- })
1102
- );
3393
+ out.jsonData({
3394
+ event: "watch_config_applied",
3395
+ targets: Array.from(currentTargets),
3396
+ ignored: Array.from(ignoredOutDirs)
3397
+ });
1103
3398
  }
1104
- const analyzer = new SchemaAnalyzer(cfg.schema);
3399
+ for (const w of source.warnings) out.warn(w);
3400
+ const analyzer = new SchemaAnalyzer2(source.schema);
1105
3401
  const analysis = await analyzer.analyze({
1106
3402
  includeRelations: cfg.analyzer.includeRelations,
1107
3403
  validateConstraints: cfg.analyzer.validateConstraints,
1108
3404
  includeHeuristicRelations: cfg.analyzer.includeHeuristicRelations
1109
3405
  });
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
+ }
1110
3417
  const narrowed = filterColumns(analysis.tables, cfg.columns);
1111
3418
  const filterWarnings = tableFilterWarnings(narrowed.tables, cfg);
1112
3419
  analysis.tables = filterTables(narrowed.tables, cfg);
1113
- if (!opts.json)
1114
- for (const w of [...narrowed.warnings, ...filterWarnings]) console.warn(chalk3.yellow(w));
1115
- if (!opts.json) reportWideColumns(analysis.issues);
1116
- if (opts.pipeline === "analyze") {
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) {
1117
3423
  if (opts.json) {
1118
- console.log(
1119
- JSON.stringify({
1120
- event: "analyze_complete",
1121
- issues: analysis.issues,
1122
- tables: analysis.tables.length
1123
- })
1124
- );
3424
+ out.jsonData({
3425
+ event: "analyze_complete",
3426
+ issues: analysis.issues,
3427
+ tables: analysis.tables.length
3428
+ });
1125
3429
  } else {
1126
- console.log(chalk3.green("Analyze complete."));
3430
+ out.succeed("Analyze complete.");
1127
3431
  }
1128
3432
  return;
1129
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
+ }
1130
3443
  const newFiles = [];
1131
- const servicesDir = cfg.generators.find((x) => x.kind === "service")?.path ?? "src/services";
1132
- const PIPELINE_KINDS = {
1133
- "generate-orpc": "orpc",
1134
- "generate-trpc": "trpc"
1135
- };
1136
- for (const g of cfg.generators) {
1137
- if (opts.pipeline !== "all" && PIPELINE_KINDS[opts.pipeline] !== g.kind) {
1138
- 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.');
1139
3451
  }
1140
- if (g.kind === "orpc") {
1141
- const gen = new ORPCGenerator(analysis);
1142
- const { files } = await gen.generate({
1143
- outputDir: cfg.outDir,
1144
- template: g.template,
1145
- includeRelations: g.includeRelations,
1146
- naming: g.naming,
1147
- outputHeader: g.outputHeader,
1148
- format: g.format,
1149
- templateOptions: g.templateOptions,
1150
- importExtension: g.importExtension,
1151
- validation: g.validation,
1152
- databaseInjection: g.databaseInjection,
1153
- servicesDir
1154
- });
1155
- opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
1156
- chalk3.green(`Generated (${g.kind}):`),
1157
- files.map((f) => chalk3.cyan(f)).join(", ")
1158
- );
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);
1159
3460
  newFiles.push(...files);
1160
- } else if (g.kind === "trpc") {
1161
- try {
1162
- const { TRPCGenerator } = await loadGenerator(
1163
- "@drzl/generator-trpc",
1164
- () => import("./dist-XBGVORL3.js")
1165
- );
1166
- const gen = new TRPCGenerator(analysis);
1167
- const { files } = await gen.generate(trpcOptions(g, cfg, servicesDir));
1168
- opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
1169
- chalk3.green(`Generated (trpc): ${files.length} files`),
1170
- files.map((f) => chalk3.cyan(f)).join(", ")
1171
- );
1172
- newFiles.push(...files);
1173
- } catch (e) {
1174
- reportGeneratorFailure(g.kind, e);
1175
- return;
1176
- }
1177
- } else if (g.kind === "service") {
1178
- try {
1179
- const { ServiceGenerator } = await loadGenerator(
1180
- "@drzl/generator-service",
1181
- () => import("@drzl/generator-service")
1182
- );
1183
- const gen = new ServiceGenerator(analysis);
1184
- const target = g.path ?? "src/services";
1185
- const files = await gen.generate({
1186
- outDir: target,
1187
- outputHeader: g.outputHeader,
1188
- format: g.format,
1189
- dataAccess: g.dataAccess,
1190
- dbImportPath: g.dbImportPath,
1191
- schemaImportPath: g.schemaImportPath,
1192
- importExtension: g.importExtension,
1193
- databaseInjection: g.databaseInjection
1194
- });
1195
- opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
1196
- chalk3.green(`Generated (service): ${files.length} files`),
1197
- files.map((f) => chalk3.cyan(f)).join(", ")
1198
- );
1199
- newFiles.push(...files);
1200
- } catch (e) {
1201
- reportGeneratorFailure(g.kind, e);
1202
- return;
1203
- }
1204
- } else if (g.kind === "zod") {
1205
- try {
1206
- const { ZodGenerator } = await loadGenerator(
1207
- "@drzl/generator-zod",
1208
- () => import("@drzl/generator-zod")
1209
- );
1210
- const gen = new ZodGenerator(analysis);
1211
- const target = g.path ?? "src/validators/zod";
1212
- const files = await gen.generate(
1213
- validationOptions(g, cfg, target, { schemaTypes: true, meta: true })
1214
- );
1215
- opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
1216
- chalk3.green(`Generated (zod): ${files.length} files`),
1217
- files.map((f) => chalk3.cyan(f)).join(", ")
1218
- );
1219
- newFiles.push(...files);
1220
- } catch (e) {
1221
- reportGeneratorFailure(g.kind, e);
1222
- return;
1223
- }
1224
- } else if (g.kind === "valibot") {
1225
- try {
1226
- const { ValibotGenerator } = await loadGenerator(
1227
- "@drzl/generator-valibot",
1228
- () => import("@drzl/generator-valibot")
1229
- );
1230
- const gen = new ValibotGenerator(analysis);
1231
- const target = g.path ?? "src/validators/valibot";
1232
- const files = await gen.generate(
1233
- validationOptions(g, cfg, target, { schemaTypes: true })
1234
- );
1235
- opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
1236
- chalk3.green(`Generated (valibot): ${files.length} files`),
1237
- files.map((f) => chalk3.cyan(f)).join(", ")
1238
- );
1239
- newFiles.push(...files);
1240
- } catch (e) {
1241
- reportGeneratorFailure(g.kind, e);
1242
- return;
1243
- }
1244
- } else if (g.kind === "arktype") {
1245
- try {
1246
- const { ArkTypeGenerator } = await loadGenerator(
1247
- "@drzl/generator-arktype",
1248
- () => import("@drzl/generator-arktype")
1249
- );
1250
- const gen = new ArkTypeGenerator(analysis);
1251
- const target = g.path ?? "src/validators/arktype";
1252
- const files = await gen.generate(
1253
- validationOptions(g, cfg, target, { schemaTypes: false })
1254
- );
1255
- opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
1256
- chalk3.green(`Generated (arktype): ${files.length} files`),
1257
- files.map((f) => chalk3.cyan(f)).join(", ")
1258
- );
1259
- newFiles.push(...files);
1260
- } catch (e) {
1261
- reportGeneratorFailure(g.kind, e);
1262
- return;
1263
- }
1264
- } else if (g.kind === "typebox") {
1265
- try {
1266
- const { TypeBoxGenerator } = await loadGenerator(
1267
- "@drzl/generator-typebox",
1268
- () => import("@drzl/generator-typebox")
1269
- );
1270
- const gen = new TypeBoxGenerator(analysis);
1271
- const target = g.path ?? "src/validators/typebox";
1272
- const files = await gen.generate(
1273
- validationOptions(g, cfg, target, {
1274
- schemaTypes: true,
1275
- standardSchema: true
1276
- })
1277
- );
1278
- opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
1279
- chalk3.green(`Generated (typebox): ${files.length} files`),
1280
- files.map((f) => chalk3.cyan(f)).join(", ")
1281
- );
1282
- newFiles.push(...files);
1283
- } catch (e) {
1284
- reportGeneratorFailure(g.kind, e);
1285
- return;
1286
- }
1287
- } else if (g.kind === "effect") {
1288
- try {
1289
- const { EffectGenerator } = await loadGenerator(
1290
- "@drzl/generator-effect",
1291
- () => import("./dist-WHAW3AMQ.js")
1292
- );
1293
- const gen = new EffectGenerator(analysis);
1294
- const target = g.path ?? "src/validators/effect";
1295
- const files = await gen.generate(
1296
- validationOptions(g, cfg, target, { schemaTypes: true })
1297
- );
1298
- opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
1299
- chalk3.green(`Generated (effect): ${files.length} files`),
1300
- files.map((f) => chalk3.cyan(f)).join(", ")
1301
- );
1302
- newFiles.push(...files);
1303
- } catch (e) {
1304
- reportGeneratorFailure(g.kind, e);
1305
- return;
1306
- }
1307
- } else if (g.kind === "json-schema") {
1308
- try {
1309
- const { JsonSchemaGenerator } = await loadGenerator(
1310
- "@drzl/generator-json-schema",
1311
- () => import("./dist-K6N4F3XW.js")
1312
- );
1313
- const gen = new JsonSchemaGenerator(analysis);
1314
- const target = g.path ?? "src/validators/json-schema";
1315
- const files = await gen.generate(jsonSchemaOptions(g, cfg, target));
1316
- opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
1317
- chalk3.green(`Generated (json-schema): ${files.length} files`),
1318
- files.map((f) => chalk3.cyan(f)).join(", ")
1319
- );
1320
- newFiles.push(...files);
1321
- } catch (e) {
1322
- reportGeneratorFailure(g.kind, e);
1323
- return;
1324
- }
3461
+ } catch (e) {
3462
+ reportGeneratorFailure(out, g.kind, e);
3463
+ return;
1325
3464
  }
1326
3465
  }
1327
3466
  const added = newFiles.filter((f) => !lastFiles.includes(f));
1328
3467
  const removed = lastFiles.filter((f) => !newFiles.includes(f));
1329
- opts.json ? console.log(JSON.stringify({ event: "diff", added, removed })) : (() => {
1330
- if (added.length) console.log(chalk3.blue(`Added: ${added.join(", ")}`));
1331
- if (removed.length) console.log(chalk3.yellow(`Removed: ${removed.join(", ")}`));
1332
- })();
1333
- if (newFiles.length && !opts.json) {
1334
- const reason = opts.pipeline && opts.pipeline !== "all" ? `watch:${opts.pipeline}` : "watch";
1335
- 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 });
1336
3477
  }
1337
3478
  lastFiles = newFiles;
1338
3479
  } catch (e) {
1339
- 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);
1340
3483
  }
1341
3484
  };
1342
- const debounced = Number(opts.debounce) || 200;
1343
- let timer = null;
3485
+ const scheduler = createRebuildScheduler({
3486
+ run,
3487
+ debounceMs: resolveDebounce(opts.debounce, (w) => out.warn(w))
3488
+ });
1344
3489
  const trigger = (file) => {
1345
3490
  if (file) {
1346
3491
  const full = abs(file);
@@ -1348,62 +3493,73 @@ program.command("watch").description("Watch schema and regenerate on changes").o
1348
3493
  if (full === dir || isInside(full, dir)) return;
1349
3494
  }
1350
3495
  }
1351
- if (timer) clearTimeout(timer);
1352
- timer = setTimeout(run, debounced);
3496
+ scheduler.trigger();
1353
3497
  };
1354
3498
  if (opts.json) {
1355
- console.log(
1356
- JSON.stringify({
1357
- event: "watching",
1358
- targets: Array.from(currentTargets),
1359
- ignored: Array.from(ignoredOutDirs)
1360
- })
1361
- );
3499
+ out.jsonData({
3500
+ event: "watching",
3501
+ targets: Array.from(currentTargets),
3502
+ ignored: Array.from(ignoredOutDirs)
3503
+ });
1362
3504
  } else {
1363
- console.log(
1364
- chalk3.gray(
1365
- "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 ")
1366
3508
  )
1367
3509
  );
1368
3510
  }
1369
- 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));
1370
- 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();
1371
3513
  });
1372
- program.command("init").description("Scaffold a drzl.config.ts").option("-y, --yes", "accept defaults").action(async (_opts) => {
1373
- const fs2 = await import("fs/promises");
1374
- const path5 = await import("path");
1375
- const target = path5.resolve(process.cwd(), "drzl.config.ts");
1376
- const template = `export default {
1377
- schema: 'src/db/schema.ts',
1378
- outDir: 'src/api',
1379
- analyzer: { includeRelations: true, validateConstraints: true },
1380
- generators: [
1381
- // For tRPC instead: { kind: 'trpc', template: 'standard', includeRelations: true }
1382
- // To run both, give one of them its own \`path\`; they share \`outDir\` otherwise.
1383
- { kind: 'orpc', template: 'standard', includeRelations: true }
1384
- ]
1385
- } as const
1386
- `;
1387
- try {
1388
- await fs2.writeFile(target, template, { flag: "wx" });
1389
- console.log(chalk3.green(`Created ${target}`));
1390
- } catch (e) {
1391
- console.error(chalk3.red("Init failed:"), e?.message ?? e);
1392
- 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
+ );
1393
3550
  }
3551
+ process.exit(outcome.code === 0 ? EXIT_OK : EXIT_FAILED);
1394
3552
  });
1395
- function reportWideColumns(issues) {
3553
+ function wideColumnWarning(issues) {
1396
3554
  const wide = issues.filter((i) => i.code === "DRZL_ANL_UNKNOWN_COLUMN");
1397
- if (!wide.length) return;
1398
- console.warn(
1399
- chalk3.yellow(`
1400
- ${wide.length} column${wide.length === 1 ? "" : "s"} could not be typed:`)
1401
- );
1402
- for (const i of wide.slice(0, 10)) console.warn(chalk3.gray(` - ${i.message}`));
1403
- if (wide.length > 10) console.warn(chalk3.gray(` ... and ${wide.length - 10} more`));
1404
- const hints = [...new Set(wide.map((i) => i.hint).filter(Boolean))];
1405
- for (const h of hints) console.warn(chalk3.gray(` ${h}`));
1406
- 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")];
1407
3563
  }
1408
3564
  program.parseAsync(process.argv);
1409
3565
  //# sourceMappingURL=cli.js.map