@eslint-config-snapshot/cli 0.9.0 → 0.14.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/index.js CHANGED
@@ -1,187 +1,540 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import {
5
- aggregateRules,
6
- assignGroupsByMatch,
7
- buildSnapshot,
8
- diffSnapshots,
9
- discoverWorkspaces,
10
- extractRulesForWorkspaceSamples,
11
- findConfigPath,
12
- getConfigScaffold,
13
- hasDiff,
14
- loadConfig,
15
- normalizePath,
16
- readSnapshotFile,
17
- resolveEslintVersionForWorkspace,
18
- sampleWorkspaceFiles,
19
- writeSnapshotFile
20
- } from "@eslint-config-snapshot/api";
21
4
  import { Command, CommanderError, InvalidArgumentError } from "commander";
22
- import fg from "fast-glob";
5
+ import createDebug2 from "debug";
6
+ import path4 from "path";
7
+
8
+ // src/commands/check.ts
9
+ import { findConfigPath } from "@eslint-config-snapshot/api";
10
+
11
+ // src/formatters.ts
12
+ function formatDiff(groupId, diff) {
13
+ const lines = [`group: ${groupId}`];
14
+ addListSection(lines, "introduced rules", diff.introducedRules);
15
+ addListSection(lines, "removed rules", diff.removedRules);
16
+ if (diff.severityChanges.length > 0) {
17
+ lines.push("severity changed:");
18
+ for (const change of diff.severityChanges) {
19
+ lines.push(` - ${change.rule}: ${change.before} -> ${change.after}`);
20
+ }
21
+ }
22
+ const optionChanges = getDisplayOptionChanges(diff);
23
+ if (optionChanges.length > 0) {
24
+ lines.push("options changed:");
25
+ for (const change of optionChanges) {
26
+ lines.push(` - ${change.rule}: ${formatValue(change.before)} -> ${formatValue(change.after)}`);
27
+ }
28
+ }
29
+ addListSection(lines, "workspaces added", diff.workspaceMembershipChanges.added);
30
+ addListSection(lines, "workspaces removed", diff.workspaceMembershipChanges.removed);
31
+ return lines.join("\n");
32
+ }
33
+ function getDisplayOptionChanges(diff) {
34
+ const removedRules = new Set(diff.removedRules);
35
+ const severityChangedRules = new Set(diff.severityChanges.map((change) => change.rule));
36
+ return diff.optionChanges.filter((change) => !removedRules.has(change.rule) && !severityChangedRules.has(change.rule));
37
+ }
38
+ function addListSection(lines, title, values) {
39
+ if (values.length === 0) {
40
+ return;
41
+ }
42
+ lines.push(`${title}:`);
43
+ for (const value of values) {
44
+ lines.push(` - ${value}`);
45
+ }
46
+ }
47
+ function formatValue(value) {
48
+ const serialized = JSON.stringify(value);
49
+ return serialized === void 0 ? "undefined" : serialized;
50
+ }
51
+ function summarizeChanges(changes) {
52
+ let introduced = 0;
53
+ let removed = 0;
54
+ let severity = 0;
55
+ let options = 0;
56
+ let workspace = 0;
57
+ for (const change of changes) {
58
+ introduced += change.diff.introducedRules.length;
59
+ removed += change.diff.removedRules.length;
60
+ severity += change.diff.severityChanges.length;
61
+ options += getDisplayOptionChanges(change.diff).length;
62
+ workspace += change.diff.workspaceMembershipChanges.added.length + change.diff.workspaceMembershipChanges.removed.length;
63
+ }
64
+ return { introduced, removed, severity, options, workspace };
65
+ }
66
+ function summarizeSnapshots(snapshots) {
67
+ const { rules, error, warn, off } = countRuleSeverities([...snapshots.values()].map((snapshot) => snapshot.rules));
68
+ return { groups: snapshots.size, rules, error, warn, off };
69
+ }
70
+ function countUniqueWorkspaces(snapshots) {
71
+ const workspaces = /* @__PURE__ */ new Set();
72
+ for (const snapshot of snapshots.values()) {
73
+ for (const workspace of snapshot.workspaces) {
74
+ workspaces.add(workspace);
75
+ }
76
+ }
77
+ return workspaces.size;
78
+ }
79
+ function decorateDiffLine(line, color) {
80
+ if (line.startsWith("introduced rules:") || line.startsWith("workspaces added:")) {
81
+ return color.green(`+ ${line}`);
82
+ }
83
+ if (line.startsWith("removed rules:") || line.startsWith("workspaces removed:")) {
84
+ return color.red(`- ${line}`);
85
+ }
86
+ if (line.startsWith("severity changed:") || line.startsWith("options changed:")) {
87
+ return color.yellow(`~ ${line}`);
88
+ }
89
+ return line;
90
+ }
91
+ function formatShortPrint(snapshots) {
92
+ const lines = [];
93
+ const sorted = [...snapshots].sort((a, b) => a.groupId.localeCompare(b.groupId));
94
+ for (const snapshot of sorted) {
95
+ const ruleNames = Object.keys(snapshot.rules).sort();
96
+ const severityCounts = { error: 0, warn: 0, off: 0 };
97
+ for (const name of ruleNames) {
98
+ const severity = getPrimarySeverity(snapshot.rules[name]);
99
+ if (severity) {
100
+ severityCounts[severity] += 1;
101
+ }
102
+ }
103
+ lines.push(
104
+ `group: ${snapshot.groupId}`,
105
+ `workspaces (${snapshot.workspaces.length}): ${snapshot.workspaces.length > 0 ? snapshot.workspaces.join(", ") : "(none)"}`,
106
+ `rules (${ruleNames.length}): error ${severityCounts.error}, warn ${severityCounts.warn}, off ${severityCounts.off}`
107
+ );
108
+ for (const ruleName of ruleNames) {
109
+ const entry = snapshot.rules[ruleName];
110
+ if (!entry) {
111
+ continue;
112
+ }
113
+ if (!Array.isArray(entry[0])) {
114
+ const singleEntry = entry;
115
+ const suffix = singleEntry.length > 1 ? ` ${JSON.stringify(singleEntry[1])}` : "";
116
+ lines.push(`${ruleName}: ${singleEntry[0]}${suffix}`);
117
+ continue;
118
+ }
119
+ const variants = entry;
120
+ lines.push(`${ruleName}: ${JSON.stringify(variants)}`);
121
+ }
122
+ }
123
+ return `${lines.join("\n")}
124
+ `;
125
+ }
126
+ function formatShortConfig(payload) {
127
+ const lines = [
128
+ `source: ${payload.source}`,
129
+ `workspaces (${payload.workspaces.length}): ${payload.workspaces.join(", ") || "(none)"}`,
130
+ `grouping mode: ${payload.grouping.mode} (allow empty: ${payload.grouping.allowEmptyGroups})`
131
+ ];
132
+ for (const group of payload.grouping.groups) {
133
+ lines.push(`group ${group.name} (${group.workspaces.length}): ${group.workspaces.join(", ") || "(none)"}`);
134
+ }
135
+ lines.push(`workspaceInput: ${JSON.stringify(payload.workspaceInput)}`, `sampling: ${JSON.stringify(payload.sampling)}`);
136
+ return `${lines.join("\n")}
137
+ `;
138
+ }
139
+ function formatCommandDisplayLabel(commandLabel) {
140
+ switch (commandLabel) {
141
+ case "check":
142
+ case "check:summary": {
143
+ return "Check drift against baseline (summary)";
144
+ }
145
+ case "check:diff": {
146
+ return "Check drift against baseline (detailed diff)";
147
+ }
148
+ case "check:status": {
149
+ return "Check drift against baseline (status only)";
150
+ }
151
+ case "update": {
152
+ return "Update baseline snapshot";
153
+ }
154
+ case "print:json": {
155
+ return "Print aggregated rules (JSON)";
156
+ }
157
+ case "print:short": {
158
+ return "Print aggregated rules (short view)";
159
+ }
160
+ case "config:json": {
161
+ return "Show effective runtime config (JSON)";
162
+ }
163
+ case "config:short": {
164
+ return "Show effective runtime config (short view)";
165
+ }
166
+ case "init": {
167
+ return "Initialize local configuration";
168
+ }
169
+ case "help": {
170
+ return "Show CLI help";
171
+ }
172
+ default: {
173
+ return commandLabel;
174
+ }
175
+ }
176
+ }
177
+ function formatStoredSnapshotSummary(storedSnapshots) {
178
+ if (storedSnapshots.size === 0) {
179
+ return "none";
180
+ }
181
+ const summary = summarizeSnapshots(storedSnapshots);
182
+ return `${summary.groups} groups, ${summary.rules} rules (severity mix: ${summary.error} errors, ${summary.warn} warnings, ${summary.off} off)`;
183
+ }
184
+ function countRuleSeverities(ruleObjects) {
185
+ let rules = 0;
186
+ let error = 0;
187
+ let warn = 0;
188
+ let off = 0;
189
+ for (const rulesObject of ruleObjects) {
190
+ for (const entry of Object.values(rulesObject)) {
191
+ rules += 1;
192
+ const severity = getPrimarySeverity(entry);
193
+ if (severity === "error") {
194
+ error += 1;
195
+ } else if (severity === "warn") {
196
+ warn += 1;
197
+ } else {
198
+ off += 1;
199
+ }
200
+ }
201
+ }
202
+ return { rules, error, warn, off };
203
+ }
204
+ function getPrimarySeverity(entry) {
205
+ if (!entry) {
206
+ return void 0;
207
+ }
208
+ if (!Array.isArray(entry[0])) {
209
+ return entry[0];
210
+ }
211
+ const variants = entry;
212
+ if (variants.some((variant) => variant[0] === "error")) {
213
+ return "error";
214
+ }
215
+ if (variants.some((variant) => variant[0] === "warn")) {
216
+ return "warn";
217
+ }
218
+ return "off";
219
+ }
220
+
221
+ // src/run-context.ts
222
+ import { normalizePath } from "@eslint-config-snapshot/api";
23
223
  import { existsSync, readFileSync } from "fs";
24
- import { access, mkdir, readFile, writeFile } from "fs/promises";
25
224
  import { createRequire } from "module";
26
225
  import path from "path";
27
- import { createInterface } from "readline";
28
- var SNAPSHOT_DIR = ".eslint-config-snapshot";
29
- var UPDATE_HINT = "Tip: when you intentionally accept changes, run `eslint-config-snapshot --update` to refresh the baseline.\n";
30
- var activeRunTimer;
31
226
  var cachedCliVersion;
