@santi020k/eslint-config-basic 3.1.0 → 3.2.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/cli.js CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  } from "./agent-skill-generator.js";
12
12
  import { handleCompatibility, handleExplainRule } from "./cli-advanced.js";
13
13
  import { getExplicitConfigFeaturePackages, handleMigrateV3 } from "./cli-migration.js";
14
+ import { handleExplainPreset } from "./cli-preset.js";
14
15
  import {
15
16
  handleBaseline,
16
17
  handleProfile,
@@ -157,7 +158,7 @@ const getInstallPackages = (summary, declaredDependencies, explicitFeaturePackag
157
158
  if (summary.typescript) packages.add(TYPESCRIPT_PACKAGE_NAME);
158
159
  return [...packages].filter((packageName) => !declaredDependencies.has(packageName));
159
160
  };
160
- const createInstallCommand = (packageManager, packages, workspaceRoot = false) => {
161
+ const createInstallCommand = (packageManager, packages, workspaceRoot = false, catalog = false) => {
161
162
  const packageList = packages.join(" ");
162
163
  switch (packageManager) {
163
164
  case "bun":
@@ -167,10 +168,10 @@ const createInstallCommand = (packageManager, packages, workspaceRoot = false) =
167
168
  case "yarn":
168
169
  return `yarn add -D ${packageList}`;
169
170
  default:
170
- return `pnpm add -D${workspaceRoot ? " --workspace-root" : ""} ${packageList}`;
171
+ return `pnpm add -D${workspaceRoot ? " --workspace-root" : ""}` + (catalog === true ? " --save-catalog" : "") + `${typeof catalog === "string" ? ` --save-catalog-name=${catalog}` : ""} ${packageList}`;
171
172
  }
172
173
  };
173
- const createInstallInvocation = (packageManager, packages, workspaceRoot = false) => {
174
+ const createInstallInvocation = (packageManager, packages, workspaceRoot = false, catalog = false) => {
174
175
  switch (packageManager) {
175
176
  case "bun":
176
177
  return ["bun", ["add", "-d", ...packages]];
@@ -179,15 +180,134 @@ const createInstallInvocation = (packageManager, packages, workspaceRoot = false
179
180
  case "yarn":
180
181
  return ["yarn", ["add", "-D", ...packages]];
181
182
  default:
182
- return ["pnpm", ["add", "-D", ...workspaceRoot ? ["--workspace-root"] : [], ...packages]];
183
+ return ["pnpm", [
184
+ "add",
185
+ "-D",
186
+ ...workspaceRoot ? ["--workspace-root"] : [],
187
+ ...catalog === true ? ["--save-catalog"] : [],
188
+ ...typeof catalog === "string" ? [`--save-catalog-name=${catalog}`] : [],
189
+ ...packages
190
+ ]];
191
+ }
192
+ };
193
+ const findPnpmWorkspaceRoot = (cwd) => {
194
+ let current = cwd;
195
+ for (; ; ) {
196
+ if (existsSync(join(current, "pnpm-workspace.yaml"))) return current;
197
+ const parent = dirname(current);
198
+ if (parent === current) return void 0;
199
+ current = parent;
183
200
  }
184
201
  };
185
202
  const detectPackageManager = (cwd) => {
186
- if (existsSync(join(cwd, "pnpm-lock.yaml")) || existsSync(join(cwd, "pnpm-workspace.yaml"))) return "pnpm";
203
+ if (findPnpmWorkspaceRoot(cwd) || existsSync(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
187
204
  if (existsSync(join(cwd, "yarn.lock"))) return "yarn";
188
205
  if (existsSync(join(cwd, "bun.lockb")) || existsSync(join(cwd, "bun.lock"))) return "bun";
189
206
  return "npm";
190
207
  };
208
+ const getCatalogPreference = (packageJson) => {
209
+ const dependencyFields = ["devDependencies", "dependencies"];
210
+ const dependencyRecords = [];
211
+ const parseCatalog = (value) => {
212
+ if (typeof value !== "string" || !value.startsWith("catalog:")) return false;
213
+ const name = value.slice("catalog:".length);
214
+ return name && name !== "default" ? name : true;
215
+ };
216
+ for (const field of dependencyFields) {
217
+ const dependencies = packageJson?.[field];
218
+ if (!dependencies || typeof dependencies !== "object" || Array.isArray(dependencies)) continue;
219
+ dependencyRecords.push(dependencies);
220
+ }
221
+ for (const dependencies of dependencyRecords) {
222
+ const basicCatalog = parseCatalog(Reflect.get(dependencies, BASIC_PACKAGE_NAME));
223
+ if (basicCatalog) return basicCatalog;
224
+ }
225
+ for (const dependencies of dependencyRecords) {
226
+ for (const value of Object.values(dependencies)) {
227
+ const catalog = parseCatalog(value);
228
+ if (catalog) return catalog;
229
+ }
230
+ }
231
+ return false;
232
+ };
233
+ const parseYamlMappingLine = (line) => {
234
+ const match = /^(\s*)(?:"([^"]+)"|'([^']+)'|([^'"].*?))\s*:\s*(.*?)\s*$/.exec(line);
235
+ if (!match) return void 0;
236
+ const value = match[5].replace(/\s+#.*$/, "").trim();
237
+ const key = match[2] || match[3] || match[4];
238
+ return {
239
+ indent: match[1].length,
240
+ key: key.trim(),
241
+ value: /^(['"]).*\1$/.test(value) ? value.slice(1, -1) : value
242
+ };
243
+ };
244
+ const getCatalogVersion = (workspaceRoot, packageName, catalog) => {
245
+ const workspacePath = join(workspaceRoot, "pnpm-workspace.yaml");
246
+ if (!existsSync(workspacePath)) return void 0;
247
+ const lines = readFileSync(workspacePath, "utf8").split(/\r?\n/);
248
+ let sectionIndex = -1;
249
+ let sectionIndent = -1;
250
+ if (catalog === true) {
251
+ sectionIndex = lines.findIndex((line) => {
252
+ const mapping = parseYamlMappingLine(line);
253
+ return mapping?.indent === 0 && mapping.key === "catalog" && mapping.value === "";
254
+ });
255
+ sectionIndent = 0;
256
+ } else {
257
+ const catalogsIndex = lines.findIndex((line) => {
258
+ const mapping = parseYamlMappingLine(line);
259
+ return mapping?.indent === 0 && mapping.key === "catalogs" && mapping.value === "";
260
+ });
261
+ if (catalogsIndex >= 0) {
262
+ for (let index = catalogsIndex + 1; index < lines.length; index++) {
263
+ const mapping = parseYamlMappingLine(lines.at(index) ?? "");
264
+ if (!mapping) continue;
265
+ if (mapping.indent === 0) break;
266
+ if (mapping.key === catalog && mapping.value === "") {
267
+ sectionIndex = index;
268
+ sectionIndent = mapping.indent;
269
+ break;
270
+ }
271
+ }
272
+ }
273
+ }
274
+ if (sectionIndex < 0) return void 0;
275
+ for (let index = sectionIndex + 1; index < lines.length; index++) {
276
+ const mapping = parseYamlMappingLine(lines.at(index) ?? "");
277
+ if (!mapping) continue;
278
+ if (mapping.indent <= sectionIndent) break;
279
+ if (mapping.key === packageName) return mapping.value || void 0;
280
+ }
281
+ return void 0;
282
+ };
283
+ const getCompatibleConfigVersion = (packageJson, workspaceRoot) => {
284
+ const dependencyFields = ["devDependencies", "dependencies", "peerDependencies"];
285
+ let basicSpec;
286
+ for (const field of dependencyFields) {
287
+ const dependencies = packageJson?.[field];
288
+ if (!dependencies || typeof dependencies !== "object" || Array.isArray(dependencies)) continue;
289
+ const value = Object.entries(dependencies).find(([packageName]) => packageName === BASIC_PACKAGE_NAME)?.[1];
290
+ if (typeof value === "string") {
291
+ basicSpec = value;
292
+ break;
293
+ }
294
+ }
295
+ const catalogName = basicSpec?.startsWith("catalog:") ? basicSpec.slice("catalog:".length) : void 0;
296
+ let selectedCatalog;
297
+ if (catalogName !== void 0) {
298
+ selectedCatalog = catalogName && catalogName !== "default" ? catalogName : true;
299
+ }
300
+ const resolvedSpec = selectedCatalog && workspaceRoot ? getCatalogVersion(workspaceRoot, BASIC_PACKAGE_NAME, selectedCatalog) : basicSpec;
301
+ const match = /(\d+)\.(\d+)\.(\d+)/.exec(resolvedSpec ?? getCliVersion());
302
+ return match ? `^${match[1]}.${match[2]}.${match[3]}` : "^3.1.0";
303
+ };
304
+ const addCompatibleConfigVersions = (packages, version) => {
305
+ const major = /\d+/.exec(version)?.[0];
306
+ const companionVersion = major ? `^${major}.0.0` : version;
307
+ return packages.map(
308
+ (packageName) => packageName.startsWith("@santi020k/eslint-config-") && packageName !== LITE_PACKAGE_NAME && packageName !== INTEGRATIONS_PACKAGE_NAME ? `${packageName}@${packageName === BASIC_PACKAGE_NAME ? version : companionVersion}` : packageName
309
+ );
310
+ };
191
311
  const hasLintScript = (cwd) => {
192
312
  const packageJson = readPackageJson(cwd);
193
313
  return Boolean(packageJson?.scripts?.lint);
@@ -237,13 +357,13 @@ const detectWorkspaceProjects = (cwd) => {
237
357
  const createExplicitOptions = (cwd) => {
238
358
  const summary = getProjectSummary(cwd);
239
359
  const options = [];
240
- const optionalCategories = [
241
- ["libraries", summary.libraries],
242
- ["testing", summary.testing],
243
- ["formats", summary.formats],
244
- ["tools", summary.tools],
245
- ["extensions", summary.extensions]
246
- ];
360
+ const features = [.../* @__PURE__ */ new Set([
361
+ ...summary.extensions,
362
+ ...summary.formats,
363
+ ...summary.libraries,
364
+ ...summary.testing,
365
+ ...summary.tools
366
+ ])].sort();
247
367
  if (summary.typescript) options.push(" typescript: true,");
248
368
  if (summary.frameworks.length > 0) {
249
369
  options.push(
@@ -252,10 +372,12 @@ const createExplicitOptions = (cwd) => {
252
372
  " },"
253
373
  );
254
374
  }
255
- for (const [category, values] of optionalCategories) {
256
- if (values.length > 0) {
257
- options.push(` ${category}: ${JSON.stringify(values)},`);
258
- }
375
+ if (features.length > 0) {
376
+ options.push(
377
+ " features: {",
378
+ ...features.map((feature) => ` ${toPropertyKey(feature)}: true,`),
379
+ " },"
380
+ );
259
381
  }
260
382
  return options;
261
383
  };
@@ -297,7 +419,11 @@ const getProjectSummary = (cwd) => {
297
419
  };
298
420
  const getInstallProjectSummary = (cwd) => {
299
421
  const rootSummary = getProjectSummary(cwd);
300
- const projectSummaries = rootSummary.detectedProjects.map((projectPath) => getProjectSummary(join(cwd, projectPath)));
422
+ const projectPaths = [.../* @__PURE__ */ new Set([
423
+ ...rootSummary.detectedProjects,
424
+ ...rootSummary.workspaceProjects
425
+ ])];
426
+ const projectSummaries = projectPaths.map((projectPath) => getProjectSummary(join(cwd, projectPath)));
301
427
  const summaries = [rootSummary, ...projectSummaries];
302
428
  return {
303
429
  ...rootSummary,
@@ -368,6 +494,7 @@ const printUsage = () => {
368
494
  " init Create eslint.config.js/mjs if missing",
369
495
  " update Regenerate eslint.config.js/mjs from detection",
370
496
  " explain Print detected v3 config inputs",
497
+ " explain-preset Compare the current config with a preset",
371
498
  " inspect Print detected inputs and active config features",
372
499
  " install Install missing packages for detected v3 features",
373
500
  " doctor Check project setup for common v3 adoption issues",
@@ -387,7 +514,7 @@ const printUsage = () => {
387
514
  " --create generate-skill: scaffold a root AGENTS.md when missing",
388
515
  " --dry-run install: print the detected install command without running it",
389
516
  " --explicit init: write detected settings explicitly",
390
- " --file profile/snapshot/diff: representative file or lint target (repeatable)",
517
+ " --file explain/profile/snapshot/diff: representative file or lint target (repeatable)",
391
518
  " --fix doctor: safely repair generated config and package metadata",
392
519
  " --full migrate --to v3: choose the batteries-included package",
393
520
  " --json Print JSON for commands that support it",
@@ -395,6 +522,8 @@ const printUsage = () => {
395
522
  " --max-duration profile: maximum duration in milliseconds",
396
523
  " --max-rule-time profile: maximum time for the slowest rule in milliseconds",
397
524
  " --max-warnings profile: maximum warning count",
525
+ " --compatibility explain-preset: write a temporary compatibility override",
526
+ " --output explain-preset: compatibility override path",
398
527
  " --preset baseline: enable ci or pedantic strict mode before suppressing",
399
528
  " --prune baseline: remove suppressions for resolved violations",
400
529
  " --snapshot-path snapshot/diff: override .eslint-config-snapshot.json",
@@ -405,6 +534,94 @@ const printUsage = () => {
405
534
  " --version, -v Show CLI version"
406
535
  ].join("\n"));
407
536
  };
537
+ const COMMAND_OPTIONS = {
538
+ baseline: ["--json", "--preset", "--prune"],
539
+ compatibility: ["--json"],
540
+ diff: ["--file", "--json", "--snapshot-path"],
541
+ docs: [],
542
+ doctor: ["--fix", "--json", "--lite-install"],
543
+ explain: ["--file", "--json"],
544
+ "explain-preset": ["--compatibility", "--file", "--json", "--output"],
545
+ "generate-skill": ["--check", "--create", "--force", "--with-eslint-mcp"],
546
+ init: ["--check", "--explicit"],
547
+ inspect: ["--json"],
548
+ install: ["--dry-run"],
549
+ migrate: ["--check", "--full", "--json", "--to", "--write"],
550
+ profile: ["--concurrency", "--file", "--json", "--max-duration", "--max-rule-time", "--max-warnings"],
551
+ snapshot: ["--check", "--file", "--json", "--snapshot-path"],
552
+ update: []
553
+ };
554
+ const VALUE_OPTIONS = /* @__PURE__ */ new Set([
555
+ "--concurrency",
556
+ "--file",
557
+ "--max-duration",
558
+ "--max-rule-time",
559
+ "--max-warnings",
560
+ "--output",
561
+ "--preset",
562
+ "--snapshot-path",
563
+ "--to"
564
+ ]);
565
+ const printCommandUsage = (command) => {
566
+ let positional = "";
567
+ if (command === "explain") positional = " [rule]";
568
+ else if (command === "explain-preset") positional = " <preset>";
569
+ const options = COMMAND_OPTIONS[command] ?? [];
570
+ console.log([
571
+ "Usage: basic-eslint <command> [options]",
572
+ "",
573
+ `Usage: basic-eslint ${command}${positional}${options.length > 0 ? " [options]" : ""}`,
574
+ ...options.length > 0 ? ["", "Options:", ...options.map((option) => ` ${option}`)] : []
575
+ ].join("\n"));
576
+ };
577
+ const validateCommandArguments = (command, args) => {
578
+ const allowed = COMMAND_OPTIONS[command];
579
+ if (!allowed) return { ok: true };
580
+ const allowedOptions = new Set(allowed);
581
+ const positionals = [];
582
+ for (let index = 0; index < args.length; index++) {
583
+ const argument = args[index];
584
+ if (!argument.startsWith("-")) {
585
+ positionals.push(argument);
586
+ continue;
587
+ }
588
+ const option = argument.includes("=") ? argument.slice(0, argument.indexOf("=")) : argument;
589
+ if (!allowedOptions.has(option)) {
590
+ console.error(`Unknown option for ${command}: ${option}`);
591
+ process.exitCode = 1;
592
+ return { ok: false };
593
+ }
594
+ if (argument.includes("=") && !VALUE_OPTIONS.has(option)) {
595
+ console.error(`Option ${option} does not accept a value.`);
596
+ process.exitCode = 1;
597
+ return { ok: false };
598
+ }
599
+ if (VALUE_OPTIONS.has(option) && !argument.includes("=")) {
600
+ const value = args[index + 1];
601
+ if (!value || value.startsWith("-")) {
602
+ console.error(
603
+ ["--max-duration", "--max-rule-time", "--max-warnings"].includes(option) ? `${option} must be a non-negative number.` : `Option ${option} requires a value.`
604
+ );
605
+ process.exitCode = 1;
606
+ return { ok: false };
607
+ }
608
+ index++;
609
+ } else if (VALUE_OPTIONS.has(option) && argument.endsWith("=")) {
610
+ console.error(
611
+ ["--max-duration", "--max-rule-time", "--max-warnings"].includes(option) ? `${option} must be a non-negative number.` : `Option ${option} requires a value.`
612
+ );
613
+ process.exitCode = 1;
614
+ return { ok: false };
615
+ }
616
+ }
617
+ const maximumPositionals = command === "explain" || command === "explain-preset" ? 1 : 0;
618
+ if (positionals.length > maximumPositionals) {
619
+ console.error(`Unexpected argument for ${command}: ${positionals[maximumPositionals]}`);
620
+ process.exitCode = 1;
621
+ return { ok: false };
622
+ }
623
+ return { ok: true, positional: positionals[0] };
624
+ };
408
625
  const handleInit = (cwd = process.cwd(), check = false, explicit = false) => {
409
626
  const configPath = resolveConfigPath(cwd);
410
627
  if (check) {
@@ -585,6 +802,16 @@ const validateConfigContent = (configContent, hasV1FrameworkImports, declaredDep
585
802
  );
586
803
  }
587
804
  }
805
+ const missingPackages = getInstallPackages(
806
+ summary,
807
+ declaredDependencies,
808
+ getExplicitConfigFeaturePackages(configContent)
809
+ );
810
+ if (missingPackages.length > 0) {
811
+ warnings.push(
812
+ `The active configuration requires undeclared packages: ${missingPackages.join(", ")}. Run \`basic-eslint install\` to add the modular v3 dependency set.`
813
+ );
814
+ }
588
815
  return warnings;
589
816
  };
590
817
  const validateProjectSetup = (cwd, configPath, activeConfig, hasV1FrameworkImports) => {
@@ -644,7 +871,7 @@ const buildDoctorDiagnosis = (cwd, configPath, configContent, hasV1FrameworkImpo
644
871
  checkDuplicateEslint(cwd, warnings);
645
872
  return { issues, warnings };
646
873
  };
647
- const outputDoctorResult = (json, configPath, packageManager, summary, liteInstallCommand, issues, warnings, fixes = []) => {
874
+ const outputDoctorResult = (json, configPath, packageManager, summary, installCommand, requiredPackages, issues, warnings, fixes = []) => {
648
875
  let status = "passed";
649
876
  if (issues.length > 0) {
650
877
  status = "failed";
@@ -654,9 +881,10 @@ const outputDoctorResult = (json, configPath, packageManager, summary, liteInsta
654
881
  const payload = {
655
882
  configFile: configPath ? basename(configPath) : null,
656
883
  fixes,
884
+ ...installCommand ? { installCommand } : {},
657
885
  issues,
658
- liteInstallCommand,
659
886
  packageManager,
887
+ requiredPackages,
660
888
  status,
661
889
  warnings,
662
890
  workspaceProjects: summary.workspaceProjects
@@ -671,6 +899,8 @@ const outputDoctorResult = (json, configPath, packageManager, summary, liteInsta
671
899
  `- Package manager: ${payload.packageManager}`,
672
900
  `- Config file: ${payload.configFile ?? "none"}`,
673
901
  `- Workspace projects: ${formatList(summary.workspaceProjects)}`,
902
+ `- Required packages: ${formatList(requiredPackages)}`,
903
+ ...installCommand ? [`- Install command: ${installCommand}`] : [],
674
904
  ...fixes.length > 0 ? ["", "Fixes applied:", ...fixes.map((fix) => `- ${fix}`)] : [],
675
905
  ...issues.length > 0 ? ["", "Issues:", ...issues.map((issue) => `- ${issue}`)] : [],
676
906
  ...warnings.length > 0 ? ["", "Warnings:", ...warnings.map((warning) => `- ${warning}`)] : []
@@ -742,18 +972,29 @@ const applyDoctorFixes = (cwd, packageJson, configPath, configContent, summary)
742
972
  };
743
973
  const hasV1FrameworkPackageImports = (configContent) => configContent !== null && Object.keys(FRAMEWORK_PACKAGE_TO_KEY).some((packageName) => configContent.includes(packageName));
744
974
  const handleDoctor = async (cwd = process.cwd(), json = false, liteInstall = false, fix = false) => {
745
- const packageManager = detectPackageManager(cwd);
746
- const workspaceRoot = packageManager === "pnpm" && existsSync(join(cwd, "pnpm-workspace.yaml"));
747
- let configPath = getConfigPathIfPresent(cwd);
748
- let packageJson = readPackageJson(cwd);
975
+ const pnpmWorkspaceRoot = findPnpmWorkspaceRoot(cwd);
976
+ const projectRoot = pnpmWorkspaceRoot ?? cwd;
977
+ const packageManager = detectPackageManager(projectRoot);
978
+ const workspaceRoot = packageManager === "pnpm" && pnpmWorkspaceRoot !== void 0;
979
+ const catalog = workspaceRoot ? getCatalogPreference(readPackageJson(projectRoot)) : false;
980
+ let configPath = getConfigPathIfPresent(projectRoot);
981
+ let packageJson = readPackageJson(projectRoot);
749
982
  let declaredDependencies = getDeclaredDependencyNames(packageJson);
750
- let summary = getInstallProjectSummary(cwd);
751
- let activeConfig = await analyzeEslintConfig(cwd);
983
+ let summary = getInstallProjectSummary(projectRoot);
984
+ let activeConfig = await analyzeEslintConfig(projectRoot);
752
985
  let configContent = configPath ? readFileSync(configPath, "utf8") : null;
753
- let liteInstallPackages = getLiteInstallPackages(summary, declaredDependencies);
754
- let liteInstallCommand = createInstallCommand(packageManager, liteInstallPackages, workspaceRoot);
755
986
  let hasV1FrameworkImports = hasV1FrameworkPackageImports(configContent);
756
987
  if (liteInstall) {
988
+ const liteInstallPackages = getLiteInstallPackages(summary, declaredDependencies);
989
+ const liteInstallCommand = createInstallCommand(
990
+ packageManager,
991
+ addCompatibleConfigVersions(
992
+ liteInstallPackages,
993
+ getCompatibleConfigVersion(packageJson, pnpmWorkspaceRoot)
994
+ ),
995
+ workspaceRoot,
996
+ catalog
997
+ );
757
998
  if (json) {
758
999
  console.log(JSON.stringify({ command: liteInstallCommand, packageManager, packages: liteInstallPackages }, null, 2));
759
1000
  } else {
@@ -761,20 +1002,29 @@ const handleDoctor = async (cwd = process.cwd(), json = false, liteInstall = fal
761
1002
  }
762
1003
  return;
763
1004
  }
764
- const fixes = fix ? applyDoctorFixes(cwd, packageJson, configPath, configContent, summary) : [];
1005
+ const fixes = fix ? applyDoctorFixes(projectRoot, packageJson, configPath, configContent, summary) : [];
765
1006
  if (fixes.length > 0) {
766
- configPath = getConfigPathIfPresent(cwd);
767
- packageJson = readPackageJson(cwd);
1007
+ configPath = getConfigPathIfPresent(projectRoot);
1008
+ packageJson = readPackageJson(projectRoot);
768
1009
  declaredDependencies = getDeclaredDependencyNames(packageJson);
769
- summary = getInstallProjectSummary(cwd);
770
- activeConfig = await analyzeEslintConfig(cwd);
1010
+ summary = getInstallProjectSummary(projectRoot);
1011
+ activeConfig = await analyzeEslintConfig(projectRoot);
771
1012
  configContent = configPath ? readFileSync(configPath, "utf8") : null;
772
- liteInstallPackages = getLiteInstallPackages(summary, declaredDependencies);
773
- liteInstallCommand = createInstallCommand(packageManager, liteInstallPackages, workspaceRoot);
774
1013
  hasV1FrameworkImports = hasV1FrameworkPackageImports(configContent);
775
1014
  }
1015
+ const explicitFeaturePackages = configContent ? getExplicitConfigFeaturePackages(configContent) : [];
1016
+ const requiredPackages = getInstallPackages(summary, declaredDependencies, explicitFeaturePackages);
1017
+ const installCommand = requiredPackages.length > 0 ? createInstallCommand(
1018
+ packageManager,
1019
+ addCompatibleConfigVersions(
1020
+ requiredPackages,
1021
+ getCompatibleConfigVersion(packageJson, pnpmWorkspaceRoot)
1022
+ ),
1023
+ workspaceRoot,
1024
+ catalog
1025
+ ) : void 0;
776
1026
  const { issues, warnings } = buildDoctorDiagnosis(
777
- cwd,
1027
+ projectRoot,
778
1028
  configPath,
779
1029
  configContent,
780
1030
  hasV1FrameworkImports,
@@ -782,41 +1032,74 @@ const handleDoctor = async (cwd = process.cwd(), json = false, liteInstall = fal
782
1032
  declaredDependencies,
783
1033
  summary
784
1034
  );
785
- outputDoctorResult(json, configPath, packageManager, summary, liteInstallCommand, issues, warnings, fixes);
1035
+ outputDoctorResult(
1036
+ json,
1037
+ configPath,
1038
+ packageManager,
1039
+ summary,
1040
+ installCommand,
1041
+ requiredPackages,
1042
+ issues,
1043
+ warnings,
1044
+ fixes
1045
+ );
786
1046
  };
787
- const handleInstall = (cwd = process.cwd(), dryRun = false) => {
788
- const packageJson = readPackageJson(cwd);
1047
+ const isMinimumReleaseAgeFailure = (output) => /minimum[\s_-]*release[\s_-]*age/i.test(output) || /release (?:is )?(?:too young|not old enough)/i.test(output) || /blocked by (?:the )?minimum age/i.test(output);
1048
+ const handleInstall = (cwd = process.cwd(), dryRun = false, runner = spawnSync) => {
1049
+ const pnpmWorkspaceRoot = findPnpmWorkspaceRoot(cwd);
1050
+ const projectRoot = pnpmWorkspaceRoot ?? cwd;
1051
+ const packageJson = readPackageJson(projectRoot);
789
1052
  if (!packageJson) {
790
1053
  console.error("\u274C package.json is missing or invalid.");
791
1054
  process.exitCode = 1;
792
1055
  return;
793
1056
  }
794
- const packageManager = detectPackageManager(cwd);
795
- const configPath = getConfigPathIfPresent(cwd);
1057
+ const packageManager = detectPackageManager(projectRoot);
1058
+ const configPath = getConfigPathIfPresent(projectRoot);
796
1059
  const explicitFeaturePackages = configPath ? getExplicitConfigFeaturePackages(readFileSync(configPath, "utf8")) : [];
797
1060
  const packages = getInstallPackages(
798
- getInstallProjectSummary(cwd),
1061
+ getInstallProjectSummary(projectRoot),
799
1062
  getDeclaredDependencyNames(packageJson),
800
1063
  explicitFeaturePackages
801
1064
  );
802
- const workspaceRoot = packageManager === "pnpm" && existsSync(join(cwd, "pnpm-workspace.yaml"));
1065
+ const workspaceRoot = packageManager === "pnpm" && pnpmWorkspaceRoot !== void 0;
1066
+ const catalog = workspaceRoot ? getCatalogPreference(packageJson) : false;
1067
+ const installPackages = addCompatibleConfigVersions(
1068
+ packages,
1069
+ getCompatibleConfigVersion(packageJson, pnpmWorkspaceRoot)
1070
+ );
803
1071
  if (packages.length === 0) {
804
1072
  console.log("\u2705 All packages required by the detected ESLint configuration are already declared.");
805
1073
  return;
806
1074
  }
807
- const installCommand = createInstallCommand(packageManager, packages, workspaceRoot);
1075
+ const installCommand = createInstallCommand(packageManager, installPackages, workspaceRoot, catalog);
808
1076
  if (dryRun) {
809
1077
  console.log(installCommand);
810
1078
  return;
811
1079
  }
812
1080
  console.log(`Installing detected ESLint dependencies:
813
1081
  ${installCommand}`);
814
- const [command, args] = createInstallInvocation(packageManager, packages, workspaceRoot);
815
- const result = spawnSync(command, args, { cwd, stdio: "inherit" });
1082
+ const [command, args] = createInstallInvocation(packageManager, installPackages, workspaceRoot, catalog);
1083
+ const result = runner(command, args, {
1084
+ cwd: projectRoot,
1085
+ encoding: "utf8",
1086
+ maxBuffer: 16 * 1024 * 1024,
1087
+ stdio: "pipe"
1088
+ });
1089
+ const stdout = String(result.stdout ?? "");
1090
+ const stderr = String(result.stderr ?? "");
1091
+ if (stdout) process.stdout.write(stdout);
1092
+ if (stderr) process.stderr.write(stderr);
816
1093
  if (result.error) {
817
1094
  console.error(`\u274C Failed to run ${packageManager}: ${result.error.message}`);
818
1095
  process.exitCode = 1;
819
1096
  } else if (result.status !== 0) {
1097
+ if (packageManager === "pnpm" && isMinimumReleaseAgeFailure(`${stdout}
1098
+ ${stderr}`)) {
1099
+ console.error(
1100
+ `\u274C pnpm minimumReleaseAge temporarily blocks the compatible ESLint config release (${installPackages.join(", ")}). Keep the generated compatible range and either wait for the policy window or add the exact release to minimumReleaseAgeExclude in pnpm-workspace.yaml.`
1101
+ );
1102
+ }
820
1103
  process.exitCode = result.status ?? 1;
821
1104
  }
822
1105
  };
@@ -964,6 +1247,23 @@ const dispatchCommand = (command, cwd, flags) => {
964
1247
  }
965
1248
  break;
966
1249
  }
1250
+ case "explain-preset": {
1251
+ if (!flags.positional) {
1252
+ console.error("Missing preset. Usage: basic-eslint explain-preset <preset> [options]");
1253
+ process.exitCode = 1;
1254
+ break;
1255
+ }
1256
+ handleExplainPreset(cwd, flags.positional, {
1257
+ compatibility: flags.hasCompatibility,
1258
+ file: flags.files[0],
1259
+ json: flags.hasJson,
1260
+ output: flags.output
1261
+ }).catch((error) => {
1262
+ console.error(`\u274C Failed to explain preset: ${String(error)}`);
1263
+ process.exitCode = 1;
1264
+ });
1265
+ break;
1266
+ }
967
1267
  case "generate-skill": {
968
1268
  handleGenerateSkill(cwd, flags.hasForce, {
969
1269
  check: flags.hasCheck,
@@ -1065,9 +1365,9 @@ const getNumericFlagValue = (argv, flag) => {
1065
1365
  };
1066
1366
  const runCli = (argv = process.argv, cwd = process.cwd()) => {
1067
1367
  const command = argv[2];
1068
- const isHelp = command === "--help" || command === "-h";
1368
+ const isHelp = argv.slice(3).some((argument) => argument === "--help" || argument === "-h");
1069
1369
  const isVersion = command === "--version" || command === "-v";
1070
- if (!command || isHelp) {
1370
+ if (!command || command === "--help" || command === "-h") {
1071
1371
  printUsage();
1072
1372
  return;
1073
1373
  }
@@ -1075,10 +1375,18 @@ const runCli = (argv = process.argv, cwd = process.cwd()) => {
1075
1375
  console.log(getCliVersion());
1076
1376
  return;
1077
1377
  }
1378
+ if (isHelp) {
1379
+ if (COMMAND_OPTIONS[command]) printCommandUsage(command);
1380
+ else printUsage();
1381
+ return;
1382
+ }
1383
+ const validation = validateCommandArguments(command, argv.slice(3));
1384
+ if (!validation.ok) return;
1078
1385
  dispatchCommand(command, cwd, {
1079
1386
  concurrency: getFlagValue(argv, "--concurrency"),
1080
1387
  files: getFlagValues(argv, "--file"),
1081
1388
  hasCheck: argv.includes("--check"),
1389
+ hasCompatibility: argv.includes("--compatibility"),
1082
1390
  hasCreate: argv.includes("--create"),
1083
1391
  hasDryRun: argv.includes("--dry-run"),
1084
1392
  hasExplicit: argv.includes("--explicit"),
@@ -1093,8 +1401,10 @@ const runCli = (argv = process.argv, cwd = process.cwd()) => {
1093
1401
  maxDurationMs: getNumericFlagValue(argv, "--max-duration"),
1094
1402
  maxRuleTimeMs: getNumericFlagValue(argv, "--max-rule-time"),
1095
1403
  maxWarnings: getNumericFlagValue(argv, "--max-warnings"),
1404
+ output: getFlagValue(argv, "--output"),
1405
+ positional: validation.positional,
1096
1406
  preset: getFlagValue(argv, "--preset"),
1097
- rule: argv[3] && !argv[3].startsWith("-") ? argv[3] : void 0,
1407
+ rule: command === "explain" ? validation.positional : void 0,
1098
1408
  snapshotPath: getFlagValue(argv, "--snapshot-path"),
1099
1409
  target: getFlagValue(argv, "--to")
1100
1410
  });
@@ -0,0 +1,10 @@
1
+ import { ConfigInput, EslintConfigOptions, FlatConfigArray } from '@santi020k/eslint-config-core';
2
+
3
+ interface DefineConfigMetadata {
4
+ extraConfigs: ConfigInput[];
5
+ options?: EslintConfigOptions;
6
+ }
7
+ declare const attachDefineConfigMetadata: (config: FlatConfigArray, metadata: DefineConfigMetadata) => FlatConfigArray;
8
+ declare const getDefineConfigMetadata: (config: unknown) => DefineConfigMetadata | undefined;
9
+
10
+ export { type DefineConfigMetadata, attachDefineConfigMetadata, getDefineConfigMetadata };
@@ -0,0 +1,18 @@
1
+ const DEFINE_CONFIG_METADATA = /* @__PURE__ */ Symbol.for("@santi020k/eslint-config-basic/define-config-metadata");
2
+ const attachDefineConfigMetadata = (config, metadata) => {
3
+ Object.defineProperty(config, DEFINE_CONFIG_METADATA, {
4
+ configurable: false,
5
+ enumerable: false,
6
+ value: metadata,
7
+ writable: false
8
+ });
9
+ return config;
10
+ };
11
+ const getDefineConfigMetadata = (config) => (
12
+ // eslint-disable-next-line security/detect-object-injection -- shared symbol is an internal fixed metadata key
13
+ Array.isArray(config) ? config[DEFINE_CONFIG_METADATA] : void 0
14
+ );
15
+ export {
16
+ attachDefineConfigMetadata,
17
+ getDefineConfigMetadata
18
+ };
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  createModuleLoader
3
3
  } from "@santi020k/eslint-config-core";
4
+ import { isMissingRequestedPackage } from "./optional-package-errors.js";
4
5
  const loadModule = createModuleLoader((specifier) => import.meta.resolve(specifier, import.meta.url));
5
6
  const createDetectedFrameworkFlags = (detectedFrameworks = []) => Object.fromEntries(
6
7
  detectedFrameworks.map((framework) => [framework, true])
@@ -33,8 +34,10 @@ const loadFrameworkConfigInput = (frameworkName) => {
33
34
  if (!loader) return Promise.resolve([]);
34
35
  const pending = loader().catch((error) => {
35
36
  frameworkConfigCache.delete(frameworkName);
37
+ const specifier = `@santi020k/eslint-config-${frameworkName}`;
38
+ const remediation = isMissingRequestedPackage(error, specifier) ? `Install "${specifier}" or remove that framework from your defineConfig options.` : `The installed package "${specifier}" failed while evaluating. Inspect this error's cause for the original dependency or runtime failure.`;
36
39
  throw new Error(
37
- `Unable to load optional framework config "${frameworkName}". Install "@santi020k/eslint-config-${frameworkName}" or remove that framework from your defineConfig options.`,
40
+ `Unable to load optional framework config "${frameworkName}". ` + remediation,
38
41
  { cause: error }
39
42
  );
40
43
  });