@eslint-config-snapshot/cli 1.1.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,25 +1,29 @@
1
- import { DEFAULT_CONFIG, findConfigPath, loadConfig, type SnapshotConfig } from '@eslint-config-snapshot/api'
1
+ import { loadConfig } from '@eslint-config-snapshot/api'
2
2
 
3
3
  import { formatShortConfig, formatShortPrint } from '../formatters.js'
4
- import { writeRunContextHeader } from '../run-context.js'
5
- import { computeCurrentSnapshots, loadStoredSnapshots, resolveWorkspaceAssignments, type WorkspaceAssignments } from '../runtime.js'
4
+ import { resolveWorkspaceAssignments, type WorkspaceAssignments } from '../runtime.js'
6
5
  import { type TerminalIO } from '../terminal.js'
6
+ import { prepareSnapshotExecution } from './snapshot-executor.js'
7
7
 
8
8
  export type PrintFormat = 'json' | 'short'
9
9
 
10
- export async function executePrint(cwd: string, terminal: TerminalIO, snapshotDir: string, format: PrintFormat): Promise<void> {
11
- const foundConfig = await findConfigPath(cwd)
12
- const storedSnapshots = await loadStoredSnapshots(cwd, snapshotDir)
13
- writeRunContextHeader(terminal, cwd, `print:${format}`, foundConfig?.path, storedSnapshots)
14
- if (terminal.showProgress) {
15
- terminal.subtle('🔎 Checking current ESLint configuration...\n')
10
+ export async function executePrint(cwd: string, terminal: TerminalIO, snapshotDir: string, format: PrintFormat): Promise<number> {
11
+ const prepared = await prepareSnapshotExecution({
12
+ cwd,
13
+ snapshotDir,
14
+ terminal,
15
+ commandLabel: `print:${format}`,
16
+ progressMessage: '🔎 Checking current ESLint configuration...\n'
17
+ })
18
+ if (!prepared.ok) {
19
+ return prepared.exitCode
16
20
  }
17
- const allowWorkspaceExtractionFailure = !foundConfig || isDefaultEquivalentConfig(foundConfig.config)
18
- const currentSnapshots = await computeCurrentSnapshots(cwd, { allowWorkspaceExtractionFailure })
21
+
22
+ const { currentSnapshots } = prepared
19
23
 
20
24
  if (format === 'short') {
21
25
  terminal.write(formatShortPrint([...currentSnapshots.values()]))
22
- return
26
+ return 0
23
27
  }
24
28
 
25
29
  const output = [...currentSnapshots.values()].map((snapshot) => ({
@@ -27,15 +31,22 @@ export async function executePrint(cwd: string, terminal: TerminalIO, snapshotDi
27
31
  rules: snapshot.rules
28
32
  }))
29
33
  terminal.write(`${JSON.stringify(output, null, 2)}\n`)
34
+ return 0
30
35
  }
31
36
 
32
- export async function executeConfig(cwd: string, terminal: TerminalIO, snapshotDir: string, format: PrintFormat): Promise<void> {
33
- const foundConfig = await findConfigPath(cwd)
34
- const storedSnapshots = await loadStoredSnapshots(cwd, snapshotDir)
35
- writeRunContextHeader(terminal, cwd, `config:${format}`, foundConfig?.path, storedSnapshots)
36
- if (terminal.showProgress) {
37
- terminal.subtle('⚙️ Resolving effective runtime configuration...\n')
37
+ export async function executeConfig(cwd: string, terminal: TerminalIO, snapshotDir: string, format: PrintFormat): Promise<number> {
38
+ const prepared = await prepareSnapshotExecution({
39
+ cwd,
40
+ snapshotDir,
41
+ terminal,
42
+ commandLabel: `config:${format}`,
43
+ progressMessage: '⚙️ Resolving effective runtime configuration...\n'
44
+ })
45
+ if (!prepared.ok) {
46
+ return prepared.exitCode
38
47
  }
48
+
49
+ const { foundConfig } = prepared
39
50
  const config = await loadConfig(cwd)
40
51
  const resolved: WorkspaceAssignments = await resolveWorkspaceAssignments(cwd, config)
41
52
  const payload = {
@@ -52,12 +63,9 @@ export async function executeConfig(cwd: string, terminal: TerminalIO, snapshotD
52
63
 
53
64
  if (format === 'short') {
54
65
  terminal.write(formatShortConfig(payload))
55
- return
66
+ return 0
56
67
  }
57
68
 
58
69
  terminal.write(`${JSON.stringify(payload, null, 2)}\n`)
59
- }
60
-
61
- function isDefaultEquivalentConfig(config: SnapshotConfig): boolean {
62
- return JSON.stringify(config) === JSON.stringify(DEFAULT_CONFIG)
70
+ return 0
63
71
  }
@@ -3,6 +3,15 @@ import path from 'node:path'
3
3
  import { type SkippedWorkspace } from '../runtime.js'
4
4
  import { type TerminalIO } from '../terminal.js'
5
5
 
6
+ export function writeDiscoveredWorkspacesSummary(terminal: TerminalIO, workspacesRel: string[]): void {
7
+ if (workspacesRel.length === 0) {
8
+ terminal.subtle('Auto-discovered workspaces: none\n')
9
+ return
10
+ }
11
+
12
+ terminal.subtle(`Auto-discovered workspaces (${workspacesRel.length}): ${workspacesRel.join(', ')}\n`)
13
+ }
14
+
6
15
  export function writeSkippedWorkspaceSummary(
7
16
  terminal: TerminalIO,
8
17
  cwd: string,
@@ -0,0 +1,97 @@
1
+ import { DEFAULT_CONFIG, findConfigPath, type SnapshotConfig } from '@eslint-config-snapshot/api'
2
+
3
+ import { writeRunContextHeader } from '../run-context.js'
4
+ import { type BuiltSnapshot, computeCurrentSnapshots, loadStoredSnapshots, type SkippedWorkspace, type StoredSnapshot } from '../runtime.js'
5
+ import { type TerminalIO } from '../terminal.js'
6
+
7
+ type SnapshotPreparationSuccess = {
8
+ ok: true
9
+ foundConfig: Awaited<ReturnType<typeof findConfigPath>>
10
+ storedSnapshots: Map<string, StoredSnapshot>
11
+ currentSnapshots: Map<string, BuiltSnapshot>
12
+ discoveredWorkspaces: string[]
13
+ skippedWorkspaces: SkippedWorkspace[]
14
+ }
15
+
16
+ type SnapshotPreparationFailure = {
17
+ ok: false
18
+ exitCode: number
19
+ }
20
+
21
+ export type SnapshotPreparationResult = SnapshotPreparationSuccess | SnapshotPreparationFailure
22
+
23
+ export async function prepareSnapshotExecution(options: {
24
+ cwd: string
25
+ snapshotDir: string
26
+ terminal: TerminalIO
27
+ commandLabel: string
28
+ progressMessage: string
29
+ showContext?: boolean
30
+ }): Promise<SnapshotPreparationResult> {
31
+ const { cwd, snapshotDir, terminal, commandLabel, progressMessage, showContext = true } = options
32
+
33
+ const foundConfig = await findConfigPath(cwd)
34
+ const storedSnapshots = await loadStoredSnapshots(cwd, snapshotDir)
35
+ if (showContext) {
36
+ writeRunContextHeader(terminal, cwd, commandLabel, foundConfig?.path, storedSnapshots)
37
+ }
38
+ if (showContext && terminal.showProgress && progressMessage.length > 0) {
39
+ terminal.subtle(progressMessage)
40
+ }
41
+
42
+ if (showContext && !foundConfig) {
43
+ terminal.subtle(
44
+ 'Tip: no explicit config found. Using safe built-in defaults. Run `eslint-config-snapshot init` to customize when needed.\n'
45
+ )
46
+ }
47
+
48
+ const skippedWorkspaces: SkippedWorkspace[] = []
49
+ let discoveredWorkspaces: string[] = []
50
+ const allowWorkspaceExtractionFailure = !foundConfig || isDefaultEquivalentConfig(foundConfig.config)
51
+
52
+ let currentSnapshots: Map<string, BuiltSnapshot>
53
+ try {
54
+ currentSnapshots = await computeCurrentSnapshots(cwd, {
55
+ allowWorkspaceExtractionFailure,
56
+ onWorkspacesDiscovered: (workspacesRel) => {
57
+ discoveredWorkspaces = workspacesRel
58
+ },
59
+ onWorkspaceSkipped: (skipped) => {
60
+ skippedWorkspaces.push(skipped)
61
+ }
62
+ })
63
+ } catch (error: unknown) {
64
+ if (allowWorkspaceExtractionFailure && isWorkspaceDiscoveryDefaultsError(error)) {
65
+ if (showContext) {
66
+ terminal.write(
67
+ 'Automatic workspace discovery could not complete with defaults.\nRun `eslint-config-snapshot init` to configure workspaces, then run `eslint-config-snapshot --update`.\n'
68
+ )
69
+ }
70
+ return { ok: false, exitCode: 1 }
71
+ }
72
+
73
+ throw error
74
+ }
75
+
76
+ return {
77
+ ok: true,
78
+ foundConfig,
79
+ storedSnapshots,
80
+ currentSnapshots,
81
+ discoveredWorkspaces,
82
+ skippedWorkspaces
83
+ }
84
+ }
85
+
86
+ export function isWorkspaceDiscoveryDefaultsError(error: unknown): boolean {
87
+ const message = error instanceof Error ? error.message : String(error)
88
+ return (
89
+ message.includes('Unable to discover workspaces') ||
90
+ message.includes('Unmatched workspaces') ||
91
+ message.includes('zero-config mode')
92
+ )
93
+ }
94
+
95
+ function isDefaultEquivalentConfig(config: SnapshotConfig): boolean {
96
+ return JSON.stringify(config) === JSON.stringify(DEFAULT_CONFIG)
97
+ }
@@ -1,49 +1,23 @@
1
- import { DEFAULT_CONFIG, findConfigPath, type SnapshotConfig } from '@eslint-config-snapshot/api'
2
-
3
1
  import { countUniqueWorkspaces, formatBaselineSummaryLines, summarizeSnapshots } from '../formatters.js'
4
- import { writeEslintVersionSummary, writeRunContextHeader } from '../run-context.js'
5
- import { computeCurrentSnapshots, loadStoredSnapshots, resolveGroupEslintVersions, type SkippedWorkspace, writeSnapshots } from '../runtime.js'
2
+ import { writeEslintVersionSummary } from '../run-context.js'
3
+ import { resolveGroupEslintVersions, writeSnapshots } from '../runtime.js'
6
4
  import { type TerminalIO } from '../terminal.js'
7
- import { writeSkippedWorkspaceSummary } from './skipped-workspaces.js'
5
+ import { writeDiscoveredWorkspacesSummary, writeSkippedWorkspaceSummary } from './skipped-workspaces.js'
6
+ import { prepareSnapshotExecution } from './snapshot-executor.js'
8
7
 
9
8
  export async function executeUpdate(cwd: string, terminal: TerminalIO, snapshotDir: string, printSummary: boolean): Promise<number> {
10
- const foundConfig = await findConfigPath(cwd)
11
- const storedSnapshots = await loadStoredSnapshots(cwd, snapshotDir)
12
- writeRunContextHeader(terminal, cwd, 'update', foundConfig?.path, storedSnapshots)
13
- if (terminal.showProgress) {
14
- terminal.subtle('🔎 Checking current ESLint configuration...\n')
9
+ const prepared = await prepareSnapshotExecution({
10
+ cwd,
11
+ snapshotDir,
12
+ terminal,
13
+ commandLabel: 'update',
14
+ progressMessage: '🔎 Checking current ESLint configuration...\n'
15
+ })
16
+ if (prepared.ok === false) {
17
+ return prepared.exitCode
15
18
  }
16
19
 
17
- if (!foundConfig) {
18
- terminal.subtle(
19
- 'Tip: no explicit config found. Using safe built-in defaults. Run `eslint-config-snapshot init` to customize when needed.\n'
20
- )
21
- }
22
-
23
- let currentSnapshots
24
- const skippedWorkspaces: SkippedWorkspace[] = []
25
- let discoveredWorkspaces: string[] = []
26
- const allowWorkspaceExtractionFailure = !foundConfig || isDefaultEquivalentConfig(foundConfig.config)
27
- try {
28
- currentSnapshots = await computeCurrentSnapshots(cwd, {
29
- allowWorkspaceExtractionFailure,
30
- onWorkspacesDiscovered: (workspacesRel) => {
31
- discoveredWorkspaces = workspacesRel
32
- },
33
- onWorkspaceSkipped: (skipped) => {
34
- skippedWorkspaces.push(skipped)
35
- }
36
- })
37
- } catch (error: unknown) {
38
- if (!foundConfig && isWorkspaceDiscoveryDefaultsError(error)) {
39
- terminal.write(
40
- 'Automatic workspace discovery could not complete with defaults.\nRun `eslint-config-snapshot init` to configure workspaces, then run `eslint-config-snapshot --update`.\n'
41
- )
42
- return 1
43
- }
44
-
45
- throw error
46
- }
20
+ const { foundConfig, storedSnapshots, currentSnapshots, discoveredWorkspaces, skippedWorkspaces } = prepared
47
21
  if (!foundConfig) {
48
22
  writeDiscoveredWorkspacesSummary(terminal, discoveredWorkspaces)
49
23
  }
@@ -63,25 +37,3 @@ export async function executeUpdate(cwd: string, terminal: TerminalIO, snapshotD
63
37
 
64
38
  return 0
65
39
  }
66
-
67
- function isWorkspaceDiscoveryDefaultsError(error: unknown): boolean {
68
- const message = error instanceof Error ? error.message : String(error)
69
- return (
70
- message.includes('Unable to discover workspaces') ||
71
- message.includes('Unmatched workspaces') ||
72
- message.includes('zero-config mode')
73
- )
74
- }
75
-
76
- function isDefaultEquivalentConfig(config: SnapshotConfig): boolean {
77
- return JSON.stringify(config) === JSON.stringify(DEFAULT_CONFIG)
78
- }
79
-
80
- function writeDiscoveredWorkspacesSummary(terminal: TerminalIO, workspacesRel: string[]): void {
81
- if (workspacesRel.length === 0) {
82
- terminal.subtle('Auto-discovered workspaces: none\n')
83
- return
84
- }
85
-
86
- terminal.subtle(`Auto-discovered workspaces (${workspacesRel.length}): ${workspacesRel.join(', ')}\n`)
87
- }
package/src/formatters.ts CHANGED
@@ -8,6 +8,32 @@ export type SnapshotLike = {
8
8
  workspaces: string[]
9
9
  rules: RuleObject
10
10
  }
11
+ export type RuleCatalogLike = {
12
+ groupId: string
13
+ availableRules: string[]
14
+ coreRules: string[]
15
+ pluginRulesByPrefix: Record<string, string[]>
16
+ observedRules: string[]
17
+ missingRules: string[]
18
+ observedOffRules: string[]
19
+ observedActiveRules: string[]
20
+ totalStats: UsageStats & { observedOutsideCatalog: number }
21
+ coreStats: UsageStats
22
+ pluginStats: Array<{ pluginId: string } & UsageStats>
23
+ }
24
+
25
+ export type UsageStats = {
26
+ totalAvailable: number
27
+ inUse: number
28
+ active: number
29
+ inactive: number
30
+ error: number
31
+ warn: number
32
+ off: number
33
+ missing: number
34
+ inUsePct: number
35
+ activePctOfInUse: number
36
+ }
11
37
 
12
38
  export function formatDiff(groupId: string, diff: SnapshotDiff): string {
13
39
  const lines = [`group: ${groupId}`]
@@ -110,40 +136,60 @@ export function formatShortPrint(snapshots: SnapshotLike[]): string {
110
136
  const sorted = [...snapshots].sort((a, b) => a.groupId.localeCompare(b.groupId))
111
137
 
112
138
  for (const snapshot of sorted) {
113
- const ruleNames = Object.keys(snapshot.rules).sort()
114
- const severityCounts = { error: 0, warn: 0, off: 0 }
139
+ appendShortSnapshotSection(lines, snapshot)
140
+ }
115
141
 
116
- for (const name of ruleNames) {
117
- const severity = getPrimarySeverity(snapshot.rules[name])
118
- if (severity) {
119
- severityCounts[severity] += 1
120
- }
142
+ return `${lines.join('\n')}\n`
143
+ }
144
+
145
+ function appendShortSnapshotSection(lines: string[], snapshot: SnapshotLike): void {
146
+ const ruleNames = Object.keys(snapshot.rules).sort()
147
+ const severityCounts = countRuleNameSeverities(ruleNames, snapshot.rules)
148
+ lines.push(
149
+ `group: ${snapshot.groupId}`,
150
+ `workspaces (${snapshot.workspaces.length}): ${snapshot.workspaces.length > 0 ? snapshot.workspaces.join(', ') : '(none)'}`,
151
+ `rules (${ruleNames.length}): error ${severityCounts.error}, warn ${severityCounts.warn}, off ${severityCounts.off}`
152
+ )
153
+
154
+ for (const ruleName of ruleNames) {
155
+ const line = formatShortRuleLine(ruleName, snapshot.rules[ruleName])
156
+ if (line) {
157
+ lines.push(line)
121
158
  }
159
+ }
160
+ }
122
161
 
123
- lines.push(
124
- `group: ${snapshot.groupId}`,
125
- `workspaces (${snapshot.workspaces.length}): ${snapshot.workspaces.length > 0 ? snapshot.workspaces.join(', ') : '(none)'}`,
126
- `rules (${ruleNames.length}): error ${severityCounts.error}, warn ${severityCounts.warn}, off ${severityCounts.off}`
127
- )
162
+ function countRuleNameSeverities(
163
+ ruleNames: string[],
164
+ rules: RuleObject
165
+ ): {
166
+ error: number
167
+ warn: number
168
+ off: number
169
+ } {
170
+ const counts = { error: 0, warn: 0, off: 0 }
171
+ for (const name of ruleNames) {
172
+ const severity = getPrimarySeverity(rules[name])
173
+ if (severity) {
174
+ counts[severity] += 1
175
+ }
176
+ }
177
+ return counts
178
+ }
128
179
 
129
- for (const ruleName of ruleNames) {
130
- const entry = snapshot.rules[ruleName]
131
- if (!entry) {
132
- continue
133
- }
134
- if (!Array.isArray(entry[0])) {
135
- const singleEntry = entry as RuleEntry
136
- const suffix = singleEntry.length > 1 ? ` ${JSON.stringify(singleEntry[1])}` : ''
137
- lines.push(`${ruleName}: ${singleEntry[0]}${suffix}`)
138
- continue
139
- }
180
+ function formatShortRuleLine(ruleName: string, entry: SnapshotRuleEntry | undefined): string | undefined {
181
+ if (!entry) {
182
+ return undefined
183
+ }
140
184
 
141
- const variants = entry as RuleEntry[]
142
- lines.push(`${ruleName}: ${JSON.stringify(variants)}`)
143
- }
185
+ if (!Array.isArray(entry[0])) {
186
+ const singleEntry = entry as RuleEntry
187
+ const suffix = singleEntry.length > 1 ? ` ${JSON.stringify(singleEntry[1])}` : ''
188
+ return `${ruleName}: ${singleEntry[0]}${suffix}`
144
189
  }
145
190
 
146
- return `${lines.join('\n')}\n`
191
+ const variants = entry as RuleEntry[]
192
+ return `${ruleName}: ${JSON.stringify(variants)}`
147
193
  }
148
194
 
149
195
  export function formatShortConfig(payload: {
@@ -165,6 +211,42 @@ export function formatShortConfig(payload: {
165
211
  return `${lines.join('\n')}\n`
166
212
  }
167
213
 
214
+ export function formatShortCatalog(catalogs: RuleCatalogLike[], missingOnly: boolean): string {
215
+ const lines: string[] = []
216
+ const sorted = [...catalogs].sort((a, b) => a.groupId.localeCompare(b.groupId))
217
+ const showGroupHeader = sorted.length > 1 || sorted.some((catalog) => catalog.groupId !== 'default')
218
+ for (const catalog of sorted) {
219
+ if (showGroupHeader) {
220
+ lines.push(`🧭 group: ${catalog.groupId}`)
221
+ }
222
+ lines.push(
223
+ `📦 total: ${formatUsageLine(catalog.totalStats)} | outside catalog observed: ${catalog.totalStats.observedOutsideCatalog}`,
224
+ `🧱 core: ${formatUsageLine(catalog.coreStats)}`,
225
+ `🔌 plugins tracked: ${catalog.pluginStats.length}`
226
+ )
227
+ for (const plugin of catalog.pluginStats) {
228
+ lines.push(` - ${plugin.pluginId}: ${formatUsageLine(plugin)}`)
229
+ }
230
+
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}`)
235
+ }
236
+ if (showGroupHeader) {
237
+ lines.push('')
238
+ }
239
+ }
240
+ if (showGroupHeader && lines.at(-1) === '') {
241
+ lines.pop()
242
+ }
243
+ return `${lines.join('\n')}\n`
244
+ }
245
+
246
+ function formatUsageLine(stats: UsageStats): string {
247
+ return `${stats.inUse}/${stats.totalAvailable} in use (${stats.inUsePct}%) | error ${stats.error} | warn ${stats.warn} | off ${stats.off} | not used ${stats.missing}`
248
+ }
249
+
168
250
  export function formatCommandDisplayLabel(commandLabel: string): string {
169
251
  switch (commandLabel) {
170
252
  case 'check':
@@ -192,6 +274,12 @@ export function formatCommandDisplayLabel(commandLabel: string): string {
192
274
  case 'config:short': {
193
275
  return 'Show effective runtime config (short view)'
194
276
  }
277
+ case 'catalog:json': {
278
+ return 'Show discovered rule catalog (JSON)'
279
+ }
280
+ case 'catalog:short': {
281
+ return 'Show discovered rule catalog (short view)'
282
+ }
195
283
  case 'init': {
196
284
  return 'Initialize local configuration'
197
285
  }
package/src/index.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ import { loadConfig } from '@eslint-config-snapshot/api'
2
3
  import { Command, CommanderError, InvalidArgumentError } from 'commander'
3
4
  import createDebug from 'debug'
4
5
  import path from 'node:path'
5
6
 
7
+ import { type CatalogFormat, executeCatalog, executeCatalogCheck, executeCatalogUpdate } from './commands/catalog.js'
6
8
  import { type CheckFormat, executeCheck } from './commands/check.js'
7
9
  import { executeConfig, executePrint, type PrintFormat } from './commands/print.js'
8
10
  import { executeUpdate } from './commands/update.js'
@@ -79,7 +81,7 @@ async function runArgv(argv: string[], cwd: string): Promise<number> {
79
81
  }
80
82
 
81
83
  async function runDefaultInvocation(argv: string[], cwd: string, terminal: TerminalIO): Promise<number> {
82
- const known = new Set(['-u', '--update', '-h', '--help'])
84
+ const known = new Set(['-u', '--update', '-h', '--help', '--experimental-with-catalog'])
83
85
  for (const token of argv) {
84
86
  if (!known.has(token)) {
85
87
  terminal.error(`error: unknown option '${token}'\n`)
@@ -96,10 +98,22 @@ async function runDefaultInvocation(argv: string[], cwd: string, terminal: Termi
96
98
  }
97
99
 
98
100
  if (argv.includes('-u') || argv.includes('--update')) {
99
- return executeUpdate(cwd, terminal, SNAPSHOT_DIR, true)
101
+ const updateCode = await executeUpdate(cwd, terminal, SNAPSHOT_DIR, true)
102
+ const withCatalog = argv.includes('--experimental-with-catalog') || (await isCatalogHookEnabled(cwd))
103
+ if (!withCatalog) {
104
+ return updateCode
105
+ }
106
+ const catalogCode = await executeCatalogUpdate(cwd, terminal, SNAPSHOT_DIR)
107
+ return updateCode !== 0 || catalogCode !== 0 ? 1 : 0
100
108
  }
101
109
 
102
- return executeCheck(cwd, 'summary', terminal, SNAPSHOT_DIR, true)
110
+ const checkCode = await executeCheck(cwd, 'summary', terminal, SNAPSHOT_DIR, true)
111
+ const withCatalog = argv.includes('--experimental-with-catalog') || (await isCatalogHookEnabled(cwd))
112
+ if (!withCatalog) {
113
+ return checkCode
114
+ }
115
+ const catalogCode = await executeCatalogCheck(cwd, terminal, SNAPSHOT_DIR)
116
+ return checkCode !== 0 || catalogCode !== 0 ? 1 : 0
103
117
  }
104
118
 
105
119
  function createProgram(cwd: string, terminal: TerminalIO, onActionExit: (code: number) => void): Command {
@@ -122,16 +136,34 @@ function createProgram(cwd: string, terminal: TerminalIO, onActionExit: (code: n
122
136
  .command('check')
123
137
  .description('Compare current state against stored snapshots')
124
138
  .option('--format <format>', 'Output format: summary|status|diff', parseCheckFormat, 'summary')
125
- .action(async (opts: { format: CheckFormat }) => {
126
- onActionExit(await executeCheck(cwd, opts.format, terminal, SNAPSHOT_DIR))
139
+ .option('--experimental-with-catalog', 'Also run catalog baseline check after regular check')
140
+ .action(async (opts: { format: CheckFormat; experimentalWithCatalog?: boolean }) => {
141
+ const checkCode = await executeCheck(cwd, opts.format, terminal, SNAPSHOT_DIR)
142
+ const withCatalog = opts.experimentalWithCatalog === true || (await isCatalogHookEnabled(cwd))
143
+ if (!withCatalog) {
144
+ onActionExit(checkCode)
145
+ return
146
+ }
147
+
148
+ const catalogCode = await executeCatalogCheck(cwd, terminal, SNAPSHOT_DIR)
149
+ onActionExit(checkCode !== 0 || catalogCode !== 0 ? 1 : 0)
127
150
  })
128
151
 
129
152
  program
130
153
  .command('update')
131
154
  .alias('snapshot')
132
155
  .description('Compute and write snapshots to .eslint-config-snapshot/')
133
- .action(async () => {
134
- onActionExit(await executeUpdate(cwd, terminal, SNAPSHOT_DIR, true))
156
+ .option('--experimental-with-catalog', 'Also update catalog baseline after regular update')
157
+ .action(async (opts: { experimentalWithCatalog?: boolean }) => {
158
+ const updateCode = await executeUpdate(cwd, terminal, SNAPSHOT_DIR, true)
159
+ const withCatalog = opts.experimentalWithCatalog === true || (await isCatalogHookEnabled(cwd))
160
+ if (!withCatalog) {
161
+ onActionExit(updateCode)
162
+ return
163
+ }
164
+
165
+ const catalogCode = await executeCatalogUpdate(cwd, terminal, SNAPSHOT_DIR)
166
+ onActionExit(updateCode !== 0 || catalogCode !== 0 ? 1 : 0)
135
167
  })
136
168
 
137
169
  program
@@ -141,8 +173,32 @@ function createProgram(cwd: string, terminal: TerminalIO, onActionExit: (code: n
141
173
  .option('--short', 'Alias for --format short')
142
174
  .action(async (opts: { format: PrintFormat; short?: boolean }) => {
143
175
  const format: PrintFormat = opts.short ? 'short' : opts.format
144
- await executePrint(cwd, terminal, SNAPSHOT_DIR, format)
145
- onActionExit(0)
176
+ onActionExit(await executePrint(cwd, terminal, SNAPSHOT_DIR, format))
177
+ })
178
+
179
+ program
180
+ .command('catalog')
181
+ .description('Print discovered rule catalog and missing rules')
182
+ .option('--format <format>', 'Output format: json|short', parsePrintFormat, 'json')
183
+ .option('--short', 'Alias for --format short')
184
+ .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
+ const format: CatalogFormat = opts.short ? 'short' : opts.format
187
+ onActionExit(await executeCatalog(cwd, terminal, SNAPSHOT_DIR, format, Boolean(opts.missing)))
188
+ })
189
+
190
+ program
191
+ .command('catalog-check')
192
+ .description('Compare current catalog against stored catalog baseline')
193
+ .action(async () => {
194
+ onActionExit(await executeCatalogCheck(cwd, terminal, SNAPSHOT_DIR))
195
+ })
196
+
197
+ program
198
+ .command('catalog-update')
199
+ .description('Compute and write catalog baselines to .eslint-config-snapshot/')
200
+ .action(async () => {
201
+ onActionExit(await executeCatalogUpdate(cwd, terminal, SNAPSHOT_DIR))
146
202
  })
147
203
 
148
204
  program
@@ -152,8 +208,7 @@ function createProgram(cwd: string, terminal: TerminalIO, onActionExit: (code: n
152
208
  .option('--short', 'Alias for --format short')
153
209
  .action(async (opts: { format: PrintFormat; short?: boolean }) => {
154
210
  const format: PrintFormat = opts.short ? 'short' : opts.format
155
- await executeConfig(cwd, terminal, SNAPSHOT_DIR, format)
156
- onActionExit(0)
211
+ onActionExit(await executeConfig(cwd, terminal, SNAPSHOT_DIR, format))
157
212
  })
158
213
 
159
214
  program
@@ -247,6 +302,11 @@ function parseInitPreset(value: string): InitPreset {
247
302
  throw new InvalidArgumentError('Expected one of: recommended, minimal, full')
248
303
  }
249
304
 
305
+ async function isCatalogHookEnabled(cwd: string): Promise<boolean> {
306
+ const config = await loadConfig(cwd)
307
+ return config.experimentalWithCatalog === true
308
+ }
309
+
250
310
  export async function main(): Promise<void> {
251
311
  const code = await runArgv(process.argv.slice(2), process.cwd())
252
312
  process.exitCode = code