@eslint-config-snapshot/cli 1.3.0 → 1.3.2
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 +16 -0
- package/dist/index.cjs +92 -26
- package/dist/index.js +92 -26
- package/package.json +2 -2
- package/src/commands/catalog.ts +26 -4
- package/src/formatters.ts +95 -6
- package/src/index.ts +3 -2
- package/src/terminal.ts +2 -0
- package/test/cli.integration.test.ts +12 -3
- package/test/cli.terminal.integration.test.ts +16 -4
- package/test/formatters.unit.test.ts +31 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @eslint-config-snapshot/cli
|
|
2
2
|
|
|
3
|
+
## 1.3.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Refine catalog output defaults with optional detailed grouped rule states and semantic colorized labels.
|
|
8
|
+
- Updated dependencies
|
|
9
|
+
- @eslint-config-snapshot/api@1.3.2
|
|
10
|
+
|
|
11
|
+
## 1.3.1
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- Improve catalog check/update summaries and dedupe baseline totals across groups.
|
|
16
|
+
- Updated dependencies
|
|
17
|
+
- @eslint-config-snapshot/api@1.3.1
|
|
18
|
+
|
|
3
19
|
## 1.3.0
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
package/dist/index.cjs
CHANGED
|
@@ -102,7 +102,7 @@ function summarizeChanges(changes) {
|
|
|
102
102
|
return { introduced, removed, severity, options, workspace };
|
|
103
103
|
}
|
|
104
104
|
function summarizeSnapshots(snapshots) {
|
|
105
|
-
const { rules, error, warn, off } =
|
|
105
|
+
const { rules, error, warn, off } = countUniqueRuleSeverities([...snapshots.values()].map((snapshot) => snapshot.rules));
|
|
106
106
|
return { groups: snapshots.size, rules, error, warn, off };
|
|
107
107
|
}
|
|
108
108
|
function countUniqueWorkspaces(snapshots) {
|
|
@@ -185,7 +185,8 @@ function formatShortConfig(payload) {
|
|
|
185
185
|
return `${lines.join("\n")}
|
|
186
186
|
`;
|
|
187
187
|
}
|
|
188
|
-
function formatShortCatalog(catalogs,
|
|
188
|
+
function formatShortCatalog(catalogs, options) {
|
|
189
|
+
const { missingOnly, detailed, color } = options;
|
|
189
190
|
const lines = [];
|
|
190
191
|
const sorted = [...catalogs].sort((a, b) => a.groupId.localeCompare(b.groupId));
|
|
191
192
|
const showGroupHeader = sorted.length > 1 || sorted.some((catalog) => catalog.groupId !== "default");
|
|
@@ -201,10 +202,8 @@ function formatShortCatalog(catalogs, missingOnly) {
|
|
|
201
202
|
for (const plugin of catalog.pluginStats) {
|
|
202
203
|
lines.push(` - ${plugin.pluginId}: ${formatUsageLine(plugin)}`);
|
|
203
204
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
for (const ruleName of detailRules) {
|
|
207
|
-
lines.push(` - ${ruleName}`);
|
|
205
|
+
if (detailed) {
|
|
206
|
+
appendRuleStateGroups(lines, catalog, missingOnly, color);
|
|
208
207
|
}
|
|
209
208
|
if (showGroupHeader) {
|
|
210
209
|
lines.push("");
|
|
@@ -219,6 +218,44 @@ function formatShortCatalog(catalogs, missingOnly) {
|
|
|
219
218
|
function formatUsageLine(stats) {
|
|
220
219
|
return `${stats.inUse}/${stats.totalAvailable} in use (${stats.inUsePct}%) | error ${stats.error} | warn ${stats.warn} | off ${stats.off} | not used ${stats.missing}`;
|
|
221
220
|
}
|
|
221
|
+
function appendRuleStateGroups(lines, catalog, missingOnly, color) {
|
|
222
|
+
if (missingOnly) {
|
|
223
|
+
appendRuleList(lines, color?.yellow("\u{1F7E1} unused") ?? "\u{1F7E1} unused", catalog.missingRules);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const observedRuleSet = new Set(catalog.observedRules);
|
|
227
|
+
const errorRules = [];
|
|
228
|
+
const warnRules = [];
|
|
229
|
+
const offRules = [];
|
|
230
|
+
const unusedRules = [];
|
|
231
|
+
for (const ruleName of catalog.availableRules) {
|
|
232
|
+
if (!observedRuleSet.has(ruleName)) {
|
|
233
|
+
unusedRules.push(ruleName);
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
const level = catalog.observedRuleLevels[ruleName];
|
|
237
|
+
if (level === "off") {
|
|
238
|
+
offRules.push(ruleName);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (level === "error") {
|
|
242
|
+
errorRules.push(ruleName);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
warnRules.push(ruleName);
|
|
246
|
+
}
|
|
247
|
+
appendRuleList(lines, color?.green("\u{1F7E2} error") ?? "\u{1F7E2} error", errorRules);
|
|
248
|
+
appendRuleList(lines, color?.green("\u{1F7E2} warn") ?? "\u{1F7E2} warn", warnRules);
|
|
249
|
+
appendRuleList(lines, color?.cyan("\u{1F535} off") ?? "\u{1F535} off", offRules);
|
|
250
|
+
appendRuleList(lines, color?.yellow("\u{1F7E1} unused") ?? "\u{1F7E1} unused", unusedRules);
|
|
251
|
+
}
|
|
252
|
+
function appendRuleList(lines, label, rules) {
|
|
253
|
+
lines.push(`${label} (${rules.length}):`);
|
|
254
|
+
const sortedRules = [...rules].sort((a, b) => a.localeCompare(b));
|
|
255
|
+
for (const ruleName of sortedRules) {
|
|
256
|
+
lines.push(` - ${ruleName}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
222
259
|
function formatCommandDisplayLabel(commandLabel) {
|
|
223
260
|
switch (commandLabel) {
|
|
224
261
|
case "check":
|
|
@@ -276,25 +313,34 @@ function formatBaselineSummaryLines(summary, workspaceCount) {
|
|
|
276
313
|
- \u{1F39A}\uFE0F severity mix: ${summary.error} errors, ${summary.warn} warnings, ${summary.off} off
|
|
277
314
|
`;
|
|
278
315
|
}
|
|
279
|
-
function
|
|
280
|
-
|
|
316
|
+
function countUniqueRuleSeverities(ruleObjects) {
|
|
317
|
+
const severityRank = { off: 0, warn: 1, error: 2 };
|
|
318
|
+
const severityByRule = /* @__PURE__ */ new Map();
|
|
319
|
+
for (const rulesObject of ruleObjects) {
|
|
320
|
+
for (const [ruleName, entry] of Object.entries(rulesObject)) {
|
|
321
|
+
const nextSeverity = getPrimarySeverity(entry);
|
|
322
|
+
if (!nextSeverity) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
const currentSeverity = severityByRule.get(ruleName);
|
|
326
|
+
if (!currentSeverity || severityRank[nextSeverity] > severityRank[currentSeverity]) {
|
|
327
|
+
severityByRule.set(ruleName, nextSeverity);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
281
331
|
let error = 0;
|
|
282
332
|
let warn = 0;
|
|
283
333
|
let off = 0;
|
|
284
|
-
for (const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
warn += 1;
|
|
292
|
-
} else {
|
|
293
|
-
off += 1;
|
|
294
|
-
}
|
|
334
|
+
for (const severity of severityByRule.values()) {
|
|
335
|
+
if (severity === "error") {
|
|
336
|
+
error += 1;
|
|
337
|
+
} else if (severity === "warn") {
|
|
338
|
+
warn += 1;
|
|
339
|
+
} else {
|
|
340
|
+
off += 1;
|
|
295
341
|
}
|
|
296
342
|
}
|
|
297
|
-
return { rules, error, warn, off };
|
|
343
|
+
return { rules: severityByRule.size, error, warn, off };
|
|
298
344
|
}
|
|
299
345
|
function getPrimarySeverity(entry) {
|
|
300
346
|
if (!entry) {
|
|
@@ -845,10 +891,10 @@ function isDefaultEquivalentConfig(config) {
|
|
|
845
891
|
|
|
846
892
|
// src/commands/catalog.ts
|
|
847
893
|
var CATALOG_FILE_SUFFIX = ".catalog.json";
|
|
848
|
-
async function executeCatalog(cwd, terminal, snapshotDir, format, missingOnly) {
|
|
894
|
+
async function executeCatalog(cwd, terminal, snapshotDir, format, missingOnly, detailed) {
|
|
849
895
|
const rows = await computeCatalogRows(cwd, terminal, snapshotDir, `catalog:${format}`, true);
|
|
850
896
|
if (format === "short") {
|
|
851
|
-
terminal.write(formatShortCatalog(rows, missingOnly));
|
|
897
|
+
terminal.write(formatShortCatalog(rows, { missingOnly, detailed, color: terminal.colors }));
|
|
852
898
|
return 0;
|
|
853
899
|
}
|
|
854
900
|
const output = rows.map((row) => {
|
|
@@ -871,10 +917,12 @@ async function executeCatalogUpdate(cwd, terminal, snapshotDir) {
|
|
|
871
917
|
const rows = await computeCatalogRows(cwd, terminal, snapshotDir, "catalog:update", false);
|
|
872
918
|
await writeCatalogBaselineFiles(cwd, snapshotDir, rows);
|
|
873
919
|
const groups = rows.length;
|
|
874
|
-
const available = rows.
|
|
875
|
-
const inUse = rows.
|
|
920
|
+
const available = countUniqueRules(rows.map((row) => row.availableRules));
|
|
921
|
+
const inUse = countUniqueRules(rows.map((row) => row.observedRules.filter((ruleName) => row.availableRules.includes(ruleName))));
|
|
876
922
|
terminal.write(`\u{1F9EA} Catalog baseline updated: ${groups} groups, ${available} available rules, ${inUse} currently in use.
|
|
877
923
|
`);
|
|
924
|
+
terminal.section("\u{1F4CA} Catalog summary");
|
|
925
|
+
terminal.write(formatShortCatalog(rows, { missingOnly: false, detailed: false, color: terminal.colors }));
|
|
878
926
|
return 0;
|
|
879
927
|
}
|
|
880
928
|
async function executeCatalogCheck(cwd, terminal, snapshotDir) {
|
|
@@ -889,8 +937,12 @@ async function executeCatalogCheck(cwd, terminal, snapshotDir) {
|
|
|
889
937
|
const diffs = compareCatalogBaselines(stored, current);
|
|
890
938
|
if (diffs.length === 0) {
|
|
891
939
|
terminal.write("Great news: no catalog drift detected.\n");
|
|
940
|
+
terminal.section("\u{1F4CA} Catalog summary");
|
|
941
|
+
terminal.write(formatShortCatalog(rows, { missingOnly: false, detailed: false, color: terminal.colors }));
|
|
892
942
|
return 0;
|
|
893
943
|
}
|
|
944
|
+
terminal.section("\u{1F4CA} Catalog summary");
|
|
945
|
+
terminal.write(formatShortCatalog(rows, { missingOnly: false, detailed: false, color: terminal.colors }));
|
|
894
946
|
terminal.write(`\u26A0\uFE0F Heads up: catalog drift detected in ${diffs.length} groups.
|
|
895
947
|
`);
|
|
896
948
|
for (const diff of diffs) {
|
|
@@ -935,6 +987,9 @@ async function computeCatalogRows(cwd, terminal, snapshotDir, commandLabel, prin
|
|
|
935
987
|
const availableRules = catalog?.allRules ?? [];
|
|
936
988
|
const availableRuleSet = new Set(availableRules);
|
|
937
989
|
const missingRules = availableRules.filter((ruleName) => !snapshot.rules[ruleName]);
|
|
990
|
+
const observedRuleLevels = Object.fromEntries(
|
|
991
|
+
observedRules.map((ruleName) => [ruleName, getPrimarySeverity2(snapshot.rules[ruleName])])
|
|
992
|
+
);
|
|
938
993
|
const observedOffRules = observedRules.filter((ruleName) => isRuleOffOnly(snapshot.rules[ruleName]));
|
|
939
994
|
const observedActiveRules = observedRules.filter((ruleName) => !isRuleOffOnly(snapshot.rules[ruleName]));
|
|
940
995
|
const observedOutsideCatalog = observedRules.filter((ruleName) => !availableRuleSet.has(ruleName)).length;
|
|
@@ -955,6 +1010,7 @@ async function computeCatalogRows(cwd, terminal, snapshotDir, commandLabel, prin
|
|
|
955
1010
|
coreRules,
|
|
956
1011
|
pluginRulesByPrefix,
|
|
957
1012
|
observedRules,
|
|
1013
|
+
observedRuleLevels,
|
|
958
1014
|
missingRules,
|
|
959
1015
|
observedOffRules,
|
|
960
1016
|
observedActiveRules,
|
|
@@ -1131,6 +1187,15 @@ function toPercent(value, total) {
|
|
|
1131
1187
|
}
|
|
1132
1188
|
return Number((value / total * 100).toFixed(1));
|
|
1133
1189
|
}
|
|
1190
|
+
function countUniqueRules(ruleLists) {
|
|
1191
|
+
const unique = /* @__PURE__ */ new Set();
|
|
1192
|
+
for (const rules of ruleLists) {
|
|
1193
|
+
for (const rule of rules) {
|
|
1194
|
+
unique.add(rule);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
return unique.size;
|
|
1198
|
+
}
|
|
1134
1199
|
|
|
1135
1200
|
// src/commands/check.ts
|
|
1136
1201
|
var UPDATE_HINT = "Tip: when you intentionally accept changes, run `eslint-config-snapshot --update` to refresh the baseline.\n";
|
|
@@ -1751,6 +1816,7 @@ function createColorizer() {
|
|
|
1751
1816
|
return {
|
|
1752
1817
|
green: (text) => wrap("32", text),
|
|
1753
1818
|
yellow: (text) => wrap("33", text),
|
|
1819
|
+
cyan: (text) => wrap("36", text),
|
|
1754
1820
|
red: (text) => wrap("31", text),
|
|
1755
1821
|
bold: (text) => wrap("1", text),
|
|
1756
1822
|
dim: (text) => wrap("2", text)
|
|
@@ -1877,9 +1943,9 @@ function createProgram(cwd, terminal, onActionExit) {
|
|
|
1877
1943
|
const format = opts.short ? "short" : opts.format;
|
|
1878
1944
|
onActionExit(await executePrint(cwd, terminal, SNAPSHOT_DIR, format));
|
|
1879
1945
|
});
|
|
1880
|
-
program.command("catalog").description("Print discovered rule catalog and missing rules").option("--format <format>", "Output format: json|short", parsePrintFormat, "json").option("--short", "Alias for --format short").option("--missing", "Only print rules that are available but not observed in current snapshots").action(async (opts) => {
|
|
1946
|
+
program.command("catalog").description("Print discovered rule catalog and missing rules").option("--format <format>", "Output format: json|short", parsePrintFormat, "json").option("--short", "Alias for --format short").option("--detailed", "Show detailed rule lists grouped by error, warn, off, and unused").option("--missing", "Only print rules that are available but not observed in current snapshots").action(async (opts) => {
|
|
1881
1947
|
const format = opts.short ? "short" : opts.format;
|
|
1882
|
-
onActionExit(await executeCatalog(cwd, terminal, SNAPSHOT_DIR, format, Boolean(opts.missing)));
|
|
1948
|
+
onActionExit(await executeCatalog(cwd, terminal, SNAPSHOT_DIR, format, Boolean(opts.missing), Boolean(opts.detailed)));
|
|
1883
1949
|
});
|
|
1884
1950
|
program.command("catalog-check").description("Compare current catalog against stored catalog baseline").action(async () => {
|
|
1885
1951
|
onActionExit(await executeCatalogCheck(cwd, terminal, SNAPSHOT_DIR));
|
package/dist/index.js
CHANGED
|
@@ -67,7 +67,7 @@ function summarizeChanges(changes) {
|
|
|
67
67
|
return { introduced, removed, severity, options, workspace };
|
|
68
68
|
}
|
|
69
69
|
function summarizeSnapshots(snapshots) {
|
|
70
|
-
const { rules, error, warn, off } =
|
|
70
|
+
const { rules, error, warn, off } = countUniqueRuleSeverities([...snapshots.values()].map((snapshot) => snapshot.rules));
|
|
71
71
|
return { groups: snapshots.size, rules, error, warn, off };
|
|
72
72
|
}
|
|
73
73
|
function countUniqueWorkspaces(snapshots) {
|
|
@@ -150,7 +150,8 @@ function formatShortConfig(payload) {
|
|
|
150
150
|
return `${lines.join("\n")}
|
|
151
151
|
`;
|
|
152
152
|
}
|
|
153
|
-
function formatShortCatalog(catalogs,
|
|
153
|
+
function formatShortCatalog(catalogs, options) {
|
|
154
|
+
const { missingOnly, detailed, color } = options;
|
|
154
155
|
const lines = [];
|
|
155
156
|
const sorted = [...catalogs].sort((a, b) => a.groupId.localeCompare(b.groupId));
|
|
156
157
|
const showGroupHeader = sorted.length > 1 || sorted.some((catalog) => catalog.groupId !== "default");
|
|
@@ -166,10 +167,8 @@ function formatShortCatalog(catalogs, missingOnly) {
|
|
|
166
167
|
for (const plugin of catalog.pluginStats) {
|
|
167
168
|
lines.push(` - ${plugin.pluginId}: ${formatUsageLine(plugin)}`);
|
|
168
169
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
for (const ruleName of detailRules) {
|
|
172
|
-
lines.push(` - ${ruleName}`);
|
|
170
|
+
if (detailed) {
|
|
171
|
+
appendRuleStateGroups(lines, catalog, missingOnly, color);
|
|
173
172
|
}
|
|
174
173
|
if (showGroupHeader) {
|
|
175
174
|
lines.push("");
|
|
@@ -184,6 +183,44 @@ function formatShortCatalog(catalogs, missingOnly) {
|
|
|
184
183
|
function formatUsageLine(stats) {
|
|
185
184
|
return `${stats.inUse}/${stats.totalAvailable} in use (${stats.inUsePct}%) | error ${stats.error} | warn ${stats.warn} | off ${stats.off} | not used ${stats.missing}`;
|
|
186
185
|
}
|
|
186
|
+
function appendRuleStateGroups(lines, catalog, missingOnly, color) {
|
|
187
|
+
if (missingOnly) {
|
|
188
|
+
appendRuleList(lines, color?.yellow("\u{1F7E1} unused") ?? "\u{1F7E1} unused", catalog.missingRules);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const observedRuleSet = new Set(catalog.observedRules);
|
|
192
|
+
const errorRules = [];
|
|
193
|
+
const warnRules = [];
|
|
194
|
+
const offRules = [];
|
|
195
|
+
const unusedRules = [];
|
|
196
|
+
for (const ruleName of catalog.availableRules) {
|
|
197
|
+
if (!observedRuleSet.has(ruleName)) {
|
|
198
|
+
unusedRules.push(ruleName);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const level = catalog.observedRuleLevels[ruleName];
|
|
202
|
+
if (level === "off") {
|
|
203
|
+
offRules.push(ruleName);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (level === "error") {
|
|
207
|
+
errorRules.push(ruleName);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
warnRules.push(ruleName);
|
|
211
|
+
}
|
|
212
|
+
appendRuleList(lines, color?.green("\u{1F7E2} error") ?? "\u{1F7E2} error", errorRules);
|
|
213
|
+
appendRuleList(lines, color?.green("\u{1F7E2} warn") ?? "\u{1F7E2} warn", warnRules);
|
|
214
|
+
appendRuleList(lines, color?.cyan("\u{1F535} off") ?? "\u{1F535} off", offRules);
|
|
215
|
+
appendRuleList(lines, color?.yellow("\u{1F7E1} unused") ?? "\u{1F7E1} unused", unusedRules);
|
|
216
|
+
}
|
|
217
|
+
function appendRuleList(lines, label, rules) {
|
|
218
|
+
lines.push(`${label} (${rules.length}):`);
|
|
219
|
+
const sortedRules = [...rules].sort((a, b) => a.localeCompare(b));
|
|
220
|
+
for (const ruleName of sortedRules) {
|
|
221
|
+
lines.push(` - ${ruleName}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
187
224
|
function formatCommandDisplayLabel(commandLabel) {
|
|
188
225
|
switch (commandLabel) {
|
|
189
226
|
case "check":
|
|
@@ -241,25 +278,34 @@ function formatBaselineSummaryLines(summary, workspaceCount) {
|
|
|
241
278
|
- \u{1F39A}\uFE0F severity mix: ${summary.error} errors, ${summary.warn} warnings, ${summary.off} off
|
|
242
279
|
`;
|
|
243
280
|
}
|
|
244
|
-
function
|
|
245
|
-
|
|
281
|
+
function countUniqueRuleSeverities(ruleObjects) {
|
|
282
|
+
const severityRank = { off: 0, warn: 1, error: 2 };
|
|
283
|
+
const severityByRule = /* @__PURE__ */ new Map();
|
|
284
|
+
for (const rulesObject of ruleObjects) {
|
|
285
|
+
for (const [ruleName, entry] of Object.entries(rulesObject)) {
|
|
286
|
+
const nextSeverity = getPrimarySeverity(entry);
|
|
287
|
+
if (!nextSeverity) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
const currentSeverity = severityByRule.get(ruleName);
|
|
291
|
+
if (!currentSeverity || severityRank[nextSeverity] > severityRank[currentSeverity]) {
|
|
292
|
+
severityByRule.set(ruleName, nextSeverity);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
246
296
|
let error = 0;
|
|
247
297
|
let warn = 0;
|
|
248
298
|
let off = 0;
|
|
249
|
-
for (const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
warn += 1;
|
|
257
|
-
} else {
|
|
258
|
-
off += 1;
|
|
259
|
-
}
|
|
299
|
+
for (const severity of severityByRule.values()) {
|
|
300
|
+
if (severity === "error") {
|
|
301
|
+
error += 1;
|
|
302
|
+
} else if (severity === "warn") {
|
|
303
|
+
warn += 1;
|
|
304
|
+
} else {
|
|
305
|
+
off += 1;
|
|
260
306
|
}
|
|
261
307
|
}
|
|
262
|
-
return { rules, error, warn, off };
|
|
308
|
+
return { rules: severityByRule.size, error, warn, off };
|
|
263
309
|
}
|
|
264
310
|
function getPrimarySeverity(entry) {
|
|
265
311
|
if (!entry) {
|
|
@@ -824,10 +870,10 @@ function isDefaultEquivalentConfig(config) {
|
|
|
824
870
|
|
|
825
871
|
// src/commands/catalog.ts
|
|
826
872
|
var CATALOG_FILE_SUFFIX = ".catalog.json";
|
|
827
|
-
async function executeCatalog(cwd, terminal, snapshotDir, format, missingOnly) {
|
|
873
|
+
async function executeCatalog(cwd, terminal, snapshotDir, format, missingOnly, detailed) {
|
|
828
874
|
const rows = await computeCatalogRows(cwd, terminal, snapshotDir, `catalog:${format}`, true);
|
|
829
875
|
if (format === "short") {
|
|
830
|
-
terminal.write(formatShortCatalog(rows, missingOnly));
|
|
876
|
+
terminal.write(formatShortCatalog(rows, { missingOnly, detailed, color: terminal.colors }));
|
|
831
877
|
return 0;
|
|
832
878
|
}
|
|
833
879
|
const output = rows.map((row) => {
|
|
@@ -850,10 +896,12 @@ async function executeCatalogUpdate(cwd, terminal, snapshotDir) {
|
|
|
850
896
|
const rows = await computeCatalogRows(cwd, terminal, snapshotDir, "catalog:update", false);
|
|
851
897
|
await writeCatalogBaselineFiles(cwd, snapshotDir, rows);
|
|
852
898
|
const groups = rows.length;
|
|
853
|
-
const available = rows.
|
|
854
|
-
const inUse = rows.
|
|
899
|
+
const available = countUniqueRules(rows.map((row) => row.availableRules));
|
|
900
|
+
const inUse = countUniqueRules(rows.map((row) => row.observedRules.filter((ruleName) => row.availableRules.includes(ruleName))));
|
|
855
901
|
terminal.write(`\u{1F9EA} Catalog baseline updated: ${groups} groups, ${available} available rules, ${inUse} currently in use.
|
|
856
902
|
`);
|
|
903
|
+
terminal.section("\u{1F4CA} Catalog summary");
|
|
904
|
+
terminal.write(formatShortCatalog(rows, { missingOnly: false, detailed: false, color: terminal.colors }));
|
|
857
905
|
return 0;
|
|
858
906
|
}
|
|
859
907
|
async function executeCatalogCheck(cwd, terminal, snapshotDir) {
|
|
@@ -868,8 +916,12 @@ async function executeCatalogCheck(cwd, terminal, snapshotDir) {
|
|
|
868
916
|
const diffs = compareCatalogBaselines(stored, current);
|
|
869
917
|
if (diffs.length === 0) {
|
|
870
918
|
terminal.write("Great news: no catalog drift detected.\n");
|
|
919
|
+
terminal.section("\u{1F4CA} Catalog summary");
|
|
920
|
+
terminal.write(formatShortCatalog(rows, { missingOnly: false, detailed: false, color: terminal.colors }));
|
|
871
921
|
return 0;
|
|
872
922
|
}
|
|
923
|
+
terminal.section("\u{1F4CA} Catalog summary");
|
|
924
|
+
terminal.write(formatShortCatalog(rows, { missingOnly: false, detailed: false, color: terminal.colors }));
|
|
873
925
|
terminal.write(`\u26A0\uFE0F Heads up: catalog drift detected in ${diffs.length} groups.
|
|
874
926
|
`);
|
|
875
927
|
for (const diff of diffs) {
|
|
@@ -914,6 +966,9 @@ async function computeCatalogRows(cwd, terminal, snapshotDir, commandLabel, prin
|
|
|
914
966
|
const availableRules = catalog?.allRules ?? [];
|
|
915
967
|
const availableRuleSet = new Set(availableRules);
|
|
916
968
|
const missingRules = availableRules.filter((ruleName) => !snapshot.rules[ruleName]);
|
|
969
|
+
const observedRuleLevels = Object.fromEntries(
|
|
970
|
+
observedRules.map((ruleName) => [ruleName, getPrimarySeverity2(snapshot.rules[ruleName])])
|
|
971
|
+
);
|
|
917
972
|
const observedOffRules = observedRules.filter((ruleName) => isRuleOffOnly(snapshot.rules[ruleName]));
|
|
918
973
|
const observedActiveRules = observedRules.filter((ruleName) => !isRuleOffOnly(snapshot.rules[ruleName]));
|
|
919
974
|
const observedOutsideCatalog = observedRules.filter((ruleName) => !availableRuleSet.has(ruleName)).length;
|
|
@@ -934,6 +989,7 @@ async function computeCatalogRows(cwd, terminal, snapshotDir, commandLabel, prin
|
|
|
934
989
|
coreRules,
|
|
935
990
|
pluginRulesByPrefix,
|
|
936
991
|
observedRules,
|
|
992
|
+
observedRuleLevels,
|
|
937
993
|
missingRules,
|
|
938
994
|
observedOffRules,
|
|
939
995
|
observedActiveRules,
|
|
@@ -1110,6 +1166,15 @@ function toPercent(value, total) {
|
|
|
1110
1166
|
}
|
|
1111
1167
|
return Number((value / total * 100).toFixed(1));
|
|
1112
1168
|
}
|
|
1169
|
+
function countUniqueRules(ruleLists) {
|
|
1170
|
+
const unique = /* @__PURE__ */ new Set();
|
|
1171
|
+
for (const rules of ruleLists) {
|
|
1172
|
+
for (const rule of rules) {
|
|
1173
|
+
unique.add(rule);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
return unique.size;
|
|
1177
|
+
}
|
|
1113
1178
|
|
|
1114
1179
|
// src/commands/check.ts
|
|
1115
1180
|
var UPDATE_HINT = "Tip: when you intentionally accept changes, run `eslint-config-snapshot --update` to refresh the baseline.\n";
|
|
@@ -1730,6 +1795,7 @@ function createColorizer() {
|
|
|
1730
1795
|
return {
|
|
1731
1796
|
green: (text) => wrap("32", text),
|
|
1732
1797
|
yellow: (text) => wrap("33", text),
|
|
1798
|
+
cyan: (text) => wrap("36", text),
|
|
1733
1799
|
red: (text) => wrap("31", text),
|
|
1734
1800
|
bold: (text) => wrap("1", text),
|
|
1735
1801
|
dim: (text) => wrap("2", text)
|
|
@@ -1856,9 +1922,9 @@ function createProgram(cwd, terminal, onActionExit) {
|
|
|
1856
1922
|
const format = opts.short ? "short" : opts.format;
|
|
1857
1923
|
onActionExit(await executePrint(cwd, terminal, SNAPSHOT_DIR, format));
|
|
1858
1924
|
});
|
|
1859
|
-
program.command("catalog").description("Print discovered rule catalog and missing rules").option("--format <format>", "Output format: json|short", parsePrintFormat, "json").option("--short", "Alias for --format short").option("--missing", "Only print rules that are available but not observed in current snapshots").action(async (opts) => {
|
|
1925
|
+
program.command("catalog").description("Print discovered rule catalog and missing rules").option("--format <format>", "Output format: json|short", parsePrintFormat, "json").option("--short", "Alias for --format short").option("--detailed", "Show detailed rule lists grouped by error, warn, off, and unused").option("--missing", "Only print rules that are available but not observed in current snapshots").action(async (opts) => {
|
|
1860
1926
|
const format = opts.short ? "short" : opts.format;
|
|
1861
|
-
onActionExit(await executeCatalog(cwd, terminal, SNAPSHOT_DIR, format, Boolean(opts.missing)));
|
|
1927
|
+
onActionExit(await executeCatalog(cwd, terminal, SNAPSHOT_DIR, format, Boolean(opts.missing), Boolean(opts.detailed)));
|
|
1862
1928
|
});
|
|
1863
1929
|
program.command("catalog-check").description("Compare current catalog against stored catalog baseline").action(async () => {
|
|
1864
1930
|
onActionExit(await executeCatalogCheck(cwd, terminal, SNAPSHOT_DIR));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eslint-config-snapshot/cli",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -31,6 +31,6 @@
|
|
|
31
31
|
"commander": "^14.0.3",
|
|
32
32
|
"debug": "^4.4.3",
|
|
33
33
|
"fast-glob": "^3.3.3",
|
|
34
|
-
"@eslint-config-snapshot/api": "1.3.
|
|
34
|
+
"@eslint-config-snapshot/api": "1.3.2"
|
|
35
35
|
}
|
|
36
36
|
}
|
package/src/commands/catalog.ts
CHANGED
|
@@ -18,6 +18,7 @@ type CatalogRow = {
|
|
|
18
18
|
coreRules: string[]
|
|
19
19
|
pluginRulesByPrefix: Record<string, string[]>
|
|
20
20
|
observedRules: string[]
|
|
21
|
+
observedRuleLevels: Record<string, 'error' | 'warn' | 'off'>
|
|
21
22
|
missingRules: string[]
|
|
22
23
|
observedOffRules: string[]
|
|
23
24
|
observedActiveRules: string[]
|
|
@@ -55,12 +56,13 @@ export async function executeCatalog(
|
|
|
55
56
|
terminal: TerminalIO,
|
|
56
57
|
snapshotDir: string,
|
|
57
58
|
format: CatalogFormat,
|
|
58
|
-
missingOnly: boolean
|
|
59
|
+
missingOnly: boolean,
|
|
60
|
+
detailed: boolean
|
|
59
61
|
): Promise<number> {
|
|
60
62
|
const rows = await computeCatalogRows(cwd, terminal, snapshotDir, `catalog:${format}`, true)
|
|
61
63
|
|
|
62
64
|
if (format === 'short') {
|
|
63
|
-
terminal.write(formatShortCatalog(rows, missingOnly))
|
|
65
|
+
terminal.write(formatShortCatalog(rows, { missingOnly, detailed, color: terminal.colors }))
|
|
64
66
|
return 0
|
|
65
67
|
}
|
|
66
68
|
|
|
@@ -87,9 +89,11 @@ export async function executeCatalogUpdate(cwd: string, terminal: TerminalIO, sn
|
|
|
87
89
|
await writeCatalogBaselineFiles(cwd, snapshotDir, rows)
|
|
88
90
|
|
|
89
91
|
const groups = rows.length
|
|
90
|
-
const available = rows.
|
|
91
|
-
const inUse = rows.
|
|
92
|
+
const available = countUniqueRules(rows.map((row) => row.availableRules))
|
|
93
|
+
const inUse = countUniqueRules(rows.map((row) => row.observedRules.filter((ruleName) => row.availableRules.includes(ruleName))))
|
|
92
94
|
terminal.write(`🧪 Catalog baseline updated: ${groups} groups, ${available} available rules, ${inUse} currently in use.\n`)
|
|
95
|
+
terminal.section('📊 Catalog summary')
|
|
96
|
+
terminal.write(formatShortCatalog(rows, { missingOnly: false, detailed: false, color: terminal.colors }))
|
|
93
97
|
return 0
|
|
94
98
|
}
|
|
95
99
|
|
|
@@ -107,9 +111,13 @@ export async function executeCatalogCheck(cwd: string, terminal: TerminalIO, sna
|
|
|
107
111
|
const diffs = compareCatalogBaselines(stored, current)
|
|
108
112
|
if (diffs.length === 0) {
|
|
109
113
|
terminal.write('Great news: no catalog drift detected.\n')
|
|
114
|
+
terminal.section('📊 Catalog summary')
|
|
115
|
+
terminal.write(formatShortCatalog(rows, { missingOnly: false, detailed: false, color: terminal.colors }))
|
|
110
116
|
return 0
|
|
111
117
|
}
|
|
112
118
|
|
|
119
|
+
terminal.section('📊 Catalog summary')
|
|
120
|
+
terminal.write(formatShortCatalog(rows, { missingOnly: false, detailed: false, color: terminal.colors }))
|
|
113
121
|
terminal.write(`⚠️ Heads up: catalog drift detected in ${diffs.length} groups.\n`)
|
|
114
122
|
for (const diff of diffs) {
|
|
115
123
|
terminal.write(
|
|
@@ -163,6 +171,9 @@ async function computeCatalogRows(
|
|
|
163
171
|
const availableRules = catalog?.allRules ?? []
|
|
164
172
|
const availableRuleSet = new Set(availableRules)
|
|
165
173
|
const missingRules = availableRules.filter((ruleName) => !snapshot.rules[ruleName])
|
|
174
|
+
const observedRuleLevels = Object.fromEntries(
|
|
175
|
+
observedRules.map((ruleName) => [ruleName, getPrimarySeverity(snapshot.rules[ruleName])])
|
|
176
|
+
) as Record<string, 'error' | 'warn' | 'off'>
|
|
166
177
|
const observedOffRules = observedRules.filter((ruleName) => isRuleOffOnly(snapshot.rules[ruleName]))
|
|
167
178
|
const observedActiveRules = observedRules.filter((ruleName) => !isRuleOffOnly(snapshot.rules[ruleName]))
|
|
168
179
|
const observedOutsideCatalog = observedRules.filter((ruleName) => !availableRuleSet.has(ruleName)).length
|
|
@@ -188,6 +199,7 @@ async function computeCatalogRows(
|
|
|
188
199
|
coreRules,
|
|
189
200
|
pluginRulesByPrefix,
|
|
190
201
|
observedRules,
|
|
202
|
+
observedRuleLevels,
|
|
191
203
|
missingRules,
|
|
192
204
|
observedOffRules,
|
|
193
205
|
observedActiveRules,
|
|
@@ -392,3 +404,13 @@ function toPercent(value: number, total: number): number {
|
|
|
392
404
|
}
|
|
393
405
|
return Number(((value / total) * 100).toFixed(1))
|
|
394
406
|
}
|
|
407
|
+
|
|
408
|
+
function countUniqueRules(ruleLists: string[][]): number {
|
|
409
|
+
const unique = new Set<string>()
|
|
410
|
+
for (const rules of ruleLists) {
|
|
411
|
+
for (const rule of rules) {
|
|
412
|
+
unique.add(rule)
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return unique.size
|
|
416
|
+
}
|
package/src/formatters.ts
CHANGED
|
@@ -14,6 +14,7 @@ export type RuleCatalogLike = {
|
|
|
14
14
|
coreRules: string[]
|
|
15
15
|
pluginRulesByPrefix: Record<string, string[]>
|
|
16
16
|
observedRules: string[]
|
|
17
|
+
observedRuleLevels: Record<string, 'error' | 'warn' | 'off'>
|
|
17
18
|
missingRules: string[]
|
|
18
19
|
observedOffRules: string[]
|
|
19
20
|
observedActiveRules: string[]
|
|
@@ -21,6 +22,11 @@ export type RuleCatalogLike = {
|
|
|
21
22
|
coreStats: UsageStats
|
|
22
23
|
pluginStats: Array<{ pluginId: string } & UsageStats>
|
|
23
24
|
}
|
|
25
|
+
type CatalogColorizer = {
|
|
26
|
+
green: (text: string) => string
|
|
27
|
+
yellow: (text: string) => string
|
|
28
|
+
cyan: (text: string) => string
|
|
29
|
+
}
|
|
24
30
|
|
|
25
31
|
export type UsageStats = {
|
|
26
32
|
totalAvailable: number
|
|
@@ -101,7 +107,7 @@ export function summarizeChanges(changes: Array<{ groupId: string; diff: Snapsho
|
|
|
101
107
|
}
|
|
102
108
|
|
|
103
109
|
export function summarizeSnapshots(snapshots: Map<string, SnapshotLike>) {
|
|
104
|
-
const { rules, error, warn, off } =
|
|
110
|
+
const { rules, error, warn, off } = countUniqueRuleSeverities([...snapshots.values()].map((snapshot) => snapshot.rules))
|
|
105
111
|
return { groups: snapshots.size, rules, error, warn, off }
|
|
106
112
|
}
|
|
107
113
|
|
|
@@ -211,7 +217,11 @@ export function formatShortConfig(payload: {
|
|
|
211
217
|
return `${lines.join('\n')}\n`
|
|
212
218
|
}
|
|
213
219
|
|
|
214
|
-
export function formatShortCatalog(
|
|
220
|
+
export function formatShortCatalog(
|
|
221
|
+
catalogs: RuleCatalogLike[],
|
|
222
|
+
options: { missingOnly: boolean; detailed: boolean; color?: CatalogColorizer }
|
|
223
|
+
): string {
|
|
224
|
+
const { missingOnly, detailed, color } = options
|
|
215
225
|
const lines: string[] = []
|
|
216
226
|
const sorted = [...catalogs].sort((a, b) => a.groupId.localeCompare(b.groupId))
|
|
217
227
|
const showGroupHeader = sorted.length > 1 || sorted.some((catalog) => catalog.groupId !== 'default')
|
|
@@ -228,10 +238,8 @@ export function formatShortCatalog(catalogs: RuleCatalogLike[], missingOnly: boo
|
|
|
228
238
|
lines.push(` - ${plugin.pluginId}: ${formatUsageLine(plugin)}`)
|
|
229
239
|
}
|
|
230
240
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
for (const ruleName of detailRules) {
|
|
234
|
-
lines.push(` - ${ruleName}`)
|
|
241
|
+
if (detailed) {
|
|
242
|
+
appendRuleStateGroups(lines, catalog, missingOnly, color)
|
|
235
243
|
}
|
|
236
244
|
if (showGroupHeader) {
|
|
237
245
|
lines.push('')
|
|
@@ -247,6 +255,54 @@ function formatUsageLine(stats: UsageStats): string {
|
|
|
247
255
|
return `${stats.inUse}/${stats.totalAvailable} in use (${stats.inUsePct}%) | error ${stats.error} | warn ${stats.warn} | off ${stats.off} | not used ${stats.missing}`
|
|
248
256
|
}
|
|
249
257
|
|
|
258
|
+
function appendRuleStateGroups(
|
|
259
|
+
lines: string[],
|
|
260
|
+
catalog: RuleCatalogLike,
|
|
261
|
+
missingOnly: boolean,
|
|
262
|
+
color?: CatalogColorizer
|
|
263
|
+
): void {
|
|
264
|
+
if (missingOnly) {
|
|
265
|
+
appendRuleList(lines, color?.yellow('🟡 unused') ?? '🟡 unused', catalog.missingRules)
|
|
266
|
+
return
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const observedRuleSet = new Set(catalog.observedRules)
|
|
270
|
+
const errorRules: string[] = []
|
|
271
|
+
const warnRules: string[] = []
|
|
272
|
+
const offRules: string[] = []
|
|
273
|
+
const unusedRules: string[] = []
|
|
274
|
+
|
|
275
|
+
for (const ruleName of catalog.availableRules) {
|
|
276
|
+
if (!observedRuleSet.has(ruleName)) {
|
|
277
|
+
unusedRules.push(ruleName)
|
|
278
|
+
continue
|
|
279
|
+
}
|
|
280
|
+
const level = catalog.observedRuleLevels[ruleName]
|
|
281
|
+
if (level === 'off') {
|
|
282
|
+
offRules.push(ruleName)
|
|
283
|
+
continue
|
|
284
|
+
}
|
|
285
|
+
if (level === 'error') {
|
|
286
|
+
errorRules.push(ruleName)
|
|
287
|
+
continue
|
|
288
|
+
}
|
|
289
|
+
warnRules.push(ruleName)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
appendRuleList(lines, color?.green('🟢 error') ?? '🟢 error', errorRules)
|
|
293
|
+
appendRuleList(lines, color?.green('🟢 warn') ?? '🟢 warn', warnRules)
|
|
294
|
+
appendRuleList(lines, color?.cyan('🔵 off') ?? '🔵 off', offRules)
|
|
295
|
+
appendRuleList(lines, color?.yellow('🟡 unused') ?? '🟡 unused', unusedRules)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function appendRuleList(lines: string[], label: string, rules: string[]): void {
|
|
299
|
+
lines.push(`${label} (${rules.length}):`)
|
|
300
|
+
const sortedRules = [...rules].sort((a, b) => a.localeCompare(b))
|
|
301
|
+
for (const ruleName of sortedRules) {
|
|
302
|
+
lines.push(` - ${ruleName}`)
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
250
306
|
export function formatCommandDisplayLabel(commandLabel: string): string {
|
|
251
307
|
switch (commandLabel) {
|
|
252
308
|
case 'check':
|
|
@@ -331,6 +387,39 @@ export function countRuleSeverities(ruleObjects: RuleObject[]) {
|
|
|
331
387
|
return { rules, error, warn, off }
|
|
332
388
|
}
|
|
333
389
|
|
|
390
|
+
export function countUniqueRuleSeverities(ruleObjects: RuleObject[]) {
|
|
391
|
+
const severityRank: Record<'off' | 'warn' | 'error', number> = { off: 0, warn: 1, error: 2 }
|
|
392
|
+
const severityByRule = new Map<string, 'off' | 'warn' | 'error'>()
|
|
393
|
+
|
|
394
|
+
for (const rulesObject of ruleObjects) {
|
|
395
|
+
for (const [ruleName, entry] of Object.entries(rulesObject)) {
|
|
396
|
+
const nextSeverity = getPrimarySeverity(entry)
|
|
397
|
+
if (!nextSeverity) {
|
|
398
|
+
continue
|
|
399
|
+
}
|
|
400
|
+
const currentSeverity = severityByRule.get(ruleName)
|
|
401
|
+
if (!currentSeverity || severityRank[nextSeverity] > severityRank[currentSeverity]) {
|
|
402
|
+
severityByRule.set(ruleName, nextSeverity)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
let error = 0
|
|
408
|
+
let warn = 0
|
|
409
|
+
let off = 0
|
|
410
|
+
for (const severity of severityByRule.values()) {
|
|
411
|
+
if (severity === 'error') {
|
|
412
|
+
error += 1
|
|
413
|
+
} else if (severity === 'warn') {
|
|
414
|
+
warn += 1
|
|
415
|
+
} else {
|
|
416
|
+
off += 1
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return { rules: severityByRule.size, error, warn, off }
|
|
421
|
+
}
|
|
422
|
+
|
|
334
423
|
function getPrimarySeverity(entry: SnapshotRuleEntry | undefined): 'off' | 'warn' | 'error' | undefined {
|
|
335
424
|
if (!entry) {
|
|
336
425
|
return undefined
|
package/src/index.ts
CHANGED
|
@@ -181,10 +181,11 @@ function createProgram(cwd: string, terminal: TerminalIO, onActionExit: (code: n
|
|
|
181
181
|
.description('Print discovered rule catalog and missing rules')
|
|
182
182
|
.option('--format <format>', 'Output format: json|short', parsePrintFormat, 'json')
|
|
183
183
|
.option('--short', 'Alias for --format short')
|
|
184
|
+
.option('--detailed', 'Show detailed rule lists grouped by error, warn, off, and unused')
|
|
184
185
|
.option('--missing', 'Only print rules that are available but not observed in current snapshots')
|
|
185
|
-
.action(async (opts: { format: CatalogFormat; short?: boolean; missing?: boolean }) => {
|
|
186
|
+
.action(async (opts: { format: CatalogFormat; short?: boolean; missing?: boolean; detailed?: boolean }) => {
|
|
186
187
|
const format: CatalogFormat = opts.short ? 'short' : opts.format
|
|
187
|
-
onActionExit(await executeCatalog(cwd, terminal, SNAPSHOT_DIR, format, Boolean(opts.missing)))
|
|
188
|
+
onActionExit(await executeCatalog(cwd, terminal, SNAPSHOT_DIR, format, Boolean(opts.missing), Boolean(opts.detailed)))
|
|
188
189
|
})
|
|
189
190
|
|
|
190
191
|
program
|
package/src/terminal.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { createInterface } from 'node:readline'
|
|
|
3
3
|
type Colorizer = {
|
|
4
4
|
green: (text: string) => string
|
|
5
5
|
yellow: (text: string) => string
|
|
6
|
+
cyan: (text: string) => string
|
|
6
7
|
red: (text: string) => string
|
|
7
8
|
bold: (text: string) => string
|
|
8
9
|
dim: (text: string) => string
|
|
@@ -171,6 +172,7 @@ function createColorizer(): Colorizer {
|
|
|
171
172
|
return {
|
|
172
173
|
green: (text: string) => wrap('32', text),
|
|
173
174
|
yellow: (text: string) => wrap('33', text),
|
|
175
|
+
cyan: (text: string) => wrap('36', text),
|
|
174
176
|
red: (text: string) => wrap('31', text),
|
|
175
177
|
bold: (text: string) => wrap('1', text),
|
|
176
178
|
dim: (text: string) => wrap('2', text)
|
|
@@ -238,10 +238,19 @@ no-debugger: off
|
|
|
238
238
|
expect(output).toContain('🧱 core: 2/3 in use')
|
|
239
239
|
expect(output).toContain('🔌 plugins tracked: 1')
|
|
240
240
|
expect(output).toContain(' - alpha: 0/2 in use')
|
|
241
|
-
expect(output).toContain('
|
|
242
|
-
|
|
241
|
+
expect(output).not.toContain('🟡 unused')
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('catalog --short --detailed prints grouped detailed lists', async () => {
|
|
245
|
+
const writeSpy = vi.spyOn(process.stdout, 'write')
|
|
246
|
+
const code = await runCli('catalog', fixtureRoot, ['--short', '--detailed'])
|
|
247
|
+
expect(code).toBe(0)
|
|
248
|
+
const output = String(writeSpy.mock.calls.at(-1)?.[0] ?? '')
|
|
249
|
+
expect(output).toContain('🟢 error')
|
|
250
|
+
expect(output).toContain('🟢 warn')
|
|
251
|
+
expect(output).toContain('🔵 off')
|
|
252
|
+
expect(output).toContain('🟡 unused')
|
|
243
253
|
expect(output).toContain('alpha/only-in-catalog')
|
|
244
|
-
expect(output).toContain('no-alert')
|
|
245
254
|
})
|
|
246
255
|
|
|
247
256
|
it('init creates scaffold config file when target=file', async () => {
|
|
@@ -255,10 +255,18 @@ no-debugger: off
|
|
|
255
255
|
expect(result.stdout).toContain('🧱 core: 2/3 in use')
|
|
256
256
|
expect(result.stdout).toContain('🔌 plugins tracked: 1')
|
|
257
257
|
expect(result.stdout).toContain(' - alpha: 0/2 in use')
|
|
258
|
-
expect(result.stdout).toContain('
|
|
259
|
-
expect(result.
|
|
258
|
+
expect(result.stdout).not.toContain('🟡 unused')
|
|
259
|
+
expect(result.stderr).toBe('')
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
it('catalog --short --detailed returns grouped state lists', () => {
|
|
263
|
+
const result = run(['catalog', '--short', '--detailed'])
|
|
264
|
+
expect(result.status).toBe(0)
|
|
265
|
+
expect(result.stdout).toContain('🟢 error')
|
|
266
|
+
expect(result.stdout).toContain('🟢 warn')
|
|
267
|
+
expect(result.stdout).toContain('🔵 off')
|
|
268
|
+
expect(result.stdout).toContain('🟡 unused')
|
|
260
269
|
expect(result.stdout).toContain('alpha/only-in-catalog')
|
|
261
|
-
expect(result.stdout).toContain('no-alert')
|
|
262
270
|
expect(result.stderr).toBe('')
|
|
263
271
|
})
|
|
264
272
|
|
|
@@ -266,11 +274,15 @@ no-debugger: off
|
|
|
266
274
|
const update = run(['catalog-update'])
|
|
267
275
|
expect(update.status).toBe(0)
|
|
268
276
|
expect(update.stdout).toContain('Catalog baseline updated:')
|
|
277
|
+
expect(update.stdout).toContain('📦 total:')
|
|
278
|
+
expect(update.stdout).toContain('🔌 plugins tracked:')
|
|
269
279
|
expect(update.stderr).toBe('')
|
|
270
280
|
|
|
271
281
|
const check = run(['catalog-check'])
|
|
272
282
|
expect(check.status).toBe(0)
|
|
273
|
-
expect(check.stdout).
|
|
283
|
+
expect(check.stdout).toContain('Great news: no catalog drift detected.')
|
|
284
|
+
expect(check.stdout).toContain('📦 total:')
|
|
285
|
+
expect(check.stdout).toContain('🔌 plugins tracked:')
|
|
274
286
|
expect(check.stderr).toBe('')
|
|
275
287
|
})
|
|
276
288
|
|
|
@@ -44,4 +44,35 @@ describe('output helpers', () => {
|
|
|
44
44
|
)
|
|
45
45
|
expect(summary).toEqual({ groups: 1, rules: 3, error: 1, warn: 1, off: 1 })
|
|
46
46
|
})
|
|
47
|
+
|
|
48
|
+
it('deduplicates rules across groups in summary', () => {
|
|
49
|
+
const summary = summarizeSnapshots(
|
|
50
|
+
new Map([
|
|
51
|
+
[
|
|
52
|
+
'group-a',
|
|
53
|
+
{
|
|
54
|
+
groupId: 'group-a',
|
|
55
|
+
workspaces: ['packages/a'],
|
|
56
|
+
rules: {
|
|
57
|
+
shared: ['warn'],
|
|
58
|
+
onlyA: ['off']
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
[
|
|
63
|
+
'group-b',
|
|
64
|
+
{
|
|
65
|
+
groupId: 'group-b',
|
|
66
|
+
workspaces: ['packages/b'],
|
|
67
|
+
rules: {
|
|
68
|
+
shared: ['error'],
|
|
69
|
+
onlyB: ['warn']
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
])
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
expect(summary).toEqual({ groups: 2, rules: 3, error: 1, warn: 1, off: 1 })
|
|
77
|
+
})
|
|
47
78
|
})
|