@eslint-config-snapshot/cli 0.1.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # @eslint-config-snapshot/cli
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Release minor with inquirer-based init UX and recommended dynamic grouping behavior.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies
12
+ - @eslint-config-snapshot/api@0.3.0
13
+
14
+ ## 0.2.0
15
+
16
+ ### Minor Changes
17
+
18
+ - Add effective config inspection command and recommended grouped init workflow with numeric workspace assignments.
19
+
20
+ ### Patch Changes
21
+
22
+ - Updated dependencies
23
+ - @eslint-config-snapshot/api@0.2.0
24
+
3
25
  ## 0.1.5
4
26
 
5
27
  ### Patch Changes
package/README.md CHANGED
@@ -30,11 +30,20 @@ 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`
36
44
  - `update`
37
45
  - `print`
46
+ - `config`
38
47
  - `init`
39
48
 
40
49
  Compatibility aliases:
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);
@@ -114,17 +113,21 @@ function createProgram(cwd, onActionExit) {
114
113
  await executePrint(cwd, format);
115
114
  onActionExit(0);
116
115
  });
117
- program.command("init").description("Initialize config (file or package.json)").option("--target <target>", "Config target: file|package-json", parseInitTarget).option("--preset <preset>", "Config preset: minimal|full", parseInitPreset).option("-f, --force", "Allow init even when an existing config is detected").option("-y, --yes", "Skip prompts and use defaults/options").addHelpText(
116
+ program.command("config").description("Print effective evaluated config").option("--format <format>", "Output format: json|short", parsePrintFormat, "json").option("--short", "Alias for --format short").action(async (opts) => {
117
+ const format = opts.short ? "short" : opts.format;
118
+ await executeConfig(cwd, format);
119
+ onActionExit(0);
120
+ });
121
+ program.command("init").description("Initialize config (file or package.json)").option("--target <target>", "Config target: file|package-json", parseInitTarget).option("--preset <preset>", "Config preset: recommended|minimal|full", parseInitPreset).option("--show-effective", "Print the evaluated config that will be written").option("-f, --force", "Allow init even when an existing config is detected").option("-y, --yes", "Skip prompts and use defaults/options").addHelpText(
118
122
  "after",
119
123
  `
120
124
  Examples:
121
125
  $ eslint-config-snapshot init
122
- Runs interactive numbered prompts:
123
- target: 1) package-json, 2) file
124
- preset: 1) minimal, 2) full
126
+ Runs interactive select prompts for target/preset.
127
+ Recommended preset uses checkbox selection for non-default workspaces and group selection.
125
128
 
126
- $ eslint-config-snapshot init --yes --target package-json --preset minimal
127
- Non-interactive minimal setup in package.json.
129
+ $ eslint-config-snapshot init --yes --target package-json --preset recommended --show-effective
130
+ Non-interactive recommended setup in package.json, with effective preview.
128
131
 
129
132
  $ eslint-config-snapshot init --yes --force --target file --preset full
130
133
  Overwrite-safe bypass when a config is already detected.
@@ -167,10 +170,10 @@ function parseInitTarget(value) {
167
170
  }
