@eslint-config-snapshot/cli 1.0.0 → 1.1.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,16 @@
1
1
  # @eslint-config-snapshot/cli
2
2
 
3
+ ## 1.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Minor release: improve skipped workspace messaging and OSS compatibility documentation.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies
12
+ - @eslint-config-snapshot/api@1.1.0
13
+
3
14
  ## 1.0.0
4
15
 
5
16
  ### Major Changes
package/dist/index.cjs CHANGED
@@ -38,7 +38,7 @@ __export(index_exports, {
38
38
  module.exports = __toCommonJS(index_exports);
39
39
  var import_commander = require("commander");
40
40
  var import_debug2 = __toESM(require("debug"), 1);
41
- var import_node_path4 = __toESM(require("path"), 1);
41
+ var import_node_path5 = __toESM(require("path"), 1);
42
42
 
43
43
  // src/commands/check.ts
44
44
  var import_api3 = require("@eslint-config-snapshot/api");
@@ -216,6 +216,12 @@ function formatStoredSnapshotSummary(storedSnapshots) {
216
216
  const summary = summarizeSnapshots(storedSnapshots);
217
217
  return `${summary.groups} groups, ${summary.rules} rules (severity mix: ${summary.error} errors, ${summary.warn} warnings, ${summary.off} off)`;
218
218
  }
219
+ function formatBaselineSummaryLines(summary, workspaceCount) {
220
+ return `- \u{1F4E6} baseline: ${summary.groups} groups, ${summary.rules} rules
221
+ - \u{1F5C2}\uFE0F workspaces scanned: ${workspaceCount}
222
+ - \u{1F39A}\uFE0F severity mix: ${summary.error} errors, ${summary.warn} warnings, ${summary.off} off
223
+ `;
224
+ }
219
225
  function countRuleSeverities(ruleObjects) {
220
226
  let rules = 0;
221
227
  let error = 0;
@@ -395,19 +401,24 @@ var import_node_path2 = __toESM(require("path"), 1);
395
401
  var debugWorkspace = (0, import_debug.default)("eslint-config-snapshot:workspace");
396
402
  var debugDiff = (0, import_debug.default)("eslint-config-snapshot:diff");
397
403
  var debugTiming = (0, import_debug.default)("eslint-config-snapshot:timing");
398
- async function computeCurrentSnapshots(cwd) {
404
+ async function computeCurrentSnapshots(cwd, options) {
405
+ const allowWorkspaceExtractionFailure = options?.allowWorkspaceExtractionFailure ?? false;
406
+ const onWorkspacesDiscovered = options?.onWorkspacesDiscovered;
407
+ const onWorkspaceSkipped = options?.onWorkspaceSkipped;
399
408
  const computeStartedAt = Date.now();
400
409
  const configStartedAt = Date.now();
401
410
  const config = await (0, import_api2.loadConfig)(cwd);
402
411
  debugTiming("phase=loadConfig elapsedMs=%d", Date.now() - configStartedAt);
403
412
  const assignmentStartedAt = Date.now();
404
413
  const { discovery, assignments } = await resolveWorkspaceAssignments(cwd, config);
414
+ onWorkspacesDiscovered?.(discovery.workspacesRel);
405
415
  debugTiming("phase=resolveWorkspaceAssignments elapsedMs=%d", Date.now() - assignmentStartedAt);
406
416
  debugWorkspace("root=%s groups=%d workspaces=%d", discovery.rootAbs, assignments.length, discovery.workspacesRel.length);
407
417
  const snapshots = /* @__PURE__ */ new Map();
408
418
  for (const group of assignments) {
409
419
  const groupStartedAt = Date.now();
410
420
  const extractedForGroup = [];
421
+ const extractedWorkspaces = [];
411
422
  debugWorkspace("group=%s workspaces=%o", group.name, group.workspaces);
412
423
  for (const workspaceRel of group.workspaces) {
413
424
  const workspaceAbs = import_node_path2.default.resolve(discovery.rootAbs, workspaceRel);
@@ -440,7 +451,7 @@ async function computeCurrentSnapshots(cwd) {
440
451
  continue;
441
452
  }
442
453
  const message = result.error instanceof Error ? result.error.message : String(result.error);
443
- if (isRecoverableExtractionError(message)) {
454
+ if (isRecoverableExtractionError(message) || allowWorkspaceExtractionFailure) {
444
455
  lastExtractionError = message;
445
456
  continue;
446
457
  }
@@ -448,10 +459,23 @@ async function computeCurrentSnapshots(cwd) {
448
459
  }
449
460
  if (extractedCount === 0) {
450
461
  const context = lastExtractionError ? ` Last error: ${lastExtractionError}` : "";
451
- throw new Error(
452
- `Unable to extract ESLint config for workspace ${workspaceRel}. All sampled files were ignored or produced non-JSON output.${context}`
453
- );
462
+ if (allowWorkspaceExtractionFailure && isSkippableWorkspaceExtractionFailure(lastExtractionError)) {
463
+ onWorkspaceSkipped?.({
464
+ groupId: group.name,
465
+ workspaceRel,
466
+ reason: lastExtractionError ?? "unknown extraction failure"
467
+ });
468
+ debugWorkspace(
469
+ "group=%s workspace=%s skipped=true reason=%s",
470
+ group.name,
471
+ workspaceRel,
472
+ lastExtractionError ?? "unknown extraction failure"
473
+ );
474
+ continue;
475
+ }
476
+ throw new Error(`Unable to extract ESLint config for workspace ${workspaceRel}.${context}`);
454
477
  }
478
+ extractedWorkspaces.push(workspaceRel);
455
479
  debugWorkspace(
456
480
  "group=%s workspace=%s extracted=%d failed=%d",
457
481
  group.name,
@@ -460,8 +484,15 @@ async function computeCurrentSnapshots(cwd) {
460
484
  results.length - extractedCount
461
485
  );
462
486
  }
487
+ if (extractedForGroup.length === 0) {
488
+ if (allowWorkspaceExtractionFailure) {
489
+ debugWorkspace("group=%s skipped=true reason=no-extracted-workspaces", group.name);
490
+ continue;
491
+ }
492
+ throw new Error(`Unable to extract ESLint config for group ${group.name}: no workspace produced a valid config`);
493
+ }
463
494
  const aggregated = (0, import_api2.aggregateRules)(extractedForGroup);
464
- snapshots.set(group.name, (0, import_api2.buildSnapshot)(group.name, group.workspaces, aggregated));
495
+ snapshots.set(group.name, (0, import_api2.buildSnapshot)(group.name, extractedWorkspaces, aggregated));
465
496
  debugWorkspace(
466
497
  "group=%s aggregatedRules=%d groupElapsedMs=%d",
467
498
  group.name,
@@ -470,11 +501,20 @@ async function computeCurrentSnapshots(cwd) {
470
501
  );
471
502
  }
472
503
  debugTiming("phase=computeCurrentSnapshots elapsedMs=%d", Date.now() - computeStartedAt);
504
+ if (snapshots.size === 0) {
505
+ throw new Error("Unable to extract ESLint config from discovered workspaces in zero-config mode");
506
+ }
473
507
  return snapshots;
474
508
  }
475
509
  function isRecoverableExtractionError(message) {
476
510
  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");
477
511
  }
512
+ function isSkippableWorkspaceExtractionFailure(message) {
513
+ if (!message) {
514
+ return true;
515
+ }
516
+ return isRecoverableExtractionError(message) || message.startsWith("Failed to load config") || message.startsWith("Failed to run eslint --print-config") || message.startsWith("Unable to resolve eslint from workspace");
517
+ }
478
518
  async function resolveWorkspaceAssignments(cwd, config) {
479
519
  const discovery = await (0, import_api2.discoverWorkspaces)({ cwd, workspaceInput: config.workspaceInput });
480
520
  const assignments = config.grouping.mode === "standalone" ? discovery.workspacesRel.map((workspace) => ({ name: workspace, workspaces: [workspace] })) : (0, import_api2.assignGroupsByMatch)(discovery.workspacesRel, config.grouping.groups ?? [{ name: "default", match: ["**/*"] }]);
@@ -544,6 +584,63 @@ async function resolveGroupEslintVersions(cwd) {
544
584
  return result;
545
585
  }
546
586
 
587
+ // src/commands/skipped-workspaces.ts
588
+ var import_node_path3 = __toESM(require("path"), 1);
589
+ function writeSkippedWorkspaceSummary(terminal, cwd, configPath, skippedWorkspaces) {
590
+ if (skippedWorkspaces.length === 0) {
591
+ return;
592
+ }
593
+ const workspacePaths = collectSkippedWorkspacePaths(skippedWorkspaces);
594
+ terminal.warning(
595
+ `Heads up: ${workspacePaths.length} workspace(s) were skipped because ESLint auto-discovery could not extract an effective config for them.
596
+ `
597
+ );
598
+ terminal.warning(`Skipped workspaces: ${workspacePaths.join(", ")}
599
+ `);
600
+ terminal.subtle(formatScopedConfigHint(cwd, configPath, workspacePaths));
601
+ }
602
+ function collectSkippedWorkspacePaths(skippedWorkspaces) {
603
+ const unique = /* @__PURE__ */ new Set();
604
+ for (const skipped of skippedWorkspaces) {
605
+ unique.add(skipped.workspaceRel);
606
+ }
607
+ return [...unique].sort();
608
+ }
609
+ function toExcludeGlobs(workspacePaths) {
610
+ return workspacePaths.map((workspacePath) => `${workspacePath}/**`);
611
+ }
612
+ function formatScopedConfigHint(cwd, configPath, workspacePaths) {
613
+ const excludeGlobs = toExcludeGlobs(workspacePaths);
614
+ if (configPath && import_node_path3.default.basename(configPath) === "package.json") {
615
+ return `Tip: if these workspaces are intentionally out of scope, add this under "eslint-config-snapshot" in package.json:
616
+ ${JSON.stringify(
617
+ {
618
+ sampling: {
619
+ excludeGlobs
620
+ }
621
+ },
622
+ null,
623
+ 2
624
+ )}
625
+ `;
626
+ }
627
+ const objectLiteral = `{
628
+ sampling: {
629
+ excludeGlobs: [
630
+ ${excludeGlobs.map((value) => ` '${value}'`).join(",\n")}
631
+ ]
632
+ }
633
+ }
634
+ `;
635
+ if (configPath) {
636
+ const relConfigPath = import_node_path3.default.relative(cwd, configPath) || import_node_path3.default.basename(configPath);
637
+ return `Tip: if these workspaces are intentionally out of scope, add this in ${relConfigPath}:
638
+ ${objectLiteral}`;
639
+ }
640
+ return `Tip: if these workspaces are intentionally out of scope, run \`eslint-config-snapshot init\` and add this config:
641
+ ${objectLiteral}`;
642
+ }
643
+
547
644
  // src/commands/check.ts
548
645
  var UPDATE_HINT = "Tip: when you intentionally accept changes, run `eslint-config-snapshot --update` to refresh the baseline.\n";
549
646
  async function executeCheck(cwd, format, terminal, snapshotDir, defaultInvocation = false) {
@@ -561,10 +658,20 @@ async function executeCheck(cwd, format, terminal, snapshotDir, defaultInvocatio
561
658
  );
562
659
  }
563
660
  let currentSnapshots;
661
+ const skippedWorkspaces = [];
662
+ let discoveredWorkspaces = [];
564
663
  try {
565
- currentSnapshots = await computeCurrentSnapshots(cwd);
664
+ currentSnapshots = await computeCurrentSnapshots(cwd, {
665
+ allowWorkspaceExtractionFailure: !foundConfig,
666
+ onWorkspacesDiscovered: (workspacesRel) => {
667
+ discoveredWorkspaces = workspacesRel;
668
+ },
669
+ onWorkspaceSkipped: (skipped) => {
670
+ skippedWorkspaces.push(skipped);
671
+ }
672
+ });
566
673
  } catch (error) {
567
- if (!foundConfig) {
674
+ if (!foundConfig && isWorkspaceDiscoveryDefaultsError(error)) {
568
675
  terminal.write(
569
676
  "Automatic workspace discovery could not complete with defaults.\nRun `eslint-config-snapshot init` to configure workspaces, then run `eslint-config-snapshot --update`.\n"
570
677
  );
@@ -572,6 +679,10 @@ async function executeCheck(cwd, format, terminal, snapshotDir, defaultInvocatio
572
679
  }
573
680
  throw error;
574
681
  }
682
+ if (!foundConfig) {
683
+ writeDiscoveredWorkspacesSummary(terminal, discoveredWorkspaces);
684
+ }
685
+ writeSkippedWorkspaceSummary(terminal, cwd, foundConfig?.path, skippedWorkspaces);
575
686
  if (storedSnapshots.size === 0) {
576
687
  const summary = summarizeSnapshots(currentSnapshots);
577
688
  terminal.write(
@@ -631,12 +742,7 @@ function printWhatChanged(terminal, changes, currentSnapshots, eslintVersionsByG
631
742
  if (changes.length === 0) {
632
743
  terminal.write(color.green("\u2705 Great news: no snapshot drift detected.\n"));
633
744
  terminal.section("\u{1F4CA} Summary");
634
- terminal.write(
635
- `- \u{1F4E6} baseline: ${currentSummary.groups} groups, ${currentSummary.rules} rules
636
- - \u{1F5C2}\uFE0F workspaces scanned: ${workspaceCount}
637
- - \u{1F39A}\uFE0F severity mix: ${currentSummary.error} errors, ${currentSummary.warn} warnings, ${currentSummary.off} off
638
- `
639
- );
745
+ terminal.write(formatBaselineSummaryLines(currentSummary, workspaceCount));
640
746
  writeEslintVersionSummary(terminal, eslintVersionsByGroup);
641
747
  return 0;
642
748
  }
@@ -671,6 +777,18 @@ function printWhatChanged(terminal, changes, currentSnapshots, eslintVersionsByG
671
777
  terminal.subtle(UPDATE_HINT);
672
778
  return 1;
673
779
  }
780
+ function isWorkspaceDiscoveryDefaultsError(error) {
781
+ const message = error instanceof Error ? error.message : String(error);
782
+ return message.includes("Unable to discover workspaces") || message.includes("Unmatched workspaces") || message.includes("zero-config mode");
783
+ }
784
+ function writeDiscoveredWorkspacesSummary(terminal, workspacesRel) {
785
+ if (workspacesRel.length === 0) {
786
+ terminal.subtle("Auto-discovered workspaces: none\n");
787
+ return;
788
+ }
789
+ terminal.subtle(`Auto-discovered workspaces (${workspacesRel.length}): ${workspacesRel.join(", ")}
790
+ `);
791
+ }
674
792
 
675
793
  // src/commands/print.ts
676
794
  var import_api4 = require("@eslint-config-snapshot/api");
@@ -736,10 +854,20 @@ async function executeUpdate(cwd, terminal, snapshotDir, printSummary) {
736
854
  );
737
855
  }
738
856
  let currentSnapshots;
857
+ const skippedWorkspaces = [];
858
+ let discoveredWorkspaces = [];
739
859
  try {
740
- currentSnapshots = await computeCurrentSnapshots(cwd);
860
+ currentSnapshots = await computeCurrentSnapshots(cwd, {
861
+ allowWorkspaceExtractionFailure: !foundConfig,
862
+ onWorkspacesDiscovered: (workspacesRel) => {
863
+ discoveredWorkspaces = workspacesRel;
864
+ },
865
+ onWorkspaceSkipped: (skipped) => {
866
+ skippedWorkspaces.push(skipped);
867
+ }
868
+ });
741
869
  } catch (error) {
742
- if (!foundConfig) {
870
+ if (!foundConfig && isWorkspaceDiscoveryDefaultsError2(error)) {
743
871
  terminal.write(
744
872
  "Automatic workspace discovery could not complete with defaults.\nRun `eslint-config-snapshot init` to configure workspaces, then run `eslint-config-snapshot --update`.\n"
745
873
  );
@@ -747,28 +875,42 @@ async function executeUpdate(cwd, terminal, snapshotDir, printSummary) {
747
875
  }
748
876
  throw error;
749
877
  }
878
+ if (!foundConfig) {
879
+ writeDiscoveredWorkspacesSummary2(terminal, discoveredWorkspaces);
880
+ }
881
+ writeSkippedWorkspaceSummary(terminal, cwd, foundConfig?.path, skippedWorkspaces);
750
882
  await writeSnapshots(cwd, snapshotDir, currentSnapshots);
751
883
  if (printSummary) {
752
884
  const summary = summarizeSnapshots(currentSnapshots);
753
885
  const workspaceCount = countUniqueWorkspaces(currentSnapshots);
754
886
  const eslintVersionsByGroup = terminal.showProgress ? await resolveGroupEslintVersions(cwd) : /* @__PURE__ */ new Map();
887
+ const baselineAction = storedSnapshots.size === 0 ? "created" : "updated";
888
+ terminal.success(`\u2705 Great news: baseline was successfully ${baselineAction} for your project.
889
+ `);
755
890
  terminal.section("\u{1F4CA} Summary");
756
- terminal.write(
757
- `Baseline updated: ${summary.groups} groups, ${summary.rules} rules.
758
- Workspaces scanned: ${workspaceCount}.
759
- Severity mix: ${summary.error} errors, ${summary.warn} warnings, ${summary.off} off.
760
- `
761
- );
891
+ terminal.write(formatBaselineSummaryLines(summary, workspaceCount));
762
892
  writeEslintVersionSummary(terminal, eslintVersionsByGroup);
763
893
  }
764
894
  return 0;
765
895
  }
896
+ function isWorkspaceDiscoveryDefaultsError2(error) {
897
+ const message = error instanceof Error ? error.message : String(error);
898
+ return message.includes("Unable to discover workspaces") || message.includes("Unmatched workspaces") || message.includes("zero-config mode");
899
+ }
900
+ function writeDiscoveredWorkspacesSummary2(terminal, workspacesRel) {
901
+ if (workspacesRel.length === 0) {
902
+ terminal.subtle("Auto-discovered workspaces: none\n");
903
+ return;
904
+ }
905
+ terminal.subtle(`Auto-discovered workspaces (${workspacesRel.length}): ${workspacesRel.join(", ")}
906
+ `);
907
+ }
766
908
 
767
909
  // src/init.ts
768
910
  var import_api6 = require("@eslint-config-snapshot/api");
769
911
  var import_fast_glob2 = __toESM(require("fast-glob"), 1);
770
912
  var import_promises2 = require("fs/promises");
771
- var import_node_path3 = __toESM(require("path"), 1);
913
+ var import_node_path4 = __toESM(require("path"), 1);
772
914
  async function runInit(cwd, opts, runtime) {
773
915
  const force = opts.force ?? false;
774
916
  const showEffective = opts.showEffective ?? false;
@@ -836,7 +978,7 @@ async function runInitInFile(cwd, configObject, force, runtime) {
836
978
  ];
837
979
  for (const candidate of candidates) {
838
980
  try {
839
- await (0, import_promises2.access)(import_node_path3.default.join(cwd, candidate));
981
+ await (0, import_promises2.access)(import_node_path4.default.join(cwd, candidate));
840
982
  if (!force) {
841
983
  runtime.writeStderr(`Config already exists: ${candidate}
842
984
  `);
@@ -845,14 +987,14 @@ async function runInitInFile(cwd, configObject, force, runtime) {
845
987
  } catch {
846
988
  }
847
989
  }
848
- const target = import_node_path3.default.join(cwd, "eslint-config-snapshot.config.mjs");
990
+ const target = import_node_path4.default.join(cwd, "eslint-config-snapshot.config.mjs");
849
991
  await (0, import_promises2.writeFile)(target, toConfigScaffold(configObject), "utf8");
850
- runtime.writeStdout(`Created ${import_node_path3.default.basename(target)}
992
+ runtime.writeStdout(`Created ${import_node_path4.default.basename(target)}
851
993
  `);
852
994
  return 0;
853
995
  }
854
996
  async function runInitInPackageJson(cwd, configObject, force, runtime) {
855
- const packageJsonPath = import_node_path3.default.join(cwd, "package.json");
997
+ const packageJsonPath = import_node_path4.default.join(cwd, "package.json");
856
998
  let packageJsonRaw;
857
999
  try {
858
1000
  packageJsonRaw = await (0, import_promises2.readFile)(packageJsonPath, "utf8");
@@ -913,7 +1055,7 @@ async function discoverInitWorkspaces(cwd) {
913
1055
  if (!(discovered.workspacesRel.length === 1 && discovered.workspacesRel[0] === ".")) {
914
1056
  return discovered.workspacesRel;
915
1057
  }
916
- const packageJsonPath = import_node_path3.default.join(cwd, "package.json");
1058
+ const packageJsonPath = import_node_path4.default.join(cwd, "package.json");
917
1059
  try {
918
1060
  const raw = await (0, import_promises2.readFile)(packageJsonPath, "utf8");
919
1061
  const parsed = JSON.parse(raw);
@@ -931,7 +1073,7 @@ async function discoverInitWorkspaces(cwd) {
931
1073
  onlyFiles: true,
932
1074
  dot: true
933
1075
  });
934
- const workspaceDirs = [...new Set(workspacePackageFiles.map((entry) => (0, import_api6.normalizePath)(import_node_path3.default.dirname(entry))))].sort(
1076
+ const workspaceDirs = [...new Set(workspacePackageFiles.map((entry) => (0, import_api6.normalizePath)(import_node_path4.default.dirname(entry))))].sort(
935
1077
  (a, b) => a.localeCompare(b)
936
1078
  );
937
1079
  if (workspaceDirs.length > 0) {
@@ -1342,7 +1484,7 @@ function isDirectCliExecution() {
1342
1484
  if (!entry) {
1343
1485
  return false;
1344
1486
  }
1345
- const normalized = import_node_path4.default.basename(entry).toLowerCase();
1487
+ const normalized = import_node_path5.default.basename(entry).toLowerCase();
1346
1488
  return normalized === "index.js" || normalized === "index.cjs" || normalized === "index.ts" || normalized === "eslint-config-snapshot";
1347
1489
  }
1348
1490
  if (isDirectCliExecution()) {
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // src/index.ts
4
4
  import { Command, CommanderError, InvalidArgumentError } from "commander";
5
5
  import createDebug2 from "debug";
6
- import path4 from "path";
6
+ import path5 from "path";
7
7
 
8
8
  // src/commands/check.ts
9
9
  import { findConfigPath } from "@eslint-config-snapshot/api";
@@ -181,6 +181,12 @@ function formatStoredSnapshotSummary(storedSnapshots) {
181
181
  const summary = summarizeSnapshots(storedSnapshots);
182
182
  return `${summary.groups} groups, ${summary.rules} rules (severity mix: ${summary.error} errors, ${summary.warn} warnings, ${summary.off} off)`;
183
183
  }
184
+ function formatBaselineSummaryLines(summary, workspaceCount) {
185
+ return `- \u{1F4E6} baseline: ${summary.groups} groups, ${summary.rules} rules
186
+ - \u{1F5C2}\uFE0F workspaces scanned: ${workspaceCount}
187
+ - \u{1F39A}\uFE0F severity mix: ${summary.error} errors, ${summary.warn} warnings, ${summary.off} off
188
+ `;
189
+ }
184
190
  function countRuleSeverities(ruleObjects) {
185
191
  let rules = 0;
186
192
  let error = 0;
@@ -373,19 +379,24 @@ import path2 from "path";
373
379
  var debugWorkspace = createDebug("eslint-config-snapshot:workspace");
374
380
  var debugDiff = createDebug("eslint-config-snapshot:diff");
375
381
  var debugTiming = createDebug("eslint-config-snapshot:timing");
376
- async function computeCurrentSnapshots(cwd) {
382
+ async function computeCurrentSnapshots(cwd, options) {
383
+ const allowWorkspaceExtractionFailure = options?.allowWorkspaceExtractionFailure ?? false;
384
+ const onWorkspacesDiscovered = options?.onWorkspacesDiscovered;
385
+ const onWorkspaceSkipped = options?.onWorkspaceSkipped;
377
386
  const computeStartedAt = Date.now();
378
387
  const configStartedAt = Date.now();
379
388
  const config = await loadConfig(cwd);
380
389
  debugTiming("phase=loadConfig elapsedMs=%d", Date.now() - configStartedAt);
381
390
  const assignmentStartedAt = Date.now();
382
391
  const { discovery, assignments } = await resolveWorkspaceAssignments(cwd, config);
392
+ onWorkspacesDiscovered?.(discovery.workspacesRel);
383
393
  debugTiming("phase=resolveWorkspaceAssignments elapsedMs=%d", Date.now() - assignmentStartedAt);
384
394
  debugWorkspace("root=%s groups=%d workspaces=%d", discovery.rootAbs, assignments.length, discovery.workspacesRel.length);
385
395
  const snapshots = /* @__PURE__ */ new Map();
386
396
  for (const group of assignments) {
387
397
  const groupStartedAt = Date.now();
388
398
  const extractedForGroup = [];
399
+ const extractedWorkspaces = [];
389
400
  debugWorkspace("group=%s workspaces=%o", group.name, group.workspaces);
390
401
  for (const workspaceRel of group.workspaces) {
391
402
  const workspaceAbs = path2.resolve(discovery.rootAbs, workspaceRel);
@@ -418,7 +429,7 @@ async function computeCurrentSnapshots(cwd) {
418
429
  continue;
419
430
  }
420
431
  const message = result.error instanceof Error ? result.error.message : String(result.error);
421
- if (isRecoverableExtractionError(message)) {
432
+ if (isRecoverableExtractionError(message) || allowWorkspaceExtractionFailure) {
422
433
  lastExtractionError = message;
423
434
  continue;
424
435
  }
@@ -426,10 +437,23 @@ async function computeCurrentSnapshots(cwd) {
426
437
  }
427
438
  if (extractedCount === 0) {
428
439
  const context = lastExtractionError ? ` Last error: ${lastExtractionError}` : "";
429
- throw new Error(
430
- `Unable to extract ESLint config for workspace ${workspaceRel}. All sampled files were ignored or produced non-JSON output.${context}`
431
- );
440
+ if (allowWorkspaceExtractionFailure && isSkippableWorkspaceExtractionFailure(lastExtractionError)) {
441
+ onWorkspaceSkipped?.({
442
+ groupId: group.name,
443
+ workspaceRel,
444
+ reason: lastExtractionError ?? "unknown extraction failure"
445
+ });
446
+ debugWorkspace(
447
+ "group=%s workspace=%s skipped=true reason=%s",
448
+ group.name,
449
+ workspaceRel,
450
+ lastExtractionError ?? "unknown extraction failure"
451
+ );
452
+ continue;
453
+ }
454
+ throw new Error(`Unable to extract ESLint config for workspace ${workspaceRel}.${context}`);
432
455
  }
456
+ extractedWorkspaces.push(workspaceRel);
433
457
  debugWorkspace(
434
458
  "group=%s workspace=%s extracted=%d failed=%d",
435
459
  group.name,
@@ -438,8 +462,15 @@ async function computeCurrentSnapshots(cwd) {
438
462
  results.length - extractedCount
439
463
  );
440
464
  }
465
+ if (extractedForGroup.length === 0) {
466
+ if (allowWorkspaceExtractionFailure) {
467
+ debugWorkspace("group=%s skipped=true reason=no-extracted-workspaces", group.name);
468
+ continue;
469
+ }
470
+ throw new Error(`Unable to extract ESLint config for group ${group.name}: no workspace produced a valid config`);
471
+ }
441
472
  const aggregated = aggregateRules(extractedForGroup);
442
- snapshots.set(group.name, buildSnapshot(group.name, group.workspaces, aggregated));
473
+ snapshots.set(group.name, buildSnapshot(group.name, extractedWorkspaces, aggregated));
443
474
  debugWorkspace(
444
475
  "group=%s aggregatedRules=%d groupElapsedMs=%d",
445
476
  group.name,
@@ -448,11 +479,20 @@ async function computeCurrentSnapshots(cwd) {
448
479
  );
449
480
  }
450
481
  debugTiming("phase=computeCurrentSnapshots elapsedMs=%d", Date.now() - computeStartedAt);
482
+ if (snapshots.size === 0) {
483
+ throw new Error("Unable to extract ESLint config from discovered workspaces in zero-config mode");
484
+ }
451
485
  return snapshots;
452
486
  }
453
487
  function isRecoverableExtractionError(message) {
454
488
  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");
455
489
  }
490
+ function isSkippableWorkspaceExtractionFailure(message) {
491
+ if (!message) {
492
+ return true;
493
+ }
494
+ return isRecoverableExtractionError(message) || message.startsWith("Failed to load config") || message.startsWith("Failed to run eslint --print-config") || message.startsWith("Unable to resolve eslint from workspace");
495
+ }
456
496
  async function resolveWorkspaceAssignments(cwd, config) {
457
497
  const discovery = await discoverWorkspaces({ cwd, workspaceInput: config.workspaceInput });
458
498
  const assignments = config.grouping.mode === "standalone" ? discovery.workspacesRel.map((workspace) => ({ name: workspace, workspaces: [workspace] })) : assignGroupsByMatch(discovery.workspacesRel, config.grouping.groups ?? [{ name: "default", match: ["**/*"] }]);
@@ -522,6 +562,63 @@ async function resolveGroupEslintVersions(cwd) {
522
562
  return result;
523
563
  }
524
564
 
565
+ // src/commands/skipped-workspaces.ts
566
+ import path3 from "path";
567
+ function writeSkippedWorkspaceSummary(terminal, cwd, configPath, skippedWorkspaces) {
568
+ if (skippedWorkspaces.length === 0) {
569
+ return;
570
+ }
571
+ const workspacePaths = collectSkippedWorkspacePaths(skippedWorkspaces);
572
+ terminal.warning(
573
+ `Heads up: ${workspacePaths.length} workspace(s) were skipped because ESLint auto-discovery could not extract an effective config for them.
574
+ `
575
+ );
576
+ terminal.warning(`Skipped workspaces: ${workspacePaths.join(", ")}
577
+ `);
578
+ terminal.subtle(formatScopedConfigHint(cwd, configPath, workspacePaths));
579
+ }
580
+ function collectSkippedWorkspacePaths(skippedWorkspaces) {
581
+ const unique = /* @__PURE__ */ new Set();
582
+ for (const skipped of skippedWorkspaces) {
583
+ unique.add(skipped.workspaceRel);
584
+ }
585
+ return [...unique].sort();
586
+ }
587
+ function toExcludeGlobs(workspacePaths) {
588
+ return workspacePaths.map((workspacePath) => `${workspacePath}/**`);
589
+ }
590
+ function formatScopedConfigHint(cwd, configPath, workspacePaths) {
591
+ const excludeGlobs = toExcludeGlobs(workspacePaths);
592
+ if (configPath && path3.basename(configPath) === "package.json") {
593
+ return `Tip: if these workspaces are intentionally out of scope, add this under "eslint-config-snapshot" in package.json:
594
+ ${JSON.stringify(
595
+ {
596
+ sampling: {
597
+ excludeGlobs
598
+ }
599
+ },
600
+ null,
601
+ 2
602
+ )}
603
+ `;
604
+ }
605
+ const objectLiteral = `{
606
+ sampling: {
607
+ excludeGlobs: [
608
+ ${excludeGlobs.map((value) => ` '${value}'`).join(",\n")}
609
+ ]
610
+ }
611
+ }
612
+ `;
613
+ if (configPath) {
614
+ const relConfigPath = path3.relative(cwd, configPath) || path3.basename(configPath);
615
+ return `Tip: if these workspaces are intentionally out of scope, add this in ${relConfigPath}:
616
+ ${objectLiteral}`;
617
+ }
618
+ return `Tip: if these workspaces are intentionally out of scope, run \`eslint-config-snapshot init\` and add this config:
619
+ ${objectLiteral}`;
620
+ }
621
+
525
622
  // src/commands/check.ts
526
623
  var UPDATE_HINT = "Tip: when you intentionally accept changes, run `eslint-config-snapshot --update` to refresh the baseline.\n";
527
624
  async function executeCheck(cwd, format, terminal, snapshotDir, defaultInvocation = false) {
@@ -539,10 +636,20 @@ async function executeCheck(cwd, format, terminal, snapshotDir, defaultInvocatio
539
636
  );
540
637
  }
541
638
  let currentSnapshots;
639
+ const skippedWorkspaces = [];
640
+ let discoveredWorkspaces = [];
542
641
  try {
543
- currentSnapshots = await computeCurrentSnapshots(cwd);
642
+ currentSnapshots = await computeCurrentSnapshots(cwd, {
643
+ allowWorkspaceExtractionFailure: !foundConfig,
644
+ onWorkspacesDiscovered: (workspacesRel) => {
645
+ discoveredWorkspaces = workspacesRel;
646
+ },
647
+ onWorkspaceSkipped: (skipped) => {
648
+ skippedWorkspaces.push(skipped);
649
+ }
650
+ });
544
651
  } catch (error) {
545
- if (!foundConfig) {
652
+ if (!foundConfig && isWorkspaceDiscoveryDefaultsError(error)) {
546
653
  terminal.write(
547
654
  "Automatic workspace discovery could not complete with defaults.\nRun `eslint-config-snapshot init` to configure workspaces, then run `eslint-config-snapshot --update`.\n"
548
655
  );
@@ -550,6 +657,10 @@ async function executeCheck(cwd, format, terminal, snapshotDir, defaultInvocatio
550
657
  }
551
658
  throw error;
552
659
  }
660
+ if (!foundConfig) {
661
+ writeDiscoveredWorkspacesSummary(terminal, discoveredWorkspaces);
662
+ }
663
+ writeSkippedWorkspaceSummary(terminal, cwd, foundConfig?.path, skippedWorkspaces);
553
664
  if (storedSnapshots.size === 0) {
554
665
  const summary = summarizeSnapshots(currentSnapshots);
555
666
  terminal.write(
@@ -609,12 +720,7 @@ function printWhatChanged(terminal, changes, currentSnapshots, eslintVersionsByG
609
720
  if (changes.length === 0) {
610
721
  terminal.write(color.green("\u2705 Great news: no snapshot drift detected.\n"));
611
722
  terminal.section("\u{1F4CA} Summary");
612
- terminal.write(
613
- `- \u{1F4E6} baseline: ${currentSummary.groups} groups, ${currentSummary.rules} rules
614
- - \u{1F5C2}\uFE0F workspaces scanned: ${workspaceCount}
615
- - \u{1F39A}\uFE0F severity mix: ${currentSummary.error} errors, ${currentSummary.warn} warnings, ${currentSummary.off} off
616
- `
617
- );
723
+ terminal.write(formatBaselineSummaryLines(currentSummary, workspaceCount));
618
724
  writeEslintVersionSummary(terminal, eslintVersionsByGroup);
619
725
  return 0;
620
726
  }
@@ -649,6 +755,18 @@ function printWhatChanged(terminal, changes, currentSnapshots, eslintVersionsByG
649
755
  terminal.subtle(UPDATE_HINT);
650
756
  return 1;
651
757
  }
758
+ function isWorkspaceDiscoveryDefaultsError(error) {
759
+ const message = error instanceof Error ? error.message : String(error);
760
+ return message.includes("Unable to discover workspaces") || message.includes("Unmatched workspaces") || message.includes("zero-config mode");
761
+ }
762
+ function writeDiscoveredWorkspacesSummary(terminal, workspacesRel) {
763
+ if (workspacesRel.length === 0) {
764
+ terminal.subtle("Auto-discovered workspaces: none\n");
765
+ return;
766
+ }
767
+ terminal.subtle(`Auto-discovered workspaces (${workspacesRel.length}): ${workspacesRel.join(", ")}
768
+ `);
769
+ }
652
770
 
653
771
  // src/commands/print.ts
654
772
  import { findConfigPath as findConfigPath2, loadConfig as loadConfig2 } from "@eslint-config-snapshot/api";
@@ -714,10 +832,20 @@ async function executeUpdate(cwd, terminal, snapshotDir, printSummary) {
714
832
  );
715
833
  }
716
834
  let currentSnapshots;
835
+ const skippedWorkspaces = [];
836
+ let discoveredWorkspaces = [];
717
837
  try {
718
- currentSnapshots = await computeCurrentSnapshots(cwd);
838
+ currentSnapshots = await computeCurrentSnapshots(cwd, {
839
+ allowWorkspaceExtractionFailure: !foundConfig,
840
+ onWorkspacesDiscovered: (workspacesRel) => {
841
+ discoveredWorkspaces = workspacesRel;
842
+ },
843
+ onWorkspaceSkipped: (skipped) => {
844
+ skippedWorkspaces.push(skipped);
845
+ }
846
+ });
719
847
  } catch (error) {
720
- if (!foundConfig) {
848
+ if (!foundConfig && isWorkspaceDiscoveryDefaultsError2(error)) {
721
849
  terminal.write(
722
850
  "Automatic workspace discovery could not complete with defaults.\nRun `eslint-config-snapshot init` to configure workspaces, then run `eslint-config-snapshot --update`.\n"
723
851
  );
@@ -725,28 +853,42 @@ async function executeUpdate(cwd, terminal, snapshotDir, printSummary) {
725
853
  }
726
854
  throw error;
727
855
  }
856
+ if (!foundConfig) {
857
+ writeDiscoveredWorkspacesSummary2(terminal, discoveredWorkspaces);
858
+ }
859
+ writeSkippedWorkspaceSummary(terminal, cwd, foundConfig?.path, skippedWorkspaces);
728
860
  await writeSnapshots(cwd, snapshotDir, currentSnapshots);
729
861
  if (printSummary) {
730
862
  const summary = summarizeSnapshots(currentSnapshots);
731
863
  const workspaceCount = countUniqueWorkspaces(currentSnapshots);
732
864
  const eslintVersionsByGroup = terminal.showProgress ? await resolveGroupEslintVersions(cwd) : /* @__PURE__ */ new Map();
865
+ const baselineAction = storedSnapshots.size === 0 ? "created" : "updated";
866
+ terminal.success(`\u2705 Great news: baseline was successfully ${baselineAction} for your project.
867
+ `);
733
868
  terminal.section("\u{1F4CA} Summary");
734
- terminal.write(
735
- `Baseline updated: ${summary.groups} groups, ${summary.rules} rules.
736
- Workspaces scanned: ${workspaceCount}.
737
- Severity mix: ${summary.error} errors, ${summary.warn} warnings, ${summary.off} off.
738
- `
739
- );
869
+ terminal.write(formatBaselineSummaryLines(summary, workspaceCount));
740
870
  writeEslintVersionSummary(terminal, eslintVersionsByGroup);
741
871
  }
742
872
  return 0;
743
873
  }
874
+ function isWorkspaceDiscoveryDefaultsError2(error) {
875
+ const message = error instanceof Error ? error.message : String(error);
876
+ return message.includes("Unable to discover workspaces") || message.includes("Unmatched workspaces") || message.includes("zero-config mode");
877
+ }
878
+ function writeDiscoveredWorkspacesSummary2(terminal, workspacesRel) {
879
+ if (workspacesRel.length === 0) {
880
+ terminal.subtle("Auto-discovered workspaces: none\n");
881
+ return;
882
+ }
883
+ terminal.subtle(`Auto-discovered workspaces (${workspacesRel.length}): ${workspacesRel.join(", ")}
884
+ `);
885
+ }
744
886
 
745
887
  // src/init.ts
746
888
  import { discoverWorkspaces as discoverWorkspaces2, findConfigPath as findConfigPath4, getConfigScaffold, normalizePath as normalizePath2 } from "@eslint-config-snapshot/api";
747
889
  import fg2 from "fast-glob";
748
890
  import { access, readFile, writeFile } from "fs/promises";
749
- import path3 from "path";
891
+ import path4 from "path";
750
892
  async function runInit(cwd, opts, runtime) {
751
893
  const force = opts.force ?? false;
752
894
  const showEffective = opts.showEffective ?? false;
@@ -814,7 +956,7 @@ async function runInitInFile(cwd, configObject, force, runtime) {
814
956
  ];
815
957
  for (const candidate of candidates) {
816
958
  try {
817
- await access(path3.join(cwd, candidate));
959
+ await access(path4.join(cwd, candidate));
818
960
  if (!force) {
819
961
  runtime.writeStderr(`Config already exists: ${candidate}
820
962
  `);
@@ -823,14 +965,14 @@ async function runInitInFile(cwd, configObject, force, runtime) {
823
965
  } catch {
824
966
  }
825
967
  }
826
- const target = path3.join(cwd, "eslint-config-snapshot.config.mjs");
968
+ const target = path4.join(cwd, "eslint-config-snapshot.config.mjs");
827
969
  await writeFile(target, toConfigScaffold(configObject), "utf8");
828
- runtime.writeStdout(`Created ${path3.basename(target)}
970
+ runtime.writeStdout(`Created ${path4.basename(target)}
829
971
  `);
830
972
  return 0;
831
973
  }
832
974
  async function runInitInPackageJson(cwd, configObject, force, runtime) {
833
- const packageJsonPath = path3.join(cwd, "package.json");
975
+ const packageJsonPath = path4.join(cwd, "package.json");
834
976
  let packageJsonRaw;
835
977
  try {
836
978
  packageJsonRaw = await readFile(packageJsonPath, "utf8");
@@ -891,7 +1033,7 @@ async function discoverInitWorkspaces(cwd) {
891
1033
  if (!(discovered.workspacesRel.length === 1 && discovered.workspacesRel[0] === ".")) {
892
1034
  return discovered.workspacesRel;
893
1035
  }
894
- const packageJsonPath = path3.join(cwd, "package.json");
1036
+ const packageJsonPath = path4.join(cwd, "package.json");
895
1037
  try {
896
1038
  const raw = await readFile(packageJsonPath, "utf8");
897
1039
  const parsed = JSON.parse(raw);
@@ -909,7 +1051,7 @@ async function discoverInitWorkspaces(cwd) {
909
1051
  onlyFiles: true,
910
1052
  dot: true
911
1053
  });
912
- const workspaceDirs = [...new Set(workspacePackageFiles.map((entry) => normalizePath2(path3.dirname(entry))))].sort(
1054
+ const workspaceDirs = [...new Set(workspacePackageFiles.map((entry) => normalizePath2(path4.dirname(entry))))].sort(
913
1055
  (a, b) => a.localeCompare(b)
914
1056
  );
915
1057
  if (workspaceDirs.length > 0) {
@@ -1320,7 +1462,7 @@ function isDirectCliExecution() {
1320
1462
  if (!entry) {
1321
1463
  return false;
1322
1464
  }
1323
- const normalized = path4.basename(entry).toLowerCase();
1465
+ const normalized = path5.basename(entry).toLowerCase();
1324
1466
  return normalized === "index.js" || normalized === "index.cjs" || normalized === "index.ts" || normalized === "eslint-config-snapshot";
1325
1467
  }
1326
1468
  if (isDirectCliExecution()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eslint-config-snapshot/cli",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -31,6 +31,6 @@
31
31
  "commander": "^14.0.3",
32
32
  "debug": "^4.4.3",
33
33
  "fast-glob": "^3.3.3",
34
- "@eslint-config-snapshot/api": "1.0.0"
34
+ "@eslint-config-snapshot/api": "1.1.0"
35
35
  }
36
36
  }
@@ -1,6 +1,13 @@
1
1
  import { findConfigPath } from '@eslint-config-snapshot/api'
2
2
 
3
- import { countUniqueWorkspaces, decorateDiffLine, formatDiff, summarizeChanges, summarizeSnapshots } from '../formatters.js'
3
+ import {
4
+ countUniqueWorkspaces,
5
+ decorateDiffLine,
6
+ formatBaselineSummaryLines,
7
+ formatDiff,
8
+ summarizeChanges,
9
+ summarizeSnapshots
10
+ } from '../formatters.js'
4
11
  import { writeEslintVersionSummary, writeRunContextHeader } from '../run-context.js'
5
12
  import {
6
13
  type BuiltSnapshot,
@@ -9,10 +16,12 @@ import {
9
16
  type GroupEslintVersions,
10
17
  loadStoredSnapshots,
11
18
  resolveGroupEslintVersions,
19
+ type SkippedWorkspace,
12
20
  type SnapshotDiff,
13
21
  writeSnapshots
14
22
  } from '../runtime.js'
15
23
  import { type TerminalIO } from '../terminal.js'
24
+ import { writeSkippedWorkspaceSummary } from './skipped-workspaces.js'
16
25
 
17
26
  export type CheckFormat = 'summary' | 'status' | 'diff'
18
27
 
@@ -42,10 +51,20 @@ export async function executeCheck(
42
51
  }
43
52
 
44
53
  let currentSnapshots: Map<string, BuiltSnapshot>
54
+ const skippedWorkspaces: SkippedWorkspace[] = []
55
+ let discoveredWorkspaces: string[] = []
45
56
  try {
46
- currentSnapshots = await computeCurrentSnapshots(cwd)
57
+ currentSnapshots = await computeCurrentSnapshots(cwd, {
58
+ allowWorkspaceExtractionFailure: !foundConfig,
59
+ onWorkspacesDiscovered: (workspacesRel) => {
60
+ discoveredWorkspaces = workspacesRel
61
+ },
62
+ onWorkspaceSkipped: (skipped) => {
63
+ skippedWorkspaces.push(skipped)
64
+ }
65
+ })
47
66
  } catch (error: unknown) {
48
- if (!foundConfig) {
67
+ if (!foundConfig && isWorkspaceDiscoveryDefaultsError(error)) {
49
68
  terminal.write(
50
69
  'Automatic workspace discovery could not complete with defaults.\nRun `eslint-config-snapshot init` to configure workspaces, then run `eslint-config-snapshot --update`.\n'
51
70
  )
@@ -54,6 +73,10 @@ export async function executeCheck(
54
73
 
55
74
  throw error
56
75
  }
76
+ if (!foundConfig) {
77
+ writeDiscoveredWorkspacesSummary(terminal, discoveredWorkspaces)
78
+ }
79
+ writeSkippedWorkspaceSummary(terminal, cwd, foundConfig?.path, skippedWorkspaces)
57
80
  if (storedSnapshots.size === 0) {
58
81
  const summary = summarizeSnapshots(currentSnapshots)
59
82
  terminal.write(
@@ -126,9 +149,7 @@ function printWhatChanged(
126
149
  if (changes.length === 0) {
127
150
  terminal.write(color.green('✅ Great news: no snapshot drift detected.\n'))
128
151
  terminal.section('📊 Summary')
129
- terminal.write(
130
- `- 📦 baseline: ${currentSummary.groups} groups, ${currentSummary.rules} rules\n- 🗂️ workspaces scanned: ${workspaceCount}\n- 🎚️ severity mix: ${currentSummary.error} errors, ${currentSummary.warn} warnings, ${currentSummary.off} off\n`
131
- )
152
+ terminal.write(formatBaselineSummaryLines(currentSummary, workspaceCount))
132
153
  writeEslintVersionSummary(terminal, eslintVersionsByGroup)
133
154
  return 0
134
155
  }
@@ -155,3 +176,21 @@ function printWhatChanged(
155
176
 
156
177
  return 1
157
178
  }
179
+
180
+ function isWorkspaceDiscoveryDefaultsError(error: unknown): boolean {
181
+ const message = error instanceof Error ? error.message : String(error)
182
+ return (
183
+ message.includes('Unable to discover workspaces') ||
184
+ message.includes('Unmatched workspaces') ||
185
+ message.includes('zero-config mode')
186
+ )
187
+ }
188
+
189
+ function writeDiscoveredWorkspacesSummary(terminal: TerminalIO, workspacesRel: string[]): void {
190
+ if (workspacesRel.length === 0) {
191
+ terminal.subtle('Auto-discovered workspaces: none\n')
192
+ return
193
+ }
194
+
195
+ terminal.subtle(`Auto-discovered workspaces (${workspacesRel.length}): ${workspacesRel.join(', ')}\n`)
196
+ }
@@ -0,0 +1,66 @@
1
+ import path from 'node:path'
2
+
3
+ import { type SkippedWorkspace } from '../runtime.js'
4
+ import { type TerminalIO } from '../terminal.js'
5
+
6
+ export function writeSkippedWorkspaceSummary(
7
+ terminal: TerminalIO,
8
+ cwd: string,
9
+ configPath: string | undefined,
10
+ skippedWorkspaces: SkippedWorkspace[]
11
+ ): void {
12
+ if (skippedWorkspaces.length === 0) {
13
+ return
14
+ }
15
+
16
+ const workspacePaths = collectSkippedWorkspacePaths(skippedWorkspaces)
17
+ terminal.warning(
18
+ `Heads up: ${workspacePaths.length} workspace(s) were skipped because ESLint auto-discovery could not extract an effective config for them.\n`
19
+ )
20
+ terminal.warning(`Skipped workspaces: ${workspacePaths.join(', ')}\n`)
21
+ terminal.subtle(formatScopedConfigHint(cwd, configPath, workspacePaths))
22
+ }
23
+
24
+ function collectSkippedWorkspacePaths(skippedWorkspaces: SkippedWorkspace[]): string[] {
25
+ const unique = new Set<string>()
26
+ for (const skipped of skippedWorkspaces) {
27
+ unique.add(skipped.workspaceRel)
28
+ }
29
+
30
+ return [...unique].sort()
31
+ }
32
+
33
+ function toExcludeGlobs(workspacePaths: string[]): string[] {
34
+ return workspacePaths.map((workspacePath) => `${workspacePath}/**`)
35
+ }
36
+
37
+ function formatScopedConfigHint(cwd: string, configPath: string | undefined, workspacePaths: string[]): string {
38
+ const excludeGlobs = toExcludeGlobs(workspacePaths)
39
+
40
+ if (configPath && path.basename(configPath) === 'package.json') {
41
+ return `Tip: if these workspaces are intentionally out of scope, add this under "eslint-config-snapshot" in package.json:\n${JSON.stringify(
42
+ {
43
+ sampling: {
44
+ excludeGlobs
45
+ }
46
+ },
47
+ null,
48
+ 2
49
+ )}\n`
50
+ }
51
+
52
+ const objectLiteral = `{
53
+ sampling: {
54
+ excludeGlobs: [
55
+ ${excludeGlobs.map((value) => ` '${value}'`).join(',\n')}
56
+ ]
57
+ }
58
+ }\n`
59
+
60
+ if (configPath) {
61
+ const relConfigPath = path.relative(cwd, configPath) || path.basename(configPath)
62
+ return `Tip: if these workspaces are intentionally out of scope, add this in ${relConfigPath}:\n${objectLiteral}`
63
+ }
64
+
65
+ return `Tip: if these workspaces are intentionally out of scope, run \`eslint-config-snapshot init\` and add this config:\n${objectLiteral}`
66
+ }
@@ -1,9 +1,10 @@
1
1
  import { findConfigPath } from '@eslint-config-snapshot/api'
2
2
 
3
- import { countUniqueWorkspaces, summarizeSnapshots } from '../formatters.js'
3
+ import { countUniqueWorkspaces, formatBaselineSummaryLines, summarizeSnapshots } from '../formatters.js'
4
4
  import { writeEslintVersionSummary, writeRunContextHeader } from '../run-context.js'
5
- import { computeCurrentSnapshots, loadStoredSnapshots, resolveGroupEslintVersions, writeSnapshots } from '../runtime.js'
5
+ import { computeCurrentSnapshots, loadStoredSnapshots, resolveGroupEslintVersions, type SkippedWorkspace, writeSnapshots } from '../runtime.js'
6
6
  import { type TerminalIO } from '../terminal.js'
7
+ import { writeSkippedWorkspaceSummary } from './skipped-workspaces.js'
7
8
 
8
9
  export async function executeUpdate(cwd: string, terminal: TerminalIO, snapshotDir: string, printSummary: boolean): Promise<number> {
9
10
  const foundConfig = await findConfigPath(cwd)
@@ -20,10 +21,20 @@ export async function executeUpdate(cwd: string, terminal: TerminalIO, snapshotD
20
21
  }
21
22
 
22
23
  let currentSnapshots
24
+ const skippedWorkspaces: SkippedWorkspace[] = []
25
+ let discoveredWorkspaces: string[] = []
23
26
  try {
24
- currentSnapshots = await computeCurrentSnapshots(cwd)
27
+ currentSnapshots = await computeCurrentSnapshots(cwd, {
28
+ allowWorkspaceExtractionFailure: !foundConfig,
29
+ onWorkspacesDiscovered: (workspacesRel) => {
30
+ discoveredWorkspaces = workspacesRel
31
+ },
32
+ onWorkspaceSkipped: (skipped) => {
33
+ skippedWorkspaces.push(skipped)
34
+ }
35
+ })
25
36
  } catch (error: unknown) {
26
- if (!foundConfig) {
37
+ if (!foundConfig && isWorkspaceDiscoveryDefaultsError(error)) {
27
38
  terminal.write(
28
39
  'Automatic workspace discovery could not complete with defaults.\nRun `eslint-config-snapshot init` to configure workspaces, then run `eslint-config-snapshot --update`.\n'
29
40
  )
@@ -32,18 +43,40 @@ export async function executeUpdate(cwd: string, terminal: TerminalIO, snapshotD
32
43
 
33
44
  throw error
34
45
  }
46
+ if (!foundConfig) {
47
+ writeDiscoveredWorkspacesSummary(terminal, discoveredWorkspaces)
48
+ }
49
+ writeSkippedWorkspaceSummary(terminal, cwd, foundConfig?.path, skippedWorkspaces)
35
50
  await writeSnapshots(cwd, snapshotDir, currentSnapshots)
36
51
 
37
52
  if (printSummary) {
38
53
  const summary = summarizeSnapshots(currentSnapshots)
39
54
  const workspaceCount = countUniqueWorkspaces(currentSnapshots)
40
55
  const eslintVersionsByGroup = terminal.showProgress ? await resolveGroupEslintVersions(cwd) : new Map<string, string[]>()
56
+ const baselineAction = storedSnapshots.size === 0 ? 'created' : 'updated'
57
+ terminal.success(`✅ Great news: baseline was successfully ${baselineAction} for your project.\n`)
41
58
  terminal.section('📊 Summary')
42
- terminal.write(
43
- `Baseline updated: ${summary.groups} groups, ${summary.rules} rules.\nWorkspaces scanned: ${workspaceCount}.\nSeverity mix: ${summary.error} errors, ${summary.warn} warnings, ${summary.off} off.\n`
44
- )
59
+ terminal.write(formatBaselineSummaryLines(summary, workspaceCount))
45
60
  writeEslintVersionSummary(terminal, eslintVersionsByGroup)
46
61
  }
47
62
 
48
63
  return 0
49
64
  }
65
+
66
+ function isWorkspaceDiscoveryDefaultsError(error: unknown): boolean {
67
+ const message = error instanceof Error ? error.message : String(error)
68
+ return (
69
+ message.includes('Unable to discover workspaces') ||
70
+ message.includes('Unmatched workspaces') ||
71
+ message.includes('zero-config mode')
72
+ )
73
+ }
74
+
75
+ function writeDiscoveredWorkspacesSummary(terminal: TerminalIO, workspacesRel: string[]): void {
76
+ if (workspacesRel.length === 0) {
77
+ terminal.subtle('Auto-discovered workspaces: none\n')
78
+ return
79
+ }
80
+
81
+ terminal.subtle(`Auto-discovered workspaces (${workspacesRel.length}): ${workspacesRel.join(', ')}\n`)
82
+ }
package/src/formatters.ts CHANGED
@@ -213,6 +213,13 @@ export function formatStoredSnapshotSummary(storedSnapshots: Map<string, Snapsho
213
213
  return `${summary.groups} groups, ${summary.rules} rules (severity mix: ${summary.error} errors, ${summary.warn} warnings, ${summary.off} off)`
214
214
  }
215
215
 
216
+ export function formatBaselineSummaryLines(
217
+ summary: { groups: number; rules: number; error: number; warn: number; off: number },
218
+ workspaceCount: number
219
+ ): string {
220
+ return `- 📦 baseline: ${summary.groups} groups, ${summary.rules} rules\n- 🗂️ workspaces scanned: ${workspaceCount}\n- 🎚️ severity mix: ${summary.error} errors, ${summary.warn} warnings, ${summary.off} off\n`
221
+ }
222
+
216
223
  export function countRuleSeverities(ruleObjects: RuleObject[]) {
217
224
  let rules = 0
218
225
  let error = 0
package/src/runtime.ts CHANGED
@@ -32,8 +32,23 @@ export type WorkspaceAssignments = {
32
32
  discovery: WorkspaceDiscovery
33
33
  assignments: GroupAssignment[]
34
34
  }
35
+ export type SkippedWorkspace = {
36
+ groupId: string
37
+ workspaceRel: string
38
+ reason: string
39
+ }
35
40
 
36
- export async function computeCurrentSnapshots(cwd: string): Promise<Map<string, BuiltSnapshot>> {
41
+ export async function computeCurrentSnapshots(
42
+ cwd: string,
43
+ options?: {
44
+ allowWorkspaceExtractionFailure?: boolean
45
+ onWorkspacesDiscovered?: (workspacesRel: string[]) => void
46
+ onWorkspaceSkipped?: (skipped: SkippedWorkspace) => void
47
+ }
48
+ ): Promise<Map<string, BuiltSnapshot>> {
49
+ const allowWorkspaceExtractionFailure = options?.allowWorkspaceExtractionFailure ?? false
50
+ const onWorkspacesDiscovered = options?.onWorkspacesDiscovered
51
+ const onWorkspaceSkipped = options?.onWorkspaceSkipped
37
52
  const computeStartedAt = Date.now()
38
53
  const configStartedAt = Date.now()
39
54
  const config = await loadConfig(cwd)
@@ -41,6 +56,7 @@ export async function computeCurrentSnapshots(cwd: string): Promise<Map<string,
41
56
 
42
57
  const assignmentStartedAt = Date.now()
43
58
  const { discovery, assignments } = await resolveWorkspaceAssignments(cwd, config)
59
+ onWorkspacesDiscovered?.(discovery.workspacesRel)
44
60
  debugTiming('phase=resolveWorkspaceAssignments elapsedMs=%d', Date.now() - assignmentStartedAt)
45
61
  debugWorkspace('root=%s groups=%d workspaces=%d', discovery.rootAbs, assignments.length, discovery.workspacesRel.length)
46
62
 
@@ -49,6 +65,7 @@ export async function computeCurrentSnapshots(cwd: string): Promise<Map<string,
49
65
  for (const group of assignments) {
50
66
  const groupStartedAt = Date.now()
51
67
  const extractedForGroup = []
68
+ const extractedWorkspaces: string[] = []
52
69
  debugWorkspace('group=%s workspaces=%o', group.name, group.workspaces)
53
70
 
54
71
  for (const workspaceRel of group.workspaces) {
@@ -85,7 +102,7 @@ export async function computeCurrentSnapshots(cwd: string): Promise<Map<string,
85
102
  }
86
103
 
87
104
  const message = result.error instanceof Error ? result.error.message : String(result.error)
88
- if (isRecoverableExtractionError(message)) {
105
+ if (isRecoverableExtractionError(message) || allowWorkspaceExtractionFailure) {
89
106
  lastExtractionError = message
90
107
  continue
91
108
  }
@@ -95,10 +112,24 @@ export async function computeCurrentSnapshots(cwd: string): Promise<Map<string,
95
112
 
96
113
  if (extractedCount === 0) {
97
114
  const context = lastExtractionError ? ` Last error: ${lastExtractionError}` : ''
98
- throw new Error(
99
- `Unable to extract ESLint config for workspace ${workspaceRel}. All sampled files were ignored or produced non-JSON output.${context}`
100
- )
115
+ if (allowWorkspaceExtractionFailure && isSkippableWorkspaceExtractionFailure(lastExtractionError)) {
116
+ onWorkspaceSkipped?.({
117
+ groupId: group.name,
118
+ workspaceRel,
119
+ reason: lastExtractionError ?? 'unknown extraction failure'
120
+ })
121
+ debugWorkspace(
122
+ 'group=%s workspace=%s skipped=true reason=%s',
123
+ group.name,
124
+ workspaceRel,
125
+ lastExtractionError ?? 'unknown extraction failure'
126
+ )
127
+ continue
128
+ }
129
+
130
+ throw new Error(`Unable to extract ESLint config for workspace ${workspaceRel}.${context}`)
101
131
  }
132
+ extractedWorkspaces.push(workspaceRel)
102
133
 
103
134
  debugWorkspace(
104
135
  'group=%s workspace=%s extracted=%d failed=%d',
@@ -109,8 +140,16 @@ export async function computeCurrentSnapshots(cwd: string): Promise<Map<string,
109
140
  )
110
141
  }
111
142
 
143
+ if (extractedForGroup.length === 0) {
144
+ if (allowWorkspaceExtractionFailure) {
145
+ debugWorkspace('group=%s skipped=true reason=no-extracted-workspaces', group.name)
146
+ continue
147
+ }
148
+ throw new Error(`Unable to extract ESLint config for group ${group.name}: no workspace produced a valid config`)
149
+ }
150
+
112
151
  const aggregated = aggregateRules(extractedForGroup)
113
- snapshots.set(group.name, buildSnapshot(group.name, group.workspaces, aggregated))
152
+ snapshots.set(group.name, buildSnapshot(group.name, extractedWorkspaces, aggregated))
114
153
  debugWorkspace(
115
154
  'group=%s aggregatedRules=%d groupElapsedMs=%d',
116
155
  group.name,
@@ -120,6 +159,9 @@ export async function computeCurrentSnapshots(cwd: string): Promise<Map<string,
120
159
  }
121
160
 
122
161
  debugTiming('phase=computeCurrentSnapshots elapsedMs=%d', Date.now() - computeStartedAt)
162
+ if (snapshots.size === 0) {
163
+ throw new Error('Unable to extract ESLint config from discovered workspaces in zero-config mode')
164
+ }
123
165
  return snapshots
124
166
  }
125
167
 
@@ -132,6 +174,19 @@ function isRecoverableExtractionError(message: string): boolean {
132
174
  )
133
175
  }
134
176
 
177
+ function isSkippableWorkspaceExtractionFailure(message: string | undefined): boolean {
178
+ if (!message) {
179
+ return true
180
+ }
181
+
182
+ return (
183
+ isRecoverableExtractionError(message) ||
184
+ message.startsWith('Failed to load config') ||
185
+ message.startsWith('Failed to run eslint --print-config') ||
186
+ message.startsWith('Unable to resolve eslint from workspace')
187
+ )
188
+ }
189
+
135
190
  export async function resolveWorkspaceAssignments(cwd: string, config: SnapshotConfig): Promise<WorkspaceAssignments> {
136
191
  const discovery = await discoverWorkspaces({ cwd, workspaceInput: config.workspaceInput })
137
192
 
@@ -105,6 +105,43 @@ describe.sequential('cli integration', () => {
105
105
  expect(code).toBe(1)
106
106
  })
107
107
 
108
+ it('update in zero-config mode skips workspaces with unrecoverable eslint extraction failures', async () => {
109
+ await rm(path.join(fixtureRoot, 'eslint-config-snapshot.config.mjs'), { force: true })
110
+ const packageJsonPath = path.join(fixtureRoot, 'package.json')
111
+ const packageJsonRaw = await readFile(packageJsonPath, 'utf8')
112
+ const packageJson = JSON.parse(packageJsonRaw) as Record<string, unknown>
113
+ delete packageJson['eslint-config-snapshot']
114
+ await writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`)
115
+
116
+ await writeFile(
117
+ path.join(fixtureRoot, 'packages/ws-b/node_modules/eslint/bin/eslint.js'),
118
+ "console.error('Failed to load config \"next/core-web-vitals\" to extend from.'); process.exit(1)\n"
119
+ )
120
+
121
+ const writeSpy = vi.spyOn(process.stdout, 'write')
122
+ const code = await runCli('update', fixtureRoot)
123
+ expect(code).toBe(0)
124
+ expect(writeSpy).toHaveBeenCalledWith(
125
+ expect.stringContaining('workspace(s) were skipped because ESLint auto-discovery could not extract an effective config')
126
+ )
127
+ expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('Skipped workspaces: packages/ws-b'))
128
+ expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('excludeGlobs'))
129
+ expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('packages/ws-b/**'))
130
+
131
+ const snapshotRaw = await readFile(path.join(fixtureRoot, '.eslint-config-snapshot/default.json'), 'utf8')
132
+ const snapshot = JSON.parse(snapshotRaw)
133
+
134
+ expect(snapshot).toEqual({
135
+ formatVersion: 1,
136
+ groupId: 'default',
137
+ workspaces: ['packages/ws-a'],
138
+ rules: {
139
+ eqeqeq: ['error', 'always'],
140
+ 'no-console': ['warn']
141
+ }
142
+ })
143
+ })
144
+
108
145
  it('status is minimal and exits 0 when clean', async () => {
109
146
  await runCli('snapshot', fixtureRoot)
110
147
 
@@ -185,7 +222,7 @@ no-debugger: off
185
222
  const writeSpy = vi.spyOn(process.stdout, 'write')
186
223
  const code = await runCli(undefined, fixtureRoot, ['--update'])
187
224
  expect(code).toBe(0)
188
- expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('Baseline updated:'))
225
+ expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('baseline was successfully created'))
189
226
  })
190
227
 
191
228
  it('supports canonical check and update commands', async () => {
@@ -81,7 +81,7 @@ describe('cli terminal invocation', () => {
81
81
  it('snapshot succeeds and compare returns clean result', () => {
82
82
  const snapshot = run(['snapshot'])
83
83
  expect(snapshot.status).toBe(0)
84
- expect(snapshot.stdout).toContain('Baseline updated:')
84
+ expect(snapshot.stdout).toContain('baseline was successfully created')
85
85
  expect(snapshot.stderr).toBe('')
86
86
 
87
87
  const compare = run(['compare'])
@@ -384,10 +384,21 @@ no-debugger: off
384
384
  it('updates snapshots with --update without command', () => {
385
385
  const result = run(['--update'])
386
386
  expect(result.status).toBe(0)
387
- expect(result.stdout).toContain('Baseline updated:')
387
+ expect(result.stdout).toContain('baseline was successfully created')
388
388
  expect(result.stderr).toBe('')
389
389
  })
390
390
 
391
+ it('prints updated message when baseline already exists', () => {
392
+ const first = run(['--update'])
393
+ expect(first.status).toBe(0)
394
+ expect(first.stdout).toContain('baseline was successfully created')
395
+
396
+ const second = run(['--update'])
397
+ expect(second.status).toBe(0)
398
+ expect(second.stdout).toContain('baseline was successfully updated')
399
+ expect(second.stderr).toBe('')
400
+ })
401
+
391
402
  it('prints init help with select-prompt and force guidance', () => {
392
403
  const result = run(['init', '--help'])
393
404
  expect(result.status).toBe(0)
@@ -403,7 +414,7 @@ no-debugger: off
403
414
  it('supports canonical check and update commands', () => {
404
415
  const update = run(['update'])
405
416
  expect(update.status).toBe(0)
406
- expect(update.stdout).toContain('Baseline updated:')
417
+ expect(update.stdout).toContain('baseline was successfully created')
407
418
 
408
419
  const check = run(['check'])
409
420
  expect(check.status).toBe(0)