@eslint-config-snapshot/cli 0.2.0 → 0.3.2

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.3.2
4
+
5
+ ### Patch Changes
6
+
7
+ - Fix deterministic aggregation when same-severity ESLint rule options differ across sampled files, preventing update crashes.
8
+ - Updated dependencies
9
+ - @eslint-config-snapshot/api@0.3.2
10
+
11
+ ## 0.3.1
12
+
13
+ ### Patch Changes
14
+
15
+ - Release patch bump after init UX clarity improvements for default catch-all group messaging.
16
+ - Updated dependencies
17
+ - @eslint-config-snapshot/api@0.3.1
18
+
19
+ ## 0.3.0
20
+
21
+ ### Minor Changes
22
+
23
+ - Release minor with inquirer-based init UX and recommended dynamic grouping behavior.
24
+
25
+ ### Patch Changes
26
+
27
+ - Updated dependencies
28
+ - @eslint-config-snapshot/api@0.3.0
29
+
3
30
  ## 0.2.0
4
31
 
5
32
  ### Minor Changes
package/README.md CHANGED
@@ -30,6 +30,14 @@ Check drift:
30
30
  eslint-config-snapshot
31
31
  ```
32
32
 
33
+ Recommended setup flow:
34
+
35
+ ```bash
36
+ eslint-config-snapshot init
37
+ ```
38
+
39
+ In `recommended` preset, keep default `*` group and only select outlier workspaces via checkbox, assigning numeric groups to those exceptions.
40
+
33
41
  ## Commands
34
42
 
35
43
  - `check`
package/dist/index.cjs CHANGED
@@ -31,9 +31,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  // src/index.ts
32
32
  var index_exports = {};
33
33
  __export(index_exports, {
34
+ buildRecommendedConfigFromAssignments: () => buildRecommendedConfigFromAssignments,
34
35
  main: () => main,
35
- parseInitPresetChoice: () => parseInitPresetChoice,
36
- parseInitTargetChoice: () => parseInitTargetChoice,
37
36
  runCli: () => runCli
38
37
  });
39
38
  module.exports = __toCommonJS(index_exports);
@@ -124,10 +123,8 @@ function createProgram(cwd, onActionExit) {
124
123
  `
125
124
  Examples:
126
125
  $ eslint-config-snapshot init
127
- Runs interactive numbered prompts:
128
- target: 1) package-json, 2) file
129
- preset: 1) recommended, 2) minimal, 3) full
130
- recommended preset supports per-workspace group number assignment.
126
+ Runs interactive select prompts for target/preset.
127
+ Recommended preset keeps a dynamic catch-all default group ("*") and asks only for static exception groups.
131
128
 
132
129
  $ eslint-config-snapshot init --yes --target package-json --preset recommended --show-effective
133
130
  Non-interactive recommended setup in package.json, with effective preview.
@@ -469,69 +466,29 @@ ${JSON.stringify(configObject, null, 2)}
469
466
  return runInitInFile(cwd, configObject, force);
470
467
  }
471
468
  async function askInitPreferences() {
472
- const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
473
- try {
474
- const target = await askInitTarget(rl);
475
- const preset = await askInitPreset(rl);
476
- return { target, preset };
477
- } finally {
478
- rl.close();
479
- }
480
- }
481
- async function askInitTarget(rl) {
482
- while (true) {
483
- const answer = await askQuestion(
484
- rl,
485
- "Select config target:\n 1) package-json (recommended)\n 2) file\nChoose [1]: "
486
- );
487
- const parsed = parseInitTargetChoice(answer);
488
- if (parsed) {
489
- return parsed;
490
- }
491
- process.stdout.write("Please choose 1 (package-json) or 2 (file).\n");
492
- }
493
- }
494
- async function askInitPreset(rl) {
495
- while (true) {
496
- const answer = await askQuestion(
497
- rl,
498
- "Select preset:\n 1) recommended (group by workspace numbers)\n 2) minimal\n 3) full\nChoose [1]: "
499
- );
500
- const parsed = parseInitPresetChoice(answer);
501
- if (parsed) {
502
- return parsed;
503
- }
504
- process.stdout.write("Please choose 1 (recommended), 2 (minimal), or 3 (full).\n");
505
- }
506
- }
507
- function parseInitTargetChoice(value) {
508
- const normalized = value.trim().toLowerCase();
509
- if (normalized === "") {
510
- return "package-json";
511
- }
512
- if (normalized === "1" || normalized === "package-json" || normalized === "packagejson" || normalized === "package" || normalized === "pkg") {
513
- return "package-json";
514
- }
515
- if (normalized === "2" || normalized === "file") {
516
- return "file";
517
- }
518
- return void 0;
469
+ const { select } = await import("@inquirer/prompts");
470
+ const target = await askInitTarget(select);
471
+ const preset = await askInitPreset(select);
472
+ return { target, preset };
473
+ }
474
+ async function askInitTarget(selectPrompt) {
475
+ return selectPrompt({
476
+ message: "Select config target",
477
+ choices: [
478
+ { name: "package-json (recommended)", value: "package-json" },
479
+ { name: "file", value: "file" }
480
+ ]
481
+ });
519
482
  }
