@ankurhalder/codebase-snapshot 2.2.0 → 4.0.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,8 +1,118 @@
1
- import { parseByteSize, GENERATOR_VERSION, ARTIFACT_NAMES, getPreset, unknownPresetMessage, MIN_MAX_PART_BYTES, loadConfigFileDetailed, resolveConfig, validateSnapshot, explainPath, compareManifests, loadManifest, renderDiffText, generateSnapshot, GENERATOR_NAME, runInit, PromptCancelled, SCHEMA_VERSION, PRESETS, renderExplainText } from './chunk-5I564BGX.js';
1
+ import { parseByteSize, GENERATOR_VERSION, ARTIFACT_NAMES, getPreset, unknownPresetMessage, MIN_MAX_PART_BYTES, loadConfigFileDetailed, resolveConfig, validateSnapshot, explainPath, compareManifests, loadManifest, renderDiffText, generateSnapshot, GENERATOR_NAME, runInit, PromptCancelled, SCHEMA_VERSION, PRESETS, renderExplainText } from './chunk-FM4OOQPF.js';
2
2
  import * as fs from 'fs';
3
3
  import * as path2 from 'path';
4
4
  import { parseArgs } from 'util';
5
5
 
6
+ // src/agent/option-scope.ts
7
+ var COMMAND_OPTION_SCOPE = {
8
+ generate: [
9
+ "cwd",
10
+ "out-dir",
11
+ "max-part-bytes",
12
+ "large-file-bytes",
13
+ "max-monolith-bytes",
14
+ "include-monolith",
15
+ "include-sensitive-files",
16
+ "skip-artifacts",
17
+ "only-artifacts",
18
+ "no-config",
19
+ "format",
20
+ "mode",
21
+ "dry-run",
22
+ "watch",
23
+ "help",
24
+ "version"
25
+ ],
26
+ validate: [
27
+ "cwd",
28
+ "out-dir",
29
+ "max-part-bytes",
30
+ "large-file-bytes",
31
+ "max-monolith-bytes",
32
+ "include-monolith",
33
+ "include-sensitive-files",
34
+ "skip-artifacts",
35
+ "only-artifacts",
36
+ "no-config",
37
+ "format",
38
+ "mode",
39
+ "help",
40
+ "version"
41
+ ],
42
+ report: [
43
+ "cwd",
44
+ "out-dir",
45
+ "max-part-bytes",
46
+ "large-file-bytes",
47
+ "max-monolith-bytes",
48
+ "include-monolith",
49
+ "include-sensitive-files",
50
+ "skip-artifacts",
51
+ "only-artifacts",
52
+ "no-config",
53
+ "format",
54
+ "mode",
55
+ "help",
56
+ "version"
57
+ ],
58
+ explain: [
59
+ "cwd",
60
+ "out-dir",
61
+ "max-part-bytes",
62
+ "large-file-bytes",
63
+ "max-monolith-bytes",
64
+ "include-monolith",
65
+ "include-sensitive-files",
66
+ "skip-artifacts",
67
+ "only-artifacts",
68
+ "no-config",
69
+ "format",
70
+ "mode",
71
+ "help",
72
+ "version"
73
+ ],
74
+ diff: ["format", "mode", "help", "version"],
75
+ describe: ["format", "mode", "help", "version"],
76
+ init: [
77
+ "cwd",
78
+ "out-dir",
79
+ "max-part-bytes",
80
+ "include-sensitive-files",
81
+ "skip-artifacts",
82
+ "only-artifacts",
83
+ "preset",
84
+ "include",
85
+ "exclude",
86
+ "yes",
87
+ "minimal",
88
+ "force",
89
+ "dry-run",
90
+ "format",
91
+ "mode",
92
+ "help",
93
+ "version"
94
+ ],
95
+ skill: ["cwd", "format", "mode", "install", "target", "help", "version"]
96
+ };
97
+ var COMMAND_ORDER = [
98
+ "generate",
99
+ "validate",
100
+ "report",
101
+ "explain",
102
+ "init",
103
+ "diff",
104
+ "describe",
105
+ "skill"
106
+ ];
107
+ function commandsForOption(option) {
108
+ return COMMAND_ORDER.filter((command) => COMMAND_OPTION_SCOPE[command].includes(option));
109
+ }
110
+ function optionAppliesTo(option) {
111
+ const commands = commandsForOption(option);
112
+ if (commands.length === COMMAND_ORDER.length) return "all commands";
113
+ return commands.join("/");
114
+ }
115
+
6
116
  // src/agent/contract.ts
