@eslint-config-snapshot/cli 0.9.0 → 0.14.1
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 +74 -0
- package/README.md +10 -0
- package/dist/index.cjs +975 -794
- package/dist/index.js +978 -800
- package/package.json +3 -2
- package/src/commands/check.ts +157 -0
- package/src/commands/print.ts +58 -0
- package/src/commands/update.ts +49 -0
- package/src/formatters.ts +256 -0
- package/src/index.ts +48 -1204
- package/src/init.ts +331 -0
- package/src/run-context.ts +161 -0
- package/src/runtime.ts +224 -0
- package/src/terminal.ts +178 -0
- package/test/cli.integration.test.ts +4 -6
- package/test/cli.npm-isolated.integration.test.ts +1 -1
- package/test/cli.pnpm-isolated.integration.test.ts +1 -1
- package/test/cli.terminal.integration.test.ts +10 -5
- package/test/fixtures/npm-isolated-template/eslint-config-snapshot.config.mjs +1 -2
- package/test/fixtures/repo/eslint-config-snapshot.config.mjs +1 -2
- package/test/formatters.unit.test.ts +47 -0
- package/test/init.unit.test.ts +31 -0
- package/test/runtime.unit.test.ts +36 -0
- package/test/ui.unit.test.ts +12 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eslint-config-snapshot/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@inquirer/prompts": "^8.2.0",
|
|
31
31
|
"commander": "^14.0.3",
|
|
32
|
+
"debug": "^4.4.3",
|
|
32
33
|
"fast-glob": "^3.3.3",
|
|
33
|
-
"@eslint-config-snapshot/api": "0.
|
|
34
|
+
"@eslint-config-snapshot/api": "0.14.1"
|
|
34
35
|
}
|
|
35
36
|
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { findConfigPath } from '@eslint-config-snapshot/api'
|
|
2
|
+
|
|
3
|
+
import { countUniqueWorkspaces, decorateDiffLine, formatDiff, summarizeChanges, summarizeSnapshots } from '../formatters.js'
|
|
4
|
+
import { writeEslintVersionSummary, writeRunContextHeader } from '../run-context.js'
|
|
5
|
+
import {
|
|
6
|
+
type BuiltSnapshot,
|
|
7
|
+
compareSnapshotMaps,
|
|
8
|
+
computeCurrentSnapshots,
|
|
9
|
+
type GroupEslintVersions,
|
|
10
|
+
loadStoredSnapshots,
|
|
11
|
+
resolveGroupEslintVersions,
|
|
12
|
+
type SnapshotDiff,
|
|
13
|
+
writeSnapshots
|
|
14
|
+
} from '../runtime.js'
|
|
15
|
+
import { type TerminalIO } from '../terminal.js'
|
|
16
|
+
|
|
17
|
+
export type CheckFormat = 'summary' | 'status' | 'diff'
|
|
18
|
+
|
|
19
|
+
const UPDATE_HINT = 'Tip: when you intentionally accept changes, run `eslint-config-snapshot --update` to refresh the baseline.\n'
|
|
20
|
+
|
|
21
|
+
export async function executeCheck(
|
|
22
|
+
cwd: string,
|
|
23
|
+
format: CheckFormat,
|
|
24
|
+
terminal: TerminalIO,
|
|
25
|
+
snapshotDir: string,
|
|
26
|
+
defaultInvocation = false
|
|
27
|
+
): Promise<number> {
|
|
28
|
+
const foundConfig = await findConfigPath(cwd)
|
|
29
|
+
const storedSnapshots = await loadStoredSnapshots(cwd, snapshotDir)
|
|
30
|
+
|
|
31
|
+
if (format !== 'status') {
|
|
32
|
+
writeRunContextHeader(terminal, cwd, defaultInvocation ? 'check' : `check:${format}`, foundConfig?.path, storedSnapshots)
|
|
33
|
+
if (terminal.showProgress) {
|
|
34
|
+
terminal.subtle('🔎 Checking current ESLint configuration...\n')
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!foundConfig) {
|
|
39
|
+
terminal.subtle(
|
|
40
|
+
'Tip: no explicit config found. Using safe built-in defaults. Run `eslint-config-snapshot init` to customize when needed.\n'
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let currentSnapshots: Map<string, BuiltSnapshot>
|
|
45
|
+
try {
|
|
46
|
+
currentSnapshots = await computeCurrentSnapshots(cwd)
|
|
47
|
+
} catch (error: unknown) {
|
|
48
|
+
if (!foundConfig) {
|
|
49
|
+
terminal.write(
|
|
50
|
+
'Automatic workspace discovery could not complete with defaults.\nRun `eslint-config-snapshot init` to configure workspaces, then run `eslint-config-snapshot --update`.\n'
|
|
51
|
+
)
|
|
52
|
+
return 1
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
throw error
|
|
56
|
+
}
|
|
57
|
+
if (storedSnapshots.size === 0) {
|
|
58
|
+
const summary = summarizeSnapshots(currentSnapshots)
|
|
59
|
+
terminal.write(
|
|
60
|
+
`Rules found in this analysis: ${summary.groups} groups, ${summary.rules} rules (severity mix: ${summary.error} errors, ${summary.warn} warnings, ${summary.off} off).\n`
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
const canPromptBaseline = defaultInvocation || format === 'summary'
|
|
64
|
+
if (canPromptBaseline && terminal.isInteractive) {
|
|
65
|
+
const shouldCreateBaseline = await terminal.askYesNo(
|
|
66
|
+
'No baseline yet. Do you want to save this analyzed rule state as your baseline now? [Y/n] ',
|
|
67
|
+
true
|
|
68
|
+
)
|
|
69
|
+
if (shouldCreateBaseline) {
|
|
70
|
+
await writeSnapshots(cwd, snapshotDir, currentSnapshots)
|
|
71
|
+
const createdSummary = summarizeSnapshots(currentSnapshots)
|
|
72
|
+
terminal.write(`Great start: baseline created with ${createdSummary.groups} groups and ${createdSummary.rules} rules.\n`)
|
|
73
|
+
terminal.subtle(UPDATE_HINT)
|
|
74
|
+
return 0
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
terminal.write('You are almost set: no baseline snapshot found yet.\n')
|
|
79
|
+
terminal.write('Run `eslint-config-snapshot --update` to create your first baseline.\n')
|
|
80
|
+
return 1
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const changes = compareSnapshotMaps(storedSnapshots, currentSnapshots)
|
|
84
|
+
const eslintVersionsByGroup = terminal.showProgress ? await resolveGroupEslintVersions(cwd) : new Map<string, string[]>()
|
|
85
|
+
|
|
86
|
+
if (format === 'status') {
|
|
87
|
+
if (changes.length === 0) {
|
|
88
|
+
terminal.write('clean\n')
|
|
89
|
+
return 0
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
terminal.write('changes\n')
|
|
93
|
+
terminal.subtle(UPDATE_HINT)
|
|
94
|
+
return 1
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (format === 'diff') {
|
|
98
|
+
if (changes.length === 0) {
|
|
99
|
+
terminal.write('Great news: no snapshot changes detected.\n')
|
|
100
|
+
writeEslintVersionSummary(terminal, eslintVersionsByGroup)
|
|
101
|
+
return 0
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (const change of changes) {
|
|
105
|
+
terminal.write(`${formatDiff(change.groupId, change.diff)}\n`)
|
|
106
|
+
}
|
|
107
|
+
terminal.subtle(UPDATE_HINT)
|
|
108
|
+
|
|
109
|
+
return 1
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return printWhatChanged(terminal, changes, currentSnapshots, eslintVersionsByGroup)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function printWhatChanged(
|
|
116
|
+
terminal: TerminalIO,
|
|
117
|
+
changes: Array<{ groupId: string; diff: SnapshotDiff }>,
|
|
118
|
+
currentSnapshots: Map<string, BuiltSnapshot>,
|
|
119
|
+
eslintVersionsByGroup: GroupEslintVersions
|
|
120
|
+
): number {
|
|
121
|
+
const color = terminal.colors
|
|
122
|
+
const currentSummary = summarizeSnapshots(currentSnapshots)
|
|
123
|
+
const workspaceCount = countUniqueWorkspaces(currentSnapshots)
|
|
124
|
+
const changeSummary = summarizeChanges(changes)
|
|
125
|
+
|
|
126
|
+
if (changes.length === 0) {
|
|
127
|
+
terminal.write(color.green('✅ Great news: no snapshot drift detected.\n'))
|
|
128
|
+
terminal.section('📊 Summary')
|
|
129
|
+
terminal.write(
|
|
130
|
+
`- 📦 baseline: ${currentSummary.groups} groups, ${currentSummary.rules} rules\n- 🗂️ workspaces scanned: ${workspaceCount}\n- 🎚️ severity mix: ${currentSummary.error} errors, ${currentSummary.warn} warnings, ${currentSummary.off} off\n`
|
|
131
|
+
)
|
|
132
|
+
writeEslintVersionSummary(terminal, eslintVersionsByGroup)
|
|
133
|
+
return 0
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
terminal.write(color.red('⚠️ Heads up: snapshot drift detected.\n'))
|
|
137
|
+
terminal.section('📊 Summary')
|
|
138
|
+
terminal.write(
|
|
139
|
+
`- changed groups: ${changes.length}\n- introduced rules: ${changeSummary.introduced}\n- removed rules: ${changeSummary.removed}\n- severity changes: ${changeSummary.severity}\n- options changes: ${changeSummary.options}\n- workspace membership changes: ${changeSummary.workspace}\n- 🗂️ workspaces scanned: ${workspaceCount}\n- current baseline: ${currentSummary.groups} groups, ${currentSummary.rules} rules\n- current severity mix: ${currentSummary.error} errors, ${currentSummary.warn} warnings, ${currentSummary.off} off\n`
|
|
140
|
+
)
|
|
141
|
+
writeEslintVersionSummary(terminal, eslintVersionsByGroup)
|
|
142
|
+
terminal.write('\n')
|
|
143
|
+
|
|
144
|
+
terminal.section('🧾 Changes')
|
|
145
|
+
for (const change of changes) {
|
|
146
|
+
terminal.write(color.bold(`group ${change.groupId}\n`))
|
|
147
|
+
const lines = formatDiff(change.groupId, change.diff).split('\n').slice(1)
|
|
148
|
+
for (const line of lines) {
|
|
149
|
+
const decorated = decorateDiffLine(line, color)
|
|
150
|
+
terminal.write(`${decorated}\n`)
|
|
151
|
+
}
|
|
152
|
+
terminal.write('\n')
|
|
153
|
+
}
|
|
154
|
+
terminal.subtle(UPDATE_HINT)
|
|
155
|
+
|
|
156
|
+
return 1
|
|
157
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { findConfigPath, loadConfig } from '@eslint-config-snapshot/api'
|
|
2
|
+
|
|
3
|
+
import { formatShortConfig, formatShortPrint } from '../formatters.js'
|
|
4
|
+
import { writeRunContextHeader } from '../run-context.js'
|
|
5
|
+
import { computeCurrentSnapshots, loadStoredSnapshots, resolveWorkspaceAssignments, type WorkspaceAssignments } from '../runtime.js'
|
|
6
|
+
import { type TerminalIO } from '../terminal.js'
|
|
7
|
+
|
|
8
|
+
export type PrintFormat = 'json' | 'short'
|
|
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')
|
|
16
|
+
}
|
|
17
|
+
const currentSnapshots = await computeCurrentSnapshots(cwd)
|
|
18
|
+
|
|
19
|
+
if (format === 'short') {
|
|
20
|
+
terminal.write(formatShortPrint([...currentSnapshots.values()]))
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const output = [...currentSnapshots.values()].map((snapshot) => ({
|
|
25
|
+
groupId: snapshot.groupId,
|
|
26
|
+
rules: snapshot.rules
|
|
27
|
+
}))
|
|
28
|
+
terminal.write(`${JSON.stringify(output, null, 2)}\n`)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function executeConfig(cwd: string, terminal: TerminalIO, snapshotDir: string, format: PrintFormat): Promise<void> {
|
|
32
|
+
const foundConfig = await findConfigPath(cwd)
|
|
33
|
+
const storedSnapshots = await loadStoredSnapshots(cwd, snapshotDir)
|
|
34
|
+
writeRunContextHeader(terminal, cwd, `config:${format}`, foundConfig?.path, storedSnapshots)
|
|
35
|
+
if (terminal.showProgress) {
|
|
36
|
+
terminal.subtle('⚙️ Resolving effective runtime configuration...\n')
|
|
37
|
+
}
|
|
38
|
+
const config = await loadConfig(cwd)
|
|
39
|
+
const resolved: WorkspaceAssignments = await resolveWorkspaceAssignments(cwd, config)
|
|
40
|
+
const payload = {
|
|
41
|
+
source: foundConfig?.path ?? 'built-in-defaults',
|
|
42
|
+
workspaceInput: config.workspaceInput,
|
|
43
|
+
workspaces: resolved.discovery.workspacesRel,
|
|
44
|
+
grouping: {
|
|
45
|
+
mode: config.grouping.mode,
|
|
46
|
+
allowEmptyGroups: config.grouping.allowEmptyGroups ?? false,
|
|
47
|
+
groups: resolved.assignments.map((entry) => ({ name: entry.name, workspaces: entry.workspaces }))
|
|
48
|
+
},
|
|
49
|
+
sampling: config.sampling
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (format === 'short') {
|
|
53
|
+
terminal.write(formatShortConfig(payload))
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
terminal.write(`${JSON.stringify(payload, null, 2)}\n`)
|
|
58
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { findConfigPath } from '@eslint-config-snapshot/api'
|
|
2
|
+
|
|
3
|
+
import { countUniqueWorkspaces, summarizeSnapshots } from '../formatters.js'
|
|
4
|
+
import { writeEslintVersionSummary, writeRunContextHeader } from '../run-context.js'
|
|
5
|
+
import { computeCurrentSnapshots, loadStoredSnapshots, resolveGroupEslintVersions, writeSnapshots } from '../runtime.js'
|
|
6
|
+
import { type TerminalIO } from '../terminal.js'
|
|
7
|
+
|
|
8
|
+
export async function executeUpdate(cwd: string, terminal: TerminalIO, snapshotDir: string, printSummary: boolean): Promise<number> {
|
|
9
|
+
const foundConfig = await findConfigPath(cwd)
|
|
10
|
+
const storedSnapshots = await loadStoredSnapshots(cwd, snapshotDir)
|
|
11
|
+
writeRunContextHeader(terminal, cwd, 'update', foundConfig?.path, storedSnapshots)
|
|
12
|
+
if (terminal.showProgress) {
|
|
13
|
+
terminal.subtle('🔎 Checking current ESLint configuration...\n')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (!foundConfig) {
|
|
17
|
+
terminal.subtle(
|
|
18
|
+
'Tip: no explicit config found. Using safe built-in defaults. Run `eslint-config-snapshot init` to customize when needed.\n'
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let currentSnapshots
|
|
23
|
+
try {
|
|
24
|
+
currentSnapshots = await computeCurrentSnapshots(cwd)
|
|
25
|
+
} catch (error: unknown) {
|
|
26
|
+
if (!foundConfig) {
|
|
27
|
+
terminal.write(
|
|
28
|
+
'Automatic workspace discovery could not complete with defaults.\nRun `eslint-config-snapshot init` to configure workspaces, then run `eslint-config-snapshot --update`.\n'
|
|
29
|
+
)
|
|
30
|
+
return 1
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
throw error
|
|
34
|
+
}
|
|
35
|
+
await writeSnapshots(cwd, snapshotDir, currentSnapshots)
|
|
36
|
+
|
|
37
|
+
if (printSummary) {
|
|
38
|
+
const summary = summarizeSnapshots(currentSnapshots)
|
|
39
|
+
const workspaceCount = countUniqueWorkspaces(currentSnapshots)
|
|
40
|
+
const eslintVersionsByGroup = terminal.showProgress ? await resolveGroupEslintVersions(cwd) : new Map<string, string[]>()
|
|
41
|
+
terminal.section('📊 Summary')
|
|
42
|
+
terminal.write(
|
|
43
|
+
`Baseline updated: ${summary.groups} groups, ${summary.rules} rules.\nWorkspaces scanned: ${workspaceCount}.\nSeverity mix: ${summary.error} errors, ${summary.warn} warnings, ${summary.off} off.\n`
|
|
44
|
+
)
|
|
45
|
+
writeEslintVersionSummary(terminal, eslintVersionsByGroup)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return 0
|
|
49
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import type { SnapshotDiff, SnapshotRuleEntry } from '@eslint-config-snapshot/api'
|
|
2
|
+
|
|
3
|
+
export type RuleEntry = [severity: 'off' | 'warn' | 'error'] | [severity: 'off' | 'warn' | 'error', options: unknown]
|
|
4
|
+
export type RuleObject = Record<string, SnapshotRuleEntry>
|
|
5
|
+
|
|
6
|
+
export type SnapshotLike = {
|
|
7
|
+
groupId: string
|
|
8
|
+
workspaces: string[]
|
|
9
|
+
rules: RuleObject
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function formatDiff(groupId: string, diff: SnapshotDiff): string {
|
|
13
|
+
const lines = [`group: ${groupId}`]
|
|
14
|
+
|
|
15
|
+
addListSection(lines, 'introduced rules', diff.introducedRules)
|
|
16
|
+
addListSection(lines, 'removed rules', diff.removedRules)
|
|
17
|
+
|
|
18
|
+
if (diff.severityChanges.length > 0) {
|
|
19
|
+
lines.push('severity changed:')
|
|
20
|
+
for (const change of diff.severityChanges) {
|
|
21
|
+
lines.push(` - ${change.rule}: ${change.before} -> ${change.after}`)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const optionChanges = getDisplayOptionChanges(diff)
|
|
26
|
+
if (optionChanges.length > 0) {
|
|
27
|
+
lines.push('options changed:')
|
|
28
|
+
for (const change of optionChanges) {
|
|
29
|
+
lines.push(` - ${change.rule}: ${formatValue(change.before)} -> ${formatValue(change.after)}`)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
addListSection(lines, 'workspaces added', diff.workspaceMembershipChanges.added)
|
|
34
|
+
addListSection(lines, 'workspaces removed', diff.workspaceMembershipChanges.removed)
|
|
35
|
+
|
|
36
|
+
return lines.join('\n')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getDisplayOptionChanges(diff: SnapshotDiff): SnapshotDiff['optionChanges'] {
|
|
40
|
+
const removedRules = new Set(diff.removedRules)
|
|
41
|
+
const severityChangedRules = new Set(diff.severityChanges.map((change) => change.rule))
|
|
42
|
+
return diff.optionChanges.filter((change) => !removedRules.has(change.rule) && !severityChangedRules.has(change.rule))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function addListSection(lines: string[], title: string, values: string[]): void {
|
|
46
|
+
if (values.length === 0) {
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
lines.push(`${title}:`)
|
|
51
|
+
for (const value of values) {
|
|
52
|
+
lines.push(` - ${value}`)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function formatValue(value: unknown): string {
|
|
57
|
+
const serialized = JSON.stringify(value)
|
|
58
|
+
return serialized === undefined ? 'undefined' : serialized
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function summarizeChanges(changes: Array<{ groupId: string; diff: SnapshotDiff }>) {
|
|
62
|
+
let introduced = 0
|
|
63
|
+
let removed = 0
|
|
64
|
+
let severity = 0
|
|
65
|
+
let options = 0
|
|
66
|
+
let workspace = 0
|
|
67
|
+
for (const change of changes) {
|
|
68
|
+
introduced += change.diff.introducedRules.length
|
|
69
|
+
removed += change.diff.removedRules.length
|
|
70
|
+
severity += change.diff.severityChanges.length
|
|
71
|
+
options += getDisplayOptionChanges(change.diff).length
|
|
72
|
+
workspace += change.diff.workspaceMembershipChanges.added.length + change.diff.workspaceMembershipChanges.removed.length
|
|
73
|
+
}
|
|
74
|
+
return { introduced, removed, severity, options, workspace }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function summarizeSnapshots(snapshots: Map<string, SnapshotLike>) {
|
|
78
|
+
const { rules, error, warn, off } = countRuleSeverities([...snapshots.values()].map((snapshot) => snapshot.rules))
|
|
79
|
+
return { groups: snapshots.size, rules, error, warn, off }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function countUniqueWorkspaces(snapshots: Map<string, SnapshotLike>): number {
|
|
83
|
+
const workspaces = new Set<string>()
|
|
84
|
+
for (const snapshot of snapshots.values()) {
|
|
85
|
+
for (const workspace of snapshot.workspaces) {
|
|
86
|
+
workspaces.add(workspace)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return workspaces.size
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function decorateDiffLine(
|
|
93
|
+
line: string,
|
|
94
|
+
color: { green: (text: string) => string; red: (text: string) => string; yellow: (text: string) => string }
|
|
95
|
+
): string {
|
|
96
|
+
if (line.startsWith('introduced rules:') || line.startsWith('workspaces added:')) {
|
|
97
|
+
return color.green(`+ ${line}`)
|
|
98
|
+
}
|
|
99
|
+
if (line.startsWith('removed rules:') || line.startsWith('workspaces removed:')) {
|
|
100
|
+
return color.red(`- ${line}`)
|
|
101
|
+
}
|
|
102
|
+
if (line.startsWith('severity changed:') || line.startsWith('options changed:')) {
|
|
103
|
+
return color.yellow(`~ ${line}`)
|
|
104
|
+
}
|
|
105
|
+
return line
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function formatShortPrint(snapshots: SnapshotLike[]): string {
|
|
109
|
+
const lines: string[] = []
|
|
110
|
+
const sorted = [...snapshots].sort((a, b) => a.groupId.localeCompare(b.groupId))
|
|
111
|
+
|
|
112
|
+
for (const snapshot of sorted) {
|
|
113
|
+
const ruleNames = Object.keys(snapshot.rules).sort()
|
|
114
|
+
const severityCounts = { error: 0, warn: 0, off: 0 }
|
|
115
|
+
|
|
116
|
+
for (const name of ruleNames) {
|
|
117
|
+
const severity = getPrimarySeverity(snapshot.rules[name])
|
|
118
|
+
if (severity) {
|
|
119
|
+
severityCounts[severity] += 1
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
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
|
+
)
|
|
128
|
+
|
|
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
|
+
}
|
|
140
|
+
|
|
141
|
+
const variants = entry as RuleEntry[]
|
|
142
|
+
lines.push(`${ruleName}: ${JSON.stringify(variants)}`)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return `${lines.join('\n')}\n`
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function formatShortConfig(payload: {
|
|
150
|
+
source: string
|
|
151
|
+
workspaceInput: unknown
|
|
152
|
+
workspaces: string[]
|
|
153
|
+
grouping: { mode: string; allowEmptyGroups: boolean; groups: Array<{ name: string; workspaces: string[] }> }
|
|
154
|
+
sampling: unknown
|
|
155
|
+
}): string {
|
|
156
|
+
const lines: string[] = [
|
|
157
|
+
`source: ${payload.source}`,
|
|
158
|
+
`workspaces (${payload.workspaces.length}): ${payload.workspaces.join(', ') || '(none)'}`,
|
|
159
|
+
`grouping mode: ${payload.grouping.mode} (allow empty: ${payload.grouping.allowEmptyGroups})`
|
|
160
|
+
]
|
|
161
|
+
for (const group of payload.grouping.groups) {
|
|
162
|
+
lines.push(`group ${group.name} (${group.workspaces.length}): ${group.workspaces.join(', ') || '(none)'}`)
|
|
163
|
+
}
|
|
164
|
+
lines.push(`workspaceInput: ${JSON.stringify(payload.workspaceInput)}`, `sampling: ${JSON.stringify(payload.sampling)}`)
|
|
165
|
+
return `${lines.join('\n')}\n`
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function formatCommandDisplayLabel(commandLabel: string): string {
|
|
169
|
+
switch (commandLabel) {
|
|
170
|
+
case 'check':
|
|
171
|
+
case 'check:summary': {
|
|
172
|
+
return 'Check drift against baseline (summary)'
|
|
173
|
+
}
|
|
174
|
+
case 'check:diff': {
|
|
175
|
+
return 'Check drift against baseline (detailed diff)'
|
|
176
|
+
}
|
|
177
|
+
case 'check:status': {
|
|
178
|
+
return 'Check drift against baseline (status only)'
|
|
179
|
+
}
|
|
180
|
+
case 'update': {
|
|
181
|
+
return 'Update baseline snapshot'
|
|
182
|
+
}
|
|
183
|
+
case 'print:json': {
|
|
184
|
+
return 'Print aggregated rules (JSON)'
|
|
185
|
+
}
|
|
186
|
+
case 'print:short': {
|
|
187
|
+
return 'Print aggregated rules (short view)'
|
|
188
|
+
}
|
|
189
|
+
case 'config:json': {
|
|
190
|
+
return 'Show effective runtime config (JSON)'
|
|
191
|
+
}
|
|
192
|
+
case 'config:short': {
|
|
193
|
+
return 'Show effective runtime config (short view)'
|
|
194
|
+
}
|
|
195
|
+
case 'init': {
|
|
196
|
+
return 'Initialize local configuration'
|
|
197
|
+
}
|
|
198
|
+
case 'help': {
|
|
199
|
+
return 'Show CLI help'
|
|
200
|
+
}
|
|
201
|
+
default: {
|
|
202
|
+
return commandLabel
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function formatStoredSnapshotSummary(storedSnapshots: Map<string, SnapshotLike>): string {
|
|
208
|
+
if (storedSnapshots.size === 0) {
|
|
209
|
+
return 'none'
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const summary = summarizeSnapshots(storedSnapshots)
|
|
213
|
+
return `${summary.groups} groups, ${summary.rules} rules (severity mix: ${summary.error} errors, ${summary.warn} warnings, ${summary.off} off)`
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function countRuleSeverities(ruleObjects: RuleObject[]) {
|
|
217
|
+
let rules = 0
|
|
218
|
+
let error = 0
|
|
219
|
+
let warn = 0
|
|
220
|
+
let off = 0
|
|
221
|
+
|
|
222
|
+
for (const rulesObject of ruleObjects) {
|
|
223
|
+
for (const entry of Object.values(rulesObject)) {
|
|
224
|
+
rules += 1
|
|
225
|
+
const severity = getPrimarySeverity(entry)
|
|
226
|
+
if (severity === 'error') {
|
|
227
|
+
error += 1
|
|
228
|
+
} else if (severity === 'warn') {
|
|
229
|
+
warn += 1
|
|
230
|
+
} else {
|
|
231
|
+
off += 1
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return { rules, error, warn, off }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function getPrimarySeverity(entry: SnapshotRuleEntry | undefined): 'off' | 'warn' | 'error' | undefined {
|
|
240
|
+
if (!entry) {
|
|
241
|
+
return undefined
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (!Array.isArray(entry[0])) {
|
|
245
|
+
return (entry as RuleEntry)[0]
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const variants = entry as RuleEntry[]
|
|
249
|
+
if (variants.some((variant) => variant[0] === 'error')) {
|
|
250
|
+
return 'error'
|
|
251
|
+
}
|
|
252
|
+
if (variants.some((variant) => variant[0] === 'warn')) {
|
|
253
|
+
return 'warn'
|
|
254
|
+
}
|
|
255
|
+
return 'off'
|
|
256
|
+
}
|