@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/dist/cli.cjs CHANGED
@@ -81,7 +81,13 @@ function compareManifests(before, after) {
81
81
  };
82
82
  }
83
83
  function loadManifest(target) {
84
- const manifestPath = fs9__namespace.statSync(target).isDirectory() ? path10__namespace.join(target, "manifest.json") : target;
84
+ let isDirectory;
85
+ try {
86
+ isDirectory = fs9__namespace.statSync(target).isDirectory();
87
+ } catch {
88
+ throw new Error(`No snapshot found at ${target}: the path does not exist.`);
89
+ }
90
+ const manifestPath = isDirectory ? path10__namespace.join(target, "manifest.json") : target;
85
91
  let raw;
86
92
  try {
87
93
  raw = fs9__namespace.readFileSync(manifestPath, "utf8");
@@ -97,6 +103,9 @@ function loadManifest(target) {
97
103
  if (!parsed || !Array.isArray(parsed.files)) {
98
104
  throw new Error(`${manifestPath} is not a snapshot manifest (no "files" array).`);
99
105
  }
106
+ if (!parsed.counts || typeof parsed.counts !== "object") {
107
+ throw new Error(`${manifestPath} is not a snapshot manifest (no "counts" object).`);
108
+ }
100
109
  return parsed;
101
110
  }
102
111
  function renderDiffText(diff) {
@@ -959,11 +968,12 @@ function detectPreset(cwd, forced) {
959
968
  }
960
969
  function getGitInfo(cwd, fallbackBranch = "main") {
961
970
  try {
962
- const branch = childProcess__namespace.execSync("git rev-parse --abbrev-ref HEAD", {
971
+ const reported = childProcess__namespace.execSync("git rev-parse --abbrev-ref HEAD", {
963
972
  cwd,
964
973
  stdio: ["ignore", "pipe", "ignore"],
965
974
  encoding: "utf8"
966
- }).trim() || fallbackBranch;
975
+ }).trim();
976
+ const branch = !reported || reported === "HEAD" ? fallbackBranch : reported;
967
977
  const commit = childProcess__namespace.execSync("git rev-parse HEAD", {
968
978
  cwd,
969
979
  stdio: ["ignore", "pipe", "ignore"],
@@ -1584,6 +1594,9 @@ async function loadConfigFileDetailed(cwd) {
1584
1594
  if (snapshotKey && typeof snapshotKey === "object") {
1585
1595
  return { config: snapshotKey, file: pkgJsonPath, warnings, errors };
1586
1596
  }
1597
+ if (snapshotKey !== void 0) {
1598
+ warnings.push(`${pkgJsonPath}: the "snapshot" key must be an object; it was ignored.`);
1599
+ }
1587
1600
  } catch (err) {
1588
1601
  const message = err.message;
1589
1602
  warnings.push(message);
@@ -2096,11 +2109,12 @@ var PRESET_NAMES = [
2096
2109
  "java-spring \xB7 dotnet \xB7 php-laravel \xB7 ruby-rails",
2097
2110
  "monorepo \xB7 docs \xB7 infra \xB7 generic"
2098
2111
  ];
2112
+ var CONFIG_HEADER = `// Generated by codebase-snapshot. A plain object on purpose: this file must load
2113
+ // even when the package itself is only run through npx.
2114
+ `;
2099
2115
  function renderConfig(answers) {
2100
2116
  const { artifacts } = answers;
2101
- return `import { defineConfig } from "@ankurhalder/codebase-snapshot";
2102
-
2103
- export default defineConfig({
2117
+ return `${CONFIG_HEADER}export default {
2104
2118
  // \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
2105
2119
  // Which project shape to use. Detected from the repo when omitted.
2106
2120
  // ${PRESET_NAMES.join("\n // ")}
@@ -2169,7 +2183,7 @@ export default defineConfig({
2169
2183
  // \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
2170
2184
  // afterGenerate: (manifest) => { /* write your own artifact */ }
2171
2185
  hooks: {},
2172
- });
2186
+ };
2173
2187
  `;
2174
2188
  }
2175
2189
  function renderMinimalConfig(answers) {
@@ -2197,16 +2211,12 @@ ${entries},
2197
2211
  },`);
2198
2212
  }
2199
2213
  if (lines.length === 0) {
2200
- return `import { defineConfig } from "@ankurhalder/codebase-snapshot";
2201
-
2202
- export default defineConfig({});
2214
+ return `${CONFIG_HEADER}export default {};
2203
2215
  `;
2204
2216
  }
2205
- return `import { defineConfig } from "@ankurhalder/codebase-snapshot";
2206
-
2207
- export default defineConfig({
2217
+ return `${CONFIG_HEADER}export default {
2208
2218
  ${lines.join("\n")}
2209
- });
2219
+ };
2210
2220
  `;
2211
2221
  }
2212
2222
  var PromptCancelled = class extends Error {
@@ -2467,6 +2477,17 @@ var CONFIG_CANDIDATES = [
2467
2477
  ".snapshotrc.json",
2468
2478
  ".snapshotrc"
2469
2479
  ];
2480
+ function chooseConfigFileName(cwd) {
2481
+ if (!supportsTypeScriptImports()) return "snapshot.config.mjs";
2482
+ const pkgPath = path10__namespace.join(cwd, "package.json");
2483
+ if (!fs9__namespace.existsSync(pkgPath)) return "snapshot.config.mjs";
2484
+ try {
2485
+ const parsed = JSON.parse(fs9__namespace.readFileSync(pkgPath, "utf8"));
2486
+ return parsed.type === "module" ? "snapshot.config.ts" : "snapshot.config.mjs";
2487
+ } catch {
2488
+ return "snapshot.config.mjs";
2489
+ }
2490
+ }
2470
2491
  var SIZE_CHOICES = [
2471
2492
  { value: "60k", label: "Small", hint: "60 KB \xB7 ~15k tokens \xB7 tight context windows" },
2472
2493
  { value: "120k", label: "Medium", hint: "120 KB \xB7 ~30k tokens \xB7 recommended" },
@@ -2672,7 +2693,7 @@ async function runInit(options) {
2672
2693
  if (!proceed) throw new PromptCancelled();
2673
2694
  }
2674
2695
  const configText = options.minimal ? renderMinimalConfig(answers) : renderConfig(answers);
2675
- const configName = supportsTypeScriptImports() ? "snapshot.config.ts" : "snapshot.config.mjs";
2696
+ const configName = chooseConfigFileName(options.cwd);
2676
2697
  const configPath = path10__namespace.join(options.cwd, configName);
2677
2698
  if (options.dryRun) {
2678
2699
  return {
@@ -2930,9 +2951,9 @@ var JAVA_METHOD = /^\s+(?:public|private|protected)\s+(?:static\s+)?(?:final\s+)
2930
2951
  var jvmProfile = {
2931
2952
  id: "jvm",
2932
2953
  extensions: [".java", ".kt", ".kts", ".scala"],
2933
- summarize(path12, content) {
2954
+ summarize(path13, content) {
2934
2955
  const summary = emptySummary();
2935
- const lower = path12.toLowerCase();
2956
+ const lower = path13.toLowerCase();
2936
2957
  const isJava = lower.endsWith(".java");
2937
2958
  const isKotlin = lower.endsWith(".kt") || lower.endsWith(".kts");
2938
2959
  eachLine(content, (line) => {
@@ -3353,16 +3374,16 @@ function summarizeTypeScriptSource(relativePath, content) {
3353
3374
  // src/ast/profiles/sfc.ts
3354
3375
  var SCRIPT_BLOCK = /<script[^>]*>([\s\S]*?)<\/script>/gi;
3355
3376
  var ASTRO_FRONTMATTER = /^\uFEFF?---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?=\r?\n|$)/;
3356
- function componentName(path12) {
3357
- const base = path12.split("/").pop() ?? path12;
3377
+ function componentName(path13) {
3378
+ const base = path13.split("/").pop() ?? path13;
3358
3379
  return base.replace(/\.[^.]+$/, "");
3359
3380
  }
3360
3381
  var sfcProfile = {
3361
3382
  id: "sfc",
3362
3383
  extensions: [".vue", ".svelte", ".astro"],
3363
- summarize(path12, content) {
3384
+ summarize(path13, content) {
3364
3385
  let script = "";
3365
- if (path12.toLowerCase().endsWith(".astro")) {
3386
+ if (path13.toLowerCase().endsWith(".astro")) {
3366
3387
  const frontmatter = ASTRO_FRONTMATTER.exec(content);
3367
3388
  if (frontmatter) script += `${frontmatter[1]}
3368
3389
  `;
@@ -3371,7 +3392,7 @@ var sfcProfile = {
3371
3392
  script += `${match[1]}
3372
3393
  `;
3373
3394
  }
3374
- const name = componentName(path12);
3395
+ const name = componentName(path13);
3375
3396
  if (!script.trim()) {
3376
3397
  const summary2 = emptySummary();
3377
3398
  if (name) {
@@ -3405,15 +3426,15 @@ for (const profile of LANGUAGE_PROFILES) {
3405
3426
  BY_EXTENSION.set(extension.toLowerCase(), profile);
3406
3427
  }
3407
3428
  }
3408
- function profileForPath(path12) {
3409
- const lower = path12.toLowerCase();
3429
+ function profileForPath(path13) {
3430
+ const lower = path13.toLowerCase();
3410
3431
  const dot = lower.lastIndexOf(".");
3411
3432
  if (dot === -1) return null;
3412
3433
  return BY_EXTENSION.get(lower.slice(dot)) ?? null;
3413
3434
  }
3414
- function summarizeWithProfile(path12, content) {
3415
- const profile = profileForPath(path12);
3416
- return profile ? profile.summarize(path12, content) : null;
3435
+ function summarizeWithProfile(path13, content) {
3436
+ const profile = profileForPath(path13);
3437
+ return profile ? profile.summarize(path13, content) : null;
3417
3438
  }
3418
3439
 
3419
3440
  // src/ast/structured.ts
@@ -4274,6 +4295,7 @@ function validateSnapshot(configInput) {
4274
4295
  drift.missingOnDisk.push(snapshotFile);
4275
4296
  }
4276
4297
  }
4298
+ drift.missingOnDisk = [...new Set(drift.missingOnDisk)];
4277
4299
  const maxEffectiveBudget = Math.max(
4278
4300
  config.maxPartBytes,
4279
4301
  config.budgets.default,
@@ -4533,7 +4555,7 @@ function validateSnapshot(configInput) {
4533
4555
 
4534
4556
  // src/index.ts
4535
4557
  var GENERATOR_NAME = "@ankurhalder/codebase-snapshot";
4536
- var GENERATOR_VERSION = "2.1.0";
4558
+ var GENERATOR_VERSION = "2.2.0";
4537
4559
  var SCHEMA_VERSION = "3.0.0";
4538
4560
  function sha256Hex2(input) {
4539
4561
  return crypto__namespace.createHash("sha256").update(input).digest("hex");
@@ -4777,7 +4799,9 @@ function artifactWarnings(artifacts) {
4777
4799
  );
4778
4800
  }
4779
4801
  if (ARTIFACT_NAMES.every((name) => artifacts[name] === false)) {
4780
- messages.push("every artifact is disabled: the snapshot directory will be empty.");
4802
+ messages.push(
4803
+ "every artifact is disabled: only the .codebase-snapshot ownership marker will be written."
4804
+ );
4781
4805
  }
4782
4806
  return messages;
4783
4807
  }
@@ -5104,6 +5128,630 @@ function generateSnapshot(configInput, options) {
5104
5128
  return manifest;
5105
5129
  }
5106
5130
 
5131
+ // src/agent/contract.ts
5132
+ var CONTRACT_VERSION = "1.0.0";
5133
+ var PACKAGE_NAME = "@ankurhalder/codebase-snapshot";
5134
+ var ARTIFACT_SUMMARY = {
5135
+ readme: "README.md describing the snapshot at the output root.",
5136
+ manifest: "manifest.json: machine-readable index of every file, part, hash and config.",
5137
+ parts: "parts/*.md: the packed, budget-limited markdown parts that carry the code.",
5138
+ navigation: "reports/navigation.md: links between parts and layers.",
5139
+ projectTree: "reports/project-tree.md: the directory tree of included files.",
5140
+ audit: "reports/audit.md: secret findings, large files, parse diagnostics, git state.",
5141
+ monolith: "full-codebase.json and full-codebase.md: the whole repository in one file (off by default)."
5142
+ };
5143
+ var ARTIFACT_DEFAULTS = {
5144
+ readme: true,
5145
+ manifest: true,
5146
+ parts: true,
5147
+ navigation: true,
5148
+ projectTree: true,
5149
+ audit: true,
5150
+ monolith: false
5151
+ };
5152
+ function renderContractText(contract) {
5153
+ const lines = [];
5154
+ lines.push(`${contract.package} v${contract.version}`);
5155
+ lines.push(contract.summary);
5156
+ lines.push("");
5157
+ lines.push(`Node : ${contract.requires.node}`);
5158
+ lines.push(`Schema : ${contract.schemaVersion} (contract ${contract.contractVersion})`);
5159
+ lines.push(`One-shot : ${contract.invocation.oneShot} <command>`);
5160
+ lines.push(`Installed : ${contract.invocation.installed} <command>`);
5161
+ lines.push("");
5162
+ lines.push("Workflow:");
5163
+ for (const step of contract.workflow) {
5164
+ lines.push(` ${step.step}. ${step.id}`);
5165
+ lines.push(` run : ${step.run}`);
5166
+ lines.push(` expect : ${step.expect}`);
5167
+ if (step.notes) lines.push(` note : ${step.notes}`);
5168
+ }
5169
+ lines.push("");
5170
+ lines.push("Commands:");
5171
+ for (const command of contract.commands) {
5172
+ const args = command.positionals.length > 0 ? ` ${command.positionals.join(" ")}` : "";
5173
+ lines.push(` ${(command.name + args).padEnd(26)} ${command.summary}`);
5174
+ }
5175
+ lines.push("");
5176
+ lines.push("Exit codes:");
5177
+ for (const [code, meaning] of Object.entries(contract.exitCodes)) {
5178
+ lines.push(` ${code} ${meaning}`);
5179
+ }
5180
+ lines.push("");
5181
+ lines.push(`Presets (${contract.presets.length}): ${contract.presets.map((p) => p.name).join(", ")}`);
5182
+ lines.push("");
5183
+ lines.push("Artifacts:");
5184
+ for (const artifact of contract.artifacts) {
5185
+ lines.push(` ${artifact.name.padEnd(12)} ${artifact.default ? "on " : "off"} ${artifact.summary}`);
5186
+ }
5187
+ lines.push("");
5188
+ lines.push("Options:");
5189
+ for (const option of contract.options) {
5190
+ const fallback = option.default === void 0 ? "" : ` [${JSON.stringify(option.default)}]`;
5191
+ lines.push(` ${option.flag.padEnd(24)} ${option.appliesTo.padEnd(32)}${fallback}`);
5192
+ lines.push(` ${"".padEnd(24)} ${option.summary}`);
5193
+ }
5194
+ lines.push("");
5195
+ lines.push("Safety:");
5196
+ for (const note of contract.safety) lines.push(` - ${note}`);
5197
+ lines.push("");
5198
+ lines.push("Troubleshooting:");
5199
+ for (const item of contract.troubleshooting) {
5200
+ lines.push(` ${item.symptom}`);
5201
+ lines.push(` cause: ${item.cause}`);
5202
+ lines.push(` fix : ${item.fix}`);
5203
+ }
5204
+ lines.push("");
5205
+ lines.push("This is the human rendering. Agents should use: describe --format json");
5206
+ return lines.join("\n");
5207
+ }
5208
+ function buildAgentContract() {
5209
+ return {
5210
+ generator: GENERATOR_NAME,
5211
+ package: PACKAGE_NAME,
5212
+ version: GENERATOR_VERSION,
5213
+ schemaVersion: SCHEMA_VERSION,
5214
+ contractVersion: CONTRACT_VERSION,
5215
+ 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).",
5216
+ requires: { node: ">=18.0.0", dependencies: ["typescript"] },
5217
+ invocation: {
5218
+ oneShot: `npx ${PACKAGE_NAME}@latest`,
5219
+ installed: "npx snap",
5220
+ bins: ["snap", "codebase-snapshot"],
5221
+ notes: [
5222
+ `Pin the tag (${PACKAGE_NAME}@latest or @2) instead of the bare package name.`,
5223
+ "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.",
5224
+ "Human progress goes to stderr. With --format json the payload on stdout is pure JSON."
5225
+ ]
5226
+ },
5227
+ workflow: [
5228
+ {
5229
+ step: 1,
5230
+ id: "verify",
5231
+ run: `npx ${PACKAGE_NAME}@latest --version`,
5232
+ expect: "prints v2.x.x on stdout and exits 0",
5233
+ notes: "If it prints nothing, re-run with @latest explicitly, or use `npx -p <pkg> snap`."
5234
+ },
5235
+ {
5236
+ step: 2,
5237
+ id: "understand",
5238
+ run: `npx ${PACKAGE_NAME}@latest describe --format json`,
5239
+ expect: "the full contract as JSON: commands, options, presets, exit codes",
5240
+ notes: "Read this before acting if you have no prior knowledge of the tool."
5241
+ },
5242
+ {
5243
+ step: 3,
5244
+ id: "configure",
5245
+ run: `npx ${PACKAGE_NAME}@latest init --yes`,
5246
+ expect: "writes snapshot.config.ts listing every setting at its current value",
5247
+ notes: "Non-interactive. The detected preset is chosen automatically; override with --preset <name>. Skip this step to run on built-in defaults."
5248
+ },
5249
+ {
5250
+ step: 4,
5251
+ id: "generate",
5252
+ run: `npx ${PACKAGE_NAME}@latest generate`,
5253
+ expect: "exit 0, and ./snapshot/ written with parts, reports and manifest.json"
5254
+ },
5255
+ {
5256
+ step: 5,
5257
+ id: "verify-output",
5258
+ run: `npx ${PACKAGE_NAME}@latest validate`,
5259
+ expect: "exit 0 means the snapshot matches the files on disk",
5260
+ notes: "Exit 1 means drift or a damaged snapshot; re-run generate."
5261
+ },
5262
+ {
5263
+ step: 6,
5264
+ id: "consume",
5265
+ run: "read ./snapshot/parts/*.md, then ./snapshot/manifest.json, then ./snapshot/reports/audit.md",
5266
+ expect: "the code, the index of what was included, and the risk findings",
5267
+ 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."
5268
+ }
5269
+ ],
5270
+ commands: [
5271
+ {
5272
+ name: "generate",
5273
+ aliases: ["default"],
5274
+ summary: "Write the snapshot. This is the command you normally want.",
5275
+ positionals: [],
5276
+ exits: [0, 1]
5277
+ },
5278
+ {
5279
+ name: "validate",
5280
+ aliases: ["check"],
5281
+ summary: "Compare the snapshot on disk with the repository; exit 1 on drift.",
5282
+ positionals: [],
5283
+ exits: [0, 1]
5284
+ },
5285
+ {
5286
+ name: "report",
5287
+ aliases: [],
5288
+ summary: "Print snapshot health, layer metrics and the secret audit.",
5289
+ positionals: [],
5290
+ exits: [0, 1]
5291
+ },
5292
+ {
5293
+ name: "init",
5294
+ aliases: [],
5295
+ summary: "Write a commented snapshot.config.ts (interactive, or --yes for CI).",
5296
+ positionals: [],
5297
+ exits: [0, 1, 2]
5298
+ },
5299
+ {
5300
+ name: "explain",
5301
+ aliases: [],
5302
+ summary: "Explain how one path would be treated: layer, kind, part, skip reason.",
5303
+ positionals: ["<path>"],
5304
+ exits: [0, 1]
5305
+ },
5306
+ {
5307
+ name: "diff",
5308
+ aliases: [],
5309
+ summary: "Compare two snapshots (directories or manifest.json files).",
5310
+ positionals: ["<before>", "<after>"],
5311
+ exits: [0, 1]
5312
+ },
5313
+ {
5314
+ name: "describe",
5315
+ aliases: [],
5316
+ summary: "Print this contract. --format json for machines, text for humans.",
5317
+ positionals: [],
5318
+ exits: [0]
5319
+ },
5320
+ {
5321
+ name: "skill",
5322
+ aliases: [],
5323
+ summary: "Print the agent skill, or --install it into the repository (AGENTS.md, .claude/skills, .cursor/rules).",
5324
+ positionals: [],
5325
+ exits: [0, 1, 2]
5326
+ }
5327
+ ],
5328
+ exitCodes: {
5329
+ "0": "success (and validate: no drift)",
5330
+ "1": "drift detected, or generation refused/failed (nothing was written)",
5331
+ "2": "usage error: unknown flag/command, bad value, wrong argument count"
5332
+ },
5333
+ options: [
5334
+ {
5335
+ flag: "--out-dir",
5336
+ type: "string",
5337
+ default: "snapshot",
5338
+ appliesTo: "generate/validate/report/explain",
5339
+ summary: "Where the snapshot is written. Must not be the repo root or a directory the tool did not create."
5340
+ },
5341
+ {
5342
+ flag: "--max-part-bytes",
5343
+ type: "string",
5344
+ default: 307200,
5345
+ appliesTo: "generate/validate/report",
5346
+ summary: 'Byte budget per markdown part. Accepts "120k" / "1.5mb". Minimum 4096.'
5347
+ },
5348
+ {
5349
+ flag: "--large-file-bytes",
5350
+ type: "string",
5351
+ default: 204800,
5352
+ appliesTo: "generate/validate/report",
5353
+ summary: "Above this size a file gets a head/tail preview instead of the full body."
5354
+ },
5355
+ {
5356
+ flag: "--max-monolith-bytes",
5357
+ type: "string",
5358
+ default: "unset",
5359
+ appliesTo: "generate",
5360
+ summary: "Skip the monolith artifacts when they would exceed this size."
5361
+ },
5362
+ {
5363
+ flag: "--include-monolith",
5364
+ type: "boolean",
5365
+ default: false,
5366
+ appliesTo: "generate",
5367
+ summary: "Also write full-codebase.json and full-codebase.md."
5368
+ },
5369
+ {
5370
+ flag: "--include-sensitive-files",
5371
+ type: "boolean",
5372
+ default: false,
5373
+ appliesTo: "generate/validate/report",
5374
+ summary: "Also emit .env / *.pem / .npmrc style files. Off by default; they are recorded as skipped."
5375
+ },
5376
+ {
5377
+ flag: "--skip-artifacts",
5378
+ type: "string",
5379
+ default: "unset",
5380
+ appliesTo: "generate",
5381
+ summary: "Comma-separated artifacts to omit (readme, manifest, parts, navigation, projectTree, audit, monolith)."
5382
+ },
5383
+ {
5384
+ flag: "--only-artifacts",
5385
+ type: "string",
5386
+ default: "unset",
5387
+ appliesTo: "generate",
5388
+ summary: "Write only these artifacts. Cannot be combined with --skip-artifacts."
5389
+ },
5390
+ {
5391
+ flag: "--format",
5392
+ type: "string",
5393
+ default: "text",
5394
+ appliesTo: "all commands",
5395
+ summary: 'Output format: "text" or "json". JSON goes to stdout, progress to stderr.'
5396
+ },
5397
+ {
5398
+ flag: "--cwd",
5399
+ type: "string",
5400
+ default: "process.cwd()",
5401
+ appliesTo: "all commands",
5402
+ summary: "Repository to operate on. Must be an existing directory."
5403
+ },
5404
+ {
5405
+ flag: "--dry-run",
5406
+ type: "boolean",
5407
+ default: false,
5408
+ appliesTo: "generate",
5409
+ summary: "Compute everything and print the result without writing."
5410
+ },
5411
+ {
5412
+ flag: "--watch",
5413
+ type: "boolean",
5414
+ default: false,
5415
+ appliesTo: "generate",
5416
+ summary: "Regenerate on every change until interrupted."
5417
+ },
5418
+ {
5419
+ flag: "--preset",
5420
+ type: "string",
5421
+ default: "detected",
5422
+ appliesTo: "init",
5423
+ summary: "Force a project preset instead of detecting one. Unknown names are a usage error."
5424
+ },
5425
+ {
5426
+ flag: "--include",
5427
+ type: "string",
5428
+ default: "unset",
5429
+ appliesTo: "init",
5430
+ summary: "Comma-separated allow-list of globs to record in the generated config."
5431
+ },
5432
+ {
5433
+ flag: "--exclude",
5434
+ type: "string",
5435
+ default: "unset",
5436
+ appliesTo: "init",
5437
+ summary: "Comma-separated globs to record in the generated config."
5438
+ },
5439
+ {
5440
+ flag: "--yes",
5441
+ type: "boolean",
5442
+ default: false,
5443
+ appliesTo: "init",
5444
+ summary: "Accept every default without prompting. Required in a non-TTY."
5445
+ },
5446
+ {
5447
+ flag: "--minimal",
5448
+ type: "boolean",
5449
+ default: false,
5450
+ appliesTo: "init",
5451
+ summary: "Write only the settings that differ from the defaults."
5452
+ },
5453
+ {
5454
+ flag: "--force",
5455
+ type: "boolean",
5456
+ default: false,
5457
+ appliesTo: "init",
5458
+ summary: "Overwrite an existing config file."
5459
+ },
5460
+ {
5461
+ flag: "--install",
5462
+ type: "boolean",
5463
+ default: false,
5464
+ appliesTo: "skill",
5465
+ summary: "Write the skill into the repository instead of printing it."
5466
+ },
5467
+ {
5468
+ flag: "--target",
5469
+ type: "string",
5470
+ default: "all",
5471
+ appliesTo: "skill",
5472
+ summary: 'Where to install: "claude", "agents", "cursor" or "all".'
5473
+ }
5474
+ ],
5475
+ presets: PRESETS.map((preset) => ({
5476
+ name: preset.name,
5477
+ priority: preset.priority,
5478
+ recommendedBudget: preset.recommendedBudget ?? "300k",
5479
+ excludes: preset.exclude ? [...preset.exclude] : []
5480
+ })),
5481
+ artifacts: ARTIFACT_NAMES.map((name) => ({
5482
+ name,
5483
+ default: ARTIFACT_DEFAULTS[name] ?? true,
5484
+ summary: ARTIFACT_SUMMARY[name] ?? name
5485
+ })),
5486
+ config: {
5487
+ fileNames: [
5488
+ "snapshot.config.ts",
5489
+ "snapshot.config.js",
5490
+ "snapshot.config.mjs",
5491
+ "snapshot.config.cjs",
5492
+ "snapshot.config.json",
5493
+ ".snapshotrc",
5494
+ ".snapshotrc.json",
5495
+ 'package.json \u2192 "snapshot" key'
5496
+ ],
5497
+ settings: [
5498
+ { key: "outDir", type: "string", default: "snapshot", summary: "Snapshot output directory." },
5499
+ { key: "tmpDir", type: "string", default: "snapshot.tmp", summary: "Scratch directory used for the atomic swap." },
5500
+ { key: "branch", type: "string", default: "main", summary: "Branch recorded when git cannot report one." },
5501
+ { key: "preset", type: "string", default: "detected", summary: "Force a preset. Unknown names are an error." },
5502
+ { key: "maxPartBytes", type: "number | string", default: 307200, summary: "Per-part byte budget." },
5503
+ { key: "largeFileBytes", type: "number | string", default: 204800, summary: "Preview threshold for large files." },
5504
+ { key: "previewHeadLines", type: "number", default: 80, summary: "Lines kept from the head of a large file." },
5505
+ { key: "previewTailLines", type: "number", default: 40, summary: "Lines kept from the tail of a large file." },
5506
+ { key: "includeMonolith", type: "boolean", default: false, summary: "Also write the single-file monolith." },
5507
+ { key: "maxMonolithBytes", type: "number | string | null", default: null, summary: "Skip the monolith above this size." },
5508
+ { key: "includeSensitiveFiles", type: "boolean", default: false, summary: "Emit .env / *.pem / credential files." },
5509
+ { key: "include", type: "string[]", default: "[]", summary: "Allow-list of globs; empty means everything." },
5510
+ { key: "excludePatterns", type: "string[]", default: "[]", summary: "Extra exclude globs." },
5511
+ { key: "artifacts", type: "object", default: "all on except monolith", summary: "Per-artifact opt-out." },
5512
+ { key: "budgets", type: "object", default: "{ default, byLayer }", summary: "Per-layer byte budgets." },
5513
+ { key: "layersMode", type: '"merge" | "replace"', default: "merge", summary: "Whether custom layers extend or replace the preset's." },
5514
+ { key: "layers", type: "array", default: "preset layers", summary: "Custom layer rules (glob or RegExp)." },
5515
+ { key: "kinds", type: "array", default: "preset kinds", summary: "Custom kind rules." },
5516
+ { key: "secrets", type: "object", default: "{}", summary: "Custom secret patterns, or disable the defaults." },
5517
+ { key: "hooks", type: "object", default: "{}", summary: "afterGenerate(manifest) runs after a successful generate." }
5518
+ ]
5519
+ },
5520
+ output: {
5521
+ layout: [
5522
+ "<outDir>/README.md",
5523
+ "<outDir>/manifest.json",
5524
+ "<outDir>/parts/part-NNN.md",
5525
+ "<outDir>/reports/project-tree.md",
5526
+ "<outDir>/reports/navigation.md",
5527
+ "<outDir>/reports/audit.md",
5528
+ "<outDir>/.codebase-snapshot (ownership marker, always written)"
5529
+ ],
5530
+ notes: [
5531
+ "Parts are ordered and budget-limited; a single file larger than the budget becomes a head/tail preview.",
5532
+ "manifest.json.files[] carries relativePath, layer, kind, language, part, inclusionMode, hash and a structural summary.",
5533
+ "manifest.skipped[] carries the path and the exact reason it was excluded.",
5534
+ `The output directory is replaced atomically; ${MIN_MAX_PART_BYTES} bytes is the smallest legal budget.`
5535
+ ]
5536
+ },
5537
+ safety: [
5538
+ "Sensitive files (.env, *.pem, .npmrc, .ssh/, .tfstate) are excluded by default and listed in manifest.skipped[].",
5539
+ "Secret-shaped strings in ordinary source files are redacted in the emitted parts.",
5540
+ "The tool refuses to replace an outDir that it did not create, so a mistyped outDir cannot delete source code.",
5541
+ "Nothing is ever written above the repository root; --out-dir may not be the root or an ancestor."
5542
+ ],
5543
+ troubleshooting: [
5544
+ {
5545
+ symptom: "Command prints nothing and exits 0",
5546
+ cause: "A bare scoped spec resolved to a bin-less placeholder manifest on this npm build.",
5547
+ fix: `Use the pinned form: npx ${PACKAGE_NAME}@latest ...`
5548
+ },
5549
+ {
5550
+ symptom: "Exit 1 with 'Refusing to replace outDir'",
5551
+ cause: "The outDir exists and contains files this tool did not write.",
5552
+ fix: "Choose a new/empty --out-dir, or delete the existing directory yourself."
5553
+ },
5554
+ {
5555
+ symptom: "Exit 1 with 'The config file could not be read'",
5556
+ cause: "snapshot.config.* exists but does not parse.",
5557
+ fix: "Fix the syntax, or delete the config to use the defaults."
5558
+ },
5559
+ {
5560
+ symptom: "Exit 1 from validate",
5561
+ cause: "The snapshot no longer matches the files on disk.",
5562
+ fix: "Re-run generate."
5563
+ },
5564
+ {
5565
+ symptom: "A directory you expected is missing from the snapshot",
5566
+ cause: "It is excluded by the preset, by default ignores, or by your own globs.",
5567
+ fix: "Run `explain <path>`; manifest.skipped[] also records the reason for every file."
5568
+ }
5569
+ ]
5570
+ };
5571
+ }
5572
+
5573
+ // src/agent/skill.ts
5574
+ var PACKAGE_NAME2 = "@ankurhalder/codebase-snapshot";
5575
+ var SKILL_NAME = "codebase-snapshot";
5576
+ var AGENTS_MARKER_START = "<!-- codebase-snapshot:skill:start -->";
5577
+ var AGENTS_MARKER_END = "<!-- codebase-snapshot:skill:end -->";
5578
+ function renderSkill() {
5579
+ return `---
5580
+ name: ${SKILL_NAME}
5581
+ 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.
5582
+ version: ${GENERATOR_VERSION}
5583
+ ---
5584
+
5585
+ # codebase-snapshot
5586
+
5587
+ Turns a repository into an LLM-ready snapshot under \`./snapshot/\`: code packed into
5588
+ size-budgeted markdown parts, plus a manifest that indexes every file, a project tree, a
5589
+ navigation index and a secret audit.
5590
+
5591
+ Use it when you need to understand a whole repository, or produce repository context for
5592
+ another model, and reading files one by one would not fit in context.
5593
+
5594
+ ## Before you start
5595
+
5596
+ This tool is a Node CLI. It needs **Node.js 18 or newer**. You do not need to install it
5597
+ globally, and you do not need to clone anything.
5598
+
5599
+ **Always pin the tag** \u2014 \`npx ${PACKAGE_NAME2}@latest <command>\`. A bare scoped spec can
5600
+ resolve to a placeholder manifest with no executable on some npm builds and exit 0
5601
+ printing nothing; the pinned form cannot. If a command exits 0 with no output, that is
5602
+ the cause, not a broken repository.
5603
+
5604
+ ## Step by step
5605
+
5606
+ 1. **Verify the tool.** Run \`npx ${PACKAGE_NAME2}@latest --version\`. Expect \`v${GENERATOR_VERSION}\` or
5607
+ newer on stdout. Empty output means you hit the bare-spec problem above.
5608
+
5609
+ 2. **Read the contract, not the prose.** Run
5610
+ \`npx ${PACKAGE_NAME2}@latest describe --format json\`. It returns the full machine
5611
+ contract: every command, every option with its default, the exit-code meanings, the
5612
+ 25 project presets, the artifact list, the config settings and a troubleshooting
5613
+ table. Read it before you make decisions; do not guess option names.
5614
+
5615
+ 3. **Look before you leap (optional but cheap).** To see how a specific file or directory
5616
+ would be treated, run \`npx ${PACKAGE_NAME2}@latest explain <path>\`. It reports the
5617
+ layer, kind, language, target part, and the exact reason a path would be skipped.
5618
+ \`--format json\` if you want to parse it.
5619
+
5620
+ 4. **Configure (recommended).** Run \`npx ${PACKAGE_NAME2}@latest init --yes\`. It detects
5621
+ the project shape, then writes \`snapshot.config.ts\` listing **every** setting at its
5622
+ current value, commented. Pass \`--preset <name>\` only if detection is wrong; the
5623
+ preset names are in the contract. This step is optional: without a config the tool
5624
+ runs on the same defaults.
5625
+
5626
+ 5. **Generate.** Run \`npx ${PACKAGE_NAME2}@latest generate\`. Expect exit 0 and a
5627
+ \`./snapshot/\` directory. If you need a different location use \`--out-dir <path>\`; if
5628
+ you need smaller or larger parts use \`--max-part-bytes 120k\`.
5629
+
5630
+ 6. **Verify.** Run \`npx ${PACKAGE_NAME2}@latest validate\`. Exit 0 means the snapshot
5631
+ matches the files on disk. Exit 1 means drift \u2014 re-run step 5.
5632
+
5633
+ 7. **Read the output in this order:**
5634
+ - \`snapshot/parts/part-*.md\` \u2014 the code itself, each file tagged with its layer, kind,
5635
+ language and a structural summary (imports, exports, classes, functions).
5636
+ - \`snapshot/manifest.json\` \u2014 the index. \`files[]\` has \`relativePath\`, \`layer\`,
5637
+ \`kind\`, \`language\`, \`part\`, \`inclusionMode\`, \`hashSha256\` and \`summary\`.
5638
+ \`skipped[]\` records every excluded file **and why**. Prefer querying this over
5639
+ re-reading the parts.
5640
+ - \`snapshot/reports/audit.md\` \u2014 secrets found, large files, parse diagnostics, git
5641
+ state. Read this before you quote code back to a user.
5642
+ - \`snapshot/reports/project-tree.md\` and \`navigation.md\` \u2014 structure and cross-links.
5643
+
5644
+ ## Project types
5645
+
5646
+ Detection is automatic and covers Next.js, Nuxt, SvelteKit, Remix, Astro, Angular, Solid,
5647
+ Vue+React with Vite, Nest, Express/Fastify/Koa, any Node library, monorepos, Django,
5648
+ FastAPI/Flask, plain Python, Go, Rust, Java/Spring, .NET, PHP/Laravel, Ruby/Rails,
5649
+ Terraform/Docker infra, docs-only repositories, and a generic fallback. It sets sensible
5650
+ layers, kinds, ecosystem ignore patterns and a recommended part budget. For non-TypeScript
5651
+ languages it extracts imports, exports, classes and functions structurally; Vue, Svelte and
5652
+ Astro script blocks go through the real TypeScript compiler. You do not need to configure
5653
+ any of this to get a good first snapshot.
5654
+
5655
+ ## Things that will surprise you
5656
+
5657
+ - **Sensitive files are excluded by default** (\`.env\`, \`*.pem\`, \`.npmrc\`, \`.ssh/\`,
5658
+ \`.tfstate\`) and are listed in \`manifest.skipped[]\`. Add \`--include-sensitive-files\` only
5659
+ if the user asks for them explicitly.
5660
+ - **Secret-shaped strings inside normal source files are redacted** in the emitted parts.
5661
+ If the user needs the raw value, say so rather than working around it.
5662
+ - **The output directory is owned by the tool.** It is replaced atomically, and the tool
5663
+ refuses to replace a directory it did not create. Never point \`--out-dir\` at source
5664
+ code; if you see \`Refusing to replace outDir\`, choose a new directory rather than
5665
+ deleting anything.
5666
+ - **Exit 2 is a usage error**, not a failure of the repository. Re-read the contract
5667
+ before retrying.
5668
+ - **\`--format json\` keeps stdout pure.** Progress goes to stderr, so \`stdout\` is safe to
5669
+ pipe into a JSON parser.
5670
+
5671
+ ## Making this available to other agents
5672
+
5673
+ Run \`npx ${PACKAGE_NAME2}@latest skill --install\`. It writes this skill into the
5674
+ repository in the places agents look for it by default: an \`AGENTS.md\` section,
5675
+ \`.claude/skills/${SKILL_NAME}/SKILL.md\`, and \`.cursor/rules/${SKILL_NAME}.mdc\`. The command
5676
+ is idempotent and only replaces its own marked section.
5677
+ `;
5678
+ }
5679
+
5680
+ // src/agent/install-skill.ts
5681
+ var SKILL_TARGETS = ["claude", "agents", "cursor", "all"];
5682
+ function targetPath(cwd, target) {
5683
+ switch (target) {
5684
+ case "claude":
5685
+ return path10__namespace.join(cwd, ".claude", "skills", SKILL_NAME, "SKILL.md");
5686
+ case "cursor":
5687
+ return path10__namespace.join(cwd, ".cursor", "rules", `${SKILL_NAME}.mdc`);
5688
+ case "agents":
5689
+ return path10__namespace.join(cwd, "AGENTS.md");
5690
+ }
5691
+ }
5692
+ function writeIfChanged(target, filePath, content) {
5693
+ const existing = fs9__namespace.existsSync(filePath) ? fs9__namespace.readFileSync(filePath, "utf8") : null;
5694
+ if (existing === content) return { target, path: filePath, action: "unchanged" };
5695
+ fs9__namespace.mkdirSync(path10__namespace.dirname(filePath), { recursive: true });
5696
+ fs9__namespace.writeFileSync(filePath, content, "utf8");
5697
+ return { target, path: filePath, action: existing === null ? "created" : "updated" };
5698
+ }
5699
+ function agentsBlock(skill) {
5700
+ return [
5701
+ AGENTS_MARKER_START,
5702
+ "",
5703
+ `## ${SKILL_NAME} (${"@ankurhalder/codebase-snapshot"})`,
5704
+ "",
5705
+ // Drop the YAML frontmatter: inside AGENTS.md it is noise, and the heading
5706
+ // above already names the skill.
5707
+ skill.replace(/^---[\s\S]*?---\r?\n/, "").trim(),
5708
+ "",
5709
+ AGENTS_MARKER_END,
5710
+ ""
5711
+ ].join("\n");
5712
+ }
5713
+ function mergeAgentsFile(cwd, skill) {
5714
+ const filePath = targetPath(cwd, "agents");
5715
+ const block = agentsBlock(skill);
5716
+ const existing = fs9__namespace.existsSync(filePath) ? fs9__namespace.readFileSync(filePath, "utf8") : null;
5717
+ let next;
5718
+ if (existing === null) {
5719
+ next = `# AGENTS.md
5720
+
5721
+ ${block}`;
5722
+ } else if (existing.includes(AGENTS_MARKER_START) && existing.includes(AGENTS_MARKER_END)) {
5723
+ const start = existing.indexOf(AGENTS_MARKER_START);
5724
+ const end = existing.indexOf(AGENTS_MARKER_END) + AGENTS_MARKER_END.length;
5725
+ next = `${existing.slice(0, start)}${block.trimEnd()}${existing.slice(end)}`;
5726
+ } else {
5727
+ next = `${existing.trimEnd()}
5728
+
5729
+ ${block}`;
5730
+ }
5731
+ const action = existing === null ? "created" : existing === next ? "unchanged" : "updated";
5732
+ if (action !== "unchanged") {
5733
+ fs9__namespace.mkdirSync(path10__namespace.dirname(filePath), { recursive: true });
5734
+ fs9__namespace.writeFileSync(filePath, next, "utf8");
5735
+ }
5736
+ return { target: "agents", path: filePath, action };
5737
+ }
5738
+ function installSkill(cwd, target = "all") {
5739
+ const skill = renderSkill();
5740
+ const written = [];
5741
+ const wanted = target === "all" ? ["agents", "claude", "cursor"] : [target];
5742
+ for (const item of wanted) {
5743
+ if (item === "agents") {
5744
+ written.push(mergeAgentsFile(cwd, skill));
5745
+ continue;
5746
+ }
5747
+ written.push(writeIfChanged(item, targetPath(cwd, item), skill));
5748
+ }
5749
+ return { cwd, written };
5750
+ }
5751
+ function isSkillTarget(value) {
5752
+ return SKILL_TARGETS.includes(value);
5753
+ }
5754
+
5107
5755
  // src/cli.ts
5108
5756
  var VERSION = GENERATOR_VERSION;
5109
5757
  var COMMAND_ALIASES = /* @__PURE__ */ new Map([
@@ -5113,7 +5761,9 @@ var COMMAND_ALIASES = /* @__PURE__ */ new Map([
5113
5761
  ["report", "report"],
5114
5762
  ["init", "init"],
5115
5763
  ["explain", "explain"],
5116
- ["diff", "diff"]
5764
+ ["diff", "diff"],
5765
+ ["describe", "describe"],
5766
+ ["skill", "skill"]
5117
5767
  ]);
5118
5768
  var COMMAND_ARITY = {
5119
5769
  generate: 0,
@@ -5121,7 +5771,9 @@ var COMMAND_ARITY = {
5121
5771
  report: 0,
5122
5772
  init: 0,
5123
5773
  explain: 1,
5124
- diff: 2
5774
+ diff: 2,
5775
+ describe: 0,
5776
+ skill: 0
5125
5777
  };
5126
5778
  var CLI_OPTIONS = {
5127
5779
  cwd: { type: "string" },
@@ -5143,6 +5795,8 @@ var CLI_OPTIONS = {
5143
5795
  yes: { type: "boolean" },
5144
5796
  minimal: { type: "boolean" },
5145
5797
  force: { type: "boolean" },
5798
+ install: { type: "boolean" },
5799
+ target: { type: "string" },
5146
5800
  help: { type: "boolean", short: "h" },
5147
5801
  version: { type: "boolean", short: "v" }
5148
5802
  };
@@ -5163,6 +5817,8 @@ Commands:
5163
5817
  report Print codebase health, layer metrics, and secret audit
5164
5818
  explain <path> Show how one path would be treated: layer, kind, part, skips
5165
5819
  diff <a> <b> Compare two snapshots (directories or manifest.json files)
5820
+ describe Print the machine-readable contract (--format json for agents)
5821
+ skill Print the bundled agent skill; --install writes it into the repo
5166
5822
 
5167
5823
  Options:
5168
5824
  --cwd <path> Target repository directory (default: current directory)
@@ -5188,11 +5844,17 @@ Options:
5188
5844
  --minimal Write only the settings that differ from defaults
5189
5845
  --yes Accept every default without prompting
5190
5846
  --force Overwrite an existing config
5847
+
5848
+ skill only:
5849
+ --install Write the skill into the repository instead of printing
5850
+ --target <name> claude | agents | cursor | all (default: all)
5851
+
5191
5852
  -h, --help Display this help message
5192
5853
  -v, --version Display version
5193
5854
 
5194
5855
  All options also accept the --option=value form.
5195
5856
  Human-readable progress is written to stderr; payloads (--format json) go to stdout.
5857
+ Agents with no prior knowledge of this tool should start with: describe --format json
5196
5858
  `.trim();
5197
5859
  function parsePositiveInteger(raw, optionName, minimum) {
5198
5860
  if (raw === void 0) return void 0;
@@ -5244,9 +5906,32 @@ function parseCliArgs(argv) {
5244
5906
  strict: true
5245
5907
  });
5246
5908
  } catch (err) {
5247
- throw new UsageError(err.message);
5909
+ const message = err.message;
5910
+ const ambiguous = /^Option '(--[^']+)' argument is ambiguous/i.exec(message);
5911
+ if (ambiguous) {
5912
+ throw new UsageError(
5913
+ `${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.`
5914
+ );
5915
+ }
5916
+ throw new UsageError(message);
5248
5917
  }
5249
5918
  const values = parsed.values;
5919
+ for (const [name, spec] of Object.entries(CLI_OPTIONS)) {
5920
+ if (spec.type !== "string") continue;
5921
+ const raw = values[name];
5922
+ if (raw !== void 0 && String(raw).trim() === "") {
5923
+ throw new UsageError(`--${name} requires a value (received an empty string).`);
5924
+ }
5925
+ }
5926
+ if (values.cwd !== void 0) {
5927
+ const target = String(values.cwd);
5928
+ if (!fs9__namespace.existsSync(target)) {
5929
+ throw new UsageError(`--cwd directory does not exist: ${target}`);
5930
+ }
5931
+ if (!fs9__namespace.statSync(target).isDirectory()) {
5932
+ throw new UsageError(`--cwd is not a directory: ${target}`);
5933
+ }
5934
+ }
5250
5935
  if (values.help) {
5251
5936
  process.stdout.write(`${USAGE}
5252
5937
  `);
@@ -5330,6 +6015,14 @@ function parseCliArgs(argv) {
5330
6015
  const requested = String(values.preset);
5331
6016
  if (!getPreset(requested)) throw new UsageError(unknownPresetMessage(requested));
5332
6017
  }
6018
+ if (command !== "skill" && (values.install !== void 0 || values.target !== void 0)) {
6019
+ throw new UsageError("--install and --target are only valid with `skill`.");
6020
+ }
6021
+ if (values.target !== void 0 && !isSkillTarget(String(values.target))) {
6022
+ throw new UsageError(
6023
+ `--target must be one of: ${SKILL_TARGETS.join(", ")} (received "${String(values.target)}").`
6024
+ );
6025
+ }
5333
6026
  return {
5334
6027
  command,
5335
6028
  args,
@@ -5343,6 +6036,8 @@ function parseCliArgs(argv) {
5343
6036
  yes: values.yes === true,
5344
6037
  minimal: values.minimal === true,
5345
6038
  force: values.force === true,
6039
+ install: values.install === true,
6040
+ target: values.target === void 0 ? void 0 : String(values.target),
5346
6041
  cwd: values.cwd === void 0 ? process.cwd() : path10__namespace.resolve(String(values.cwd)),
5347
6042
  outDir: values["out-dir"] === void 0 ? void 0 : String(values["out-dir"]),
5348
6043
  includeMonolith: values["include-monolith"] === true ? true : void 0,
@@ -5433,6 +6128,9 @@ async function runInitCommand(invocation) {
5433
6128
  for (const note of result.notes) info(` \u26A0 ${note}`);
5434
6129
  info("");
5435
6130
  info(" Next: npm run snapshot");
6131
+ info(
6132
+ ` Tip: \`${GENERATOR_NAME} skill --install\` makes this repo discoverable by AI agents.`
6133
+ );
5436
6134
  return 0;
5437
6135
  } catch (err) {
5438
6136
  if (err instanceof PromptCancelled) {
@@ -5443,6 +6141,31 @@ async function runInitCommand(invocation) {
5443
6141
  return 1;
5444
6142
  }
5445
6143
  }
6144
+ function runSkillCommand(invocation) {
6145
+ if (!invocation.install) {
6146
+ process.stdout.write(renderSkill());
6147
+ return 0;
6148
+ }
6149
+ try {
6150
+ const result = installSkill(invocation.cwd, invocation.target);
6151
+ if (invocation.format === "json") {
6152
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
6153
+ `);
6154
+ return 0;
6155
+ }
6156
+ info(`\u2705 Agent skill installed (${SKILL_NAME}):`);
6157
+ for (const item of result.written) {
6158
+ const shown = path10__namespace.relative(invocation.cwd, item.path) || item.path;
6159
+ info(` - ${item.action.padEnd(9)} ${shown}`);
6160
+ }
6161
+ info("");
6162
+ info(" Agents that read AGENTS.md, .claude/skills or .cursor/rules will pick this up automatically.");
6163
+ return 0;
6164
+ } catch (err) {
6165
+ warn(`\u274C Could not install the skill: ${err.message}`);
6166
+ return 1;
6167
+ }
6168
+ }
5446
6169
  function renderValidateText(result) {
5447
6170
  if (result.valid) {
5448
6171
  info("\u2705 Snapshot is valid and up to date with disk.");
@@ -5464,6 +6187,9 @@ function renderValidateText(result) {
5464
6187
  for (const missing of result.drift.missingOnDisk.slice(0, 20)) {
5465
6188
  warn(` - deleted: ${missing}`);
5466
6189
  }
6190
+ for (const mismatch of result.drift.totalsMismatches.slice(0, 20)) {
6191
+ warn(` - counts: ${mismatch}`);
6192
+ }
5467
6193
  }
5468
6194
  for (const warning of result.warnings) warn(` \u26A0 ${warning}`);
5469
6195
  }
@@ -5485,6 +6211,14 @@ ${USAGE}
5485
6211
  if (invocation.command === "init") {
5486
6212
  return await runInitCommand(invocation);
5487
6213
  }
6214
+ if (invocation.command === "describe") {
6215
+ const contract = buildAgentContract();
6216
+ emit(invocation.format, contract, () => info(renderContractText(contract)));
6217
+ return 0;
6218
+ }
6219
+ if (invocation.command === "skill") {
6220
+ return runSkillCommand(invocation);
6221
+ }
5488
6222
  const loaded = await loadConfigFileDetailed(invocation.cwd);
5489
6223
  for (const warning of loaded.warnings) warn(`\u26A0 Config: ${warning}`);
5490
6224
  if (loaded.errors.length > 0) {
@@ -5594,9 +6328,23 @@ ${USAGE}
5594
6328
  info(
5595
6329
  ` - Total size: ${(manifest.counts.totalSizeBytes / (1024 * 1024)).toFixed(2)} MB`
5596
6330
  );
5597
- info(` - Parts written: ${manifest.counts.partsCount} under ${resolved.outDir}/parts/`);
5598
- info(` - Reports: ${resolved.outDir}/reports/ (project-tree, navigation, audit)`);
5599
- info(` - Manifest: ${resolved.outDir}/manifest.json`);
6331
+ const { artifacts } = resolved;
6332
+ if (artifacts.parts) {
6333
+ info(` - Parts written: ${manifest.counts.partsCount} under ${resolved.outDir}/parts/`);
6334
+ }
6335
+ const reports = [
6336
+ artifacts.projectTree && "project-tree",
6337
+ artifacts.navigation && "navigation",
6338
+ artifacts.audit && "audit"
6339
+ ].filter(Boolean);
6340
+ if (reports.length > 0) {
6341
+ info(` - Reports: ${resolved.outDir}/reports/ (${reports.join(", ")})`);
6342
+ }
6343
+ if (artifacts.manifest) info(` - Manifest: ${resolved.outDir}/manifest.json`);
6344
+ if (artifacts.monolith) info(` - Monolith: ${resolved.outDir}/full-codebase.{json,md}`);
6345
+ if (reports.length === 0 && !artifacts.parts && !artifacts.manifest && !artifacts.monolith) {
6346
+ info(` - Artifacts: all disabled (only the ownership marker was written)`);
6347
+ }
5600
6348
  info(` - Fingerprint: ${manifest.fingerprint}`);
5601
6349
  info(` - Integrity: ${manifest.validation.status}`);
5602
6350
  if (manifest.warnings.length > 0) {
@@ -5613,6 +6361,14 @@ ${USAGE}
5613
6361
  }
5614
6362
  return manifest.validation.status === "passed" ? 0 : 1;
5615
6363
  } catch (err) {
6364
+ const code = err.code;
6365
+ if (code === "EPERM" || code === "EACCES") {
6366
+ warn(
6367
+ `\u274C Cannot write the snapshot: permission denied under ${resolved.outDir}. Check the directory's permissions or choose another --out-dir.`
6368
+ );
6369
+ warn(` (${err.message})`);
6370
+ return 1;
6371
+ }
5616
6372
  warn(`\u274C Failed to generate snapshot: ${err.message}`);
5617
6373
  return 1;
5618
6374
  }