@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.
- package/CHANGELOG.md +22 -0
- package/dist/index.cjs +910 -439
- package/dist/index.js +901 -429
- package/package.json +2 -2
- package/src/commands/catalog.ts +394 -0
- package/src/commands/check.ts +82 -109
- package/src/commands/print.ts +31 -23
- package/src/commands/skipped-workspaces.ts +9 -0
- package/src/commands/snapshot-executor.ts +97 -0
- package/src/commands/update.ts +14 -62
- package/src/formatters.ts +115 -27
- package/src/index.ts +71 -11
- package/src/run-context.ts +50 -30
- package/src/runtime.ts +203 -93
- package/test/cli.integration.test.ts +92 -0
- package/test/cli.terminal.integration.test.ts +70 -0
package/src/commands/print.ts
CHANGED
|
@@ -1,25 +1,29 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { loadConfig } from '@eslint-config-snapshot/api'
|
|
2
2
|
|
|
3
3
|
import { formatShortConfig, formatShortPrint } from '../formatters.js'
|
|
4
|
-
import {
|
|
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<
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
18
|
-
const currentSnapshots =
|
|
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<
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
+
}
|
package/src/commands/update.ts
CHANGED
|
@@ -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
|
|
5
|
-
import {
|
|
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
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
114
|
-
|
|
139
|
+
appendShortSnapshotSection(lines, snapshot)
|
|
140
|
+
}
|
|
115
141
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
.
|
|
126
|
-
|
|
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
|
-
.
|
|
134
|
-
|
|
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
|
-
|
|
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
|