@eslint-config-snapshot/cli 0.3.2 → 0.5.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/src/index.ts CHANGED
@@ -5,18 +5,20 @@ import {
5
5
  buildSnapshot,
6
6
  diffSnapshots,
7
7
  discoverWorkspaces,
8
- extractRulesFromPrintConfig,
8
+ extractRulesForWorkspaceSamples,
9
9
  findConfigPath,
10
10
  getConfigScaffold,
11
11
  hasDiff,
12
12
  loadConfig,
13
13
  normalizePath,
14
14
  readSnapshotFile,
15
+ resolveEslintVersionForWorkspace,
15
16
  sampleWorkspaceFiles,
16
17
  writeSnapshotFile
17
18
  } from '@eslint-config-snapshot/api'
18
19
  import { Command, CommanderError, InvalidArgumentError } from 'commander'
19
20
  import fg from 'fast-glob'
21
+ import { existsSync, readFileSync } from 'node:fs'
20
22
  import { access, mkdir, readFile, writeFile } from 'node:fs/promises'
21
23
  import path from 'node:path'
22
24
  import { createInterface } from 'node:readline'
@@ -32,44 +34,69 @@ type CheckFormat = 'summary' | 'status' | 'diff'
32
34
  type PrintFormat = 'json' | 'short'
33
35
  type InitTarget = 'file' | 'package-json'
34
36
  type InitPreset = 'recommended' | 'minimal' | 'full'
37
+ type RuleEntry = [severity: 'off' | 'warn' | 'error'] | [severity: 'off' | 'warn' | 'error', options: unknown]
38
+ type RuleObject = Record<string, RuleEntry>
39
+ type GroupEslintVersions = Map<string, string[]>
35
40
 
36
41
  type RootOptions = {
37
42
  update?: boolean
38
43
  }
39
44
 
45
+ type RunTimer = {
46
+ label: string
47
+ startedAtMs: number
48
+ pausedMs: number
49
+ pauseStartedAtMs: number | undefined
50
+ }
51
+
52
+ let activeRunTimer: RunTimer | undefined
53
+ let cachedCliVersion: string | undefined
54
+
40
55
  export async function runCli(command: string | undefined, cwd: string, flags: string[] = []): Promise<number> {
41
56
  const argv = command ? [command, ...flags] : [...flags]
42
57
  return runArgv(argv, cwd)
43
58
  }
44
59
 