520
- function parseInitPresetChoice(value) {
521
- const normalized = value.trim().toLowerCase();
522
- if (normalized === "") {
523
- return "recommended";
524
- }
525
- if (normalized === "1" || normalized === "recommended" || normalized === "rec" || normalized === "grouped") {
526
- return "recommended";
527
- }
528
- if (normalized === "2" || normalized === "minimal" || normalized === "min") {
529
- return "minimal";
530
- }
531
- if (normalized === "3" || normalized === "full") {
532
- return "full";
533
- }
534
- return void 0;
483
+ async function askInitPreset(selectPrompt) {
484
+ return selectPrompt({
485
+ message: "Select preset",
486
+ choices: [
487
+ { name: 'recommended (dynamic catch-all "*" + optional static exceptions)', value: "recommended" },
488
+ { name: "minimal", value: "minimal" },
489
+ { name: "full", value: "full" }
490
+ ]
491
+ });
535
492
  }
536
493
  function askQuestion(rl, prompt) {
537
494
  return new Promise((resolve) => {
@@ -616,30 +573,23 @@ async function resolveInitConfigObject(cwd, preset, nonInteractive) {
616
573
  }
617
574
  async function buildRecommendedPresetObject(cwd, nonInteractive) {
618
575
  const workspaces = await discoverInitWorkspaces(cwd);
619
- const assignments = new Map(workspaces.map((workspace) => [workspace, 1]));
620
- if (!nonInteractive && process.stdin.isTTY && process.stdout.isTTY) {
621
- process.stdout.write("Recommended setup: assign a group number for each workspace (default: 1).\n");
622
- for (const workspace of workspaces) {
623
- const answer = await askGroupNumber(workspace);
624
- assignments.set(workspace, answer);
625
- }
626
- }
576
+ const useInteractiveGrouping = !nonInteractive && process.stdin.isTTY && process.stdout.isTTY;
577
+ const assignments = useInteractiveGrouping ? await askRecommendedGroupAssignments(workspaces) : /* @__PURE__ */ new Map();
578
+ return buildRecommendedConfigFromAssignments(workspaces, assignments);
579
+ }
580
+ function buildRecommendedConfigFromAssignments(workspaces, assignments) {
627
581
  const groupNumbers = [...new Set(assignments.values())].sort((a, b) => a - b);
628
- const groups = groupNumbers.map((number) => ({
582
+ if (groupNumbers.length === 0) {
583
+ return {};
584
+ }
585
+ const explicitGroups = groupNumbers.map((number) => ({
629
586
  name: `group-${number}`,
630
587
  match: workspaces.filter((workspace) => assignments.get(workspace) === number)
631
588
  }));
632
589
  return {
633
- workspaceInput: { mode: "manual", workspaces },
634
590
  grouping: {
635
591
  mode: "match",
636
- groups
637
- },
638
- sampling: {
639
- maxFilesPerWorkspace: 8,
640
- includeGlobs: ["**/*.{js,jsx,ts,tsx,cjs,mjs}"],
641
- excludeGlobs: ["**/node_modules/**", "**/dist/**"],
642
- hintGlobs: []
592
+ groups: [...explicitGroups, { name: "default", match: ["**/*"] }]
643
593
  }
644
594
  };
645
595
  }
@@ -682,26 +632,35 @@ function trimTrailingSlashes(value) {
682
632
  }
683
633
  return normalized;
684
634
  }
685
- async function askGroupNumber(workspace) {
686
- const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
687
- try {
688
- while (true) {
689
- const answer = await askQuestion(rl, `Group number for ${workspace} [1]: `);
690
- const normalized = answer.trim();
691
- if (normalized === "") {
692
- return 1;
693
- }
694
- if (/^\d+$/.test(normalized)) {
695
- const parsed = Number.parseInt(normalized, 10);
696
- if (parsed >= 1) {
697
- return parsed;
698
- }
699
- }
700
- process.stdout.write("Please provide a positive integer (1, 2, 3, ...).\n");
635
+ async function askRecommendedGroupAssignments(workspaces) {
636
+ const { checkbox, select } = await import("@inquirer/prompts");
637
+ process.stdout.write(
638
+ 'Recommended setup: default group "*" is a dynamic catch-all for every discovered workspace.\n'
639
+ );
640
+ process.stdout.write("Select only workspaces that should move to explicit static groups.\n");
641
+ const overrides = await checkbox({
642
+ message: 'Choose exception workspaces (leave empty to keep all in default "*"):',
643
+ choices: workspaces.map((workspace) => ({ name: workspace, value: workspace })),
644
+ pageSize: Math.min(12, Math.max(4, workspaces.length))
645
+ });
646
+ const assignments = /* @__PURE__ */ new Map();
647
+ let nextGroup = 1;
648
+ for (const workspace of overrides) {
649
+ const usedGroups = [...new Set(assignments.values())].sort((a, b) => a - b);
650
+ while (usedGroups.includes(nextGroup)) {
651
+ nextGroup += 1;
701
652
  }
702
- } finally {
703
- rl.close();
653
+ const selected = await select({
654
+ message: `Select group for ${workspace}`,
655
+ choices: [
656
+ ...usedGroups.map((group) => ({ name: `group-${group}`, value: group })),
657
+ { name: `create new group (group-${nextGroup})`, value: "new" }
658
+ ]
659
+ });
660
+ const groupNumber = selected === "new" ? nextGroup : selected;
661
+ assignments.set(workspace, groupNumber);
704
662
  }
663
+ return assignments;
705
664
  }
706
665
  function toConfigScaffold(configObject) {
707
666
  if (Object.keys(configObject).length === 0) {
@@ -876,8 +835,7 @@ function formatShortConfig(payload) {
876
835
  }
877
836
  // Annotate the CommonJS export names for ESM import in node:
878
837
  0 && (module.exports = {
838
+ buildRecommendedConfigFromAssignments,
879
839
  main,
880
- parseInitPresetChoice,
881
- parseInitTargetChoice,
882
840
  runCli
883
841
  });
package/dist/index.js CHANGED
@@ -103,10 +103,8 @@ function createProgram(cwd, onActionExit) {
103
103
  `
104
104
  Examples:
105
105
  $ eslint-config-snapshot init
106
- Runs interactive numbered prompts:
107
- target: 1) package-json, 2) file
108
- preset: 1) recommended, 2) minimal, 3) full
109
- recommended preset supports per-workspace group number assignment.
106
+ Runs interactive select prompts for target/preset.
107
+ Recommended preset keeps a dynamic catch-all default group ("*") and asks only for static exception groups.
110
108
 
111
109
  $ eslint-config-snapshot init --yes --target package-json --preset recommended --show-effective
112
110
  Non-interactive recommended setup in package.json, with effective preview.
@@ -448,69 +446,29 @@ ${JSON.stringify(configObject, null, 2)}
448
446
  return runInitInFile(cwd, configObject, force);
449
447
  }
450
448
  async function askInitPreferences() {
451
- const rl = createInterface({ input: process.stdin, output: process.stdout });
452
- try {
453
- const target = await askInitTarget(rl);
454
- const preset = await askInitPreset(rl);
455
- return { target, preset };
456
- } finally {
457
- rl.close();
458
- }
459
- }
460
- async function askInitTarget(rl) {
461
- while (true) {
462
- const answer = await askQuestion(
463
- rl,
464
- "Select config target:\n 1) package-json (recommended)\n 2) file\nChoose [1]: "
465
- );
466
- const parsed = parseInitTargetChoice(answer);
467
- if (parsed) {
468
- return parsed;
469
- }
470
- process.stdout.write("Please choose 1 (package-json) or 2 (file).\n");
471
- }
472
- }
473
- async function askInitPreset(rl) {
474
- while (true) {
475
- const answer = await askQuestion(
476
- rl,
477
- "Select preset:\n 1) recommended (group by workspace numbers)\n 2) minimal\n 3) full\nChoose [1]: "
478
- );
479
- const parsed = parseInitPresetChoice(answer);
480
- if (parsed) {
481
- return parsed;
482
- }
483
- process.stdout.write("Please choose 1 (recommended), 2 (minimal), or 3 (full).\n");
484
- }
485
- }
486
- function parseInitTargetChoice(value) {
487
- const normalized = value.trim().toLowerCase();
488
- if (normalized === "") {
489
- return "package-json";
490
- }
491
- if (normalized === "1" || normalized === "package-json" || normalized === "packagejson" || normalized === "package" || normalized === "pkg") {
492
- return "package-json";
493
- }
494
- if (normalized === "2" || normalized === "file") {
495
- return "file";
496
- }
497
- return void 0;
449
+ const { select } = await import("@inquirer/prompts");
450
+ const target = await askInitTarget(select);
451
+ const preset = await askInitPreset(select);
452
+ return { target, preset };
453
+ }
454
+ async function askInitTarget(selectPrompt) {
455
+ return selectPrompt({
456
+ message: "Select config target",
457
+ choices: [
458
+ { name: "package-json (recommended)", value: "package-json" },
459
+ { name: "file", value: "file" }
460
+ ]
461
+ });
498
462
  }
499
- function parseInitPresetChoice(value) {
500
- const normalized = value.trim().toLowerCase();
501
- if (normalized === "") {
502
- return "recommended";
503
- }
504
- if (normalized === "1" || normalized === "recommended" || normalized === "rec" || normalized === "grouped") {
505
- return "recommended";
506
- }
507
- if (normalized === "2" || normalized === "minimal" || normalized === "min") {
508
- return "minimal";
509
- }
510
- if (normalized === "3" || normalized === "full") {
511
- return "full";
512
- }
513
- return void 0;
463
+ async function askInitPreset(selectPrompt) {
464
+ return selectPrompt({
465
+ message: "Select preset",
466
+ choices: [
467
+ { name: 'recommended (dynamic catch-all "*" + optional static exceptions)', value: "recommended" },
468
+ { name: "minimal", value: "minimal" },
469
+ { name: "full", value: "full" }
470
+ ]
471
+ });
514
472
  }
515
473
  function askQuestion(rl, prompt) {
516
474
  return new Promise((resolve) => {
@@ -595,30 +553,23 @@ async function resolveInitConfigObject(cwd, preset, nonInteractive) {
595
553
  }
596
554
  async function buildRecommendedPresetObject(cwd, nonInteractive) {
597
555
  const workspaces = await discoverInitWorkspaces(cwd);
598
- const assignments = new Map(workspaces.map((workspace) => [workspace, 1]));
599
- if (!nonInteractive && process.stdin.isTTY && process.stdout.isTTY) {
600
- process.stdout.write("Recommended setup: assign a group number for each workspace (default: 1).\n");
601
- for (const workspace of workspaces) {
602
- const answer = await askGroupNumber(workspace);
603
- assignments.set(workspace, answer);
604
- }
605
- }
556
+ const useInteractiveGrouping = !nonInteractive && process.stdin.isTTY && process.stdout.isTTY;
557
+ const assignments = useInteractiveGrouping ? await askRecommendedGroupAssignments(workspaces) : /* @__PURE__ */ new Map();
558
+ return buildRecommendedConfigFromAssignments(workspaces, assignments);
559
+ }
560
+ function buildRecommendedConfigFromAssignments(workspaces, assignments) {
606
561
  const groupNumbers = [...new Set(assignments.values())].sort((a, b) => a - b);
607
- const groups = groupNumbers.map((number) => ({
562
+ if (groupNumbers.length === 0) {
563
+ return {};
564
+ }
565
+ const explicitGroups = groupNumbers.map((number) => ({
608
566
  name: `group-${number}`,
609
567
  match: workspaces.filter((workspace) => assignments.get(workspace) === number)
610
568
  }));
611
569
  return {
612
- workspaceInput: { mode: "manual", workspaces },
613
570
  grouping: {
614
571
  mode: "match",
615
- groups
616
- },
617
- sampling: {
618
- maxFilesPerWorkspace: 8,
619
- includeGlobs: ["**/*.{js,jsx,ts,tsx,cjs,mjs}"],
620
- excludeGlobs: ["**/node_modules/**", "**/dist/**"],
621
- hintGlobs: []
572
+ groups: [...explicitGroups, { name: "default", match: ["**/*"] }]
622
573
  }
623
574
  };
624
575
  }
@@ -661,26 +612,35 @@ function trimTrailingSlashes(value) {
661
612
  }
662
613
  return normalized;
663
614
  }
664
- async function askGroupNumber(workspace) {
665
- const rl = createInterface({ input: process.stdin, output: process.stdout });
666
- try {
667
- while (true) {
668
- const answer = await askQuestion(rl, `Group number for ${workspace} [1]: `);
669
- const normalized = answer.trim();
670
- if (normalized === "") {
671
- return 1;
672
- }
673
- if (/^\d+$/.test(normalized)) {
674
- const parsed = Number.parseInt(normalized, 10);
675
- if (parsed >= 1) {
676
- return parsed;
677
- }
678
- }
679
- process.stdout.write("Please provide a positive integer (1, 2, 3, ...).\n");
615
+ async function askRecommendedGroupAssignments(workspaces) {
616
+ const { checkbox, select } = await import("@inquirer/prompts");
617
+ process.stdout.write(
618
+ 'Recommended setup: default group "*" is a dynamic catch-all for every discovered workspace.\n'
619
+ );
620
+ process.stdout.write("Select only workspaces that should move to explicit static groups.\n");
621
+ const overrides = await checkbox({
622
+ message: 'Choose exception workspaces (leave empty to keep all in default "*"):',
623
+ choices: workspaces.map((workspace) => ({ name: workspace, value: workspace })),
624
+ pageSize: Math.min(12, Math.max(4, workspaces.length))
625
+ });
626
+ const assignments = /* @__PURE__ */ new Map();
627
+ let nextGroup = 1;
628
+ for (const workspace of overrides) {
629
+ const usedGroups = [...new Set(assignments.values())].sort((a, b) => a - b);
630
+ while (usedGroups.includes(nextGroup)) {
631
+ nextGroup += 1;
680
632
  }
681
- } finally {
682
- rl.close();
633
+ const selected = await select({
634
+ message: `Select group for ${workspace}`,
635
+ choices: [
636
+ ...usedGroups.map((group) => ({ name: `group-${group}`, value: group })),
637
+ { name: `create new group (group-${nextGroup})`, value: "new" }
638
+ ]
639
+ });
640
+ const groupNumber = selected === "new" ? nextGroup : selected;
641
+ assignments.set(workspace, groupNumber);
683
642
  }
643
+ return assignments;
684
644
  }
685
645
  function toConfigScaffold(configObject) {
686
646
  if (Object.keys(configObject).length === 0) {
@@ -854,8 +814,7 @@ function formatShortConfig(payload) {
854
814
  `;
855
815
  }
856
816
  export {
817
+ buildRecommendedConfigFromAssignments,
857
818
  main,
858
- parseInitPresetChoice,
859
- parseInitTargetChoice,
860
819
  runCli
861
820
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eslint-config-snapshot/cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.2",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -27,8 +27,9 @@
27
27
  }
28
28
  },
29
29
  "dependencies": {
30
+ "@inquirer/prompts": "^8.2.0",
30
31
  "commander": "^14.0.3",
31
32
  "fast-glob": "^3.3.3",
32
- "@eslint-config-snapshot/api": "0.2.0"
33
+ "@eslint-config-snapshot/api": "0.3.2"
33
34
  }
34
35
  }
package/src/index.ts CHANGED
@@ -163,10 +163,8 @@ function createProgram(cwd: string, onActionExit: (code: number) => void): Comma
163
163
  `
164
164
  Examples:
165
165
  $ eslint-config-snapshot init
166
- Runs interactive numbered prompts:
167
- target: 1) package-json, 2) file
168
- preset: 1) recommended, 2) minimal, 3) full
169
- recommended preset supports per-workspace group number assignment.
166
+ Runs interactive select prompts for target/preset.
167
+ Recommended preset keeps a dynamic catch-all default group ("*") and asks only for static exception groups.
170
168
 
171
169
  $ eslint-config-snapshot init --yes --target package-json --preset recommended --show-effective
172
170
  Non-interactive recommended setup in package.json, with effective preview.
@@ -595,73 +593,35 @@ async function runInit(
595
593
  }
596
594
 
597
595
  async function askInitPreferences(): Promise<{ target: InitTarget; preset: InitPreset }> {
598
- const rl = createInterface({ input: process.stdin, output: process.stdout })
599
- try {
600
- const target = await askInitTarget(rl)
601
- const preset = await askInitPreset(rl)
602
- return { target, preset }
603
- } finally {
604
- rl.close()
605
- }
606
- }
607
-
608
- async function askInitTarget(rl: ReturnType<typeof createInterface>): Promise<InitTarget> {
609
- while (true) {
610
- const answer = await askQuestion(
611
- rl,
612
- 'Select config target:\n 1) package-json (recommended)\n 2) file\nChoose [1]: '
613
- )
614
- const parsed = parseInitTargetChoice(answer)
615
- if (parsed) {
616
- return parsed
617
- }
618
- process.stdout.write('Please choose 1 (package-json) or 2 (file).\n')
619
- }
620
- }
621
-
622
- async function askInitPreset(rl: ReturnType<typeof createInterface>): Promise<InitPreset> {
623
- while (true) {
624
- const answer = await askQuestion(
625
- rl,
626
- 'Select preset:\n 1) recommended (group by workspace numbers)\n 2) minimal\n 3) full\nChoose [1]: '
627
- )
628
- const parsed = parseInitPresetChoice(answer)
629
- if (parsed) {
630
- return parsed
631
- }
632
- process.stdout.write('Please choose 1 (recommended), 2 (minimal), or 3 (full).\n')
633
- }
596
+ const { select } = await import('@inquirer/prompts')
597
+ const target = await askInitTarget(select)
598
+ const preset = await askInitPreset(select)
599
+ return { target, preset }
634
600
  }
635
601
 
636
- export function parseInitTargetChoice(value: string): InitTarget | undefined {
637
- const normalized = value.trim().toLowerCase()
638
- if (normalized === '') {
639
- return 'package-json'
640
- }
641
- if (normalized === '1' || normalized === 'package-json' || normalized === 'packagejson' || normalized === 'package' || normalized === 'pkg') {
642
- return 'package-json'
643
- }
644
- if (normalized === '2' || normalized === 'file') {
645
- return 'file'
646
- }
647
- return undefined
602
+ async function askInitTarget(
603
+ selectPrompt: (options: { message: string; choices: Array<{ name: string; value: InitTarget }> }) => Promise<InitTarget>
604
+ ): Promise<InitTarget> {
605
+ return selectPrompt({
606
+ message: 'Select config target',
607
+ choices: [
608
+ { name: 'package-json (recommended)', value: 'package-json' },
609
+ { name: 'file', value: 'file' }
610
+ ]
611
+ })
648
612
  }
649
613
 
650
- export function parseInitPresetChoice(value: string): InitPreset | undefined {
651
- const normalized = value.trim().toLowerCase()
652
- if (normalized === '') {
653
- return 'recommended'
654
- }
655
- if (normalized === '1' || normalized === 'recommended' || normalized === 'rec' || normalized === 'grouped') {
656
- return 'recommended'
657
- }
658
- if (normalized === '2' || normalized === 'minimal' || normalized === 'min') {
659
- return 'minimal'
660
- }
661
- if (normalized === '3' || normalized === 'full') {
662
- return 'full'
663
- }
664
- return undefined
614
+ async function askInitPreset(
615
+ selectPrompt: (options: { message: string; choices: Array<{ name: string; value: InitPreset }> }) => Promise<InitPreset>
616
+ ): Promise<InitPreset> {
617
+ return selectPrompt({
618
+ message: 'Select preset',
619
+ choices: [
620
+ { name: 'recommended (dynamic catch-all "*" + optional static exceptions)', value: 'recommended' },
621
+ { name: 'minimal', value: 'minimal' },
622
+ { name: 'full', value: 'full' }
623
+ ]
624
+ })
665
625
  }
666
626
 
667
627
  function askQuestion(rl: ReturnType<typeof createInterface>, prompt: string): Promise<string> {
@@ -763,33 +723,29 @@ async function resolveInitConfigObject(
763
723
 
764
724
  async function buildRecommendedPresetObject(cwd: string, nonInteractive: boolean): Promise<Record<string, unknown>> {
765
725
  const workspaces = await discoverInitWorkspaces(cwd)
766
- const assignments = new Map<string, number>(workspaces.map((workspace) => [workspace, 1]))
726
+ const useInteractiveGrouping = !nonInteractive && process.stdin.isTTY && process.stdout.isTTY
727
+ const assignments = useInteractiveGrouping ? await askRecommendedGroupAssignments(workspaces) : new Map<string, number>()
728
+ return buildRecommendedConfigFromAssignments(workspaces, assignments)
729
+ }
767
730
 
768
- if (!nonInteractive && process.stdin.isTTY && process.stdout.isTTY) {
769
- process.stdout.write('Recommended setup: assign a group number for each workspace (default: 1).\n')
770
- for (const workspace of workspaces) {
771
- const answer = await askGroupNumber(workspace)
772
- assignments.set(workspace, answer)
773
- }
731
+ export function buildRecommendedConfigFromAssignments(
732
+ workspaces: string[],
733
+ assignments: Map<string, number>
734
+ ): Record<string, unknown> {
735
+ const groupNumbers = [...new Set(assignments.values())].sort((a, b) => a - b)
736
+ if (groupNumbers.length === 0) {
737
+ return {}
774
738
  }
775
739
 
776
- const groupNumbers = [...new Set(assignments.values())].sort((a, b) => a - b)
777
- const groups = groupNumbers.map((number) => ({
740
+ const explicitGroups = groupNumbers.map((number) => ({
778
741
  name: `group-${number}`,
779
742
  match: workspaces.filter((workspace) => assignments.get(workspace) === number)
780
743
  }))
781
744
 
782
745
  return {
783
- workspaceInput: { mode: 'manual', workspaces },
784
746
  grouping: {
785
747
  mode: 'match',
786
- groups
787
- },
788
- sampling: {
789
- maxFilesPerWorkspace: 8,
790
- includeGlobs: ['**/*.{js,jsx,ts,tsx,cjs,mjs}'],
791
- excludeGlobs: ['**/node_modules/**', '**/dist/**'],
792
- hintGlobs: []
748
+ groups: [...explicitGroups, { name: 'default', match: ['**/*'] }]
793
749
  }
794
750
  }
795
751
  }
@@ -840,28 +796,38 @@ function trimTrailingSlashes(value: string): string {
840
796
  return normalized
841
797
  }
842
798
 
843
- async function askGroupNumber(workspace: string): Promise<number> {
844
- const rl = createInterface({ input: process.stdin, output: process.stdout })
845
- try {
846
- while (true) {
847
- const answer = await askQuestion(rl, `Group number for ${workspace} [1]: `)
848
- const normalized = answer.trim()
849
- if (normalized === '') {
850
- return 1
851
- }
852
-
853
- if (/^\d+$/.test(normalized)) {
854
- const parsed = Number.parseInt(normalized, 10)
855
- if (parsed >= 1) {
856
- return parsed
857
- }
858
- }
799
+ async function askRecommendedGroupAssignments(workspaces: string[]): Promise<Map<string, number>> {
800
+ const { checkbox, select } = await import('@inquirer/prompts')
801
+ process.stdout.write(
802
+ 'Recommended setup: default group "*" is a dynamic catch-all for every discovered workspace.\n'
803
+ )
804
+ 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
+ })
859
810
 
860
- process.stdout.write('Please provide a positive integer (1, 2, 3, ...).\n')
811
+ const assignments = new Map<string, number>()
812
+ let nextGroup = 1
813
+ for (const workspace of overrides) {
814
+ const usedGroups = [...new Set(assignments.values())].sort((a, b) => a - b)
815
+ while (usedGroups.includes(nextGroup)) {
816
+ nextGroup += 1
861
817
  }
862
- } finally {
863
- rl.close()
818
+
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
+ })
826
+ const groupNumber = selected === 'new' ? nextGroup : selected
827
+ assignments.set(workspace, groupNumber)
864
828
  }
829
+
830
+ return assignments
865
831
  }
866
832
 
867
833
  function toConfigScaffold(configObject: Record<string, unknown>): string {
@@ -1,18 +1,19 @@
1
1
  import { cp, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
2
2
  import os from 'node:os'
3
3
  import path from 'node:path'
4
- import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
5
5
 
6
- import { parseInitPresetChoice, parseInitTargetChoice, runCli } from '../src/index.js'
6
+ import { buildRecommendedConfigFromAssignments, runCli } from '../src/index.js'
7
7
 
8
- const fixtureRoot = path.resolve('test/fixtures/repo')
9
-
10
- afterAll(async () => {
11
- await rm(path.join(fixtureRoot, '.eslint-config-snapshot'), { recursive: true, force: true })
12
- })
8
+ const fixtureTemplateRoot = path.resolve('test/fixtures/repo')
9
+ let tmpDir = ''
10
+ let fixtureRoot = ''
13
11
 
14
12
  beforeEach(async () => {
15
- await rm(path.join(fixtureRoot, '.eslint-config-snapshot'), { recursive: true, force: true })
13
+ tmpDir = await mkdtemp(path.join(os.tmpdir(), 'snapshot-cli-integration-'))
14
+ fixtureRoot = path.join(tmpDir, 'repo')
15
+ await cp(fixtureTemplateRoot, fixtureRoot, { recursive: true })
16
+
16
17
  await mkdir(path.join(fixtureRoot, 'packages/ws-a/node_modules/eslint/bin'), { recursive: true })
17
18
  await mkdir(path.join(fixtureRoot, 'packages/ws-b/node_modules/eslint/bin'), { recursive: true })
18
19
 
@@ -35,26 +36,31 @@ beforeEach(async () => {
35
36
  )
36
37
  })
37
38
 
38
- describe('cli integration', () => {
39
- it('parses init interactive target choices from numeric and aliases', () => {
40
- expect(parseInitTargetChoice('')).toBe('package-json')
41
- expect(parseInitTargetChoice('1')).toBe('package-json')
42
- expect(parseInitTargetChoice('package')).toBe('package-json')
43
- expect(parseInitTargetChoice('pkg')).toBe('package-json')
44
- expect(parseInitTargetChoice('2')).toBe('file')
45
- expect(parseInitTargetChoice('file')).toBe('file')
46
- expect(parseInitTargetChoice('invalid')).toBeUndefined()
39
+ afterEach(async () => {
40
+ if (tmpDir) {
41
+ await rm(tmpDir, { recursive: true, force: true })
42
+ tmpDir = ''
43
+ fixtureRoot = ''
44
+ }
45
+ })
46
+
47
+ describe.sequential('cli integration', () => {
48
+ it('builds recommended config as dynamic-only when no static overrides are selected', () => {
49
+ const config = buildRecommendedConfigFromAssignments(['packages/ws-a', 'packages/ws-b'], new Map())
50
+ expect(config).toEqual({})
47
51
  })
48
52
 
49
- it('parses init interactive preset choices from numeric and aliases', () => {
50
- expect(parseInitPresetChoice('')).toBe('recommended')
51
- expect(parseInitPresetChoice('1')).toBe('recommended')
52
- expect(parseInitPresetChoice('rec')).toBe('recommended')
53
- expect(parseInitPresetChoice('2')).toBe('minimal')
54
- expect(parseInitPresetChoice('min')).toBe('minimal')
55
- expect(parseInitPresetChoice('3')).toBe('full')
56
- expect(parseInitPresetChoice('full')).toBe('full')
57
- expect(parseInitPresetChoice('invalid')).toBeUndefined()
53
+ it('builds recommended config with static overrides plus dynamic catch-all', () => {
54
+ const config = buildRecommendedConfigFromAssignments(['packages/ws-a', 'packages/ws-b'], new Map([['packages/ws-b', 2]]))
55
+ expect(config).toEqual({
56
+ grouping: {
57
+ mode: 'match',
58
+ groups: [
59
+ { name: 'group-2', match: ['packages/ws-b'] },
60
+ { name: 'default', match: ['**/*'] }
61
+ ]
62
+ }
63
+ })
58
64
  })
59
65
 
60
66
  it('snapshot writes deterministic snapshot files', async () => {
@@ -275,20 +275,27 @@ no-debugger: off
275
275
 
276
276
  const packageJsonRaw = await readFile(path.join(repoRoot, 'package.json'), 'utf8')
277
277
  const parsed = JSON.parse(packageJsonRaw) as {
278
- 'eslint-config-snapshot'?: {
279
- workspaceInput?: { mode?: string; workspaces?: string[] }
280
- grouping?: { mode?: string; groups?: Array<{ name: string; match: string[] }> }
281
- }
278
+ 'eslint-config-snapshot'?: Record<string, unknown>
282
279
  }
283
280
 
284
- expect(parsed['eslint-config-snapshot']?.workspaceInput).toEqual({
285
- mode: 'manual',
286
- workspaces: ['packages/ws-a', 'packages/ws-b']
287
- })
288
- expect(parsed['eslint-config-snapshot']?.grouping).toEqual({
289
- mode: 'match',
290
- groups: [{ name: 'group-1', match: ['packages/ws-a', 'packages/ws-b'] }]
291
- })
281
+ expect(parsed['eslint-config-snapshot']).toEqual({})
282
+ })
283
+
284
+ it('init recommended --show-effective prints preview without explicit sampling block', async () => {
285
+ const initRoot = path.join(tmpDir, 'init-recommended-preview-case')
286
+ await rm(initRoot, { recursive: true, force: true })
287
+ await cp(fixtureRoot, initRoot, { recursive: true })
288
+ repoRoot = initRoot
289
+
290
+ await rm(path.join(repoRoot, 'eslint-config-snapshot.config.mjs'), { force: true })
291
+
292
+ const result = run(['init', '--yes', '--target', 'package-json', '--preset', 'recommended', '--show-effective'])
293
+ expect(result.status).toBe(0)
294
+ expect(result.stdout).toContain('Effective config preview:')
295
+ expect(result.stdout).toContain('{}')
296
+ expect(result.stdout).not.toContain('"workspaceInput"')
297
+ expect(result.stdout).not.toContain('"grouping"')
298
+ expect(result.stdout).not.toContain('"sampling"')
292
299
  })
293
300
 
294
301
  it('init fails early on existing config unless --force is provided', async () => {
@@ -376,14 +383,13 @@ no-debugger: off
376
383
  expect(result.stderr).toBe('')
377
384
  })
378
385
 
379
- it('prints init help with numbered prompt and force guidance', () => {
386
+ it('prints init help with select-prompt and force guidance', () => {
380
387
  const result = run(['init', '--help'])
381
388
  expect(result.status).toBe(0)
382
389
  expect(result.stdout).toContain('Initialize config (file or package.json)')
383
390
  expect(result.stdout).toContain('-f, --force')
384
- expect(result.stdout).toContain('Runs interactive numbered prompts:')
385
- expect(result.stdout).toContain('target: 1) package-json, 2) file')
386
- expect(result.stdout).toContain('preset: 1) recommended, 2) minimal, 3) full')
391
+ expect(result.stdout).toContain('Runs interactive select prompts for target/preset.')
392
+ expect(result.stdout).toContain('Recommended preset keeps a dynamic catch-all default group ("*")')
387
393
  expect(result.stdout).toContain('--show-effective')
388
394
  expect(result.stdout).toContain('--yes --force --target file --preset full')
389
395
  expect(result.stderr).toBe('')
@@ -1 +0,0 @@
1
- console.log(JSON.stringify({ rules: { 'no-console': 1, eqeqeq: [2, 'always'] } }))
@@ -1,4 +0,0 @@
1
- {
2
- "name": "eslint",
3
- "version": "9.0.0"
4
- }
@@ -1 +0,0 @@
1
- console.log(JSON.stringify({ rules: { 'no-console': 2, 'no-debugger': 0 } }))
@@ -1,4 +0,0 @@
1
- {
2
- "name": "eslint",
3
- "version": "9.0.0"
4
- }