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