@ankurhalder/codebase-snapshot 4.0.6 → 4.1.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/README.md +4 -1
- package/dist/bin.js +1310 -276
- package/dist/{chunk-GAY7W5MK.js → chunk-6SMZR5EM.js} +1020 -200
- package/dist/cli.cjs +1317 -276
- package/dist/cli.d.cts +60 -2
- package/dist/cli.d.ts +60 -2
- package/dist/cli.js +304 -80
- package/dist/index.cjs +1019 -197
- package/dist/index.d.cts +163 -54
- package/dist/index.d.ts +163 -54
- package/dist/index.js +1 -1
- package/dist/{types-x7aogSer.d.cts → types-BMP676ER.d.cts} +41 -2
- package/dist/{types-x7aogSer.d.ts → types-BMP676ER.d.ts} +41 -2
- package/docs/agent.md +3 -1
- package/docs/cli.md +34 -3
- package/docs/configuration.md +29 -4
- package/docs/install.md +5 -3
- package/docs/output.md +22 -9
- package/docs/troubleshooting.md +60 -3
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { loadConfigFileDetailed, resolveConfig, validateSnapshot, explainPath, compareManifests, loadManifest, renderDiffText, generateSnapshot,
|
|
1
|
+
import { GENERATOR_VERSION, loadConfigFileDetailed, resolveConfig, validateSnapshot, explainPath, compareManifests, loadManifest, renderDiffText, generateSnapshot, isDiscoveryMode, ARTIFACT_NAMES, getPreset, unknownPresetMessage, GENERATOR_NAME, runInit, PromptCancelled, MIN_MAX_PART_BYTES, SCHEMA_VERSION, PRESETS, parseByteSize, escapeControls, renderExplainText } from './chunk-6SMZR5EM.js';
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import * as path2 from 'path';
|
|
4
4
|
import { parseArgs } from 'util';
|
|
@@ -16,6 +16,7 @@ var COMMAND_OPTION_SCOPE = {
|
|
|
16
16
|
"skip-artifacts",
|
|
17
17
|
"only-artifacts",
|
|
18
18
|
"no-config",
|
|
19
|
+
"discovery",
|
|
19
20
|
"format",
|
|
20
21
|
"mode",
|
|
21
22
|
"dry-run",
|
|
@@ -34,6 +35,7 @@ var COMMAND_OPTION_SCOPE = {
|
|
|
34
35
|
"skip-artifacts",
|
|
35
36
|
"only-artifacts",
|
|
36
37
|
"no-config",
|
|
38
|
+
"discovery",
|
|
37
39
|
"format",
|
|
38
40
|
"mode",
|
|
39
41
|
"help",
|
|
@@ -66,12 +68,13 @@ var COMMAND_OPTION_SCOPE = {
|
|
|
66
68
|
"skip-artifacts",
|
|
67
69
|
"only-artifacts",
|
|
68
70
|
"no-config",
|
|
71
|
+
"discovery",
|
|
69
72
|
"format",
|
|
70
73
|
"mode",
|
|
71
74
|
"help",
|
|
72
75
|
"version"
|
|
73
76
|
],
|
|
74
|
-
diff: ["format", "mode", "help", "version"],
|
|
77
|
+
diff: ["format", "mode", "no-config", "help", "version"],
|
|
75
78
|
describe: ["format", "mode", "help", "version"],
|
|
76
79
|
init: [
|
|
77
80
|
"cwd",
|
|
@@ -116,6 +119,7 @@ function optionAppliesTo(option) {
|
|
|
116
119
|
// src/agent/contract.ts
|
|
117
120
|
var CONTRACT_VERSION = "1.0.0";
|
|
118
121
|
var PACKAGE_NAME = "@ankurhalder/codebase-snapshot";
|
|
122
|
+
var MAJOR_VERSION = GENERATOR_VERSION.split(".")[0];
|
|
119
123
|
var ARTIFACT_SUMMARY = {
|
|
120
124
|
readme: "README.md describing the snapshot at the output root.",
|
|
121
125
|
manifest: "manifest.json: machine-readable index of every file, part, hash and config.",
|
|
@@ -215,8 +219,11 @@ function buildAgentContract() {
|
|
|
215
219
|
step: 1,
|
|
216
220
|
id: "verify",
|
|
217
221
|
run: `npx ${PACKAGE_NAME}@latest --version`,
|
|
218
|
-
|
|
219
|
-
|
|
222
|
+
// The requirement is the major version; the exact build is context. A
|
|
223
|
+
// floating tag may print 4.0.7 today and 4.0.9 next week, and an agent
|
|
224
|
+
// must not conclude that a working tool is broken because of a patch.
|
|
225
|
+
expect: `prints v${MAJOR_VERSION} or newer on stdout and exits 0 (this build is v${GENERATOR_VERSION}: compare the major version, not the patch)`,
|
|
226
|
+
notes: `If it prints an older major than this contract's, the @latest tag is behind the release: pin the major with \`npx ${PACKAGE_NAME}@${MAJOR_VERSION} --version\` instead of retrying @latest. If it prints nothing, clear the npm cache and retry, or use \`npx -p <pkg> codebase-snapshot\`.`
|
|
220
227
|
},
|
|
221
228
|
{
|
|
222
229
|
step: 2,
|
|
@@ -229,7 +236,7 @@ function buildAgentContract() {
|
|
|
229
236
|
step: 3,
|
|
230
237
|
id: "configure",
|
|
231
238
|
run: `npx ${PACKAGE_NAME}@latest init --yes`,
|
|
232
|
-
expect:
|
|
239
|
+
expect: 'writes a commented config file listing every setting at its current value: snapshot.config.ts only when the running Node can strip types (>= 22.6) and the project is an ES module (`"type": "module"` in package.json); snapshot.config.mjs otherwise',
|
|
233
240
|
notes: "Non-interactive. The detected preset is chosen automatically; override with --preset <name>. Skip this step to run on built-in defaults."
|
|
234
241
|
},
|
|
235
242
|
{
|
|
@@ -278,7 +285,7 @@ function buildAgentContract() {
|
|
|
278
285
|
{
|
|
279
286
|
name: "init",
|
|
280
287
|
aliases: [],
|
|
281
|
-
summary: "Write a commented snapshot.config.ts
|
|
288
|
+
summary: "Write a commented config file (snapshot.config.ts only when Node can strip types and the project is an ES module; snapshot.config.mjs otherwise). Interactive, or --yes for CI.",
|
|
282
289
|
positionals: [],
|
|
283
290
|
exits: [0, 1, 2]
|
|
284
291
|
},
|
|
@@ -359,6 +366,13 @@ function buildAgentContract() {
|
|
|
359
366
|
appliesTo: optionAppliesTo("include-sensitive-files"),
|
|
360
367
|
summary: "Also emit .env / *.pem / .npmrc style files. Off by default; they are recorded as skipped."
|
|
361
368
|
},
|
|
369
|
+
{
|
|
370
|
+
flag: "--discovery",
|
|
371
|
+
type: "string",
|
|
372
|
+
default: "auto",
|
|
373
|
+
appliesTo: optionAppliesTo("discovery"),
|
|
374
|
+
summary: "How files are discovered: auto (git when usable), git (require git), walk (read the root .gitignore directly; the escape hatch for a .git that cannot be listed)."
|
|
375
|
+
},
|
|
362
376
|
{
|
|
363
377
|
flag: "--skip-artifacts",
|
|
364
378
|
type: "string",
|
|
@@ -501,6 +515,7 @@ function buildAgentContract() {
|
|
|
501
515
|
{ key: "includeSensitiveFiles", type: "boolean", default: false, summary: "Emit .env / *.pem / credential files." },
|
|
502
516
|
{ key: "include", type: "string[]", default: "[]", summary: "Allow-list of globs; empty means everything." },
|
|
503
517
|
{ key: "excludePatterns", type: "string[]", default: "[]", summary: "Extra exclude globs." },
|
|
518
|
+
{ key: "discovery", type: '"auto" | "git" | "walk"', default: "auto", summary: "How files are discovered; `walk` scans the tree and reads .gitignore directly when a .git directory cannot be listed." },
|
|
504
519
|
{ key: "artifacts", type: "object", default: "all on except monolith", summary: "Per-artifact opt-out." },
|
|
505
520
|
{ key: "budgets", type: "object", default: "{ default, byLayer }", summary: "Per-layer byte budgets." },
|
|
506
521
|
{ key: "layersMode", type: '"merge" | "replace"', default: "merge", summary: "Whether custom layers extend or replace the preset's." },
|
|
@@ -522,7 +537,7 @@ function buildAgentContract() {
|
|
|
522
537
|
],
|
|
523
538
|
notes: [
|
|
524
539
|
"Parts are ordered and budget-limited; a single file larger than the budget becomes a head/tail preview.",
|
|
525
|
-
"manifest.json.files[] carries relativePath, layer, kind, language, part, inclusionMode
|
|
540
|
+
"manifest.json.files[] carries relativePath, layer, kind, language, part, inclusionMode and hash; `summary` is conditional \u2014 it is absent for languages with no structural summary, and `summaryOmitted: true` marks a summary dropped to fit the part budget; encoding appears when the file was not plain UTF-8.",
|
|
526
541
|
"manifest.skipped[] carries the path and the exact reason it was excluded.",
|
|
527
542
|
`The output directory is replaced atomically; ${MIN_MAX_PART_BYTES} bytes is the smallest legal budget.`
|
|
528
543
|
]
|
|
@@ -565,6 +580,7 @@ function buildAgentContract() {
|
|
|
565
580
|
|
|
566
581
|
// src/agent/skill.ts
|
|
567
582
|
var PACKAGE_NAME2 = "@ankurhalder/codebase-snapshot";
|
|
583
|
+
var MAJOR_VERSION2 = GENERATOR_VERSION.split(".")[0];
|
|
568
584
|
var SKILL_NAME = "codebase-snapshot";
|
|
569
585
|
var AGENTS_MARKER_START = "<!-- codebase-snapshot:skill:start -->";
|
|
570
586
|
var AGENTS_MARKER_END = "<!-- codebase-snapshot:skill:end -->";
|
|
@@ -589,15 +605,20 @@ another model, and reading files one by one would not fit in context.
|
|
|
589
605
|
This tool is a Node CLI. It needs **Node.js 18 or newer**. You do not need to install it
|
|
590
606
|
globally, and you do not need to clone anything.
|
|
591
607
|
|
|
592
|
-
**Pin
|
|
593
|
-
every machine. The bare package name works too: this package ships
|
|
594
|
-
|
|
595
|
-
|
|
608
|
+
**Pin a version** \u2014 \`npx ${PACKAGE_NAME2}@${MAJOR_VERSION2} <command>\` (or an exact release) \u2014 when you
|
|
609
|
+
need the same version on every machine. The bare package name works too: this package ships
|
|
610
|
+
one executable file, published under two names (\`codebase-snapshot\` and \`snap\`), which is what
|
|
611
|
+
lets npm pick it. If a command exits 0 printing nothing,
|
|
612
|
+
the executable was not resolved \u2014 clear the npm cache and retry; do not assume the repository
|
|
613
|
+
is broken.
|
|
596
614
|
|
|
597
615
|
## Step by step
|
|
598
616
|
|
|
599
|
-
1. **Verify the tool.** Run \`npx ${PACKAGE_NAME2}@latest --version\`. Expect \`v${
|
|
600
|
-
newer on stdout
|
|
617
|
+
1. **Verify the tool.** Run \`npx ${PACKAGE_NAME2}@latest --version\`. Expect \`v${MAJOR_VERSION2}\` or
|
|
618
|
+
newer on stdout \u2014 compare the major version, not the patch, because the
|
|
619
|
+
\`@latest\` tag can lag this release. If the printed major is older, pin the
|
|
620
|
+
major (\`@${MAJOR_VERSION2}\`) instead of retrying. Empty output means you hit the
|
|
621
|
+
bare-spec problem above.
|
|
601
622
|
|
|
602
623
|
2. **Read the contract, not the prose.** Run
|
|
603
624
|
\`npx ${PACKAGE_NAME2}@latest describe --format json\`. It returns the full machine
|
|
@@ -611,11 +632,12 @@ not resolved \u2014 clear the npm cache and retry; do not assume the repository
|
|
|
611
632
|
\`--format json\` if you want to parse it.
|
|
612
633
|
|
|
613
634
|
4. **Configure (recommended).** Run \`npx ${PACKAGE_NAME2}@latest init --yes\`. It detects
|
|
614
|
-
the project shape, then writes
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
635
|
+
the project shape, then writes a commented config file listing **every** setting at its
|
|
636
|
+
current value. The filename depends on the project: \`snapshot.config.ts\` only when
|
|
637
|
+
the running Node can strip types (Node >= 22.6) **and** the project is an ES module
|
|
638
|
+
(\`"type": "module"\` in package.json); otherwise \`snapshot.config.mjs\`. Pass
|
|
639
|
+
\`--preset <name>\` only if detection is wrong; the preset names are in the contract.
|
|
640
|
+
This step is optional: without a config the tool runs on the same defaults.
|
|
619
641
|
|
|
620
642
|
5. **Generate.** Run \`npx ${PACKAGE_NAME2}@latest generate\`. Expect exit 0 and a
|
|
621
643
|
\`./snapshot/\` directory. If you need a different location use \`--out-dir <path>\`; if
|
|
@@ -626,9 +648,13 @@ not resolved \u2014 clear the npm cache and retry; do not assume the repository
|
|
|
626
648
|
|
|
627
649
|
7. **Read the output in this order:**
|
|
628
650
|
- \`snapshot/parts/part-*.md\` \u2014 the code itself, each file tagged with its layer, kind,
|
|
629
|
-
language and a structural summary (imports, exports,
|
|
651
|
+
language and, when it fits the budget, a structural summary (imports, exports,
|
|
652
|
+
classes, functions). A dropped summary appears as a \`Summary: (omitted \u2026)\` line.
|
|
630
653
|
- \`snapshot/manifest.json\` \u2014 the index. \`files[]\` has \`relativePath\`, \`layer\`,
|
|
631
|
-
\`kind\`, \`language\`, \`part\`, \`inclusionMode
|
|
654
|
+
\`kind\`, \`language\`, \`part\`, \`inclusionMode\` and \`hashSha256\`, plus \`encoding\`
|
|
655
|
+
when the file was not plain UTF-8. \`summary\` is **optional**: it is absent for
|
|
656
|
+
languages with no structural summary, and \`summaryOmitted: true\` marks one dropped
|
|
657
|
+
to fit the part budget \u2014 never assume it exists.
|
|
632
658
|
\`skipped[]\` records every excluded file **and why**. Prefer querying this over
|
|
633
659
|
re-reading the parts.
|
|
634
660
|
- \`snapshot/reports/audit.md\` \u2014 secrets found, large files, parse diagnostics, git
|
|
@@ -667,12 +693,16 @@ any of this to get a good first snapshot.
|
|
|
667
693
|
Run \`npx ${PACKAGE_NAME2}@latest skill --install\`. It writes this skill into the
|
|
668
694
|
repository in the places agents look for it by default: an \`AGENTS.md\` section,
|
|
669
695
|
\`.claude/skills/${SKILL_NAME}/SKILL.md\`, and \`.cursor/rules/${SKILL_NAME}.mdc\`. The command
|
|
670
|
-
is idempotent
|
|
696
|
+
is idempotent: it replaces its own marked section, and repairs a malformed one (missing,
|
|
697
|
+
duplicated or nested markers) into a single block while leaving the rest of the file alone.
|
|
698
|
+
It refuses a target that resolves outside the repository, so a linked \`.claude/\` cannot
|
|
699
|
+
be used to write elsewhere.
|
|
671
700
|
`;
|
|
672
701
|
}
|
|
673
702
|
|
|
674
703
|
// src/agent/install-skill.ts
|
|
675
704
|
var SKILL_TARGETS = ["claude", "agents", "cursor", "all"];
|
|
705
|
+
var REFUSED = "Refusing to install the agent skill";
|
|
676
706
|
function targetPath(cwd, target) {
|
|
677
707
|
switch (target) {
|
|
678
708
|
case "claude":
|
|
@@ -683,6 +713,41 @@ function targetPath(cwd, target) {
|
|
|
683
713
|
return path2.join(cwd, "AGENTS.md");
|
|
684
714
|
}
|
|
685
715
|
}
|
|
716
|
+
function assertWritableInsideRepository(cwd, filePath) {
|
|
717
|
+
const root = fs.realpathSync(cwd);
|
|
718
|
+
const shown = path2.relative(root, filePath) || filePath;
|
|
719
|
+
let stat = null;
|
|
720
|
+
try {
|
|
721
|
+
stat = fs.lstatSync(filePath);
|
|
722
|
+
} catch {
|
|
723
|
+
stat = null;
|
|
724
|
+
}
|
|
725
|
+
if (stat !== null && stat.isSymbolicLink()) {
|
|
726
|
+
throw new Error(
|
|
727
|
+
`${REFUSED}: ${shown} is a symbolic link or junction. Remove the link, or write the skill into the directory it points at yourself, and run again.`
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
if (stat !== null && stat.isFile() && stat.nlink > 1) {
|
|
731
|
+
throw new Error(
|
|
732
|
+
`${REFUSED}: ${shown} has ${stat.nlink} hard links, so writing it would also rewrite a file outside the repository. Replace it with an ordinary file and run again.`
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
let existing = filePath;
|
|
736
|
+
const missing = [];
|
|
737
|
+
while (!fs.existsSync(existing)) {
|
|
738
|
+
missing.unshift(path2.basename(existing));
|
|
739
|
+
const parent = path2.dirname(existing);
|
|
740
|
+
if (parent === existing) break;
|
|
741
|
+
existing = parent;
|
|
742
|
+
}
|
|
743
|
+
const realAncestor = fs.realpathSync(existing);
|
|
744
|
+
const resolved = missing.length === 0 ? realAncestor : path2.join(realAncestor, ...missing);
|
|
745
|
+
if (resolved !== root && !resolved.startsWith(`${root}${path2.sep}`)) {
|
|
746
|
+
throw new Error(
|
|
747
|
+
`${REFUSED}: ${shown} resolves outside the repository (${resolved}). Move it back inside ${root}, or remove the link, and run again.`
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
686
751
|
function writeIfChanged(target, filePath, content) {
|
|
687
752
|
const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : null;
|
|
688
753
|
if (existing === content) return { target, path: filePath, action: "unchanged" };
|
|
@@ -704,34 +769,60 @@ function agentsBlock(skill) {
|
|
|
704
769
|
""
|
|
705
770
|
].join("\n");
|
|
706
771
|
}
|
|
772
|
+
function ownSpans(existing) {
|
|
773
|
+
const marks = [];
|
|
774
|
+
let cursor = 0;
|
|
775
|
+
while (cursor < existing.length) {
|
|
776
|
+
const start = existing.indexOf(AGENTS_MARKER_START, cursor);
|
|
777
|
+
const end = existing.indexOf(AGENTS_MARKER_END, cursor);
|
|
778
|
+
if (start === -1 && end === -1) break;
|
|
779
|
+
if (start !== -1 && (end === -1 || start < end)) {
|
|
780
|
+
marks.push({ kind: "start", start, end: start + AGENTS_MARKER_START.length });
|
|
781
|
+
cursor = start + AGENTS_MARKER_START.length;
|
|
782
|
+
} else {
|
|
783
|
+
marks.push({ kind: "end", start: end, end: end + AGENTS_MARKER_END.length });
|
|
784
|
+
cursor = end + AGENTS_MARKER_END.length;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
const spans = [];
|
|
788
|
+
let open = -1;
|
|
789
|
+
for (const mark of marks) {
|
|
790
|
+
if (mark.kind === "start") {
|
|
791
|
+
if (open === -1) open = mark.start;
|
|
792
|
+
} else if (open !== -1) {
|
|
793
|
+
spans.push({ start: open, end: mark.end });
|
|
794
|
+
open = -1;
|
|
795
|
+
} else {
|
|
796
|
+
spans.push({ start: mark.start, end: mark.end });
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
if (open !== -1) spans.push({ start: open, end: open + AGENTS_MARKER_START.length });
|
|
800
|
+
return spans;
|
|
801
|
+
}
|
|
802
|
+
function mergeMarkedSection(existing, block) {
|
|
803
|
+
const spans = ownSpans(existing);
|
|
804
|
+
if (spans.length === 0) return `${existing.trimEnd()}
|
|
805
|
+
|
|
806
|
+
${block}`;
|
|
807
|
+
let stripped = "";
|
|
808
|
+
let from = 0;
|
|
809
|
+
let insertAt = -1;
|
|
810
|
+
for (const span of spans) {
|
|
811
|
+
if (span.start < from) continue;
|
|
812
|
+
stripped += existing.slice(from, span.start);
|
|
813
|
+
if (insertAt === -1) insertAt = stripped.length;
|
|
814
|
+
from = span.end;
|
|
815
|
+
}
|
|
816
|
+
stripped += existing.slice(from);
|
|
817
|
+
return `${stripped.slice(0, insertAt)}${block.trimEnd()}${stripped.slice(insertAt)}`;
|
|
818
|
+
}
|
|
707
819
|
function mergeAgentsFile(cwd, skill) {
|
|
708
820
|
const filePath = targetPath(cwd, "agents");
|
|
709
821
|
const block = agentsBlock(skill);
|
|
710
822
|
const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : null;
|
|
711
|
-
|
|
712
|
-
if (existing === null) {
|
|
713
|
-
next = `# AGENTS.md
|
|
714
|
-
|
|
715
|
-
${block}`;
|
|
716
|
-
} else {
|
|
717
|
-
const starts = existing.split(AGENTS_MARKER_START).length - 1;
|
|
718
|
-
const ends = existing.split(AGENTS_MARKER_END).length - 1;
|
|
719
|
-
const start = existing.indexOf(AGENTS_MARKER_START);
|
|
720
|
-
const end = existing.indexOf(AGENTS_MARKER_END) + AGENTS_MARKER_END.length;
|
|
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()}
|
|
823
|
+
const next = existing === null ? `# AGENTS.md
|
|
731
824
|
|
|
732
|
-
${block}
|
|
733
|
-
}
|
|
734
|
-
}
|
|
825
|
+
${block}` : mergeMarkedSection(existing, block);
|
|
735
826
|
const action = existing === null ? "created" : existing === next ? "unchanged" : "updated";
|
|
736
827
|
if (action !== "unchanged") {
|
|
737
828
|
fs.mkdirSync(path2.dirname(filePath), { recursive: true });
|
|
@@ -743,6 +834,7 @@ function installSkill(cwd, target = "all") {
|
|
|
743
834
|
const skill = renderSkill();
|
|
744
835
|
const written = [];
|
|
745
836
|
const wanted = target === "all" ? ["agents", "claude", "cursor"] : [target];
|
|
837
|
+
for (const item of wanted) assertWritableInsideRepository(cwd, targetPath(cwd, item));
|
|
746
838
|
for (const item of wanted) {
|
|
747
839
|
if (item === "agents") {
|
|
748
840
|
written.push(mergeAgentsFile(cwd, skill));
|
|
@@ -794,6 +886,7 @@ var CLI_OPTIONS = {
|
|
|
794
886
|
"dry-run": { type: "boolean" },
|
|
795
887
|
watch: { type: "boolean" },
|
|
796
888
|
"no-config": { type: "boolean" },
|
|
889
|
+
discovery: { type: "string" },
|
|
797
890
|
preset: { type: "string" },
|
|
798
891
|
include: { type: "string" },
|
|
799
892
|
exclude: { type: "string" },
|
|
@@ -841,6 +934,9 @@ Options:
|
|
|
841
934
|
--dry-run Compute the snapshot without writing anything
|
|
842
935
|
--watch Re-generate whenever the repository changes
|
|
843
936
|
--no-config Ignore snapshot.config.* and use the built-in defaults
|
|
937
|
+
--discovery <mode> auto | git | walk \u2014 how files are discovered.
|
|
938
|
+
auto: git when usable. git: require git. walk: read
|
|
939
|
+
.gitignore directly (escape hatch for a broken .git)
|
|
844
940
|
|
|
845
941
|
init only:
|
|
846
942
|
--preset <name> Force a project preset instead of detecting one
|
|
@@ -970,6 +1066,19 @@ function parseCliArgs(argv) {
|
|
|
970
1066
|
}
|
|
971
1067
|
command = resolved;
|
|
972
1068
|
}
|
|
1069
|
+
if (explicitMode !== void 0) {
|
|
1070
|
+
const resolvedMode = COMMAND_ALIASES.get(explicitMode);
|
|
1071
|
+
if (!resolvedMode) {
|
|
1072
|
+
throw new UsageError(
|
|
1073
|
+
`Unknown --mode "${explicitMode}". Expected one of: ${Array.from(COMMAND_ALIASES.keys()).join(", ")}.`
|
|
1074
|
+
);
|
|
1075
|
+
}
|
|
1076
|
+
if (firstPositional !== void 0 && resolvedMode !== command) {
|
|
1077
|
+
throw new UsageError(
|
|
1078
|
+
`--mode ${explicitMode} contradicts the command "${firstPositional}". Pass one or the other.`
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
973
1082
|
const expectedArgs = COMMAND_ARITY[command];
|
|
974
1083
|
if (args.length !== expectedArgs) {
|
|
975
1084
|
if (command === "explain") {
|
|
@@ -988,6 +1097,13 @@ function parseCliArgs(argv) {
|
|
|
988
1097
|
if (rawFormat !== "text" && rawFormat !== "json") {
|
|
989
1098
|
throw new UsageError(`--format must be "text" or "json", received "${rawFormat}".`);
|
|
990
1099
|
}
|
|
1100
|
+
const rawDiscovery = values.discovery === void 0 ? void 0 : String(values.discovery);
|
|
1101
|
+
if (rawDiscovery !== void 0 && !isDiscoveryMode(rawDiscovery)) {
|
|
1102
|
+
throw new UsageError(
|
|
1103
|
+
`--discovery must be one of: auto, git, walk (received "${rawDiscovery}").`
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
let onlyNamesMonolith = false;
|
|
991
1107
|
const rawSkip = values["skip-artifacts"];
|
|
992
1108
|
const rawOnly = values["only-artifacts"];
|
|
993
1109
|
if (rawSkip !== void 0 && rawOnly !== void 0) {
|
|
@@ -996,6 +1112,7 @@ function parseCliArgs(argv) {
|
|
|
996
1112
|
let skipArtifacts;
|
|
997
1113
|
if (rawOnly !== void 0) {
|
|
998
1114
|
const keep = parseArtifactList(String(rawOnly), "--only-artifacts");
|
|
1115
|
+
onlyNamesMonolith = keep.includes("monolith");
|
|
999
1116
|
skipArtifacts = ARTIFACT_NAMES.filter(
|
|
1000
1117
|
(name) => !keep.includes(name)
|
|
1001
1118
|
);
|
|
@@ -1050,7 +1167,7 @@ function parseCliArgs(argv) {
|
|
|
1050
1167
|
target: values.target === void 0 ? void 0 : String(values.target),
|
|
1051
1168
|
cwd: values.cwd === void 0 ? process.cwd() : path2.resolve(String(values.cwd)),
|
|
1052
1169
|
outDir: values["out-dir"] === void 0 ? void 0 : String(values["out-dir"]),
|
|
1053
|
-
includeMonolith: values["include-monolith"] === true ? true : void 0,
|
|
1170
|
+
includeMonolith: values["include-monolith"] === true || onlyNamesMonolith === true ? true : void 0,
|
|
1054
1171
|
includeSensitiveFiles: values["include-sensitive-files"] === true ? true : void 0,
|
|
1055
1172
|
maxPartBytes: parseByteOption(
|
|
1056
1173
|
values["max-part-bytes"],
|
|
@@ -1067,17 +1184,21 @@ function parseCliArgs(argv) {
|
|
|
1067
1184
|
"max-monolith-bytes",
|
|
1068
1185
|
1
|
|
1069
1186
|
),
|
|
1070
|
-
skipArtifacts
|
|
1187
|
+
skipArtifacts,
|
|
1188
|
+
discovery: isDiscoveryMode(rawDiscovery) ? rawDiscovery : void 0
|
|
1071
1189
|
};
|
|
1072
1190
|
}
|
|
1073
1191
|
var HelpRequested = class extends Error {
|
|
1074
1192
|
};
|
|
1193
|
+
function escapeForTerminal(message) {
|
|
1194
|
+
return message.split("\n").map((line) => escapeControls(line)).join("\n");
|
|
1195
|
+
}
|
|
1075
1196
|
function info(message) {
|
|
1076
|
-
process.stderr.write(`${message}
|
|
1197
|
+
process.stderr.write(`${escapeForTerminal(message)}
|
|
1077
1198
|
`);
|
|
1078
1199
|
}
|
|
1079
1200
|
function warn(message) {
|
|
1080
|
-
process.stderr.write(`${message}
|
|
1201
|
+
process.stderr.write(`${escapeForTerminal(message)}
|
|
1081
1202
|
`);
|
|
1082
1203
|
}
|
|
1083
1204
|
function emit(format, payload, textRenderer) {
|
|
@@ -1094,8 +1215,8 @@ function splitList(value) {
|
|
|
1094
1215
|
}
|
|
1095
1216
|
function displayOutPath(cwd, outDir, ...segments) {
|
|
1096
1217
|
const absolute = path2.resolve(cwd, outDir, ...segments);
|
|
1097
|
-
const
|
|
1098
|
-
if (
|
|
1218
|
+
const relative3 = path2.relative(cwd, absolute);
|
|
1219
|
+
if (relative3 && !relative3.startsWith("..") && !path2.isAbsolute(relative3)) return relative3;
|
|
1099
1220
|
return absolute;
|
|
1100
1221
|
}
|
|
1101
1222
|
function describeManifestShapeProblem(manifest) {
|
|
@@ -1107,11 +1228,32 @@ function describeManifestShapeProblem(manifest) {
|
|
|
1107
1228
|
if (!counts || typeof counts !== "object") return "missing `counts`";
|
|
1108
1229
|
for (const key of ["totalFiles", "totalLines", "totalSizeBytes", "partsCount"]) {
|
|
1109
1230
|
if (typeof counts[key] !== "number") return `missing numeric \`counts.${key}\``;
|
|
1231
|
+
if (!Number.isSafeInteger(counts[key]) || counts[key] < 0) {
|
|
1232
|
+
return `\`counts.${key}\` must be a non-negative integer`;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
if (candidate.fingerprint !== void 0 && !/^[0-9a-f]{64}$/.test(String(candidate.fingerprint))) {
|
|
1236
|
+
return "`fingerprint` must be a 64-character sha256 hex digest";
|
|
1110
1237
|
}
|
|
1111
1238
|
if (!candidate.layers || typeof candidate.layers !== "object") return "missing `layers`";
|
|
1112
1239
|
if (!Array.isArray(candidate.files)) return "missing `files` array";
|
|
1113
1240
|
return null;
|
|
1114
1241
|
}
|
|
1242
|
+
function nextStepHint(cwd, scriptsAddedNow) {
|
|
1243
|
+
let hasScript = scriptsAddedNow;
|
|
1244
|
+
if (!hasScript) {
|
|
1245
|
+
try {
|
|
1246
|
+
const parsed = JSON.parse(fs.readFileSync(path2.join(cwd, "package.json"), "utf8"));
|
|
1247
|
+
hasScript = typeof parsed.scripts?.snapshot === "string";
|
|
1248
|
+
} catch {
|
|
1249
|
+
hasScript = false;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
if (!hasScript) return `npx ${GENERATOR_NAME} generate`;
|
|
1253
|
+
const bin = path2.join(cwd, "node_modules", ".bin", "codebase-snapshot");
|
|
1254
|
+
const installed = fs.existsSync(bin) || fs.existsSync(`${bin}.cmd`);
|
|
1255
|
+
return installed ? "npm run snapshot" : "npm install && npm run snapshot";
|
|
1256
|
+
}
|
|
1115
1257
|
async function runInitCommand(invocation) {
|
|
1116
1258
|
try {
|
|
1117
1259
|
const result = await runInit({
|
|
@@ -1144,11 +1286,7 @@ async function runInitCommand(invocation) {
|
|
|
1144
1286
|
}
|
|
1145
1287
|
for (const note of result.notes) info(` \u26A0 ${note}`);
|
|
1146
1288
|
info("");
|
|
1147
|
-
|
|
1148
|
-
info(" Next: npm run snapshot");
|
|
1149
|
-
} else {
|
|
1150
|
-
info(` Next: ${GENERATOR_NAME} generate`);
|
|
1151
|
-
}
|
|
1289
|
+
info(` Next: ${nextStepHint(invocation.cwd, result.scripts.length > 0)}`);
|
|
1152
1290
|
info(
|
|
1153
1291
|
` Tip: \`${GENERATOR_NAME} skill --install\` makes this repo discoverable by AI agents.`
|
|
1154
1292
|
);
|
|
@@ -1200,6 +1338,9 @@ function renderValidateText(result) {
|
|
|
1200
1338
|
} else {
|
|
1201
1339
|
warn("\u274C Snapshot validation failed:");
|
|
1202
1340
|
for (const error of result.errors) warn(` - ${error}`);
|
|
1341
|
+
for (const unreadable of result.drift.unreadable.slice(0, 20)) {
|
|
1342
|
+
warn(` - unreadable: ${unreadable}`);
|
|
1343
|
+
}
|
|
1203
1344
|
for (const drift of result.drift.hashMismatches.slice(0, 20)) {
|
|
1204
1345
|
warn(` - modified: ${drift.path}`);
|
|
1205
1346
|
}
|
|
@@ -1247,7 +1388,8 @@ ${USAGE}
|
|
|
1247
1388
|
if (invocation.command === "skill") {
|
|
1248
1389
|
return runSkillCommand(invocation);
|
|
1249
1390
|
}
|
|
1250
|
-
const
|
|
1391
|
+
const skipConfig = invocation.noConfig || invocation.command === "diff";
|
|
1392
|
+
const loaded = skipConfig ? { config: null, warnings: [], errors: [], file: null } : await loadConfigFileDetailed(invocation.cwd);
|
|
1251
1393
|
if (invocation.noConfig) info("\u2699 --no-config: using built-in defaults, config file ignored");
|
|
1252
1394
|
for (const warning of loaded.warnings) warn(`\u26A0 Config: ${warning}`);
|
|
1253
1395
|
if (loaded.errors.length > 0) {
|
|
@@ -1268,12 +1410,27 @@ ${USAGE}
|
|
|
1268
1410
|
maxPartBytes: invocation.maxPartBytes,
|
|
1269
1411
|
largeFileBytes: invocation.largeFileBytes,
|
|
1270
1412
|
maxMonolithBytes: invocation.maxMonolithBytes,
|
|
1271
|
-
skipArtifacts: invocation.skipArtifacts
|
|
1413
|
+
skipArtifacts: invocation.skipArtifacts,
|
|
1414
|
+
discovery: invocation.discovery
|
|
1272
1415
|
},
|
|
1273
1416
|
invocation.cwd
|
|
1274
1417
|
);
|
|
1275
1418
|
if (invocation.command === "validate") {
|
|
1276
1419
|
const result = validateSnapshot(resolved);
|
|
1420
|
+
if (result.valid) {
|
|
1421
|
+
const manifestPath = path2.resolve(resolved.cwd, resolved.outDir, "manifest.json");
|
|
1422
|
+
let manifest;
|
|
1423
|
+
try {
|
|
1424
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
1425
|
+
} catch {
|
|
1426
|
+
manifest = void 0;
|
|
1427
|
+
}
|
|
1428
|
+
const shapeError = manifest === void 0 ? null : describeManifestShapeProblem(manifest);
|
|
1429
|
+
if (shapeError) {
|
|
1430
|
+
warn(`\u274C ${manifestPath} is not a snapshot manifest: ${shapeError}`);
|
|
1431
|
+
return 1;
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1277
1434
|
emit(invocation.format, result, () => renderValidateText(result));
|
|
1278
1435
|
return result.valid ? 0 : 1;
|
|
1279
1436
|
}
|
|
@@ -1423,24 +1580,79 @@ ${USAGE}
|
|
|
1423
1580
|
return 1;
|
|
1424
1581
|
}
|
|
1425
1582
|
}
|
|
1583
|
+
var WATCH_DEBOUNCE_MS = 250;
|
|
1584
|
+
var WATCH_SETTLE_MS = 1500;
|
|
1585
|
+
var VCS_SEGMENTS = /* @__PURE__ */ new Set([".git", ".hg", ".svn", ".bzr"]);
|
|
1586
|
+
function initialWatchState() {
|
|
1587
|
+
return { running: false, pending: false, settleUntil: 0 };
|
|
1588
|
+
}
|
|
1589
|
+
function beginWatchRun(state) {
|
|
1590
|
+
return { ...state, running: true, pending: false };
|
|
1591
|
+
}
|
|
1592
|
+
function finishWatchRun(state, now) {
|
|
1593
|
+
return {
|
|
1594
|
+
state: { running: false, pending: false, settleUntil: now + WATCH_SETTLE_MS },
|
|
1595
|
+
schedule: state.pending
|
|
1596
|
+
};
|
|
1597
|
+
}
|
|
1598
|
+
function comparablePath(value) {
|
|
1599
|
+
const absolute = path2.resolve(value);
|
|
1600
|
+
return process.platform === "win32" ? absolute.toLowerCase() : absolute;
|
|
1601
|
+
}
|
|
1602
|
+
function isSameOrInside(parent, candidate) {
|
|
1603
|
+
const relative3 = path2.relative(parent, candidate);
|
|
1604
|
+
return relative3 === "" || relative3 !== ".." && !relative3.startsWith(`..${path2.sep}`) && !path2.isAbsolute(relative3);
|
|
1605
|
+
}
|
|
1606
|
+
function isGeneratedWatchPath(candidate, paths) {
|
|
1607
|
+
const absolute = comparablePath(path2.resolve(paths.cwd, candidate));
|
|
1608
|
+
const outDir = comparablePath(path2.resolve(paths.cwd, paths.outDir));
|
|
1609
|
+
const tmpDir = comparablePath(path2.resolve(paths.cwd, paths.tmpDir));
|
|
1610
|
+
if (isSameOrInside(outDir, absolute) || isSameOrInside(tmpDir, absolute)) return true;
|
|
1611
|
+
const parent = path2.dirname(absolute);
|
|
1612
|
+
if (parent === path2.dirname(outDir) && path2.basename(absolute).startsWith(`${path2.basename(outDir)}.backup-`)) {
|
|
1613
|
+
return true;
|
|
1614
|
+
}
|
|
1615
|
+
return absolute.split(/[\\/]+/).some((segment) => VCS_SEGMENTS.has(segment));
|
|
1616
|
+
}
|
|
1617
|
+
function decideWatchEvent(state, event, paths) {
|
|
1618
|
+
if (event.path !== null) {
|
|
1619
|
+
if (isGeneratedWatchPath(event.path, paths)) return { state, action: "ignore" };
|
|
1620
|
+
} else if (state.running || event.now < state.settleUntil) {
|
|
1621
|
+
return { state, action: "ignore" };
|
|
1622
|
+
}
|
|
1623
|
+
if (state.running) {
|
|
1624
|
+
return { state: { ...state, pending: true }, action: "pending" };
|
|
1625
|
+
}
|
|
1626
|
+
return { state, action: "schedule" };
|
|
1627
|
+
}
|
|
1426
1628
|
async function runWatchLoop(invocation, resolved) {
|
|
1427
1629
|
const outDir = path2.resolve(resolved.cwd, resolved.outDir);
|
|
1428
1630
|
const tmpDir = path2.resolve(resolved.cwd, resolved.tmpDir);
|
|
1429
|
-
const
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1631
|
+
const paths = { cwd: resolved.cwd, outDir, tmpDir };
|
|
1632
|
+
let state = initialWatchState();
|
|
1633
|
+
let timer = null;
|
|
1634
|
+
let stopped = false;
|
|
1635
|
+
const schedule = (delay = WATCH_DEBOUNCE_MS) => {
|
|
1636
|
+
if (stopped) return;
|
|
1637
|
+
if (timer) clearTimeout(timer);
|
|
1638
|
+
timer = setTimeout(() => {
|
|
1639
|
+
timer = null;
|
|
1640
|
+
void regenerate();
|
|
1641
|
+
}, delay);
|
|
1642
|
+
};
|
|
1438
1643
|
const regenerate = async () => {
|
|
1439
|
-
if (running)
|
|
1440
|
-
|
|
1644
|
+
if (state.running) {
|
|
1645
|
+
state = { ...state, pending: true };
|
|
1646
|
+
if (timer) {
|
|
1647
|
+
clearTimeout(timer);
|
|
1648
|
+
timer = null;
|
|
1649
|
+
}
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1652
|
+
state = beginWatchRun(state);
|
|
1441
1653
|
const started = performance.now();
|
|
1442
1654
|
try {
|
|
1443
|
-
const loaded = await loadConfigFileDetailed(invocation.cwd);
|
|
1655
|
+
const loaded = invocation.noConfig ? { config: null, warnings: [], errors: [], file: null } : await loadConfigFileDetailed(invocation.cwd);
|
|
1444
1656
|
if (loaded.errors.length > 0) {
|
|
1445
1657
|
for (const error of loaded.errors) warn(`\u26A0 Config: ${error}`);
|
|
1446
1658
|
warn("\u274C The config file could not be read. Fix it, or delete it to use the defaults.");
|
|
@@ -1460,7 +1672,8 @@ async function runWatchLoop(invocation, resolved) {
|
|
|
1460
1672
|
maxPartBytes: invocation.maxPartBytes,
|
|
1461
1673
|
largeFileBytes: invocation.largeFileBytes,
|
|
1462
1674
|
maxMonolithBytes: invocation.maxMonolithBytes,
|
|
1463
|
-
skipArtifacts: invocation.skipArtifacts
|
|
1675
|
+
skipArtifacts: invocation.skipArtifacts,
|
|
1676
|
+
discovery: invocation.discovery
|
|
1464
1677
|
},
|
|
1465
1678
|
invocation.cwd
|
|
1466
1679
|
);
|
|
@@ -1472,24 +1685,35 @@ async function runWatchLoop(invocation, resolved) {
|
|
|
1472
1685
|
} catch (err) {
|
|
1473
1686
|
warn(`\u274C ${err.message}`);
|
|
1474
1687
|
} finally {
|
|
1475
|
-
|
|
1476
|
-
|
|
1688
|
+
const finished = finishWatchRun(state, performance.now());
|
|
1689
|
+
state = finished.state;
|
|
1690
|
+
if (finished.schedule) schedule(0);
|
|
1477
1691
|
}
|
|
1478
1692
|
};
|
|
1479
1693
|
info("\u{1F440} Watching for changes. Press Ctrl+C to stop.");
|
|
1480
1694
|
void regenerate();
|
|
1481
|
-
let timer = null;
|
|
1482
1695
|
const watcher = fs.watch(resolved.cwd, { recursive: true }, (_event, filename) => {
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1696
|
+
const decision = decideWatchEvent(
|
|
1697
|
+
state,
|
|
1698
|
+
{
|
|
1699
|
+
path: filename ? path2.resolve(resolved.cwd, filename.toString()) : null,
|
|
1700
|
+
now: performance.now()
|
|
1701
|
+
},
|
|
1702
|
+
paths
|
|
1703
|
+
);
|
|
1704
|
+
state = decision.state;
|
|
1705
|
+
if (decision.action === "pending") {
|
|
1706
|
+
if (timer) {
|
|
1707
|
+
clearTimeout(timer);
|
|
1708
|
+
timer = null;
|
|
1709
|
+
}
|
|
1710
|
+
return;
|
|
1487
1711
|
}
|
|
1488
|
-
if (
|
|
1489
|
-
timer = setTimeout(() => void regenerate(), 250);
|
|
1712
|
+
if (decision.action === "schedule") schedule();
|
|
1490
1713
|
});
|
|
1491
1714
|
await new Promise((resolve2) => {
|
|
1492
1715
|
const stop = () => {
|
|
1716
|
+
stopped = true;
|
|
1493
1717
|
if (timer) clearTimeout(timer);
|
|
1494
1718
|
watcher.close();
|
|
1495
1719
|
resolve2();
|
|
@@ -1515,6 +1739,6 @@ if (invokedDirectly) {
|
|
|
1515
1739
|
);
|
|
1516
1740
|
}
|
|
1517
1741
|
|
|
1518
|
-
export { UsageError, main, parseByteOption, parseCliArgs, parsePositiveInteger };
|
|
1742
|
+
export { UsageError, WATCH_DEBOUNCE_MS, WATCH_SETTLE_MS, beginWatchRun, decideWatchEvent, finishWatchRun, initialWatchState, isGeneratedWatchPath, main, parseByteOption, parseCliArgs, parsePositiveInteger };
|
|
1519
1743
|
//# sourceMappingURL=cli.js.map
|
|
1520
1744
|
//# sourceMappingURL=cli.js.map
|