@eslint-config-snapshot/cli 0.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.
Files changed (28) hide show
  1. package/dist/index.cjs +720 -0
  2. package/dist/index.js +697 -0
  3. package/package.json +26 -0
  4. package/project.json +35 -0
  5. package/src/index.ts +861 -0
  6. package/test/cli.integration.test.ts +247 -0
  7. package/test/cli.npm-isolated.integration.test.ts +109 -0
  8. package/test/cli.pnpm-isolated.integration.test.ts +140 -0
  9. package/test/cli.terminal.integration.test.ts +370 -0
  10. package/test/fixtures/npm-isolated-template/eslint-config-snapshot.config.mjs +16 -0
  11. package/test/fixtures/npm-isolated-template/package.json +7 -0
  12. package/test/fixtures/npm-isolated-template/packages/ws-a/.eslintrc.cjs +7 -0
  13. package/test/fixtures/npm-isolated-template/packages/ws-a/package.json +7 -0
  14. package/test/fixtures/npm-isolated-template/packages/ws-a/src/index.ts +1 -0
  15. package/test/fixtures/npm-isolated-template/packages/ws-b/.eslintrc.cjs +7 -0
  16. package/test/fixtures/npm-isolated-template/packages/ws-b/package.json +7 -0
  17. package/test/fixtures/npm-isolated-template/packages/ws-b/src/index.ts +1 -0
  18. package/test/fixtures/repo/eslint-config-snapshot.config.mjs +16 -0
  19. package/test/fixtures/repo/package.json +7 -0
  20. package/test/fixtures/repo/packages/ws-a/node_modules/eslint/bin/eslint.js +1 -0
  21. package/test/fixtures/repo/packages/ws-a/node_modules/eslint/package.json +4 -0
  22. package/test/fixtures/repo/packages/ws-a/package.json +4 -0
  23. package/test/fixtures/repo/packages/ws-a/src/index.ts +1 -0
  24. package/test/fixtures/repo/packages/ws-b/node_modules/eslint/bin/eslint.js +1 -0
  25. package/test/fixtures/repo/packages/ws-b/node_modules/eslint/package.json +4 -0
  26. package/test/fixtures/repo/packages/ws-b/package.json +4 -0
  27. package/test/fixtures/repo/packages/ws-b/src/index.ts +1 -0
  28. package/tsconfig.json +12 -0
