@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/dist/index.js CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  getConfigScaffold,
13
13
  hasDiff,
14
14
  loadConfig,
15
+ normalizePath,
15
16
  readSnapshotFile,
16
17
  sampleWorkspaceFiles,
17
18
  writeSnapshotFile
@@ -92,17 +93,21 @@ function createProgram(cwd, onActionExit) {
92
93
  await executePrint(cwd, format);
93
94
  onActionExit(0);
94
95
  });
95
- 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(
96
+ 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) => {
97
+ const format = opts.short ? "short" : opts.format;
98
+ await executeConfig(cwd, format);
99
+ onActionExit(0);
100
+ });
101
+ 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(
96
102
  "after",
97
103
  `
98
104
  Examples:
99
105
  $ eslint-config-snapshot init
100
- Runs interactive numbered prompts:
101
- target: 1) package-json, 2) file
102
- preset: 1) minimal, 2) full
106
+ Runs interactive select prompts for target/preset.
107
+ Recommended preset uses checkbox selection for non-default workspaces and group selection.
103
108
 
104
- $ eslint-config-snapshot init --yes --target package-json --preset minimal
105
- Non-interactive minimal setup in package.json.
109
+ $ eslint-config-snapshot init --yes --target package-json --preset recommended --show-effective
110
+ Non-interactive recommended setup in package.json, with effective preview.
106
111
 
107
112
  $ eslint-config-snapshot init --yes --force --target file --preset full
108
113
  Overwrite-safe bypass when a config is already detected.
@@ -145,10 +150,10 @@ function parseInitTarget(value) {
145
150
  }
146
151
  function parseInitPreset(value) {
147
152
  const normalized = value.trim().toLowerCase();
148
- if (normalized === "minimal" || normalized === "full") {
153
+ if (normalized === "recommended" || normalized === "minimal" || normalized === "full") {
149
154
  return normalized;
150
155
  }
151
- throw new InvalidArgumentError("Expected one of: minimal, full");
156
+ throw new InvalidArgumentError("Expected one of: recommended, minimal, full");
152
157
  }
153
158
  async function executeCheck(cwd, format, defaultInvocation = false) {
154
159
  const foundConfig = await findConfigPath(cwd);
@@ -259,17 +264,31 @@ async function executePrint(cwd, format) {
259
264
  process.stdout.write(`${JSON.stringify(output, null, 2)}
260
265
  `);
261
266
  }
262
- async function computeCurrentSnapshots(cwd) {
267
+ async function executeConfig(cwd, format) {
268
+ const foundConfig = await findConfigPath(cwd);
263
269
  const config = await loadConfig(cwd);
264
- const discovery = await discoverWorkspaces({ cwd, workspaceInput: config.workspaceInput });
265
- const assignments = config.grouping.mode === "standalone" ? discovery.workspacesRel.map((workspace) => ({ name: workspace, workspaces: [workspace] })) : assignGroupsByMatch(discovery.workspacesRel, config.grouping.groups ?? [{ name: "default", match: ["**/*"] }]);
266
- const allowEmptyGroups = config.grouping.allowEmptyGroups ?? false;
267
- if (!allowEmptyGroups) {
268
- const empty = assignments.filter((group) => group.workspaces.length === 0);
269
- if (empty.length > 0) {
270
- throw new Error(`Empty groups are not allowed: ${empty.map((entry) => entry.name).join(", ")}`);
271
- }
270
+ const resolved = await resolveWorkspaceAssignments(cwd, config);
271
+ const payload = {
272
+ source: foundConfig?.path ?? "built-in-defaults",
273
+ workspaceInput: config.workspaceInput,
274
+ workspaces: resolved.discovery.workspacesRel,
275
+ grouping: {
276
+ mode: config.grouping.mode,
277
+ allowEmptyGroups: config.grouping.allowEmptyGroups ?? false,
278
+ groups: resolved.assignments.map((entry) => ({ name: entry.name, workspaces: entry.workspaces }))
279
+ },
280
+ sampling: config.sampling
281
+ };
282
+ if (format === "short") {
283
+ process.stdout.write(formatShortConfig(payload));
284
+ return;
272
285
  }
286
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}
287
+ `);
288
+ }
289
+ async function computeCurrentSnapshots(cwd) {
290
+ const config = await loadConfig(cwd);
291
+ const { discovery, assignments } = await resolveWorkspaceAssignments(cwd, config);
273
292
  const snapshots = /* @__PURE__ */ new Map();
274
293
  for (const group of assignments) {
275
294
  const extractedForGroup = [];
@@ -304,6 +323,18 @@ async function computeCurrentSnapshots(cwd) {
304
323
  }
305
324
  return snapshots;
306
325
  }
326
+ async function resolveWorkspaceAssignments(cwd, config) {
327
+ const discovery = await discoverWorkspaces({ cwd, workspaceInput: config.workspaceInput });
328
+ const assignments = config.grouping.mode === "standalone" ? discovery.workspacesRel.map((workspace) => ({ name: workspace, workspaces: [workspace] })) : assignGroupsByMatch(discovery.workspacesRel, config.grouping.groups ?? [{ name: "default", match: ["**/*"] }]);
329
+ const allowEmptyGroups = config.grouping.allowEmptyGroups ?? false;
330
+ if (!allowEmptyGroups) {
331
+ const empty = assignments.filter((group) => group.workspaces.length === 0);
332
+ if (empty.length > 0) {
333
+ throw new Error(`Empty groups are not allowed: ${empty.map((entry) => entry.name).join(", ")}`);
334
+ }
335
+ }
336
+ return { discovery, assignments };
337
+ }
307
338
  async function loadStoredSnapshots(cwd) {
308
339
  const dir = path.join(cwd, SNAPSHOT_DIR);
309
340
  const files = await fg("**/*.json", { cwd: dir, absolute: true, onlyFiles: true, dot: true, suppressErrors: true });
@@ -385,6 +416,7 @@ function getDisplayOptionChanges(diff) {
385
416
  }
386
417
  async function runInit(cwd, opts = {}) {
387
418
  const force = opts.force ?? false;
419
+ const showEffective = opts.showEffective ?? false;
388
420
  const existing = await findConfigPath(cwd);
389
421
  if (existing && !force) {
390
422
  process.stderr.write(
@@ -401,70 +433,42 @@ async function runInit(cwd, opts = {}) {
401
433
  preset = interactive.preset;
402
434
  }
403
435
  const finalTarget = target ?? "file";
404
- const finalPreset = preset ?? "minimal";
436
+ const finalPreset = preset ?? "recommended";
437
+ const configObject = await resolveInitConfigObject(cwd, finalPreset, Boolean(opts.yes));
438
+ if (showEffective) {
439
+ process.stdout.write(`Effective config preview:
440
+ ${JSON.stringify(configObject, null, 2)}
441
+ `);
442
+ }
405
443
  if (finalTarget === "package-json") {
406
- return runInitInPackageJson(cwd, finalPreset, force);
444
+ return runInitInPackageJson(cwd, configObject, force);
407
445
  }
408
- return runInitInFile(cwd, finalPreset, force);
446
+ return runInitInFile(cwd, configObject, force);
409
447
  }
410
448
  async function askInitPreferences() {
411
- const rl = createInterface({ input: process.stdin, output: process.stdout });
412
- try {
413
- const target = await askInitTarget(rl);
414
- const preset = await askInitPreset(rl);
415
- return { target, preset };
416
- } finally {
417
- rl.close();
418
- }
419
- }
420
- async function askInitTarget(rl) {
421
- while (true) {
422
- const answer = await askQuestion(
423
- rl,
424
- "Select config target:\n 1) package-json (recommended)\n 2) file\nChoose [1]: "
425
- );
426
- const parsed = parseInitTargetChoice(answer);
427
- if (parsed) {
428
- return parsed;
429
- }
430
- process.stdout.write("Please choose 1 (package-json) or 2 (file).\n");
431
- }
432
- }
433
- async function askInitPreset(rl) {
434
- while (true) {
435
- const answer = await askQuestion(rl, "Select preset:\n 1) minimal (recommended)\n 2) full\nChoose [1]: ");
436
- const parsed = parseInitPresetChoice(answer);
437
- if (parsed) {
438
- return parsed;
439
- }
440
- process.stdout.write("Please choose 1 (minimal) or 2 (full).\n");
441
- }
442
- }
443
- function parseInitTargetChoice(value) {
444
- const normalized = value.trim().toLowerCase();
445
- if (normalized === "") {
446
- return "package-json";
447
- }
448
- if (normalized === "1" || normalized === "package-json" || normalized === "packagejson" || normalized === "package" || normalized === "pkg") {
449
- return "package-json";
450
- }
451
- if (normalized === "2" || normalized === "file") {
452
- return "file";
453
- }
454
- 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
+ });
455
462
  }
456
- function parseInitPresetChoice(value) {
457
- const normalized = value.trim().toLowerCase();
458
- if (normalized === "") {
459
- return "minimal";
460
- }
461
- if (normalized === "1" || normalized === "minimal" || normalized === "min") {
462
- return "minimal";
463
- }
464
- if (normalized === "2" || normalized === "full") {
465
- return "full";
466
- }
467
- return void 0;
463
+ async function askInitPreset(selectPrompt) {
464
+ return selectPrompt({
465
+ message: "Select preset",
466
+ choices: [
467
+ { name: 'recommended (default group "*" + static overrides)', value: "recommended" },
468
+ { name: "minimal", value: "minimal" },
469
+ { name: "full", value: "full" }
470
+ ]
471
+ });
468
472
  }
469
473
  function askQuestion(rl, prompt) {
470
474
  return new Promise((resolve) => {
@@ -486,7 +490,7 @@ async function askYesNo(prompt, defaultYes) {
486
490
  rl.close();
487
491
  }
488
492
  }
489
- async function runInitInFile(cwd, preset, force) {
493
+ async function runInitInFile(cwd, configObject, force) {
490
494
  const candidates = [
491
495
  ".eslint-config-snapshot.js",
492
496
  ".eslint-config-snapshot.cjs",
@@ -507,12 +511,12 @@ async function runInitInFile(cwd, preset, force) {
507
511
  }
508
512
  }
509
513
  const target = path.join(cwd, "eslint-config-snapshot.config.mjs");
510
- await writeFile(target, getConfigScaffold(preset), "utf8");
514
+ await writeFile(target, toConfigScaffold(configObject), "utf8");
511
515
  process.stdout.write(`Created ${path.basename(target)}
512
516
  `);
513
517
  return 0;
514
518
  }
515
- async function runInitInPackageJson(cwd, preset, force) {
519
+ async function runInitInPackageJson(cwd, configObject, force) {
516
520
  const packageJsonPath = path.join(cwd, "package.json");
517
521
  let packageJsonRaw;
518
522
  try {
@@ -532,12 +536,116 @@ async function runInitInPackageJson(cwd, preset, force) {
532
536
  process.stderr.write("Config already exists in package.json: eslint-config-snapshot\n");
533
537
  return 1;
534
538
  }
535
- parsed["eslint-config-snapshot"] = preset === "full" ? getFullPresetObject() : {};
539
+ parsed["eslint-config-snapshot"] = configObject;
536
540
  await writeFile(packageJsonPath, `${JSON.stringify(parsed, null, 2)}
537
541
  `, "utf8");
538
542
  process.stdout.write('Created config in package.json under "eslint-config-snapshot"\n');
539
543
  return 0;
540
544
  }
545
+ async function resolveInitConfigObject(cwd, preset, nonInteractive) {
546
+ if (preset === "minimal") {
547
+ return {};
548
+ }
549
+ if (preset === "full") {
550
+ return getFullPresetObject();
551
+ }
552
+ return buildRecommendedPresetObject(cwd, nonInteractive);
553
+ }
554
+ async function buildRecommendedPresetObject(cwd, nonInteractive) {
555
+ const workspaces = await discoverInitWorkspaces(cwd);
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) {
561
+ const groupNumbers = [...new Set(assignments.values())].sort((a, b) => a - b);
562
+ if (groupNumbers.length === 0) {
563
+ return {};
564
+ }
565
+ const explicitGroups = groupNumbers.map((number) => ({
566
+ name: `group-${number}`,
567
+ match: workspaces.filter((workspace) => assignments.get(workspace) === number)
568
+ }));
569
+ return {
570
+ grouping: {
571
+ mode: "match",
572
+ groups: [...explicitGroups, { name: "default", match: ["**/*"] }]
573
+ }
574
+ };
575
+ }
576
+ async function discoverInitWorkspaces(cwd) {
577
+ const discovered = await discoverWorkspaces({ cwd, workspaceInput: { mode: "discover" } });
578
+ if (!(discovered.workspacesRel.length === 1 && discovered.workspacesRel[0] === ".")) {
579
+ return discovered.workspacesRel;
580
+ }
581
+ const packageJsonPath = path.join(cwd, "package.json");
582
+ try {
583
+ const raw = await readFile(packageJsonPath, "utf8");
584
+ const parsed = JSON.parse(raw);
585
+ let workspacePatterns = [];
586
+ if (Array.isArray(parsed.workspaces)) {
587
+ workspacePatterns = parsed.workspaces;
588
+ } else if (parsed.workspaces && typeof parsed.workspaces === "object" && Array.isArray(parsed.workspaces.packages)) {
589
+ workspacePatterns = parsed.workspaces.packages;
590
+ }
591
+ if (workspacePatterns.length === 0) {
592
+ return discovered.workspacesRel;
593
+ }
594
+ const workspacePackageFiles = await fg(
595
+ workspacePatterns.map((pattern) => `${trimTrailingSlashes(pattern)}/package.json`),
596
+ { cwd, onlyFiles: true, dot: true }
597
+ );
598
+ const workspaceDirs = [...new Set(workspacePackageFiles.map((entry) => normalizePath(path.dirname(entry))))].sort(
599
+ (a, b) => a.localeCompare(b)
600
+ );
601
+ if (workspaceDirs.length > 0) {
602
+ return workspaceDirs;
603
+ }
604
+ } catch {
605
+ }
606
+ return discovered.workspacesRel;
607
+ }
608
+ function trimTrailingSlashes(value) {
609
+ let normalized = value;
610
+ while (normalized.endsWith("/")) {
611
+ normalized = normalized.slice(0, -1);
612
+ }
613
+ return normalized;
614
+ }
615
+ async function askRecommendedGroupAssignments(workspaces) {
616
+ const { checkbox, select } = await import("@inquirer/prompts");
617
+ process.stdout.write('Recommended setup: select only workspaces that should leave default group "*".\n');
618
+ const overrides = await checkbox({
619
+ message: "Workspaces outside default group:",
620
+ choices: workspaces.map((workspace) => ({ name: workspace, value: workspace })),
621
+ pageSize: Math.min(12, Math.max(4, workspaces.length))
622
+ });
623
+ const assignments = /* @__PURE__ */ new Map();
624
+ let nextGroup = 1;
625
+ for (const workspace of overrides) {
626
+ const usedGroups = [...new Set(assignments.values())].sort((a, b) => a - b);
627
+ while (usedGroups.includes(nextGroup)) {
628
+ nextGroup += 1;
629
+ }
630
+ const selected = await select({
631
+ message: `Select group for ${workspace}`,
632
+ choices: [
633
+ ...usedGroups.map((group) => ({ name: `group-${group}`, value: group })),
634
+ { name: `create new group (group-${nextGroup})`, value: "new" }
635
+ ]
636
+ });
637
+ const groupNumber = selected === "new" ? nextGroup : selected;
638
+ assignments.set(workspace, groupNumber);
639
+ }
640
+ return assignments;
641
+ }
642
+ function toConfigScaffold(configObject) {
643
+ if (Object.keys(configObject).length === 0) {
644
+ return getConfigScaffold("minimal");
645
+ }
646
+ return `export default ${JSON.stringify(configObject, null, 2)}
647
+ `;
648
+ }
541
649
  function getFullPresetObject() {
542
650
  return {
543
651
  workspaceInput: { mode: "discover" },
@@ -689,9 +797,21 @@ function formatShortPrint(snapshots) {
689
797
  return `${lines.join("\n")}
690
798
  `;
691
799
  }
800
+ function formatShortConfig(payload) {
801
+ const lines = [
802
+ `source: ${payload.source}`,
803
+ `workspaces (${payload.workspaces.length}): ${payload.workspaces.join(", ") || "(none)"}`,
804
+ `grouping mode: ${payload.grouping.mode} (allow empty: ${payload.grouping.allowEmptyGroups})`
805
+ ];
806
+ for (const group of payload.grouping.groups) {
807
+ lines.push(`group ${group.name} (${group.workspaces.length}): ${group.workspaces.join(", ") || "(none)"}`);
808
+ }
809
+ lines.push(`workspaceInput: ${JSON.stringify(payload.workspaceInput)}`, `sampling: ${JSON.stringify(payload.sampling)}`);
810
+ return `${lines.join("\n")}
811
+ `;
812
+ }
692
813
  export {
814
+ buildRecommendedConfigFromAssignments,
693
815
  main,
694
- parseInitPresetChoice,
695
- parseInitTargetChoice,
696
816
  runCli
697
817
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eslint-config-snapshot/cli",
3
- "version": "0.1.5",
3
+ "version": "0.3.0",
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.1.5"
33
+ "@eslint-config-snapshot/api": "0.3.0"
33
34
  }
34
35
  }