45
60
  async function runArgv(argv: string[], cwd: string): Promise<number> {
46
- const hasCommandToken = argv.some((token) => !token.startsWith('-'))
47
- if (!hasCommandToken) {
48
- return runDefaultInvocation(argv, cwd)
49
- }
61
+ const invocationLabel = resolveInvocationLabel(argv)
62
+ beginRunTimer(invocationLabel)
63
+ let exitCode = 1
64
+
65
+ try {
66
+ const hasCommandToken = argv.some((token) => !token.startsWith('-'))
67
+ if (!hasCommandToken) {
68
+ exitCode = await runDefaultInvocation(argv, cwd)
69
+ return exitCode
70
+ }
50
71
 
51
- let actionCode: number | undefined
72
+ let actionCode: number | undefined
52
73
 
53
- const program = createProgram(cwd, (code) => {
54
- actionCode = code
55
- })
74
+ const program = createProgram(cwd, (code) => {
75
+ actionCode = code
76
+ })
56
77
 
57
- try {
58
- await program.parseAsync(argv, { from: 'user' })
59
- } catch (error: unknown) {
60
- if (error instanceof CommanderError) {
61
- if (error.code === 'commander.helpDisplayed') {
62
- return 0
78
+ try {
79
+ await program.parseAsync(argv, { from: 'user' })
80
+ } catch (error: unknown) {
81
+ if (error instanceof CommanderError) {
82
+ if (error.code === 'commander.helpDisplayed') {
83
+ exitCode = 0
84
+ return exitCode
85
+ }
86
+ exitCode = error.exitCode
87
+ return exitCode
63
88
  }
64
- return error.exitCode
89
+
90
+ const message = error instanceof Error ? error.message : String(error)
91
+ process.stderr.write(`${message}\n`)
92
+ return 1
65
93
  }
66
94
 
67
- const message = error instanceof Error ? error.message : String(error)
68
- process.stderr.write(`${message}\n`)
69
- return 1
95
+ exitCode = actionCode ?? 0
96
+ return exitCode
97
+ } finally {
98
+ endRunTimer(exitCode)
70
99
  }
71
-
72
- return actionCode ?? 0
73
100
  }
74
101
 
75
102
  async function runDefaultInvocation(argv: string[], cwd: string): Promise<number> {
@@ -238,6 +265,15 @@ function parseInitPreset(value: string): InitPreset {
238
265
 
239
266
  async function executeCheck(cwd: string, format: CheckFormat, defaultInvocation = false): Promise<number> {
240
267
  const foundConfig = await findConfigPath(cwd)
268
+ const storedSnapshots = await loadStoredSnapshots(cwd)
269
+
270
+ if (format !== 'status') {
271
+ writeRunContextHeader(cwd, defaultInvocation ? 'check' : `check:${format}`, foundConfig?.path, storedSnapshots)
272
+ if (shouldShowRunLogs()) {
273
+ writeSubtleInfo('Analyzing current ESLint configuration...\n')
274
+ }
275
+ }
276
+
241
277
  if (!foundConfig) {
242
278
  writeSubtleInfo(
243
279
  'Tip: no explicit config found. Using safe built-in defaults. Run `eslint-config-snapshot init` to customize when needed.\n'
@@ -257,8 +293,6 @@ async function executeCheck(cwd: string, format: CheckFormat, defaultInvocation
257
293
 
258
294
  throw error
259
295
  }
260
- const storedSnapshots = await loadStoredSnapshots(cwd)
261
-
262
296
  if (storedSnapshots.size === 0) {
263
297
  const summary = summarizeSnapshots(currentSnapshots)
264
298
  process.stdout.write(
@@ -286,6 +320,7 @@ async function executeCheck(cwd: string, format: CheckFormat, defaultInvocation
286
320
  }
287
321
 
288
322
  const changes = compareSnapshotMaps(storedSnapshots, currentSnapshots)
323
+ const eslintVersionsByGroup = shouldShowRunLogs() ? await resolveGroupEslintVersions(cwd) : new Map<string, string[]>()
289
324
 
290
325
  if (format === 'status') {
291
326
  if (changes.length === 0) {
@@ -301,6 +336,7 @@ async function executeCheck(cwd: string, format: CheckFormat, defaultInvocation
301
336
  if (format === 'diff') {
302
337
  if (changes.length === 0) {
303
338
  process.stdout.write('Great news: no snapshot changes detected.\n')
339
+ writeEslintVersionSummary(eslintVersionsByGroup)
304
340
  return 0
305
341
  }
306
342
 
@@ -312,11 +348,17 @@ async function executeCheck(cwd: string, format: CheckFormat, defaultInvocation
312
348
  return 1
313
349
  }
314
350
 
315
- return printWhatChanged(changes, currentSnapshots)
351
+ return printWhatChanged(changes, currentSnapshots, eslintVersionsByGroup)
316
352
  }
317
353
 
318
354
  async function executeUpdate(cwd: string, printSummary: boolean): Promise<number> {
319
355
  const foundConfig = await findConfigPath(cwd)
356
+ const storedSnapshots = await loadStoredSnapshots(cwd)
357
+ writeRunContextHeader(cwd, 'update', foundConfig?.path, storedSnapshots)
358
+ if (shouldShowRunLogs()) {
359
+ writeSubtleInfo('Analyzing current ESLint configuration...\n')
360
+ }
361
+
320
362
  if (!foundConfig) {
321
363
  writeSubtleInfo(
322
364
  'Tip: no explicit config found. Using safe built-in defaults. Run `eslint-config-snapshot init` to customize when needed.\n'
@@ -340,13 +382,25 @@ async function executeUpdate(cwd: string, printSummary: boolean): Promise<number
340
382
 
341
383
  if (printSummary) {
342
384
  const summary = summarizeSnapshots(currentSnapshots)
343
- process.stdout.write(`Baseline updated: ${summary.groups} groups, ${summary.rules} rules.\n`)
385
+ const color = createColorizer()
386
+ const eslintVersionsByGroup = shouldShowRunLogs() ? await resolveGroupEslintVersions(cwd) : new Map<string, string[]>()
387
+ writeSectionTitle('Summary', color)
388
+ process.stdout.write(
389
+ `Baseline updated: ${summary.groups} groups, ${summary.rules} rules.\nSeverity mix: ${summary.error} errors, ${summary.warn} warnings, ${summary.off} off.\n`
390
+ )
391
+ writeEslintVersionSummary(eslintVersionsByGroup)
344
392
  }
345
393
 
346
394
  return 0
347
395
  }
348
396
 
349
397
  async function executePrint(cwd: string, format: PrintFormat): Promise<void> {
398
+ const foundConfig = await findConfigPath(cwd)
399
+ const storedSnapshots = await loadStoredSnapshots(cwd)
400
+ writeRunContextHeader(cwd, `print:${format}`, foundConfig?.path, storedSnapshots)
401
+ if (shouldShowRunLogs()) {
402
+ writeSubtleInfo('Analyzing current ESLint configuration...\n')
403
+ }
350
404
  const currentSnapshots = await computeCurrentSnapshots(cwd)
351
405
 
352
406
  if (format === 'short') {
@@ -363,6 +417,11 @@ async function executePrint(cwd: string, format: PrintFormat): Promise<void> {
363
417
 
364
418
  async function executeConfig(cwd: string, format: PrintFormat): Promise<void> {
365
419
  const foundConfig = await findConfigPath(cwd)
420
+ const storedSnapshots = await loadStoredSnapshots(cwd)
421
+ writeRunContextHeader(cwd, `config:${format}`, foundConfig?.path, storedSnapshots)
422
+ if (shouldShowRunLogs()) {
423
+ writeSubtleInfo('Resolving effective runtime configuration...\n')
424
+ }
366
425
  const config = await loadConfig(cwd)
367
426
  const resolved = await resolveWorkspaceAssignments(cwd, config)
368
427
  const payload = {
@@ -400,23 +459,23 @@ async function computeCurrentSnapshots(cwd: string): Promise<Map<string, BuiltSn
400
459
  let extractedCount = 0
401
460
  let lastExtractionError: string | undefined
402
461
 
403
- for (const sampledRel of sampled) {
404
- const sampledAbs = path.resolve(workspaceAbs, sampledRel)
405
- try {
406
- extractedForGroup.push(extractRulesFromPrintConfig(workspaceAbs, sampledAbs))
462
+ const sampledAbs = sampled.map((sampledRel) => path.resolve(workspaceAbs, sampledRel))
463
+ const results = await extractRulesForWorkspaceSamples(workspaceAbs, sampledAbs)
464
+
465
+ for (const result of results) {
466
+ if (result.rules) {
467
+ extractedForGroup.push(result.rules)
407
468
  extractedCount += 1
408
- } catch (error: unknown) {
409
- const message = error instanceof Error ? error.message : String(error)
410
- if (
411
- message.startsWith('Invalid JSON from eslint --print-config') ||
412
- message.startsWith('Empty ESLint print-config output')
413
- ) {
414
- lastExtractionError = message
415
- continue
416
- }
417
-
418
- throw error
469
+ continue
419
470
  }
471
+
472
+ const message = result.error instanceof Error ? result.error.message : String(result.error)
473
+ if (isRecoverableExtractionError(message)) {
474
+ lastExtractionError = message
475
+ continue
476
+ }
477
+
478
+ throw result.error ?? new Error(message)
420
479
  }
421
480
 
422
481
  if (extractedCount === 0) {
@@ -434,6 +493,15 @@ async function computeCurrentSnapshots(cwd: string): Promise<Map<string, BuiltSn
434
493
  return snapshots
435
494
  }
436
495
 
496
+ function isRecoverableExtractionError(message: string): boolean {
497
+ return (
498
+ message.startsWith('Invalid JSON from eslint --print-config') ||
499
+ message.startsWith('Empty ESLint print-config output') ||
500
+ message.includes('File ignored because of a matching ignore pattern') ||
501
+ message.includes('File ignored by default')
502
+ )
503
+ }
504
+
437
505
  async function resolveWorkspaceAssignments(cwd: string, config: Awaited<ReturnType<typeof loadConfig>>) {
438
506
  const discovery = await discoverWorkspaces({ cwd, workspaceInput: config.workspaceInput })
439
507
 
@@ -594,8 +662,8 @@ async function runInit(
594
662
 
595
663
  async function askInitPreferences(): Promise<{ target: InitTarget; preset: InitPreset }> {
596
664
  const { select } = await import('@inquirer/prompts')
597
- const target = await askInitTarget(select)
598
- const preset = await askInitPreset(select)
665
+ const target = await runPromptWithPausedTimer(() => askInitTarget(select))
666
+ const preset = await runPromptWithPausedTimer(() => askInitPreset(select))
599
667
  return { target, preset }
600
668
  }
601
669
 
@@ -625,8 +693,10 @@ async function askInitPreset(
625
693
  }
626
694
 
627
695
  function askQuestion(rl: ReturnType<typeof createInterface>, prompt: string): Promise<string> {
696
+ pauseRunTimer()
628
697
  return new Promise((resolve) => {
629
698
  rl.question(prompt, (answer) => {
699
+ resumeRunTimer()
630
700
  resolve(answer)
631
701
  })
632
702
  })
@@ -802,11 +872,13 @@ async function askRecommendedGroupAssignments(workspaces: string[]): Promise<Map
802
872
  'Recommended setup: default group "*" is a dynamic catch-all for every discovered workspace.\n'
803
873
  )
804
874
  process.stdout.write('Select only workspaces that should move to explicit static groups.\n')
805
- const overrides = await checkbox<string>({
806
- message: 'Choose exception workspaces (leave empty to keep all in default "*"):',
807
- choices: workspaces.map((workspace) => ({ name: workspace, value: workspace })),
808
- pageSize: Math.min(12, Math.max(4, workspaces.length))
809
- })
875
+ const overrides = await runPromptWithPausedTimer(() =>
876
+ checkbox<string>({
877
+ message: 'Choose exception workspaces (leave empty to keep all in default "*"):',
878
+ choices: workspaces.map((workspace) => ({ name: workspace, value: workspace })),
879
+ pageSize: Math.min(12, Math.max(4, workspaces.length))
880
+ })
881
+ )
810
882
 
811
883
  const assignments = new Map<string, number>()
812
884
  let nextGroup = 1
@@ -816,13 +888,15 @@ async function askRecommendedGroupAssignments(workspaces: string[]): Promise<Map
816
888
  nextGroup += 1
817
889
  }
818
890
 
819
- const selected = await select<number | 'new'>({
820
- message: `Select group for ${workspace}`,
821
- choices: [
822
- ...usedGroups.map((group) => ({ name: `group-${group}`, value: group })),
823
- { name: `create new group (group-${nextGroup})`, value: 'new' }
824
- ]
825
- })
891
+ const selected = await runPromptWithPausedTimer(() =>
892
+ select<number | 'new'>({
893
+ message: `Select group for ${workspace}`,
894
+ choices: [
895
+ ...usedGroups.map((group) => ({ name: `group-${group}`, value: group })),
896
+ { name: `create new group (group-${nextGroup})`, value: 'new' }
897
+ ]
898
+ })
899
+ )
826
900
  const groupNumber = selected === 'new' ? nextGroup : selected
827
901
  assignments.set(workspace, groupNumber)
828
902
  }
@@ -873,27 +947,34 @@ if (isDirectCliExecution()) {
873
947
  void main()
874
948
  }
875
949
 
876
- function printWhatChanged(changes: Array<{ groupId: string; diff: SnapshotDiff }>, currentSnapshots: Map<string, BuiltSnapshot>): number {
950
+ function printWhatChanged(
951
+ changes: Array<{ groupId: string; diff: SnapshotDiff }>,
952
+ currentSnapshots: Map<string, BuiltSnapshot>,
953
+ eslintVersionsByGroup: GroupEslintVersions
954
+ ): number {
877
955
  const color = createColorizer()
878
956
  const currentSummary = summarizeSnapshots(currentSnapshots)
879
957
  const changeSummary = summarizeChanges(changes)
880
958
 
881
959
  if (changes.length === 0) {
882
960
  process.stdout.write(color.green('Great news: no snapshot drift detected.\n'))
961
+ writeSectionTitle('Summary', color)
883
962
  process.stdout.write(
884
- `Baseline status: ${currentSummary.groups} groups, ${currentSummary.rules} rules (severity mix: ${currentSummary.error} errors, ${currentSummary.warn} warnings, ${currentSummary.off} off).\n`
963
+ `- baseline: ${currentSummary.groups} groups, ${currentSummary.rules} rules\n- severity mix: ${currentSummary.error} errors, ${currentSummary.warn} warnings, ${currentSummary.off} off\n`
885
964
  )
965
+ writeEslintVersionSummary(eslintVersionsByGroup)
886
966
  return 0
887
967
  }
888
968
 
889
969
  process.stdout.write(color.red('Heads up: snapshot drift detected.\n'))
970
+ writeSectionTitle('Summary', color)
890
971
  process.stdout.write(
891
- `Changed groups: ${changes.length} | introduced: ${changeSummary.introduced} | removed: ${changeSummary.removed} | severity: ${changeSummary.severity} | options: ${changeSummary.options} | workspace membership: ${changeSummary.workspace}\n`
892
- )
893
- process.stdout.write(
894
- `Current rules: ${currentSummary.rules} (severity mix: ${currentSummary.error} errors, ${currentSummary.warn} warnings, ${currentSummary.off} off)\n\n`
972
+ `- 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- current baseline: ${currentSummary.groups} groups, ${currentSummary.rules} rules\n- current severity mix: ${currentSummary.error} errors, ${currentSummary.warn} warnings, ${currentSummary.off} off\n\n`
895
973
  )
974
+ writeEslintVersionSummary(eslintVersionsByGroup)
975
+ process.stdout.write('\n')
896
976
 
977
+ writeSectionTitle('Changes', color)
897
978
  for (const change of changes) {
898
979
  process.stdout.write(color.bold(`group ${change.groupId}\n`))
899
980
  const lines = formatDiff(change.groupId, change.diff).split('\n').slice(1)
@@ -908,6 +989,10 @@ function printWhatChanged(changes: Array<{ groupId: string; diff: SnapshotDiff }
908
989
  return 1
909
990
  }
910
991
 
992
+ function writeSectionTitle(title: string, color: ReturnType<typeof createColorizer>): void {
993
+ process.stdout.write(`${color.bold(title)}\n`)
994
+ }
995
+
911
996
  function summarizeChanges(changes: Array<{ groupId: string; diff: SnapshotDiff }>) {
912
997
  let introduced = 0
913
998
  let removed = 0
@@ -925,22 +1010,7 @@ function summarizeChanges(changes: Array<{ groupId: string; diff: SnapshotDiff }
925
1010
  }
926
1011
 
927
1012
  function summarizeSnapshots(snapshots: Map<string, BuiltSnapshot>) {
928
- let rules = 0
929
- let error = 0
930
- let warn = 0
931
- let off = 0
932
- for (const snapshot of snapshots.values()) {
933
- for (const entry of Object.values(snapshot.rules)) {
934
- rules += 1
935
- if (entry[0] === 'error') {
936
- error += 1
937
- } else if (entry[0] === 'warn') {
938
- warn += 1
939
- } else {
940
- off += 1
941
- }
942
- }
943
- }
1013
+ const { rules, error, warn, off } = countRuleSeverities([...snapshots.values()].map((snapshot) => snapshot.rules))
944
1014
  return { groups: snapshots.size, rules, error, warn, off }
945
1015
  }
946
1016
 
@@ -974,11 +1044,234 @@ function writeSubtleInfo(text: string): void {
974
1044
  process.stdout.write(color.dim(text))
975
1045
  }
976
1046
 
1047
+ function resolveInvocationLabel(argv: string[]): string {
1048
+ const commandToken = argv.find((entry) => !entry.startsWith('-'))
1049
+ if (commandToken) {
1050
+ return commandToken
1051
+ }
1052
+ if (argv.includes('-u') || argv.includes('--update')) {
1053
+ return 'update'
1054
+ }
1055
+ if (argv.includes('-h') || argv.includes('--help')) {
1056
+ return 'help'
1057
+ }
1058
+ return 'check'
1059
+ }
1060
+
1061
+ function shouldShowRunLogs(): boolean {
1062
+ if (process.env.ESLINT_CONFIG_SNAPSHOT_NO_PROGRESS === '1') {
1063
+ return false
1064
+ }
1065
+ return process.stdout.isTTY === true
1066
+ }
1067
+
1068
+ function beginRunTimer(label: string): void {
1069
+ if (!shouldShowRunLogs()) {
1070
+ activeRunTimer = undefined
1071
+ return
1072
+ }
1073
+
1074
+ activeRunTimer = {
1075
+ label,
1076
+ startedAtMs: Date.now(),
1077
+ pausedMs: 0,
1078
+ pauseStartedAtMs: undefined
1079
+ }
1080
+ }
1081
+
1082
+ function endRunTimer(exitCode: number): void {
1083
+ if (!activeRunTimer || !shouldShowRunLogs()) {
1084
+ return
1085
+ }
1086
+
1087
+ if (activeRunTimer.pauseStartedAtMs !== undefined) {
1088
+ activeRunTimer.pausedMs += Date.now() - activeRunTimer.pauseStartedAtMs
1089
+ activeRunTimer.pauseStartedAtMs = undefined
1090
+ }
1091
+
1092
+ const elapsedMs = Math.max(0, Date.now() - activeRunTimer.startedAtMs - activeRunTimer.pausedMs)
1093
+ const color = createColorizer()
1094
+ const status = exitCode === 0 ? color.green('done') : color.red('failed')
1095
+ const seconds = (elapsedMs / 1000).toFixed(2)
1096
+ writeSubtleInfo(`Run ${status} in ${seconds}s\n`)
1097
+ activeRunTimer = undefined
1098
+ }
1099
+
1100
+ function pauseRunTimer(): void {
1101
+ if (!activeRunTimer || activeRunTimer.pauseStartedAtMs !== undefined) {
1102
+ return
1103
+ }
1104
+ activeRunTimer.pauseStartedAtMs = Date.now()
1105
+ }
1106
+
1107
+ function resumeRunTimer(): void {
1108
+ if (!activeRunTimer || activeRunTimer.pauseStartedAtMs === undefined) {
1109
+ return
1110
+ }
1111
+
1112
+ activeRunTimer.pausedMs += Date.now() - activeRunTimer.pauseStartedAtMs
1113
+ activeRunTimer.pauseStartedAtMs = undefined
1114
+ }
1115
+
1116
+ async function runPromptWithPausedTimer<T>(prompt: () => Promise<T>): Promise<T> {
1117
+ pauseRunTimer()
1118
+ try {
1119
+ return await prompt()
1120
+ } finally {
1121
+ resumeRunTimer()
1122
+ }
1123
+ }
1124
+
1125
+ function readCliVersion(): string {
1126
+ if (cachedCliVersion !== undefined) {
1127
+ return cachedCliVersion
1128
+ }
1129
+
1130
+ const scriptPath = process.argv[1]
1131
+ if (!scriptPath) {
1132
+ cachedCliVersion = 'unknown'
1133
+ return cachedCliVersion
1134
+ }
1135
+
1136
+ let current = path.resolve(path.dirname(scriptPath))
1137
+ while (true) {
1138
+ const packageJsonPath = path.join(current, 'package.json')
1139
+ if (existsSync(packageJsonPath)) {
1140
+ try {
1141
+ const raw = readFileSync(packageJsonPath, 'utf8')
1142
+ const parsed = JSON.parse(raw) as { version?: string }
1143
+ cachedCliVersion = parsed.version ?? 'unknown'
1144
+ return cachedCliVersion
1145
+ } catch {
1146
+ break
1147
+ }
1148
+ }
1149
+
1150
+ const parent = path.dirname(current)
1151
+ if (parent === current) {
1152
+ break
1153
+ }
1154
+ current = parent
1155
+ }
1156
+
1157
+ cachedCliVersion = 'unknown'
1158
+ return cachedCliVersion
1159
+ }
1160
+
1161
+ function writeRunContextHeader(
1162
+ cwd: string,
1163
+ commandLabel: string,
1164
+ configPath: string | undefined,
1165
+ storedSnapshots: Map<string, StoredSnapshot>
1166
+ ): void {
1167
+ if (!shouldShowRunLogs()) {
1168
+ return
1169
+ }
1170
+
1171
+ const color = createColorizer()
1172
+ process.stdout.write(color.bold(`eslint-config-snapshot v${readCliVersion()}\n`))
1173
+ process.stdout.write(`Command: ${commandLabel}\n`)
1174
+ process.stdout.write(`Repository: ${cwd}\n`)
1175
+ process.stdout.write(`Config: ${formatConfigSource(cwd, configPath)}\n`)
1176
+ process.stdout.write(`Baseline: ${formatStoredSnapshotSummary(storedSnapshots)}\n\n`)
1177
+ }
1178
+
1179
+ function formatConfigSource(cwd: string, configPath: string | undefined): string {
1180
+ if (!configPath) {
1181
+ return 'built-in defaults'
1182
+ }
1183
+
1184
+ const rel = normalizePath(path.relative(cwd, configPath))
1185
+ if (path.basename(configPath) === 'package.json') {
1186
+ return `${rel} (eslint-config-snapshot field)`
1187
+ }
1188
+
1189
+ return rel
1190
+ }
1191
+
1192
+ function formatStoredSnapshotSummary(storedSnapshots: Map<string, StoredSnapshot>): string {
1193
+ if (storedSnapshots.size === 0) {
1194
+ return 'none'
1195
+ }
1196
+
1197
+ const summary = summarizeStoredSnapshots(storedSnapshots)
1198
+ return `${summary.groups} groups, ${summary.rules} rules (severity mix: ${summary.error} errors, ${summary.warn} warnings, ${summary.off} off)`
1199
+ }
1200
+
1201
+ async function resolveGroupEslintVersions(cwd: string): Promise<GroupEslintVersions> {
1202
+ const config = await loadConfig(cwd)
1203
+ const { discovery, assignments } = await resolveWorkspaceAssignments(cwd, config)
1204
+ const result = new Map<string, string[]>()
1205
+
1206
+ for (const group of assignments) {
1207
+ const versions = new Set<string>()
1208
+ for (const workspaceRel of group.workspaces) {
1209
+ const workspaceAbs = path.resolve(discovery.rootAbs, workspaceRel)
1210
+ versions.add(resolveEslintVersionForWorkspace(workspaceAbs))
1211
+ }
1212
+ result.set(group.name, [...versions].sort((a, b) => a.localeCompare(b)))
1213
+ }
1214
+
1215
+ return result
1216
+ }
1217
+
1218
+ function writeEslintVersionSummary(eslintVersionsByGroup: GroupEslintVersions): void {
1219
+ if (!shouldShowRunLogs() || eslintVersionsByGroup.size === 0) {
1220
+ return
1221
+ }
1222
+
1223
+ const allVersions = new Set<string>()
1224
+ for (const versions of eslintVersionsByGroup.values()) {
1225
+ for (const version of versions) {
1226
+ allVersions.add(version)
1227
+ }
1228
+ }
1229
+
1230
+ const sortedAllVersions = [...allVersions].sort((a, b) => a.localeCompare(b))
1231
+ if (sortedAllVersions.length === 1) {
1232
+ process.stdout.write(`- eslint runtime: ${sortedAllVersions[0]} (all groups)\n`)
1233
+ return
1234
+ }
1235
+
1236
+ process.stdout.write('- eslint runtime by group:\n')
1237
+ const sortedEntries = [...eslintVersionsByGroup.entries()].sort((a, b) => a[0].localeCompare(b[0]))
1238
+ for (const [groupName, versions] of sortedEntries) {
1239
+ process.stdout.write(` - ${groupName}: ${versions.join(', ')}\n`)
1240
+ }
1241
+ }
1242
+
1243
+ function summarizeStoredSnapshots(snapshots: Map<string, StoredSnapshot>) {
1244
+ const { rules, error, warn, off } = countRuleSeverities([...snapshots.values()].map((snapshot) => snapshot.rules))
1245
+ return { groups: snapshots.size, rules, error, warn, off }
1246
+ }
1247
+
1248
+ function countRuleSeverities(ruleObjects: RuleObject[]) {
1249
+ let rules = 0
1250
+ let error = 0
1251
+ let warn = 0
1252
+ let off = 0
1253
+
1254
+ for (const rulesObject of ruleObjects) {
1255
+ for (const entry of Object.values(rulesObject)) {
1256
+ rules += 1
1257
+ if (entry[0] === 'error') {
1258
+ error += 1
1259
+ } else if (entry[0] === 'warn') {
1260
+ warn += 1
1261
+ } else {
1262
+ off += 1
1263
+ }
1264
+ }
1265
+ }
1266
+
1267
+ return { rules, error, warn, off }
1268
+ }
1269
+
977
1270
  function formatShortPrint(
978
1271
  snapshots: Array<{
979
1272
  groupId: string
980
1273
  workspaces: string[]
981
- rules: Record<string, [severity: 'off' | 'warn' | 'error'] | [severity: 'off' | 'warn' | 'error', options: unknown]>
1274
+ rules: RuleObject
982
1275
  }>
983
1276
  ): string {
984
1277
  const lines: string[] = []
@@ -8,8 +8,11 @@ import { buildRecommendedConfigFromAssignments, runCli } from '../src/index.js'
8
8
  const fixtureTemplateRoot = path.resolve('test/fixtures/repo')
9
9
  let tmpDir = ''
10
10
  let fixtureRoot = ''
11
+ let previousNoProgress = ''
11
12
 
12
13
  beforeEach(async () => {
14
+ previousNoProgress = process.env.ESLINT_CONFIG_SNAPSHOT_NO_PROGRESS ?? ''
15
+ process.env.ESLINT_CONFIG_SNAPSHOT_NO_PROGRESS = '1'
13
16
  tmpDir = await mkdtemp(path.join(os.tmpdir(), 'snapshot-cli-integration-'))
14
17
  fixtureRoot = path.join(tmpDir, 'repo')
15
18
  await cp(fixtureTemplateRoot, fixtureRoot, { recursive: true })
@@ -37,6 +40,11 @@ beforeEach(async () => {
37
40
  })
38
41
 
39
42
  afterEach(async () => {
43
+ if (previousNoProgress === '') {
44
+ delete process.env.ESLINT_CONFIG_SNAPSHOT_NO_PROGRESS
45
+ } else {
46
+ process.env.ESLINT_CONFIG_SNAPSHOT_NO_PROGRESS = previousNoProgress
47
+ }
40
48
  if (tmpDir) {
41
49
  await rm(tmpDir, { recursive: true, force: true })
42
50
  tmpDir = ''