@ankurhalder/codebase-snapshot 2.1.0 → 2.2.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 +36 -0
- package/dist/bin.js +809 -49
- package/dist/{chunk-QEIOXPMU.js → chunk-5I564BGX.js} +43 -19
- package/dist/cli.cjs +791 -35
- package/dist/cli.d.cts +5 -1
- package/dist/cli.d.ts +5 -1
- package/dist/cli.js +750 -17
- package/dist/index.cjs +41 -17
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/docs/agent.md +80 -0
- package/docs/install.md +15 -1
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import * as
|
|
5
|
-
import * as
|
|
4
|
+
import * as fs11 from "fs";
|
|
5
|
+
import * as path12 from "path";
|
|
6
6
|
import { parseArgs } from "util";
|
|
7
7
|
|
|
8
8
|
// src/diff.ts
|
|
@@ -52,7 +52,13 @@ function compareManifests(before, after) {
|
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
54
|
function loadManifest(target) {
|
|
55
|
-
|
|
55
|
+
let isDirectory;
|
|
56
|
+
try {
|
|
57
|
+
isDirectory = fs.statSync(target).isDirectory();
|
|
58
|
+
} catch {
|
|
59
|
+
throw new Error(`No snapshot found at ${target}: the path does not exist.`);
|
|
60
|
+
}
|
|
61
|
+
const manifestPath = isDirectory ? path.join(target, "manifest.json") : target;
|
|
56
62
|
let raw;
|
|
57
63
|
try {
|
|
58
64
|
raw = fs.readFileSync(manifestPath, "utf8");
|
|
@@ -68,6 +74,9 @@ function loadManifest(target) {
|
|
|
68
74
|
if (!parsed || !Array.isArray(parsed.files)) {
|
|
69
75
|
throw new Error(`${manifestPath} is not a snapshot manifest (no "files" array).`);
|
|
70
76
|
}
|
|
77
|
+
if (!parsed.counts || typeof parsed.counts !== "object") {
|
|
78
|
+
throw new Error(`${manifestPath} is not a snapshot manifest (no "counts" object).`);
|
|
79
|
+
}
|
|
71
80
|
return parsed;
|
|
72
81
|
}
|
|
73
82
|
function renderDiffText(diff) {
|
|
@@ -948,11 +957,12 @@ import * as fs3 from "fs";
|
|
|
948
957
|
import * as path3 from "path";
|
|
949
958
|
function getGitInfo(cwd, fallbackBranch = "main") {
|
|
950
959
|
try {
|
|
951
|
-
const
|
|
960
|
+
const reported = childProcess.execSync("git rev-parse --abbrev-ref HEAD", {
|
|
952
961
|
cwd,
|
|
953
962
|
stdio: ["ignore", "pipe", "ignore"],
|
|
954
963
|
encoding: "utf8"
|
|
955
|
-
}).trim()
|
|
964
|
+
}).trim();
|
|
965
|
+
const branch = !reported || reported === "HEAD" ? fallbackBranch : reported;
|
|
956
966
|
const commit = childProcess.execSync("git rev-parse HEAD", {
|
|
957
967
|
cwd,
|
|
958
968
|
stdio: ["ignore", "pipe", "ignore"],
|
|
@@ -1573,6 +1583,9 @@ async function loadConfigFileDetailed(cwd) {
|
|
|
1573
1583
|
if (snapshotKey && typeof snapshotKey === "object") {
|
|
1574
1584
|
return { config: snapshotKey, file: pkgJsonPath, warnings, errors };
|
|
1575
1585
|
}
|
|
1586
|
+
if (snapshotKey !== void 0) {
|
|
1587
|
+
warnings.push(`${pkgJsonPath}: the "snapshot" key must be an object; it was ignored.`);
|
|
1588
|
+
}
|
|
1576
1589
|
} catch (err) {
|
|
1577
1590
|
const message = err.message;
|
|
1578
1591
|
warnings.push(message);
|
|
@@ -2094,11 +2107,12 @@ var PRESET_NAMES = [
|
|
|
2094
2107
|
"java-spring \xB7 dotnet \xB7 php-laravel \xB7 ruby-rails",
|
|
2095
2108
|
"monorepo \xB7 docs \xB7 infra \xB7 generic"
|
|
2096
2109
|
];
|
|
2110
|
+
var CONFIG_HEADER = `// Generated by codebase-snapshot. A plain object on purpose: this file must load
|
|
2111
|
+
// even when the package itself is only run through npx.
|
|
2112
|
+
`;
|
|
2097
2113
|
function renderConfig(answers) {
|
|
2098
2114
|
const { artifacts } = answers;
|
|
2099
|
-
return
|
|
2100
|
-
|
|
2101
|
-
export default defineConfig({
|
|
2115
|
+
return `${CONFIG_HEADER}export default {
|
|
2102
2116
|
// \u2500\u2500 Project \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2103
2117
|
// Which project shape to use. Detected from the repo when omitted.
|
|
2104
2118
|
// ${PRESET_NAMES.join("\n // ")}
|
|
@@ -2167,7 +2181,7 @@ export default defineConfig({
|
|
|
2167
2181
|
// \u2500\u2500 Advanced \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2168
2182
|
// afterGenerate: (manifest) => { /* write your own artifact */ }
|
|
2169
2183
|
hooks: {},
|
|
2170
|
-
}
|
|
2184
|
+
};
|
|
2171
2185
|
`;
|
|
2172
2186
|
}
|
|
2173
2187
|
function renderMinimalConfig(answers) {
|
|
@@ -2195,16 +2209,12 @@ ${entries},
|
|
|
2195
2209
|
},`);
|
|
2196
2210
|
}
|
|
2197
2211
|
if (lines.length === 0) {
|
|
2198
|
-
return
|
|
2199
|
-
|
|
2200
|
-
export default defineConfig({});
|
|
2212
|
+
return `${CONFIG_HEADER}export default {};
|
|
2201
2213
|
`;
|
|
2202
2214
|
}
|
|
2203
|
-
return
|
|
2204
|
-
|
|
2205
|
-
export default defineConfig({
|
|
2215
|
+
return `${CONFIG_HEADER}export default {
|
|
2206
2216
|
${lines.join("\n")}
|
|
2207
|
-
}
|
|
2217
|
+
};
|
|
2208
2218
|
`;
|
|
2209
2219
|
}
|
|
2210
2220
|
|
|
@@ -2468,6 +2478,17 @@ var CONFIG_CANDIDATES = [
|
|
|
2468
2478
|
".snapshotrc.json",
|
|
2469
2479
|
".snapshotrc"
|
|
2470
2480
|
];
|
|
2481
|
+
function chooseConfigFileName(cwd) {
|
|
2482
|
+
if (!supportsTypeScriptImports()) return "snapshot.config.mjs";
|
|
2483
|
+
const pkgPath = path7.join(cwd, "package.json");
|
|
2484
|
+
if (!fs6.existsSync(pkgPath)) return "snapshot.config.mjs";
|
|
2485
|
+
try {
|
|
2486
|
+
const parsed = JSON.parse(fs6.readFileSync(pkgPath, "utf8"));
|
|
2487
|
+
return parsed.type === "module" ? "snapshot.config.ts" : "snapshot.config.mjs";
|
|
2488
|
+
} catch {
|
|
2489
|
+
return "snapshot.config.mjs";
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2471
2492
|
var SIZE_CHOICES = [
|
|
2472
2493
|
{ value: "60k", label: "Small", hint: "60 KB \xB7 ~15k tokens \xB7 tight context windows" },
|
|
2473
2494
|
{ value: "120k", label: "Medium", hint: "120 KB \xB7 ~30k tokens \xB7 recommended" },
|
|
@@ -2673,7 +2694,7 @@ async function runInit(options) {
|
|
|
2673
2694
|
if (!proceed) throw new PromptCancelled();
|
|
2674
2695
|
}
|
|
2675
2696
|
const configText = options.minimal ? renderMinimalConfig(answers) : renderConfig(answers);
|
|
2676
|
-
const configName =
|
|
2697
|
+
const configName = chooseConfigFileName(options.cwd);
|
|
2677
2698
|
const configPath = path7.join(options.cwd, configName);
|
|
2678
2699
|
if (options.dryRun) {
|
|
2679
2700
|
return {
|
|
@@ -2936,9 +2957,9 @@ var JAVA_METHOD = /^\s+(?:public|private|protected)\s+(?:static\s+)?(?:final\s+)
|
|
|
2936
2957
|
var jvmProfile = {
|
|
2937
2958
|
id: "jvm",
|
|
2938
2959
|
extensions: [".java", ".kt", ".kts", ".scala"],
|
|
2939
|
-
summarize(
|
|
2960
|
+
summarize(path13, content) {
|
|
2940
2961
|
const summary = emptySummary();
|
|
2941
|
-
const lower =
|
|
2962
|
+
const lower = path13.toLowerCase();
|
|
2942
2963
|
const isJava = lower.endsWith(".java");
|
|
2943
2964
|
const isKotlin = lower.endsWith(".kt") || lower.endsWith(".kts");
|
|
2944
2965
|
eachLine(content, (line) => {
|
|
@@ -3362,16 +3383,16 @@ function summarizeTypeScriptSource(relativePath, content) {
|
|
|
3362
3383
|
// src/ast/profiles/sfc.ts
|
|
3363
3384
|
var SCRIPT_BLOCK = /<script[^>]*>([\s\S]*?)<\/script>/gi;
|
|
3364
3385
|
var ASTRO_FRONTMATTER = /^\uFEFF?---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?=\r?\n|$)/;
|
|
3365
|
-
function componentName(
|
|
3366
|
-
const base =
|
|
3386
|
+
function componentName(path13) {
|
|
3387
|
+
const base = path13.split("/").pop() ?? path13;
|
|
3367
3388
|
return base.replace(/\.[^.]+$/, "");
|
|
3368
3389
|
}
|
|
3369
3390
|
var sfcProfile = {
|
|
3370
3391
|
id: "sfc",
|
|
3371
3392
|
extensions: [".vue", ".svelte", ".astro"],
|
|
3372
|
-
summarize(
|
|
3393
|
+
summarize(path13, content) {
|
|
3373
3394
|
let script = "";
|
|
3374
|
-
if (
|
|
3395
|
+
if (path13.toLowerCase().endsWith(".astro")) {
|
|
3375
3396
|
const frontmatter = ASTRO_FRONTMATTER.exec(content);
|
|
3376
3397
|
if (frontmatter) script += `${frontmatter[1]}
|
|
3377
3398
|
`;
|
|
@@ -3380,7 +3401,7 @@ var sfcProfile = {
|
|
|
3380
3401
|
script += `${match[1]}
|
|
3381
3402
|
`;
|
|
3382
3403
|
}
|
|
3383
|
-
const name = componentName(
|
|
3404
|
+
const name = componentName(path13);
|
|
3384
3405
|
if (!script.trim()) {
|
|
3385
3406
|
const summary2 = emptySummary();
|
|
3386
3407
|
if (name) {
|
|
@@ -3414,15 +3435,15 @@ for (const profile of LANGUAGE_PROFILES) {
|
|
|
3414
3435
|
BY_EXTENSION.set(extension.toLowerCase(), profile);
|
|
3415
3436
|
}
|
|
3416
3437
|
}
|
|
3417
|
-
function profileForPath(
|
|
3418
|
-
const lower =
|
|
3438
|
+
function profileForPath(path13) {
|
|
3439
|
+
const lower = path13.toLowerCase();
|
|
3419
3440
|
const dot = lower.lastIndexOf(".");
|
|
3420
3441
|
if (dot === -1) return null;
|
|
3421
3442
|
return BY_EXTENSION.get(lower.slice(dot)) ?? null;
|
|
3422
3443
|
}
|
|
3423
|
-
function summarizeWithProfile(
|
|
3424
|
-
const profile = profileForPath(
|
|
3425
|
-
return profile ? profile.summarize(
|
|
3444
|
+
function summarizeWithProfile(path13, content) {
|
|
3445
|
+
const profile = profileForPath(path13);
|
|
3446
|
+
return profile ? profile.summarize(path13, content) : null;
|
|
3426
3447
|
}
|
|
3427
3448
|
|
|
3428
3449
|
// src/ast/structured.ts
|
|
@@ -4295,6 +4316,7 @@ function validateSnapshot(configInput) {
|
|
|
4295
4316
|
drift.missingOnDisk.push(snapshotFile);
|
|
4296
4317
|
}
|
|
4297
4318
|
}
|
|
4319
|
+
drift.missingOnDisk = [...new Set(drift.missingOnDisk)];
|
|
4298
4320
|
const maxEffectiveBudget = Math.max(
|
|
4299
4321
|
config.maxPartBytes,
|
|
4300
4322
|
config.budgets.default,
|
|
@@ -4554,7 +4576,7 @@ function validateSnapshot(configInput) {
|
|
|
4554
4576
|
|
|
4555
4577
|
// src/index.ts
|
|
4556
4578
|
var GENERATOR_NAME = "@ankurhalder/codebase-snapshot";
|
|
4557
|
-
var GENERATOR_VERSION = "2.
|
|
4579
|
+
var GENERATOR_VERSION = "2.2.0";
|
|
4558
4580
|
var SCHEMA_VERSION = "3.0.0";
|
|
4559
4581
|
function sha256Hex2(input) {
|
|
4560
4582
|
return crypto3.createHash("sha256").update(input).digest("hex");
|
|
@@ -4798,7 +4820,9 @@ function artifactWarnings(artifacts) {
|
|
|
4798
4820
|
);
|
|
4799
4821
|
}
|
|
4800
4822
|
if (ARTIFACT_NAMES.every((name) => artifacts[name] === false)) {
|
|
4801
|
-
messages.push(
|
|
4823
|
+
messages.push(
|
|
4824
|
+
"every artifact is disabled: only the .codebase-snapshot ownership marker will be written."
|
|
4825
|
+
);
|
|
4802
4826
|
}
|
|
4803
4827
|
return messages;
|
|
4804
4828
|
}
|
|
@@ -5125,6 +5149,634 @@ function generateSnapshot(configInput, options) {
|
|
|
5125
5149
|
return manifest;
|
|
5126
5150
|
}
|
|
5127
5151
|
|
|
5152
|
+
// src/agent/contract.ts
|
|
5153
|
+
var CONTRACT_VERSION = "1.0.0";
|
|
5154
|
+
var PACKAGE_NAME = "@ankurhalder/codebase-snapshot";
|
|
5155
|
+
var ARTIFACT_SUMMARY = {
|
|
5156
|
+
readme: "README.md describing the snapshot at the output root.",
|
|
5157
|
+
manifest: "manifest.json: machine-readable index of every file, part, hash and config.",
|
|
5158
|
+
parts: "parts/*.md: the packed, budget-limited markdown parts that carry the code.",
|
|
5159
|
+
navigation: "reports/navigation.md: links between parts and layers.",
|
|
5160
|
+
projectTree: "reports/project-tree.md: the directory tree of included files.",
|
|
5161
|
+
audit: "reports/audit.md: secret findings, large files, parse diagnostics, git state.",
|
|
5162
|
+
monolith: "full-codebase.json and full-codebase.md: the whole repository in one file (off by default)."
|
|
5163
|
+
};
|
|
5164
|
+
var ARTIFACT_DEFAULTS = {
|
|
5165
|
+
readme: true,
|
|
5166
|
+
manifest: true,
|
|
5167
|
+
parts: true,
|
|
5168
|
+
navigation: true,
|
|
5169
|
+
projectTree: true,
|
|
5170
|
+
audit: true,
|
|
5171
|
+
monolith: false
|
|
5172
|
+
};
|
|
5173
|
+
function renderContractText(contract) {
|
|
5174
|
+
const lines = [];
|
|
5175
|
+
lines.push(`${contract.package} v${contract.version}`);
|
|
5176
|
+
lines.push(contract.summary);
|
|
5177
|
+
lines.push("");
|
|
5178
|
+
lines.push(`Node : ${contract.requires.node}`);
|
|
5179
|
+
lines.push(`Schema : ${contract.schemaVersion} (contract ${contract.contractVersion})`);
|
|
5180
|
+
lines.push(`One-shot : ${contract.invocation.oneShot} <command>`);
|
|
5181
|
+
lines.push(`Installed : ${contract.invocation.installed} <command>`);
|
|
5182
|
+
lines.push("");
|
|
5183
|
+
lines.push("Workflow:");
|
|
5184
|
+
for (const step of contract.workflow) {
|
|
5185
|
+
lines.push(` ${step.step}. ${step.id}`);
|
|
5186
|
+
lines.push(` run : ${step.run}`);
|
|
5187
|
+
lines.push(` expect : ${step.expect}`);
|
|
5188
|
+
if (step.notes) lines.push(` note : ${step.notes}`);
|
|
5189
|
+
}
|
|
5190
|
+
lines.push("");
|
|
5191
|
+
lines.push("Commands:");
|
|
5192
|
+
for (const command of contract.commands) {
|
|
5193
|
+
const args = command.positionals.length > 0 ? ` ${command.positionals.join(" ")}` : "";
|
|
5194
|
+
lines.push(` ${(command.name + args).padEnd(26)} ${command.summary}`);
|
|
5195
|
+
}
|
|
5196
|
+
lines.push("");
|
|
5197
|
+
lines.push("Exit codes:");
|
|
5198
|
+
for (const [code, meaning] of Object.entries(contract.exitCodes)) {
|
|
5199
|
+
lines.push(` ${code} ${meaning}`);
|
|
5200
|
+
}
|
|
5201
|
+
lines.push("");
|
|
5202
|
+
lines.push(`Presets (${contract.presets.length}): ${contract.presets.map((p) => p.name).join(", ")}`);
|
|
5203
|
+
lines.push("");
|
|
5204
|
+
lines.push("Artifacts:");
|
|
5205
|
+
for (const artifact of contract.artifacts) {
|
|
5206
|
+
lines.push(` ${artifact.name.padEnd(12)} ${artifact.default ? "on " : "off"} ${artifact.summary}`);
|
|
5207
|
+
}
|
|
5208
|
+
lines.push("");
|
|
5209
|
+
lines.push("Options:");
|
|
5210
|
+
for (const option of contract.options) {
|
|
5211
|
+
const fallback = option.default === void 0 ? "" : ` [${JSON.stringify(option.default)}]`;
|
|
5212
|
+
lines.push(` ${option.flag.padEnd(24)} ${option.appliesTo.padEnd(32)}${fallback}`);
|
|
5213
|
+
lines.push(` ${"".padEnd(24)} ${option.summary}`);
|
|
5214
|
+
}
|
|
5215
|
+
lines.push("");
|
|
5216
|
+
lines.push("Safety:");
|
|
5217
|
+
for (const note of contract.safety) lines.push(` - ${note}`);
|
|
5218
|
+
lines.push("");
|
|
5219
|
+
lines.push("Troubleshooting:");
|
|
5220
|
+
for (const item of contract.troubleshooting) {
|
|
5221
|
+
lines.push(` ${item.symptom}`);
|
|
5222
|
+
lines.push(` cause: ${item.cause}`);
|
|
5223
|
+
lines.push(` fix : ${item.fix}`);
|
|
5224
|
+
}
|
|
5225
|
+
lines.push("");
|
|
5226
|
+
lines.push("This is the human rendering. Agents should use: describe --format json");
|
|
5227
|
+
return lines.join("\n");
|
|
5228
|
+
}
|
|
5229
|
+
function buildAgentContract() {
|
|
5230
|
+
return {
|
|
5231
|
+
generator: GENERATOR_NAME,
|
|
5232
|
+
package: PACKAGE_NAME,
|
|
5233
|
+
version: GENERATOR_VERSION,
|
|
5234
|
+
schemaVersion: SCHEMA_VERSION,
|
|
5235
|
+
contractVersion: CONTRACT_VERSION,
|
|
5236
|
+
summary: "Turns a repository into an LLM-ready snapshot: code packed into size-budgeted markdown parts, plus a manifest, a project tree, navigation and a secret audit. Auto-detects the project shape (framework, language, monorepo, docs, infra).",
|
|
5237
|
+
requires: { node: ">=18.0.0", dependencies: ["typescript"] },
|
|
5238
|
+
invocation: {
|
|
5239
|
+
oneShot: `npx ${PACKAGE_NAME}@latest`,
|
|
5240
|
+
installed: "npx snap",
|
|
5241
|
+
bins: ["snap", "codebase-snapshot"],
|
|
5242
|
+
notes: [
|
|
5243
|
+
`Pin the tag (${PACKAGE_NAME}@latest or @2) instead of the bare package name.`,
|
|
5244
|
+
"A bare scoped spec can resolve to a bin-less placeholder on some npm builds and exit 0 with no output; a pinned spec cannot.",
|
|
5245
|
+
"Human progress goes to stderr. With --format json the payload on stdout is pure JSON."
|
|
5246
|
+
]
|
|
5247
|
+
},
|
|
5248
|
+
workflow: [
|
|
5249
|
+
{
|
|
5250
|
+
step: 1,
|
|
5251
|
+
id: "verify",
|
|
5252
|
+
run: `npx ${PACKAGE_NAME}@latest --version`,
|
|
5253
|
+
expect: "prints v2.x.x on stdout and exits 0",
|
|
5254
|
+
notes: "If it prints nothing, re-run with @latest explicitly, or use `npx -p <pkg> snap`."
|
|
5255
|
+
},
|
|
5256
|
+
{
|
|
5257
|
+
step: 2,
|
|
5258
|
+
id: "understand",
|
|
5259
|
+
run: `npx ${PACKAGE_NAME}@latest describe --format json`,
|
|
5260
|
+
expect: "the full contract as JSON: commands, options, presets, exit codes",
|
|
5261
|
+
notes: "Read this before acting if you have no prior knowledge of the tool."
|
|
5262
|
+
},
|
|
5263
|
+
{
|
|
5264
|
+
step: 3,
|
|
5265
|
+
id: "configure",
|
|
5266
|
+
run: `npx ${PACKAGE_NAME}@latest init --yes`,
|
|
5267
|
+
expect: "writes snapshot.config.ts listing every setting at its current value",
|
|
5268
|
+
notes: "Non-interactive. The detected preset is chosen automatically; override with --preset <name>. Skip this step to run on built-in defaults."
|
|
5269
|
+
},
|
|
5270
|
+
{
|
|
5271
|
+
step: 4,
|
|
5272
|
+
id: "generate",
|
|
5273
|
+
run: `npx ${PACKAGE_NAME}@latest generate`,
|
|
5274
|
+
expect: "exit 0, and ./snapshot/ written with parts, reports and manifest.json"
|
|
5275
|
+
},
|
|
5276
|
+
{
|
|
5277
|
+
step: 5,
|
|
5278
|
+
id: "verify-output",
|
|
5279
|
+
run: `npx ${PACKAGE_NAME}@latest validate`,
|
|
5280
|
+
expect: "exit 0 means the snapshot matches the files on disk",
|
|
5281
|
+
notes: "Exit 1 means drift or a damaged snapshot; re-run generate."
|
|
5282
|
+
},
|
|
5283
|
+
{
|
|
5284
|
+
step: 6,
|
|
5285
|
+
id: "consume",
|
|
5286
|
+
run: "read ./snapshot/parts/*.md, then ./snapshot/manifest.json, then ./snapshot/reports/audit.md",
|
|
5287
|
+
expect: "the code, the index of what was included, and the risk findings",
|
|
5288
|
+
notes: "manifest.json.files[] lists every included file with layer, kind, language, part and summary. manifest.skipped[] explains every file that was left out and why."
|
|
5289
|
+
}
|
|
5290
|
+
],
|
|
5291
|
+
commands: [
|
|
5292
|
+
{
|
|
5293
|
+
name: "generate",
|
|
5294
|
+
aliases: ["default"],
|
|
5295
|
+
summary: "Write the snapshot. This is the command you normally want.",
|
|
5296
|
+
positionals: [],
|
|
5297
|
+
exits: [0, 1]
|
|
5298
|
+
},
|
|
5299
|
+
{
|
|
5300
|
+
name: "validate",
|
|
5301
|
+
aliases: ["check"],
|
|
5302
|
+
summary: "Compare the snapshot on disk with the repository; exit 1 on drift.",
|
|
5303
|
+
positionals: [],
|
|
5304
|
+
exits: [0, 1]
|
|
5305
|
+
},
|
|
5306
|
+
{
|
|
5307
|
+
name: "report",
|
|
5308
|
+
aliases: [],
|
|
5309
|
+
summary: "Print snapshot health, layer metrics and the secret audit.",
|
|
5310
|
+
positionals: [],
|
|
5311
|
+
exits: [0, 1]
|
|
5312
|
+
},
|
|
5313
|
+
{
|
|
5314
|
+
name: "init",
|
|
5315
|
+
aliases: [],
|
|
5316
|
+
summary: "Write a commented snapshot.config.ts (interactive, or --yes for CI).",
|
|
5317
|
+
positionals: [],
|
|
5318
|
+
exits: [0, 1, 2]
|
|
5319
|
+
},
|
|
5320
|
+
{
|
|
5321
|
+
name: "explain",
|
|
5322
|
+
aliases: [],
|
|
5323
|
+
summary: "Explain how one path would be treated: layer, kind, part, skip reason.",
|
|
5324
|
+
positionals: ["<path>"],
|
|
5325
|
+
exits: [0, 1]
|
|
5326
|
+
},
|
|
5327
|
+
{
|
|
5328
|
+
name: "diff",
|
|
5329
|
+
aliases: [],
|
|
5330
|
+
summary: "Compare two snapshots (directories or manifest.json files).",
|
|
5331
|
+
positionals: ["<before>", "<after>"],
|
|
5332
|
+
exits: [0, 1]
|
|
5333
|
+
},
|
|
5334
|
+
{
|
|
5335
|
+
name: "describe",
|
|
5336
|
+
aliases: [],
|
|
5337
|
+
summary: "Print this contract. --format json for machines, text for humans.",
|
|
5338
|
+
positionals: [],
|
|
5339
|
+
exits: [0]
|
|
5340
|
+
},
|
|
5341
|
+
{
|
|
5342
|
+
name: "skill",
|
|
5343
|
+
aliases: [],
|
|
5344
|
+
summary: "Print the agent skill, or --install it into the repository (AGENTS.md, .claude/skills, .cursor/rules).",
|
|
5345
|
+
positionals: [],
|
|
5346
|
+
exits: [0, 1, 2]
|
|
5347
|
+
}
|
|
5348
|
+
],
|
|
5349
|
+
exitCodes: {
|
|
5350
|
+
"0": "success (and validate: no drift)",
|
|
5351
|
+
"1": "drift detected, or generation refused/failed (nothing was written)",
|
|
5352
|
+
"2": "usage error: unknown flag/command, bad value, wrong argument count"
|
|
5353
|
+
},
|
|
5354
|
+
options: [
|
|
5355
|
+
{
|
|
5356
|
+
flag: "--out-dir",
|
|
5357
|
+
type: "string",
|
|
5358
|
+
default: "snapshot",
|
|
5359
|
+
appliesTo: "generate/validate/report/explain",
|
|
5360
|
+
summary: "Where the snapshot is written. Must not be the repo root or a directory the tool did not create."
|
|
5361
|
+
},
|
|
5362
|
+
{
|
|
5363
|
+
flag: "--max-part-bytes",
|
|
5364
|
+
type: "string",
|
|
5365
|
+
default: 307200,
|
|
5366
|
+
appliesTo: "generate/validate/report",
|
|
5367
|
+
summary: 'Byte budget per markdown part. Accepts "120k" / "1.5mb". Minimum 4096.'
|
|
5368
|
+
},
|
|
5369
|
+
{
|
|
5370
|
+
flag: "--large-file-bytes",
|
|
5371
|
+
type: "string",
|
|
5372
|
+
default: 204800,
|
|
5373
|
+
appliesTo: "generate/validate/report",
|
|
5374
|
+
summary: "Above this size a file gets a head/tail preview instead of the full body."
|
|
5375
|
+
},
|
|
5376
|
+
{
|
|
5377
|
+
flag: "--max-monolith-bytes",
|
|
5378
|
+
type: "string",
|
|
5379
|
+
default: "unset",
|
|
5380
|
+
appliesTo: "generate",
|
|
5381
|
+
summary: "Skip the monolith artifacts when they would exceed this size."
|
|
5382
|
+
},
|
|
5383
|
+
{
|
|
5384
|
+
flag: "--include-monolith",
|
|
5385
|
+
type: "boolean",
|
|
5386
|
+
default: false,
|
|
5387
|
+
appliesTo: "generate",
|
|
5388
|
+
summary: "Also write full-codebase.json and full-codebase.md."
|
|
5389
|
+
},
|
|
5390
|
+
{
|
|
5391
|
+
flag: "--include-sensitive-files",
|
|
5392
|
+
type: "boolean",
|
|
5393
|
+
default: false,
|
|
5394
|
+
appliesTo: "generate/validate/report",
|
|
5395
|
+
summary: "Also emit .env / *.pem / .npmrc style files. Off by default; they are recorded as skipped."
|
|
5396
|
+
},
|
|
5397
|
+
{
|
|
5398
|
+
flag: "--skip-artifacts",
|
|
5399
|
+
type: "string",
|
|
5400
|
+
default: "unset",
|
|
5401
|
+
appliesTo: "generate",
|
|
5402
|
+
summary: "Comma-separated artifacts to omit (readme, manifest, parts, navigation, projectTree, audit, monolith)."
|
|
5403
|
+
},
|
|
5404
|
+
{
|
|
5405
|
+
flag: "--only-artifacts",
|
|
5406
|
+
type: "string",
|
|
5407
|
+
default: "unset",
|
|
5408
|
+
appliesTo: "generate",
|
|
5409
|
+
summary: "Write only these artifacts. Cannot be combined with --skip-artifacts."
|
|
5410
|
+
},
|
|
5411
|
+
{
|
|
5412
|
+
flag: "--format",
|
|
5413
|
+
type: "string",
|
|
5414
|
+
default: "text",
|
|
5415
|
+
appliesTo: "all commands",
|
|
5416
|
+
summary: 'Output format: "text" or "json". JSON goes to stdout, progress to stderr.'
|
|
5417
|
+
},
|
|
5418
|
+
{
|
|
5419
|
+
flag: "--cwd",
|
|
5420
|
+
type: "string",
|
|
5421
|
+
default: "process.cwd()",
|
|
5422
|
+
appliesTo: "all commands",
|
|
5423
|
+
summary: "Repository to operate on. Must be an existing directory."
|
|
5424
|
+
},
|
|
5425
|
+
{
|
|
5426
|
+
flag: "--dry-run",
|
|
5427
|
+
type: "boolean",
|
|
5428
|
+
default: false,
|
|
5429
|
+
appliesTo: "generate",
|
|
5430
|
+
summary: "Compute everything and print the result without writing."
|
|
5431
|
+
},
|
|
5432
|
+
{
|
|
5433
|
+
flag: "--watch",
|
|
5434
|
+
type: "boolean",
|
|
5435
|
+
default: false,
|
|
5436
|
+
appliesTo: "generate",
|
|
5437
|
+
summary: "Regenerate on every change until interrupted."
|
|
5438
|
+
},
|
|
5439
|
+
{
|
|
5440
|
+
flag: "--preset",
|
|
5441
|
+
type: "string",
|
|
5442
|
+
default: "detected",
|
|
5443
|
+
appliesTo: "init",
|
|
5444
|
+
summary: "Force a project preset instead of detecting one. Unknown names are a usage error."
|
|
5445
|
+
},
|
|
5446
|
+
{
|
|
5447
|
+
flag: "--include",
|
|
5448
|
+
type: "string",
|
|
5449
|
+
default: "unset",
|
|
5450
|
+
appliesTo: "init",
|
|
5451
|
+
summary: "Comma-separated allow-list of globs to record in the generated config."
|
|
5452
|
+
},
|
|
5453
|
+
{
|
|
5454
|
+
flag: "--exclude",
|
|
5455
|
+
type: "string",
|
|
5456
|
+
default: "unset",
|
|
5457
|
+
appliesTo: "init",
|
|
5458
|
+
summary: "Comma-separated globs to record in the generated config."
|
|
5459
|
+
},
|
|
5460
|
+
{
|
|
5461
|
+
flag: "--yes",
|
|
5462
|
+
type: "boolean",
|
|
5463
|
+
default: false,
|
|
5464
|
+
appliesTo: "init",
|
|
5465
|
+
summary: "Accept every default without prompting. Required in a non-TTY."
|
|
5466
|
+
},
|
|
5467
|
+
{
|
|
5468
|
+
flag: "--minimal",
|
|
5469
|
+
type: "boolean",
|
|
5470
|
+
default: false,
|
|
5471
|
+
appliesTo: "init",
|
|
5472
|
+
summary: "Write only the settings that differ from the defaults."
|
|
5473
|
+
},
|
|
5474
|
+
{
|
|
5475
|
+
flag: "--force",
|
|
5476
|
+
type: "boolean",
|
|
5477
|
+
default: false,
|
|
5478
|
+
appliesTo: "init",
|
|
5479
|
+
summary: "Overwrite an existing config file."
|
|
5480
|
+
},
|
|
5481
|
+
{
|
|
5482
|
+
flag: "--install",
|
|
5483
|
+
type: "boolean",
|
|
5484
|
+
default: false,
|
|
5485
|
+
appliesTo: "skill",
|
|
5486
|
+
summary: "Write the skill into the repository instead of printing it."
|
|
5487
|
+
},
|
|
5488
|
+
{
|
|
5489
|
+
flag: "--target",
|
|
5490
|
+
type: "string",
|
|
5491
|
+
default: "all",
|
|
5492
|
+
appliesTo: "skill",
|
|
5493
|
+
summary: 'Where to install: "claude", "agents", "cursor" or "all".'
|
|
5494
|
+
}
|
|
5495
|
+
],
|
|
5496
|
+
presets: PRESETS.map((preset) => ({
|
|
5497
|
+
name: preset.name,
|
|
5498
|
+
priority: preset.priority,
|
|
5499
|
+
recommendedBudget: preset.recommendedBudget ?? "300k",
|
|
5500
|
+
excludes: preset.exclude ? [...preset.exclude] : []
|
|
5501
|
+
})),
|
|
5502
|
+
artifacts: ARTIFACT_NAMES.map((name) => ({
|
|
5503
|
+
name,
|
|
5504
|
+
default: ARTIFACT_DEFAULTS[name] ?? true,
|
|
5505
|
+
summary: ARTIFACT_SUMMARY[name] ?? name
|
|
5506
|
+
})),
|
|
5507
|
+
config: {
|
|
5508
|
+
fileNames: [
|
|
5509
|
+
"snapshot.config.ts",
|
|
5510
|
+
"snapshot.config.js",
|
|
5511
|
+
"snapshot.config.mjs",
|
|
5512
|
+
"snapshot.config.cjs",
|
|
5513
|
+
"snapshot.config.json",
|
|
5514
|
+
".snapshotrc",
|
|
5515
|
+
".snapshotrc.json",
|
|
5516
|
+
'package.json \u2192 "snapshot" key'
|
|
5517
|
+
],
|
|
5518
|
+
settings: [
|
|
5519
|
+
{ key: "outDir", type: "string", default: "snapshot", summary: "Snapshot output directory." },
|
|
5520
|
+
{ key: "tmpDir", type: "string", default: "snapshot.tmp", summary: "Scratch directory used for the atomic swap." },
|
|
5521
|
+
{ key: "branch", type: "string", default: "main", summary: "Branch recorded when git cannot report one." },
|
|
5522
|
+
{ key: "preset", type: "string", default: "detected", summary: "Force a preset. Unknown names are an error." },
|
|
5523
|
+
{ key: "maxPartBytes", type: "number | string", default: 307200, summary: "Per-part byte budget." },
|
|
5524
|
+
{ key: "largeFileBytes", type: "number | string", default: 204800, summary: "Preview threshold for large files." },
|
|
5525
|
+
{ key: "previewHeadLines", type: "number", default: 80, summary: "Lines kept from the head of a large file." },
|
|
5526
|
+
{ key: "previewTailLines", type: "number", default: 40, summary: "Lines kept from the tail of a large file." },
|
|
5527
|
+
{ key: "includeMonolith", type: "boolean", default: false, summary: "Also write the single-file monolith." },
|
|
5528
|
+
{ key: "maxMonolithBytes", type: "number | string | null", default: null, summary: "Skip the monolith above this size." },
|
|
5529
|
+
{ key: "includeSensitiveFiles", type: "boolean", default: false, summary: "Emit .env / *.pem / credential files." },
|
|
5530
|
+
{ key: "include", type: "string[]", default: "[]", summary: "Allow-list of globs; empty means everything." },
|
|
5531
|
+
{ key: "excludePatterns", type: "string[]", default: "[]", summary: "Extra exclude globs." },
|
|
5532
|
+
{ key: "artifacts", type: "object", default: "all on except monolith", summary: "Per-artifact opt-out." },
|
|
5533
|
+
{ key: "budgets", type: "object", default: "{ default, byLayer }", summary: "Per-layer byte budgets." },
|
|
5534
|
+
{ key: "layersMode", type: '"merge" | "replace"', default: "merge", summary: "Whether custom layers extend or replace the preset's." },
|
|
5535
|
+
{ key: "layers", type: "array", default: "preset layers", summary: "Custom layer rules (glob or RegExp)." },
|
|
5536
|
+
{ key: "kinds", type: "array", default: "preset kinds", summary: "Custom kind rules." },
|
|
5537
|
+
{ key: "secrets", type: "object", default: "{}", summary: "Custom secret patterns, or disable the defaults." },
|
|
5538
|
+
{ key: "hooks", type: "object", default: "{}", summary: "afterGenerate(manifest) runs after a successful generate." }
|
|
5539
|
+
]
|
|
5540
|
+
},
|
|
5541
|
+
output: {
|
|
5542
|
+
layout: [
|
|
5543
|
+
"<outDir>/README.md",
|
|
5544
|
+
"<outDir>/manifest.json",
|
|
5545
|
+
"<outDir>/parts/part-NNN.md",
|
|
5546
|
+
"<outDir>/reports/project-tree.md",
|
|
5547
|
+
"<outDir>/reports/navigation.md",
|
|
5548
|
+
"<outDir>/reports/audit.md",
|
|
5549
|
+
"<outDir>/.codebase-snapshot (ownership marker, always written)"
|
|
5550
|
+
],
|
|
5551
|
+
notes: [
|
|
5552
|
+
"Parts are ordered and budget-limited; a single file larger than the budget becomes a head/tail preview.",
|
|
5553
|
+
"manifest.json.files[] carries relativePath, layer, kind, language, part, inclusionMode, hash and a structural summary.",
|
|
5554
|
+
"manifest.skipped[] carries the path and the exact reason it was excluded.",
|
|
5555
|
+
`The output directory is replaced atomically; ${MIN_MAX_PART_BYTES} bytes is the smallest legal budget.`
|
|
5556
|
+
]
|
|
5557
|
+
},
|
|
5558
|
+
safety: [
|
|
5559
|
+
"Sensitive files (.env, *.pem, .npmrc, .ssh/, .tfstate) are excluded by default and listed in manifest.skipped[].",
|
|
5560
|
+
"Secret-shaped strings in ordinary source files are redacted in the emitted parts.",
|
|
5561
|
+
"The tool refuses to replace an outDir that it did not create, so a mistyped outDir cannot delete source code.",
|
|
5562
|
+
"Nothing is ever written above the repository root; --out-dir may not be the root or an ancestor."
|
|
5563
|
+
],
|
|
5564
|
+
troubleshooting: [
|
|
5565
|
+
{
|
|
5566
|
+
symptom: "Command prints nothing and exits 0",
|
|
5567
|
+
cause: "A bare scoped spec resolved to a bin-less placeholder manifest on this npm build.",
|
|
5568
|
+
fix: `Use the pinned form: npx ${PACKAGE_NAME}@latest ...`
|
|
5569
|
+
},
|
|
5570
|
+
{
|
|
5571
|
+
symptom: "Exit 1 with 'Refusing to replace outDir'",
|
|
5572
|
+
cause: "The outDir exists and contains files this tool did not write.",
|
|
5573
|
+
fix: "Choose a new/empty --out-dir, or delete the existing directory yourself."
|
|
5574
|
+
},
|
|
5575
|
+
{
|
|
5576
|
+
symptom: "Exit 1 with 'The config file could not be read'",
|
|
5577
|
+
cause: "snapshot.config.* exists but does not parse.",
|
|
5578
|
+
fix: "Fix the syntax, or delete the config to use the defaults."
|
|
5579
|
+
},
|
|
5580
|
+
{
|
|
5581
|
+
symptom: "Exit 1 from validate",
|
|
5582
|
+
cause: "The snapshot no longer matches the files on disk.",
|
|
5583
|
+
fix: "Re-run generate."
|
|
5584
|
+
},
|
|
5585
|
+
{
|
|
5586
|
+
symptom: "A directory you expected is missing from the snapshot",
|
|
5587
|
+
cause: "It is excluded by the preset, by default ignores, or by your own globs.",
|
|
5588
|
+
fix: "Run `explain <path>`; manifest.skipped[] also records the reason for every file."
|
|
5589
|
+
}
|
|
5590
|
+
]
|
|
5591
|
+
};
|
|
5592
|
+
}
|
|
5593
|
+
|
|
5594
|
+
// src/agent/install-skill.ts
|
|
5595
|
+
import * as fs10 from "fs";
|
|
5596
|
+
import * as path11 from "path";
|
|
5597
|
+
|
|
5598
|
+
// src/agent/skill.ts
|
|
5599
|
+
var PACKAGE_NAME2 = "@ankurhalder/codebase-snapshot";
|
|
5600
|
+
var SKILL_NAME = "codebase-snapshot";
|
|
5601
|
+
var AGENTS_MARKER_START = "<!-- codebase-snapshot:skill:start -->";
|
|
5602
|
+
var AGENTS_MARKER_END = "<!-- codebase-snapshot:skill:end -->";
|
|
5603
|
+
function renderSkill() {
|
|
5604
|
+
return `---
|
|
5605
|
+
name: ${SKILL_NAME}
|
|
5606
|
+
description: Create an LLM-ready snapshot of a repository (size-budgeted markdown parts, a JSON manifest, project tree, navigation and a secret audit) when a whole codebase has to be read, summarized or handed to a model that cannot clone it. Use when asked to snapshot a codebase, prepare repository context, or analyze a project's structure.
|
|
5607
|
+
version: ${GENERATOR_VERSION}
|
|
5608
|
+
---
|
|
5609
|
+
|
|
5610
|
+
# codebase-snapshot
|
|
5611
|
+
|
|
5612
|
+
Turns a repository into an LLM-ready snapshot under \`./snapshot/\`: code packed into
|
|
5613
|
+
size-budgeted markdown parts, plus a manifest that indexes every file, a project tree, a
|
|
5614
|
+
navigation index and a secret audit.
|
|
5615
|
+
|
|
5616
|
+
Use it when you need to understand a whole repository, or produce repository context for
|
|
5617
|
+
another model, and reading files one by one would not fit in context.
|
|
5618
|
+
|
|
5619
|
+
## Before you start
|
|
5620
|
+
|
|
5621
|
+
This tool is a Node CLI. It needs **Node.js 18 or newer**. You do not need to install it
|
|
5622
|
+
globally, and you do not need to clone anything.
|
|
5623
|
+
|
|
5624
|
+
**Always pin the tag** \u2014 \`npx ${PACKAGE_NAME2}@latest <command>\`. A bare scoped spec can
|
|
5625
|
+
resolve to a placeholder manifest with no executable on some npm builds and exit 0
|
|
5626
|
+
printing nothing; the pinned form cannot. If a command exits 0 with no output, that is
|
|
5627
|
+
the cause, not a broken repository.
|
|
5628
|
+
|
|
5629
|
+
## Step by step
|
|
5630
|
+
|
|
5631
|
+
1. **Verify the tool.** Run \`npx ${PACKAGE_NAME2}@latest --version\`. Expect \`v${GENERATOR_VERSION}\` or
|
|
5632
|
+
newer on stdout. Empty output means you hit the bare-spec problem above.
|
|
5633
|
+
|
|
5634
|
+
2. **Read the contract, not the prose.** Run
|
|
5635
|
+
\`npx ${PACKAGE_NAME2}@latest describe --format json\`. It returns the full machine
|
|
5636
|
+
contract: every command, every option with its default, the exit-code meanings, the
|
|
5637
|
+
25 project presets, the artifact list, the config settings and a troubleshooting
|
|
5638
|
+
table. Read it before you make decisions; do not guess option names.
|
|
5639
|
+
|
|
5640
|
+
3. **Look before you leap (optional but cheap).** To see how a specific file or directory
|
|
5641
|
+
would be treated, run \`npx ${PACKAGE_NAME2}@latest explain <path>\`. It reports the
|
|
5642
|
+
layer, kind, language, target part, and the exact reason a path would be skipped.
|
|
5643
|
+
\`--format json\` if you want to parse it.
|
|
5644
|
+
|
|
5645
|
+
4. **Configure (recommended).** Run \`npx ${PACKAGE_NAME2}@latest init --yes\`. It detects
|
|
5646
|
+
the project shape, then writes \`snapshot.config.ts\` listing **every** setting at its
|
|
5647
|
+
current value, commented. Pass \`--preset <name>\` only if detection is wrong; the
|
|
5648
|
+
preset names are in the contract. This step is optional: without a config the tool
|
|
5649
|
+
runs on the same defaults.
|
|
5650
|
+
|
|
5651
|
+
5. **Generate.** Run \`npx ${PACKAGE_NAME2}@latest generate\`. Expect exit 0 and a
|
|
5652
|
+
\`./snapshot/\` directory. If you need a different location use \`--out-dir <path>\`; if
|
|
5653
|
+
you need smaller or larger parts use \`--max-part-bytes 120k\`.
|
|
5654
|
+
|
|
5655
|
+
6. **Verify.** Run \`npx ${PACKAGE_NAME2}@latest validate\`. Exit 0 means the snapshot
|
|
5656
|
+
matches the files on disk. Exit 1 means drift \u2014 re-run step 5.
|
|
5657
|
+
|
|
5658
|
+
7. **Read the output in this order:**
|
|
5659
|
+
- \`snapshot/parts/part-*.md\` \u2014 the code itself, each file tagged with its layer, kind,
|
|
5660
|
+
language and a structural summary (imports, exports, classes, functions).
|
|
5661
|
+
- \`snapshot/manifest.json\` \u2014 the index. \`files[]\` has \`relativePath\`, \`layer\`,
|
|
5662
|
+
\`kind\`, \`language\`, \`part\`, \`inclusionMode\`, \`hashSha256\` and \`summary\`.
|
|
5663
|
+
\`skipped[]\` records every excluded file **and why**. Prefer querying this over
|
|
5664
|
+
re-reading the parts.
|
|
5665
|
+
- \`snapshot/reports/audit.md\` \u2014 secrets found, large files, parse diagnostics, git
|
|
5666
|
+
state. Read this before you quote code back to a user.
|
|
5667
|
+
- \`snapshot/reports/project-tree.md\` and \`navigation.md\` \u2014 structure and cross-links.
|
|
5668
|
+
|
|
5669
|
+
## Project types
|
|
5670
|
+
|
|
5671
|
+
Detection is automatic and covers Next.js, Nuxt, SvelteKit, Remix, Astro, Angular, Solid,
|
|
5672
|
+
Vue+React with Vite, Nest, Express/Fastify/Koa, any Node library, monorepos, Django,
|
|
5673
|
+
FastAPI/Flask, plain Python, Go, Rust, Java/Spring, .NET, PHP/Laravel, Ruby/Rails,
|
|
5674
|
+
Terraform/Docker infra, docs-only repositories, and a generic fallback. It sets sensible
|
|
5675
|
+
layers, kinds, ecosystem ignore patterns and a recommended part budget. For non-TypeScript
|
|
5676
|
+
languages it extracts imports, exports, classes and functions structurally; Vue, Svelte and
|
|
5677
|
+
Astro script blocks go through the real TypeScript compiler. You do not need to configure
|
|
5678
|
+
any of this to get a good first snapshot.
|
|
5679
|
+
|
|
5680
|
+
## Things that will surprise you
|
|
5681
|
+
|
|
5682
|
+
- **Sensitive files are excluded by default** (\`.env\`, \`*.pem\`, \`.npmrc\`, \`.ssh/\`,
|
|
5683
|
+
\`.tfstate\`) and are listed in \`manifest.skipped[]\`. Add \`--include-sensitive-files\` only
|
|
5684
|
+
if the user asks for them explicitly.
|
|
5685
|
+
- **Secret-shaped strings inside normal source files are redacted** in the emitted parts.
|
|
5686
|
+
If the user needs the raw value, say so rather than working around it.
|
|
5687
|
+
- **The output directory is owned by the tool.** It is replaced atomically, and the tool
|
|
5688
|
+
refuses to replace a directory it did not create. Never point \`--out-dir\` at source
|
|
5689
|
+
code; if you see \`Refusing to replace outDir\`, choose a new directory rather than
|
|
5690
|
+
deleting anything.
|
|
5691
|
+
- **Exit 2 is a usage error**, not a failure of the repository. Re-read the contract
|
|
5692
|
+
before retrying.
|
|
5693
|
+
- **\`--format json\` keeps stdout pure.** Progress goes to stderr, so \`stdout\` is safe to
|
|
5694
|
+
pipe into a JSON parser.
|
|
5695
|
+
|
|
5696
|
+
## Making this available to other agents
|
|
5697
|
+
|
|
5698
|
+
Run \`npx ${PACKAGE_NAME2}@latest skill --install\`. It writes this skill into the
|
|
5699
|
+
repository in the places agents look for it by default: an \`AGENTS.md\` section,
|
|
5700
|
+
\`.claude/skills/${SKILL_NAME}/SKILL.md\`, and \`.cursor/rules/${SKILL_NAME}.mdc\`. The command
|
|
5701
|
+
is idempotent and only replaces its own marked section.
|
|
5702
|
+
`;
|
|
5703
|
+
}
|
|
5704
|
+
|
|
5705
|
+
// src/agent/install-skill.ts
|
|
5706
|
+
var SKILL_TARGETS = ["claude", "agents", "cursor", "all"];
|
|
5707
|
+
function targetPath(cwd, target) {
|
|
5708
|
+
switch (target) {
|
|
5709
|
+
case "claude":
|
|
5710
|
+
return path11.join(cwd, ".claude", "skills", SKILL_NAME, "SKILL.md");
|
|
5711
|
+
case "cursor":
|
|
5712
|
+
return path11.join(cwd, ".cursor", "rules", `${SKILL_NAME}.mdc`);
|
|
5713
|
+
case "agents":
|
|
5714
|
+
return path11.join(cwd, "AGENTS.md");
|
|
5715
|
+
}
|
|
5716
|
+
}
|
|
5717
|
+
function writeIfChanged(target, filePath, content) {
|
|
5718
|
+
const existing = fs10.existsSync(filePath) ? fs10.readFileSync(filePath, "utf8") : null;
|
|
5719
|
+
if (existing === content) return { target, path: filePath, action: "unchanged" };
|
|
5720
|
+
fs10.mkdirSync(path11.dirname(filePath), { recursive: true });
|
|
5721
|
+
fs10.writeFileSync(filePath, content, "utf8");
|
|
5722
|
+
return { target, path: filePath, action: existing === null ? "created" : "updated" };
|
|
5723
|
+
}
|
|
5724
|
+
function agentsBlock(skill) {
|
|
5725
|
+
return [
|
|
5726
|
+
AGENTS_MARKER_START,
|
|
5727
|
+
"",
|
|
5728
|
+
`## ${SKILL_NAME} (${"@ankurhalder/codebase-snapshot"})`,
|
|
5729
|
+
"",
|
|
5730
|
+
// Drop the YAML frontmatter: inside AGENTS.md it is noise, and the heading
|
|
5731
|
+
// above already names the skill.
|
|
5732
|
+
skill.replace(/^---[\s\S]*?---\r?\n/, "").trim(),
|
|
5733
|
+
"",
|
|
5734
|
+
AGENTS_MARKER_END,
|
|
5735
|
+
""
|
|
5736
|
+
].join("\n");
|
|
5737
|
+
}
|
|
5738
|
+
function mergeAgentsFile(cwd, skill) {
|
|
5739
|
+
const filePath = targetPath(cwd, "agents");
|
|
5740
|
+
const block = agentsBlock(skill);
|
|
5741
|
+
const existing = fs10.existsSync(filePath) ? fs10.readFileSync(filePath, "utf8") : null;
|
|
5742
|
+
let next;
|
|
5743
|
+
if (existing === null) {
|
|
5744
|
+
next = `# AGENTS.md
|
|
5745
|
+
|
|
5746
|
+
${block}`;
|
|
5747
|
+
} else if (existing.includes(AGENTS_MARKER_START) && existing.includes(AGENTS_MARKER_END)) {
|
|
5748
|
+
const start = existing.indexOf(AGENTS_MARKER_START);
|
|
5749
|
+
const end = existing.indexOf(AGENTS_MARKER_END) + AGENTS_MARKER_END.length;
|
|
5750
|
+
next = `${existing.slice(0, start)}${block.trimEnd()}${existing.slice(end)}`;
|
|
5751
|
+
} else {
|
|
5752
|
+
next = `${existing.trimEnd()}
|
|
5753
|
+
|
|
5754
|
+
${block}`;
|
|
5755
|
+
}
|
|
5756
|
+
const action = existing === null ? "created" : existing === next ? "unchanged" : "updated";
|
|
5757
|
+
if (action !== "unchanged") {
|
|
5758
|
+
fs10.mkdirSync(path11.dirname(filePath), { recursive: true });
|
|
5759
|
+
fs10.writeFileSync(filePath, next, "utf8");
|
|
5760
|
+
}
|
|
5761
|
+
return { target: "agents", path: filePath, action };
|
|
5762
|
+
}
|
|
5763
|
+
function installSkill(cwd, target = "all") {
|
|
5764
|
+
const skill = renderSkill();
|
|
5765
|
+
const written = [];
|
|
5766
|
+
const wanted = target === "all" ? ["agents", "claude", "cursor"] : [target];
|
|
5767
|
+
for (const item of wanted) {
|
|
5768
|
+
if (item === "agents") {
|
|
5769
|
+
written.push(mergeAgentsFile(cwd, skill));
|
|
5770
|
+
continue;
|
|
5771
|
+
}
|
|
5772
|
+
written.push(writeIfChanged(item, targetPath(cwd, item), skill));
|
|
5773
|
+
}
|
|
5774
|
+
return { cwd, written };
|
|
5775
|
+
}
|
|
5776
|
+
function isSkillTarget(value) {
|
|
5777
|
+
return SKILL_TARGETS.includes(value);
|
|
5778
|
+
}
|
|
5779
|
+
|
|
5128
5780
|
// src/cli.ts
|
|
5129
5781
|
var VERSION = GENERATOR_VERSION;
|
|
5130
5782
|
var COMMAND_ALIASES = /* @__PURE__ */ new Map([
|
|
@@ -5134,7 +5786,9 @@ var COMMAND_ALIASES = /* @__PURE__ */ new Map([
|
|
|
5134
5786
|
["report", "report"],
|
|
5135
5787
|
["init", "init"],
|
|
5136
5788
|
["explain", "explain"],
|
|
5137
|
-
["diff", "diff"]
|
|
5789
|
+
["diff", "diff"],
|
|
5790
|
+
["describe", "describe"],
|
|
5791
|
+
["skill", "skill"]
|
|
5138
5792
|
]);
|
|
5139
5793
|
var COMMAND_ARITY = {
|
|
5140
5794
|
generate: 0,
|
|
@@ -5142,7 +5796,9 @@ var COMMAND_ARITY = {
|
|
|
5142
5796
|
report: 0,
|
|
5143
5797
|
init: 0,
|
|
5144
5798
|
explain: 1,
|
|
5145
|
-
diff: 2
|
|
5799
|
+
diff: 2,
|
|
5800
|
+
describe: 0,
|
|
5801
|
+
skill: 0
|
|
5146
5802
|
};
|
|
5147
5803
|
var CLI_OPTIONS = {
|
|
5148
5804
|
cwd: { type: "string" },
|
|
@@ -5164,6 +5820,8 @@ var CLI_OPTIONS = {
|
|
|
5164
5820
|
yes: { type: "boolean" },
|
|
5165
5821
|
minimal: { type: "boolean" },
|
|
5166
5822
|
force: { type: "boolean" },
|
|
5823
|
+
install: { type: "boolean" },
|
|
5824
|
+
target: { type: "string" },
|
|
5167
5825
|
help: { type: "boolean", short: "h" },
|
|
5168
5826
|
version: { type: "boolean", short: "v" }
|
|
5169
5827
|
};
|
|
@@ -5184,6 +5842,8 @@ Commands:
|
|
|
5184
5842
|
report Print codebase health, layer metrics, and secret audit
|
|
5185
5843
|
explain <path> Show how one path would be treated: layer, kind, part, skips
|
|
5186
5844
|
diff <a> <b> Compare two snapshots (directories or manifest.json files)
|
|
5845
|
+
describe Print the machine-readable contract (--format json for agents)
|
|
5846
|
+
skill Print the bundled agent skill; --install writes it into the repo
|
|
5187
5847
|
|
|
5188
5848
|
Options:
|
|
5189
5849
|
--cwd <path> Target repository directory (default: current directory)
|
|
@@ -5209,11 +5869,17 @@ Options:
|
|
|
5209
5869
|
--minimal Write only the settings that differ from defaults
|
|
5210
5870
|
--yes Accept every default without prompting
|
|
5211
5871
|
--force Overwrite an existing config
|
|
5872
|
+
|
|
5873
|
+
skill only:
|
|
5874
|
+
--install Write the skill into the repository instead of printing
|
|
5875
|
+
--target <name> claude | agents | cursor | all (default: all)
|
|
5876
|
+
|
|
5212
5877
|
-h, --help Display this help message
|
|
5213
5878
|
-v, --version Display version
|
|
5214
5879
|
|
|
5215
5880
|
All options also accept the --option=value form.
|
|
5216
5881
|
Human-readable progress is written to stderr; payloads (--format json) go to stdout.
|
|
5882
|
+
Agents with no prior knowledge of this tool should start with: describe --format json
|
|
5217
5883
|
`.trim();
|
|
5218
5884
|
function parseByteOption(raw, optionName, minimum) {
|
|
5219
5885
|
if (raw === void 0) return void 0;
|
|
@@ -5253,9 +5919,32 @@ function parseCliArgs(argv) {
|
|
|
5253
5919
|
strict: true
|
|
5254
5920
|
});
|
|
5255
5921
|
} catch (err) {
|
|
5256
|
-
|
|
5922
|
+
const message = err.message;
|
|
5923
|
+
const ambiguous = /^Option '(--[^']+)' argument is ambiguous/i.exec(message);
|
|
5924
|
+
if (ambiguous) {
|
|
5925
|
+
throw new UsageError(
|
|
5926
|
+
`${ambiguous[1]} received a value that looks like another flag (for example a negative number). Use the "--option=value" form, and note that sizes must be positive.`
|
|
5927
|
+
);
|
|
5928
|
+
}
|
|
5929
|
+
throw new UsageError(message);
|
|
5257
5930
|
}
|
|
5258
5931
|
const values = parsed.values;
|
|
5932
|
+
for (const [name, spec] of Object.entries(CLI_OPTIONS)) {
|
|
5933
|
+
if (spec.type !== "string") continue;
|
|
5934
|
+
const raw = values[name];
|
|
5935
|
+
if (raw !== void 0 && String(raw).trim() === "") {
|
|
5936
|
+
throw new UsageError(`--${name} requires a value (received an empty string).`);
|
|
5937
|
+
}
|
|
5938
|
+
}
|
|
5939
|
+
if (values.cwd !== void 0) {
|
|
5940
|
+
const target = String(values.cwd);
|
|
5941
|
+
if (!fs11.existsSync(target)) {
|
|
5942
|
+
throw new UsageError(`--cwd directory does not exist: ${target}`);
|
|
5943
|
+
}
|
|
5944
|
+
if (!fs11.statSync(target).isDirectory()) {
|
|
5945
|
+
throw new UsageError(`--cwd is not a directory: ${target}`);
|
|
5946
|
+
}
|
|
5947
|
+
}
|
|
5259
5948
|
if (values.help) {
|
|
5260
5949
|
process.stdout.write(`${USAGE}
|
|
5261
5950
|
`);
|
|
@@ -5339,6 +6028,14 @@ function parseCliArgs(argv) {
|
|
|
5339
6028
|
const requested = String(values.preset);
|
|
5340
6029
|
if (!getPreset(requested)) throw new UsageError(unknownPresetMessage(requested));
|
|
5341
6030
|
}
|
|
6031
|
+
if (command !== "skill" && (values.install !== void 0 || values.target !== void 0)) {
|
|
6032
|
+
throw new UsageError("--install and --target are only valid with `skill`.");
|
|
6033
|
+
}
|
|
6034
|
+
if (values.target !== void 0 && !isSkillTarget(String(values.target))) {
|
|
6035
|
+
throw new UsageError(
|
|
6036
|
+
`--target must be one of: ${SKILL_TARGETS.join(", ")} (received "${String(values.target)}").`
|
|
6037
|
+
);
|
|
6038
|
+
}
|
|
5342
6039
|
return {
|
|
5343
6040
|
command,
|
|
5344
6041
|
args,
|
|
@@ -5352,7 +6049,9 @@ function parseCliArgs(argv) {
|
|
|
5352
6049
|
yes: values.yes === true,
|
|
5353
6050
|
minimal: values.minimal === true,
|
|
5354
6051
|
force: values.force === true,
|
|
5355
|
-
|
|
6052
|
+
install: values.install === true,
|
|
6053
|
+
target: values.target === void 0 ? void 0 : String(values.target),
|
|
6054
|
+
cwd: values.cwd === void 0 ? process.cwd() : path12.resolve(String(values.cwd)),
|
|
5356
6055
|
outDir: values["out-dir"] === void 0 ? void 0 : String(values["out-dir"]),
|
|
5357
6056
|
includeMonolith: values["include-monolith"] === true ? true : void 0,
|
|
5358
6057
|
includeSensitiveFiles: values["include-sensitive-files"] === true ? true : void 0,
|
|
@@ -5434,7 +6133,7 @@ async function runInitCommand(invocation) {
|
|
|
5434
6133
|
info(result.configText);
|
|
5435
6134
|
return 0;
|
|
5436
6135
|
}
|
|
5437
|
-
info(`\u2705 Created ${
|
|
6136
|
+
info(`\u2705 Created ${path12.basename(result.configPath)} (preset: ${result.preset})`);
|
|
5438
6137
|
if (result.scripts.length > 0) info(` Added npm scripts: ${result.scripts.join(", ")}`);
|
|
5439
6138
|
if (result.gitignoreEntries.length > 0) {
|
|
5440
6139
|
info(` Added to .gitignore: ${result.gitignoreEntries.join(", ")}`);
|
|
@@ -5442,6 +6141,9 @@ async function runInitCommand(invocation) {
|
|
|
5442
6141
|
for (const note of result.notes) info(` \u26A0 ${note}`);
|
|
5443
6142
|
info("");
|
|
5444
6143
|
info(" Next: npm run snapshot");
|
|
6144
|
+
info(
|
|
6145
|
+
` Tip: \`${GENERATOR_NAME} skill --install\` makes this repo discoverable by AI agents.`
|
|
6146
|
+
);
|
|
5445
6147
|
return 0;
|
|
5446
6148
|
} catch (err) {
|
|
5447
6149
|
if (err instanceof PromptCancelled) {
|
|
@@ -5452,6 +6154,31 @@ async function runInitCommand(invocation) {
|
|
|
5452
6154
|
return 1;
|
|
5453
6155
|
}
|
|
5454
6156
|
}
|
|
6157
|
+
function runSkillCommand(invocation) {
|
|
6158
|
+
if (!invocation.install) {
|
|
6159
|
+
process.stdout.write(renderSkill());
|
|
6160
|
+
return 0;
|
|
6161
|
+
}
|
|
6162
|
+
try {
|
|
6163
|
+
const result = installSkill(invocation.cwd, invocation.target);
|
|
6164
|
+
if (invocation.format === "json") {
|
|
6165
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
6166
|
+
`);
|
|
6167
|
+
return 0;
|
|
6168
|
+
}
|
|
6169
|
+
info(`\u2705 Agent skill installed (${SKILL_NAME}):`);
|
|
6170
|
+
for (const item of result.written) {
|
|
6171
|
+
const shown = path12.relative(invocation.cwd, item.path) || item.path;
|
|
6172
|
+
info(` - ${item.action.padEnd(9)} ${shown}`);
|
|
6173
|
+
}
|
|
6174
|
+
info("");
|
|
6175
|
+
info(" Agents that read AGENTS.md, .claude/skills or .cursor/rules will pick this up automatically.");
|
|
6176
|
+
return 0;
|
|
6177
|
+
} catch (err) {
|
|
6178
|
+
warn(`\u274C Could not install the skill: ${err.message}`);
|
|
6179
|
+
return 1;
|
|
6180
|
+
}
|
|
6181
|
+
}
|
|
5455
6182
|
function renderValidateText(result) {
|
|
5456
6183
|
if (result.valid) {
|
|
5457
6184
|
info("\u2705 Snapshot is valid and up to date with disk.");
|
|
@@ -5473,6 +6200,9 @@ function renderValidateText(result) {
|
|
|
5473
6200
|
for (const missing of result.drift.missingOnDisk.slice(0, 20)) {
|
|
5474
6201
|
warn(` - deleted: ${missing}`);
|
|
5475
6202
|
}
|
|
6203
|
+
for (const mismatch of result.drift.totalsMismatches.slice(0, 20)) {
|
|
6204
|
+
warn(` - counts: ${mismatch}`);
|
|
6205
|
+
}
|
|
5476
6206
|
}
|
|
5477
6207
|
for (const warning of result.warnings) warn(` \u26A0 ${warning}`);
|
|
5478
6208
|
}
|
|
@@ -5494,13 +6224,21 @@ ${USAGE}
|
|
|
5494
6224
|
if (invocation.command === "init") {
|
|
5495
6225
|
return await runInitCommand(invocation);
|
|
5496
6226
|
}
|
|
6227
|
+
if (invocation.command === "describe") {
|
|
6228
|
+
const contract = buildAgentContract();
|
|
6229
|
+
emit(invocation.format, contract, () => info(renderContractText(contract)));
|
|
6230
|
+
return 0;
|
|
6231
|
+
}
|
|
6232
|
+
if (invocation.command === "skill") {
|
|
6233
|
+
return runSkillCommand(invocation);
|
|
6234
|
+
}
|
|
5497
6235
|
const loaded = await loadConfigFileDetailed(invocation.cwd);
|
|
5498
6236
|
for (const warning of loaded.warnings) warn(`\u26A0 Config: ${warning}`);
|
|
5499
6237
|
if (loaded.errors.length > 0) {
|
|
5500
6238
|
warn("\u274C The config file could not be read. Fix it, or delete it to use the defaults.");
|
|
5501
6239
|
return 1;
|
|
5502
6240
|
}
|
|
5503
|
-
if (loaded.file) info(`\u2699 Using config ${
|
|
6241
|
+
if (loaded.file) info(`\u2699 Using config ${path12.relative(process.cwd(), loaded.file) || loaded.file}`);
|
|
5504
6242
|
const resolved = resolveConfig(
|
|
5505
6243
|
loaded.config,
|
|
5506
6244
|
{
|
|
@@ -5524,14 +6262,14 @@ ${USAGE}
|
|
|
5524
6262
|
return result.valid ? 0 : 1;
|
|
5525
6263
|
}
|
|
5526
6264
|
if (invocation.command === "report") {
|
|
5527
|
-
const manifestPath =
|
|
5528
|
-
if (!
|
|
6265
|
+
const manifestPath = path12.resolve(resolved.cwd, resolved.outDir, "manifest.json");
|
|
6266
|
+
if (!fs11.existsSync(manifestPath)) {
|
|
5529
6267
|
warn(`\u274C Manifest not found at ${manifestPath}. Run generate first.`);
|
|
5530
6268
|
return 1;
|
|
5531
6269
|
}
|
|
5532
6270
|
let manifest;
|
|
5533
6271
|
try {
|
|
5534
|
-
manifest = JSON.parse(
|
|
6272
|
+
manifest = JSON.parse(fs11.readFileSync(manifestPath, "utf8"));
|
|
5535
6273
|
} catch (err) {
|
|
5536
6274
|
warn(`\u274C Could not read ${manifestPath}: ${err.message}`);
|
|
5537
6275
|
return 1;
|
|
@@ -5603,9 +6341,23 @@ ${USAGE}
|
|
|
5603
6341
|
info(
|
|
5604
6342
|
` - Total size: ${(manifest.counts.totalSizeBytes / (1024 * 1024)).toFixed(2)} MB`
|
|
5605
6343
|
);
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
6344
|
+
const { artifacts } = resolved;
|
|
6345
|
+
if (artifacts.parts) {
|
|
6346
|
+
info(` - Parts written: ${manifest.counts.partsCount} under ${resolved.outDir}/parts/`);
|
|
6347
|
+
}
|
|
6348
|
+
const reports = [
|
|
6349
|
+
artifacts.projectTree && "project-tree",
|
|
6350
|
+
artifacts.navigation && "navigation",
|
|
6351
|
+
artifacts.audit && "audit"
|
|
6352
|
+
].filter(Boolean);
|
|
6353
|
+
if (reports.length > 0) {
|
|
6354
|
+
info(` - Reports: ${resolved.outDir}/reports/ (${reports.join(", ")})`);
|
|
6355
|
+
}
|
|
6356
|
+
if (artifacts.manifest) info(` - Manifest: ${resolved.outDir}/manifest.json`);
|
|
6357
|
+
if (artifacts.monolith) info(` - Monolith: ${resolved.outDir}/full-codebase.{json,md}`);
|
|
6358
|
+
if (reports.length === 0 && !artifacts.parts && !artifacts.manifest && !artifacts.monolith) {
|
|
6359
|
+
info(` - Artifacts: all disabled (only the ownership marker was written)`);
|
|
6360
|
+
}
|
|
5609
6361
|
info(` - Fingerprint: ${manifest.fingerprint}`);
|
|
5610
6362
|
info(` - Integrity: ${manifest.validation.status}`);
|
|
5611
6363
|
if (manifest.warnings.length > 0) {
|
|
@@ -5622,15 +6374,23 @@ ${USAGE}
|
|
|
5622
6374
|
}
|
|
5623
6375
|
return manifest.validation.status === "passed" ? 0 : 1;
|
|
5624
6376
|
} catch (err) {
|
|
6377
|
+
const code = err.code;
|
|
6378
|
+
if (code === "EPERM" || code === "EACCES") {
|
|
6379
|
+
warn(
|
|
6380
|
+
`\u274C Cannot write the snapshot: permission denied under ${resolved.outDir}. Check the directory's permissions or choose another --out-dir.`
|
|
6381
|
+
);
|
|
6382
|
+
warn(` (${err.message})`);
|
|
6383
|
+
return 1;
|
|
6384
|
+
}
|
|
5625
6385
|
warn(`\u274C Failed to generate snapshot: ${err.message}`);
|
|
5626
6386
|
return 1;
|
|
5627
6387
|
}
|
|
5628
6388
|
}
|
|
5629
6389
|
async function runWatchLoop(invocation, resolved) {
|
|
5630
|
-
const outDir =
|
|
5631
|
-
const tmpDir =
|
|
5632
|
-
const outName =
|
|
5633
|
-
const tmpName =
|
|
6390
|
+
const outDir = path12.resolve(resolved.cwd, resolved.outDir);
|
|
6391
|
+
const tmpDir = path12.resolve(resolved.cwd, resolved.tmpDir);
|
|
6392
|
+
const outName = path12.basename(outDir).toLowerCase();
|
|
6393
|
+
const tmpName = path12.basename(tmpDir).toLowerCase();
|
|
5634
6394
|
const isGenerated = (candidate) => candidate.toLowerCase().split(/[\\/]/).some(
|
|
5635
6395
|
(segment) => segment === outName || segment === tmpName || segment.startsWith(`${outName}.backup-`)
|
|
5636
6396
|
);
|
|
@@ -5654,9 +6414,9 @@ async function runWatchLoop(invocation, resolved) {
|
|
|
5654
6414
|
info("\u{1F440} Watching for changes. Press Ctrl+C to stop.");
|
|
5655
6415
|
regenerate();
|
|
5656
6416
|
let timer = null;
|
|
5657
|
-
const watcher =
|
|
6417
|
+
const watcher = fs11.watch(resolved.cwd, { recursive: true }, (_event, filename) => {
|
|
5658
6418
|
if (filename) {
|
|
5659
|
-
const changed =
|
|
6419
|
+
const changed = path12.resolve(resolved.cwd, filename.toString());
|
|
5660
6420
|
if (isGenerated(changed)) return;
|
|
5661
6421
|
}
|
|
5662
6422
|
if (timer) clearTimeout(timer);
|