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/CHANGELOG.md +873 -0
- package/README.md +3 -3
- package/dist/bin.js +6343 -3897
- package/dist/{chunk-KUVFTJHK.js → chunk-DUZK5VL2.js} +3036 -1049
- package/dist/{chunk-HQBEQ67K.js → chunk-J4W74N4Y.js} +496 -114
- package/dist/cli.cjs +6282 -3859
- package/dist/cli.d.cts +282 -3
- package/dist/cli.d.ts +282 -3
- package/dist/cli.js +198 -87
- package/dist/generate-worker-D_mUjgXw.d.cts +73 -0
- package/dist/generate-worker-D_mUjgXw.d.ts +73 -0
- package/dist/index.cjs +2981 -1057
- package/dist/index.d.cts +337 -25
- package/dist/index.d.ts +337 -25
- package/dist/index.js +1 -3
- package/dist/mcp.cjs +6426 -5065
- package/dist/mcp.d.cts +136 -68
- package/dist/mcp.d.ts +136 -68
- package/dist/mcp.js +2 -4
- package/dist/{types-Ca4PmkiQ.d.cts → types-DIX6zWSt.d.cts} +130 -2
- package/dist/{types-Ca4PmkiQ.d.ts → types-DIX6zWSt.d.ts} +130 -2
- package/docs/agent.md +1 -1
- package/docs/cli.md +26 -6
- package/docs/configuration.md +48 -9
- package/docs/how-to-feed-a-repo-to-claude.md +61 -0
- package/docs/mcp.md +36 -10
- package/docs/output.md +49 -4
- package/docs/stale-context-is-your-bug.md +13 -0
- package/docs/vs-repomix.md +1 -1
- package/package.json +5 -3
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { buildAgentContract, runStdioServer, COMMAND_OPTION_SCOPE, commandsForOption, SKILL_NAME, renderSkill, renderContractText } from './chunk-
|
|
2
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
632
|
-
|
|
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
|
|
799
|
+
let parsed = null;
|
|
730
800
|
try {
|
|
731
|
-
|
|
801
|
+
parsed = JSON.parse(generated.text);
|
|
732
802
|
} catch {
|
|
733
|
-
|
|
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
|
-
|
|
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
|
|
1150
|
-
|
|
1151
|
-
|
|
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
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
if (
|
|
1245
|
-
|
|
1246
|
-
|
|
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
|
-
|
|
1249
|
-
}
|
|
1250
|
-
|
|
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 };
|