7
117
  var CONTRACT_VERSION = "1.0.0";
8
118
  var PACKAGE_NAME = "@ankurhalder/codebase-snapshot";
@@ -91,11 +201,12 @@ function buildAgentContract() {
91
201
  requires: { node: ">=18.0.0", dependencies: ["typescript"] },
92
202
  invocation: {
93
203
  oneShot: `npx ${PACKAGE_NAME}@latest`,
94
- installed: "npx snap",
95
- bins: ["snap", "codebase-snapshot"],
204
+ installed: "npx codebase-snapshot",
205
+ bins: ["codebase-snapshot", "snap"],
96
206
  notes: [
97
- `Pin the tag (${PACKAGE_NAME}@latest or @2) instead of the bare package name.`,
98
- "A bare scoped spec can resolve to a bin-less placeholder on some npm builds and exit 0 with no output; a pinned spec cannot.",
207
+ `The bare package name also works: npx ${PACKAGE_NAME} <command>.`,
208
+ "Pin @latest (or @4) when you need the exact same version on every machine.",
209
+ "Both bin names point at the same file. npm resolves a package by its bin entries, so they must stay aliases of one executable.",
99
210
  "Human progress goes to stderr. With --format json the payload on stdout is pure JSON."
100
211
  ]
101
212
  },
@@ -104,8 +215,8 @@ function buildAgentContract() {
104
215
  step: 1,
105
216
  id: "verify",
106
217
  run: `npx ${PACKAGE_NAME}@latest --version`,
107
- expect: "prints v2.x.x on stdout and exits 0",
108
- notes: "If it prints nothing, re-run with @latest explicitly, or use `npx -p <pkg> snap`."
218
+ expect: `prints v${GENERATOR_VERSION} on stdout and exits 0`,
219
+ notes: "If it prints nothing, clear the npm cache and retry, or use `npx -p <pkg> codebase-snapshot`."
109
220
  },
110
221
  {
111
222
  step: 2,
@@ -174,9 +285,9 @@ function buildAgentContract() {
174
285
  {
175
286
  name: "explain",
176
287
  aliases: [],
177
- summary: "Explain how one path would be treated: layer, kind, part, skip reason.",
288
+ summary: "Explain how one path would be treated: layer, kind, part, skip reason. Always exits 0 when it can report; read `included` and `reason` instead of the exit code.",
178
289
  positionals: ["<path>"],
179
- exits: [0, 1]
290
+ exits: [0]
180
291
  },
181
292
  {
182
293
  name: "diff",
@@ -210,140 +321,147 @@ function buildAgentContract() {
210
321
  flag: "--out-dir",
211
322
  type: "string",
212
323
  default: "snapshot",
213
- appliesTo: "generate/validate/report/explain",
324
+ appliesTo: optionAppliesTo("out-dir"),
214
325
  summary: "Where the snapshot is written. Must not be the repo root or a directory the tool did not create."
215
326
  },
216
327
  {
217
328
  flag: "--max-part-bytes",
218
329
  type: "string",
219
330
  default: 307200,
220
- appliesTo: "generate/validate/report",
331
+ appliesTo: optionAppliesTo("max-part-bytes"),
221
332
  summary: 'Byte budget per markdown part. Accepts "120k" / "1.5mb". Minimum 4096.'
222
333
  },
223
334
  {
224
335
  flag: "--large-file-bytes",
225
336
  type: "string",
226
337
  default: 204800,
227
- appliesTo: "generate/validate/report",
338
+ appliesTo: optionAppliesTo("large-file-bytes"),
228
339
  summary: "Above this size a file gets a head/tail preview instead of the full body."
229
340
  },
230
341
  {
231
342
  flag: "--max-monolith-bytes",
232
343
  type: "string",
233
344
  default: "unset",
234
- appliesTo: "generate",
345
+ appliesTo: optionAppliesTo("max-monolith-bytes"),
235
346
  summary: "Skip the monolith artifacts when they would exceed this size."
236
347
  },
237
348
  {
238
349
  flag: "--include-monolith",
239
350
  type: "boolean",
240
351
  default: false,
241
- appliesTo: "generate",
352
+ appliesTo: optionAppliesTo("include-monolith"),
242
353
  summary: "Also write full-codebase.json and full-codebase.md."
243
354
  },
244
355
  {
245
356
  flag: "--include-sensitive-files",
246
357
  type: "boolean",
247
358
  default: false,
248
- appliesTo: "generate/validate/report",
359
+ appliesTo: optionAppliesTo("include-sensitive-files"),
249
360
  summary: "Also emit .env / *.pem / .npmrc style files. Off by default; they are recorded as skipped."
250
361
  },
251
362
  {
252
363
  flag: "--skip-artifacts",
253
364
  type: "string",
254
365
  default: "unset",
255
- appliesTo: "generate",
366
+ appliesTo: optionAppliesTo("skip-artifacts"),
256
367
  summary: "Comma-separated artifacts to omit (readme, manifest, parts, navigation, projectTree, audit, monolith)."
257
368
  },
258
369
  {
259
370
  flag: "--only-artifacts",
260
371
  type: "string",
261
372
  default: "unset",
262
- appliesTo: "generate",
373
+ appliesTo: optionAppliesTo("only-artifacts"),
263
374
  summary: "Write only these artifacts. Cannot be combined with --skip-artifacts."
264
375
  },
265
376
  {
266
377
  flag: "--format",
267
378
  type: "string",
268
379
  default: "text",
269
- appliesTo: "all commands",
380
+ appliesTo: optionAppliesTo("format"),
270
381
  summary: 'Output format: "text" or "json". JSON goes to stdout, progress to stderr.'
271
382
  },
272
383
  {
273
384
  flag: "--cwd",
274
385
  type: "string",
275
386
  default: "process.cwd()",
276
- appliesTo: "all commands",
387
+ appliesTo: optionAppliesTo("cwd"),
277
388
  summary: "Repository to operate on. Must be an existing directory."
278
389
  },
279
390
  {
280
391
  flag: "--dry-run",
281
392
  type: "boolean",
282
393
  default: false,
283
- appliesTo: "generate",
394
+ appliesTo: optionAppliesTo("dry-run"),
284
395
  summary: "Compute everything and print the result without writing."
285
396
  },
286
397
  {
287
398
  flag: "--watch",
288
399
  type: "boolean",
289
400
  default: false,
290
- appliesTo: "generate",
401
+ appliesTo: optionAppliesTo("watch"),
291
402
  summary: "Regenerate on every change until interrupted."
292
403
  },
404
+ {
405
+ flag: "--no-config",
406
+ type: "boolean",
407
+ default: false,
408
+ appliesTo: optionAppliesTo("no-config"),
409
+ summary: "Ignore the config file and run on the built-in defaults."
410
+ },
293
411
  {
294
412
  flag: "--preset",
295
413
  type: "string",
296
414
  default: "detected",
297
- appliesTo: "init",
415
+ appliesTo: optionAppliesTo("preset"),
298
416
  summary: "Force a project preset instead of detecting one. Unknown names are a usage error."
299
417
  },
300
418
  {
301
419
  flag: "--include",
302
420
  type: "string",
303
421
  default: "unset",
304
- appliesTo: "init",
422
+ appliesTo: optionAppliesTo("include"),
305
423
  summary: "Comma-separated allow-list of globs to record in the generated config."
306
424
  },
307
425
  {
308
426
  flag: "--exclude",
309
427
  type: "string",
310
428
  default: "unset",
311
- appliesTo: "init",
429
+ appliesTo: optionAppliesTo("exclude"),
312
430
  summary: "Comma-separated globs to record in the generated config."
313
431
  },
314
432
  {
315
433
  flag: "--yes",
316
434
  type: "boolean",
317
435
  default: false,
318
- appliesTo: "init",
436
+ appliesTo: optionAppliesTo("yes"),
319
437
  summary: "Accept every default without prompting. Required in a non-TTY."
320
438
  },
321
439
  {
322
440
  flag: "--minimal",
323
441
  type: "boolean",
324
442
  default: false,
325
- appliesTo: "init",
443
+ appliesTo: optionAppliesTo("minimal"),
326
444
  summary: "Write only the settings that differ from the defaults."
327
445
  },
328
446
  {
329
447
  flag: "--force",
330
448
  type: "boolean",
331
449
  default: false,
332
- appliesTo: "init",
450
+ appliesTo: optionAppliesTo("force"),
333
451
  summary: "Overwrite an existing config file."
334
452
  },
335
453
  {
336
454
  flag: "--install",
337
455
  type: "boolean",
338
456
  default: false,
339
- appliesTo: "skill",
457
+ appliesTo: optionAppliesTo("install"),
340
458
  summary: "Write the skill into the repository instead of printing it."
341
459
  },
342
460
  {
343
461
  flag: "--target",
344
462
  type: "string",
345
463
  default: "all",
346
- appliesTo: "skill",
464
+ appliesTo: optionAppliesTo("target"),
347
465
  summary: 'Where to install: "claude", "agents", "cursor" or "all".'
348
466
  }
349
467
  ],
@@ -418,8 +536,8 @@ function buildAgentContract() {
418
536
  troubleshooting: [
419
537
  {
420
538
  symptom: "Command prints nothing and exits 0",
421
- cause: "A bare scoped spec resolved to a bin-less placeholder manifest on this npm build.",
422
- fix: `Use the pinned form: npx ${PACKAGE_NAME}@latest ...`
539
+ cause: "A stale global install of this package (often a leftover `npm link`) is shadowing npx: npm runs the global binary instead of installing the published one.",
540
+ fix: "Check `npm ls -g --depth=0` for @ankurhalder/codebase-snapshot and remove it with `npm rm -g @ankurhalder/codebase-snapshot`. If it is not installed globally, clear the npx cache with `npm cache clean --force` and retry."
423
541
  },
424
542
  {
425
543
  symptom: "Exit 1 with 'Refusing to replace outDir'",
@@ -471,10 +589,10 @@ another model, and reading files one by one would not fit in context.
471
589
  This tool is a Node CLI. It needs **Node.js 18 or newer**. You do not need to install it
472
590
  globally, and you do not need to clone anything.
473
591
 
474
- **Always pin the tag** \u2014 \`npx ${PACKAGE_NAME2}@latest <command>\`. A bare scoped spec can
475
- resolve to a placeholder manifest with no executable on some npm builds and exit 0
476
- printing nothing; the pinned form cannot. If a command exits 0 with no output, that is
477
- the cause, not a broken repository.
592
+ **Pin the tag** \u2014 \`npx ${PACKAGE_NAME2}@latest <command>\` \u2014 when you need the same version on
593
+ every machine. The bare package name works too: this package ships exactly one executable,
594
+ which is what lets npm pick it. If a command exits 0 printing nothing, the executable was
595
+ not resolved \u2014 clear the npm cache and retry; do not assume the repository is broken.
478
596
 
479
597
  ## Step by step
480
598
 
@@ -493,7 +611,8 @@ the cause, not a broken repository.
493
611
  \`--format json\` if you want to parse it.
494
612
 
495
613
  4. **Configure (recommended).** Run \`npx ${PACKAGE_NAME2}@latest init --yes\`. It detects
496
- the project shape, then writes \`snapshot.config.ts\` listing **every** setting at its
614
+ the project shape, then writes \`snapshot.config.ts\` (or \`.mjs\` when the project is
615
+ not an ES module) listing **every** setting at its
497
616
  current value, commented. Pass \`--preset <name>\` only if detection is wrong; the
498
617
  preset names are in the contract. This step is optional: without a config the tool
499
618
  runs on the same defaults.
@@ -594,14 +713,24 @@ function mergeAgentsFile(cwd, skill) {
594
713
  next = `# AGENTS.md
595
714
 
596
715
  ${block}`;
597
- } else if (existing.includes(AGENTS_MARKER_START) && existing.includes(AGENTS_MARKER_END)) {
716
+ } else {
717
+ const starts = existing.split(AGENTS_MARKER_START).length - 1;
718
+ const ends = existing.split(AGENTS_MARKER_END).length - 1;
598
719
  const start = existing.indexOf(AGENTS_MARKER_START);
599
720
  const end = existing.indexOf(AGENTS_MARKER_END) + AGENTS_MARKER_END.length;
600
- next = `${existing.slice(0, start)}${block.trimEnd()}${existing.slice(end)}`;
601
- } else {
602
- next = `${existing.trimEnd()}
721
+ if (starts === 1 && ends === 1 && start < end) {
722
+ next = `${existing.slice(0, start)}${block.trimEnd()}${existing.slice(end)}`;
723
+ } else if (starts > 0 && ends === 0) {
724
+ next = `${existing.slice(0, start)}${block.trimEnd()}${existing.slice(
725
+ start + AGENTS_MARKER_START.length
726
+ )}`;
727
+ } else if (existing.includes(block.trimEnd())) {
728
+ next = existing;
729
+ } else {
730
+ next = `${existing.trimEnd()}
603
731
 
604
732
  ${block}`;
733
+ }
605
734
  }
606
735
  const action = existing === null ? "created" : existing === next ? "unchanged" : "updated";
607
736
  if (action !== "unchanged") {
@@ -664,6 +793,7 @@ var CLI_OPTIONS = {
664
793
  mode: { type: "string" },
665
794
  "dry-run": { type: "boolean" },
666
795
  watch: { type: "boolean" },
796
+ "no-config": { type: "boolean" },
667
797
  preset: { type: "string" },
668
798
  include: { type: "string" },
669
799
  exclude: { type: "string" },
@@ -683,7 +813,6 @@ Production-grade LLM codebase snapshot generator, auditor, and AST partitioner
683
813
 
684
814
  Usage:
685
815
  codebase-snapshot [command] [options]
686
- snap [command] [options]
687
816
 
688
817
  Commands:
689
818
  generate (default) Generate a complete partitioned codebase snapshot
@@ -711,6 +840,7 @@ Options:
711
840
  --format <text|json> CLI output format (default: text)
712
841
  --dry-run Compute the snapshot without writing anything
713
842
  --watch Re-generate whenever the repository changes
843
+ --no-config Ignore snapshot.config.* and use the built-in defaults
714
844
 
715
845
  init only:
716
846
  --preset <name> Force a project preset instead of detecting one
@@ -872,28 +1002,32 @@ function parseCliArgs(argv) {
872
1002
  } else if (rawSkip !== void 0) {
873
1003
  skipArtifacts = parseArtifactList(String(rawSkip), "--skip-artifacts");
874
1004
  }
875
- if (command !== "init") {
876
- const initOnly = [];
877
- if (values.preset !== void 0) initOnly.push("--preset");
878
- if (values.include !== void 0) initOnly.push("--include");
879
- if (values.exclude !== void 0) initOnly.push("--exclude");
880
- if (values.yes === true) initOnly.push("--yes");
881
- if (values.minimal === true) initOnly.push("--minimal");
882
- if (values.force === true) initOnly.push("--force");
883
- if (initOnly.length > 0) {
884
- throw new UsageError(
885
- `${initOnly.join(", ")} ${initOnly.length === 1 ? "is" : "are"} only valid with \`init\`. To scope \`${command}\`, set include/excludePatterns/preset in snapshot.config.ts (or run \`snap init\` to generate one).`
886
- );
887
- }
1005
+ const scope = COMMAND_OPTION_SCOPE[command];
1006
+ const ignored = [];
1007
+ for (const name of Object.keys(CLI_OPTIONS)) {
1008
+ if (scope.includes(name)) continue;
1009
+ const value = values[name];
1010
+ if (value === void 0 || value === false) continue;
1011
+ ignored.push(`--${name}`);
1012
+ }
1013
+ if (ignored.length > 0) {
1014
+ const owners = Array.from(
1015
+ new Set(
1016
+ ignored.flatMap(
1017
+ (flag) => commandsForOption(flag.replace(/^--/, "")).map((owner) => `\`${owner}\``)
1018
+ )
1019
+ )
1020
+ );
1021
+ const hint = ignored.some((flag) => ["--preset", "--include", "--exclude"].includes(flag)) ? " To scope a run, set include/excludePatterns/preset in snapshot.config.ts (or run `codebase-snapshot init` to generate one)." : "";
1022
+ throw new UsageError(
1023
+ `${ignored.join(", ")} ${ignored.length === 1 ? "does" : "do"} not apply to \`${command}\` and ${ignored.length === 1 ? "was" : "were"} not applied. ${ignored.length === 1 ? "It is" : "They are"} accepted by: ${owners.join(", ")}.${hint}`
1024
+ );
888
1025
  }
889
1026
  if (command === "init" && values.preset !== void 0) {
890
1027
  const requested = String(values.preset);
891
1028
  if (!getPreset(requested)) throw new UsageError(unknownPresetMessage(requested));
892
1029
  }
893
- if (command !== "skill" && (values.install !== void 0 || values.target !== void 0)) {
894
- throw new UsageError("--install and --target are only valid with `skill`.");
895
- }
896
- if (values.target !== void 0 && !isSkillTarget(String(values.target))) {
1030
+ if (command === "skill" && values.target !== void 0 && !isSkillTarget(String(values.target))) {
897
1031
  throw new UsageError(
898
1032
  `--target must be one of: ${SKILL_TARGETS.join(", ")} (received "${String(values.target)}").`
899
1033
  );
@@ -904,6 +1038,7 @@ function parseCliArgs(argv) {
904
1038
  format: rawFormat,
905
1039
  dryRun: values["dry-run"] === true,
906
1040
  watch: values.watch === true,
1041
+ noConfig: values["no-config"] === true,
907
1042
  preset: values.preset === void 0 ? void 0 : String(values.preset),
908
1043
  maxPartBytesRaw: values["max-part-bytes"] === void 0 ? void 0 : String(values["max-part-bytes"]),
909
1044
  include: values.include === void 0 ? void 0 : String(values.include),
@@ -957,6 +1092,12 @@ function splitList(value) {
957
1092
  if (value === void 0) return void 0;
958
1093
  return value.split(",").map((entry) => entry.trim()).filter(Boolean);
959
1094
  }
1095
+ function displayOutPath(cwd, outDir, ...segments) {
1096
+ const absolute = path2.resolve(cwd, outDir, ...segments);
1097
+ const relative2 = path2.relative(cwd, absolute);
1098
+ if (relative2 && !relative2.startsWith("..") && !path2.isAbsolute(relative2)) return relative2;
1099
+ return absolute;
1100
+ }
960
1101
  function describeManifestShapeProblem(manifest) {
961
1102
  if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
962
1103
  return "not a JSON object";
@@ -976,6 +1117,7 @@ async function runInitCommand(invocation) {
976
1117
  const result = await runInit({
977
1118
  cwd: invocation.cwd,
978
1119
  preset: invocation.preset,
1120
+ outDir: invocation.outDir,
979
1121
  maxPartBytes: invocation.maxPartBytesRaw ?? invocation.maxPartBytes,
980
1122
  include: splitList(invocation.include),
981
1123
  exclude: splitList(invocation.exclude),
@@ -1002,7 +1144,11 @@ async function runInitCommand(invocation) {
1002
1144
  }
1003
1145
  for (const note of result.notes) info(` \u26A0 ${note}`);
1004
1146
  info("");
1005
- info(" Next: npm run snapshot");
1147
+ if (result.scripts.length > 0) {
1148
+ info(" Next: npm run snapshot");
1149
+ } else {
1150
+ info(` Next: ${GENERATOR_NAME} generate`);
1151
+ }
1006
1152
  info(
1007
1153
  ` Tip: \`${GENERATOR_NAME} skill --install\` makes this repo discoverable by AI agents.`
1008
1154
  );
@@ -1018,6 +1164,13 @@ async function runInitCommand(invocation) {
1018
1164
  }
1019
1165
  function runSkillCommand(invocation) {
1020
1166
  if (!invocation.install) {
1167
+ if (invocation.format === "json") {
1168
+ process.stdout.write(
1169
+ `${JSON.stringify({ name: SKILL_NAME, version: GENERATOR_VERSION, skill: renderSkill() }, null, 2)}
1170
+ `
1171
+ );
1172
+ return 0;
1173
+ }
1021
1174
  process.stdout.write(renderSkill());
1022
1175
  return 0;
1023
1176
  }
@@ -1094,7 +1247,8 @@ ${USAGE}
1094
1247
  if (invocation.command === "skill") {
1095
1248
  return runSkillCommand(invocation);
1096
1249
  }
1097
- const loaded = await loadConfigFileDetailed(invocation.cwd);
1250
+ const loaded = invocation.noConfig ? { config: null, warnings: [], errors: [], file: null } : await loadConfigFileDetailed(invocation.cwd);
1251
+ if (invocation.noConfig) info("\u2699 --no-config: using built-in defaults, config file ignored");
1098
1252
  for (const warning of loaded.warnings) warn(`\u26A0 Config: ${warning}`);
1099
1253
  if (loaded.errors.length > 0) {
1100
1254
  warn("\u274C The config file could not be read. Fix it, or delete it to use the defaults.");
@@ -1166,7 +1320,7 @@ ${USAGE}
1166
1320
  if (invocation.command === "explain") {
1167
1321
  const result = explainPath(resolved, invocation.args[0]);
1168
1322
  emit(invocation.format, result, () => info(renderExplainText(result)));
1169
- return result.included ? 0 : 1;
1323
+ return 0;
1170
1324
  }
1171
1325
  if (invocation.command === "diff") {
1172
1326
  try {
@@ -1190,7 +1344,9 @@ ${USAGE}
1190
1344
  if (invocation.dryRun) {
1191
1345
  info("\n\u{1F9EA} Dry run \u2014 nothing was written:");
1192
1346
  info(` - Would track: ${manifest.counts.totalFiles} files`);
1193
- info(` - Would write: ${manifest.counts.partsCount} part(s) under ${resolved.outDir}/parts/`);
1347
+ info(
1348
+ ` - Would write: ${manifest.counts.partsCount} part(s) under ${displayOutPath(resolved.cwd, resolved.outDir, "parts")}`
1349
+ );
1194
1350
  info(` - Skipped: ${manifest.counts.skippedCount} file(s)`);
1195
1351
  info(` - Warnings: ${manifest.warnings.length}`);
1196
1352
  info(` - Integrity: ${manifest.validation.status}`);
@@ -1205,7 +1361,9 @@ ${USAGE}
1205
1361
  );
1206
1362
  const { artifacts } = resolved;
1207
1363
  if (artifacts.parts) {
1208
- info(` - Parts written: ${manifest.counts.partsCount} under ${resolved.outDir}/parts/`);
1364
+ info(
1365
+ ` - Parts written: ${manifest.counts.partsCount} under ${displayOutPath(resolved.cwd, resolved.outDir, "parts")}/`
1366
+ );
1209
1367
  }
1210
1368
  const reports = [
1211
1369
  artifacts.projectTree && "project-tree",
@@ -1213,10 +1371,28 @@ ${USAGE}
1213
1371
  artifacts.audit && "audit"
1214
1372
  ].filter(Boolean);
1215
1373
  if (reports.length > 0) {
1216
- info(` - Reports: ${resolved.outDir}/reports/ (${reports.join(", ")})`);
1374
+ info(
1375
+ ` - Reports: ${displayOutPath(resolved.cwd, resolved.outDir, "reports")}/ (${reports.join(", ")})`
1376
+ );
1377
+ }
1378
+ if (artifacts.manifest) {
1379
+ info(` - Manifest: ${displayOutPath(resolved.cwd, resolved.outDir, "manifest.json")}`);
1380
+ }
1381
+ if (artifacts.monolith) {
1382
+ const monolithFiles = [
1383
+ manifest.artifacts?.fullJson ? "full-codebase.json" : null,
1384
+ manifest.artifacts?.fullMarkdown ? "full-codebase.md" : null
1385
+ ].filter((name) => name !== null);
1386
+ if (monolithFiles.length === 2) {
1387
+ info(
1388
+ ` - Monolith: ${displayOutPath(resolved.cwd, resolved.outDir, "full-codebase.{json,md}")}`
1389
+ );
1390
+ } else if (monolithFiles.length === 1) {
1391
+ info(
1392
+ ` - Monolith: ${displayOutPath(resolved.cwd, resolved.outDir, monolithFiles[0])} (the other was skipped by --max-monolith-bytes)`
1393
+ );
1394
+ }
1217
1395
  }
1218
- if (artifacts.manifest) info(` - Manifest: ${resolved.outDir}/manifest.json`);
1219
- if (artifacts.monolith) info(` - Monolith: ${resolved.outDir}/full-codebase.{json,md}`);
1220
1396
  if (reports.length === 0 && !artifacts.parts && !artifacts.manifest && !artifacts.monolith) {
1221
1397
  info(` - Artifacts: all disabled (only the ownership marker was written)`);
1222
1398
  }
@@ -1251,18 +1427,43 @@ ${USAGE}
1251
1427
  async function runWatchLoop(invocation, resolved) {
1252
1428
  const outDir = path2.resolve(resolved.cwd, resolved.outDir);
1253
1429
  const tmpDir = path2.resolve(resolved.cwd, resolved.tmpDir);
1430
+ const VCS_SEGMENTS = /* @__PURE__ */ new Set([".git", ".hg", ".svn", ".bzr"]);
1254
1431
  const outName = path2.basename(outDir).toLowerCase();
1255
1432
  const tmpName = path2.basename(tmpDir).toLowerCase();
1256
1433
  const isGenerated = (candidate) => candidate.toLowerCase().split(/[\\/]/).some(
1257
- (segment) => segment === outName || segment === tmpName || segment.startsWith(`${outName}.backup-`)
1434
+ (segment) => segment === outName || segment === tmpName || segment.startsWith(`${outName}.backup-`) || VCS_SEGMENTS.has(segment)
1258
1435
  );
1259
1436
  let running = false;
1260
- const regenerate = () => {
1437
+ const regenerate = async () => {
1261
1438
  if (running) return;
1262
1439
  running = true;
1263
1440
  const started = performance.now();
1264
1441
  try {
1265
- const manifest = generateSnapshot(resolved, { dryRun: invocation.dryRun });
1442
+ const loaded = await loadConfigFileDetailed(invocation.cwd);
1443
+ if (loaded.errors.length > 0) {
1444
+ for (const error of loaded.errors) warn(`\u26A0 Config: ${error}`);
1445
+ warn("\u274C The config file could not be read. Fix it, or delete it to use the defaults.");
1446
+ return;
1447
+ }
1448
+ for (const warning of loaded.warnings) warn(`\u26A0 Config: ${warning}`);
1449
+ const next = resolveConfig(
1450
+ loaded.config,
1451
+ {
1452
+ mode: invocation.command,
1453
+ format: invocation.format,
1454
+ dryRun: invocation.dryRun,
1455
+ cwd: invocation.cwd,
1456
+ outDir: invocation.outDir,
1457
+ includeMonolith: invocation.includeMonolith,
1458
+ includeSensitiveFiles: invocation.includeSensitiveFiles,
1459
+ maxPartBytes: invocation.maxPartBytes,
1460
+ largeFileBytes: invocation.largeFileBytes,
1461
+ maxMonolithBytes: invocation.maxMonolithBytes,
1462
+ skipArtifacts: invocation.skipArtifacts
1463
+ },
1464
+ invocation.cwd
1465
+ );
1466
+ const manifest = generateSnapshot(next, { dryRun: invocation.dryRun });
1266
1467
  const elapsed = ((performance.now() - started) / 1e3).toFixed(2);
1267
1468
  info(
1268
1469
  `\u267B\uFE0F ${(/* @__PURE__ */ new Date()).toISOString()} ${manifest.counts.totalFiles} file(s), ${manifest.counts.partsCount} part(s), ${manifest.validation.status} (${elapsed}s)`
@@ -1274,7 +1475,7 @@ async function runWatchLoop(invocation, resolved) {
1274
1475
  }
1275
1476
  };
1276
1477
  info("\u{1F440} Watching for changes. Press Ctrl+C to stop.");
1277
- regenerate();
1478
+ void regenerate();
1278
1479
  let timer = null;
1279
1480
  const watcher = fs.watch(resolved.cwd, { recursive: true }, (_event, filename) => {
1280
1481
  if (filename) {
@@ -1282,7 +1483,7 @@ async function runWatchLoop(invocation, resolved) {
1282
1483
  if (isGenerated(changed)) return;
1283
1484
  }
1284
1485
  if (timer) clearTimeout(timer);
1285
- timer = setTimeout(regenerate, 250);
1486
+ timer = setTimeout(() => void regenerate(), 250);
1286
1487
  });
1287
1488
  await new Promise((resolve2) => {
1288
1489
  const stop = () => {