@drzl/cli 4.37.0 → 4.39.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
@@ -30,19 +30,20 @@ import {
30
30
  nestjsOutDir,
31
31
  nextOutDir,
32
32
  openApiFetchOutDir,
33
+ pothosOutDir,
33
34
  seedOutDir,
34
35
  tableAliases,
35
36
  tableFilterWarnings,
36
37
  tanstackStartOutDir,
37
38
  trpcOutDir,
38
39
  tsRestOutDir
39
- } from "./chunk-EQ4OSVMJ.js";
40
+ } from "./chunk-VG6SD2GP.js";
40
41
 
41
42
  // src/cli.ts
42
43
  import { qualifiedTableName as qualifiedTableName2, SchemaAnalyzer as SchemaAnalyzer2 } from "@drzl/analyzer";
43
44
  import chokidar from "chokidar";
44
45
  import { Command } from "commander";
45
- import * as path7 from "path";
46
+ import * as path8 from "path";
46
47
 
47
48
  // src/output.ts
48
49
  import { Chalk } from "chalk";
@@ -531,6 +532,17 @@ function fastCheckOptions(g, cfg) {
531
532
  };
532
533
  }
533
534
 
535
+ // src/pothos-options.ts
536
+ function pothosOptions(g, cfg) {
537
+ return {
538
+ outputDir: pothosOutDir(g, cfg),
539
+ naming: g.naming,
540
+ outputHeader: g.outputHeader,
541
+ format: g.format,
542
+ importExtension: g.importExtension
543
+ };
544
+ }
545
+
534
546
  // src/h3-options.ts
