codebase-snapshot 0.9.9 → 0.9.14

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,5 +1,6 @@
1
- import { buildAgentContract, runStdioServer, COMMAND_OPTION_SCOPE, commandsForOption, SKILL_NAME, renderSkill, renderContractText } from './chunk-HQBEQ67K.js';
2
- import { shouldCheck, checkForUpdate, GENERATOR_VERSION, cacheFilePath, formatUpdateNotice, installKind, loadConfigFileDetailed, resolveConfig, validateSnapshot, describeManifestShapeProblem, explainPath, compareManifests, loadManifest, renderDiffText, generateSnapshot, formatNumber, isDiscoveryMode, ARTIFACT_NAMES, getPreset, unknownPresetMessage, GENERATOR_NAME, runInit, PromptCancelled, planFirstRun, isEffectivelyEmpty, isInteractive, defaultIO, hasPriorSnapshot, DEFAULT_ANSWERS, findConfigFile, firstRunMessage, parseByteSize, gitignorePath, appendGitignoreEntries, escapeControls, MIN_MAX_PART_BYTES, renderExplainText } from './chunk-KUVFTJHK.js';
1
+ import { buildAgentContract, runStdioServer, COMMAND_OPTION_SCOPE, commandsForOption, SKILL_NAME, renderSkill, renderContractText } from './chunk-J4W74N4Y.js';
2
+ export { buildAgentContract } from './chunk-J4W74N4Y.js';
3
+ import { shouldCheck, checkForUpdate, GENERATOR_VERSION, cacheFilePath, formatUpdateNotice, installKind, loadConfigFileDetailed, resolveConfig, validateSnapshot, describeManifestShapeProblem, explainPath, compareManifests, loadManifest, renderDiffText, generateSnapshot, formatNumber, isDiscoveryMode, ARTIFACT_NAMES, getPreset, unknownPresetMessage, GENERATOR_NAME, runInit, installPreCommitHook, PromptCancelled, planFirstRun, isEffectivelyEmpty, isInteractive, defaultIO, hasPriorSnapshot, DEFAULT_ANSWERS, findConfigFile, firstRunMessage, parseByteSize, stripJsonComments, gitignorePath, appendGitignoreEntries, runWithStdoutCapture, escapeControls, MIN_MAX_PART_TOKENS, MIN_MAX_PART_BYTES, renderExplainText } from './chunk-DUZK5VL2.js';
3
4
  import { spawnSync } from 'child_process';
4
5
  import * as fs from 'fs';
5
6
  import * as path2 from 'path';
