@ankurhalder/codebase-snapshot 0.0.0-stage → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.d.cts ADDED
@@ -0,0 +1,48 @@
1
+ import { O as OutputFormat } from './types-CJqkePM4.cjs';
2
+
3
+ type Command = "generate" | "validate" | "report" | "init" | "explain" | "diff";
4
+ /** A user-input problem: printed without a stack trace, exits 2. */
5
+ declare class UsageError extends Error {
6
+ }
7
+ interface ParsedInvocation {
8
+ command: Command;
9
+ /** Positional arguments that follow the command name. */
10
+ args: string[];
11
+ format: OutputFormat;
12
+ dryRun: boolean;
13
+ watch: boolean;
14
+ preset?: string;
15
+ /** The size exactly as typed, so the generated config can keep `120k`. */
16
+ maxPartBytesRaw?: string;
17
+ include?: string;
18
+ exclude?: string;
19
+ yes: boolean;
20
+ minimal: boolean;
21
+ force: boolean;
22
+ cwd: string;
23
+ outDir?: string;
24
+ includeMonolith?: boolean;
25
+ includeSensitiveFiles?: boolean;
26
+ maxPartBytes?: number;
27
+ largeFileBytes?: number;
28
+ maxMonolithBytes?: number | null;
29
+ skipArtifacts?: string[];
30
+ }
31
+ declare function parsePositiveInteger(raw: string | undefined, optionName: string, minimum: number): number | undefined;
32
+ /**
33
+ * Parses a byte-budget option, accepting human-readable units (`120k`, `1.5mb`)
34
+ * as well as raw byte counts.
35
+ */
36
+ declare function parseByteOption(raw: string | undefined, optionName: string, minimum: number): number | undefined;
37
+ /**
38
+ * Parses argv.
39
+ *
40
+ * Uses `node:util.parseArgs` in strict mode, so `--option=value` works, unknown
41
+ * flags are rejected instead of silently ignored, and a typo can no longer fall
42
+ * through to the writable `generate` command.
43
+ */
44
+ declare function parseCliArgs(argv: string[]): ParsedInvocation;
45
+ /** Runs the CLI and returns the process exit code. */
46
+ declare function main(argv?: string[]): Promise<number>;
47
+
48
+ export { UsageError, main, parseByteOption, parseCliArgs, parsePositiveInteger };
package/dist/cli.d.ts ADDED
@@ -0,0 +1,48 @@
1
+ import { O as OutputFormat } from './types-CJqkePM4.js';
2
+
3
+ type Command = "generate" | "validate" | "report" | "init" | "explain" | "diff";
4
+ /** A user-input problem: printed without a stack trace, exits 2. */
5
+ declare class UsageError extends Error {
6
+ }
7
+ interface ParsedInvocation {
8
+ command: Command;
9
+ /** Positional arguments that follow the command name. */
10
+ args: string[];
11
+ format: OutputFormat;
12
+ dryRun: boolean;
13
+ watch: boolean;
14
+ preset?: string;
15
+ /** The size exactly as typed, so the generated config can keep `120k`. */
16
+ maxPartBytesRaw?: string;
17
+ include?: string;
18
+ exclude?: string;
19
+ yes: boolean;
20
+ minimal: boolean;
21
+ force: boolean;
22
+ cwd: string;
23
+ outDir?: string;
24
+ includeMonolith?: boolean;
25
+ includeSensitiveFiles?: boolean;
26
+ maxPartBytes?: number;
27
+ largeFileBytes?: number;
28
+ maxMonolithBytes?: number | null;
29
+ skipArtifacts?: string[];
30
+ }
31
+ declare function parsePositiveInteger(raw: string | undefined, optionName: string, minimum: number): number | undefined;
32
+ /**
33
+ * Parses a byte-budget option, accepting human-readable units (`120k`, `1.5mb`)
34
+ * as well as raw byte counts.
35
+ */
36
+ declare function parseByteOption(raw: string | undefined, optionName: string, minimum: number): number | undefined;
37
+ /**
38
+ * Parses argv.
39
+ *
40
+ * Uses `node:util.parseArgs` in strict mode, so `--option=value` works, unknown
41
+ * flags are rejected instead of silently ignored, and a typo can no longer fall
42
+ * through to the writable `generate` command.
43
+ */
44
+ declare function parseCliArgs(argv: string[]): ParsedInvocation;
45
+ /** Runs the CLI and returns the process exit code. */
46
+ declare function main(argv?: string[]): Promise<number>;
47
+
48
+ export { UsageError, main, parseByteOption, parseCliArgs, parsePositiveInteger };
package/dist/cli.js ADDED
@@ -0,0 +1,568 @@
1
+ import { parseByteSize, GENERATOR_VERSION, ARTIFACT_NAMES, getPreset, unknownPresetMessage, MIN_MAX_PART_BYTES, loadConfigFileDetailed, resolveConfig, validateSnapshot, explainPath, compareManifests, loadManifest, renderDiffText, generateSnapshot, GENERATOR_NAME, runInit, PromptCancelled, renderExplainText } from './chunk-QEIOXPMU.js';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { parseArgs } from 'util';
5
+
6
+ var VERSION = GENERATOR_VERSION;
7
+ var COMMAND_ALIASES = /* @__PURE__ */ new Map([
8
+ ["generate", "generate"],
9
+ ["validate", "validate"],
10
+ ["check", "validate"],
11
+ ["report", "report"],
12
+ ["init", "init"],
13
+ ["explain", "explain"],
14
+ ["diff", "diff"]
15
+ ]);
16
+ var COMMAND_ARITY = {
17
+ generate: 0,
18
+ validate: 0,
19
+ report: 0,
20
+ init: 0,
21
+ explain: 1,
22
+ diff: 2
23
+ };
24
+ var CLI_OPTIONS = {
25
+ cwd: { type: "string" },
26
+ "out-dir": { type: "string" },
27
+ "max-part-bytes": { type: "string" },
28
+ "large-file-bytes": { type: "string" },
29
+ "max-monolith-bytes": { type: "string" },
30
+ "include-monolith": { type: "boolean" },
31
+ "include-sensitive-files": { type: "boolean" },
32
+ "skip-artifacts": { type: "string" },
33
+ "only-artifacts": { type: "string" },
34
+ format: { type: "string" },
35
+ mode: { type: "string" },
36
+ "dry-run": { type: "boolean" },
37
+ watch: { type: "boolean" },
38
+ preset: { type: "string" },
39
+ include: { type: "string" },
40
+ exclude: { type: "string" },
41
+ yes: { type: "boolean" },
42
+ minimal: { type: "boolean" },
43
+ force: { type: "boolean" },
44
+ help: { type: "boolean", short: "h" },
45
+ version: { type: "boolean", short: "v" }
46
+ };
47
+ var UsageError = class extends Error {
48
+ };
49
+ var USAGE = `
50
+ ${GENERATOR_NAME} v${VERSION}
51
+ Production-grade LLM codebase snapshot generator, auditor, and AST partitioner
52
+
53
+ Usage:
54
+ codebase-snapshot [command] [options]
55
+ snap [command] [options]
56
+
57
+ Commands:
58
+ generate (default) Generate a complete partitioned codebase snapshot
59
+ validate, check Validate snapshot integrity and check for drift
60
+ init Generate a starter snapshot.config file
61
+ report Print codebase health, layer metrics, and secret audit
62
+ explain <path> Show how one path would be treated: layer, kind, part, skips
63
+ diff <a> <b> Compare two snapshots (directories or manifest.json files)
64
+
65
+ Options:
66
+ --cwd <path> Target repository directory (default: current directory)
67
+ --out-dir <path> Snapshot output directory (default: snapshot)
68
+ --include-monolith Also generate full-codebase.json and full-codebase.md
69
+ --max-monolith-bytes <n> Skip monolith artefacts larger than n bytes
70
+ --max-part-bytes <n> Maximum byte budget per markdown part (default: 307200).
71
+ Accepts units: 120k, 1.5mb
72
+ --large-file-bytes <n> Threshold for head/tail truncation (default: 204800)
73
+ --include-sensitive-files Emit .env / *.pem / .npmrc / credential files
74
+ --skip-artifacts <names> Comma-separated artifacts to omit
75
+ (readme, manifest, parts, navigation,
76
+ projectTree, audit, monolith)
77
+ --only-artifacts <names> Write *only* these artifacts
78
+ --format <text|json> CLI output format (default: text)
79
+ --dry-run Compute the snapshot without writing anything
80
+ --watch Re-generate whenever the repository changes
81
+
82
+ init only:
83
+ --preset <name> Force a project preset instead of detecting one
84
+ --include <globs> Comma-separated allow-list
85
+ --exclude <globs> Comma-separated exclude patterns
86
+ --minimal Write only the settings that differ from defaults
87
+ --yes Accept every default without prompting
88
+ --force Overwrite an existing config
89
+ -h, --help Display this help message
90
+ -v, --version Display version
91
+
92
+ All options also accept the --option=value form.
93
+ Human-readable progress is written to stderr; payloads (--format json) go to stdout.
94
+ `.trim();
95
+ function parsePositiveInteger(raw, optionName, minimum) {
96
+ if (raw === void 0) return void 0;
97
+ const trimmed = raw.trim();
98
+ if (!/^\d+$/.test(trimmed)) {
99
+ throw new UsageError(`--${optionName} expects a whole number, received "${raw}".`);
100
+ }
101
+ const value = Number(trimmed);
102
+ if (!Number.isSafeInteger(value) || value < minimum) {
103
+ throw new UsageError(`--${optionName} must be at least ${minimum}, received "${raw}".`);
104
+ }
105
+ return value;
106
+ }
107
+ function parseByteOption(raw, optionName, minimum) {
108
+ if (raw === void 0) return void 0;
109
+ const value = parseByteSize(raw);
110
+ if (value === void 0 || !Number.isSafeInteger(value)) {
111
+ throw new UsageError(
112
+ `--${optionName} expects a byte count such as 120000 or "120k", received "${raw}".`
113
+ );
114
+ }
115
+ if (value < minimum) {
116
+ throw new UsageError(`--${optionName} must be at least ${minimum}, received "${raw}".`);
117
+ }
118
+ return value;
119
+ }
120
+ function parseArtifactList(raw, flag) {
121
+ const names = raw.split(",").map((name) => name.trim()).filter(Boolean);
122
+ if (names.length === 0) {
123
+ throw new UsageError(
124
+ `${flag} expects at least one artifact name (${ARTIFACT_NAMES.join(", ")}).`
125
+ );
126
+ }
127
+ const unknown = names.filter((name) => !ARTIFACT_NAMES.includes(name));
128
+ if (unknown.length > 0) {
129
+ throw new UsageError(
130
+ `Unknown artifact(s): ${unknown.join(", ")}. Expected one of: ${ARTIFACT_NAMES.join(", ")}.`
131
+ );
132
+ }
133
+ return names;
134
+ }
135
+ function parseCliArgs(argv) {
136
+ let parsed;
137
+ try {
138
+ parsed = parseArgs({
139
+ args: argv,
140
+ options: CLI_OPTIONS,
141
+ allowPositionals: true,
142
+ strict: true
143
+ });
144
+ } catch (err) {
145
+ throw new UsageError(err.message);
146
+ }
147
+ const values = parsed.values;
148
+ if (values.help) {
149
+ process.stdout.write(`${USAGE}
150
+ `);
151
+ throw new HelpRequested();
152
+ }
153
+ if (values.version) {
154
+ process.stdout.write(`v${VERSION}
155
+ `);
156
+ throw new HelpRequested();
157
+ }
158
+ const positionals = parsed.positionals;
159
+ let command = "generate";
160
+ let args = [];
161
+ const explicitMode = values.mode ? String(values.mode) : void 0;
162
+ const firstPositional = positionals[0];
163
+ if (firstPositional !== void 0) {
164
+ const resolved = COMMAND_ALIASES.get(firstPositional);
165
+ if (!resolved) {
166
+ throw new UsageError(
167
+ `Unknown command "${firstPositional}". Expected one of: ${Array.from(COMMAND_ALIASES.keys()).join(", ")}.`
168
+ );
169
+ }
170
+ command = resolved;
171
+ args = positionals.slice(1);
172
+ } else if (explicitMode !== void 0) {
173
+ const resolved = COMMAND_ALIASES.get(explicitMode);
174
+ if (!resolved) {
175
+ throw new UsageError(
176
+ `Unknown --mode "${explicitMode}". Expected one of: ${Array.from(COMMAND_ALIASES.keys()).join(", ")}.`
177
+ );
178
+ }
179
+ command = resolved;
180
+ }
181
+ const expectedArgs = COMMAND_ARITY[command];
182
+ if (args.length !== expectedArgs) {
183
+ if (command === "explain") {
184
+ throw new UsageError("explain expects exactly one path.");
185
+ }
186
+ if (command === "diff") {
187
+ throw new UsageError(
188
+ "diff expects exactly two arguments: two snapshot directories or manifest.json files."
189
+ );
190
+ }
191
+ throw new UsageError(
192
+ `Unexpected argument(s): ${args.join(" ")}. ${command} takes no positional arguments.`
193
+ );
194
+ }
195
+ const rawFormat = values.format === void 0 ? "text" : String(values.format);
196
+ if (rawFormat !== "text" && rawFormat !== "json") {
197
+ throw new UsageError(`--format must be "text" or "json", received "${rawFormat}".`);
198
+ }
199
+ const rawSkip = values["skip-artifacts"];
200
+ const rawOnly = values["only-artifacts"];
201
+ if (rawSkip !== void 0 && rawOnly !== void 0) {
202
+ throw new UsageError("--skip-artifacts and --only-artifacts cannot be combined.");
203
+ }
204
+ let skipArtifacts;
205
+ if (rawOnly !== void 0) {
206
+ const keep = parseArtifactList(String(rawOnly), "--only-artifacts");
207
+ skipArtifacts = ARTIFACT_NAMES.filter(
208
+ (name) => !keep.includes(name)
209
+ );
210
+ } else if (rawSkip !== void 0) {
211
+ skipArtifacts = parseArtifactList(String(rawSkip), "--skip-artifacts");
212
+ }
213
+ if (command !== "init") {
214
+ const initOnly = [];
215
+ if (values.preset !== void 0) initOnly.push("--preset");
216
+ if (values.include !== void 0) initOnly.push("--include");
217
+ if (values.exclude !== void 0) initOnly.push("--exclude");
218
+ if (values.yes === true) initOnly.push("--yes");
219
+ if (values.minimal === true) initOnly.push("--minimal");
220
+ if (values.force === true) initOnly.push("--force");
221
+ if (initOnly.length > 0) {
222
+ throw new UsageError(
223
+ `${initOnly.join(", ")} ${initOnly.length === 1 ? "is" : "are"} only valid with \`init\`. To scope \`${command}\`, set include/excludePatterns/preset in snapshot.config.ts (or run \`snap init\` to generate one).`
224
+ );
225
+ }
226
+ }
227
+ if (command === "init" && values.preset !== void 0) {
228
+ const requested = String(values.preset);
229
+ if (!getPreset(requested)) throw new UsageError(unknownPresetMessage(requested));
230
+ }
231
+ return {
232
+ command,
233
+ args,
234
+ format: rawFormat,
235
+ dryRun: values["dry-run"] === true,
236
+ watch: values.watch === true,
237
+ preset: values.preset === void 0 ? void 0 : String(values.preset),
238
+ maxPartBytesRaw: values["max-part-bytes"] === void 0 ? void 0 : String(values["max-part-bytes"]),
239
+ include: values.include === void 0 ? void 0 : String(values.include),
240
+ exclude: values.exclude === void 0 ? void 0 : String(values.exclude),
241
+ yes: values.yes === true,
242
+ minimal: values.minimal === true,
243
+ force: values.force === true,
244
+ cwd: values.cwd === void 0 ? process.cwd() : path.resolve(String(values.cwd)),
245
+ outDir: values["out-dir"] === void 0 ? void 0 : String(values["out-dir"]),
246
+ includeMonolith: values["include-monolith"] === true ? true : void 0,
247
+ includeSensitiveFiles: values["include-sensitive-files"] === true ? true : void 0,
248
+ maxPartBytes: parseByteOption(
249
+ values["max-part-bytes"],
250
+ "max-part-bytes",
251
+ MIN_MAX_PART_BYTES
252
+ ),
253
+ largeFileBytes: parseByteOption(
254
+ values["large-file-bytes"],
255
+ "large-file-bytes",
256
+ 1
257
+ ),
258
+ maxMonolithBytes: parseByteOption(
259
+ values["max-monolith-bytes"],
260
+ "max-monolith-bytes",
261
+ 1
262
+ ),
263
+ skipArtifacts
264
+ };
265
+ }
266
+ var HelpRequested = class extends Error {
267
+ };
268
+ function info(message) {
269
+ process.stderr.write(`${message}
270
+ `);
271
+ }
272
+ function warn(message) {
273
+ process.stderr.write(`${message}
274
+ `);
275
+ }
276
+ function emit(format, payload, textRenderer) {
277
+ if (format === "json") {
278
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}
279
+ `);
280
+ return;
281
+ }
282
+ textRenderer();
283
+ }
284
+ function splitList(value) {
285
+ if (value === void 0) return void 0;
286
+ return value.split(",").map((entry) => entry.trim()).filter(Boolean);
287
+ }
288
+ function describeManifestShapeProblem(manifest) {
289
+ if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
290
+ return "not a JSON object";
291
+ }
292
+ const candidate = manifest;
293
+ const counts = candidate.counts;
294
+ if (!counts || typeof counts !== "object") return "missing `counts`";
295
+ for (const key of ["totalFiles", "totalLines", "totalSizeBytes", "partsCount"]) {
296
+ if (typeof counts[key] !== "number") return `missing numeric \`counts.${key}\``;
297
+ }
298
+ if (!candidate.layers || typeof candidate.layers !== "object") return "missing `layers`";
299
+ if (!Array.isArray(candidate.files)) return "missing `files` array";
300
+ return null;
301
+ }
302
+ async function runInitCommand(invocation) {
303
+ try {
304
+ const result = await runInit({
305
+ cwd: invocation.cwd,
306
+ preset: invocation.preset,
307
+ maxPartBytes: invocation.maxPartBytesRaw ?? invocation.maxPartBytes,
308
+ include: splitList(invocation.include),
309
+ exclude: splitList(invocation.exclude),
310
+ skipArtifacts: invocation.skipArtifacts,
311
+ includeSensitiveFiles: invocation.includeSensitiveFiles,
312
+ yes: invocation.yes,
313
+ minimal: invocation.minimal,
314
+ force: invocation.force,
315
+ dryRun: invocation.dryRun
316
+ });
317
+ if (invocation.format === "json") {
318
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
319
+ `);
320
+ return 0;
321
+ }
322
+ if (!result.written) {
323
+ info(result.configText);
324
+ return 0;
325
+ }
326
+ info(`\u2705 Created ${path.basename(result.configPath)} (preset: ${result.preset})`);
327
+ if (result.scripts.length > 0) info(` Added npm scripts: ${result.scripts.join(", ")}`);
328
+ if (result.gitignoreEntries.length > 0) {
329
+ info(` Added to .gitignore: ${result.gitignoreEntries.join(", ")}`);
330
+ }
331
+ for (const note of result.notes) info(` \u26A0 ${note}`);
332
+ info("");
333
+ info(" Next: npm run snapshot");
334
+ return 0;
335
+ } catch (err) {
336
+ if (err instanceof PromptCancelled) {
337
+ warn("\u274C Cancelled.");
338
+ return 1;
339
+ }
340
+ warn(`\u274C ${err.message}`);
341
+ return 1;
342
+ }
343
+ }
344
+ function renderValidateText(result) {
345
+ if (result.valid) {
346
+ info("\u2705 Snapshot is valid and up to date with disk.");
347
+ } else {
348
+ warn("\u274C Snapshot validation failed:");
349
+ for (const error of result.errors) warn(` - ${error}`);
350
+ for (const drift of result.drift.hashMismatches.slice(0, 20)) {
351
+ warn(` - modified: ${drift.path}`);
352
+ }
353
+ for (const issue of result.drift.partIssues.slice(0, 20)) {
354
+ warn(` - part: ${issue.part} (${issue.issue})`);
355
+ }
356
+ for (const mismatch of result.drift.configMismatches.slice(0, 20)) {
357
+ warn(` - config: ${mismatch}`);
358
+ }
359
+ for (const missing of result.drift.missingInSnapshot.slice(0, 20)) {
360
+ warn(` - new file: ${missing}`);
361
+ }
362
+ for (const missing of result.drift.missingOnDisk.slice(0, 20)) {
363
+ warn(` - deleted: ${missing}`);
364
+ }
365
+ }
366
+ for (const warning of result.warnings) warn(` \u26A0 ${warning}`);
367
+ }
368
+ async function main(argv = process.argv.slice(2)) {
369
+ let invocation;
370
+ try {
371
+ invocation = parseCliArgs(argv);
372
+ } catch (err) {
373
+ if (err instanceof HelpRequested) return 0;
374
+ if (err instanceof UsageError) {
375
+ process.stderr.write(`\u274C ${err.message}
376
+
377
+ ${USAGE}
378
+ `);
379
+ return 2;
380
+ }
381
+ throw err;
382
+ }
383
+ if (invocation.command === "init") {
384
+ return await runInitCommand(invocation);
385
+ }
386
+ const loaded = await loadConfigFileDetailed(invocation.cwd);
387
+ for (const warning of loaded.warnings) warn(`\u26A0 Config: ${warning}`);
388
+ if (loaded.errors.length > 0) {
389
+ warn("\u274C The config file could not be read. Fix it, or delete it to use the defaults.");
390
+ return 1;
391
+ }
392
+ if (loaded.file) info(`\u2699 Using config ${path.relative(process.cwd(), loaded.file) || loaded.file}`);
393
+ const resolved = resolveConfig(
394
+ loaded.config,
395
+ {
396
+ mode: invocation.command,
397
+ format: invocation.format,
398
+ dryRun: invocation.dryRun,
399
+ cwd: invocation.cwd,
400
+ outDir: invocation.outDir,
401
+ includeMonolith: invocation.includeMonolith,
402
+ includeSensitiveFiles: invocation.includeSensitiveFiles,
403
+ maxPartBytes: invocation.maxPartBytes,
404
+ largeFileBytes: invocation.largeFileBytes,
405
+ maxMonolithBytes: invocation.maxMonolithBytes,
406
+ skipArtifacts: invocation.skipArtifacts
407
+ },
408
+ invocation.cwd
409
+ );
410
+ if (invocation.command === "validate") {
411
+ const result = validateSnapshot(resolved);
412
+ emit(invocation.format, result, () => renderValidateText(result));
413
+ return result.valid ? 0 : 1;
414
+ }
415
+ if (invocation.command === "report") {
416
+ const manifestPath = path.resolve(resolved.cwd, resolved.outDir, "manifest.json");
417
+ if (!fs.existsSync(manifestPath)) {
418
+ warn(`\u274C Manifest not found at ${manifestPath}. Run generate first.`);
419
+ return 1;
420
+ }
421
+ let manifest;
422
+ try {
423
+ manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
424
+ } catch (err) {
425
+ warn(`\u274C Could not read ${manifestPath}: ${err.message}`);
426
+ return 1;
427
+ }
428
+ const shapeError = describeManifestShapeProblem(manifest);
429
+ if (shapeError) {
430
+ warn(`\u274C ${manifestPath} is not a snapshot manifest: ${shapeError}`);
431
+ return 1;
432
+ }
433
+ const validation = validateSnapshot(resolved);
434
+ emit(invocation.format, { manifest, validation }, () => {
435
+ info(`
436
+ \u{1F4CA} Codebase Snapshot Report (${resolved.cwd})`);
437
+ info(` - Generated at: ${manifest.generatedAt}`);
438
+ info(` - Schema: ${manifest.schemaVersion}`);
439
+ info(` - Fingerprint: ${manifest.fingerprint}`);
440
+ info(` - Files included: ${manifest.counts.totalFiles}`);
441
+ info(` - Total lines: ${manifest.counts.totalLines.toLocaleString()}`);
442
+ info(
443
+ ` - Total size: ${(manifest.counts.totalSizeBytes / (1024 * 1024)).toFixed(2)} MB`
444
+ );
445
+ info(` - Total parts: ${manifest.counts.partsCount}`);
446
+ info(` - Integrity: ${validation.valid ? "\u2705 Clean (no drift)" : "\u274C Drift detected"}`);
447
+ info(` - Warnings: ${manifest.warnings?.length ?? 0}`);
448
+ info("\n Layers:");
449
+ for (const [layer, count] of Object.entries(manifest.layers)) {
450
+ info(` - ${layer.padEnd(16)}: ${count} files`);
451
+ }
452
+ });
453
+ return validation.valid ? 0 : 1;
454
+ }
455
+ if (invocation.command === "explain") {
456
+ const result = explainPath(resolved, invocation.args[0]);
457
+ emit(invocation.format, result, () => info(renderExplainText(result)));
458
+ return result.included ? 0 : 1;
459
+ }
460
+ if (invocation.command === "diff") {
461
+ try {
462
+ const [beforePath, afterPath] = invocation.args;
463
+ const diff = compareManifests(loadManifest(beforePath), loadManifest(afterPath));
464
+ emit(invocation.format, diff, () => info(renderDiffText(diff)));
465
+ return 0;
466
+ } catch (err) {
467
+ warn(`\u274C ${err.message}`);
468
+ return 1;
469
+ }
470
+ }
471
+ const started = performance.now();
472
+ info(`\u{1F4F8} ${invocation.dryRun ? "Planning snapshot (dry run)" : "Generating snapshot"} for: ${resolved.cwd}`);
473
+ info(
474
+ ` Branch: ${resolved.branch} | Part budget: ${(resolved.maxPartBytes / 1024).toFixed(0)} KB`
475
+ );
476
+ try {
477
+ const manifest = generateSnapshot(resolved, { dryRun: invocation.dryRun });
478
+ const elapsed = ((performance.now() - started) / 1e3).toFixed(2);
479
+ if (invocation.dryRun) {
480
+ info("\n\u{1F9EA} Dry run \u2014 nothing was written:");
481
+ info(` - Would track: ${manifest.counts.totalFiles} files`);
482
+ info(` - Would write: ${manifest.counts.partsCount} part(s) under ${resolved.outDir}/parts/`);
483
+ info(` - Skipped: ${manifest.counts.skippedCount} file(s)`);
484
+ info(` - Warnings: ${manifest.warnings.length}`);
485
+ info(` - Integrity: ${manifest.validation.status}`);
486
+ for (const error of manifest.validation.errors.slice(0, 10)) info(` - ${error}`);
487
+ } else {
488
+ info(`
489
+ \u2705 Snapshot generated in ${elapsed}s:`);
490
+ info(` - Tracked files: ${manifest.counts.totalFiles}`);
491
+ info(` - Total lines: ${manifest.counts.totalLines.toLocaleString()}`);
492
+ info(
493
+ ` - Total size: ${(manifest.counts.totalSizeBytes / (1024 * 1024)).toFixed(2)} MB`
494
+ );
495
+ info(` - Parts written: ${manifest.counts.partsCount} under ${resolved.outDir}/parts/`);
496
+ info(` - Reports: ${resolved.outDir}/reports/ (project-tree, navigation, audit)`);
497
+ info(` - Manifest: ${resolved.outDir}/manifest.json`);
498
+ info(` - Fingerprint: ${manifest.fingerprint}`);
499
+ info(` - Integrity: ${manifest.validation.status}`);
500
+ if (manifest.warnings.length > 0) {
501
+ info(` - Warnings: ${manifest.warnings.length}`);
502
+ for (const warning of manifest.warnings.slice(0, 10)) info(` - ${warning}`);
503
+ }
504
+ }
505
+ if (invocation.format === "json") {
506
+ process.stdout.write(`${JSON.stringify(manifest, null, 2)}
507
+ `);
508
+ }
509
+ if (invocation.watch) {
510
+ return await runWatchLoop(invocation, resolved);
511
+ }
512
+ return manifest.validation.status === "passed" ? 0 : 1;
513
+ } catch (err) {
514
+ warn(`\u274C Failed to generate snapshot: ${err.message}`);
515
+ return 1;
516
+ }
517
+ }
518
+ async function runWatchLoop(invocation, resolved) {
519
+ const outDir = path.resolve(resolved.cwd, resolved.outDir);
520
+ const tmpDir = path.resolve(resolved.cwd, resolved.tmpDir);
521
+ const outName = path.basename(outDir).toLowerCase();
522
+ const tmpName = path.basename(tmpDir).toLowerCase();
523
+ const isGenerated = (candidate) => candidate.toLowerCase().split(/[\\/]/).some(
524
+ (segment) => segment === outName || segment === tmpName || segment.startsWith(`${outName}.backup-`)
525
+ );
526
+ let running = false;
527
+ const regenerate = () => {
528
+ if (running) return;
529
+ running = true;
530
+ const started = performance.now();
531
+ try {
532
+ const manifest = generateSnapshot(resolved, { dryRun: invocation.dryRun });
533
+ const elapsed = ((performance.now() - started) / 1e3).toFixed(2);
534
+ info(
535
+ `\u267B\uFE0F ${(/* @__PURE__ */ new Date()).toISOString()} ${manifest.counts.totalFiles} file(s), ${manifest.counts.partsCount} part(s), ${manifest.validation.status} (${elapsed}s)`
536
+ );
537
+ } catch (err) {
538
+ warn(`\u274C ${err.message}`);
539
+ } finally {
540
+ running = false;
541
+ }
542
+ };
543
+ info("\u{1F440} Watching for changes. Press Ctrl+C to stop.");
544
+ regenerate();
545
+ let timer = null;
546
+ const watcher = fs.watch(resolved.cwd, { recursive: true }, (_event, filename) => {
547
+ if (filename) {
548
+ const changed = path.resolve(resolved.cwd, filename.toString());
549
+ if (isGenerated(changed)) return;
550
+ }
551
+ if (timer) clearTimeout(timer);
552
+ timer = setTimeout(regenerate, 250);
553
+ });
554
+ await new Promise((resolve2) => {
555
+ const stop = () => {
556
+ if (timer) clearTimeout(timer);
557
+ watcher.close();
558
+ resolve2();
559
+ };
560
+ process.once("SIGINT", stop);
561
+ process.once("SIGTERM", stop);
562
+ });
563
+ return 0;
564
+ }
565
+
566
+ export { UsageError, main, parseByteOption, parseCliArgs, parsePositiveInteger };
567
+ //# sourceMappingURL=cli.js.map
568
+ //# sourceMappingURL=cli.js.map