package/dist/index.cjs ADDED
@@ -0,0 +1,720 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/index.ts
32
+ var index_exports = {};
33
+ __export(index_exports, {
34
+ main: () => main,
35
+ parseInitPresetChoice: () => parseInitPresetChoice,
36
+ parseInitTargetChoice: () => parseInitTargetChoice,
37
+ runCli: () => runCli
38
+ });
39
+ module.exports = __toCommonJS(index_exports);
40
+ var import_api = require("@eslint-config-snapshot/api");
41
+ var import_commander = require("commander");
42
+ var import_fast_glob = __toESM(require("fast-glob"), 1);
43
+ var import_promises = require("fs/promises");
44
+ var import_node_path = __toESM(require("path"), 1);
45
+ var import_node_readline = require("readline");
46
+ var SNAPSHOT_DIR = ".eslint-config-snapshot";
47
+ var UPDATE_HINT = "Tip: when you intentionally accept changes, run `eslint-config-snapshot --update` to refresh the baseline.\n";
48
+ async function runCli(command, cwd, flags = []) {
49
+ const argv = command ? [command, ...flags] : [...flags];
50
+ return runArgv(argv, cwd);
51
+ }
52
+ async function runArgv(argv, cwd) {
53
+ const hasCommandToken = argv.some((token) => !token.startsWith("-"));
54
+ if (!hasCommandToken) {
55
+ return runDefaultInvocation(argv, cwd);
56
+ }
57
+ let actionCode;
58
+ const program = createProgram(cwd, (code) => {
59
+ actionCode = code;
60
+ });
61
+ try {
62
+ await program.parseAsync(argv, { from: "user" });
63
+ } catch (error) {
64
+ if (error instanceof import_commander.CommanderError) {
65
+ if (error.code === "commander.helpDisplayed") {
66
+ return 0;
67
+ }
68
+ return error.exitCode;
69
+ }
70
+ const message = error instanceof Error ? error.message : String(error);
71
+ process.stderr.write(`${message}
72
+ `);
73
+ return 1;
74
+ }
75
+ return actionCode ?? 0;
76
+ }
77
+ async function runDefaultInvocation(argv, cwd) {
78
+ const known = /* @__PURE__ */ new Set(["-u", "--update", "-h", "--help"]);
79
+ for (const token of argv) {
80
+ if (!known.has(token)) {
81
+ process.stderr.write(`error: unknown option '${token}'
82
+ `);
83
+ return 1;
84
+ }
85
+ }
86
+ if (argv.includes("-h") || argv.includes("--help")) {
87
+ const program = createProgram(cwd, () => {
88
+ });
89
+ program.outputHelp();
90
+ return 0;
91
+ }
92
+ if (argv.includes("-u") || argv.includes("--update")) {
93
+ return executeUpdate(cwd, true);
94
+ }
95
+ return executeCheck(cwd, "summary", true);
96
+ }
97
+ function createProgram(cwd, onActionExit) {
98
+ const program = new import_commander.Command();
99
+ program.name("eslint-config-snapshot").description("Deterministic ESLint config snapshot drift checker for workspaces").showHelpAfterError("(add --help for usage)").option("-u, --update", "Update snapshots (default mode only)");
100
+ program.hook("preAction", (thisCommand) => {
101
+ const opts = thisCommand.opts();
102
+ if (opts.update) {
103
+ throw new Error("--update can only be used without a command");
104
+ }
105
+ });
106
+ program.command("check").description("Compare current state against stored snapshots").option("--format <format>", "Output format: summary|status|diff", parseCheckFormat, "summary").action(async (opts) => {
107
+ onActionExit(await executeCheck(cwd, opts.format));
108
+ });
109
+ program.command("update").alias("snapshot").description("Compute and write snapshots to .eslint-config-snapshot/").action(async () => {
110
+ onActionExit(await executeUpdate(cwd, true));
111
+ });
112
+ program.command("print").description("Print aggregated rules").option("--format <format>", "Output format: json|short", parsePrintFormat, "json").option("--short", "Alias for --format short").action(async (opts) => {
113
+ const format = opts.short ? "short" : opts.format;
114
+ await executePrint(cwd, format);
115
+ onActionExit(0);
116
+ });
117
+ program.command("init").description("Initialize config (file or package.json)").option("--target <target>", "Config target: file|package-json", parseInitTarget).option("--preset <preset>", "Config preset: minimal|full", parseInitPreset).option("-f, --force", "Allow init even when an existing config is detected").option("-y, --yes", "Skip prompts and use defaults/options").addHelpText(
118
+ "after",
119
+ `
120
+ Examples:
121
+ $ eslint-config-snapshot init
122
+ Runs interactive numbered prompts:
123
+ target: 1) package-json, 2) file
124
+ preset: 1) minimal, 2) full
125
+
126
+ $ eslint-config-snapshot init --yes --target package-json --preset minimal
127
+ Non-interactive minimal setup in package.json.
128
+
129
+ $ eslint-config-snapshot init --yes --force --target file --preset full
130
+ Overwrite-safe bypass when a config is already detected.
131
+ `
132
+ ).action(async (opts) => {
133
+ onActionExit(await runInit(cwd, opts));
134
+ });
135
+ program.command("compare", { hidden: true }).action(async () => {
136
+ onActionExit(await executeCheck(cwd, "diff"));
137
+ });
138
+ program.command("status", { hidden: true }).action(async () => {
139
+ onActionExit(await executeCheck(cwd, "status"));
140
+ });
141
+ program.command("what-changed", { hidden: true }).action(async () => {
142
+ onActionExit(await executeCheck(cwd, "summary"));
143
+ });
144
+ program.exitOverride();
145
+ return program;
146
+ }
147
+ function parseCheckFormat(value) {
148
+ const normalized = value.trim().toLowerCase();
149
+ if (normalized === "summary" || normalized === "status" || normalized === "diff") {
150
+ return normalized;
151
+ }
152
+ throw new import_commander.InvalidArgumentError("Expected one of: summary, status, diff");
153
+ }
154
+ function parsePrintFormat(value) {
155
+ const normalized = value.trim().toLowerCase();
156
+ if (normalized === "json" || normalized === "short") {
157
+ return normalized;
158
+ }
159
+ throw new import_commander.InvalidArgumentError("Expected one of: json, short");
160
+ }
161
+ function parseInitTarget(value) {
162
+ const normalized = value.trim().toLowerCase();
163
+ if (normalized === "file" || normalized === "package-json") {
164
+ return normalized;
165
+ }
166
+ throw new import_commander.InvalidArgumentError("Expected one of: file, package-json");
167
+ }
168
+ function parseInitPreset(value) {
169
+ const normalized = value.trim().toLowerCase();
170
+ if (normalized === "minimal" || normalized === "full") {
171
+ return normalized;
172
+ }
173
+ throw new import_commander.InvalidArgumentError("Expected one of: minimal, full");
174
+ }
175
+ async function executeCheck(cwd, format, defaultInvocation = false) {
176
+ const foundConfig = await (0, import_api.findConfigPath)(cwd);
177
+ if (!foundConfig) {
178
+ writeSubtleInfo(
179
+ "Tip: no explicit config found. Using safe built-in defaults. Run `eslint-config-snapshot init` to customize when needed.\n"
180
+ );
181
+ }
182
+ let currentSnapshots;
183
+ try {
184
+ currentSnapshots = await computeCurrentSnapshots(cwd);
185
+ } catch (error) {
186
+ if (!foundConfig) {
187
+ process.stdout.write(
188
+ "Automatic workspace discovery could not complete with defaults.\nRun `eslint-config-snapshot init` to configure workspaces, then run `eslint-config-snapshot --update`.\n"
189
+ );
190
+ return 1;
191
+ }
192
+ throw error;
193
+ }
194
+ const storedSnapshots = await loadStoredSnapshots(cwd);
195
+ if (storedSnapshots.size === 0) {
196
+ const summary = summarizeSnapshots(currentSnapshots);
197
+ process.stdout.write(
198
+ `Current rule state: ${summary.groups} groups, ${summary.rules} rules (severity mix: ${summary.error} errors, ${summary.warn} warnings, ${summary.off} off).
199
+ `
200
+ );
201
+ const canPromptBaseline = defaultInvocation || format === "summary";
202
+ if (canPromptBaseline && process.stdin.isTTY && process.stdout.isTTY) {
203
+ const shouldCreateBaseline = await askYesNo(
204
+ "No baseline yet. Use current rule state as your baseline now? [Y/n] ",
205
+ true
206
+ );
207
+ if (shouldCreateBaseline) {
208
+ await writeSnapshots(cwd, currentSnapshots);
209
+ const summary2 = summarizeSnapshots(currentSnapshots);
210
+ process.stdout.write(`Great start: baseline created with ${summary2.groups} groups and ${summary2.rules} rules.
211
+ `);
212
+ writeSubtleInfo(UPDATE_HINT);
213
+ return 0;
214
+ }
215
+ }
216
+ process.stdout.write("You are almost set: no baseline snapshot found yet.\n");
217
+ process.stdout.write("Run `eslint-config-snapshot --update` to create your first baseline.\n");
218
+ return 1;
219
+ }
220
+ const changes = compareSnapshotMaps(storedSnapshots, currentSnapshots);
221
+ if (format === "status") {
222
+ if (changes.length === 0) {
223
+ process.stdout.write("clean\n");
224
+ return 0;
225
+ }
226
+ process.stdout.write("changes\n");
227
+ writeSubtleInfo(UPDATE_HINT);
228
+ return 1;
229
+ }
230
+ if (format === "diff") {
231
+ if (changes.length === 0) {
232
+ process.stdout.write("Great news: no snapshot changes detected.\n");
233
+ return 0;
234
+ }
235
+ for (const change of changes) {
236
+ process.stdout.write(`${formatDiff(change.groupId, change.diff)}
237
+ `);
238
+ }
239
+ writeSubtleInfo(UPDATE_HINT);
240
+ return 1;
241
+ }
242
+ return printWhatChanged(changes, currentSnapshots);
243
+ }
244
+ async function executeUpdate(cwd, printSummary) {
245
+ const foundConfig = await (0, import_api.findConfigPath)(cwd);
246
+ if (!foundConfig) {
247
+ writeSubtleInfo(
248
+ "Tip: no explicit config found. Using safe built-in defaults. Run `eslint-config-snapshot init` to customize when needed.\n"
249
+ );
250
+ }
251
+ let currentSnapshots;
252
+ try {
253
+ currentSnapshots = await computeCurrentSnapshots(cwd);
254
+ } catch (error) {
255
+ if (!foundConfig) {
256
+ process.stdout.write(
257
+ "Automatic workspace discovery could not complete with defaults.\nRun `eslint-config-snapshot init` to configure workspaces, then run `eslint-config-snapshot --update`.\n"
258
+ );
259
+ return 1;
260
+ }
261
+ throw error;
262
+ }
263
+ await writeSnapshots(cwd, currentSnapshots);
264
+ if (printSummary) {
265
+ const summary = summarizeSnapshots(currentSnapshots);
266
+ process.stdout.write(`Baseline updated: ${summary.groups} groups, ${summary.rules} rules.
267
+ `);
268
+ }
269
+ return 0;
270
+ }
271
+ async function executePrint(cwd, format) {
272
+ const currentSnapshots = await computeCurrentSnapshots(cwd);
273
+ if (format === "short") {
274
+ process.stdout.write(formatShortPrint([...currentSnapshots.values()]));
275
+ return;
276
+ }
277
+ const output = [...currentSnapshots.values()].map((snapshot) => ({
278
+ groupId: snapshot.groupId,
279
+ rules: snapshot.rules
280
+ }));
281
+ process.stdout.write(`${JSON.stringify(output, null, 2)}
282
+ `);
283
+ }
284
+ async function computeCurrentSnapshots(cwd) {
285
+ const config = await (0, import_api.loadConfig)(cwd);
286
+ const discovery = await (0, import_api.discoverWorkspaces)({ cwd, workspaceInput: config.workspaceInput });
287
+ const assignments = config.grouping.mode === "standalone" ? discovery.workspacesRel.map((workspace) => ({ name: workspace, workspaces: [workspace] })) : (0, import_api.assignGroupsByMatch)(discovery.workspacesRel, config.grouping.groups ?? [{ name: "default", match: ["**/*"] }]);
288
+ const allowEmptyGroups = config.grouping.allowEmptyGroups ?? false;
289
+ if (!allowEmptyGroups) {
290
+ const empty = assignments.filter((group) => group.workspaces.length === 0);
291
+ if (empty.length > 0) {
292
+ throw new Error(`Empty groups are not allowed: ${empty.map((entry) => entry.name).join(", ")}`);
293
+ }
294
+ }
295
+ const snapshots = /* @__PURE__ */ new Map();
296
+ for (const group of assignments) {
297
+ const extractedForGroup = [];
298
+ for (const workspaceRel of group.workspaces) {
299
+ const workspaceAbs = import_node_path.default.resolve(discovery.rootAbs, workspaceRel);
300
+ const sampled = await (0, import_api.sampleWorkspaceFiles)(workspaceAbs, config.sampling);
301
+ let extractedCount = 0;
302
+ let lastExtractionError;
303
+ for (const sampledRel of sampled) {
304
+ const sampledAbs = import_node_path.default.resolve(workspaceAbs, sampledRel);
305
+ try {
306
+ extractedForGroup.push((0, import_api.extractRulesFromPrintConfig)(workspaceAbs, sampledAbs));
307
+ extractedCount += 1;
308
+ } catch (error) {
309
+ const message = error instanceof Error ? error.message : String(error);
310
+ if (message.startsWith("Invalid JSON from eslint --print-config") || message.startsWith("Empty ESLint print-config output")) {
311
+ lastExtractionError = message;
312
+ continue;
313
+ }
314
+ throw error;
315
+ }
316
+ }
317
+ if (extractedCount === 0) {
318
+ const context = lastExtractionError ? ` Last error: ${lastExtractionError}` : "";
319
+ throw new Error(
320
+ `Unable to extract ESLint config for workspace ${workspaceRel}. All sampled files were ignored or produced non-JSON output.${context}`
321
+ );
322
+ }
323
+ }
324
+ const aggregated = (0, import_api.aggregateRules)(extractedForGroup);
325
+ snapshots.set(group.name, (0, import_api.buildSnapshot)(group.name, group.workspaces, aggregated));
326
+ }
327
+ return snapshots;
328
+ }
329
+ async function loadStoredSnapshots(cwd) {
330
+ const dir = import_node_path.default.join(cwd, SNAPSHOT_DIR);
331
+ const files = await (0, import_fast_glob.default)("**/*.json", { cwd: dir, absolute: true, onlyFiles: true, dot: true, suppressErrors: true });
332
+ const snapshots = /* @__PURE__ */ new Map();
333
+ const sortedFiles = [...files].sort((a, b) => a.localeCompare(b));
334
+ for (const file of sortedFiles) {
335
+ const snapshot = await (0, import_api.readSnapshotFile)(file);
336
+ snapshots.set(snapshot.groupId, snapshot);
337
+ }
338
+ return snapshots;
339
+ }
340
+ async function writeSnapshots(cwd, snapshots) {
341
+ await (0, import_promises.mkdir)(import_node_path.default.join(cwd, SNAPSHOT_DIR), { recursive: true });
342
+ for (const snapshot of snapshots.values()) {
343
+ await (0, import_api.writeSnapshotFile)(import_node_path.default.join(cwd, SNAPSHOT_DIR), snapshot);
344
+ }
345
+ }
346
+ function compareSnapshotMaps(before, after) {
347
+ const ids = [.../* @__PURE__ */ new Set([...before.keys(), ...after.keys()])].sort();
348
+ const changes = [];
349
+ for (const id of ids) {
350
+ const prev = before.get(id) ?? {
351
+ formatVersion: 1,
352
+ groupId: id,
353
+ workspaces: [],
354
+ rules: {}
355
+ };
356
+ const next = after.get(id) ?? {
357
+ formatVersion: 1,
358
+ groupId: id,
359
+ workspaces: [],
360
+ rules: {}
361
+ };
362
+ const diff = (0, import_api.diffSnapshots)(prev, next);
363
+ if ((0, import_api.hasDiff)(diff)) {
364
+ changes.push({ groupId: id, diff });
365
+ }
366
+ }
367
+ return changes;
368
+ }
369
+ function formatDiff(groupId, diff) {
370
+ const lines = [`group: ${groupId}`];
371
+ addListSection(lines, "introduced rules", diff.introducedRules);
372
+ addListSection(lines, "removed rules", diff.removedRules);
373
+ if (diff.severityChanges.length > 0) {
374
+ lines.push("severity changed:");
375
+ for (const change of diff.severityChanges) {
376
+ lines.push(` - ${change.rule}: ${change.before} -> ${change.after}`);
377
+ }
378
+ }
379
+ const optionChanges = getDisplayOptionChanges(diff);
380
+ if (optionChanges.length > 0) {
381
+ lines.push("options changed:");
382
+ for (const change of optionChanges) {
383
+ lines.push(` - ${change.rule}: ${formatValue(change.before)} -> ${formatValue(change.after)}`);
384
+ }
385
+ }
386
+ addListSection(lines, "workspaces added", diff.workspaceMembershipChanges.added);
387
+ addListSection(lines, "workspaces removed", diff.workspaceMembershipChanges.removed);
388
+ return lines.join("\n");
389
+ }
390
+ function addListSection(lines, title, values) {
391
+ if (values.length === 0) {
392
+ return;
393
+ }
394
+ lines.push(`${title}:`);
395
+ for (const value of values) {
396
+ lines.push(` - ${value}`);
397
+ }
398
+ }
399
+ function formatValue(value) {
400
+ const serialized = JSON.stringify(value);
401
+ return serialized === void 0 ? "undefined" : serialized;
402
+ }
403
+ function getDisplayOptionChanges(diff) {
404
+ const removedRules = new Set(diff.removedRules);
405
+ const severityChangedRules = new Set(diff.severityChanges.map((change) => change.rule));
406
+ return diff.optionChanges.filter((change) => !removedRules.has(change.rule) && !severityChangedRules.has(change.rule));
407
+ }
408
+ async function runInit(cwd, opts = {}) {
409
+ const force = opts.force ?? false;
410
+ const existing = await (0, import_api.findConfigPath)(cwd);
411
+ if (existing && !force) {
412
+ process.stderr.write(
413
+ `Existing config detected at ${existing.path}. Creating another config can cause conflicts. Remove the existing config or rerun with --force.
414
+ `
415
+ );
416
+ return 1;
417
+ }
418
+ let target = opts.target;
419
+ let preset = opts.preset;
420
+ if (!opts.yes && !target && !preset && process.stdin.isTTY && process.stdout.isTTY) {
421
+ const interactive = await askInitPreferences();
422
+ target = interactive.target;
423
+ preset = interactive.preset;
424
+ }
425
+ const finalTarget = target ?? "file";
426
+ const finalPreset = preset ?? "minimal";
427
+ if (finalTarget === "package-json") {
428
+ return runInitInPackageJson(cwd, finalPreset, force);
429
+ }
430
+ return runInitInFile(cwd, finalPreset, force);
431
+ }
432
+ async function askInitPreferences() {
433
+ const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
434
+ try {
435
+ const target = await askInitTarget(rl);
436
+ const preset = await askInitPreset(rl);
437
+ return { target, preset };
438
+ } finally {
439
+ rl.close();
440
+ }
441
+ }
442
+ async function askInitTarget(rl) {
443
+ while (true) {
444
+ const answer = await askQuestion(
445
+ rl,
446
+ "Select config target:\n 1) package-json (recommended)\n 2) file\nChoose [1]: "
447
+ );
448
+ const parsed = parseInitTargetChoice(answer);
449
+ if (parsed) {
450
+ return parsed;
451
+ }
452
+ process.stdout.write("Please choose 1 (package-json) or 2 (file).\n");
453
+ }
454
+ }
455
+ async function askInitPreset(rl) {
456
+ while (true) {
457
+ const answer = await askQuestion(rl, "Select preset:\n 1) minimal (recommended)\n 2) full\nChoose [1]: ");
458
+ const parsed = parseInitPresetChoice(answer);
459
+ if (parsed) {
460
+ return parsed;
461
+ }
462
+ process.stdout.write("Please choose 1 (minimal) or 2 (full).\n");
463
+ }
464
+ }
465
+ function parseInitTargetChoice(value) {
466
+ const normalized = value.trim().toLowerCase();
467
+ if (normalized === "") {
468
+ return "package-json";
469
+ }
470
+ if (normalized === "1" || normalized === "package-json" || normalized === "packagejson" || normalized === "package" || normalized === "pkg") {
471
+ return "package-json";
472
+ }
473
+ if (normalized === "2" || normalized === "file") {
474
+ return "file";
475
+ }
476
+ return void 0;
477
+ }
478
+ function parseInitPresetChoice(value) {
479
+ const normalized = value.trim().toLowerCase();
480
+ if (normalized === "") {
481
+ return "minimal";
482
+ }
483
+ if (normalized === "1" || normalized === "minimal" || normalized === "min") {
484
+ return "minimal";
485
+ }
486
+ if (normalized === "2" || normalized === "full") {
487
+ return "full";
488
+ }
489
+ return void 0;
490
+ }
491
+ function askQuestion(rl, prompt) {
492
+ return new Promise((resolve) => {
493
+ rl.question(prompt, (answer) => {
494
+ resolve(answer);
495
+ });
496
+ });
497
+ }
498
+ async function askYesNo(prompt, defaultYes) {
499
+ const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
500
+ try {
501
+ const answerRaw = await askQuestion(rl, prompt);
502
+ const answer = answerRaw.trim().toLowerCase();
503
+ if (answer.length === 0) {
504
+ return defaultYes;
505
+ }
506
+ return answer === "y" || answer === "yes";
507
+ } finally {
508
+ rl.close();
509
+ }
510
+ }
511
+ async function runInitInFile(cwd, preset, force) {
512
+ const candidates = [
513
+ ".eslint-config-snapshot.js",
514
+ ".eslint-config-snapshot.cjs",
515
+ ".eslint-config-snapshot.mjs",
516
+ "eslint-config-snapshot.config.js",
517
+ "eslint-config-snapshot.config.cjs",
518
+ "eslint-config-snapshot.config.mjs"
519
+ ];
520
+ for (const candidate of candidates) {
521
+ try {
522
+ await (0, import_promises.access)(import_node_path.default.join(cwd, candidate));
523
+ if (!force) {
524
+ process.stderr.write(`Config already exists: ${candidate}
525
+ `);
526
+ return 1;
527
+ }
528
+ } catch {
529
+ }
530
+ }
531
+ const target = import_node_path.default.join(cwd, "eslint-config-snapshot.config.mjs");
532
+ await (0, import_promises.writeFile)(target, (0, import_api.getConfigScaffold)(preset), "utf8");
533
+ process.stdout.write(`Created ${import_node_path.default.basename(target)}
534
+ `);
535
+ return 0;
536
+ }
537
+ async function runInitInPackageJson(cwd, preset, force) {
538
+ const packageJsonPath = import_node_path.default.join(cwd, "package.json");
539
+ let packageJsonRaw;
540
+ try {
541
+ packageJsonRaw = await (0, import_promises.readFile)(packageJsonPath, "utf8");
542
+ } catch {
543
+ process.stderr.write("package.json not found in current directory.\n");
544
+ return 1;
545
+ }
546
+ let parsed;
547
+ try {
548
+ parsed = JSON.parse(packageJsonRaw);
549
+ } catch {
550
+ process.stderr.write("Invalid package.json (must be valid JSON).\n");
551
+ return 1;
552
+ }
553
+ if (parsed["eslint-config-snapshot"] !== void 0 && !force) {
554
+ process.stderr.write("Config already exists in package.json: eslint-config-snapshot\n");
555
+ return 1;
556
+ }
557
+ parsed["eslint-config-snapshot"] = preset === "full" ? getFullPresetObject() : {};
558
+ await (0, import_promises.writeFile)(packageJsonPath, `${JSON.stringify(parsed, null, 2)}
559
+ `, "utf8");
560
+ process.stdout.write('Created config in package.json under "eslint-config-snapshot"\n');
561
+ return 0;
562
+ }
563
+ function getFullPresetObject() {
564
+ return {
565
+ workspaceInput: { mode: "discover" },
566
+ grouping: {
567
+ mode: "match",
568
+ groups: [{ name: "default", match: ["**/*"] }]
569
+ },
570
+ sampling: {
571
+ maxFilesPerWorkspace: 8,
572
+ includeGlobs: ["**/*.{js,jsx,ts,tsx,cjs,mjs}"],
573
+ excludeGlobs: ["**/node_modules/**", "**/dist/**"],
574
+ hintGlobs: []
575
+ }
576
+ };
577
+ }
578
+ async function main() {
579
+ const code = await runArgv(process.argv.slice(2), process.cwd());
580
+ process.exit(code);
581
+ }
582
+ function isDirectCliExecution() {
583
+ const entry = process.argv[1];
584
+ if (!entry) {
585
+ return false;
586
+ }
587
+ const normalized = import_node_path.default.basename(entry).toLowerCase();
588
+ return normalized === "index.js" || normalized === "index.cjs" || normalized === "index.ts" || normalized === "eslint-config-snapshot";
589
+ }
590
+ if (isDirectCliExecution()) {
591
+ void main();
592
+ }
593
+ function printWhatChanged(changes, currentSnapshots) {
594
+ const color = createColorizer();
595
+ const currentSummary = summarizeSnapshots(currentSnapshots);
596
+ const changeSummary = summarizeChanges(changes);
597
+ if (changes.length === 0) {
598
+ process.stdout.write(color.green("Great news: no snapshot drift detected.\n"));
599
+ process.stdout.write(
600
+ `Baseline status: ${currentSummary.groups} groups, ${currentSummary.rules} rules (severity mix: ${currentSummary.error} errors, ${currentSummary.warn} warnings, ${currentSummary.off} off).
601
+ `
602
+ );
603
+ return 0;
604
+ }
605
+ process.stdout.write(color.red("Heads up: snapshot drift detected.\n"));
606
+ process.stdout.write(
607
+ `Changed groups: ${changes.length} | introduced: ${changeSummary.introduced} | removed: ${changeSummary.removed} | severity: ${changeSummary.severity} | options: ${changeSummary.options} | workspace membership: ${changeSummary.workspace}
608
+ `
609
+ );
610
+ process.stdout.write(
611
+ `Current rules: ${currentSummary.rules} (severity mix: ${currentSummary.error} errors, ${currentSummary.warn} warnings, ${currentSummary.off} off)
612
+
613
+ `
614
+ );
615
+ for (const change of changes) {
616
+ process.stdout.write(color.bold(`group ${change.groupId}
617
+ `));
618
+ const lines = formatDiff(change.groupId, change.diff).split("\n").slice(1);
619
+ for (const line of lines) {
620
+ const decorated = decorateDiffLine(line, color);
621
+ process.stdout.write(`${decorated}
622
+ `);
623
+ }
624
+ process.stdout.write("\n");
625
+ }
626
+ writeSubtleInfo(UPDATE_HINT);
627
+ return 1;
628
+ }
629
+ function summarizeChanges(changes) {
630
+ let introduced = 0;
631
+ let removed = 0;
632
+ let severity = 0;
633
+ let options = 0;
634
+ let workspace = 0;
635
+ for (const change of changes) {
636
+ introduced += change.diff.introducedRules.length;
637
+ removed += change.diff.removedRules.length;
638
+ severity += change.diff.severityChanges.length;
639
+ options += getDisplayOptionChanges(change.diff).length;
640
+ workspace += change.diff.workspaceMembershipChanges.added.length + change.diff.workspaceMembershipChanges.removed.length;
641
+ }
642
+ return { introduced, removed, severity, options, workspace };
643
+ }
644
+ function summarizeSnapshots(snapshots) {
645
+ let rules = 0;
646
+ let error = 0;
647
+ let warn = 0;
648
+ let off = 0;
649
+ for (const snapshot of snapshots.values()) {
650
+ for (const entry of Object.values(snapshot.rules)) {
651
+ rules += 1;
652
+ if (entry[0] === "error") {
653
+ error += 1;
654
+ } else if (entry[0] === "warn") {
655
+ warn += 1;
656
+ } else {
657
+ off += 1;
658
+ }
659
+ }
660
+ }
661
+ return { groups: snapshots.size, rules, error, warn, off };
662
+ }
663
+ function decorateDiffLine(line, color) {
664
+ if (line.startsWith("introduced rules:") || line.startsWith("workspaces added:")) {
665
+ return color.green(`+ ${line}`);
666
+ }
667
+ if (line.startsWith("removed rules:") || line.startsWith("workspaces removed:")) {
668
+ return color.red(`- ${line}`);
669
+ }
670
+ if (line.startsWith("severity changed:") || line.startsWith("options changed:")) {
671
+ return color.yellow(`~ ${line}`);
672
+ }
673
+ return line;
674
+ }
675
+ function createColorizer() {
676
+ const enabled = process.stdout.isTTY && process.env.NO_COLOR === void 0 && process.env.TERM !== "dumb";
677
+ const wrap = (code, text) => enabled ? `\x1B[${code}m${text}\x1B[0m` : text;
678
+ return {
679
+ green: (text) => wrap("32", text),
680
+ yellow: (text) => wrap("33", text),
681
+ red: (text) => wrap("31", text),
682
+ bold: (text) => wrap("1", text),
683
+ dim: (text) => wrap("2", text)
684
+ };
685
+ }
686
+ function writeSubtleInfo(text) {
687
+ const color = createColorizer();
688
+ process.stdout.write(color.dim(text));
689
+ }
690
+ function formatShortPrint(snapshots) {
691
+ const lines = [];
692
+ const sorted = [...snapshots].sort((a, b) => a.groupId.localeCompare(b.groupId));
693
+ for (const snapshot of sorted) {
694
+ const ruleNames = Object.keys(snapshot.rules).sort();
695
+ const severityCounts = { error: 0, warn: 0, off: 0 };
696
+ for (const name of ruleNames) {
697
+ const severity = snapshot.rules[name][0];
698
+ severityCounts[severity] += 1;
699
+ }
700
+ lines.push(
701
+ `group: ${snapshot.groupId}`,
702
+ `workspaces (${snapshot.workspaces.length}): ${snapshot.workspaces.length > 0 ? snapshot.workspaces.join(", ") : "(none)"}`,
703
+ `rules (${ruleNames.length}): error ${severityCounts.error}, warn ${severityCounts.warn}, off ${severityCounts.off}`
704
+ );
705
+ for (const ruleName of ruleNames) {
706
+ const entry = snapshot.rules[ruleName];
707
+ const suffix = entry.length > 1 ? ` ${JSON.stringify(entry[1])}` : "";
708
+ lines.push(`${ruleName}: ${entry[0]}${suffix}`);
709
+ }
710
+ }
711
+ return `${lines.join("\n")}
712
+ `;
713
+ }
714
+ // Annotate the CommonJS export names for ESM import in node:
715
+ 0 && (module.exports = {
716
+ main,
717
+ parseInitPresetChoice,
718
+ parseInitTargetChoice,
719
+ runCli
720
+ });