@@ -136,6 +137,7 @@ var CLI_OPTIONS = {
136
137
  watch: { type: "boolean" },
137
138
  "no-config": { type: "boolean" },
138
139
  discovery: { type: "string" },
140
+ "max-part-tokens": { type: "string" },
139
141
  preset: { type: "string" },
140
142
  include: { type: "string" },
141
143
  exclude: { type: "string" },
@@ -146,6 +148,12 @@ var CLI_OPTIONS = {
146
148
  "no-install": { type: "boolean" },
147
149
  target: { type: "string" },
148
150
  "no-update-check": { type: "boolean" },
151
+ "install-hook": { type: "boolean" },
152
+ "changed-since": { type: "string" },
153
+ "with-dependents": { type: "boolean" },
154
+ staged: { type: "boolean" },
155
+ skeleton: { type: "boolean" },
156
+ sarif: { type: "boolean" },
149
157
  help: { type: "boolean", short: "h" },
150
158
  version: { type: "boolean", short: "v" }
151
159
  };
@@ -185,7 +193,13 @@ Options:
185
193
  --max-monolith-bytes <n> Skip monolith artefacts larger than n bytes
186
194
  --max-part-bytes <n> Maximum byte budget per markdown part (default: 307200).
187
195
  Accepts units: 120k, 1.5mb
196
+ --max-part-tokens <n> Per-part budget in tokens; narrows --max-part-bytes
188
197
  --large-file-bytes <n> Threshold for head/tail truncation (default: 204800)
198
+ --changed-since <ref> Only files changed since this git ref, plus untracked ones
199
+ --staged Only files staged for the next commit
200
+ --with-dependents With the two above: also include their importers
201
+ --skeleton Emit TypeScript signatures instead of bodies
202
+ --sarif Also write reports/audit.sarif for GitHub Code Scanning
189
203
  --include-sensitive-files Emit .env / *.pem / .npmrc / credential files
190
204
  --skip-artifacts <names> Comma-separated artifacts to omit
191
205
  (readme, manifest, parts, navigation,
@@ -211,9 +225,12 @@ Options:
211
225
  --exclude <globs> Comma-separated exclude patterns
212
226
  --yes Accept every default without prompting
213
227
  --force Overwrite an existing config
228
+ --install-hook Write .git/hooks/pre-commit so a stale snapshot cannot be
229
+ committed. An existing hook is never replaced.
214
230
 
215
231
  setup only:
216
232
  --no-install Skip installing the dependency before generating
233
+ --install-hook As for init
217
234
  --preset / --include / --exclude / --force As for init
218
235
 
219
236
  skill only:
@@ -241,12 +258,12 @@ function parsePositiveInteger(raw, optionName, minimum) {
241
258
  }
242
259
  return value;
243
260
  }
244
- function parseByteOption(raw, optionName, minimum) {
261
+ function parseByteOption(raw, optionName, minimum, unit = "byte count") {
245
262
  if (raw === void 0) return void 0;
246
263
  const value = parseByteSize(raw);
247
264
  if (value === void 0 || !Number.isSafeInteger(value)) {
248
265
  throw new UsageError(
249
- `--${optionName} expects a byte count such as 120000 or "120k", received "${raw}".`
266
+ `--${optionName} expects a ${unit} such as 120000 or "120k", received "${raw}".`
250
267
  );
251
268
  }
252
269
  if (value < minimum) {
@@ -402,8 +419,10 @@ function parseCliArgs(argv) {
402
419
  throw new UsageError("--skip-artifacts and --only-artifacts cannot be combined.");
403
420
  }
404
421
  let skipArtifacts;
422
+ let onlyArtifacts;
405
423
  if (rawOnly !== void 0) {
406
424
  const keep = parseArtifactList(String(rawOnly), "--only-artifacts");
425
+ onlyArtifacts = keep;
407
426
  onlyNamesMonolith = keep.includes("monolith");
408
427
  skipArtifacts = ARTIFACT_NAMES.filter(
409
428
  (name) => !keep.includes(name)
@@ -432,7 +451,7 @@ function parseCliArgs(argv) {
432
451
  `${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}`
433
452
  );
434
453
  }
435
- if (command === "init" && values.preset !== void 0) {
454
+ if ((command === "init" || command === "setup") && values.preset !== void 0) {
436
455
  const requested = String(values.preset);
437
456
  if (!getPreset(requested)) throw new UsageError(unknownPresetMessage(requested));
438
457
  }
@@ -476,6 +495,15 @@ function parseCliArgs(argv) {
476
495
  "max-part-bytes",
477
496
  MIN_MAX_PART_BYTES
478
497
  ),
498
+ // Tokens are counted, so `32k` is a shorthand a context window is often named with; the
499
+ // same parser accepts a bare integer. The floor is the byte floor rewritten in tokens —
500
+ // clamping a smaller budget up to it accepted the flag and then applied a different number.
501
+ maxPartTokens: parseByteOption(
502
+ values["max-part-tokens"],
503
+ "max-part-tokens",
504
+ MIN_MAX_PART_TOKENS,
505
+ "token count"
506
+ ),
479
507
  largeFileBytes: parseByteOption(
480
508
  values["large-file-bytes"],
481
509
  "large-file-bytes",
@@ -487,7 +515,22 @@ function parseCliArgs(argv) {
487
515
  1
488
516
  ),
489
517
  skipArtifacts,
490
- discovery: isDiscoveryMode(rawDiscovery) ? rawDiscovery : void 0
518
+ onlyArtifacts,
519
+ discovery: isDiscoveryMode(rawDiscovery) ? rawDiscovery : void 0,
520
+ noUpdateCheck: values["no-update-check"] === true,
521
+ installHook: values["install-hook"] === true,
522
+ changedSince: values["changed-since"] === void 0 ? void 0 : String(values["changed-since"]),
523
+ // `undefined` when the flag was not given, never `false`.
524
+ //
525
+ // `resolveConfig` reads these as `cliOptions.x ?? config.x`, so a `false` here is an
526
+ // *explicit override* — it beat `skeleton: true` in a `snapshot.config.ts` and turned the
527
+ // setting off without a word. Every run that did not pass the flag sent `false`, so a config
528
+ // file could not turn any of these four on at all, and a snapshot generated with `--skeleton`
529
+ // could never be validated against the config that describes it.
530
+ staged: values.staged === true ? true : void 0,
531
+ withDependents: values["with-dependents"] === true ? true : void 0,
532
+ skeleton: values.skeleton === true ? true : void 0,
533
+ sarif: values.sarif === true ? true : void 0
491
534
  };
492
535
  }
493
536
  var HelpRequested = class extends Error {
@@ -533,7 +576,9 @@ function nextStepHint(cwd, scriptsAddedNow) {
533
576
  let hasScript = scriptsAddedNow;
534
577
  if (!hasScript) {
535
578
  try {
536
- const parsed = JSON.parse(fs.readFileSync(path2.join(cwd, "package.json"), "utf8"));
579
+ const parsed = JSON.parse(
580
+ stripJsonComments(fs.readFileSync(path2.join(cwd, "package.json"), "utf8"))
581
+ );
537
582
  hasScript = typeof parsed.scripts?.snapshot === "string";
538
583
  } catch {
539
584
  hasScript = false;
@@ -557,18 +602,24 @@ async function runInitCommand(invocation) {
557
602
  include: splitList(invocation.include),
558
603
  exclude: splitList(invocation.exclude),
559
604
  skipArtifacts: invocation.skipArtifacts,
605
+ onlyArtifacts: invocation.onlyArtifacts,
606
+ includeMonolith: invocation.includeMonolith,
560
607
  includeSensitiveFiles: invocation.includeSensitiveFiles,
561
608
  yes: invocation.yes,
562
609
  force: invocation.force,
563
610
  dryRun: invocation.dryRun
564
611
  });
612
+ const hook = invocation.installHook === true ? installPreCommitHook(invocation.cwd, result.outDir) : null;
565
613
  if (invocation.format === "json") {
566
- process.stdout.write(`${JSON.stringify(result, null, 2)}
567
- `);
614
+ process.stdout.write(
615
+ `${JSON.stringify(hook === null ? result : { ...result, hook }, null, 2)}
616
+ `
617
+ );
568
618
  return 0;
569
619
  }
570
620
  if (!result.written) {
571
621
  info(result.configText);
622
+ if (hook) for (const note of hook.notes) info(` ${note}`);
572
623
  return 0;
573
624
  }
574
625
  info(`\u2705 Created ${path2.basename(result.configPath)} (${presetSummary(result)})`);
@@ -580,6 +631,7 @@ async function runInitCommand(invocation) {
580
631
  info(` Added to .gitignore: ${result.gitignoreEntries.join(", ")}`);
581
632
  }
582
633
  for (const note of result.notes) info(` \u26A0 ${note}`);
634
+ if (hook) for (const note of hook.notes) info(` ${note}`);
583
635
  info("");
584
636
  info(` Next: ${nextStepHint(invocation.cwd, result.scripts.length > 0)}`);
585
637
  return 0;
@@ -628,18 +680,8 @@ function installDependencies(cwd) {
628
680
  return { manager: manager.name, command, ok: true, detail: seconds };
629
681
  }
630
682
  async function captureStdout(fn) {
631
- const original = process.stdout.write.bind(process.stdout);
632
- let text = "";
633
- process.stdout.write = ((chunk) => {
634
- text += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
635
- return true;
636
- });
637
- try {
638
- const code = await fn();
639
- return { code, text };
640
- } finally {
641
- process.stdout.write = original;
642
- }
683
+ const { value, text } = await runWithStdoutCapture(fn);
684
+ return { code: value, text };
643
685
  }
644
686
  async function runSetupCommand(invocation, options = {}) {
645
687
  try {
@@ -649,15 +691,28 @@ async function runSetupCommand(invocation, options = {}) {
649
691
  preset: invocation.preset,
650
692
  outDir: invocation.outDir,
651
693
  maxPartBytes: invocation.maxPartBytesRaw ?? invocation.maxPartBytes,
694
+ // Written into the config, not just forwarded to the generation below. A setting that
695
+ // reaches the snapshot but not the file beside it is drift the very next run reports:
696
+ // `setup --discovery walk --large-file-bytes 100000` used to exit 0 and then fail its own
697
+ // `validate` with three configuration mismatches.
698
+ maxPartTokens: invocation.maxPartTokens,
699
+ largeFileBytes: invocation.largeFileBytes,
700
+ maxMonolithBytes: invocation.maxMonolithBytes ?? void 0,
701
+ discovery: invocation.discovery,
702
+ skeleton: invocation.skeleton === true ? true : void 0,
703
+ sarif: invocation.sarif === true ? true : void 0,
652
704
  include: splitList(invocation.include),
653
705
  exclude: splitList(invocation.exclude),
654
706
  skipArtifacts: invocation.skipArtifacts,
707
+ onlyArtifacts: invocation.onlyArtifacts,
708
+ includeMonolith: invocation.includeMonolith,
655
709
  includeSensitiveFiles: invocation.includeSensitiveFiles,
656
710
  yes: !interactive,
657
711
  force: invocation.force,
658
712
  reuseConfig: true,
659
713
  dryRun: invocation.dryRun
660
714
  });
715
+ const hook = invocation.installHook === true ? installPreCommitHook(invocation.cwd, init.outDir) : null;
661
716
  if (invocation.dryRun) {
662
717
  const manager = invocation.noInstall ? null : detectPackageManager(invocation.cwd).name;
663
718
  if (invocation.format === "json") {
@@ -703,6 +758,7 @@ async function runSetupCommand(invocation, options = {}) {
703
758
  if (init.gitignoreEntries.length > 0) {
704
759
  info(` Added to .gitignore: ${init.gitignoreEntries.join(", ")}`);
705
760
  }
761
+ if (hook) for (const note of hook.notes) info(` ${note}`);
706
762
  }
707
763
  let install = null;
708
764
  if (invocation.noInstall) {
@@ -723,33 +779,50 @@ async function runSetupCommand(invocation, options = {}) {
723
779
  if ((invocation.skipArtifacts ?? []).length > 0) {
724
780
  generateArgs.push("--skip-artifacts", (invocation.skipArtifacts ?? []).join(","));
725
781
  }
782
+ if (invocation.includeMonolith === true) generateArgs.push("--include-monolith");
783
+ if (invocation.discovery !== void 0) generateArgs.push("--discovery", invocation.discovery);
784
+ if (invocation.largeFileBytes !== void 0) {
785
+ generateArgs.push("--large-file-bytes", String(invocation.largeFileBytes));
786
+ }
787
+ if (invocation.maxMonolithBytes !== void 0 && invocation.maxMonolithBytes !== null) {
788
+ generateArgs.push("--max-monolith-bytes", String(invocation.maxMonolithBytes));
789
+ }
790
+ if (invocation.maxPartTokens !== void 0) {
791
+ generateArgs.push("--max-part-tokens", String(invocation.maxPartTokens));
792
+ }
793
+ if (invocation.skeleton === true) generateArgs.push("--skeleton");
794
+ if (invocation.sarif === true) generateArgs.push("--sarif");
795
+ if (invocation.noUpdateCheck) generateArgs.push("--no-update-check");
726
796
  if (invocation.watch) generateArgs.push("--watch");
727
797
  if (invocation.format === "json") {
728
798
  const generated = await captureStdout(() => main(generateArgs));
729
- let manifest = null;
799
+ let parsed = null;
730
800
  try {
731
- manifest = JSON.parse(generated.text);
801
+ parsed = JSON.parse(generated.text);
732
802
  } catch {
733
- manifest = null;
803
+ parsed = null;
804
+ }
805
+ const setup = {
806
+ configPath: init.configPath,
807
+ configReused: init.reused,
808
+ preset: init.preset,
809
+ scripts: init.scripts,
810
+ dependency: init.dependency,
811
+ gitignoreEntries: init.gitignoreEntries,
812
+ install,
813
+ notes: init.notes,
814
+ hook
815
+ };
816
+ if (generated.code !== 0) {
817
+ const inner = parsed && typeof parsed.error === "object" && parsed.error !== null ? parsed.error : { message: "Snapshot generation failed during setup.", exitCode: generated.code };
818
+ process.stdout.write(
819
+ `${JSON.stringify({ error: inner, setup, manifest: null }, null, 2)}
820
+ `
821
+ );
822
+ return generated.code;
734
823
  }
735
824
  process.stdout.write(
736
- `${JSON.stringify(
737
- {
738
- setup: {
739
- configPath: init.configPath,
740
- configReused: init.reused,
741
- preset: init.preset,
742
- scripts: init.scripts,
743
- dependency: init.dependency,
744
- gitignoreEntries: init.gitignoreEntries,
745
- install,
746
- notes: init.notes
747
- },
748
- manifest
749
- },
750
- null,
751
- 2
752
- )}
825
+ `${JSON.stringify({ setup, manifest: parsed }, null, 2)}
753
826
  `
754
827
  );
755
828
  return generated.code;
@@ -938,28 +1011,25 @@ ${USAGE}
938
1011
  includeMonolith: invocation.includeMonolith,
939
1012
  includeSensitiveFiles: invocation.includeSensitiveFiles,
940
1013
  maxPartBytes: invocation.maxPartBytes,
1014
+ maxPartTokens: invocation.maxPartTokens,
941
1015
  largeFileBytes: invocation.largeFileBytes,
942
1016
  maxMonolithBytes: invocation.maxMonolithBytes,
943
1017
  skipArtifacts: invocation.skipArtifacts,
944
- discovery: invocation.discovery
1018
+ // `--only-artifacts` is forwarded as itself, not only as the derived suppression list:
1019
+ // suppression cannot switch an artifact *on*, so a config that had turned one of the
1020
+ // requested artifacts off left the run with nothing to write.
1021
+ onlyArtifacts: invocation.onlyArtifacts,
1022
+ discovery: invocation.discovery,
1023
+ changedSince: invocation.changedSince,
1024
+ staged: invocation.staged,
1025
+ withDependents: invocation.withDependents,
1026
+ skeleton: invocation.skeleton,
1027
+ sarif: invocation.sarif
945
1028
  },
946
1029
  invocation.cwd
947
1030
  );
948
1031
  if (invocation.command === "validate") {
949
1032
  const result = validateSnapshot(resolved);
950
- if (result.valid) {
951
- const manifestPath = path2.resolve(resolved.cwd, resolved.outDir, "manifest.json");
952
- let manifest;
953
- try {
954
- manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
955
- } catch {
956
- manifest = void 0;
957
- }
958
- const shapeError = manifest === void 0 ? null : describeManifestShapeProblem(manifest);
959
- if (shapeError) {
960
- return fail(invocation.format, `${manifestPath} is not a snapshot manifest: ${shapeError}`);
961
- }
962
- }
963
1033
  emit(invocation.format, result, () => renderValidateText(result));
964
1034
  return result.valid ? 0 : 1;
965
1035
  }
@@ -1017,6 +1087,10 @@ ${USAGE}
1017
1087
  return fail(invocation.format, err.message);
1018
1088
  }
1019
1089
  }
1090
+ if (invocation.watch) {
1091
+ const problem = watchSupportProblem();
1092
+ if (problem !== null) return fail(invocation.format, problem);
1093
+ }
1020
1094
  const started = performance.now();
1021
1095
  info(`\u{1F4F8} ${invocation.dryRun ? "Planning snapshot (dry run)" : "Generating snapshot"} for: ${resolved.cwd}`);
1022
1096
  info(
@@ -1057,7 +1131,11 @@ ${USAGE}
1057
1131
  const reports = [
1058
1132
  artifacts.projectTree && "project-tree",
1059
1133
  artifacts.navigation && "navigation",
1060
- artifacts.audit && "audit"
1134
+ artifacts.audit && "audit",
1135
+ // `--sarif` writes beside the audit rather than inside `artifacts`, so it is listed
1136
+ // here by hand. Without it a run that produced `reports/audit.sarif` still reported
1137
+ // that every artifact was disabled and only the ownership marker had been written.
1138
+ resolved.sarif === true && "audit.sarif"
1061
1139
  ].filter(Boolean);
1062
1140
  if (reports.length > 0) {
1063
1141
  info(
@@ -1104,7 +1182,7 @@ ${USAGE}
1104
1182
  `);
1105
1183
  }
1106
1184
  if (invocation.watch) {
1107
- return await runWatchLoop(invocation, resolved);
1185
+ return await runWatchLoop(invocation, resolved, { generatedAlready: true });
1108
1186
  }
1109
1187
  return manifest.validation.status === "passed" ? 0 : 1;
1110
1188
  } catch (err) {
@@ -1146,10 +1224,13 @@ function isGeneratedWatchPath(candidate, paths) {
1146
1224
  const outDir = comparablePath(path2.resolve(paths.cwd, paths.outDir));
1147
1225
  const tmpDir = comparablePath(path2.resolve(paths.cwd, paths.tmpDir));
1148
1226
  if (isSameOrInside(outDir, absolute) || isSameOrInside(tmpDir, absolute)) return true;
1149
- const parent = path2.dirname(absolute);
1150
- if (parent === path2.dirname(outDir) && path2.basename(absolute).startsWith(`${path2.basename(outDir)}.backup-`)) {
1151
- return true;
1152
- }
1227
+ const offsets = [path2.basename(outDir), path2.basename(tmpDir)].filter((name) => name.length > 0);
1228
+ const leftover = absolute.split(/[\\/]+/).some(
1229
+ (segment) => offsets.some(
1230
+ (name) => segment.startsWith(`${name}.backup-`) || segment.startsWith(`${name}.partial-`)
1231
+ )
1232
+ );
1233
+ if (leftover) return true;
1153
1234
  return absolute.split(/[\\/]+/).some((segment) => VCS_SEGMENTS.has(segment));
1154
1235
  }
1155
1236
  function decideWatchEvent(state, event, paths) {
@@ -1163,10 +1244,22 @@ function decideWatchEvent(state, event, paths) {
1163
1244
  }
1164
1245
  return { state, action: "schedule" };
1165
1246
  }
1166
- async function runWatchLoop(invocation, resolved) {
1167
- const outDir = path2.resolve(resolved.cwd, resolved.outDir);
1168
- const tmpDir = path2.resolve(resolved.cwd, resolved.tmpDir);
1169
- const paths = { cwd: resolved.cwd, outDir, tmpDir };
1247
+ function watchPathsFor(config) {
1248
+ return {
1249
+ cwd: config.cwd,
1250
+ outDir: path2.resolve(config.cwd, config.outDir),
1251
+ tmpDir: path2.resolve(config.cwd, config.tmpDir)
1252
+ };
1253
+ }
1254
+ function watchSupportProblem(platform = process.platform, version = process.versions.node) {
1255
+ if (platform !== "linux") return null;
1256
+ const [major, minor] = version.split(".").map((part) => Number.parseInt(part, 10));
1257
+ if (!Number.isFinite(major)) return null;
1258
+ if (major > 20 || major === 20 && (minor ?? 0) >= 13) return null;
1259
+ return `--watch needs recursive directory watching, which Node only provides on Linux from 20.13 (this is ${version}). Re-run \`generate\` after each change, or upgrade to Node 20.13 or later.`;
1260
+ }
1261
+ async function runWatchLoop(invocation, resolved, options) {
1262
+ let paths = watchPathsFor(resolved);
1170
1263
  let state = initialWatchState();
1171
1264
  let timer = null;
1172
1265
  let stopped = false;
@@ -1208,14 +1301,22 @@ async function runWatchLoop(invocation, resolved) {
1208
1301
  includeMonolith: invocation.includeMonolith,
1209
1302
  includeSensitiveFiles: invocation.includeSensitiveFiles,
1210
1303
  maxPartBytes: invocation.maxPartBytes,
1304
+ maxPartTokens: invocation.maxPartTokens,
1211
1305
  largeFileBytes: invocation.largeFileBytes,
1212
1306
  maxMonolithBytes: invocation.maxMonolithBytes,
1213
1307
  skipArtifacts: invocation.skipArtifacts,
1214
- discovery: invocation.discovery
1308
+ onlyArtifacts: invocation.onlyArtifacts,
1309
+ discovery: invocation.discovery,
1310
+ changedSince: invocation.changedSince,
1311
+ staged: invocation.staged,
1312
+ withDependents: invocation.withDependents,
1313
+ skeleton: invocation.skeleton,
1314
+ sarif: invocation.sarif
1215
1315
  },
1216
1316
  invocation.cwd
1217
1317
  );
1218
1318
  const manifest = generateSnapshot(next, { dryRun: invocation.dryRun });
1319
+ paths = watchPathsFor(next);
1219
1320
  const elapsed = ((performance.now() - started) / 1e3).toFixed(2);
1220
1321
  info(
1221
1322
  `\u267B\uFE0F ${(/* @__PURE__ */ new Date()).toISOString()} ${manifest.counts.totalFiles} file(s), ${manifest.counts.partsCount} part(s), ${manifest.validation.status} (${elapsed}s)`
@@ -1229,25 +1330,37 @@ async function runWatchLoop(invocation, resolved) {
1229
1330
  }
1230
1331
  };
1231
1332
  info("\u{1F440} Watching for changes. Press Ctrl+C to stop.");
1232
- void regenerate();
1233
- const watcher = fs.watch(resolved.cwd, { recursive: true }, (_event, filename) => {
1234
- const decision = decideWatchEvent(
1235
- state,
1236
- {
1237
- path: filename ? path2.resolve(resolved.cwd, filename.toString()) : null,
1238
- now: performance.now()
1239
- },
1240
- paths
1241
- );
1242
- state = decision.state;
1243
- if (decision.action === "pending") {
1244
- if (timer) {
1245
- clearTimeout(timer);
1246
- timer = null;
1333
+ let watcher;
1334
+ try {
1335
+ watcher = fs.watch(resolved.cwd, { recursive: true }, (_event, filename) => {
1336
+ const decision = decideWatchEvent(
1337
+ state,
1338
+ {
1339
+ path: filename ? path2.resolve(resolved.cwd, filename.toString()) : null,
1340
+ now: performance.now()
1341
+ },
1342
+ paths
1343
+ );
1344
+ state = decision.state;
1345
+ if (decision.action === "pending") {
1346
+ if (timer) {
1347
+ clearTimeout(timer);
1348
+ timer = null;
1349
+ }
1350
+ return;
1247
1351
  }
1248
- return;
1249
- }
1250
- if (decision.action === "schedule") schedule();
1352
+ if (decision.action === "schedule") schedule();
1353
+ });
1354
+ } catch (err) {
1355
+ const code = err.code;
1356
+ const detail = code === "ENOSPC" ? "the operating system's file-watch limit is exhausted; raise it (`fs.inotify.max_user_watches` on Linux) or run `generate` by hand" : code === "ERR_FEATURE_UNAVAILABLE_ON_PLATFORM" ? watchSupportProblem() ?? "this platform has no recursive directory watching" : err.message;
1357
+ warn(`\u274C Could not start watching ${resolved.cwd}: ${detail}`);
1358
+ warn(" The snapshot that was just written is complete and valid.");
1359
+ return 1;
1360
+ }
1361
+ watcher.on("error", (err) => {
1362
+ info(`\u26A0 Watch error: ${err.message}`);
1363
+ info(" The watcher may have missed changes. Restart with Ctrl+C if they stop arriving.");
1251
1364
  });
1252
1365
  await new Promise((resolve2) => {
1253
1366
  const stop = () => {
@@ -1277,6 +1390,4 @@ if (invokedDirectly) {
1277
1390
  );
1278
1391
  }
1279
1392
 
1280
- export { UsageError, WATCH_DEBOUNCE_MS, WATCH_SETTLE_MS, beginWatchRun, cleanInstallEnv, decideWatchEvent, finishWatchRun, initialWatchState, isGeneratedWatchPath, main, parseByteOption, parseCliArgs, parsePositiveInteger };
1281
- //# sourceMappingURL=cli.js.map
1282
- //# sourceMappingURL=cli.js.map
1393
+ export { CLI_OPTIONS, UsageError, WATCH_DEBOUNCE_MS, WATCH_SETTLE_MS, beginWatchRun, cleanInstallEnv, decideWatchEvent, finishWatchRun, initialWatchState, isGeneratedWatchPath, main, parseByteOption, parseCliArgs, parsePositiveInteger, watchPathsFor, watchSupportProblem };
@@ -0,0 +1,73 @@
1
+ /** The options `resolveConfig` was called with, as the CLI would have passed them. */
2
+ interface WorkspaceCliOptions {
3
+ cwd: string;
4
+ maxPartBytes?: number;
5
+ maxPartTokens?: number;
6
+ outDir?: string;
7
+ /**
8
+ * `true` or absent, never `false`.
9
+ *
10
+ * `resolveConfig` reads these as `cliOptions.x ?? config.x`, so a `false` here is an *explicit
11
+ * override* that beats `skeleton: true` in the project's configuration — the same trap the CLI
12
+ * documents. Absent means "whatever the configuration says".
13
+ */
14
+ skeleton?: true;
15
+ sarif?: true;
16
+ staged?: true;
17
+ withDependents?: true;
18
+ changedSince?: string;
19
+ }
20
+
21
+ /** What the caller needs from a finished run; the whole manifest can be megabytes. */
22
+ interface GenerateSummary {
23
+ fingerprint: string;
24
+ totalFiles: number;
25
+ skippedCount: number;
26
+ partsCount: number;
27
+ parts: Array<{
28
+ filename: string;
29
+ byteSize: number;
30
+ }>;
31
+ totalContentBytes: number;
32
+ warnings: string[];
33
+ }
34
+ type WorkerResults = {
35
+ ok: true;
36
+ summary: GenerateSummary;
37
+ } | {
38
+ ok: false;
39
+ message: string;
40
+ };
41
+ /**
42
+ * The absolute path of the package entry this process is running from.
43
+ *
44
+ * `__dirname` exists in the CJS build and `import.meta.url` in the ESM one, and `typeof` on an
45
+ * undeclared identifier does not throw — so this resolves correctly in both without a build-time
46
+ * substitution.
47
+ */
48
+ /**
49
+ * Where the worker finds this package's entry: the path, and the URL the worker imports.
50
+ *
51
+ * Exported so the URL can be tested directly. The defect it fixes only shows up for an install
52
+ * path that contains a `#`, `?` or `%`, or on a Windows drive letter, and no test can move the
53
+ * package's own `dist/` to such a path.
54
+ *
55
+ * `pathToFileURL` rather than `` `file://${path}` ``: the interpolation form turns a Windows path
56
+ * into `file://C:/…`, whose **host** is the drive letter, reads a `#` as a fragment and a `?` as
57
+ * a query, and leaves a space unescaped. `config.ts` already carries this note for the same
58
+ * reason.
59
+ */
60
+ declare function workerEntry(directory: string, isCjs: boolean): {
61
+ path: string;
62
+ url: string;
63
+ };
64
+ /**
65
+ * Generates in a worker and resolves with the summary.
66
+ *
67
+ * A worker that fails to start, exits without posting, or throws at the module level is reported
68
+ * as a tool failure naming the reason — never silently retried on the calling thread, because a
69
+ * quiet fallback would hide exactly the condition this exists to avoid.
70
+ */
71
+ declare function generateInWorker(cwd: string, cliOptions: WorkspaceCliOptions): Promise<WorkerResults>;
72
+
73
+ export { type GenerateSummary as G, type WorkerResults as W, generateInWorker as g, workerEntry as w };
@@ -0,0 +1,73 @@
1
+ /** The options `resolveConfig` was called with, as the CLI would have passed them. */
2
+ interface WorkspaceCliOptions {
3
+ cwd: string;
4
+ maxPartBytes?: number;
5
+ maxPartTokens?: number;
6
+ outDir?: string;
7
+ /**
8
+ * `true` or absent, never `false`.
9
+ *
10
+ * `resolveConfig` reads these as `cliOptions.x ?? config.x`, so a `false` here is an *explicit
11
+ * override* that beats `skeleton: true` in the project's configuration — the same trap the CLI
12
+ * documents. Absent means "whatever the configuration says".
13
+ */
14
+ skeleton?: true;
15
+ sarif?: true;
16
+ staged?: true;
17
+ withDependents?: true;
18
+ changedSince?: string;
19
+ }
20
+
21
+ /** What the caller needs from a finished run; the whole manifest can be megabytes. */
22
+ interface GenerateSummary {
23
+ fingerprint: string;
24
+ totalFiles: number;
25
+ skippedCount: number;
26
+ partsCount: number;
27
+ parts: Array<{
28
+ filename: string;
29
+ byteSize: number;
30
+ }>;
31
+ totalContentBytes: number;
32
+ warnings: string[];
33
+ }
34
+ type WorkerResults = {
35
+ ok: true;
36
+ summary: GenerateSummary;
37
+ } | {
38
+ ok: false;
39
+ message: string;
40
+ };
41
+ /**
42
+ * The absolute path of the package entry this process is running from.
43
+ *
44
+ * `__dirname` exists in the CJS build and `import.meta.url` in the ESM one, and `typeof` on an
45
+ * undeclared identifier does not throw — so this resolves correctly in both without a build-time
46
+ * substitution.
47
+ */
48
+ /**
49
+ * Where the worker finds this package's entry: the path, and the URL the worker imports.
50
+ *
51
+ * Exported so the URL can be tested directly. The defect it fixes only shows up for an install
52
+ * path that contains a `#`, `?` or `%`, or on a Windows drive letter, and no test can move the
53
+ * package's own `dist/` to such a path.
54
+ *
55
+ * `pathToFileURL` rather than `` `file://${path}` ``: the interpolation form turns a Windows path
56
+ * into `file://C:/…`, whose **host** is the drive letter, reads a `#` as a fragment and a `?` as
57
+ * a query, and leaves a space unescaped. `config.ts` already carries this note for the same
58
+ * reason.
59
+ */
60
+ declare function workerEntry(directory: string, isCjs: boolean): {
61
+ path: string;
62
+ url: string;
63
+ };
64
+ /**
65
+ * Generates in a worker and resolves with the summary.
66
+ *
67
+ * A worker that fails to start, exits without posting, or throws at the module level is reported
68
+ * as a tool failure naming the reason — never silently retried on the calling thread, because a
69
+ * quiet fallback would hide exactly the condition this exists to avoid.
70
+ */
71
+ declare function generateInWorker(cwd: string, cliOptions: WorkspaceCliOptions): Promise<WorkerResults>;
72
+
73
+ export { type GenerateSummary as G, type WorkerResults as W, generateInWorker as g, workerEntry as w };