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