32
- async function runCli(command, cwd, flags = []) {
33
- const argv = command ? [command, ...flags] : [...flags];
34
- return runArgv(argv, cwd);
227
+ function writeRunContextHeader(terminal, cwd, commandLabel, configPath, storedSnapshots) {
228
+ if (!terminal.showProgress) {
229
+ return;
230
+ }
231
+ terminal.write(terminal.bold(`eslint-config-snapshot v${readCliVersion()} \u2022 ${formatCommandDisplayLabel(commandLabel)}
232
+ `));
233
+ terminal.write(`\u{1F4C1} Repository: ${cwd}
234
+ `);
235
+ terminal.write(`\u{1F4C1} Baseline: ${formatStoredSnapshotSummary(storedSnapshots)}
236
+ `);
237
+ terminal.write(`\u2699\uFE0F Config source: ${formatConfigSource(cwd, configPath)}
238
+ `);
239
+ terminal.write("\n");
35
240
  }
36
- async function runArgv(argv, cwd) {
37
- const invocationLabel = resolveInvocationLabel(argv);
38
- beginRunTimer(invocationLabel);
39
- let exitCode = 1;
241
+ function writeEslintVersionSummary(terminal, eslintVersionsByGroup) {
242
+ if (!terminal.showProgress || eslintVersionsByGroup.size === 0) {
243
+ return;
244
+ }
245
+ const allVersions = /* @__PURE__ */ new Set();
246
+ for (const versions of eslintVersionsByGroup.values()) {
247
+ for (const version of versions) {
248
+ allVersions.add(version);
249
+ }
250
+ }
251
+ const sortedAllVersions = [...allVersions].sort((a, b) => a.localeCompare(b));
252
+ if (sortedAllVersions.length === 1) {
253
+ terminal.write(`- \u{1F9E9} eslint runtime: ${sortedAllVersions[0]} (all groups)
254
+ `);
255
+ return;
256
+ }
257
+ terminal.write("- \u{1F9E9} eslint runtime by group:\n");
258
+ const sortedEntries = [...eslintVersionsByGroup.entries()].sort((a, b) => a[0].localeCompare(b[0]));
259
+ for (const [groupName, versions] of sortedEntries) {
260
+ terminal.write(` - ${groupName}: ${versions.join(", ")}
261
+ `);
262
+ }
263
+ }
264
+ function formatConfigSource(cwd, configPath) {
265
+ if (!configPath) {
266
+ return "built-in defaults";
267
+ }
268
+ const rel = normalizePath(path.relative(cwd, configPath));
269
+ if (path.basename(configPath) === "package.json") {
270
+ return `${rel} (eslint-config-snapshot field)`;
271
+ }
272
+ return rel;
273
+ }
274
+ function readCliVersion() {
275
+ if (cachedCliVersion !== void 0) {
276
+ return cachedCliVersion;
277
+ }
278
+ const envPackageName = process.env.npm_package_name;
279
+ const envPackageVersion = process.env.npm_package_version;
280
+ if (isCliPackageName(envPackageName) && typeof envPackageVersion === "string" && envPackageVersion.length > 0) {
281
+ cachedCliVersion = envPackageVersion;
282
+ return cachedCliVersion;
283
+ }
284
+ const scriptPath = process.argv[1];
285
+ if (!scriptPath) {
286
+ cachedCliVersion = "unknown";
287
+ return cachedCliVersion;
288
+ }
40
289
  try {
41
- const hasCommandToken = argv.some((token) => !token.startsWith("-"));
42
- if (!hasCommandToken) {
43
- exitCode = await runDefaultInvocation(argv, cwd);
44
- return exitCode;
290
+ const req = createRequire(path.resolve(scriptPath));
291
+ const resolvedCliEntry = req.resolve("@eslint-config-snapshot/cli");
292
+ const resolvedVersion = readVersionFromResolvedEntry(resolvedCliEntry);
293
+ if (resolvedVersion !== void 0) {
294
+ cachedCliVersion = resolvedVersion;
295
+ return cachedCliVersion;
45
296
  }
46
- let actionCode;
47
- const program = createProgram(cwd, (code) => {
48
- actionCode = code;
49
- });
50
- try {
51
- await program.parseAsync(argv, { from: "user" });
52
- } catch (error) {
53
- if (error instanceof CommanderError) {
54
- if (error.code === "commander.helpDisplayed") {
55
- exitCode = 0;
56
- return exitCode;
297
+ } catch {
298
+ }
299
+ let current = path.resolve(path.dirname(scriptPath));
300
+ let fallbackVersion;
301
+ while (true) {
302
+ const packageJsonPath = path.join(current, "package.json");
303
+ if (existsSync(packageJsonPath)) {
304
+ try {
305
+ const raw = readFileSync(packageJsonPath, "utf8");
306
+ const parsed = JSON.parse(raw);
307
+ if (typeof parsed.version === "string" && parsed.version.length > 0) {
308
+ if (isCliPackageName(parsed.name)) {
309
+ cachedCliVersion = parsed.version;
310
+ return cachedCliVersion;
311
+ }
312
+ if (fallbackVersion === void 0) {
313
+ fallbackVersion = parsed.version;
314
+ }
57
315
  }
58
- exitCode = error.exitCode;
59
- return exitCode;
316
+ } catch {
60
317
  }
61
- const message = error instanceof Error ? error.message : String(error);
62
- process.stderr.write(`${message}
63
- `);
64
- return 1;
65
318
  }
66
- exitCode = actionCode ?? 0;
67
- return exitCode;
68
- } finally {
69
- endRunTimer(exitCode);
319
+ const parent = path.dirname(current);
320
+ if (parent === current) {
321
+ break;
322
+ }
323
+ current = parent;
70
324
  }
325
+ cachedCliVersion = fallbackVersion ?? "unknown";
326
+ return cachedCliVersion;
71
327
  }
72
- async function runDefaultInvocation(argv, cwd) {
73
- const known = /* @__PURE__ */ new Set(["-u", "--update", "-h", "--help"]);
74
- for (const token of argv) {
75
- if (!known.has(token)) {
76
- process.stderr.write(`error: unknown option '${token}'
77
- `);
78
- return 1;
328
+ function isCliPackageName(value) {
329
+ return value === "@eslint-config-snapshot/cli" || value === "eslint-config-snapshot";
330
+ }
331
+ function readVersionFromResolvedEntry(entryAbs) {
332
+ let current = path.resolve(path.dirname(entryAbs));
333
+ while (true) {
334
+ const packageJsonPath = path.join(current, "package.json");
335
+ if (existsSync(packageJsonPath)) {
336
+ try {
337
+ const raw = readFileSync(packageJsonPath, "utf8");
338
+ const parsed = JSON.parse(raw);
339
+ if (isCliPackageName(parsed.name) && typeof parsed.version === "string" && parsed.version.length > 0) {
340
+ return parsed.version;
341
+ }
342
+ } catch {
343
+ }
79
344
  }
345
+ const parent = path.dirname(current);
346
+ if (parent === current) {
347
+ break;
348
+ }
349
+ current = parent;
80
350
  }
81
- if (argv.includes("-h") || argv.includes("--help")) {
82
- const program = createProgram(cwd, () => {
83
- });
84
- program.outputHelp();
85
- return 0;
86
- }
87
- if (argv.includes("-u") || argv.includes("--update")) {
88
- return executeUpdate(cwd, true);
89
- }
90
- return executeCheck(cwd, "summary", true);
351
+ return void 0;
91
352
  }
92
- function createProgram(cwd, onActionExit) {
93
- const program = new Command();
94
- 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)");
95
- program.hook("preAction", (thisCommand) => {
96
- const opts = thisCommand.opts();
97
- if (opts.update) {
98
- throw new Error("--update can only be used without a command");
99
- }
100
- });
101
- program.command("check").description("Compare current state against stored snapshots").option("--format <format>", "Output format: summary|status|diff", parseCheckFormat, "summary").action(async (opts) => {
102
- onActionExit(await executeCheck(cwd, opts.format));
103
- });
104
- program.command("update").alias("snapshot").description("Compute and write snapshots to .eslint-config-snapshot/").action(async () => {
105
- onActionExit(await executeUpdate(cwd, true));
106
- });
107
- 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) => {
108
- const format = opts.short ? "short" : opts.format;
109
- await executePrint(cwd, format);
110
- onActionExit(0);
111
- });
112
- program.command("config").description("Print effective evaluated config").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 executeConfig(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: recommended|minimal|full", parseInitPreset).option("--show-effective", "Print the evaluated config that will be written").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 select prompts for target/preset.
123
- Recommended preset keeps a dynamic catch-all default group ("*") and asks only for static exception groups.
124
353
 
125
- $ eslint-config-snapshot init --yes --target package-json --preset recommended --show-effective
126
- Non-interactive recommended setup in package.json, with effective preview.
127
-
128
- $ eslint-config-snapshot init --yes --force --target file --preset full
129
- Overwrite-safe bypass when a config is already detected.
130
- `
131
- ).action(async (opts) => {
132
- onActionExit(await runInit(cwd, opts));
133
- });
134
- program.command("compare", { hidden: true }).action(async () => {
135
- onActionExit(await executeCheck(cwd, "diff"));
136
- });
137
- program.command("status", { hidden: true }).action(async () => {
138
- onActionExit(await executeCheck(cwd, "status"));
139
- });
140
- program.command("what-changed", { hidden: true }).action(async () => {
141
- onActionExit(await executeCheck(cwd, "summary"));
142
- });
143
- program.exitOverride();
144
- return program;
354
+ // src/runtime.ts
355
+ import {
356
+ aggregateRules,
357
+ assignGroupsByMatch,
358
+ buildSnapshot,
359
+ diffSnapshots,
360
+ discoverWorkspaces,
361
+ extractRulesForWorkspaceSamples,
362
+ hasDiff,
363
+ loadConfig,
364
+ readSnapshotFile,
365
+ resolveEslintVersionForWorkspace,
366
+ sampleWorkspaceFiles,
367
+ writeSnapshotFile
368
+ } from "@eslint-config-snapshot/api";
369
+ import createDebug from "debug";
370
+ import fg from "fast-glob";
371
+ import { mkdir } from "fs/promises";
372
+ import path2 from "path";
373
+ var debugWorkspace = createDebug("eslint-config-snapshot:workspace");
374
+ var debugDiff = createDebug("eslint-config-snapshot:diff");
375
+ var debugTiming = createDebug("eslint-config-snapshot:timing");
376
+ async function computeCurrentSnapshots(cwd) {
377
+ const computeStartedAt = Date.now();
378
+ const configStartedAt = Date.now();
379
+ const config = await loadConfig(cwd);
380
+ debugTiming("phase=loadConfig elapsedMs=%d", Date.now() - configStartedAt);
381
+ const assignmentStartedAt = Date.now();
382
+ const { discovery, assignments } = await resolveWorkspaceAssignments(cwd, config);
383
+ debugTiming("phase=resolveWorkspaceAssignments elapsedMs=%d", Date.now() - assignmentStartedAt);
384
+ debugWorkspace("root=%s groups=%d workspaces=%d", discovery.rootAbs, assignments.length, discovery.workspacesRel.length);
385
+ const snapshots = /* @__PURE__ */ new Map();
386
+ for (const group of assignments) {
387
+ const groupStartedAt = Date.now();
388
+ const extractedForGroup = [];
389
+ debugWorkspace("group=%s workspaces=%o", group.name, group.workspaces);
390
+ for (const workspaceRel of group.workspaces) {
391
+ const workspaceAbs = path2.resolve(discovery.rootAbs, workspaceRel);
392
+ const sampleStartedAt = Date.now();
393
+ const sampled = await sampleWorkspaceFiles(workspaceAbs, config.sampling);
394
+ debugWorkspace(
395
+ "group=%s workspace=%s sampled=%d sampleElapsedMs=%d files=%o",
396
+ group.name,
397
+ workspaceRel,
398
+ sampled.length,
399
+ Date.now() - sampleStartedAt,
400
+ sampled
401
+ );
402
+ let extractedCount = 0;
403
+ let lastExtractionError;
404
+ const sampledAbs = sampled.map((sampledRel) => path2.resolve(workspaceAbs, sampledRel));
405
+ const extractStartedAt = Date.now();
406
+ const results = await extractRulesForWorkspaceSamples(workspaceAbs, sampledAbs);
407
+ debugTiming(
408
+ "phase=extract group=%s workspace=%s sampled=%d elapsedMs=%d",
409
+ group.name,
410
+ workspaceRel,
411
+ sampledAbs.length,
412
+ Date.now() - extractStartedAt
413
+ );
414
+ for (const result of results) {
415
+ if (result.rules) {
416
+ extractedForGroup.push(result.rules);
417
+ extractedCount += 1;
418
+ continue;
419
+ }
420
+ const message = result.error instanceof Error ? result.error.message : String(result.error);
421
+ if (isRecoverableExtractionError(message)) {
422
+ lastExtractionError = message;
423
+ continue;
424
+ }
425
+ throw result.error ?? new Error(message);
426
+ }
427
+ if (extractedCount === 0) {
428
+ const context = lastExtractionError ? ` Last error: ${lastExtractionError}` : "";
429
+ throw new Error(
430
+ `Unable to extract ESLint config for workspace ${workspaceRel}. All sampled files were ignored or produced non-JSON output.${context}`
431
+ );
432
+ }
433
+ debugWorkspace(
434
+ "group=%s workspace=%s extracted=%d failed=%d",
435
+ group.name,
436
+ workspaceRel,
437
+ extractedCount,
438
+ results.length - extractedCount
439
+ );
440
+ }
441
+ const aggregated = aggregateRules(extractedForGroup);
442
+ snapshots.set(group.name, buildSnapshot(group.name, group.workspaces, aggregated));
443
+ debugWorkspace(
444
+ "group=%s aggregatedRules=%d groupElapsedMs=%d",
445
+ group.name,
446
+ aggregated.size,
447
+ Date.now() - groupStartedAt
448
+ );
449
+ }
450
+ debugTiming("phase=computeCurrentSnapshots elapsedMs=%d", Date.now() - computeStartedAt);
451
+ return snapshots;
145
452
  }
146
- function parseCheckFormat(value) {
147
- const normalized = value.trim().toLowerCase();
148
- if (normalized === "summary" || normalized === "status" || normalized === "diff") {
149
- return normalized;
453
+ function isRecoverableExtractionError(message) {
454
+ return message.startsWith("Invalid JSON from eslint --print-config") || message.startsWith("Empty ESLint print-config output") || message.includes("File ignored because of a matching ignore pattern") || message.includes("File ignored by default");
455
+ }
456
+ async function resolveWorkspaceAssignments(cwd, config) {
457
+ const discovery = await discoverWorkspaces({ cwd, workspaceInput: config.workspaceInput });
458
+ const assignments = config.grouping.mode === "standalone" ? discovery.workspacesRel.map((workspace) => ({ name: workspace, workspaces: [workspace] })) : assignGroupsByMatch(discovery.workspacesRel, config.grouping.groups ?? [{ name: "default", match: ["**/*"] }]);
459
+ const allowEmptyGroups = config.grouping.allowEmptyGroups ?? false;
460
+ if (!allowEmptyGroups) {
461
+ const empty = assignments.filter((group) => group.workspaces.length === 0);
462
+ if (empty.length > 0) {
463
+ throw new Error(`Empty groups are not allowed: ${empty.map((entry) => entry.name).join(", ")}`);
464
+ }
150
465
  }
151
- throw new InvalidArgumentError("Expected one of: summary, status, diff");
466
+ return { discovery, assignments };
152
467
  }
153
- function parsePrintFormat(value) {
154
- const normalized = value.trim().toLowerCase();
155
- if (normalized === "json" || normalized === "short") {
156
- return normalized;
468
+ async function loadStoredSnapshots(cwd, snapshotDir) {
469
+ const dir = path2.join(cwd, snapshotDir);
470
+ const files = await fg("**/*.json", { cwd: dir, absolute: true, onlyFiles: true, dot: true, suppressErrors: true });
471
+ const snapshots = /* @__PURE__ */ new Map();
472
+ const sortedFiles = [...files].sort((a, b) => a.localeCompare(b));
473
+ for (const file of sortedFiles) {
474
+ const snapshot = await readSnapshotFile(file);
475
+ snapshots.set(snapshot.groupId, snapshot);
157
476
  }
158
- throw new InvalidArgumentError("Expected one of: json, short");
477
+ return snapshots;
159
478
  }
160
- function parseInitTarget(value) {
161
- const normalized = value.trim().toLowerCase();
162
- if (normalized === "file" || normalized === "package-json") {
163
- return normalized;
479
+ async function writeSnapshots(cwd, snapshotDir, snapshots) {
480
+ await mkdir(path2.join(cwd, snapshotDir), { recursive: true });
481
+ for (const snapshot of snapshots.values()) {
482
+ await writeSnapshotFile(path2.join(cwd, snapshotDir), snapshot);
164
483
  }
165
- throw new InvalidArgumentError("Expected one of: file, package-json");
166
484
  }
167
- function parseInitPreset(value) {
168
- const normalized = value.trim().toLowerCase();
169
- if (normalized === "recommended" || normalized === "minimal" || normalized === "full") {
170
- return normalized;
485
+ function compareSnapshotMaps(before, after) {
486
+ const startedAt = Date.now();
487
+ const ids = [.../* @__PURE__ */ new Set([...before.keys(), ...after.keys()])].sort();
488
+ const changes = [];
489
+ for (const id of ids) {
490
+ const prev = before.get(id) ?? {
491
+ formatVersion: 1,
492
+ groupId: id,
493
+ workspaces: [],
494
+ rules: {}
495
+ };
496
+ const next = after.get(id) ?? {
497
+ formatVersion: 1,
498
+ groupId: id,
499
+ workspaces: [],
500
+ rules: {}
501
+ };
502
+ const diff = diffSnapshots(prev, next);
503
+ if (hasDiff(diff)) {
504
+ changes.push({ groupId: id, diff });
505
+ }
171
506
  }
172
- throw new InvalidArgumentError("Expected one of: recommended, minimal, full");
507
+ debugDiff("groupsCompared=%d changedGroups=%d elapsedMs=%d", ids.length, changes.length, Date.now() - startedAt);
508
+ return changes;
509
+ }
510
+ async function resolveGroupEslintVersions(cwd) {
511
+ const config = await loadConfig(cwd);
512
+ const { discovery, assignments } = await resolveWorkspaceAssignments(cwd, config);
513
+ const result = /* @__PURE__ */ new Map();
514
+ for (const group of assignments) {
515
+ const versions = /* @__PURE__ */ new Set();
516
+ for (const workspaceRel of group.workspaces) {
517
+ const workspaceAbs = path2.resolve(discovery.rootAbs, workspaceRel);
518
+ versions.add(resolveEslintVersionForWorkspace(workspaceAbs));
519
+ }
520
+ result.set(group.name, [...versions].sort((a, b) => a.localeCompare(b)));
521
+ }
522
+ return result;
173
523
  }
174
- async function executeCheck(cwd, format, defaultInvocation = false) {
524
+
525
+ // src/commands/check.ts
526
+ var UPDATE_HINT = "Tip: when you intentionally accept changes, run `eslint-config-snapshot --update` to refresh the baseline.\n";
527
+ async function executeCheck(cwd, format, terminal, snapshotDir, defaultInvocation = false) {
175
528
  const foundConfig = await findConfigPath(cwd);
176
- const storedSnapshots = await loadStoredSnapshots(cwd);
529
+ const storedSnapshots = await loadStoredSnapshots(cwd, snapshotDir);
177
530
  if (format !== "status") {
178
- writeRunContextHeader(cwd, defaultInvocation ? "check" : `check:${format}`, foundConfig?.path, storedSnapshots);
179
- if (shouldShowRunLogs()) {
180
- writeSubtleInfo("\u{1F50E} Checking current ESLint configuration...\n");
531
+ writeRunContextHeader(terminal, cwd, defaultInvocation ? "check" : `check:${format}`, foundConfig?.path, storedSnapshots);
532
+ if (terminal.showProgress) {
533
+ terminal.subtle("\u{1F50E} Checking current ESLint configuration...\n");
181
534
  }
182
535
  }
183
536
  if (!foundConfig) {
184
- writeSubtleInfo(
537
+ terminal.subtle(
185
538
  "Tip: no explicit config found. Using safe built-in defaults. Run `eslint-config-snapshot init` to customize when needed.\n"
186
539
  );
187
540
  }
@@ -190,7 +543,7 @@ async function executeCheck(cwd, format, defaultInvocation = false) {
190
543
  currentSnapshots = await computeCurrentSnapshots(cwd);
191
544
  } catch (error) {
192
545
  if (!foundConfig) {
193
- process.stdout.write(
546
+ terminal.write(
194
547
  "Automatic workspace discovery could not complete with defaults.\nRun `eslint-config-snapshot init` to configure workspaces, then run `eslint-config-snapshot --update`.\n"
195
548
  );
196
549
  return 1;
@@ -199,121 +552,133 @@ async function executeCheck(cwd, format, defaultInvocation = false) {
199
552
  }
200
553
  if (storedSnapshots.size === 0) {
201
554
  const summary = summarizeSnapshots(currentSnapshots);
202
- process.stdout.write(
555
+ terminal.write(
203
556
  `Rules found in this analysis: ${summary.groups} groups, ${summary.rules} rules (severity mix: ${summary.error} errors, ${summary.warn} warnings, ${summary.off} off).
204
557
  `
205
558
  );
206
559
  const canPromptBaseline = defaultInvocation || format === "summary";
207
- if (canPromptBaseline && process.stdin.isTTY && process.stdout.isTTY) {
208
- const shouldCreateBaseline = await askYesNo(
560
+ if (canPromptBaseline && terminal.isInteractive) {
561
+ const shouldCreateBaseline = await terminal.askYesNo(
209
562
  "No baseline yet. Do you want to save this analyzed rule state as your baseline now? [Y/n] ",
210
563
  true
211
564
  );
212
565
  if (shouldCreateBaseline) {
213
- await writeSnapshots(cwd, currentSnapshots);
214
- const summary2 = summarizeSnapshots(currentSnapshots);
215
- process.stdout.write(`Great start: baseline created with ${summary2.groups} groups and ${summary2.rules} rules.
566
+ await writeSnapshots(cwd, snapshotDir, currentSnapshots);
567
+ const createdSummary = summarizeSnapshots(currentSnapshots);
568
+ terminal.write(`Great start: baseline created with ${createdSummary.groups} groups and ${createdSummary.rules} rules.
216
569
  `);
217
- writeSubtleInfo(UPDATE_HINT);
570
+ terminal.subtle(UPDATE_HINT);
218
571
  return 0;
219
572
  }
220
573
  }
221
- process.stdout.write("You are almost set: no baseline snapshot found yet.\n");
222
- process.stdout.write("Run `eslint-config-snapshot --update` to create your first baseline.\n");
574
+ terminal.write("You are almost set: no baseline snapshot found yet.\n");
575
+ terminal.write("Run `eslint-config-snapshot --update` to create your first baseline.\n");
223
576
  return 1;
224
577
  }
225
578
  const changes = compareSnapshotMaps(storedSnapshots, currentSnapshots);
226
- const eslintVersionsByGroup = shouldShowRunLogs() ? await resolveGroupEslintVersions(cwd) : /* @__PURE__ */ new Map();
579
+ const eslintVersionsByGroup = terminal.showProgress ? await resolveGroupEslintVersions(cwd) : /* @__PURE__ */ new Map();
227
580
  if (format === "status") {
228
581
  if (changes.length === 0) {
229
- process.stdout.write("clean\n");
582
+ terminal.write("clean\n");
230
583
  return 0;
231
584
  }
232
- process.stdout.write("changes\n");
233
- writeSubtleInfo(UPDATE_HINT);
585
+ terminal.write("changes\n");
586
+ terminal.subtle(UPDATE_HINT);
234
587
  return 1;
235
588
  }
236
589
  if (format === "diff") {
237
590
  if (changes.length === 0) {
238
- process.stdout.write("Great news: no snapshot changes detected.\n");
239
- writeEslintVersionSummary(eslintVersionsByGroup);
591
+ terminal.write("Great news: no snapshot changes detected.\n");
592
+ writeEslintVersionSummary(terminal, eslintVersionsByGroup);
240
593
  return 0;
241
594
  }
242
595
  for (const change of changes) {
243
- process.stdout.write(`${formatDiff(change.groupId, change.diff)}
596
+ terminal.write(`${formatDiff(change.groupId, change.diff)}
244
597
  `);
245
598
  }
246
- writeSubtleInfo(UPDATE_HINT);
599
+ terminal.subtle(UPDATE_HINT);
247
600
  return 1;
248
601
  }
249
- return printWhatChanged(changes, currentSnapshots, eslintVersionsByGroup);
602
+ return printWhatChanged(terminal, changes, currentSnapshots, eslintVersionsByGroup);
250
603
  }
251
- async function executeUpdate(cwd, printSummary) {
252
- const foundConfig = await findConfigPath(cwd);
253
- const storedSnapshots = await loadStoredSnapshots(cwd);
254
- writeRunContextHeader(cwd, "update", foundConfig?.path, storedSnapshots);
255
- if (shouldShowRunLogs()) {
256
- writeSubtleInfo("\u{1F50E} Checking current ESLint configuration...\n");
257
- }
258
- if (!foundConfig) {
259
- writeSubtleInfo(
260
- "Tip: no explicit config found. Using safe built-in defaults. Run `eslint-config-snapshot init` to customize when needed.\n"
604
+ function printWhatChanged(terminal, changes, currentSnapshots, eslintVersionsByGroup) {
605
+ const color = terminal.colors;
606
+ const currentSummary = summarizeSnapshots(currentSnapshots);
607
+ const workspaceCount = countUniqueWorkspaces(currentSnapshots);
608
+ const changeSummary = summarizeChanges(changes);
609
+ if (changes.length === 0) {
610
+ terminal.write(color.green("\u2705 Great news: no snapshot drift detected.\n"));
611
+ terminal.section("\u{1F4CA} Summary");
612
+ terminal.write(
613
+ `- \u{1F4E6} baseline: ${currentSummary.groups} groups, ${currentSummary.rules} rules
614
+ - \u{1F5C2}\uFE0F workspaces scanned: ${workspaceCount}
615
+ - \u{1F39A}\uFE0F severity mix: ${currentSummary.error} errors, ${currentSummary.warn} warnings, ${currentSummary.off} off
616
+ `
261
617
  );
618
+ writeEslintVersionSummary(terminal, eslintVersionsByGroup);
619
+ return 0;
262
620
  }
263
- let currentSnapshots;
264
- try {
265
- currentSnapshots = await computeCurrentSnapshots(cwd);
266
- } catch (error) {
267
- if (!foundConfig) {
268
- process.stdout.write(
269
- "Automatic workspace discovery could not complete with defaults.\nRun `eslint-config-snapshot init` to configure workspaces, then run `eslint-config-snapshot --update`.\n"
270
- );
271
- return 1;
272
- }
273
- throw error;
274
- }
275
- await writeSnapshots(cwd, currentSnapshots);
276
- if (printSummary) {
277
- const summary = summarizeSnapshots(currentSnapshots);
278
- const color = createColorizer();
279
- const eslintVersionsByGroup = shouldShowRunLogs() ? await resolveGroupEslintVersions(cwd) : /* @__PURE__ */ new Map();
280
- writeSectionTitle("Summary", color);
281
- process.stdout.write(
282
- `Baseline updated: ${summary.groups} groups, ${summary.rules} rules.
283
- Severity mix: ${summary.error} errors, ${summary.warn} warnings, ${summary.off} off.
621
+ terminal.write(color.red("\u26A0\uFE0F Heads up: snapshot drift detected.\n"));
622
+ terminal.section("\u{1F4CA} Summary");
623
+ terminal.write(
624
+ `- changed groups: ${changes.length}
625
+ - introduced rules: ${changeSummary.introduced}
626
+ - removed rules: ${changeSummary.removed}
627
+ - severity changes: ${changeSummary.severity}
628
+ - options changes: ${changeSummary.options}
629
+ - workspace membership changes: ${changeSummary.workspace}
630
+ - \u{1F5C2}\uFE0F workspaces scanned: ${workspaceCount}
631
+ - current baseline: ${currentSummary.groups} groups, ${currentSummary.rules} rules
632
+ - current severity mix: ${currentSummary.error} errors, ${currentSummary.warn} warnings, ${currentSummary.off} off
284
633
  `
285
- );
286
- writeEslintVersionSummary(eslintVersionsByGroup);
634
+ );
635
+ writeEslintVersionSummary(terminal, eslintVersionsByGroup);
636
+ terminal.write("\n");
637
+ terminal.section("\u{1F9FE} Changes");
638
+ for (const change of changes) {
639
+ terminal.write(color.bold(`group ${change.groupId}
640
+ `));
641
+ const lines = formatDiff(change.groupId, change.diff).split("\n").slice(1);
642
+ for (const line of lines) {
643
+ const decorated = decorateDiffLine(line, color);
644
+ terminal.write(`${decorated}
645
+ `);
646
+ }
647
+ terminal.write("\n");
287
648
  }
288
- return 0;
649
+ terminal.subtle(UPDATE_HINT);
650
+ return 1;
289
651
  }
290
- async function executePrint(cwd, format) {
291
- const foundConfig = await findConfigPath(cwd);
292
- const storedSnapshots = await loadStoredSnapshots(cwd);
293
- writeRunContextHeader(cwd, `print:${format}`, foundConfig?.path, storedSnapshots);
294
- if (shouldShowRunLogs()) {
295
- writeSubtleInfo("\u{1F50E} Checking current ESLint configuration...\n");
652
+
653
+ // src/commands/print.ts
654
+ import { findConfigPath as findConfigPath2, loadConfig as loadConfig2 } from "@eslint-config-snapshot/api";
655
+ async function executePrint(cwd, terminal, snapshotDir, format) {
656
+ const foundConfig = await findConfigPath2(cwd);
657
+ const storedSnapshots = await loadStoredSnapshots(cwd, snapshotDir);
658
+ writeRunContextHeader(terminal, cwd, `print:${format}`, foundConfig?.path, storedSnapshots);
659
+ if (terminal.showProgress) {
660
+ terminal.subtle("\u{1F50E} Checking current ESLint configuration...\n");
296
661
  }
297
662
  const currentSnapshots = await computeCurrentSnapshots(cwd);
298
663
  if (format === "short") {
299
- process.stdout.write(formatShortPrint([...currentSnapshots.values()]));
664
+ terminal.write(formatShortPrint([...currentSnapshots.values()]));
300
665
  return;
301
666
  }
302
667
  const output = [...currentSnapshots.values()].map((snapshot) => ({
303
668
  groupId: snapshot.groupId,
304
669
  rules: snapshot.rules
305
670
  }));
306
- process.stdout.write(`${JSON.stringify(output, null, 2)}
671
+ terminal.write(`${JSON.stringify(output, null, 2)}
307
672
  `);
308
673
  }
309
- async function executeConfig(cwd, format) {
310
- const foundConfig = await findConfigPath(cwd);
311
- const storedSnapshots = await loadStoredSnapshots(cwd);
312
- writeRunContextHeader(cwd, `config:${format}`, foundConfig?.path, storedSnapshots);
313
- if (shouldShowRunLogs()) {
314
- writeSubtleInfo("\u2699\uFE0F Resolving effective runtime configuration...\n");
674
+ async function executeConfig(cwd, terminal, snapshotDir, format) {
675
+ const foundConfig = await findConfigPath2(cwd);
676
+ const storedSnapshots = await loadStoredSnapshots(cwd, snapshotDir);
677
+ writeRunContextHeader(terminal, cwd, `config:${format}`, foundConfig?.path, storedSnapshots);
678
+ if (terminal.showProgress) {
679
+ terminal.subtle("\u2699\uFE0F Resolving effective runtime configuration...\n");
315
680
  }
316
- const config = await loadConfig(cwd);
681
+ const config = await loadConfig2(cwd);
317
682
  const resolved = await resolveWorkspaceAssignments(cwd, config);
318
683
  const payload = {
319
684
  source: foundConfig?.path ?? "built-in-defaults",
@@ -327,150 +692,67 @@ async function executeConfig(cwd, format) {
327
692
  sampling: config.sampling
328
693
  };
329
694
  if (format === "short") {
330
- process.stdout.write(formatShortConfig(payload));
695
+ terminal.write(formatShortConfig(payload));
331
696
  return;
332
697
  }
333
- process.stdout.write(`${JSON.stringify(payload, null, 2)}
698
+ terminal.write(`${JSON.stringify(payload, null, 2)}
334
699
  `);
335
700
  }
336
- async function computeCurrentSnapshots(cwd) {
337
- const config = await loadConfig(cwd);
338
- const { discovery, assignments } = await resolveWorkspaceAssignments(cwd, config);
339
- const snapshots = /* @__PURE__ */ new Map();
340
- for (const group of assignments) {
341
- const extractedForGroup = [];
342
- for (const workspaceRel of group.workspaces) {
343
- const workspaceAbs = path.resolve(discovery.rootAbs, workspaceRel);
344
- const sampled = await sampleWorkspaceFiles(workspaceAbs, config.sampling);
345
- let extractedCount = 0;
346
- let lastExtractionError;
347
- const sampledAbs = sampled.map((sampledRel) => path.resolve(workspaceAbs, sampledRel));
348
- const results = await extractRulesForWorkspaceSamples(workspaceAbs, sampledAbs);
349
- for (const result of results) {
350
- if (result.rules) {
351
- extractedForGroup.push(result.rules);
352
- extractedCount += 1;
353
- continue;
354
- }
355
- const message = result.error instanceof Error ? result.error.message : String(result.error);
356
- if (isRecoverableExtractionError(message)) {
357
- lastExtractionError = message;
358
- continue;
359
- }
360
- throw result.error ?? new Error(message);
361
- }
362
- if (extractedCount === 0) {
363
- const context = lastExtractionError ? ` Last error: ${lastExtractionError}` : "";
364
- throw new Error(
365
- `Unable to extract ESLint config for workspace ${workspaceRel}. All sampled files were ignored or produced non-JSON output.${context}`
366
- );
367
- }
368
- }
369
- const aggregated = aggregateRules(extractedForGroup);
370
- snapshots.set(group.name, buildSnapshot(group.name, group.workspaces, aggregated));
371
- }
372
- return snapshots;
373
- }
374
- function isRecoverableExtractionError(message) {
375
- return message.startsWith("Invalid JSON from eslint --print-config") || message.startsWith("Empty ESLint print-config output") || message.includes("File ignored because of a matching ignore pattern") || message.includes("File ignored by default");
376
- }
377
- async function resolveWorkspaceAssignments(cwd, config) {
378
- const discovery = await discoverWorkspaces({ cwd, workspaceInput: config.workspaceInput });
379
- const assignments = config.grouping.mode === "standalone" ? discovery.workspacesRel.map((workspace) => ({ name: workspace, workspaces: [workspace] })) : assignGroupsByMatch(discovery.workspacesRel, config.grouping.groups ?? [{ name: "default", match: ["**/*"] }]);
380
- const allowEmptyGroups = config.grouping.allowEmptyGroups ?? false;
381
- if (!allowEmptyGroups) {
382
- const empty = assignments.filter((group) => group.workspaces.length === 0);
383
- if (empty.length > 0) {
384
- throw new Error(`Empty groups are not allowed: ${empty.map((entry) => entry.name).join(", ")}`);
385
- }
386
- }
387
- return { discovery, assignments };
388
- }
389
- async function loadStoredSnapshots(cwd) {
390
- const dir = path.join(cwd, SNAPSHOT_DIR);
391
- const files = await fg("**/*.json", { cwd: dir, absolute: true, onlyFiles: true, dot: true, suppressErrors: true });
392
- const snapshots = /* @__PURE__ */ new Map();
393
- const sortedFiles = [...files].sort((a, b) => a.localeCompare(b));
394
- for (const file of sortedFiles) {
395
- const snapshot = await readSnapshotFile(file);
396
- snapshots.set(snapshot.groupId, snapshot);
397
- }
398
- return snapshots;
399
- }
400
- async function writeSnapshots(cwd, snapshots) {
401
- await mkdir(path.join(cwd, SNAPSHOT_DIR), { recursive: true });
402
- for (const snapshot of snapshots.values()) {
403
- await writeSnapshotFile(path.join(cwd, SNAPSHOT_DIR), snapshot);
404
- }
405
- }
406
- function compareSnapshotMaps(before, after) {
407
- const ids = [.../* @__PURE__ */ new Set([...before.keys(), ...after.keys()])].sort();
408
- const changes = [];
409
- for (const id of ids) {
410
- const prev = before.get(id) ?? {
411
- formatVersion: 1,
412
- groupId: id,
413
- workspaces: [],
414
- rules: {}
415
- };
416
- const next = after.get(id) ?? {
417
- formatVersion: 1,
418
- groupId: id,
419
- workspaces: [],
420
- rules: {}
421
- };
422
- const diff = diffSnapshots(prev, next);
423
- if (hasDiff(diff)) {
424
- changes.push({ groupId: id, diff });
425
- }
701
+
702
+ // src/commands/update.ts
703
+ import { findConfigPath as findConfigPath3 } from "@eslint-config-snapshot/api";
704
+ async function executeUpdate(cwd, terminal, snapshotDir, printSummary) {
705
+ const foundConfig = await findConfigPath3(cwd);
706
+ const storedSnapshots = await loadStoredSnapshots(cwd, snapshotDir);
707
+ writeRunContextHeader(terminal, cwd, "update", foundConfig?.path, storedSnapshots);
708
+ if (terminal.showProgress) {
709
+ terminal.subtle("\u{1F50E} Checking current ESLint configuration...\n");
426
710
  }
427
- return changes;
428
- }
429
- function formatDiff(groupId, diff) {
430
- const lines = [`group: ${groupId}`];
431
- addListSection(lines, "introduced rules", diff.introducedRules);
432
- addListSection(lines, "removed rules", diff.removedRules);
433
- if (diff.severityChanges.length > 0) {
434
- lines.push("severity changed:");
435
- for (const change of diff.severityChanges) {
436
- lines.push(` - ${change.rule}: ${change.before} -> ${change.after}`);
437
- }
711
+ if (!foundConfig) {
712
+ terminal.subtle(
713
+ "Tip: no explicit config found. Using safe built-in defaults. Run `eslint-config-snapshot init` to customize when needed.\n"
714
+ );
438
715
  }
439
- const optionChanges = getDisplayOptionChanges(diff);
440
- if (optionChanges.length > 0) {
441
- lines.push("options changed:");
442
- for (const change of optionChanges) {
443
- lines.push(` - ${change.rule}: ${formatValue(change.before)} -> ${formatValue(change.after)}`);
716
+ let currentSnapshots;
717
+ try {
718
+ currentSnapshots = await computeCurrentSnapshots(cwd);
719
+ } catch (error) {
720
+ if (!foundConfig) {
721
+ terminal.write(
722
+ "Automatic workspace discovery could not complete with defaults.\nRun `eslint-config-snapshot init` to configure workspaces, then run `eslint-config-snapshot --update`.\n"
723
+ );
724
+ return 1;
444
725
  }
726
+ throw error;
445
727
  }
446
- addListSection(lines, "workspaces added", diff.workspaceMembershipChanges.added);
447
- addListSection(lines, "workspaces removed", diff.workspaceMembershipChanges.removed);
448
- return lines.join("\n");
449
- }
450
- function addListSection(lines, title, values) {
451
- if (values.length === 0) {
452
- return;
453
- }
454
- lines.push(`${title}:`);
455
- for (const value of values) {
456
- lines.push(` - ${value}`);
728
+ await writeSnapshots(cwd, snapshotDir, currentSnapshots);
729
+ if (printSummary) {
730
+ const summary = summarizeSnapshots(currentSnapshots);
731
+ const workspaceCount = countUniqueWorkspaces(currentSnapshots);
732
+ const eslintVersionsByGroup = terminal.showProgress ? await resolveGroupEslintVersions(cwd) : /* @__PURE__ */ new Map();
733
+ terminal.section("\u{1F4CA} Summary");
734
+ terminal.write(
735
+ `Baseline updated: ${summary.groups} groups, ${summary.rules} rules.
736
+ Workspaces scanned: ${workspaceCount}.
737
+ Severity mix: ${summary.error} errors, ${summary.warn} warnings, ${summary.off} off.
738
+ `
739
+ );
740
+ writeEslintVersionSummary(terminal, eslintVersionsByGroup);
457
741
  }
742
+ return 0;
458
743
  }
459
- function formatValue(value) {
460
- const serialized = JSON.stringify(value);
461
- return serialized === void 0 ? "undefined" : serialized;
462
- }
463
- function getDisplayOptionChanges(diff) {
464
- const removedRules = new Set(diff.removedRules);
465
- const severityChangedRules = new Set(diff.severityChanges.map((change) => change.rule));
466
- return diff.optionChanges.filter((change) => !removedRules.has(change.rule) && !severityChangedRules.has(change.rule));
467
- }
468
- async function runInit(cwd, opts = {}) {
744
+
745
+ // src/init.ts
746
+ import { discoverWorkspaces as discoverWorkspaces2, findConfigPath as findConfigPath4, getConfigScaffold, normalizePath as normalizePath2 } from "@eslint-config-snapshot/api";
747
+ import fg2 from "fast-glob";
748
+ import { access, readFile, writeFile } from "fs/promises";
749
+ import path3 from "path";
750
+ async function runInit(cwd, opts, runtime) {
469
751
  const force = opts.force ?? false;
470
752
  const showEffective = opts.showEffective ?? false;
471
- const existing = await findConfigPath(cwd);
753
+ const existing = await findConfigPath4(cwd);
472
754
  if (existing && !force) {
473
- process.stderr.write(
755
+ runtime.writeStderr(
474
756
  `Existing config detected at ${existing.path}. Creating another config can cause conflicts. Remove the existing config or rerun with --force.
475
757
  `
476
758
  );
@@ -479,27 +761,27 @@ async function runInit(cwd, opts = {}) {
479
761
  let target = opts.target;
480
762
  let preset = opts.preset;
481
763
  if (!opts.yes && !target && !preset && process.stdin.isTTY && process.stdout.isTTY) {
482
- const interactive = await askInitPreferences();
764
+ const interactive = await askInitPreferences(runtime);
483
765
  target = interactive.target;
484
766
  preset = interactive.preset;
485
767
  }
486
768
  const finalTarget = target ?? "file";
487
769
  const finalPreset = preset ?? "recommended";
488
- const configObject = await resolveInitConfigObject(cwd, finalPreset, Boolean(opts.yes));
770
+ const configObject = await resolveInitConfigObject(cwd, finalPreset, Boolean(opts.yes), runtime);
489
771
  if (showEffective) {
490
- process.stdout.write(`Effective config preview:
772
+ runtime.writeStdout(`Effective config preview:
491
773
  ${JSON.stringify(configObject, null, 2)}
492
774
  `);
493
775
  }
494
776
  if (finalTarget === "package-json") {
495
- return runInitInPackageJson(cwd, configObject, force);
777
+ return runInitInPackageJson(cwd, configObject, force, runtime);
496
778
  }
497
- return runInitInFile(cwd, configObject, force);
779
+ return runInitInFile(cwd, configObject, force, runtime);
498
780
  }
499
- async function askInitPreferences() {
781
+ async function askInitPreferences(runtime) {
500
782
  const { select } = await import("@inquirer/prompts");
501
- const target = await runPromptWithPausedTimer(() => askInitTarget(select));
502
- const preset = await runPromptWithPausedTimer(() => askInitPreset(select));
783
+ const target = await runtime.runPromptWithPausedTimer(() => askInitTarget(select));
784
+ const preset = await runtime.runPromptWithPausedTimer(() => askInitPreset(select));
503
785
  return { target, preset };
504
786
  }
505
787
  async function askInitTarget(selectPrompt) {
@@ -521,29 +803,7 @@ async function askInitPreset(selectPrompt) {
521
803
  ]
522
804
  });
523
805
  }
524
- function askQuestion(rl, prompt) {
525
- pauseRunTimer();
526
- return new Promise((resolve) => {
527
- rl.question(prompt, (answer) => {
528
- resumeRunTimer();
529
- resolve(answer);
530
- });
531
- });
532
- }
533
- async function askYesNo(prompt, defaultYes) {
534
- const rl = createInterface({ input: process.stdin, output: process.stdout });
535
- try {
536
- const answerRaw = await askQuestion(rl, prompt);
537
- const answer = answerRaw.trim().toLowerCase();
538
- if (answer.length === 0) {
539
- return defaultYes;
540
- }
541
- return answer === "y" || answer === "yes";
542
- } finally {
543
- rl.close();
544
- }
545
- }
546
- async function runInitInFile(cwd, configObject, force) {
806
+ async function runInitInFile(cwd, configObject, force, runtime) {
547
807
  const candidates = [
548
808
  ".eslint-config-snapshot.js",
549
809
  ".eslint-config-snapshot.cjs",
@@ -554,60 +814,60 @@ async function runInitInFile(cwd, configObject, force) {
554
814
  ];
555
815
  for (const candidate of candidates) {
556
816
  try {
557
- await access(path.join(cwd, candidate));
817
+ await access(path3.join(cwd, candidate));
558
818
  if (!force) {
559
- process.stderr.write(`Config already exists: ${candidate}
819
+ runtime.writeStderr(`Config already exists: ${candidate}
560
820
  `);
561
821
  return 1;
562
822
  }
563
823
  } catch {
564
824
  }
565
825
  }
566
- const target = path.join(cwd, "eslint-config-snapshot.config.mjs");
826
+ const target = path3.join(cwd, "eslint-config-snapshot.config.mjs");
567
827
  await writeFile(target, toConfigScaffold(configObject), "utf8");
568
- process.stdout.write(`Created ${path.basename(target)}
828
+ runtime.writeStdout(`Created ${path3.basename(target)}
569
829
  `);
570
830
  return 0;
571
831
  }
572
- async function runInitInPackageJson(cwd, configObject, force) {
573
- const packageJsonPath = path.join(cwd, "package.json");
832
+ async function runInitInPackageJson(cwd, configObject, force, runtime) {
833
+ const packageJsonPath = path3.join(cwd, "package.json");
574
834
  let packageJsonRaw;
575
835
  try {
576
836
  packageJsonRaw = await readFile(packageJsonPath, "utf8");
577
837
  } catch {
578
- process.stderr.write("package.json not found in current directory.\n");
838
+ runtime.writeStderr("package.json not found in current directory.\n");
579
839
  return 1;
580
840
  }
581
841
  let parsed;
582
842
  try {
583
843
  parsed = JSON.parse(packageJsonRaw);
584
844
  } catch {
585
- process.stderr.write("Invalid package.json (must be valid JSON).\n");
845
+ runtime.writeStderr("Invalid package.json (must be valid JSON).\n");
586
846
  return 1;
587
847
  }
588
848
  if (parsed["eslint-config-snapshot"] !== void 0 && !force) {
589
- process.stderr.write("Config already exists in package.json: eslint-config-snapshot\n");
849
+ runtime.writeStderr("Config already exists in package.json: eslint-config-snapshot\n");
590
850
  return 1;
591
851
  }
592
852
  parsed["eslint-config-snapshot"] = configObject;
593
853
  await writeFile(packageJsonPath, `${JSON.stringify(parsed, null, 2)}
594
854
  `, "utf8");
595
- process.stdout.write('Created config in package.json under "eslint-config-snapshot"\n');
855
+ runtime.writeStdout('Created config in package.json under "eslint-config-snapshot"\n');
596
856
  return 0;
597
857
  }
598
- async function resolveInitConfigObject(cwd, preset, nonInteractive) {
858
+ async function resolveInitConfigObject(cwd, preset, nonInteractive, runtime) {
599
859
  if (preset === "minimal") {
600
860
  return {};
601
861
  }
602
862
  if (preset === "full") {
603
863
  return getFullPresetObject();
604
864
  }
605
- return buildRecommendedPresetObject(cwd, nonInteractive);
865
+ return buildRecommendedPresetObject(cwd, nonInteractive, runtime);
606
866
  }
607
- async function buildRecommendedPresetObject(cwd, nonInteractive) {
867
+ async function buildRecommendedPresetObject(cwd, nonInteractive, runtime) {
608
868
  const workspaces = await discoverInitWorkspaces(cwd);
609
869
  const useInteractiveGrouping = !nonInteractive && process.stdin.isTTY && process.stdout.isTTY;
610
- const assignments = useInteractiveGrouping ? await askRecommendedGroupAssignments(workspaces) : /* @__PURE__ */ new Map();
870
+ const assignments = useInteractiveGrouping ? await askRecommendedGroupAssignments(workspaces, runtime) : /* @__PURE__ */ new Map();
611
871
  return buildRecommendedConfigFromAssignments(workspaces, assignments);
612
872
  }
613
873
  function buildRecommendedConfigFromAssignments(workspaces, assignments) {
@@ -627,11 +887,11 @@ function buildRecommendedConfigFromAssignments(workspaces, assignments) {
627
887
  };
628
888
  }
629
889
  async function discoverInitWorkspaces(cwd) {
630
- const discovered = await discoverWorkspaces({ cwd, workspaceInput: { mode: "discover" } });
890
+ const discovered = await discoverWorkspaces2({ cwd, workspaceInput: { mode: "discover" } });
631
891
  if (!(discovered.workspacesRel.length === 1 && discovered.workspacesRel[0] === ".")) {
632
892
  return discovered.workspacesRel;
633
893
  }
634
- const packageJsonPath = path.join(cwd, "package.json");
894
+ const packageJsonPath = path3.join(cwd, "package.json");
635
895
  try {
636
896
  const raw = await readFile(packageJsonPath, "utf8");
637
897
  const parsed = JSON.parse(raw);
@@ -644,11 +904,12 @@ async function discoverInitWorkspaces(cwd) {
644
904
  if (workspacePatterns.length === 0) {
645
905
  return discovered.workspacesRel;
646
906
  }
647
- const workspacePackageFiles = await fg(
648
- workspacePatterns.map((pattern) => `${trimTrailingSlashes(pattern)}/package.json`),
649
- { cwd, onlyFiles: true, dot: true }
650
- );
651
- const workspaceDirs = [...new Set(workspacePackageFiles.map((entry) => normalizePath(path.dirname(entry))))].sort(
907
+ const workspacePackageFiles = await fg2(workspacePatterns.map((pattern) => `${trimTrailingSlashes(pattern)}/package.json`), {
908
+ cwd,
909
+ onlyFiles: true,
910
+ dot: true
911
+ });
912
+ const workspaceDirs = [...new Set(workspacePackageFiles.map((entry) => normalizePath2(path3.dirname(entry))))].sort(
652
913
  (a, b) => a.localeCompare(b)
653
914
  );
654
915
  if (workspaceDirs.length > 0) {
@@ -665,13 +926,11 @@ function trimTrailingSlashes(value) {
665
926
  }
666
927
  return normalized;
667
928
  }
668
- async function askRecommendedGroupAssignments(workspaces) {
929
+ async function askRecommendedGroupAssignments(workspaces, runtime) {
669
930
  const { checkbox, select } = await import("@inquirer/prompts");
670
- process.stdout.write(
671
- 'Recommended setup: default group "*" is a dynamic catch-all for every discovered workspace.\n'
672
- );
673
- process.stdout.write("Select only workspaces that should move to explicit static groups.\n");
674
- const overrides = await runPromptWithPausedTimer(
931
+ runtime.writeStdout('Recommended setup: default group "*" is a dynamic catch-all for every discovered workspace.\n');
932
+ runtime.writeStdout("Select only workspaces that should move to explicit static groups.\n");
933
+ const overrides = await runtime.runPromptWithPausedTimer(
675
934
  () => checkbox({
676
935
  message: 'Choose exception workspaces (leave empty to keep all in default "*"):',
677
936
  choices: workspaces.map((workspace) => ({ name: workspace, value: workspace })),
@@ -685,7 +944,7 @@ async function askRecommendedGroupAssignments(workspaces) {
685
944
  while (usedGroups.includes(nextGroup)) {
686
945
  nextGroup += 1;
687
946
  }
688
- const selected = await runPromptWithPausedTimer(
947
+ const selected = await runtime.runPromptWithPausedTimer(
689
948
  () => select({
690
949
  message: `Select group for ${workspace}`,
691
950
  choices: [
@@ -714,439 +973,358 @@ function getFullPresetObject() {
714
973
  groups: [{ name: "default", match: ["**/*"] }]
715
974
  },
716
975
  sampling: {
717
- maxFilesPerWorkspace: 8,
718
- includeGlobs: ["**/*.{js,jsx,ts,tsx,cjs,mjs}"],
976
+ maxFilesPerWorkspace: 10,
977
+ includeGlobs: ["**/*.{js,jsx,ts,tsx,cjs,mjs,md,mdx}"],
719
978
  excludeGlobs: ["**/node_modules/**", "**/dist/**"],
720
- hintGlobs: []
979
+ tokenHints: [
980
+ "chunk",
981
+ "conf",
982
+ "config",
983
+ "container",
984
+ "controller",
985
+ "helpers",
986
+ "mock",
987
+ "mocks",
988
+ "presentation",
989
+ "repository",
990
+ "route",
991
+ "routes",
992
+ "schema",
993
+ "setup",
994
+ "spec",
995
+ "stories",
996
+ "style",
997
+ "styles",
998
+ "test",
999
+ "type",
1000
+ "types",
1001
+ "utils",
1002
+ "view",
1003
+ "views"
1004
+ ]
721
1005
  }
722
1006
  };
723
1007
  }
724
- async function main() {
725
- const code = await runArgv(process.argv.slice(2), process.cwd());
726
- process.exit(code);
727
- }
728
- function isDirectCliExecution() {
729
- const entry = process.argv[1];
730
- if (!entry) {
731
- return false;
732
- }
733
- const normalized = path.basename(entry).toLowerCase();
734
- return normalized === "index.js" || normalized === "index.cjs" || normalized === "index.ts" || normalized === "eslint-config-snapshot";
735
- }
736
- if (isDirectCliExecution()) {
737
- void main();
738
- }
739
- function printWhatChanged(changes, currentSnapshots, eslintVersionsByGroup) {
740
- const color = createColorizer();
741
- const currentSummary = summarizeSnapshots(currentSnapshots);
742
- const changeSummary = summarizeChanges(changes);
743
- if (changes.length === 0) {
744
- process.stdout.write(color.green("Great news: no snapshot drift detected.\n"));
745
- writeSectionTitle("Summary", color);
746
- process.stdout.write(
747
- `- baseline: ${currentSummary.groups} groups, ${currentSummary.rules} rules
748
- - severity mix: ${currentSummary.error} errors, ${currentSummary.warn} warnings, ${currentSummary.off} off
749
- `
750
- );
751
- writeEslintVersionSummary(eslintVersionsByGroup);
752
- return 0;
753
- }
754
- process.stdout.write(color.red("Heads up: snapshot drift detected.\n"));
755
- writeSectionTitle("Summary", color);
756
- process.stdout.write(
757
- `- changed groups: ${changes.length}
758
- - introduced rules: ${changeSummary.introduced}
759
- - removed rules: ${changeSummary.removed}
760
- - severity changes: ${changeSummary.severity}
761
- - options changes: ${changeSummary.options}
762
- - workspace membership changes: ${changeSummary.workspace}
763
- - current baseline: ${currentSummary.groups} groups, ${currentSummary.rules} rules
764
- - current severity mix: ${currentSummary.error} errors, ${currentSummary.warn} warnings, ${currentSummary.off} off
765
- `
766
- );
767
- writeEslintVersionSummary(eslintVersionsByGroup);
768
- process.stdout.write("\n");
769
- writeSectionTitle("Changes", color);
770
- for (const change of changes) {
771
- process.stdout.write(color.bold(`group ${change.groupId}
772
- `));
773
- const lines = formatDiff(change.groupId, change.diff).split("\n").slice(1);
774
- for (const line of lines) {
775
- const decorated = decorateDiffLine(line, color);
776
- process.stdout.write(`${decorated}
777
- `);
1008
+
1009
+ // src/terminal.ts
1010
+ import { createInterface } from "readline";
1011
+ var TerminalIO = class {
1012
+ color = createColorizer();
1013
+ activeRunTimer;
1014
+ get isTTY() {
1015
+ return process.stdout.isTTY === true;
1016
+ }
1017
+ get isInteractive() {
1018
+ return process.stdin.isTTY === true && process.stdout.isTTY === true;
1019
+ }
1020
+ get showProgress() {
1021
+ if (process.env.ESLINT_CONFIG_SNAPSHOT_NO_PROGRESS === "1") {
1022
+ return false;
778
1023
  }
779
- process.stdout.write("\n");
780
- }
781
- writeSubtleInfo(UPDATE_HINT);
782
- return 1;
783
- }
784
- function writeSectionTitle(title, color) {
785
- process.stdout.write(`${color.bold(title)}
786
- `);
787
- }
788
- function summarizeChanges(changes) {
789
- let introduced = 0;
790
- let removed = 0;
791
- let severity = 0;
792
- let options = 0;
793
- let workspace = 0;
794
- for (const change of changes) {
795
- introduced += change.diff.introducedRules.length;
796
- removed += change.diff.removedRules.length;
797
- severity += change.diff.severityChanges.length;
798
- options += getDisplayOptionChanges(change.diff).length;
799
- workspace += change.diff.workspaceMembershipChanges.added.length + change.diff.workspaceMembershipChanges.removed.length;
800
- }
801
- return { introduced, removed, severity, options, workspace };
802
- }
803
- function summarizeSnapshots(snapshots) {
804
- const { rules, error, warn, off } = countRuleSeverities([...snapshots.values()].map((snapshot) => snapshot.rules));
805
- return { groups: snapshots.size, rules, error, warn, off };
806
- }
807
- function decorateDiffLine(line, color) {
808
- if (line.startsWith("introduced rules:") || line.startsWith("workspaces added:")) {
809
- return color.green(`+ ${line}`);
810
- }
811
- if (line.startsWith("removed rules:") || line.startsWith("workspaces removed:")) {
812
- return color.red(`- ${line}`);
813
- }
814
- if (line.startsWith("severity changed:") || line.startsWith("options changed:")) {
815
- return color.yellow(`~ ${line}`);
816
- }
817
- return line;
818
- }
819
- function createColorizer() {
820
- const enabled = process.stdout.isTTY && process.env.NO_COLOR === void 0 && process.env.TERM !== "dumb";
821
- const wrap = (code, text) => enabled ? `\x1B[${code}m${text}\x1B[0m` : text;
822
- return {
823
- green: (text) => wrap("32", text),
824
- yellow: (text) => wrap("33", text),
825
- red: (text) => wrap("31", text),
826
- bold: (text) => wrap("1", text),
827
- dim: (text) => wrap("2", text)
828
- };
829
- }
830
- function writeSubtleInfo(text) {
831
- const color = createColorizer();
832
- process.stdout.write(color.dim(text));
833
- }
834
- function resolveInvocationLabel(argv) {
835
- const commandToken = argv.find((entry) => !entry.startsWith("-"));
836
- if (commandToken) {
837
- return commandToken;
838
- }
839
- if (argv.includes("-u") || argv.includes("--update")) {
840
- return "update";
841
- }
842
- if (argv.includes("-h") || argv.includes("--help")) {
843
- return "help";
1024
+ return this.isTTY;
844
1025
  }
845
- return "check";
846
- }
847
- function shouldShowRunLogs() {
848
- if (process.env.ESLINT_CONFIG_SNAPSHOT_NO_PROGRESS === "1") {
849
- return false;
1026
+ get colors() {
1027
+ return this.color;
850
1028
  }
851
- return process.stdout.isTTY === true;
852
- }
853
- function beginRunTimer(label) {
854
- if (!shouldShowRunLogs()) {
855
- activeRunTimer = void 0;
856
- return;
1029
+ write(text) {
1030
+ process.stdout.write(text);
857
1031
  }
858
- activeRunTimer = {
859
- label,
860
- startedAtMs: Date.now(),
861
- pausedMs: 0,
862
- pauseStartedAtMs: void 0
863
- };
864
- }
865
- function endRunTimer(exitCode) {
866
- if (!activeRunTimer || !shouldShowRunLogs()) {
867
- return;
1032
+ error(text) {
1033
+ process.stderr.write(text);
868
1034
  }
869
- if (activeRunTimer.pauseStartedAtMs !== void 0) {
870
- activeRunTimer.pausedMs += Date.now() - activeRunTimer.pauseStartedAtMs;
871
- activeRunTimer.pauseStartedAtMs = void 0;
1035
+ subtle(text) {
1036
+ this.write(this.color.dim(text));
872
1037
  }
873
- const elapsedMs = Math.max(0, Date.now() - activeRunTimer.startedAtMs - activeRunTimer.pausedMs);
874
- const seconds = (elapsedMs / 1e3).toFixed(2);
875
- if (exitCode === 0) {
876
- writeSubtleInfo(`Finished in ${seconds}s
1038
+ section(title) {
1039
+ this.write(`${this.color.bold(title)}
877
1040
  `);
878
- } else {
879
- writeSubtleInfo(`Finished with errors in ${seconds}s
880
- `);
881
- }
882
- activeRunTimer = void 0;
883
- }
884
- function pauseRunTimer() {
885
- if (!activeRunTimer || activeRunTimer.pauseStartedAtMs !== void 0) {
886
- return;
887
- }
888
- activeRunTimer.pauseStartedAtMs = Date.now();
889
- }
890
- function resumeRunTimer() {
891
- if (!activeRunTimer || activeRunTimer.pauseStartedAtMs === void 0) {
892
- return;
893
- }
894
- activeRunTimer.pausedMs += Date.now() - activeRunTimer.pauseStartedAtMs;
895
- activeRunTimer.pauseStartedAtMs = void 0;
896
- }
897
- async function runPromptWithPausedTimer(prompt) {
898
- pauseRunTimer();
899
- try {
900
- return await prompt();
901
- } finally {
902
- resumeRunTimer();
903
- }
904
- }
905
- function readCliVersion() {
906
- if (cachedCliVersion !== void 0) {
907
- return cachedCliVersion;
908
1041
  }
909
- const envPackageName = process.env.npm_package_name;
910
- const envPackageVersion = process.env.npm_package_version;
911
- if (isCliPackageName(envPackageName) && typeof envPackageVersion === "string" && envPackageVersion.length > 0) {
912
- cachedCliVersion = envPackageVersion;
913
- return cachedCliVersion;
914
- }
915
- const scriptPath = process.argv[1];
916
- if (!scriptPath) {
917
- cachedCliVersion = "unknown";
918
- return cachedCliVersion;
919
- }
920
- try {
921
- const req = createRequire(path.resolve(scriptPath));
922
- const resolvedCliEntry = req.resolve("@eslint-config-snapshot/cli");
923
- const resolvedVersion = readVersionFromResolvedEntry(resolvedCliEntry);
924
- if (resolvedVersion !== void 0) {
925
- cachedCliVersion = resolvedVersion;
926
- return cachedCliVersion;
927
- }
928
- } catch {
929
- }
930
- let current = path.resolve(path.dirname(scriptPath));
931
- let fallbackVersion;
932
- while (true) {
933
- const packageJsonPath = path.join(current, "package.json");
934
- if (existsSync(packageJsonPath)) {
935
- try {
936
- const raw = readFileSync(packageJsonPath, "utf8");
937
- const parsed = JSON.parse(raw);
938
- if (typeof parsed.version === "string" && parsed.version.length > 0) {
939
- if (isCliPackageName(parsed.name)) {
940
- cachedCliVersion = parsed.version;
941
- return cachedCliVersion;
942
- }
943
- if (fallbackVersion === void 0) {
944
- fallbackVersion = parsed.version;
945
- }
946
- }
947
- } catch {
948
- }
949
- }
950
- const parent = path.dirname(current);
951
- if (parent === current) {
952
- break;
953
- }
954
- current = parent;
1042
+ success(text) {
1043
+ this.write(this.color.green(text));
955
1044
  }
956
- cachedCliVersion = fallbackVersion ?? "unknown";
957
- return cachedCliVersion;
958
- }
959
- function isCliPackageName(value) {
960
- return value === "@eslint-config-snapshot/cli" || value === "eslint-config-snapshot";
961
- }
962
- function readVersionFromResolvedEntry(entryAbs) {
963
- let current = path.resolve(path.dirname(entryAbs));
964
- while (true) {
965
- const packageJsonPath = path.join(current, "package.json");
966
- if (existsSync(packageJsonPath)) {
967
- try {
968
- const raw = readFileSync(packageJsonPath, "utf8");
969
- const parsed = JSON.parse(raw);
970
- if (isCliPackageName(parsed.name) && typeof parsed.version === "string" && parsed.version.length > 0) {
971
- return parsed.version;
972
- }
973
- } catch {
974
- }
975
- }
976
- const parent = path.dirname(current);
977
- if (parent === current) {
978
- break;
979
- }
980
- current = parent;
1045
+ warning(text) {
1046
+ this.write(this.color.yellow(text));
981
1047
  }
982
- return void 0;
983
- }
984
- function writeRunContextHeader(cwd, commandLabel, configPath, storedSnapshots) {
985
- if (!shouldShowRunLogs()) {
986
- return;
1048
+ danger(text) {
1049
+ this.write(this.color.red(text));
987
1050
  }
988
- const color = createColorizer();
989
- process.stdout.write(color.bold(`eslint-config-snapshot v${readCliVersion()} \u2022 ${formatCommandDisplayLabel(commandLabel)}
990
- `));
991
- process.stdout.write(`\u{1F4C1} Repository: ${cwd}
992
- `);
993
- process.stdout.write(`\u{1F4C1} Baseline: ${formatStoredSnapshotSummary(storedSnapshots)}
994
- `);
995
- process.stdout.write(`\u2699\uFE0F Config source: ${formatConfigSource(cwd, configPath)}
996
- `);
997
- process.stdout.write("\n");
998
- }
999
- function formatCommandDisplayLabel(commandLabel) {
1000
- switch (commandLabel) {
1001
- case "check":
1002
- case "check:summary": {
1003
- return "Check drift against baseline (summary)";
1004
- }
1005
- case "check:diff": {
1006
- return "Check drift against baseline (detailed diff)";
1007
- }
1008
- case "check:status": {
1009
- return "Check drift against baseline (status only)";
1010
- }
1011
- case "update": {
1012
- return "Update baseline snapshot";
1013
- }
1014
- case "print:json": {
1015
- return "Print aggregated rules (JSON)";
1051
+ bold(text) {
1052
+ return this.color.bold(text);
1053
+ }
1054
+ beginRun(label) {
1055
+ if (!this.showProgress) {
1056
+ this.activeRunTimer = void 0;
1057
+ return;
1016
1058
  }
1017
- case "print:short": {
1018
- return "Print aggregated rules (short view)";
1059
+ this.activeRunTimer = {
1060
+ label,
1061
+ startedAtMs: Date.now(),
1062
+ pausedMs: 0,
1063
+ pauseStartedAtMs: void 0
1064
+ };
1065
+ }
1066
+ endRun(exitCode, logTiming) {
1067
+ if (!this.activeRunTimer || !this.showProgress) {
1068
+ return;
1019
1069
  }
1020
- case "config:json": {
1021
- return "Show effective runtime config (JSON)";
1070
+ if (this.activeRunTimer.pauseStartedAtMs !== void 0) {
1071
+ this.activeRunTimer.pausedMs += Date.now() - this.activeRunTimer.pauseStartedAtMs;
1072
+ this.activeRunTimer.pauseStartedAtMs = void 0;
1022
1073
  }
1023
- case "config:short": {
1024
- return "Show effective runtime config (short view)";
1074
+ const elapsedMs = Math.max(0, Date.now() - this.activeRunTimer.startedAtMs - this.activeRunTimer.pausedMs);
1075
+ logTiming(this.activeRunTimer, elapsedMs);
1076
+ const seconds = (elapsedMs / 1e3).toFixed(2);
1077
+ this.subtle(exitCode === 0 ? `\u23F1\uFE0F Finished in ${seconds}s
1078
+ ` : `\u23F1\uFE0F Finished with errors in ${seconds}s
1079
+ `);
1080
+ this.activeRunTimer = void 0;
1081
+ }
1082
+ pauseRun() {
1083
+ if (!this.activeRunTimer || this.activeRunTimer.pauseStartedAtMs !== void 0) {
1084
+ return;
1025
1085
  }
1026
- case "init": {
1027
- return "Initialize local configuration";
1086
+ this.activeRunTimer.pauseStartedAtMs = Date.now();
1087
+ }
1088
+ resumeRun() {
1089
+ if (!this.activeRunTimer || this.activeRunTimer.pauseStartedAtMs === void 0) {
1090
+ return;
1028
1091
  }
1029
- case "help": {
1030
- return "Show CLI help";
1092
+ this.activeRunTimer.pausedMs += Date.now() - this.activeRunTimer.pauseStartedAtMs;
1093
+ this.activeRunTimer.pauseStartedAtMs = void 0;
1094
+ }
1095
+ async withPausedRunTimer(task) {
1096
+ this.pauseRun();
1097
+ try {
1098
+ return await task();
1099
+ } finally {
1100
+ this.resumeRun();
1031
1101
  }
1032
- default: {
1033
- return commandLabel;
1102
+ }
1103
+ async askYesNo(prompt, defaultYes) {
1104
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1105
+ try {
1106
+ const answerRaw = await this.askQuestion(rl, prompt);
1107
+ const answer = answerRaw.trim().toLowerCase();
1108
+ if (answer.length === 0) {
1109
+ return defaultYes;
1110
+ }
1111
+ return answer === "y" || answer === "yes";
1112
+ } finally {
1113
+ rl.close();
1034
1114
  }
1035
1115
  }
1036
- }
1037
- function formatConfigSource(cwd, configPath) {
1038
- if (!configPath) {
1039
- return "built-in defaults";
1116
+ async askQuestion(rl, prompt) {
1117
+ this.pauseRun();
1118
+ return new Promise((resolve) => {
1119
+ rl.question(prompt, (answer) => {
1120
+ this.resumeRun();
1121
+ resolve(answer);
1122
+ });
1123
+ });
1040
1124
  }
1041
- const rel = normalizePath(path.relative(cwd, configPath));
1042
- if (path.basename(configPath) === "package.json") {
1043
- return `${rel} (eslint-config-snapshot field)`;
1125
+ };
1126
+ function resolveInvocationLabel(argv) {
1127
+ const commandToken = argv.find((entry) => !entry.startsWith("-"));
1128
+ if (commandToken) {
1129
+ return commandToken;
1044
1130
  }
1045
- return rel;
1046
- }
1047
- function formatStoredSnapshotSummary(storedSnapshots) {
1048
- if (storedSnapshots.size === 0) {
1049
- return "none";
1131
+ if (argv.includes("-u") || argv.includes("--update")) {
1132
+ return "update";
1050
1133
  }
1051
- const summary = summarizeStoredSnapshots(storedSnapshots);
1052
- return `${summary.groups} groups, ${summary.rules} rules (severity mix: ${summary.error} errors, ${summary.warn} warnings, ${summary.off} off)`;
1134
+ if (argv.includes("-h") || argv.includes("--help")) {
1135
+ return "help";
1136
+ }
1137
+ return "check";
1053
1138
  }
1054
- async function resolveGroupEslintVersions(cwd) {
1055
- const config = await loadConfig(cwd);
1056
- const { discovery, assignments } = await resolveWorkspaceAssignments(cwd, config);
1057
- const result = /* @__PURE__ */ new Map();
1058
- for (const group of assignments) {
1059
- const versions = /* @__PURE__ */ new Set();
1060
- for (const workspaceRel of group.workspaces) {
1061
- const workspaceAbs = path.resolve(discovery.rootAbs, workspaceRel);
1062
- versions.add(resolveEslintVersionForWorkspace(workspaceAbs));
1139
+ function createColorizer() {
1140
+ const enabled = process.stdout.isTTY && process.env.NO_COLOR === void 0 && process.env.TERM !== "dumb";
1141
+ const wrap = (code, text) => enabled ? `\x1B[${code}m${text}\x1B[0m` : text;
1142
+ return {
1143
+ green: (text) => wrap("32", text),
1144
+ yellow: (text) => wrap("33", text),
1145
+ red: (text) => wrap("31", text),
1146
+ bold: (text) => wrap("1", text),
1147
+ dim: (text) => wrap("2", text)
1148
+ };
1149
+ }
1150
+
1151
+ // src/index.ts
1152
+ var SNAPSHOT_DIR = ".eslint-config-snapshot";
1153
+ var debugRun = createDebug2("eslint-config-snapshot:run");
1154
+ var debugTiming2 = createDebug2("eslint-config-snapshot:timing");
1155
+ async function runCli(command, cwd, flags = []) {
1156
+ const argv = command ? [command, ...flags] : [...flags];
1157
+ return runArgv(argv, cwd);
1158
+ }
1159
+ async function runArgv(argv, cwd) {
1160
+ const terminal = new TerminalIO();
1161
+ const invocationLabel = resolveInvocationLabel(argv);
1162
+ terminal.beginRun(invocationLabel);
1163
+ debugRun("start label=%s cwd=%s argv=%o", invocationLabel, cwd, argv);
1164
+ let exitCode = 1;
1165
+ try {
1166
+ const hasCommandToken = argv.some((token) => !token.startsWith("-"));
1167
+ if (!hasCommandToken) {
1168
+ exitCode = await runDefaultInvocation(argv, cwd, terminal);
1169
+ return exitCode;
1063
1170
  }
1064
- result.set(group.name, [...versions].sort((a, b) => a.localeCompare(b)));
1171
+ let actionCode;
1172
+ const program = createProgram(cwd, terminal, (code) => {
1173
+ actionCode = code;
1174
+ });
1175
+ try {
1176
+ await program.parseAsync(argv, { from: "user" });
1177
+ } catch (error) {
1178
+ if (error instanceof CommanderError) {
1179
+ if (error.code === "commander.helpDisplayed") {
1180
+ exitCode = 0;
1181
+ return exitCode;
1182
+ }
1183
+ exitCode = error.exitCode;
1184
+ return exitCode;
1185
+ }
1186
+ const message = error instanceof Error ? error.message : String(error);
1187
+ terminal.error(`${message}
1188
+ `);
1189
+ return 1;
1190
+ }
1191
+ exitCode = actionCode ?? 0;
1192
+ debugRun("done label=%s exitCode=%d", invocationLabel, exitCode);
1193
+ return exitCode;
1194
+ } finally {
1195
+ terminal.endRun(exitCode, (timer, elapsedMs) => {
1196
+ debugTiming2(
1197
+ "command=%s exitCode=%d elapsedMs=%d pausedMs=%d",
1198
+ timer.label,
1199
+ exitCode,
1200
+ elapsedMs,
1201
+ timer.pausedMs
1202
+ );
1203
+ });
1065
1204
  }
1066
- return result;
1067
1205
  }
1068
- function writeEslintVersionSummary(eslintVersionsByGroup) {
1069
- if (!shouldShowRunLogs() || eslintVersionsByGroup.size === 0) {
1070
- return;
1071
- }
1072
- const allVersions = /* @__PURE__ */ new Set();
1073
- for (const versions of eslintVersionsByGroup.values()) {
1074
- for (const version of versions) {
1075
- allVersions.add(version);
1206
+ async function runDefaultInvocation(argv, cwd, terminal) {
1207
+ const known = /* @__PURE__ */ new Set(["-u", "--update", "-h", "--help"]);
1208
+ for (const token of argv) {
1209
+ if (!known.has(token)) {
1210
+ terminal.error(`error: unknown option '${token}'
1211
+ `);
1212
+ return 1;
1076
1213
  }
1077
1214
  }
1078
- const sortedAllVersions = [...allVersions].sort((a, b) => a.localeCompare(b));
1079
- if (sortedAllVersions.length === 1) {
1080
- process.stdout.write(`- eslint runtime: ${sortedAllVersions[0]} (all groups)
1081
- `);
1082
- return;
1215
+ if (argv.includes("-h") || argv.includes("--help")) {
1216
+ const program = createProgram(cwd, terminal, () => {
1217
+ });
1218
+ program.outputHelp();
1219
+ return 0;
1083
1220
  }
1084
- process.stdout.write("- eslint runtime by group:\n");
1085
- const sortedEntries = [...eslintVersionsByGroup.entries()].sort((a, b) => a[0].localeCompare(b[0]));
1086
- for (const [groupName, versions] of sortedEntries) {
1087
- process.stdout.write(` - ${groupName}: ${versions.join(", ")}
1088
- `);
1221
+ if (argv.includes("-u") || argv.includes("--update")) {
1222
+ return executeUpdate(cwd, terminal, SNAPSHOT_DIR, true);
1089
1223
  }
1224
+ return executeCheck(cwd, "summary", terminal, SNAPSHOT_DIR, true);
1090
1225
  }
1091
- function summarizeStoredSnapshots(snapshots) {
1092
- const { rules, error, warn, off } = countRuleSeverities([...snapshots.values()].map((snapshot) => snapshot.rules));
1093
- return { groups: snapshots.size, rules, error, warn, off };
1094
- }
1095
- function countRuleSeverities(ruleObjects) {
1096
- let rules = 0;
1097
- let error = 0;
1098
- let warn = 0;
1099
- let off = 0;
1100
- for (const rulesObject of ruleObjects) {
1101
- for (const entry of Object.values(rulesObject)) {
1102
- rules += 1;
1103
- if (entry[0] === "error") {
1104
- error += 1;
1105
- } else if (entry[0] === "warn") {
1106
- warn += 1;
1107
- } else {
1108
- off += 1;
1109
- }
1226
+ function createProgram(cwd, terminal, onActionExit) {
1227
+ const program = new Command();
1228
+ 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)");
1229
+ program.hook("preAction", (thisCommand) => {
1230
+ const opts = thisCommand.opts();
1231
+ if (opts.update) {
1232
+ throw new Error("--update can only be used without a command");
1110
1233
  }
1234
+ });
1235
+ program.command("check").description("Compare current state against stored snapshots").option("--format <format>", "Output format: summary|status|diff", parseCheckFormat, "summary").action(async (opts) => {
1236
+ onActionExit(await executeCheck(cwd, opts.format, terminal, SNAPSHOT_DIR));
1237
+ });
1238
+ program.command("update").alias("snapshot").description("Compute and write snapshots to .eslint-config-snapshot/").action(async () => {
1239
+ onActionExit(await executeUpdate(cwd, terminal, SNAPSHOT_DIR, true));
1240
+ });
1241
+ 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) => {
1242
+ const format = opts.short ? "short" : opts.format;
1243
+ await executePrint(cwd, terminal, SNAPSHOT_DIR, format);
1244
+ onActionExit(0);
1245
+ });
1246
+ program.command("config").description("Print effective evaluated config").option("--format <format>", "Output format: json|short", parsePrintFormat, "json").option("--short", "Alias for --format short").action(async (opts) => {
1247
+ const format = opts.short ? "short" : opts.format;
1248
+ await executeConfig(cwd, terminal, SNAPSHOT_DIR, format);
1249
+ onActionExit(0);
1250
+ });
1251
+ program.command("init").description("Initialize config (file or package.json)").option("--target <target>", "Config target: file|package-json", parseInitTarget).option("--preset <preset>", "Config preset: recommended|minimal|full", parseInitPreset).option("--show-effective", "Print the evaluated config that will be written").option("-f, --force", "Allow init even when an existing config is detected").option("-y, --yes", "Skip prompts and use defaults/options").addHelpText(
1252
+ "after",
1253
+ `
1254
+ Examples:
1255
+ $ eslint-config-snapshot init
1256
+ Runs interactive select prompts for target/preset.
1257
+ Recommended preset keeps a dynamic catch-all default group ("*") and asks only for static exception groups.
1258
+
1259
+ $ eslint-config-snapshot init --yes --target package-json --preset recommended --show-effective
1260
+ Non-interactive recommended setup in package.json, with effective preview.
1261
+
1262
+ $ eslint-config-snapshot init --yes --force --target file --preset full
1263
+ Overwrite-safe bypass when a config is already detected.
1264
+ `
1265
+ ).action(async (opts) => {
1266
+ onActionExit(
1267
+ await runInit(cwd, opts, {
1268
+ runPromptWithPausedTimer: terminal.withPausedRunTimer.bind(terminal),
1269
+ writeStdout: terminal.write.bind(terminal),
1270
+ writeStderr: terminal.error.bind(terminal)
1271
+ })
1272
+ );
1273
+ });
1274
+ program.command("compare", { hidden: true }).action(async () => {
1275
+ onActionExit(await executeCheck(cwd, "diff", terminal, SNAPSHOT_DIR));
1276
+ });
1277
+ program.command("status", { hidden: true }).action(async () => {
1278
+ onActionExit(await executeCheck(cwd, "status", terminal, SNAPSHOT_DIR));
1279
+ });
1280
+ program.command("what-changed", { hidden: true }).action(async () => {
1281
+ onActionExit(await executeCheck(cwd, "summary", terminal, SNAPSHOT_DIR));
1282
+ });
1283
+ program.exitOverride();
1284
+ return program;
1285
+ }
1286
+ function parseCheckFormat(value) {
1287
+ const normalized = value.trim().toLowerCase();
1288
+ if (normalized === "summary" || normalized === "status" || normalized === "diff") {
1289
+ return normalized;
1111
1290
  }
1112
- return { rules, error, warn, off };
1291
+ throw new InvalidArgumentError("Expected one of: summary, status, diff");
1113
1292
  }
1114
- function formatShortPrint(snapshots) {
1115
- const lines = [];
1116
- const sorted = [...snapshots].sort((a, b) => a.groupId.localeCompare(b.groupId));
1117
- for (const snapshot of sorted) {
1118
- const ruleNames = Object.keys(snapshot.rules).sort();
1119
- const severityCounts = { error: 0, warn: 0, off: 0 };
1120
- for (const name of ruleNames) {
1121
- const severity = snapshot.rules[name][0];
1122
- severityCounts[severity] += 1;
1123
- }
1124
- lines.push(
1125
- `group: ${snapshot.groupId}`,
1126
- `workspaces (${snapshot.workspaces.length}): ${snapshot.workspaces.length > 0 ? snapshot.workspaces.join(", ") : "(none)"}`,
1127
- `rules (${ruleNames.length}): error ${severityCounts.error}, warn ${severityCounts.warn}, off ${severityCounts.off}`
1128
- );
1129
- for (const ruleName of ruleNames) {
1130
- const entry = snapshot.rules[ruleName];
1131
- const suffix = entry.length > 1 ? ` ${JSON.stringify(entry[1])}` : "";
1132
- lines.push(`${ruleName}: ${entry[0]}${suffix}`);
1133
- }
1293
+ function parsePrintFormat(value) {
1294
+ const normalized = value.trim().toLowerCase();
1295
+ if (normalized === "json" || normalized === "short") {
1296
+ return normalized;
1134
1297
  }
1135
- return `${lines.join("\n")}
1136
- `;
1298
+ throw new InvalidArgumentError("Expected one of: json, short");
1137
1299
  }
1138
- function formatShortConfig(payload) {
1139
- const lines = [
1140
- `source: ${payload.source}`,
1141
- `workspaces (${payload.workspaces.length}): ${payload.workspaces.join(", ") || "(none)"}`,
1142
- `grouping mode: ${payload.grouping.mode} (allow empty: ${payload.grouping.allowEmptyGroups})`
1143
- ];
1144
- for (const group of payload.grouping.groups) {
1145
- lines.push(`group ${group.name} (${group.workspaces.length}): ${group.workspaces.join(", ") || "(none)"}`);
1300
+ function parseInitTarget(value) {
1301
+ const normalized = value.trim().toLowerCase();
1302
+ if (normalized === "file" || normalized === "package-json") {
1303
+ return normalized;
1146
1304
  }
1147
- lines.push(`workspaceInput: ${JSON.stringify(payload.workspaceInput)}`, `sampling: ${JSON.stringify(payload.sampling)}`);
1148
- return `${lines.join("\n")}
1149
- `;
1305
+ throw new InvalidArgumentError("Expected one of: file, package-json");
1306
+ }
1307
+ function parseInitPreset(value) {
1308
+ const normalized = value.trim().toLowerCase();
1309
+ if (normalized === "recommended" || normalized === "minimal" || normalized === "full") {
1310
+ return normalized;
1311
+ }
1312
+ throw new InvalidArgumentError("Expected one of: recommended, minimal, full");
1313
+ }
1314
+ async function main() {
1315
+ const code = await runArgv(process.argv.slice(2), process.cwd());
1316
+ process.exitCode = code;
1317
+ }
1318
+ function isDirectCliExecution() {
1319
+ const entry = process.argv[1];
1320
+ if (!entry) {
1321
+ return false;
1322
+ }
1323
+ const normalized = path4.basename(entry).toLowerCase();
1324
+ return normalized === "index.js" || normalized === "index.cjs" || normalized === "index.ts" || normalized === "eslint-config-snapshot";
1325
+ }
1326
+ if (isDirectCliExecution()) {
1327
+ void main();
1150
1328
  }
1151
1329
  export {
1152
1330
  buildRecommendedConfigFromAssignments,