@eslint-config-snapshot/cli 1.3.1 → 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 +8 -0
- package/dist/index.cjs +54 -12
- package/dist/index.js +54 -12
- package/package.json +2 -2
- package/src/commands/catalog.ts +11 -5
- package/src/formatters.ts +61 -5
- 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 +11 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
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
|
+
|
|
3
11
|
## 1.3.1
|
|
4
12
|
|
|
5
13
|
### Patch Changes
|
package/dist/index.cjs
CHANGED
|
@@ -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":
|
|
@@ -854,10 +891,10 @@ function isDefaultEquivalentConfig(config) {
|
|
|
854
891
|
|
|
855
892
|
// src/commands/catalog.ts
|
|
856
893
|
var CATALOG_FILE_SUFFIX = ".catalog.json";
|
|
857
|
-
async function executeCatalog(cwd, terminal, snapshotDir, format, missingOnly) {
|
|
894
|
+
async function executeCatalog(cwd, terminal, snapshotDir, format, missingOnly, detailed) {
|
|
858
895
|
const rows = await computeCatalogRows(cwd, terminal, snapshotDir, `catalog:${format}`, true);
|
|
859
896
|
if (format === "short") {
|
|
860
|
-
terminal.write(formatShortCatalog(rows, missingOnly));
|
|
897
|
+
terminal.write(formatShortCatalog(rows, { missingOnly, detailed, color: terminal.colors }));
|
|
861
898
|
return 0;
|
|
862
899
|
}
|
|
863
900
|
const output = rows.map((row) => {
|
|
@@ -885,7 +922,7 @@ async function executeCatalogUpdate(cwd, terminal, snapshotDir) {
|
|
|
885
922
|
terminal.write(`\u{1F9EA} Catalog baseline updated: ${groups} groups, ${available} available rules, ${inUse} currently in use.
|
|
886
923
|
`);
|
|
887
924
|
terminal.section("\u{1F4CA} Catalog summary");
|
|
888
|
-
terminal.write(formatShortCatalog(rows, false));
|
|
925
|
+
terminal.write(formatShortCatalog(rows, { missingOnly: false, detailed: false, color: terminal.colors }));
|
|
889
926
|
return 0;
|
|
890
927
|
}
|
|
891
928
|
async function executeCatalogCheck(cwd, terminal, snapshotDir) {
|
|
@@ -901,11 +938,11 @@ async function executeCatalogCheck(cwd, terminal, snapshotDir) {
|
|
|
901
938
|
if (diffs.length === 0) {
|
|
902
939
|
terminal.write("Great news: no catalog drift detected.\n");
|
|
903
940
|
terminal.section("\u{1F4CA} Catalog summary");
|
|
904
|
-
terminal.write(formatShortCatalog(rows, false));
|
|
941
|
+
terminal.write(formatShortCatalog(rows, { missingOnly: false, detailed: false, color: terminal.colors }));
|
|
905
942
|
return 0;
|
|
906
943
|
}
|
|
907
944
|
terminal.section("\u{1F4CA} Catalog summary");
|
|
908
|
-
terminal.write(formatShortCatalog(rows, false));
|
|
945
|
+
terminal.write(formatShortCatalog(rows, { missingOnly: false, detailed: false, color: terminal.colors }));
|
|
909
946
|
terminal.write(`\u26A0\uFE0F Heads up: catalog drift detected in ${diffs.length} groups.
|
|
910
947
|
`);
|
|
911
948
|
for (const diff of diffs) {
|
|
@@ -950,6 +987,9 @@ async function computeCatalogRows(cwd, terminal, snapshotDir, commandLabel, prin
|
|
|
950
987
|
const availableRules = catalog?.allRules ?? [];
|
|
951
988
|
const availableRuleSet = new Set(availableRules);
|
|
952
989
|
const missingRules = availableRules.filter((ruleName) => !snapshot.rules[ruleName]);
|
|
990
|
+
const observedRuleLevels = Object.fromEntries(
|
|
991
|
+
observedRules.map((ruleName) => [ruleName, getPrimarySeverity2(snapshot.rules[ruleName])])
|
|
992
|
+
);
|
|
953
993
|
const observedOffRules = observedRules.filter((ruleName) => isRuleOffOnly(snapshot.rules[ruleName]));
|
|
954
994
|
const observedActiveRules = observedRules.filter((ruleName) => !isRuleOffOnly(snapshot.rules[ruleName]));
|
|
955
995
|
const observedOutsideCatalog = observedRules.filter((ruleName) => !availableRuleSet.has(ruleName)).length;
|
|
@@ -970,6 +1010,7 @@ async function computeCatalogRows(cwd, terminal, snapshotDir, commandLabel, prin
|
|
|
970
1010
|
coreRules,
|
|
971
1011
|
pluginRulesByPrefix,
|
|
972
1012
|
observedRules,
|
|
1013
|
+
observedRuleLevels,
|
|
973
1014
|
missingRules,
|
|
974
1015
|
observedOffRules,
|
|
975
1016
|
observedActiveRules,
|
|
@@ -1775,6 +1816,7 @@ function createColorizer() {
|
|
|
1775
1816
|
return {
|
|
1776
1817
|
green: (text) => wrap("32", text),
|
|
1777
1818
|
yellow: (text) => wrap("33", text),
|
|
1819
|
+
cyan: (text) => wrap("36", text),
|
|
1778
1820
|
red: (text) => wrap("31", text),
|
|
1779
1821
|
bold: (text) => wrap("1", text),
|
|
1780
1822
|
dim: (text) => wrap("2", text)
|
|
@@ -1901,9 +1943,9 @@ function createProgram(cwd, terminal, onActionExit) {
|
|
|
1901
1943
|
const format = opts.short ? "short" : opts.format;
|
|
1902
1944
|
onActionExit(await executePrint(cwd, terminal, SNAPSHOT_DIR, format));
|
|
1903
1945
|
});
|
|
1904
|
-
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) => {
|
|
1905
1947
|
const format = opts.short ? "short" : opts.format;
|
|
1906
|
-
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)));
|
|
1907
1949
|
});
|
|
1908
1950
|
program.command("catalog-check").description("Compare current catalog against stored catalog baseline").action(async () => {
|
|
1909
1951
|
onActionExit(await executeCatalogCheck(cwd, terminal, SNAPSHOT_DIR));
|
package/dist/index.js
CHANGED
|
@@ -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":
|
|
@@ -833,10 +870,10 @@ function isDefaultEquivalentConfig(config) {
|
|
|
833
870
|
|
|
834
871
|
// src/commands/catalog.ts
|
|
835
872
|
var CATALOG_FILE_SUFFIX = ".catalog.json";
|
|
836
|
-
async function executeCatalog(cwd, terminal, snapshotDir, format, missingOnly) {
|
|
873
|
+
async function executeCatalog(cwd, terminal, snapshotDir, format, missingOnly, detailed) {
|
|
837
874
|
const rows = await computeCatalogRows(cwd, terminal, snapshotDir, `catalog:${format}`, true);
|
|
838
875
|
if (format === "short") {
|
|
839
|
-
terminal.write(formatShortCatalog(rows, missingOnly));
|
|
876
|
+
terminal.write(formatShortCatalog(rows, { missingOnly, detailed, color: terminal.colors }));
|
|
840
877
|
return 0;
|
|
841
878
|
}
|
|
842
879
|
const output = rows.map((row) => {
|
|
@@ -864,7 +901,7 @@ async function executeCatalogUpdate(cwd, terminal, snapshotDir) {
|
|
|
864
901
|
terminal.write(`\u{1F9EA} Catalog baseline updated: ${groups} groups, ${available} available rules, ${inUse} currently in use.
|
|
865
902
|
`);
|
|
866
903
|
terminal.section("\u{1F4CA} Catalog summary");
|
|
867
|
-
terminal.write(formatShortCatalog(rows, false));
|
|
904
|
+
terminal.write(formatShortCatalog(rows, { missingOnly: false, detailed: false, color: terminal.colors }));
|
|
868
905
|
return 0;
|
|
869
906
|
}
|
|
870
907
|
async function executeCatalogCheck(cwd, terminal, snapshotDir) {
|
|
@@ -880,11 +917,11 @@ async function executeCatalogCheck(cwd, terminal, snapshotDir) {
|
|
|
880
917
|
if (diffs.length === 0) {
|
|
881
918
|
terminal.write("Great news: no catalog drift detected.\n");
|
|
882
919
|
terminal.section("\u{1F4CA} Catalog summary");
|
|
883
|
-
terminal.write(formatShortCatalog(rows, false));
|
|
920
|
+
terminal.write(formatShortCatalog(rows, { missingOnly: false, detailed: false, color: terminal.colors }));
|
|
884
921
|
return 0;
|
|
885
922
|
}
|
|
886
923
|
terminal.section("\u{1F4CA} Catalog summary");
|
|
887
|
-
terminal.write(formatShortCatalog(rows, false));
|
|
924
|
+
terminal.write(formatShortCatalog(rows, { missingOnly: false, detailed: false, color: terminal.colors }));
|
|
888
925
|
terminal.write(`\u26A0\uFE0F Heads up: catalog drift detected in ${diffs.length} groups.
|
|
889
926
|
`);
|
|
890
927
|
for (const diff of diffs) {
|
|
@@ -929,6 +966,9 @@ async function computeCatalogRows(cwd, terminal, snapshotDir, commandLabel, prin
|
|
|
929
966
|
const availableRules = catalog?.allRules ?? [];
|
|
930
967
|
const availableRuleSet = new Set(availableRules);
|
|
931
968
|
const missingRules = availableRules.filter((ruleName) => !snapshot.rules[ruleName]);
|
|
969
|
+
const observedRuleLevels = Object.fromEntries(
|
|
970
|
+
observedRules.map((ruleName) => [ruleName, getPrimarySeverity2(snapshot.rules[ruleName])])
|
|
971
|
+
);
|
|
932
972
|
const observedOffRules = observedRules.filter((ruleName) => isRuleOffOnly(snapshot.rules[ruleName]));
|
|
933
973
|
const observedActiveRules = observedRules.filter((ruleName) => !isRuleOffOnly(snapshot.rules[ruleName]));
|
|
934
974
|
const observedOutsideCatalog = observedRules.filter((ruleName) => !availableRuleSet.has(ruleName)).length;
|
|
@@ -949,6 +989,7 @@ async function computeCatalogRows(cwd, terminal, snapshotDir, commandLabel, prin
|
|
|
949
989
|
coreRules,
|
|
950
990
|
pluginRulesByPrefix,
|
|
951
991
|
observedRules,
|
|
992
|
+
observedRuleLevels,
|
|
952
993
|
missingRules,
|
|
953
994
|
observedOffRules,
|
|
954
995
|
observedActiveRules,
|
|
@@ -1754,6 +1795,7 @@ function createColorizer() {
|
|
|
1754
1795
|
return {
|
|
1755
1796
|
green: (text) => wrap("32", text),
|
|
1756
1797
|
yellow: (text) => wrap("33", text),
|
|
1798
|
+
cyan: (text) => wrap("36", text),
|
|
1757
1799
|
red: (text) => wrap("31", text),
|
|
1758
1800
|
bold: (text) => wrap("1", text),
|
|
1759
1801
|
dim: (text) => wrap("2", text)
|
|
@@ -1880,9 +1922,9 @@ function createProgram(cwd, terminal, onActionExit) {
|
|
|
1880
1922
|
const format = opts.short ? "short" : opts.format;
|
|
1881
1923
|
onActionExit(await executePrint(cwd, terminal, SNAPSHOT_DIR, format));
|
|
1882
1924
|
});
|
|
1883
|
-
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) => {
|
|
1884
1926
|
const format = opts.short ? "short" : opts.format;
|
|
1885
|
-
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)));
|
|
1886
1928
|
});
|
|
1887
1929
|
program.command("catalog-check").description("Compare current catalog against stored catalog baseline").action(async () => {
|
|
1888
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
|
|
|
@@ -91,7 +93,7 @@ export async function executeCatalogUpdate(cwd: string, terminal: TerminalIO, sn
|
|
|
91
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`)
|
|
93
95
|
terminal.section('📊 Catalog summary')
|
|
94
|
-
terminal.write(formatShortCatalog(rows, false))
|
|
96
|
+
terminal.write(formatShortCatalog(rows, { missingOnly: false, detailed: false, color: terminal.colors }))
|
|
95
97
|
return 0
|
|
96
98
|
}
|
|
97
99
|
|
|
@@ -110,12 +112,12 @@ export async function executeCatalogCheck(cwd: string, terminal: TerminalIO, sna
|
|
|
110
112
|
if (diffs.length === 0) {
|
|
111
113
|
terminal.write('Great news: no catalog drift detected.\n')
|
|
112
114
|
terminal.section('📊 Catalog summary')
|
|
113
|
-
terminal.write(formatShortCatalog(rows, false))
|
|
115
|
+
terminal.write(formatShortCatalog(rows, { missingOnly: false, detailed: false, color: terminal.colors }))
|
|
114
116
|
return 0
|
|
115
117
|
}
|
|
116
118
|
|
|
117
119
|
terminal.section('📊 Catalog summary')
|
|
118
|
-
terminal.write(formatShortCatalog(rows, false))
|
|
120
|
+
terminal.write(formatShortCatalog(rows, { missingOnly: false, detailed: false, color: terminal.colors }))
|
|
119
121
|
terminal.write(`⚠️ Heads up: catalog drift detected in ${diffs.length} groups.\n`)
|
|
120
122
|
for (const diff of diffs) {
|
|
121
123
|
terminal.write(
|
|
@@ -169,6 +171,9 @@ async function computeCatalogRows(
|
|
|
169
171
|
const availableRules = catalog?.allRules ?? []
|
|
170
172
|
const availableRuleSet = new Set(availableRules)
|
|
171
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'>
|
|
172
177
|
const observedOffRules = observedRules.filter((ruleName) => isRuleOffOnly(snapshot.rules[ruleName]))
|
|
173
178
|
const observedActiveRules = observedRules.filter((ruleName) => !isRuleOffOnly(snapshot.rules[ruleName]))
|
|
174
179
|
const observedOutsideCatalog = observedRules.filter((ruleName) => !availableRuleSet.has(ruleName)).length
|
|
@@ -194,6 +199,7 @@ async function computeCatalogRows(
|
|
|
194
199
|
coreRules,
|
|
195
200
|
pluginRulesByPrefix,
|
|
196
201
|
observedRules,
|
|
202
|
+
observedRuleLevels,
|
|
197
203
|
missingRules,
|
|
198
204
|
observedOffRules,
|
|
199
205
|
observedActiveRules,
|
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
|
|
@@ -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':
|
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
|
|