535
547
  var VALIDATOR_DEFAULT_DIRS6 = {
536
548
  zod: "src/validators/zod",
@@ -893,6 +905,14 @@ var GENERATORS = [
893
905
  outputDir: (g, cfg) => fastCheckOutDir(g, cfg),
894
906
  options: (g, cfg) => fastCheckOptions(g, cfg)
895
907
  },
908
+ {
909
+ kind: "pothos",
910
+ specifier: "@drzl/generator-pothos",
911
+ load: () => import("@drzl/generator-pothos"),
912
+ construct: (m, analysis) => new m.PothosGenerator(analysis),
913
+ outputDir: (g, cfg) => pothosOutDir(g, cfg),
914
+ options: (g, cfg) => pothosOptions(g, cfg)
915
+ },
896
916
  {
897
917
  kind: "service",
898
918
  specifier: "@drzl/generator-service",
@@ -1136,14 +1156,109 @@ import { parseCheck as parseCheck2 } from "@drzl/validation-core";
1136
1156
 
1137
1157
  // src/doctor.ts
1138
1158
  import { lengthMeasure, parseCheck } from "@drzl/validation-core";
1159
+
1160
+ // src/auth-tables.ts
1161
+ var SIGNATURES = [
1162
+ {
1163
+ model: "account",
1164
+ // `providerId` beside `accountId` is the distinctive pair: it is a link to an external identity
1165
+ // provider, which an application table has no reason to model this way.
1166
+ required: ["accountId", "providerId", "userId"],
1167
+ supporting: ["accessToken", "refreshToken", "idToken", "password", "scope"],
1168
+ conventionalName: "account",
1169
+ secrets: ["accessToken", "refreshToken", "idToken", "password"]
1170
+ },
1171
+ {
1172
+ model: "session",
1173
+ required: ["token", "expiresAt", "userId"],
1174
+ supporting: ["ipAddress", "userAgent"],
1175
+ conventionalName: "session",
1176
+ // A session token is a bearer credential: whoever reads it is that user until it expires.
1177
+ secrets: ["token"]
1178
+ },
1179
+ {
1180
+ model: "verification",
1181
+ // `identifier` and `value` are deliberately generic names, which is why `expiresAt` is required
1182
+ // too: the three together are a short-lived token store and little else.
1183
+ required: ["identifier", "value", "expiresAt"],
1184
+ supporting: [],
1185
+ conventionalName: "verification",
1186
+ secrets: ["value"]
1187
+ },
1188
+ {
1189
+ model: "user",
1190
+ required: ["email", "emailVerified"],
1191
+ supporting: ["name", "image"],
1192
+ conventionalName: "user",
1193
+ secrets: []
1194
+ }
1195
+ ];
1196
+ function columnKeys(table) {
1197
+ const out = /* @__PURE__ */ new Map();
1198
+ for (const c of table.columns) out.set(normalise(c.name), c);
1199
+ return out;
1200
+ }
1201
+ function normalise(name) {
1202
+ return name.replace(/[_-]/g, "").toLowerCase();
1203
+ }
1204
+ function has(keys, name) {
1205
+ return keys.has(normalise(name));
1206
+ }
1207
+ function actualName(table, wanted) {
1208
+ return table.columns.find((c) => normalise(c.name) === normalise(wanted))?.name;
1209
+ }
1210
+ function matchOne(table, sig) {
1211
+ const keys = columnKeys(table);
1212
+ const missing = sig.required.filter((r) => !has(keys, r));
1213
+ if (missing.length) return void 0;
1214
+ const supporting = sig.supporting.filter((s) => has(keys, s));
1215
+ const nameMatches = normalise(table.name) === normalise(sig.conventionalName) || normalise(table.name) === `${normalise(sig.conventionalName)}s`;
1216
+ const confidence = supporting.length > 0 || nameMatches ? "strong" : "likely";
1217
+ const matched = [...sig.required, ...supporting].map((c) => actualName(table, c)).filter((c) => Boolean(c));
1218
+ const secrets = sig.secrets.map((c) => actualName(table, c)).filter((c) => Boolean(c));
1219
+ return { table: table.name, model: sig.model, confidence, matched, secrets };
1220
+ }
1221
+ function detectAuthTables(analysis) {
1222
+ const found = [];
1223
+ for (const table of analysis.tables) {
1224
+ for (const sig of SIGNATURES) {
1225
+ const m = matchOne(table, sig);
1226
+ if (m) {
1227
+ found.push(m);
1228
+ break;
1229
+ }
1230
+ }
1231
+ }
1232
+ const hasNonUser = found.some((f) => f.model !== "user");
1233
+ return hasNonUser ? found : found.filter((f) => f.model !== "user");
1234
+ }
1235
+ function authTablesWithSecrets(matches) {
1236
+ return matches.filter((m) => m.secrets.length > 0);
1237
+ }
1238
+ function excludeSuggestion(matches) {
1239
+ const names = [...new Set(matches.map((m) => m.table))].sort();
1240
+ return `exclude: [${names.map((n) => `'${n}'`).join(", ")}]`;
1241
+ }
1242
+ function authTableWarnings(analysis) {
1243
+ const risky = authTablesWithSecrets(detectAuthTables(analysis));
1244
+ if (!risky.length) return [];
1245
+ const lines = risky.map(
1246
+ (m) => `"${m.table}" looks like an authentication library's ${m.model} table and holds ${m.secrets.map((c) => `"${c}"`).join(", ")}. Generating for it publishes ${m.secrets.length === 1 ? "that column" : "those columns"}.`
1247
+ );
1248
+ return [
1249
+ `drzl generate: ${lines.join(" ")} Keep ${risky.length === 1 ? "it" : "them"} out with ${excludeSuggestion(risky)}, or leave it if the route is deliberate.`
1250
+ ];
1251
+ }
1252
+
1253
+ // src/doctor.ts
1139
1254
  import { Chalk as Chalk2 } from "chalk";
1140
1255
  var PLAIN = new Chalk2({ level: 0 });
1141
1256
  var HANDLED_CODES = /* @__PURE__ */ new Set(["DRZL_ANL_UNKNOWN_COLUMN"]);
1142
- function splitPath(path8) {
1143
- if (!path8) return {};
1144
- const dot = path8.lastIndexOf(".");
1145
- if (dot <= 0) return { table: path8 };
1146
- return { table: path8.slice(0, dot), column: path8.slice(dot + 1) };
1257
+ function splitPath(path9) {
1258
+ if (!path9) return {};
1259
+ const dot = path9.lastIndexOf(".");
1260
+ if (dot <= 0) return { table: path9 };
1261
+ return { table: path9.slice(0, dot), column: path9.slice(dot + 1) };
1147
1262
  }
1148
1263
  function namedColumns(parsed) {
1149
1264
  const out = [];
@@ -1308,6 +1423,18 @@ function buildDoctorReport(analysis, schemaPath) {
1308
1423
  hint: i.hint
1309
1424
  });
1310
1425
  }
1426
+ for (const match of detectAuthTables(analysis)) {
1427
+ const secrets = match.secrets.length ? ` It holds ${match.secrets.map((c) => `\`${c}\``).join(", ")}, which a generated read route would return to whoever calls it.` : "";
1428
+ findings.push({
1429
+ kind: "auth-table",
1430
+ // A warning rather than an error: this is a real leak and it is also a guess about someone
1431
+ // else's schema, and a doctor that failed the build on a guess would be switched off.
1432
+ level: "warn",
1433
+ table: match.table,
1434
+ message: `"${match.table}" matches the shape of an authentication library's ${match.model} table (${match.matched.join(", ")}).${secrets}`,
1435
+ hint: `Keep it out of every generator with ${excludeSuggestion([match])}.`
1436
+ });
1437
+ }
1311
1438
  for (const i of analysis.issues) {
1312
1439
  if (i.code !== "DRZL_ANL_UNKNOWN_COLUMN") continue;
1313
1440
  findings.push({
@@ -1346,6 +1473,11 @@ var SECTIONS = [
1346
1473
  title: "Columns DRZL cannot type",
1347
1474
  why: "These get a validator that accepts any value."
1348
1475
  },
1476
+ {
1477
+ kinds: ["auth-table"],
1478
+ title: "Tables that look like an authentication library's",
1479
+ why: "Every generator loops over every table it finds, so these get routes too."
1480
+ },
1349
1481
  {
1350
1482
  kinds: ["check-declined", "check-unknown-column", "check-not-scalar", "check-uncountable"],
1351
1483
  title: "CHECK constraints DRZL does not enforce",
@@ -1678,11 +1810,11 @@ function issueTouches(issue, table) {
1678
1810
  return dot > 0 && own.includes(issue.path.slice(0, dot));
1679
1811
  }
1680
1812
  function issueColumn(issue, table) {
1681
- const path8 = issue.path ?? "";
1813
+ const path9 = issue.path ?? "";
1682
1814
  const names = namesOf(table);
1683
1815
  for (const prefix of [names.qualified, names.tsName, names.name, table.name]) {
1684
- if (path8.startsWith(`${prefix}.`)) {
1685
- const rest = path8.slice(prefix.length + 1);
1816
+ if (path9.startsWith(`${prefix}.`)) {
1817
+ const rest = path9.slice(prefix.length + 1);
1686
1818
  if (table.columns.some((c) => c.name === rest)) return rest;
1687
1819
  }
1688
1820
  }
@@ -2841,6 +2973,79 @@ function displayPath(file, cwd = process.cwd()) {
2841
2973
  return rel && !rel.startsWith("..") ? rel : file;
2842
2974
  }
2843
2975
 
2976
+ // src/manifest.ts
2977
+ import path4 from "path";
2978
+ import { promises as fs4 } from "fs";
2979
+ var MANIFEST_VERSION = 1;
2980
+ var MANIFEST_PATH = path4.join(".drzl", "manifest.json");
2981
+ function toPosix(p) {
2982
+ return p.split(path4.sep).join("/");
2983
+ }
2984
+ function manifestEntries(files, root) {
2985
+ const rel = files.map((f) => toPosix(path4.relative(root, f)));
2986
+ return [...new Set(rel)].sort();
2987
+ }
2988
+ async function readManifest(root) {
2989
+ try {
2990
+ const raw = await fs4.readFile(path4.join(root, MANIFEST_PATH), "utf8");
2991
+ const parsed = JSON.parse(raw);
2992
+ if (!parsed || typeof parsed !== "object" || parsed.version !== MANIFEST_VERSION || !Array.isArray(parsed.files) || !parsed.files.every((f) => typeof f === "string")) {
2993
+ return void 0;
2994
+ }
2995
+ return { version: MANIFEST_VERSION, files: parsed.files };
2996
+ } catch {
2997
+ return void 0;
2998
+ }
2999
+ }
3000
+ async function writeManifest(root, files) {
3001
+ const manifest = { version: MANIFEST_VERSION, files: manifestEntries(files, root) };
3002
+ const target = path4.join(root, MANIFEST_PATH);
3003
+ await fs4.mkdir(path4.dirname(target), { recursive: true });
3004
+ await fs4.writeFile(target, `${JSON.stringify(manifest, null, 2)}
3005
+ `, "utf8");
3006
+ }
3007
+ async function staleFiles(previous, writtenNow, root) {
3008
+ if (!previous) return [];
3009
+ const current = new Set(manifestEntries(writtenNow, root));
3010
+ const gone = previous.files.filter((f) => !current.has(f));
3011
+ const out = [];
3012
+ for (const file of gone) {
3013
+ let present = true;
3014
+ try {
3015
+ await fs4.access(path4.join(root, file));
3016
+ } catch {
3017
+ present = false;
3018
+ }
3019
+ out.push({ file, present });
3020
+ }
3021
+ return out;
3022
+ }
3023
+ async function pruneStale(root, stale) {
3024
+ const deleted = [];
3025
+ for (const entry of stale) {
3026
+ if (!entry.present) continue;
3027
+ const target = path4.resolve(root, entry.file);
3028
+ const inside = target === root || target.startsWith(root + path4.sep);
3029
+ if (!inside) continue;
3030
+ try {
3031
+ await fs4.rm(target);
3032
+ deleted.push(entry.file);
3033
+ } catch {
3034
+ }
3035
+ }
3036
+ return deleted;
3037
+ }
3038
+ function staleWarning(stale) {
3039
+ const present = stale.filter((s) => s.present);
3040
+ if (!present.length) return void 0;
3041
+ const list = present.map((s) => s.file).join(", ");
3042
+ return `drzl generate: ${present.length} file(s) written by a previous run were not written by this one, and are still on disk: ${list}. That usually means a table was renamed or removed. Delete them with \`drzl generate --prune\`, which removes only files a previous run recorded writing.`;
3043
+ }
3044
+ function nextManifestFiles(writtenNow, stale, root) {
3045
+ const kept = stale.filter((s) => s.present).map((s) => path4.resolve(root, s.file));
3046
+ return manifestEntries([...writtenNow, ...kept], root);
3047
+ }
3048
+
2844
3049
  // src/unified-diff.ts
2845
3050
  var DEFAULT_DIFF_LIMITS = {
2846
3051
  maxLines: 4e3,
@@ -3083,8 +3288,8 @@ function createRebuildScheduler(options) {
3083
3288
 
3084
3289
  // src/init.ts
3085
3290
  import { SchemaAnalyzer } from "@drzl/analyzer";
3086
- import * as fs4 from "fs";
3087
- import * as path4 from "path";
3291
+ import * as fs5 from "fs";
3292
+ import * as path5 from "path";
3088
3293
  var INIT_GENERATOR_CHOICES = [
3089
3294
  { kind: "zod", packageName: "@drzl/generator-zod", label: "Zod validators" },
3090
3295
  { kind: "valibot", packageName: "@drzl/generator-valibot", label: "Valibot validators" },
@@ -3155,7 +3360,7 @@ async function detectSchema(cwd) {
3155
3360
  kitFiles = null;
3156
3361
  }
3157
3362
  if (kitFiles && kitPath) {
3158
- const rel = path4.relative(cwd, kitPath) || path4.basename(kitPath);
3363
+ const rel = path5.relative(cwd, kitPath) || path5.basename(kitPath);
3159
3364
  const report = await classifySchemaCandidate(kitFiles);
3160
3365
  if (report.verdict === "confirmed" || report.verdict === "unverified") {
3161
3366
  notes.push(
@@ -3171,10 +3376,10 @@ async function detectSchema(cwd) {
3171
3376
  }
3172
3377
  notes.push(`${rel} names schema files that declare no Drizzle tables; looking elsewhere.`);
3173
3378
  }
3174
- const present = schemaCandidates().filter((c) => fs4.existsSync(path4.resolve(cwd, c)));
3379
+ const present = schemaCandidates().filter((c) => fs5.existsSync(path5.resolve(cwd, c)));
3175
3380
  const unverified = [];
3176
3381
  for (const file of present) {
3177
- const report = await classifySchemaCandidate(path4.resolve(cwd, file));
3382
+ const report = await classifySchemaCandidate(path5.resolve(cwd, file));
3178
3383
  if (report.verdict === "confirmed") {
3179
3384
  notes.push(
3180
3385
  `Schema found at ${file} (${report.tables} table${report.tables === 1 ? "" : "s"})`
@@ -3276,7 +3481,7 @@ async function promptForPlan(args) {
3276
3481
  if (answer === null) endedEarly = true;
3277
3482
  else if (answer.trim()) {
3278
3483
  const typed = answer.trim();
3279
- const report = await classifySchemaCandidate(path4.resolve(cwd, typed));
3484
+ const report = await classifySchemaCandidate(path5.resolve(cwd, typed));
3280
3485
  if (report.verdict === "confirmed") {
3281
3486
  write(` ${typed}: ${report.tables} table${report.tables === 1 ? "" : "s"}`);
3282
3487
  } else if (report.verdict === "unverified") {
@@ -3338,8 +3543,8 @@ function parseGeneratorsFlag(value) {
3338
3543
  return parts;
3339
3544
  }
3340
3545
  async function runInit(args) {
3341
- const target = path4.resolve(args.cwd, "drzl.config.ts");
3342
- const existing = CONFIG_FILE_NAMES.find((name) => fs4.existsSync(path4.resolve(args.cwd, name)));
3546
+ const target = path5.resolve(args.cwd, "drzl.config.ts");
3547
+ const existing = CONFIG_FILE_NAMES.find((name) => fs5.existsSync(path5.resolve(args.cwd, name)));
3343
3548
  if (existing) {
3344
3549
  args.error(
3345
3550
  `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.`
@@ -3378,8 +3583,8 @@ async function runInit(args) {
3378
3583
  generators: fromFlag ?? [DEFAULT_GENERATOR_KIND]
3379
3584
  };
3380
3585
  if (args.schemaFlag) {
3381
- const full = path4.resolve(args.cwd, args.schemaFlag);
3382
- if (!fs4.existsSync(full)) {
3586
+ const full = path5.resolve(args.cwd, args.schemaFlag);
3587
+ if (!fs5.existsSync(full)) {
3383
3588
  args.log(`--schema ${args.schemaFlag} is not there yet. Writing it anyway.`);
3384
3589
  } else if ((await classifySchemaCandidate(full)).verdict === "rejected") {
3385
3590
  args.log(`--schema ${args.schemaFlag} declares no Drizzle tables. Writing it anyway.`);
@@ -3387,7 +3592,7 @@ async function runInit(args) {
3387
3592
  }
3388
3593
  }
3389
3594
  try {
3390
- fs4.writeFileSync(target, renderInitConfig(plan), { flag: "wx" });
3595
+ fs5.writeFileSync(target, renderInitConfig(plan), { flag: "wx" });
3391
3596
  } catch (e) {
3392
3597
  if (e?.code === "EEXIST") {
3393
3598
  args.error(
@@ -3411,9 +3616,9 @@ async function runInit(args) {
3411
3616
 
3412
3617
  // src/sponsor.ts
3413
3618
  import { existsSync as existsSync3, mkdirSync, readFileSync, writeFileSync as writeFileSync2 } from "fs";
3414
- import path5 from "path";
3415
- var CACHE_DIR = path5.join(process.cwd(), "node_modules", ".cache", "@drzl");
3416
- var CACHE_FILE = path5.join(CACHE_DIR, "sponsor-message.json");
3619
+ import path6 from "path";
3620
+ var CACHE_DIR = path6.join(process.cwd(), "node_modules", ".cache", "@drzl");
3621
+ var CACHE_FILE = path6.join(CACHE_DIR, "sponsor-message.json");
3417
3622
  var DEFAULT_INTERVAL_MS = 1e3 * 60 * 15;
3418
3623
  var shownThisProcess = false;
3419
3624
  var tips = [
@@ -3482,11 +3687,11 @@ function writeCache(payload) {
3482
3687
 
3483
3688
  // src/version.ts
3484
3689
  import { readFileSync as readFileSync2 } from "fs";
3485
- import * as path6 from "path";
3690
+ import * as path7 from "path";
3486
3691
  import { fileURLToPath } from "url";
3487
3692
  var PACKAGE_NAME = "@drzl/cli";
3488
3693
  function moduleDir() {
3489
- return path6.dirname(fileURLToPath(import.meta.url));
3694
+ return path7.dirname(fileURLToPath(import.meta.url));
3490
3695
  }
3491
3696
  function readVersionFrom(manifestPath) {
3492
3697
  let raw;
@@ -3509,7 +3714,7 @@ function readVersionFrom(manifestPath) {
3509
3714
  return manifest.version;
3510
3715
  }
3511
3716
  function readCliVersion() {
3512
- return readVersionFrom(path6.join(moduleDir(), "..", "package.json"));
3717
+ return readVersionFrom(path7.join(moduleDir(), "..", "package.json"));
3513
3718
  }
3514
3719
  var CLI_VERSION = readCliVersion();
3515
3720
 
@@ -3598,8 +3803,8 @@ withOutputFlags(
3598
3803
  const errors = res.issues.some((i) => i.level === "error");
3599
3804
  const code = unreadable ? EXIT_FAILED : errors ? EXIT_FINDINGS : EXIT_OK;
3600
3805
  if (opts.out && !opts.json) {
3601
- const fs5 = await import("fs/promises");
3602
- await fs5.writeFile(opts.out, JSON.stringify(res, null, 2), "utf8");
3806
+ const fs6 = await import("fs/promises");
3807
+ await fs6.writeFile(opts.out, JSON.stringify(res, null, 2), "utf8");
3603
3808
  spinner.succeed(`Analysis written to ${opts.out} in ${ms}ms`);
3604
3809
  } else {
3605
3810
  spinner.succeed(`Analyzed in ${ms}ms`);
@@ -3736,7 +3941,7 @@ async function explainSchemaSource(opts, out) {
3736
3941
  schema: source.schema,
3737
3942
  label: describeSchemaTarget(source.schema),
3738
3943
  config: cfg,
3739
- ...source.source === "drizzle-kit" && source.drizzleKitConfigPath ? { note: `Schema from ${path7.relative(process.cwd(), source.drizzleKitConfigPath)}` } : {}
3944
+ ...source.source === "drizzle-kit" && source.drizzleKitConfigPath ? { note: `Schema from ${path8.relative(process.cwd(), source.drizzleKitConfigPath)}` } : {}
3740
3945
  };
3741
3946
  }
3742
3947
  const detected = await detectSchema(process.cwd());
@@ -3846,7 +4051,11 @@ withOutputFlags(
3846
4051
  ).option(
3847
4052
  "--check",
3848
4053
  "regenerate and fail if the result differs from what is on disk, without changing it"
3849
- ).option("--dry-run", "report what would be written, and write nothing", false)
4054
+ ).option("--dry-run", "report what would be written, and write nothing", false).option(
4055
+ "--prune",
4056
+ "delete files a previous run wrote that this one did not, and nothing else",
4057
+ false
4058
+ )
3850
4059
  ).action(async (opts) => {
3851
4060
  const out = outputFor(opts);
3852
4061
  const planning = !!opts.check || !!opts.dryRun;
@@ -3896,7 +4105,7 @@ withOutputFlags(
3896
4105
  const n = source.schema.length;
3897
4106
  out.note(
3898
4107
  out.errStyle.gray(
3899
- `Schema from ${path7.relative(process.cwd(), source.drizzleKitConfigPath)} (${n} file${n === 1 ? "" : "s"})`
4108
+ `Schema from ${path8.relative(process.cwd(), source.drizzleKitConfigPath)} (${n} file${n === 1 ? "" : "s"})`
3900
4109
  )
3901
4110
  );
3902
4111
  }
@@ -3925,6 +4134,7 @@ withOutputFlags(
3925
4134
  analysis.tables = filterTables(narrowed.tables, cfg);
3926
4135
  for (const w of [...narrowed.warnings, ...filterWarnings]) warn(w);
3927
4136
  for (const w of wideColumnWarning(analysis.issues)) warn(w);
4137
+ for (const w of authTableWarnings(analysis)) warn(w);
3928
4138
  const empty = nothingToGenerate({
3929
4139
  schema: source.schema,
3930
4140
  analyzed: narrowed.tables,
@@ -3994,6 +4204,19 @@ withOutputFlags(
3994
4204
  files: e.files,
3995
4205
  changes: e.changes.map((c) => ({ file: displayPath(c.file), status: c.status }))
3996
4206
  }));
4207
+ const previousManifest = await readManifest(process.cwd());
4208
+ const writtenNow = plan.files.map((f) => f.file);
4209
+ const stale = await staleFiles(previousManifest, writtenNow, process.cwd());
4210
+ let stillStale = stale;
4211
+ if (opts.prune && !planning) {
4212
+ const removed = await pruneStale(process.cwd(), stale);
4213
+ for (const file of removed) out.warn(`drzl generate: removed stale ${file}`);
4214
+ const gone = new Set(removed);
4215
+ stillStale = stale.filter((s) => !gone.has(s.file));
4216
+ } else {
4217
+ const warning = staleWarning(stale);
4218
+ if (warning) warn(warning);
4219
+ }
3997
4220
  if (planning) {
3998
4221
  const wrote = await verifyNothingWasWritten(outputDirs, existing);
3999
4222
  if (wrote.length) {
@@ -4003,6 +4226,14 @@ withOutputFlags(
4003
4226
  process.exit(EXIT_FAILED);
4004
4227
  }
4005
4228
  }
4229
+ if (!planning) {
4230
+ await writeManifest(
4231
+ process.cwd(),
4232
+ nextManifestFiles(writtenNow, stillStale, process.cwd()).map(
4233
+ (f) => path8.resolve(process.cwd(), f)
4234
+ )
4235
+ );
4236
+ }
4006
4237
  if (opts.check) {
4007
4238
  const drift = pendingChanges(plan);
4008
4239
  const upToDate = drift.length === 0;
@@ -4244,10 +4475,10 @@ program.command("watch").description("Watch schema and regenerate on changes").o
4244
4475
  return;
4245
4476
  }
4246
4477
  let cfg = loaded;
4247
- const abs = (p) => path7.resolve(process.cwd(), p);
4478
+ const abs = (p) => path8.resolve(process.cwd(), p);
4248
4479
  const isInside = (child, parent) => {
4249
- const rel = path7.relative(parent, child);
4250
- return !!rel && !rel.startsWith("..") && !path7.isAbsolute(rel);
4480
+ const rel = path8.relative(parent, child);
4481
+ return !!rel && !rel.startsWith("..") && !path8.isAbsolute(rel);
4251
4482
  };
4252
4483
  let source;
4253
4484
  try {
@@ -4283,7 +4514,7 @@ program.command("watch").description("Watch schema and regenerate on changes").o
4283
4514
  if (full === dir || isInside(full, dir)) return true;
4284
4515
  }
4285
4516
  if (stats?.isDirectory()) return false;
4286
- const ext = path7.extname(full);
4517
+ const ext = path8.extname(full);
4287
4518
  if (!ext) return false;
4288
4519
  return !WATCHED_EXTENSIONS.has(ext);
4289
4520
  };
@@ -4446,7 +4677,7 @@ program.command("watch").description("Watch schema and regenerate on changes").o
4446
4677
  } else {
4447
4678
  out.note(
4448
4679
  out.errStyle.gray(
4449
- "Watching:\n " + Array.from(currentTargets).map((p) => path7.relative(process.cwd(), p)).join("\n ")
4680
+ "Watching:\n " + Array.from(currentTargets).map((p) => path8.relative(process.cwd(), p)).join("\n ")
4450
4681
  )
4451
4682
  );
4452
4683
  }