@eslint-config-snapshot/cli 0.3.0 → 0.4.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 CHANGED
@@ -1,5 +1,32 @@
1
1
  # @eslint-config-snapshot/cli
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Minor release with improved CLI output consistency and faster workspace extraction using ESLint API fallback strategy.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies
12
+ - @eslint-config-snapshot/api@0.4.0
13
+
14
+ ## 0.3.2
15
+
16
+ ### Patch Changes
17
+
18
+ - Fix deterministic aggregation when same-severity ESLint rule options differ across sampled files, preventing update crashes.
19
+ - Updated dependencies
20
+ - @eslint-config-snapshot/api@0.3.2
21
+
22
+ ## 0.3.1
23
+
24
+ ### Patch Changes
25
+
26
+ - Release patch bump after init UX clarity improvements for default catch-all group messaging.
27
+ - Updated dependencies
28
+ - @eslint-config-snapshot/api@0.3.1
29
+
3
30
  ## 0.3.0
4
31
 
5
32
  ### Minor Changes
package/dist/index.cjs CHANGED
@@ -124,7 +124,7 @@ function createProgram(cwd, onActionExit) {
124
124
  Examples:
125
125
  $ eslint-config-snapshot init
126
126
  Runs interactive select prompts for target/preset.
127
- Recommended preset uses checkbox selection for non-default workspaces and group selection.
127
+ Recommended preset keeps a dynamic catch-all default group ("*") and asks only for static exception groups.
128
128
 
129
129
  $ eslint-config-snapshot init --yes --target package-json --preset recommended --show-effective
130
130
  Non-interactive recommended setup in package.json, with effective preview.
@@ -266,8 +266,13 @@ async function executeUpdate(cwd, printSummary) {
266
266
  await writeSnapshots(cwd, currentSnapshots);
267
267
  if (printSummary) {
268
268
  const summary = summarizeSnapshots(currentSnapshots);
269
- process.stdout.write(`Baseline updated: ${summary.groups} groups, ${summary.rules} rules.
270
- `);
269
+ const color = createColorizer();
270
+ writeSectionTitle("Summary", color);
271
+ process.stdout.write(
272
+ `Baseline updated: ${summary.groups} groups, ${summary.rules} rules.
273
+ Severity mix: ${summary.error} errors, ${summary.warn} warnings, ${summary.off} off.
274
+ `
275
+ );
271
276
  }
272
277
  return 0;
273
278
  }
@@ -317,19 +322,20 @@ async function computeCurrentSnapshots(cwd) {
317
322
  const sampled = await (0, import_api.sampleWorkspaceFiles)(workspaceAbs, config.sampling);
318
323
  let extractedCount = 0;
319
324
  let lastExtractionError;
320
- for (const sampledRel of sampled) {
321
- const sampledAbs = import_node_path.default.resolve(workspaceAbs, sampledRel);
322
- try {
323
- extractedForGroup.push((0, import_api.extractRulesFromPrintConfig)(workspaceAbs, sampledAbs));
325
+ const sampledAbs = sampled.map((sampledRel) => import_node_path.default.resolve(workspaceAbs, sampledRel));
326
+ const results = await (0, import_api.extractRulesForWorkspaceSamples)(workspaceAbs, sampledAbs);
327
+ for (const result of results) {
328
+ if (result.rules) {
329
+ extractedForGroup.push(result.rules);
324
330
  extractedCount += 1;
325
- } catch (error) {
326
- const message = error instanceof Error ? error.message : String(error);
327
- if (message.startsWith("Invalid JSON from eslint --print-config") || message.startsWith("Empty ESLint print-config output")) {
328
- lastExtractionError = message;
329
- continue;
330
- }
331
- throw error;
331
+ continue;
332
332
  }
333
+ const message = result.error instanceof Error ? result.error.message : String(result.error);
334
+ if (isRecoverableExtractionError(message)) {
335
+ lastExtractionError = message;
336
+ continue;
337
+ }
338
+ throw result.error ?? new Error(message);
333
339
  }
334
340
  if (extractedCount === 0) {
335
341
  const context = lastExtractionError ? ` Last error: ${lastExtractionError}` : "";
@@ -343,6 +349,9 @@ async function computeCurrentSnapshots(cwd) {
343
349
  }
344
350
  return snapshots;
345
351
  }
352
+ function isRecoverableExtractionError(message) {
353
+ return message.startsWith("Invalid JSON from eslint --print-config") || message.startsWith("Empty ESLint print-config output") || message.includes("File ignored because of a matching ignore pattern") || message.includes("File ignored by default");
354
+ }
346
355
  async function resolveWorkspaceAssignments(cwd, config) {
347
356
  const discovery = await (0, import_api.discoverWorkspaces)({ cwd, workspaceInput: config.workspaceInput });
348
357
  const assignments = config.grouping.mode === "standalone" ? discovery.workspacesRel.map((workspace) => ({ name: workspace, workspaces: [workspace] })) : (0, import_api.assignGroupsByMatch)(discovery.workspacesRel, config.grouping.groups ?? [{ name: "default", match: ["**/*"] }]);
@@ -484,7 +493,7 @@ async function askInitPreset(selectPrompt) {
484
493
  return selectPrompt({
485
494
  message: "Select preset",
486
495
  choices: [
487
- { name: 'recommended (default group "*" + static overrides)', value: "recommended" },
496
+ { name: 'recommended (dynamic catch-all "*" + optional static exceptions)', value: "recommended" },
488
497
  { name: "minimal", value: "minimal" },
489
498
  { name: "full", value: "full" }
490
499
  ]
@@ -634,9 +643,12 @@ function trimTrailingSlashes(value) {
634
643
  }
635
644
  async function askRecommendedGroupAssignments(workspaces) {
636
645
  const { checkbox, select } = await import("@inquirer/prompts");
637
- process.stdout.write('Recommended setup: select only workspaces that should leave default group "*".\n');
646
+ process.stdout.write(
647
+ 'Recommended setup: default group "*" is a dynamic catch-all for every discovered workspace.\n'
648
+ );
649
+ process.stdout.write("Select only workspaces that should move to explicit static groups.\n");
638
650
  const overrides = await checkbox({
639
- message: "Workspaces outside default group:",
651
+ message: 'Choose exception workspaces (leave empty to keep all in default "*"):',
640
652
  choices: workspaces.map((workspace) => ({ name: workspace, value: workspace })),
641
653
  pageSize: Math.min(12, Math.max(4, workspaces.length))
642
654
  });
@@ -702,22 +714,29 @@ function printWhatChanged(changes, currentSnapshots) {
702
714
  const changeSummary = summarizeChanges(changes);
703
715
  if (changes.length === 0) {
704
716
  process.stdout.write(color.green("Great news: no snapshot drift detected.\n"));
717
+ writeSectionTitle("Summary", color);
705
718
  process.stdout.write(
706
- `Baseline status: ${currentSummary.groups} groups, ${currentSummary.rules} rules (severity mix: ${currentSummary.error} errors, ${currentSummary.warn} warnings, ${currentSummary.off} off).
719
+ `- baseline: ${currentSummary.groups} groups, ${currentSummary.rules} rules
720
+ - severity mix: ${currentSummary.error} errors, ${currentSummary.warn} warnings, ${currentSummary.off} off
707
721
  `
708
722
  );
709
723
  return 0;
710
724
  }
711
725
  process.stdout.write(color.red("Heads up: snapshot drift detected.\n"));
726
+ writeSectionTitle("Summary", color);
712
727
  process.stdout.write(
713
- `Changed groups: ${changes.length} | introduced: ${changeSummary.introduced} | removed: ${changeSummary.removed} | severity: ${changeSummary.severity} | options: ${changeSummary.options} | workspace membership: ${changeSummary.workspace}
714
- `
715
- );
716
- process.stdout.write(
717
- `Current rules: ${currentSummary.rules} (severity mix: ${currentSummary.error} errors, ${currentSummary.warn} warnings, ${currentSummary.off} off)
728
+ `- changed groups: ${changes.length}
729
+ - introduced rules: ${changeSummary.introduced}
730
+ - removed rules: ${changeSummary.removed}
731
+ - severity changes: ${changeSummary.severity}
732
+ - options changes: ${changeSummary.options}
733
+ - workspace membership changes: ${changeSummary.workspace}
734
+ - current baseline: ${currentSummary.groups} groups, ${currentSummary.rules} rules
735
+ - current severity mix: ${currentSummary.error} errors, ${currentSummary.warn} warnings, ${currentSummary.off} off
718
736
 
719
737
  `
720
738
  );
739
+ writeSectionTitle("Changes", color);
721
740
  for (const change of changes) {
722
741
  process.stdout.write(color.bold(`group ${change.groupId}
723
742
  `));
@@ -732,6 +751,10 @@ function printWhatChanged(changes, currentSnapshots) {
732
751
  writeSubtleInfo(UPDATE_HINT);
733
752
  return 1;
734
753
  }
754
+ function writeSectionTitle(title, color) {
755
+ process.stdout.write(`${color.bold(title)}
756
+ `);
757
+ }
735
758
  function summarizeChanges(changes) {
736
759
  let introduced = 0;
737
760
  let removed = 0;
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  buildSnapshot,
8
8
  diffSnapshots,
9
9
  discoverWorkspaces,
10
- extractRulesFromPrintConfig,
10
+ extractRulesForWorkspaceSamples,
11
11
  findConfigPath,
12
12
  getConfigScaffold,
13
13
  hasDiff,
@@ -104,7 +104,7 @@ function createProgram(cwd, onActionExit) {
104
104
  Examples:
105
105
  $ eslint-config-snapshot init
106
106
  Runs interactive select prompts for target/preset.
107
- Recommended preset uses checkbox selection for non-default workspaces and group selection.
107
+ Recommended preset keeps a dynamic catch-all default group ("*") and asks only for static exception groups.
108
108
 
109
109
  $ eslint-config-snapshot init --yes --target package-json --preset recommended --show-effective
110
110
  Non-interactive recommended setup in package.json, with effective preview.
@@ -246,8 +246,13 @@ async function executeUpdate(cwd, printSummary) {
246
246
  await writeSnapshots(cwd, currentSnapshots);
247
247
  if (printSummary) {
248
248
  const summary = summarizeSnapshots(currentSnapshots);
249
- process.stdout.write(`Baseline updated: ${summary.groups} groups, ${summary.rules} rules.
250
- `);
249
+ const color = createColorizer();
250
+ writeSectionTitle("Summary", color);
251
+ process.stdout.write(
252
+ `Baseline updated: ${summary.groups} groups, ${summary.rules} rules.
253
+ Severity mix: ${summary.error} errors, ${summary.warn} warnings, ${summary.off} off.
254
+ `
255
+ );
251
256
  }
252
257
  return 0;
253
258
  }
@@ -297,19 +302,20 @@ async function computeCurrentSnapshots(cwd) {
297
302
  const sampled = await sampleWorkspaceFiles(workspaceAbs, config.sampling);
298
303
  let extractedCount = 0;
299
304
  let lastExtractionError;
300
- for (const sampledRel of sampled) {
301
- const sampledAbs = path.resolve(workspaceAbs, sampledRel);
302
- try {
303
- extractedForGroup.push(extractRulesFromPrintConfig(workspaceAbs, sampledAbs));
305
+ const sampledAbs = sampled.map((sampledRel) => path.resolve(workspaceAbs, sampledRel));
306
+ const results = await extractRulesForWorkspaceSamples(workspaceAbs, sampledAbs);
307
+ for (const result of results) {
308
+ if (result.rules) {
309
+ extractedForGroup.push(result.rules);
304
310
  extractedCount += 1;
305
- } catch (error) {
306
- const message = error instanceof Error ? error.message : String(error);
307
- if (message.startsWith("Invalid JSON from eslint --print-config") || message.startsWith("Empty ESLint print-config output")) {
308
- lastExtractionError = message;
309
- continue;
310
- }
311
- throw error;
311
+ continue;
312
312
  }
313
+ const message = result.error instanceof Error ? result.error.message : String(result.error);
314
+ if (isRecoverableExtractionError(message)) {
315
+ lastExtractionError = message;
316
+ continue;
317
+ }
318
+ throw result.error ?? new Error(message);
313
319
  }
314
320
  if (extractedCount === 0) {
315
321
  const context = lastExtractionError ? ` Last error: ${lastExtractionError}` : "";
@@ -323,6 +329,9 @@ async function computeCurrentSnapshots(cwd) {
323
329
  }
324
330
  return snapshots;
325
331
  }
332
+ function isRecoverableExtractionError(message) {
333
+ return message.startsWith("Invalid JSON from eslint --print-config") || message.startsWith("Empty ESLint print-config output") || message.includes("File ignored because of a matching ignore pattern") || message.includes("File ignored by default");
334
+ }
326
335
  async function resolveWorkspaceAssignments(cwd, config) {
327
336
  const discovery = await discoverWorkspaces({ cwd, workspaceInput: config.workspaceInput });
328
337
  const assignments = config.grouping.mode === "standalone" ? discovery.workspacesRel.map((workspace) => ({ name: workspace, workspaces: [workspace] })) : assignGroupsByMatch(discovery.workspacesRel, config.grouping.groups ?? [{ name: "default", match: ["**/*"] }]);
@@ -464,7 +473,7 @@ async function askInitPreset(selectPrompt) {
464
473
  return selectPrompt({
465
474
  message: "Select preset",
466
475
  choices: [
467
- { name: 'recommended (default group "*" + static overrides)', value: "recommended" },
476
+ { name: 'recommended (dynamic catch-all "*" + optional static exceptions)', value: "recommended" },
468
477
  { name: "minimal", value: "minimal" },
469
478
  { name: "full", value: "full" }
470
479
  ]
@@ -614,9 +623,12 @@ function trimTrailingSlashes(value) {
614
623
  }
615
624
  async function askRecommendedGroupAssignments(workspaces) {
616
625
  const { checkbox, select } = await import("@inquirer/prompts");
617
- process.stdout.write('Recommended setup: select only workspaces that should leave default group "*".\n');
626
+ process.stdout.write(
627
+ 'Recommended setup: default group "*" is a dynamic catch-all for every discovered workspace.\n'
628
+ );
629
+ process.stdout.write("Select only workspaces that should move to explicit static groups.\n");
618
630
  const overrides = await checkbox({
619
- message: "Workspaces outside default group:",
631
+ message: 'Choose exception workspaces (leave empty to keep all in default "*"):',
620
632
  choices: workspaces.map((workspace) => ({ name: workspace, value: workspace })),
621
633
  pageSize: Math.min(12, Math.max(4, workspaces.length))
622
634
  });
@@ -682,22 +694,29 @@ function printWhatChanged(changes, currentSnapshots) {
682
694
  const changeSummary = summarizeChanges(changes);
683
695
  if (changes.length === 0) {
684
696
  process.stdout.write(color.green("Great news: no snapshot drift detected.\n"));
697
+ writeSectionTitle("Summary", color);
685
698
  process.stdout.write(
686
- `Baseline status: ${currentSummary.groups} groups, ${currentSummary.rules} rules (severity mix: ${currentSummary.error} errors, ${currentSummary.warn} warnings, ${currentSummary.off} off).
699
+ `- baseline: ${currentSummary.groups} groups, ${currentSummary.rules} rules
700
+ - severity mix: ${currentSummary.error} errors, ${currentSummary.warn} warnings, ${currentSummary.off} off
687
701
  `
688
702
  );
689
703
  return 0;
690
704
  }
691
705
  process.stdout.write(color.red("Heads up: snapshot drift detected.\n"));
706
+ writeSectionTitle("Summary", color);
692
707
  process.stdout.write(
693
- `Changed groups: ${changes.length} | introduced: ${changeSummary.introduced} | removed: ${changeSummary.removed} | severity: ${changeSummary.severity} | options: ${changeSummary.options} | workspace membership: ${changeSummary.workspace}
694
- `
695
- );
696
- process.stdout.write(
697
- `Current rules: ${currentSummary.rules} (severity mix: ${currentSummary.error} errors, ${currentSummary.warn} warnings, ${currentSummary.off} off)
708
+ `- changed groups: ${changes.length}
709
+ - introduced rules: ${changeSummary.introduced}
710
+ - removed rules: ${changeSummary.removed}
711
+ - severity changes: ${changeSummary.severity}
712
+ - options changes: ${changeSummary.options}
713
+ - workspace membership changes: ${changeSummary.workspace}
714
+ - current baseline: ${currentSummary.groups} groups, ${currentSummary.rules} rules
715
+ - current severity mix: ${currentSummary.error} errors, ${currentSummary.warn} warnings, ${currentSummary.off} off
698
716
 
699
717
  `
700
718
  );
719
+ writeSectionTitle("Changes", color);
701
720
  for (const change of changes) {
702
721
  process.stdout.write(color.bold(`group ${change.groupId}
703
722
  `));
@@ -712,6 +731,10 @@ function printWhatChanged(changes, currentSnapshots) {
712
731
  writeSubtleInfo(UPDATE_HINT);
713
732
  return 1;
714
733
  }
734
+ function writeSectionTitle(title, color) {
735
+ process.stdout.write(`${color.bold(title)}
736
+ `);
737
+ }
715
738
  function summarizeChanges(changes) {
716
739
  let introduced = 0;
717
740
  let removed = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eslint-config-snapshot/cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -30,6 +30,6 @@
30
30
  "@inquirer/prompts": "^8.2.0",
31
31
  "commander": "^14.0.3",
32
32
  "fast-glob": "^3.3.3",
33
- "@eslint-config-snapshot/api": "0.3.0"
33
+ "@eslint-config-snapshot/api": "0.4.0"
34
34
  }
35
35
  }
package/src/index.ts CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  buildSnapshot,
6
6
  diffSnapshots,
7
7
  discoverWorkspaces,
8
- extractRulesFromPrintConfig,
8
+ extractRulesForWorkspaceSamples,
9
9
  findConfigPath,
10
10
  getConfigScaffold,
11
11
  hasDiff,
@@ -164,7 +164,7 @@ function createProgram(cwd: string, onActionExit: (code: number) => void): Comma
164
164
  Examples:
165
165
  $ eslint-config-snapshot init
166
166
  Runs interactive select prompts for target/preset.
167
- Recommended preset uses checkbox selection for non-default workspaces and group selection.
167
+ Recommended preset keeps a dynamic catch-all default group ("*") and asks only for static exception groups.
168
168
 
169
169
  $ eslint-config-snapshot init --yes --target package-json --preset recommended --show-effective
170
170
  Non-interactive recommended setup in package.json, with effective preview.
@@ -340,7 +340,11 @@ async function executeUpdate(cwd: string, printSummary: boolean): Promise<number
340
340
 
341
341
  if (printSummary) {
342
342
  const summary = summarizeSnapshots(currentSnapshots)
343
- process.stdout.write(`Baseline updated: ${summary.groups} groups, ${summary.rules} rules.\n`)
343
+ const color = createColorizer()
344
+ writeSectionTitle('Summary', color)
345
+ process.stdout.write(
346
+ `Baseline updated: ${summary.groups} groups, ${summary.rules} rules.\nSeverity mix: ${summary.error} errors, ${summary.warn} warnings, ${summary.off} off.\n`
347
+ )
344
348
  }
345
349
 
346
350
  return 0
@@ -400,23 +404,23 @@ async function computeCurrentSnapshots(cwd: string): Promise<Map<string, BuiltSn
400
404
  let extractedCount = 0
401
405
  let lastExtractionError: string | undefined
402
406
 
403
- for (const sampledRel of sampled) {
404
- const sampledAbs = path.resolve(workspaceAbs, sampledRel)
405
- try {
406
- extractedForGroup.push(extractRulesFromPrintConfig(workspaceAbs, sampledAbs))
407
+ const sampledAbs = sampled.map((sampledRel) => path.resolve(workspaceAbs, sampledRel))
408
+ const results = await extractRulesForWorkspaceSamples(workspaceAbs, sampledAbs)
409
+
410
+ for (const result of results) {
411
+ if (result.rules) {
412
+ extractedForGroup.push(result.rules)
407
413
  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
414
+ continue
415
+ }
416
+
417
+ const message = result.error instanceof Error ? result.error.message : String(result.error)
418
+ if (isRecoverableExtractionError(message)) {
419
+ lastExtractionError = message
420
+ continue
419
421
  }
422
+
423
+ throw result.error ?? new Error(message)
420
424
  }
421
425
 
422
426
  if (extractedCount === 0) {
@@ -434,6 +438,15 @@ async function computeCurrentSnapshots(cwd: string): Promise<Map<string, BuiltSn
434
438
  return snapshots
435
439
  }
436
440
 
441
+ function isRecoverableExtractionError(message: string): boolean {
442
+ return (
443
+ message.startsWith('Invalid JSON from eslint --print-config') ||
444
+ message.startsWith('Empty ESLint print-config output') ||
445
+ message.includes('File ignored because of a matching ignore pattern') ||
446
+ message.includes('File ignored by default')
447
+ )
448
+ }
449
+
437
450
  async function resolveWorkspaceAssignments(cwd: string, config: Awaited<ReturnType<typeof loadConfig>>) {
438
451
  const discovery = await discoverWorkspaces({ cwd, workspaceInput: config.workspaceInput })
439
452
 
@@ -617,7 +630,7 @@ async function askInitPreset(
617
630
  return selectPrompt({
618
631
  message: 'Select preset',
619
632
  choices: [
620
- { name: 'recommended (default group "*" + static overrides)', value: 'recommended' },
633
+ { name: 'recommended (dynamic catch-all "*" + optional static exceptions)', value: 'recommended' },
621
634
  { name: 'minimal', value: 'minimal' },
622
635
  { name: 'full', value: 'full' }
623
636
  ]
@@ -798,9 +811,12 @@ function trimTrailingSlashes(value: string): string {
798
811
 
799
812
  async function askRecommendedGroupAssignments(workspaces: string[]): Promise<Map<string, number>> {
800
813
  const { checkbox, select } = await import('@inquirer/prompts')
801
- process.stdout.write('Recommended setup: select only workspaces that should leave default group "*".\n')
814
+ process.stdout.write(
815
+ 'Recommended setup: default group "*" is a dynamic catch-all for every discovered workspace.\n'
816
+ )
817
+ process.stdout.write('Select only workspaces that should move to explicit static groups.\n')
802
818
  const overrides = await checkbox<string>({
803
- message: 'Workspaces outside default group:',
819
+ message: 'Choose exception workspaces (leave empty to keep all in default "*"):',
804
820
  choices: workspaces.map((workspace) => ({ name: workspace, value: workspace })),
805
821
  pageSize: Math.min(12, Math.max(4, workspaces.length))
806
822
  })
@@ -877,20 +893,20 @@ function printWhatChanged(changes: Array<{ groupId: string; diff: SnapshotDiff }
877
893
 
878
894
  if (changes.length === 0) {
879
895
  process.stdout.write(color.green('Great news: no snapshot drift detected.\n'))
896
+ writeSectionTitle('Summary', color)
880
897
  process.stdout.write(
881
- `Baseline status: ${currentSummary.groups} groups, ${currentSummary.rules} rules (severity mix: ${currentSummary.error} errors, ${currentSummary.warn} warnings, ${currentSummary.off} off).\n`
898
+ `- baseline: ${currentSummary.groups} groups, ${currentSummary.rules} rules\n- severity mix: ${currentSummary.error} errors, ${currentSummary.warn} warnings, ${currentSummary.off} off\n`
882
899
  )
883
900
  return 0
884
901
  }
885
902
 
886
903
  process.stdout.write(color.red('Heads up: snapshot drift detected.\n'))
904
+ writeSectionTitle('Summary', color)
887
905
  process.stdout.write(
888
- `Changed groups: ${changes.length} | introduced: ${changeSummary.introduced} | removed: ${changeSummary.removed} | severity: ${changeSummary.severity} | options: ${changeSummary.options} | workspace membership: ${changeSummary.workspace}\n`
889
- )
890
- process.stdout.write(
891
- `Current rules: ${currentSummary.rules} (severity mix: ${currentSummary.error} errors, ${currentSummary.warn} warnings, ${currentSummary.off} off)\n\n`
906
+ `- 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`
892
907
  )
893
908
 
909
+ writeSectionTitle('Changes', color)
894
910
  for (const change of changes) {
895
911
  process.stdout.write(color.bold(`group ${change.groupId}\n`))
896
912
  const lines = formatDiff(change.groupId, change.diff).split('\n').slice(1)
@@ -905,6 +921,10 @@ function printWhatChanged(changes: Array<{ groupId: string; diff: SnapshotDiff }
905
921
  return 1
906
922
  }
907
923
 
924
+ function writeSectionTitle(title: string, color: ReturnType<typeof createColorizer>): void {
925
+ process.stdout.write(`${color.bold(title)}\n`)
926
+ }
927
+
908
928
  function summarizeChanges(changes: Array<{ groupId: string; diff: SnapshotDiff }>) {
909
929
  let introduced = 0
910
930
  let removed = 0
@@ -389,7 +389,7 @@ no-debugger: off
389
389
  expect(result.stdout).toContain('Initialize config (file or package.json)')
390
390
  expect(result.stdout).toContain('-f, --force')
391
391
  expect(result.stdout).toContain('Runs interactive select prompts for target/preset.')
392
- expect(result.stdout).toContain('Recommended preset uses checkbox selection')
392
+ expect(result.stdout).toContain('Recommended preset keeps a dynamic catch-all default group ("*")')
393
393
  expect(result.stdout).toContain('--show-effective')
394
394
  expect(result.stdout).toContain('--yes --force --target file --preset full')
395
395
  expect(result.stderr).toBe('')