168
171
  function parseInitPreset(value) {
169
172
  const normalized = value.trim().toLowerCase();
170
- if (normalized === "minimal" || normalized === "full") {
173
+ if (normalized === "recommended" || normalized === "minimal" || normalized === "full") {
171
174
  return normalized;
172
175
  }
173
- throw new import_commander.InvalidArgumentError("Expected one of: minimal, full");
176
+ throw new import_commander.InvalidArgumentError("Expected one of: recommended, minimal, full");
174
177
  }
175
178
  async function executeCheck(cwd, format, defaultInvocation = false) {
176
179
  const foundConfig = await (0, import_api.findConfigPath)(cwd);
@@ -281,17 +284,31 @@ async function executePrint(cwd, format) {
281
284
  process.stdout.write(`${JSON.stringify(output, null, 2)}
282
285
  `);
283
286
  }
284
- async function computeCurrentSnapshots(cwd) {
287
+ async function executeConfig(cwd, format) {
288
+ const foundConfig = await (0, import_api.findConfigPath)(cwd);
285
289
  const config = await (0, import_api.loadConfig)(cwd);
286
- const discovery = await (0, import_api.discoverWorkspaces)({ cwd, workspaceInput: config.workspaceInput });
287
- 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: ["**/*"] }]);
288
- const allowEmptyGroups = config.grouping.allowEmptyGroups ?? false;
289
- if (!allowEmptyGroups) {
290
- const empty = assignments.filter((group) => group.workspaces.length === 0);
291
- if (empty.length > 0) {
292
- throw new Error(`Empty groups are not allowed: ${empty.map((entry) => entry.name).join(", ")}`);
293
- }
290
+ const resolved = await resolveWorkspaceAssignments(cwd, config);
291
+ const payload = {
292
+ source: foundConfig?.path ?? "built-in-defaults",
293
+ workspaceInput: config.workspaceInput,
294
+ workspaces: resolved.discovery.workspacesRel,
295
+ grouping: {
296
+ mode: config.grouping.mode,
297
+ allowEmptyGroups: config.grouping.allowEmptyGroups ?? false,
298
+ groups: resolved.assignments.map((entry) => ({ name: entry.name, workspaces: entry.workspaces }))
299
+ },
300
+ sampling: config.sampling
301
+ };
302
+ if (format === "short") {
303
+ process.stdout.write(formatShortConfig(payload));
304
+ return;
294
305
  }
306
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}
307
+ `);
308
+ }
309
+ async function computeCurrentSnapshots(cwd) {
310
+ const config = await (0, import_api.loadConfig)(cwd);
311
+ const { discovery, assignments } = await resolveWorkspaceAssignments(cwd, config);
295
312
  const snapshots = /* @__PURE__ */ new Map();
296
313
  for (const group of assignments) {
297
314
  const extractedForGroup = [];
@@ -326,6 +343,18 @@ async function computeCurrentSnapshots(cwd) {
326
343
  }
327
344
  return snapshots;
328
345
  }
346
+ async function resolveWorkspaceAssignments(cwd, config) {
347
+ const discovery = await (0, import_api.discoverWorkspaces)({ cwd, workspaceInput: config.workspaceInput });
348
+ 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: ["**/*"] }]);
349
+ const allowEmptyGroups = config.grouping.allowEmptyGroups ?? false;
350
+ if (!allowEmptyGroups) {
351
+ const empty = assignments.filter((group) => group.workspaces.length === 0);
352
+ if (empty.length > 0) {
353
+ throw new Error(`Empty groups are not allowed: ${empty.map((entry) => entry.name).join(", ")}`);
354
+ }
355
+ }
356
+ return { discovery, assignments };
357
+ }
329
358
  async function loadStoredSnapshots(cwd) {
330
359
  const dir = import_node_path.default.join(cwd, SNAPSHOT_DIR);
331
360
  const files = await (0, import_fast_glob.default)("**/*.json", { cwd: dir, absolute: true, onlyFiles: true, dot: true, suppressErrors: true });
@@ -407,6 +436,7 @@ function getDisplayOptionChanges(diff) {
407
436
  }
408
437
  async function runInit(cwd, opts = {}) {
409
438
  const force = opts.force ?? false;
439
+ const showEffective = opts.showEffective ?? false;
410
440
  const existing = await (0, import_api.findConfigPath)(cwd);
411
441
  if (existing && !force) {
412
442
  process.stderr.write(
@@ -423,70 +453,42 @@ async function runInit(cwd, opts = {}) {
423
453
  preset = interactive.preset;
424
454
  }
425
455
  const finalTarget = target ?? "file";
426
- const finalPreset = preset ?? "minimal";
456
+ const finalPreset = preset ?? "recommended";
457
+ const configObject = await resolveInitConfigObject(cwd, finalPreset, Boolean(opts.yes));
458
+ if (showEffective) {
459
+ process.stdout.write(`Effective config preview:
460
+ ${JSON.stringify(configObject, null, 2)}
461
+ `);
462
+ }
427
463
  if (finalTarget === "package-json") {
428
- return runInitInPackageJson(cwd, finalPreset, force);
464
+ return runInitInPackageJson(cwd, configObject, force);
429
465
  }
430
- return runInitInFile(cwd, finalPreset, force);
466
+ return runInitInFile(cwd, configObject, force);
431
467
  }
432
468
  async function askInitPreferences() {
433
- const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
434
- try {
435
- const target = await askInitTarget(rl);
436
- const preset = await askInitPreset(rl);
437
- return { target, preset };
438
- } finally {
439
- rl.close();
440
- }
441
- }
442
- async function askInitTarget(rl) {
443
- while (true) {
444
- const answer = await askQuestion(
445
- rl,
446
- "Select config target:\n 1) package-json (recommended)\n 2) file\nChoose [1]: "
447
- );
448
- const parsed = parseInitTargetChoice(answer);
449
- if (parsed) {
450
- return parsed;
451
- }
452
- process.stdout.write("Please choose 1 (package-json) or 2 (file).\n");
453
- }
454
- }
455
- async function askInitPreset(rl) {
456
- while (true) {
457
- const answer = await askQuestion(rl, "Select preset:\n 1) minimal (recommended)\n 2) full\nChoose [1]: ");
458
- const parsed = parseInitPresetChoice(answer);
459
- if (parsed) {
460
- return parsed;
461
- }
462
- process.stdout.write("Please choose 1 (minimal) or 2 (full).\n");
463
- }
464
- }
465
- function parseInitTargetChoice(value) {
466
- const normalized = value.trim().toLowerCase();
467
- if (normalized === "") {
468
- return "package-json";
469
- }
470
- if (normalized === "1" || normalized === "package-json" || normalized === "packagejson" || normalized === "package" || normalized === "pkg") {
471
- return "package-json";
472
- }
473
- if (normalized === "2" || normalized === "file") {
474
- return "file";
475
- }
476
- 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
+ });
477
482
  }
478
- function parseInitPresetChoice(value) {
479
- const normalized = value.trim().toLowerCase();
480
- if (normalized === "") {
481
- return "minimal";
482
- }
483
- if (normalized === "1" || normalized === "minimal" || normalized === "min") {
484
- return "minimal";
485
- }
486
- if (normalized === "2" || normalized === "full") {
487
- return "full";
488
- }
489
- return void 0;
483
+ async function askInitPreset(selectPrompt) {
484
+ return selectPrompt({
485
+ message: "Select preset",
486
+ choices: [
487
+ { name: 'recommended (default group "*" + static overrides)', value: "recommended" },
488
+ { name: "minimal", value: "minimal" },
489
+ { name: "full", value: "full" }
490
+ ]
491
+ });
490
492
  }
491
493
  function askQuestion(rl, prompt) {
492
494
  return new Promise((resolve) => {
@@ -508,7 +510,7 @@ async function askYesNo(prompt, defaultYes) {
508
510
  rl.close();
509
511
  }
510
512
  }
511
- async function runInitInFile(cwd, preset, force) {
513
+ async function runInitInFile(cwd, configObject, force) {
512
514
  const candidates = [
513
515
  ".eslint-config-snapshot.js",
514
516
  ".eslint-config-snapshot.cjs",
@@ -529,12 +531,12 @@ async function runInitInFile(cwd, preset, force) {
529
531
  }
530
532
  }
531
533
  const target = import_node_path.default.join(cwd, "eslint-config-snapshot.config.mjs");
532
- await (0, import_promises.writeFile)(target, (0, import_api.getConfigScaffold)(preset), "utf8");
534
+ await (0, import_promises.writeFile)(target, toConfigScaffold(configObject), "utf8");
533
535
  process.stdout.write(`Created ${import_node_path.default.basename(target)}
534
536
  `);
535
537
  return 0;
536
538
  }
537
- async function runInitInPackageJson(cwd, preset, force) {
539
+ async function runInitInPackageJson(cwd, configObject, force) {
538
540
  const packageJsonPath = import_node_path.default.join(cwd, "package.json");
539
541
  let packageJsonRaw;
540
542
  try {
@@ -554,12 +556,116 @@ async function runInitInPackageJson(cwd, preset, force) {
554
556
  process.stderr.write("Config already exists in package.json: eslint-config-snapshot\n");
555
557
  return 1;
556
558
  }
557
- parsed["eslint-config-snapshot"] = preset === "full" ? getFullPresetObject() : {};
559
+ parsed["eslint-config-snapshot"] = configObject;
558
560
  await (0, import_promises.writeFile)(packageJsonPath, `${JSON.stringify(parsed, null, 2)}
559
561
  `, "utf8");
560
562
  process.stdout.write('Created config in package.json under "eslint-config-snapshot"\n');
561
563
  return 0;
562
564
  }
565
+ async function resolveInitConfigObject(cwd, preset, nonInteractive) {
566
+ if (preset === "minimal") {
567
+ return {};
568
+ }
569
+ if (preset === "full") {
570
+ return getFullPresetObject();
571
+ }
572
+ return buildRecommendedPresetObject(cwd, nonInteractive);
573
+ }
574
+ async function buildRecommendedPresetObject(cwd, nonInteractive) {
575
+ const workspaces = await discoverInitWorkspaces(cwd);
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) {
581
+ const groupNumbers = [...new Set(assignments.values())].sort((a, b) => a - b);
582
+ if (groupNumbers.length === 0) {
583
+ return {};
584
+ }
585
+ const explicitGroups = groupNumbers.map((number) => ({
586
+ name: `group-${number}`,
587
+ match: workspaces.filter((workspace) => assignments.get(workspace) === number)
588
+ }));
589
+ return {
590
+ grouping: {
591
+ mode: "match",
592
+ groups: [...explicitGroups, { name: "default", match: ["**/*"] }]
593
+ }
594
+ };
595
+ }
596
+ async function discoverInitWorkspaces(cwd) {
597
+ const discovered = await (0, import_api.discoverWorkspaces)({ cwd, workspaceInput: { mode: "discover" } });
598
+ if (!(discovered.workspacesRel.length === 1 && discovered.workspacesRel[0] === ".")) {
599
+ return discovered.workspacesRel;
600
+ }
601
+ const packageJsonPath = import_node_path.default.join(cwd, "package.json");
602
+ try {
603
+ const raw = await (0, import_promises.readFile)(packageJsonPath, "utf8");
604
+ const parsed = JSON.parse(raw);
605
+ let workspacePatterns = [];
606
+ if (Array.isArray(parsed.workspaces)) {
607
+ workspacePatterns = parsed.workspaces;
608
+ } else if (parsed.workspaces && typeof parsed.workspaces === "object" && Array.isArray(parsed.workspaces.packages)) {
609
+ workspacePatterns = parsed.workspaces.packages;
610
+ }
611
+ if (workspacePatterns.length === 0) {
612
+ return discovered.workspacesRel;
613
+ }
614
+ const workspacePackageFiles = await (0, import_fast_glob.default)(
615
+ workspacePatterns.map((pattern) => `${trimTrailingSlashes(pattern)}/package.json`),
616
+ { cwd, onlyFiles: true, dot: true }
617
+ );
618
+ const workspaceDirs = [...new Set(workspacePackageFiles.map((entry) => (0, import_api.normalizePath)(import_node_path.default.dirname(entry))))].sort(
619
+ (a, b) => a.localeCompare(b)
620
+ );
621
+ if (workspaceDirs.length > 0) {
622
+ return workspaceDirs;
623
+ }
624
+ } catch {
625
+ }
626
+ return discovered.workspacesRel;
627
+ }
628
+ function trimTrailingSlashes(value) {
629
+ let normalized = value;
630
+ while (normalized.endsWith("/")) {
631
+ normalized = normalized.slice(0, -1);
632
+ }
633
+ return normalized;
634
+ }
635
+ async function askRecommendedGroupAssignments(workspaces) {
636
+ const { checkbox, select } = await import("@inquirer/prompts");
637
+ process.stdout.write('Recommended setup: select only workspaces that should leave default group "*".\n');
638
+ const overrides = await checkbox({
639
+ message: "Workspaces outside default group:",
640
+ choices: workspaces.map((workspace) => ({ name: workspace, value: workspace })),
641
+ pageSize: Math.min(12, Math.max(4, workspaces.length))
642
+ });
643
+ const assignments = /* @__PURE__ */ new Map();
644
+ let nextGroup = 1;
645
+ for (const workspace of overrides) {
646
+ const usedGroups = [...new Set(assignments.values())].sort((a, b) => a - b);
647
+ while (usedGroups.includes(nextGroup)) {
648
+ nextGroup += 1;
649
+ }
650
+ const selected = await select({
651
+ message: `Select group for ${workspace}`,
652
+ choices: [
653
+ ...usedGroups.map((group) => ({ name: `group-${group}`, value: group })),
654
+ { name: `create new group (group-${nextGroup})`, value: "new" }
655
+ ]
656
+ });
657
+ const groupNumber = selected === "new" ? nextGroup : selected;
658
+ assignments.set(workspace, groupNumber);
659
+ }
660
+ return assignments;
661
+ }
662
+ function toConfigScaffold(configObject) {
663
+ if (Object.keys(configObject).length === 0) {
664
+ return (0, import_api.getConfigScaffold)("minimal");
665
+ }
666
+ return `export default ${JSON.stringify(configObject, null, 2)}
667
+ `;
668
+ }
563
669
  function getFullPresetObject() {
564
670
  return {
565
671
  workspaceInput: { mode: "discover" },
@@ -711,10 +817,22 @@ function formatShortPrint(snapshots) {
711
817
  return `${lines.join("\n")}
712
818
  `;
713
819
  }
820
+ function formatShortConfig(payload) {
821
+ const lines = [
822
+ `source: ${payload.source}`,
823
+ `workspaces (${payload.workspaces.length}): ${payload.workspaces.join(", ") || "(none)"}`,
824
+ `grouping mode: ${payload.grouping.mode} (allow empty: ${payload.grouping.allowEmptyGroups})`
825
+ ];
826
+ for (const group of payload.grouping.groups) {
827
+ lines.push(`group ${group.name} (${group.workspaces.length}): ${group.workspaces.join(", ") || "(none)"}`);
828
+ }
829
+ lines.push(`workspaceInput: ${JSON.stringify(payload.workspaceInput)}`, `sampling: ${JSON.stringify(payload.sampling)}`);
830
+ return `${lines.join("\n")}
831
+ `;
832
+ }
714
833
  // Annotate the CommonJS export names for ESM import in node:
715
834
  0 && (module.exports = {
835
+ buildRecommendedConfigFromAssignments,
716
836
  main,
717
- parseInitPresetChoice,
718
- parseInitTargetChoice,
719
837
  runCli
720
838
  });