@ankurhalder/codebase-snapshot 4.0.7 → 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 +1279 -268
- package/dist/{chunk-VRJTXNJV.js → chunk-6SMZR5EM.js} +998 -193
- package/dist/cli.cjs +1286 -268
- package/dist/cli.d.cts +60 -2
- package/dist/cli.d.ts +60 -2
- package/dist/cli.js +288 -79
- package/dist/index.cjs +997 -190
- 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
|
|
@@ -1001,6 +1097,12 @@ function parseCliArgs(argv) {
|
|
|
1001
1097
|
if (rawFormat !== "text" && rawFormat !== "json") {
|
|
1002
1098
|
throw new UsageError(`--format must be "text" or "json", received "${rawFormat}".`);
|
|
1003
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
|
+
}
|
|
1004
1106
|
let onlyNamesMonolith = false;
|
|
1005
1107
|
const rawSkip = values["skip-artifacts"];
|
|
1006
1108
|
const rawOnly = values["only-artifacts"];
|
|
@@ -1082,17 +1184,21 @@ function parseCliArgs(argv) {
|
|
|
1082
1184
|
"max-monolith-bytes",
|
|
1083
1185
|
1
|
|
1084
1186
|
),
|
|
1085
|
-
skipArtifacts
|
|
1187
|
+
skipArtifacts,
|
|
1188
|
+
discovery: isDiscoveryMode(rawDiscovery) ? rawDiscovery : void 0
|
|
1086
1189
|
};
|
|
1087
1190
|
}
|
|
1088
1191
|
var HelpRequested = class extends Error {
|
|
1089
1192
|
};
|
|
1193
|
+
function escapeForTerminal(message) {
|
|
1194
|
+
return message.split("\n").map((line) => escapeControls(line)).join("\n");
|
|
1195
|
+
}
|
|
1090
1196
|
function info(message) {
|
|
1091
|
-
process.stderr.write(`${message}
|
|
1197
|
+
process.stderr.write(`${escapeForTerminal(message)}
|
|
1092
1198
|
`);
|
|
1093
1199
|
}
|
|
1094
1200
|
function warn(message) {
|
|
1095
|
-
process.stderr.write(`${message}
|
|
1201
|
+
process.stderr.write(`${escapeForTerminal(message)}
|
|
1096
1202
|
`);
|
|
1097
1203
|
}
|
|
1098
1204
|
function emit(format, payload, textRenderer) {
|
|
@@ -1109,8 +1215,8 @@ function splitList(value) {
|
|
|
1109
1215
|
}
|
|
1110
1216
|
function displayOutPath(cwd, outDir, ...segments) {
|
|
1111
1217
|
const absolute = path2.resolve(cwd, outDir, ...segments);
|
|
1112
|
-
const
|
|
1113
|
-
if (
|
|
1218
|
+
const relative3 = path2.relative(cwd, absolute);
|
|
1219
|
+
if (relative3 && !relative3.startsWith("..") && !path2.isAbsolute(relative3)) return relative3;
|
|
1114
1220
|
return absolute;
|
|
1115
1221
|
}
|
|
1116
1222
|
function describeManifestShapeProblem(manifest) {
|
|
@@ -1122,11 +1228,32 @@ function describeManifestShapeProblem(manifest) {
|
|
|
1122
1228
|
if (!counts || typeof counts !== "object") return "missing `counts`";
|
|
1123
1229
|
for (const key of ["totalFiles", "totalLines", "totalSizeBytes", "partsCount"]) {
|
|
1124
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";
|
|
1125
1237
|
}
|
|
1126
1238
|
if (!candidate.layers || typeof candidate.layers !== "object") return "missing `layers`";
|
|
1127
1239
|
if (!Array.isArray(candidate.files)) return "missing `files` array";
|
|
1128
1240
|
return null;
|
|
1129
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
|
+
}
|
|
1130
1257
|
async function runInitCommand(invocation) {
|
|
1131
1258
|
try {
|
|
1132
1259
|
const result = await runInit({
|
|
@@ -1159,11 +1286,7 @@ async function runInitCommand(invocation) {
|
|
|
1159
1286
|
}
|
|
1160
1287
|
for (const note of result.notes) info(` \u26A0 ${note}`);
|
|
1161
1288
|
info("");
|
|
1162
|
-
|
|
1163
|
-
info(" Next: npm run snapshot");
|
|
1164
|
-
} else {
|
|
1165
|
-
info(` Next: ${GENERATOR_NAME} generate`);
|
|
1166
|
-
}
|
|
1289
|
+
info(` Next: ${nextStepHint(invocation.cwd, result.scripts.length > 0)}`);
|
|
1167
1290
|
info(
|
|
1168
1291
|
` Tip: \`${GENERATOR_NAME} skill --install\` makes this repo discoverable by AI agents.`
|
|
1169
1292
|
);
|
|
@@ -1215,6 +1338,9 @@ function renderValidateText(result) {
|
|
|
1215
1338
|
} else {
|
|
1216
1339
|
warn("\u274C Snapshot validation failed:");
|
|
1217
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
|
+
}
|
|
1218
1344
|
for (const drift of result.drift.hashMismatches.slice(0, 20)) {
|
|
1219
1345
|
warn(` - modified: ${drift.path}`);
|
|
1220
1346
|
}
|
|
@@ -1262,7 +1388,8 @@ ${USAGE}
|
|
|
1262
1388
|
if (invocation.command === "skill") {
|
|
1263
1389
|
return runSkillCommand(invocation);
|
|
1264
1390
|
}
|
|
1265
|
-
const
|
|
1391
|
+
const skipConfig = invocation.noConfig || invocation.command === "diff";
|
|
1392
|
+
const loaded = skipConfig ? { config: null, warnings: [], errors: [], file: null } : await loadConfigFileDetailed(invocation.cwd);
|
|
1266
1393
|
if (invocation.noConfig) info("\u2699 --no-config: using built-in defaults, config file ignored");
|
|
1267
1394
|
for (const warning of loaded.warnings) warn(`\u26A0 Config: ${warning}`);
|
|
1268
1395
|
if (loaded.errors.length > 0) {
|
|
@@ -1283,12 +1410,27 @@ ${USAGE}
|
|
|
1283
1410
|
maxPartBytes: invocation.maxPartBytes,
|
|
1284
1411
|
largeFileBytes: invocation.largeFileBytes,
|
|
1285
1412
|
maxMonolithBytes: invocation.maxMonolithBytes,
|
|
1286
|
-
skipArtifacts: invocation.skipArtifacts
|
|
1413
|
+
skipArtifacts: invocation.skipArtifacts,
|
|
1414
|
+
discovery: invocation.discovery
|
|
1287
1415
|
},
|
|
1288
1416
|
invocation.cwd
|
|
1289
1417
|
);
|
|
1290
1418
|
if (invocation.command === "validate") {
|
|
1291
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
|
+
}
|
|
1292
1434
|
emit(invocation.format, result, () => renderValidateText(result));
|
|
1293
1435
|
return result.valid ? 0 : 1;
|
|
1294
1436
|
}
|
|
@@ -1438,24 +1580,79 @@ ${USAGE}
|
|
|
1438
1580
|
return 1;
|
|
1439
1581
|
}
|
|
1440
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
|
+
}
|
|
1441
1628
|
async function runWatchLoop(invocation, resolved) {
|
|
1442
1629
|
const outDir = path2.resolve(resolved.cwd, resolved.outDir);
|
|
1443
1630
|
const tmpDir = path2.resolve(resolved.cwd, resolved.tmpDir);
|
|
1444
|
-
const
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
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
|
+
};
|
|
1453
1643
|
const regenerate = async () => {
|
|
1454
|
-
if (running)
|
|
1455
|
-
|
|
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);
|
|
1456
1653
|
const started = performance.now();
|
|
1457
1654
|
try {
|
|
1458
|
-
const loaded = await loadConfigFileDetailed(invocation.cwd);
|
|
1655
|
+
const loaded = invocation.noConfig ? { config: null, warnings: [], errors: [], file: null } : await loadConfigFileDetailed(invocation.cwd);
|
|
1459
1656
|
if (loaded.errors.length > 0) {
|
|
1460
1657
|
for (const error of loaded.errors) warn(`\u26A0 Config: ${error}`);
|
|
1461
1658
|
warn("\u274C The config file could not be read. Fix it, or delete it to use the defaults.");
|
|
@@ -1475,7 +1672,8 @@ async function runWatchLoop(invocation, resolved) {
|
|
|
1475
1672
|
maxPartBytes: invocation.maxPartBytes,
|
|
1476
1673
|
largeFileBytes: invocation.largeFileBytes,
|
|
1477
1674
|
maxMonolithBytes: invocation.maxMonolithBytes,
|
|
1478
|
-
skipArtifacts: invocation.skipArtifacts
|
|
1675
|
+
skipArtifacts: invocation.skipArtifacts,
|
|
1676
|
+
discovery: invocation.discovery
|
|
1479
1677
|
},
|
|
1480
1678
|
invocation.cwd
|
|
1481
1679
|
);
|
|
@@ -1487,24 +1685,35 @@ async function runWatchLoop(invocation, resolved) {
|
|
|
1487
1685
|
} catch (err) {
|
|
1488
1686
|
warn(`\u274C ${err.message}`);
|
|
1489
1687
|
} finally {
|
|
1490
|
-
|
|
1491
|
-
|
|
1688
|
+
const finished = finishWatchRun(state, performance.now());
|
|
1689
|
+
state = finished.state;
|
|
1690
|
+
if (finished.schedule) schedule(0);
|
|
1492
1691
|
}
|
|
1493
1692
|
};
|
|
1494
1693
|
info("\u{1F440} Watching for changes. Press Ctrl+C to stop.");
|
|
1495
1694
|
void regenerate();
|
|
1496
|
-
let timer = null;
|
|
1497
1695
|
const watcher = fs.watch(resolved.cwd, { recursive: true }, (_event, filename) => {
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
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;
|
|
1502
1711
|
}
|
|
1503
|
-
if (
|
|
1504
|
-
timer = setTimeout(() => void regenerate(), 250);
|
|
1712
|
+
if (decision.action === "schedule") schedule();
|
|
1505
1713
|
});
|
|
1506
1714
|
await new Promise((resolve2) => {
|
|
1507
1715
|
const stop = () => {
|
|
1716
|
+
stopped = true;
|
|
1508
1717
|
if (timer) clearTimeout(timer);
|
|
1509
1718
|
watcher.close();
|
|
1510
1719
|
resolve2();
|
|
@@ -1530,6 +1739,6 @@ if (invokedDirectly) {
|
|
|
1530
1739
|
);
|
|
1531
1740
|
}
|
|
1532
1741
|
|
|
1533
|
-
export { UsageError, main, parseByteOption, parseCliArgs, parsePositiveInteger };
|
|
1742
|
+
export { UsageError, WATCH_DEBOUNCE_MS, WATCH_SETTLE_MS, beginWatchRun, decideWatchEvent, finishWatchRun, initialWatchState, isGeneratedWatchPath, main, parseByteOption, parseCliArgs, parsePositiveInteger };
|
|
1534
1743
|
//# sourceMappingURL=cli.js.map
|
|
1535
1744
|
//# sourceMappingURL=cli.js.map
|