@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/CHANGELOG.md +66 -0
- package/README.md +10 -0
- package/dist/index.cjs +975 -794
- package/dist/index.js +978 -800
- package/package.json +3 -2
- package/src/commands/check.ts +157 -0
- package/src/commands/print.ts +58 -0
- package/src/commands/update.ts +49 -0
- package/src/formatters.ts +256 -0
- package/src/index.ts +48 -1204
- package/src/init.ts +331 -0
- package/src/run-context.ts +161 -0
- package/src/runtime.ts +224 -0
- package/src/terminal.ts +178 -0
- package/test/cli.integration.test.ts +4 -6
- package/test/cli.npm-isolated.integration.test.ts +1 -1
- package/test/cli.pnpm-isolated.integration.test.ts +1 -1
- package/test/cli.terminal.integration.test.ts +10 -5
- package/test/fixtures/npm-isolated-template/eslint-config-snapshot.config.mjs +1 -2
- package/test/fixtures/repo/eslint-config-snapshot.config.mjs +1 -2
- package/test/formatters.unit.test.ts +47 -0
- package/test/init.unit.test.ts +31 -0
- package/test/runtime.unit.test.ts +36 -0
- package/test/ui.unit.test.ts +12 -0
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
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
466
|
+
return { discovery, assignments };
|
|
152
467
|
}
|
|
153
|
-
function
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
477
|
+
return snapshots;
|
|
159
478
|
}
|
|
160
|
-
function
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
180
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 &&
|
|
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
|
|
215
|
-
|
|
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
|
-
|
|
570
|
+
terminal.subtle(UPDATE_HINT);
|
|
218
571
|
return 0;
|
|
219
572
|
}
|
|
220
573
|
}
|
|
221
|
-
|
|
222
|
-
|
|
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 =
|
|
579
|
+
const eslintVersionsByGroup = terminal.showProgress ? await resolveGroupEslintVersions(cwd) : /* @__PURE__ */ new Map();
|
|
227
580
|
if (format === "status") {
|
|
228
581
|
if (changes.length === 0) {
|
|
229
|
-
|
|
582
|
+
terminal.write("clean\n");
|
|
230
583
|
return 0;
|
|
231
584
|
}
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
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
|
-
|
|
596
|
+
terminal.write(`${formatDiff(change.groupId, change.diff)}
|
|
244
597
|
`);
|
|
245
598
|
}
|
|
246
|
-
|
|
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
|
-
|
|
252
|
-
const
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
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
|
-
|
|
649
|
+
terminal.subtle(UPDATE_HINT);
|
|
650
|
+
return 1;
|
|
289
651
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
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
|
-
|
|
671
|
+
terminal.write(`${JSON.stringify(output, null, 2)}
|
|
307
672
|
`);
|
|
308
673
|
}
|
|
309
|
-
async function executeConfig(cwd, format) {
|
|
310
|
-
const foundConfig = await
|
|
311
|
-
const storedSnapshots = await loadStoredSnapshots(cwd);
|
|
312
|
-
writeRunContextHeader(cwd, `config:${format}`, foundConfig?.path, storedSnapshots);
|
|
313
|
-
if (
|
|
314
|
-
|
|
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
|
|
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
|
-
|
|
695
|
+
terminal.write(formatShortConfig(payload));
|
|
331
696
|
return;
|
|
332
697
|
}
|
|
333
|
-
|
|
698
|
+
terminal.write(`${JSON.stringify(payload, null, 2)}
|
|
334
699
|
`);
|
|
335
700
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
|
753
|
+
const existing = await findConfigPath4(cwd);
|
|
472
754
|
if (existing && !force) {
|
|
473
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
817
|
+
await access(path3.join(cwd, candidate));
|
|
558
818
|
if (!force) {
|
|
559
|
-
|
|
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 =
|
|
826
|
+
const target = path3.join(cwd, "eslint-config-snapshot.config.mjs");
|
|
567
827
|
await writeFile(target, toConfigScaffold(configObject), "utf8");
|
|
568
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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
|
-
|
|
671
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
return
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
859
|
-
|
|
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
|
-
|
|
870
|
-
|
|
871
|
-
activeRunTimer.pauseStartedAtMs = void 0;
|
|
1035
|
+
subtle(text) {
|
|
1036
|
+
this.write(this.color.dim(text));
|
|
872
1037
|
}
|
|
873
|
-
|
|
874
|
-
|
|
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
|
-
|
|
910
|
-
|
|
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
|
-
|
|
957
|
-
|
|
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
|
-
|
|
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
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
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
|
-
|
|
1018
|
-
|
|
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
|
-
|
|
1021
|
-
|
|
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
|
-
|
|
1024
|
-
|
|
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
|
-
|
|
1027
|
-
|
|
1086
|
+
this.activeRunTimer.pauseStartedAtMs = Date.now();
|
|
1087
|
+
}
|
|
1088
|
+
resumeRun() {
|
|
1089
|
+
if (!this.activeRunTimer || this.activeRunTimer.pauseStartedAtMs === void 0) {
|
|
1090
|
+
return;
|
|
1028
1091
|
}
|
|
1029
|
-
|
|
1030
|
-
|
|
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
|
-
|
|
1033
|
-
|
|
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
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
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
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1125
|
+
};
|
|
1126
|
+
function resolveInvocationLabel(argv) {
|
|
1127
|
+
const commandToken = argv.find((entry) => !entry.startsWith("-"));
|
|
1128
|
+
if (commandToken) {
|
|
1129
|
+
return commandToken;
|
|
1044
1130
|
}
|
|
1045
|
-
|
|
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
|
-
|
|
1052
|
-
|
|
1134
|
+
if (argv.includes("-h") || argv.includes("--help")) {
|
|
1135
|
+
return "help";
|
|
1136
|
+
}
|
|
1137
|
+
return "check";
|
|
1053
1138
|
}
|
|
1054
|
-
|
|
1055
|
-
const
|
|
1056
|
-
const
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
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
|
-
|
|
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
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
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
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
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
|
-
|
|
1085
|
-
|
|
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
|
|
1092
|
-
const
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
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
|
-
|
|
1291
|
+
throw new InvalidArgumentError("Expected one of: summary, status, diff");
|
|
1113
1292
|
}
|
|
1114
|
-
function
|
|
1115
|
-
const
|
|
1116
|
-
|
|
1117
|
-
|
|
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
|
-
|
|
1136
|
-
`;
|
|
1298
|
+
throw new InvalidArgumentError("Expected one of: json, short");
|
|
1137
1299
|
}
|
|
1138
|
-
function
|
|
1139
|
-
const
|
|
1140
|
-
|
|
1141
|
-
|
|
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
|
-
|
|
1148
|
-
|
|
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,
|