@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 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, missingOnly) {
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
- const detailRules = missingOnly ? catalog.missingRules : catalog.availableRules;
205
- lines.push(`${missingOnly ? "\u{1F573}\uFE0F missing list" : "\u{1F4DA} available list"} (${detailRules.length}):`);
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, missingOnly) {
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
- const detailRules = missingOnly ? catalog.missingRules : catalog.availableRules;
170
- lines.push(`${missingOnly ? "\u{1F573}\uFE0F missing list" : "\u{1F4DA} available list"} (${detailRules.length}):`);
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.1",
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.1"
34
+ "@eslint-config-snapshot/api": "1.3.2"
35
35
  }
36
36
  }
@@ -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(catalogs: RuleCatalogLike[], missingOnly: boolean): string {
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
- const detailRules = missingOnly ? catalog.missingRules : catalog.availableRules
232
- lines.push(`${missingOnly ? '🕳️ missing list' : '📚 available list'} (${detailRules.length}):`)
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('🕳️ missing list (3):')
242
- expect(output).toContain('alpha/observed')
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('🕳️ missing list (3):')
259
- expect(result.stdout).toContain('alpha/observed')
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