@santi020k/eslint-config-basic 3.1.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/agent-skill-generator.d.ts +7 -18
  3. package/dist/agent-skill-generator.js +54 -18
  4. package/dist/agent-skill-generator.js.map +1 -0
  5. package/dist/cli-advanced.d.ts +7 -0
  6. package/dist/cli-advanced.js +74 -15
  7. package/dist/cli-advanced.js.map +1 -0
  8. package/dist/cli-migration.js +388 -34
  9. package/dist/cli-migration.js.map +1 -0
  10. package/dist/cli-package-manager.d.ts +10 -0
  11. package/dist/cli-package-manager.js +135 -0
  12. package/dist/cli-package-manager.js.map +1 -0
  13. package/dist/cli-preset.d.ts +103 -0
  14. package/dist/cli-preset.js +562 -0
  15. package/dist/cli-preset.js.map +1 -0
  16. package/dist/cli-workflows.js +1 -0
  17. package/dist/cli-workflows.js.map +1 -0
  18. package/dist/cli.d.ts +14 -2
  19. package/dist/cli.js +689 -94
  20. package/dist/cli.js.map +1 -0
  21. package/dist/define-config-metadata.d.ts +10 -0
  22. package/dist/define-config-metadata.js +16 -0
  23. package/dist/define-config-metadata.js.map +1 -0
  24. package/dist/frameworks.d.ts +1 -0
  25. package/dist/frameworks.js +6 -2
  26. package/dist/frameworks.js.map +1 -0
  27. package/dist/index.d.ts +10 -3
  28. package/dist/index.js +145 -27
  29. package/dist/index.js.map +1 -0
  30. package/dist/integrations.js +4 -1
  31. package/dist/integrations.js.map +1 -0
  32. package/dist/optional-package-errors.d.ts +3 -0
  33. package/dist/optional-package-errors.js +15 -0
  34. package/dist/optional-package-errors.js.map +1 -0
  35. package/dist/recommended.js +1 -0
  36. package/dist/recommended.js.map +1 -0
  37. package/dist/resolvers.js +1 -0
  38. package/dist/resolvers.js.map +1 -0
  39. package/dist/tailwind.d.ts +3 -1
  40. package/dist/tailwind.js +115 -13
  41. package/dist/tailwind.js.map +1 -0
  42. package/package.json +20 -19
package/dist/cli.js CHANGED
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawnSync } from "node:child_process";
3
3
  import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
4
- import { createRequire } from "node:module";
4
+ import { createRequire, findPackageJSON } from "node:module";
5
5
  import { basename, dirname, join } from "node:path";
6
- import { fileURLToPath } from "node:url";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
7
+ import { findTailwindEntryPoint } from "@santi020k/eslint-config-core";
7
8
  import {
8
9
  analyzeEslintConfig,
9
10
  ESLINT_CONFIG_FILENAMES,
@@ -11,6 +12,16 @@ import {
11
12
  } from "./agent-skill-generator.js";
12
13
  import { handleCompatibility, handleExplainRule } from "./cli-advanced.js";
13
14
  import { getExplicitConfigFeaturePackages, handleMigrateV3 } from "./cli-migration.js";
15
+ import {
16
+ addCompatibleConfigVersions,
17
+ createInstallCommand,
18
+ createInstallInvocation,
19
+ detectPackageManager,
20
+ findPnpmWorkspaceRoot,
21
+ getCatalogPreference,
22
+ getCompatibleConfigVersion
23
+ } from "./cli-package-manager.js";
24
+ import { handleExplainPreset } from "./cli-preset.js";
14
25
  import {
15
26
  handleBaseline,
16
27
  handleProfile,
@@ -18,6 +29,7 @@ import {
18
29
  handleSnapshotDiff
19
30
  } from "./cli-workflows.js";
20
31
  import { detectProjectOptions } from "./index.js";
32
+ import { findCssComponentClasses } from "./tailwind.js";
21
33
  const getDefaultConfigFilename = (cwd) => {
22
34
  const packageJsonPath = join(cwd, "package.json");
23
35
  if (!existsSync(packageJsonPath)) return "eslint.config.mjs";
@@ -80,6 +92,13 @@ const CATEGORY_PACKAGES = {
80
92
  testing: "@santi020k/eslint-config-testing",
81
93
  tools: "@santi020k/eslint-config-tools"
82
94
  };
95
+ const PRESET_INSTALL_PACKAGES = {
96
+ all: Object.values(CATEGORY_PACKAGES),
97
+ app: [CATEGORY_PACKAGES.testing, CATEGORY_PACKAGES.tools],
98
+ ci: [CATEGORY_PACKAGES.extensions, CATEGORY_PACKAGES.tools],
99
+ library: [CATEGORY_PACKAGES.extensions, CATEGORY_PACKAGES.tools],
100
+ monorepo: [CATEGORY_PACKAGES.extensions, CATEGORY_PACKAGES.tools]
101
+ };
83
102
  const getConfigPathIfPresent = (cwd) => {
84
103
  const configPath = ESLINT_CONFIG_FILENAMES.map((filename) => join(cwd, filename)).find((p) => existsSync(p));
85
104
  return configPath ?? null;
@@ -153,41 +172,13 @@ const getInstallPackages = (summary, declaredDependencies, explicitFeaturePackag
153
172
  for (const packageName of explicitFeaturePackages) {
154
173
  packages.add(packageName);
155
174
  }
175
+ for (const packageName of PRESET_INSTALL_PACKAGES[summary.preset] ?? []) {
176
+ packages.add(packageName);
177
+ }
156
178
  }
157
179
  if (summary.typescript) packages.add(TYPESCRIPT_PACKAGE_NAME);
158
180
  return [...packages].filter((packageName) => !declaredDependencies.has(packageName));
159
181
  };
160
- const createInstallCommand = (packageManager, packages, workspaceRoot = false) => {
161
- const packageList = packages.join(" ");
162
- switch (packageManager) {
163
- case "bun":
164
- return `bun add -d ${packageList}`;
165
- case "npm":
166
- return `npm install -D ${packageList}`;
167
- case "yarn":
168
- return `yarn add -D ${packageList}`;
169
- default:
170
- return `pnpm add -D${workspaceRoot ? " --workspace-root" : ""} ${packageList}`;
171
- }
172
- };
173
- const createInstallInvocation = (packageManager, packages, workspaceRoot = false) => {
174
- switch (packageManager) {
175
- case "bun":
176
- return ["bun", ["add", "-d", ...packages]];
177
- case "npm":
178
- return ["npm", ["install", "-D", ...packages]];
179
- case "yarn":
180
- return ["yarn", ["add", "-D", ...packages]];
181
- default:
182
- return ["pnpm", ["add", "-D", ...workspaceRoot ? ["--workspace-root"] : [], ...packages]];
183
- }
184
- };
185
- const detectPackageManager = (cwd) => {
186
- if (existsSync(join(cwd, "pnpm-lock.yaml")) || existsSync(join(cwd, "pnpm-workspace.yaml"))) return "pnpm";
187
- if (existsSync(join(cwd, "yarn.lock"))) return "yarn";
188
- if (existsSync(join(cwd, "bun.lockb")) || existsSync(join(cwd, "bun.lock"))) return "bun";
189
- return "npm";
190
- };
191
182
  const hasLintScript = (cwd) => {
192
183
  const packageJson = readPackageJson(cwd);
193
184
  return Boolean(packageJson?.scripts?.lint);
@@ -237,13 +228,13 @@ const detectWorkspaceProjects = (cwd) => {
237
228
  const createExplicitOptions = (cwd) => {
238
229
  const summary = getProjectSummary(cwd);
239
230
  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
- ];
231
+ const features = [.../* @__PURE__ */ new Set([
232
+ ...summary.extensions,
233
+ ...summary.formats,
234
+ ...summary.libraries,
235
+ ...summary.testing,
236
+ ...summary.tools
237
+ ])].sort();
247
238
  if (summary.typescript) options.push(" typescript: true,");
248
239
  if (summary.frameworks.length > 0) {
249
240
  options.push(
@@ -252,10 +243,12 @@ const createExplicitOptions = (cwd) => {
252
243
  " },"
253
244
  );
254
245
  }
255
- for (const [category, values] of optionalCategories) {
256
- if (values.length > 0) {
257
- options.push(` ${category}: ${JSON.stringify(values)},`);
258
- }
246
+ if (features.length > 0) {
247
+ options.push(
248
+ " features: {",
249
+ ...features.map((feature) => ` ${toPropertyKey(feature)}: true,`),
250
+ " },"
251
+ );
259
252
  }
260
253
  return options;
261
254
  };
@@ -280,6 +273,10 @@ const formatList = (values) => {
280
273
  const getProjectSummary = (cwd) => {
281
274
  const options = detectProjectOptions(cwd);
282
275
  const workspaceProjects = detectWorkspaceProjects(cwd);
276
+ let typescriptMode = "off";
277
+ if (options.typescript) {
278
+ typescriptMode = options.typescript === "syntax" ? "syntax" : "type-aware";
279
+ }
283
280
  return {
284
281
  detectedProjects: Object.keys(options.projects ?? {}).sort(),
285
282
  extensions: options.extensions ?? [],
@@ -292,13 +289,56 @@ const getProjectSummary = (cwd) => {
292
289
  testing: options.testing ?? [],
293
290
  tools: options.tools ?? [],
294
291
  typescript: Boolean(options.typescript),
292
+ typescriptMode,
295
293
  workspaceProjects
296
294
  };
297
295
  };
296
+ const getDetectedProjectPaths = (cwd) => {
297
+ const summary = getProjectSummary(cwd);
298
+ return summary.workspaceProjects.length > 0 ? [.../* @__PURE__ */ new Set([...summary.detectedProjects, ...summary.workspaceProjects])] : ["."];
299
+ };
300
+ const getTailwindUnknownClassPolicy = (detected, entryPoint, componentClasses = 0) => {
301
+ if (!detected) return "off";
302
+ if (!entryPoint) return "fallback-off";
303
+ return componentClasses > 0 ? "strict-with-css-components" : "strict";
304
+ };
305
+ const getTailwindEntryPoints = (cwd) => getDetectedProjectPaths(cwd).flatMap((projectPath) => {
306
+ const projectRoot = projectPath === "." ? cwd : join(cwd, projectPath);
307
+ const summary = getProjectSummary(projectRoot);
308
+ if (!summary.libraries.includes("tailwind")) return [];
309
+ const entryPoint = findTailwindEntryPoint(projectRoot) ?? null;
310
+ const componentClasses = entryPoint ? findCssComponentClasses(projectRoot, entryPoint).length : 0;
311
+ return [{
312
+ componentClasses,
313
+ entryPoint,
314
+ path: projectPath,
315
+ unknownClassPolicy: getTailwindUnknownClassPolicy(true, entryPoint, componentClasses)
316
+ }];
317
+ });
318
+ const getDeclarationProjectWarnings = (cwd) => getDetectedProjectPaths(cwd).flatMap((projectPath) => {
319
+ const projectRoot = projectPath === "." ? cwd : join(cwd, projectPath);
320
+ const summary = getProjectSummary(projectRoot);
321
+ if (summary.typescriptMode !== "syntax") return [];
322
+ const packageName = readPackageJson(projectRoot)?.name;
323
+ const packageLabel = typeof packageName === "string" ? `${projectPath} (${packageName})` : projectPath;
324
+ return [
325
+ `TypeScript declarations were detected in ${packageLabel}, but no supported package-local tsconfig was found. The package uses syntax-only linting to avoid a project-service parse failure. Add a tsconfig.json that includes the declarations for type-aware linting, or add their patterns to \`typescript.untypedFiles\` when a parent type-aware project intentionally owns them.`
326
+ ];
327
+ });
298
328
  const getInstallProjectSummary = (cwd) => {
299
329
  const rootSummary = getProjectSummary(cwd);
300
- const projectSummaries = rootSummary.detectedProjects.map((projectPath) => getProjectSummary(join(cwd, projectPath)));
330
+ const projectPaths = [.../* @__PURE__ */ new Set([
331
+ ...rootSummary.detectedProjects,
332
+ ...rootSummary.workspaceProjects
333
+ ])];
334
+ const projectSummaries = projectPaths.map((projectPath) => getProjectSummary(join(cwd, projectPath)));
301
335
  const summaries = [rootSummary, ...projectSummaries];
336
+ let typescriptMode = "off";
337
+ if (summaries.some((summary) => summary.typescriptMode === "type-aware")) {
338
+ typescriptMode = "type-aware";
339
+ } else if (summaries.some((summary) => summary.typescriptMode === "syntax")) {
340
+ typescriptMode = "syntax";
341
+ }
302
342
  return {
303
343
  ...rootSummary,
304
344
  extensions: [...new Set(summaries.flatMap((summary) => summary.extensions))],
@@ -307,7 +347,8 @@ const getInstallProjectSummary = (cwd) => {
307
347
  libraries: [...new Set(summaries.flatMap((summary) => summary.libraries))],
308
348
  testing: [...new Set(summaries.flatMap((summary) => summary.testing))],
309
349
  tools: [...new Set(summaries.flatMap((summary) => summary.tools))],
310
- typescript: summaries.some((summary) => summary.typescript)
350
+ typescript: summaries.some((summary) => summary.typescript),
351
+ typescriptMode
311
352
  };
312
353
  };
313
354
  const getWorkspaceDeclaredConfigPackages = (cwd, summary) => {
@@ -368,6 +409,7 @@ const printUsage = () => {
368
409
  " init Create eslint.config.js/mjs if missing",
369
410
  " update Regenerate eslint.config.js/mjs from detection",
370
411
  " explain Print detected v3 config inputs",
412
+ " explain-preset Compare the current config with a preset",
371
413
  " inspect Print detected inputs and active config features",
372
414
  " install Install missing packages for detected v3 features",
373
415
  " doctor Check project setup for common v3 adoption issues",
@@ -381,13 +423,14 @@ const printUsage = () => {
381
423
  " generate-skill Generate AI agent standards files",
382
424
  "",
383
425
  "Options:",
426
+ " --analyze-source explain-preset: lint source and preview autofix without writing",
384
427
  " --force Overwrite existing generated skill sections/files",
385
428
  " --check Verify generated files, migrations, or snapshots without writing",
386
429
  " --concurrency profile: off, auto, or a worker count",
387
430
  " --create generate-skill: scaffold a root AGENTS.md when missing",
388
431
  " --dry-run install: print the detected install command without running it",
389
432
  " --explicit init: write detected settings explicitly",
390
- " --file profile/snapshot/diff: representative file or lint target (repeatable)",
433
+ " --file explain/explain-preset/profile/snapshot/diff: file or lint target (repeatable)",
391
434
  " --fix doctor: safely repair generated config and package metadata",
392
435
  " --full migrate --to v3: choose the batteries-included package",
393
436
  " --json Print JSON for commands that support it",
@@ -395,16 +438,107 @@ const printUsage = () => {
395
438
  " --max-duration profile: maximum duration in milliseconds",
396
439
  " --max-rule-time profile: maximum time for the slowest rule in milliseconds",
397
440
  " --max-warnings profile: maximum warning count",
441
+ " --compatibility explain-preset: write a temporary compatibility override",
442
+ " --output explain-preset: compatibility override path",
398
443
  " --preset baseline: enable ci or pedantic strict mode before suppressing",
399
444
  " --prune baseline: remove suppressions for resolved violations",
400
445
  " --snapshot-path snapshot/diff: override .eslint-config-snapshot.json",
401
446
  " --to migrate: target version (v2 or v3)",
447
+ " --verbose doctor: print per-project activation details",
402
448
  " --with-eslint-mcp generate-skill: scaffold the official ESLint MCP server",
403
449
  " --write Apply safe migrations for commands that support it",
404
450
  " --help, -h Show this help message",
405
451
  " --version, -v Show CLI version"
406
452
  ].join("\n"));
407
453
  };
454
+ const COMMAND_OPTIONS = {
455
+ baseline: ["--json", "--preset", "--prune"],
456
+ compatibility: ["--json"],
457
+ diff: ["--file", "--json", "--snapshot-path"],
458
+ docs: [],
459
+ doctor: ["--fix", "--json", "--lite-install", "--verbose"],
460
+ explain: ["--file", "--json"],
461
+ "explain-preset": ["--analyze-source", "--compatibility", "--file", "--json", "--output"],
462
+ "generate-skill": ["--check", "--create", "--force", "--with-eslint-mcp"],
463
+ init: ["--check", "--explicit"],
464
+ inspect: ["--json"],
465
+ install: ["--dry-run"],
466
+ migrate: ["--check", "--full", "--json", "--to", "--write"],
467
+ profile: ["--concurrency", "--file", "--json", "--max-duration", "--max-rule-time", "--max-warnings"],
468
+ snapshot: ["--check", "--file", "--json", "--snapshot-path"],
469
+ update: []
470
+ };
471
+ const VALUE_OPTIONS = /* @__PURE__ */ new Set([
472
+ "--concurrency",
473
+ "--file",
474
+ "--max-duration",
475
+ "--max-rule-time",
476
+ "--max-warnings",
477
+ "--output",
478
+ "--preset",
479
+ "--snapshot-path",
480
+ "--to"
481
+ ]);
482
+ const printCommandUsage = (command) => {
483
+ let positional = "";
484
+ if (command === "explain") positional = " [rule]";
485
+ else if (command === "explain-preset") positional = " <preset>";
486
+ const options = COMMAND_OPTIONS[command] ?? [];
487
+ console.log([
488
+ "Usage: basic-eslint <command> [options]",
489
+ "",
490
+ `Usage: basic-eslint ${command}${positional}${options.length > 0 ? " [options]" : ""}`,
491
+ ...options.length > 0 ? ["", "Options:", ...options.map((option) => ` ${option}`)] : []
492
+ ].join("\n"));
493
+ };
494
+ const validateCommandArguments = (command, args) => {
495
+ const allowed = COMMAND_OPTIONS[command];
496
+ if (!allowed) return { ok: true };
497
+ const allowedOptions = new Set(allowed);
498
+ const positionals = [];
499
+ for (let index = 0; index < args.length; index++) {
500
+ const argument = args[index];
501
+ if (!argument.startsWith("-")) {
502
+ positionals.push(argument);
503
+ continue;
504
+ }
505
+ const option = argument.includes("=") ? argument.slice(0, argument.indexOf("=")) : argument;
506
+ if (!allowedOptions.has(option)) {
507
+ console.error(`Unknown option for ${command}: ${option}`);
508
+ process.exitCode = 1;
509
+ return { ok: false };
510
+ }
511
+ if (argument.includes("=") && !VALUE_OPTIONS.has(option)) {
512
+ console.error(`Option ${option} does not accept a value.`);
513
+ process.exitCode = 1;
514
+ return { ok: false };
515
+ }
516
+ if (VALUE_OPTIONS.has(option) && !argument.includes("=")) {
517
+ const value = args[index + 1];
518
+ if (!value || value.startsWith("-")) {
519
+ console.error(
520
+ ["--max-duration", "--max-rule-time", "--max-warnings"].includes(option) ? `${option} must be a non-negative number.` : `Option ${option} requires a value.`
521
+ );
522
+ process.exitCode = 1;
523
+ return { ok: false };
524
+ }
525
+ index++;
526
+ } else if (VALUE_OPTIONS.has(option) && argument.endsWith("=")) {
527
+ console.error(
528
+ ["--max-duration", "--max-rule-time", "--max-warnings"].includes(option) ? `${option} must be a non-negative number.` : `Option ${option} requires a value.`
529
+ );
530
+ process.exitCode = 1;
531
+ return { ok: false };
532
+ }
533
+ }
534
+ const maximumPositionals = command === "explain" || command === "explain-preset" ? 1 : 0;
535
+ if (positionals.length > maximumPositionals) {
536
+ console.error(`Unexpected argument for ${command}: ${positionals[maximumPositionals]}`);
537
+ process.exitCode = 1;
538
+ return { ok: false };
539
+ }
540
+ return { ok: true, positional: positionals[0] };
541
+ };
408
542
  const handleInit = (cwd = process.cwd(), check = false, explicit = false) => {
409
543
  const configPath = resolveConfigPath(cwd);
410
544
  if (check) {
@@ -466,7 +600,8 @@ const handleInspect = async (cwd = process.cwd(), json = false) => {
466
600
  const payload = {
467
601
  activeConfig,
468
602
  detected: summary,
469
- packageManager: detectPackageManager(cwd)
603
+ packageManager: detectPackageManager(cwd),
604
+ tailwindEntryPoints: getTailwindEntryPoints(cwd)
470
605
  };
471
606
  if (json) {
472
607
  console.log(JSON.stringify(payload, null, 2));
@@ -490,31 +625,135 @@ const handleInspect = async (cwd = process.cwd(), json = false) => {
490
625
  ].join("\n"));
491
626
  };
492
627
  const semver = createRequire(import.meta.url)("semver");
493
- const readPackageMetadataFromMain = (mainPath, packageName) => {
628
+ const intersectTwoSemverRanges = (left, right) => {
629
+ if (semver.subset(left, right)) return left;
630
+ if (semver.subset(right, left)) return right;
631
+ const intersections = /* @__PURE__ */ new Set();
632
+ const leftRange = new semver.Range(left);
633
+ const rightRange = new semver.Range(right);
634
+ for (const leftComparators of leftRange.set) {
635
+ for (const rightComparators of rightRange.set) {
636
+ const candidate = [...leftComparators, ...rightComparators].map((comparator) => comparator.value).filter(Boolean).join(" ");
637
+ const normalized = new semver.Range(candidate || "*").range || "*";
638
+ if (semver.minVersion(normalized)) intersections.add(normalized);
639
+ }
640
+ }
641
+ return intersections.size > 0 ? [...intersections].join(" || ") : null;
642
+ };
643
+ const intersectSemverRanges = (ranges) => {
644
+ const validRanges = ranges.filter((range) => semver.validRange(range) !== null);
645
+ if (validRanges.length !== ranges.length || validRanges.length === 0) return null;
646
+ let intersection = validRanges[0];
647
+ for (const range of validRanges.slice(1)) {
648
+ intersection = intersectTwoSemverRanges(intersection, range) ?? "";
649
+ if (!intersection) return null;
650
+ }
651
+ return intersection;
652
+ };
653
+ const readPackageMetadataFromMain = (mainPath) => {
494
654
  let directory = dirname(mainPath);
495
655
  for (let depth = 0; depth < 4; depth++) {
496
656
  const packagePath = join(directory, "package.json");
497
657
  if (existsSync(packagePath)) {
498
658
  const metadata = JSON.parse(readFileSync(packagePath, "utf8"));
499
- if (metadata.name === packageName) return metadata;
659
+ if (metadata.name && metadata.version) return metadata;
500
660
  }
501
661
  directory = dirname(directory);
502
662
  }
503
663
  return null;
504
664
  };
505
665
  const resolvePackageMetadata = (cwd, packageName) => {
506
- try {
507
- const projectRequire = createRequire(join(cwd, "package.json"));
666
+ const candidateRoots = [...new Set([
667
+ cwd,
668
+ findPnpmWorkspaceRoot(cwd)
669
+ ].filter((root) => Boolean(root)))];
670
+ for (const candidateRoot of candidateRoots) {
508
671
  try {
509
- return readPackageMetadataFromMain(projectRequire.resolve(packageName), packageName);
672
+ const manifestPath = findPackageJSON(
673
+ packageName,
674
+ pathToFileURL(join(candidateRoot, "package.json"))
675
+ );
676
+ if (manifestPath) {
677
+ const metadata = JSON.parse(readFileSync(manifestPath, "utf8"));
678
+ return metadata;
679
+ }
510
680
  } catch {
511
- const integrationsMain = projectRequire.resolve(INTEGRATIONS_PACKAGE_NAME);
512
- const integrationsRequire = createRequire(integrationsMain);
513
- return readPackageMetadataFromMain(integrationsRequire.resolve(packageName), packageName);
514
681
  }
515
- } catch {
516
- return null;
682
+ try {
683
+ const projectRequire = createRequire(join(candidateRoot, "package.json"));
684
+ try {
685
+ const metadata = readPackageMetadataFromMain(projectRequire.resolve(packageName));
686
+ if (metadata) return metadata;
687
+ } catch {
688
+ const integrationsMain = projectRequire.resolve(INTEGRATIONS_PACKAGE_NAME);
689
+ const integrationsRequire = createRequire(integrationsMain);
690
+ const metadata = readPackageMetadataFromMain(integrationsRequire.resolve(packageName));
691
+ if (metadata) return metadata;
692
+ }
693
+ } catch {
694
+ }
695
+ for (const projectPath of detectWorkspaceProjects(candidateRoot)) {
696
+ const metadata = readPackageJson(join(candidateRoot, projectPath));
697
+ if (metadata?.name === packageName) return metadata;
698
+ }
699
+ }
700
+ return null;
701
+ };
702
+ const assessConsumerNodeEngine = (cwd, packageJson) => {
703
+ const declaredConfigPackages = [...getDeclaredDependencyNames(packageJson)].filter((packageName) => packageName.startsWith("@santi020k/eslint-config-"));
704
+ const requiredRanges = declaredConfigPackages.flatMap((packageName) => {
705
+ const range = resolvePackageMetadata(cwd, packageName)?.engines?.node;
706
+ return range ? [range] : [];
707
+ });
708
+ const requiredRange = intersectSemverRanges(requiredRanges);
709
+ const engines = packageJson?.engines;
710
+ const consumerRange = engines && typeof engines === "object" && !Array.isArray(engines) ? engines.node : null;
711
+ const normalizedConsumerRange = typeof consumerRange === "string" ? consumerRange : null;
712
+ if (requiredRanges.length === 0) {
713
+ return {
714
+ compatible: true,
715
+ consumerRange: normalizedConsumerRange,
716
+ requiredRange: null,
717
+ targetRange: null
718
+ };
719
+ }
720
+ if (!requiredRange) {
721
+ return {
722
+ compatible: false,
723
+ consumerRange: normalizedConsumerRange,
724
+ requiredRange: null,
725
+ targetRange: null
726
+ };
517
727
  }
728
+ if (!normalizedConsumerRange || !semver.validRange(normalizedConsumerRange)) {
729
+ return {
730
+ compatible: false,
731
+ consumerRange: normalizedConsumerRange,
732
+ requiredRange,
733
+ targetRange: requiredRange
734
+ };
735
+ }
736
+ const compatible = requiredRanges.every((range) => semver.subset(normalizedConsumerRange, range));
737
+ return {
738
+ compatible,
739
+ consumerRange: normalizedConsumerRange,
740
+ requiredRange,
741
+ targetRange: compatible ? null : intersectSemverRanges([normalizedConsumerRange, ...requiredRanges])
742
+ };
743
+ };
744
+ const getConsumerNodeEngineWarning = (cwd, packageJson) => {
745
+ const assessment = assessConsumerNodeEngine(cwd, packageJson);
746
+ if (assessment.compatible) return null;
747
+ if (!assessment.requiredRange) {
748
+ return "Installed config packages do not share a compatible Node engine range.";
749
+ }
750
+ if (!assessment.consumerRange) {
751
+ return `package.json does not declare engines.node; installed config packages require ${assessment.requiredRange}.`;
752
+ }
753
+ if (!semver.validRange(assessment.consumerRange)) {
754
+ return `package.json engines.node ${assessment.consumerRange} is invalid; installed config packages require ${assessment.requiredRange}.`;
755
+ }
756
+ return `package.json engines.node ${assessment.consumerRange} permits unsupported runtimes; installed config packages require ${assessment.requiredRange}.`;
518
757
  };
519
758
  const getAstroDoctorNodeWarning = (metadata) => {
520
759
  const nodeRange = metadata.engines?.node;
@@ -562,7 +801,11 @@ const validateConfigContent = (configContent, hasV1FrameworkImports, declaredDep
562
801
  if (hasV1FrameworkImports) {
563
802
  warnings.push("Config still imports v1 framework packages. Run `basic-eslint migrate --write` or switch to framework booleans.");
564
803
  }
565
- const usesBasicComposer = /from\s*['"]@santi020k\/eslint-config-(?:basic|full|lite)['"]/.test(configContent) && /\b(?:defineConfig|eslintConfig)\s*\(/.test(configContent);
804
+ const usesComposerCall = /from\s*['"]@santi020k\/eslint-config-(?:basic|full|lite)['"]/.test(configContent) && /\b(?:defineConfig|eslintConfig)\s*\(/.test(configContent);
805
+ const usesRecommendedComposer = /from\s*['"]@santi020k\/eslint-config-(?:basic|full)\/recommended['"]/.test(
806
+ configContent
807
+ );
808
+ const usesBasicComposer = usesComposerCall || usesRecommendedComposer;
566
809
  const hasDetectedProjectScopes = summary.workspaceProjects.every((project) => summary.detectedProjects.includes(project));
567
810
  if (summary.workspaceProjects.length > 0 && !configContent.includes("projects:") && !(usesBasicComposer && hasDetectedProjectScopes)) {
568
811
  warnings.push("Workspace packages were detected, but the root config does not use `projects` scoping.");
@@ -585,6 +828,16 @@ const validateConfigContent = (configContent, hasV1FrameworkImports, declaredDep
585
828
  );
586
829
  }
587
830
  }
831
+ const missingPackages = getInstallPackages(
832
+ summary,
833
+ declaredDependencies,
834
+ getExplicitConfigFeaturePackages(configContent)
835
+ );
836
+ if (missingPackages.length > 0) {
837
+ warnings.push(
838
+ `The active configuration requires undeclared packages: ${missingPackages.join(", ")}. Run \`basic-eslint install\` to add the modular v3 dependency set.`
839
+ );
840
+ }
588
841
  return warnings;
589
842
  };
590
843
  const validateProjectSetup = (cwd, configPath, activeConfig, hasV1FrameworkImports) => {
@@ -634,7 +887,13 @@ const checkDuplicateEslint = (cwd, warnings) => {
634
887
  );
635
888
  }
636
889
  };
637
- const buildDoctorDiagnosis = (cwd, configPath, configContent, hasV1FrameworkImports, activeConfig, declaredDependencies, summary) => {
890
+ const getTailwindEntryPointWarnings = (cwd) => getTailwindEntryPoints(cwd).flatMap(({ entryPoint, path: projectPath }) => {
891
+ if (entryPoint) return [];
892
+ return [
893
+ `Tailwind CSS was detected in ${projectPath}, but no conventional CSS entry point was found. \`better-tailwindcss/no-unknown-classes\` falls back to off; set \`tailwind.entryPoint\` to enable strict validation or \`tailwind.noUnknownClasses: false\` to document a mixed CSS system.`
894
+ ];
895
+ });
896
+ const buildDoctorDiagnosis = (cwd, packageJson, configPath, configContent, hasV1FrameworkImports, activeConfig, declaredDependencies, summary) => {
638
897
  const { issues, warnings } = validateProjectSetup(cwd, configPath, activeConfig, hasV1FrameworkImports);
639
898
  if (configContent) {
640
899
  warnings.push(...validateConfigContent(configContent, hasV1FrameworkImports, declaredDependencies, summary));
@@ -642,9 +901,238 @@ const buildDoctorDiagnosis = (cwd, configPath, configContent, hasV1FrameworkImpo
642
901
  const astroDoctorEnabled = checkAstroDoctorStatus(activeConfig, configContent, summary, warnings);
643
902
  warnings.push(...getAstroDoctorCompatibilityWarnings(cwd, astroDoctorEnabled));
644
903
  checkDuplicateEslint(cwd, warnings);
904
+ warnings.push(...getDeclarationProjectWarnings(cwd));
905
+ warnings.push(...getTailwindEntryPointWarnings(cwd));
906
+ const nodeEngineWarning = getConsumerNodeEngineWarning(cwd, packageJson);
907
+ if (nodeEngineWarning) warnings.push(nodeEngineWarning);
645
908
  return { issues, warnings };
646
909
  };
647
- const outputDoctorResult = (json, configPath, packageManager, summary, liteInstallCommand, issues, warnings, fixes = []) => {
910
+ const DOCTOR_FEATURE_ALIASES = {
911
+ "astro-doctor": "Astro Doctor",
912
+ "best-practices": "Best Practices",
913
+ compat: "Browser Compat",
914
+ nest: "NestJS",
915
+ next: "Next.js",
916
+ node: "Node.js",
917
+ solid: "SolidJS",
918
+ "tanstack-start": "TanStack Start",
919
+ tailwind: "Tailwind CSS"
920
+ };
921
+ const normalizeDoctorFeature = (value) => {
922
+ const comparableValue = DOCTOR_FEATURE_ALIASES[value] ?? value;
923
+ return comparableValue.toLowerCase().replaceAll(/[^a-z0-9]/g, "");
924
+ };
925
+ const isDoctorFeatureEnabled = (name, enabled) => {
926
+ const normalizedName = normalizeDoctorFeature(name);
927
+ return enabled.some((value) => normalizeDoctorFeature(value) === normalizedName);
928
+ };
929
+ const getTypeScriptConfig = (cwd) => [
930
+ "tsconfig.json",
931
+ "tsconfig.base.json",
932
+ "tsconfig.app.json",
933
+ "tsconfig.node.json",
934
+ "tsconfig.eslint.json"
935
+ ].find((fileName) => existsSync(join(cwd, fileName))) ?? null;
936
+ const getCategoryPackageInstalled = (cwd, packageName) => [
937
+ packageName,
938
+ INTEGRATIONS_PACKAGE_NAME,
939
+ FULL_PACKAGE_NAME
940
+ ].some((candidate) => resolvePackageMetadata(cwd, candidate) !== null);
941
+ const createDoctorFeatureActivations = (cwd, category, detected, enabled, includeEnabled = false) => {
942
+ const names = [...detected];
943
+ if (includeEnabled) {
944
+ for (const enabledName of enabled) {
945
+ if (!isDoctorFeatureEnabled(enabledName, names)) names.push(enabledName);
946
+ }
947
+ }
948
+ return names.map((name) => {
949
+ const packageName = category === "frameworks" ? Object.entries(FRAMEWORK_KEY_TO_PACKAGE).find(([framework]) => isDoctorFeatureEnabled(name, [framework]))?.[1] : CATEGORY_PACKAGES[category];
950
+ if (!packageName) throw new Error(`Unknown doctor ${category} feature: ${name}`);
951
+ const installed = getCategoryPackageInstalled(cwd, packageName);
952
+ const isDetected = isDoctorFeatureEnabled(name, detected);
953
+ const isEnabled = installed && isDoctorFeatureEnabled(name, enabled);
954
+ let reason;
955
+ if (!installed) {
956
+ reason = isDetected ? `Detected, but ${packageName} is not installed.` : `Enabled in the active config, but ${packageName} is not installed.`;
957
+ } else if (!isEnabled) {
958
+ reason = "Detected and installed, but the active config did not load this pack.";
959
+ }
960
+ return {
961
+ detected: isDetected,
962
+ enabled: isEnabled,
963
+ installed,
964
+ name,
965
+ package: packageName,
966
+ ...reason ? { reason } : {}
967
+ };
968
+ });
969
+ };
970
+ const getInactiveDoctorPackages = (cwd, detectedFrameworks, summary, activeConfig) => {
971
+ const inactivePackages = Object.entries(FRAMEWORK_KEY_TO_PACKAGE).filter(([framework, packageName]) => !detectedFrameworks.includes(framework) && !isDoctorFeatureEnabled(framework, activeConfig?.frameworks ?? []) && resolvePackageMetadata(cwd, packageName) !== null).map(([, packageName]) => ({
972
+ package: packageName,
973
+ reason: "Installed, but no matching framework signal was detected in this project."
974
+ }));
975
+ for (const [category, packageName] of Object.entries(CATEGORY_PACKAGES)) {
976
+ if (summary[category].length === 0 && (activeConfig?.[category]?.length ?? 0) === 0 && resolvePackageMetadata(cwd, packageName) !== null) {
977
+ inactivePackages.push({
978
+ package: packageName,
979
+ reason: `Installed, but no ${category} signals were detected in this project.`
980
+ });
981
+ }
982
+ }
983
+ return inactivePackages;
984
+ };
985
+ const getDoctorProjectActivations = async (cwd) => {
986
+ const rootSummary = getProjectSummary(cwd);
987
+ const projectPaths = [.../* @__PURE__ */ new Set([
988
+ ".",
989
+ ...rootSummary.detectedProjects,
990
+ ...rootSummary.workspaceProjects
991
+ ])].sort((left, right) => {
992
+ if (left === ".") return -1;
993
+ if (right === ".") return 1;
994
+ return left.localeCompare(right);
995
+ });
996
+ return Promise.all(projectPaths.map(async (projectPath) => {
997
+ const projectRoot = projectPath === "." ? cwd : join(cwd, projectPath);
998
+ const summary = getProjectSummary(projectRoot);
999
+ const projectActiveConfig = await analyzeEslintConfig(cwd, { projectPath, projectPaths }) ?? void 0;
1000
+ const tsconfig = getTypeScriptConfig(projectRoot);
1001
+ const typescriptInstalled = resolvePackageMetadata(projectRoot, TYPESCRIPT_PACKAGE_NAME) !== null;
1002
+ const typescriptEnabled = summary.typescript && Boolean(projectActiveConfig?.typescript);
1003
+ const tailwindDetected = summary.libraries.includes("tailwind");
1004
+ const tailwindEntryPoint = tailwindDetected ? findTailwindEntryPoint(projectRoot) ?? null : null;
1005
+ const tailwindComponentClasses = tailwindEntryPoint ? findCssComponentClasses(projectRoot, tailwindEntryPoint).length : 0;
1006
+ let typescriptMode = "off";
1007
+ let typescriptReason;
1008
+ if (typescriptEnabled) {
1009
+ typescriptMode = summary.typescriptMode;
1010
+ }
1011
+ if (!typescriptInstalled && summary.typescript) {
1012
+ typescriptReason = summary.typescriptMode === "syntax" ? "TypeScript declarations were detected, but TypeScript is not installed." : "A tsconfig was detected, but TypeScript is not installed.";
1013
+ } else if (typescriptInstalled && !summary.typescript) {
1014
+ typescriptReason = "Installed, but no supported tsconfig was detected in this project.";
1015
+ } else if (typescriptInstalled && summary.typescript && !typescriptEnabled) {
1016
+ typescriptReason = "Detected and installed, but the active config did not enable TypeScript.";
1017
+ }
1018
+ return {
1019
+ extensions: createDoctorFeatureActivations(
1020
+ projectRoot,
1021
+ "extensions",
1022
+ summary.extensions,
1023
+ projectActiveConfig?.extensions ?? [],
1024
+ true
1025
+ ),
1026
+ formats: createDoctorFeatureActivations(
1027
+ projectRoot,
1028
+ "formats",
1029
+ summary.formats,
1030
+ projectActiveConfig?.formats ?? [],
1031
+ true
1032
+ ),
1033
+ frameworks: createDoctorFeatureActivations(
1034
+ projectRoot,
1035
+ "frameworks",
1036
+ summary.frameworks,
1037
+ projectActiveConfig?.frameworks ?? [],
1038
+ true
1039
+ ),
1040
+ ignores: projectActiveConfig?.ignores ?? [],
1041
+ inactivePackages: getInactiveDoctorPackages(projectRoot, summary.frameworks, summary, projectActiveConfig),
1042
+ libraries: createDoctorFeatureActivations(
1043
+ projectRoot,
1044
+ "libraries",
1045
+ summary.libraries,
1046
+ projectActiveConfig?.libraries ?? [],
1047
+ true
1048
+ ),
1049
+ path: projectPath,
1050
+ runtime: summary.runtime,
1051
+ tailwind: {
1052
+ componentClasses: tailwindComponentClasses,
1053
+ detected: tailwindDetected,
1054
+ entryPoint: tailwindEntryPoint,
1055
+ unknownClassPolicy: getTailwindUnknownClassPolicy(
1056
+ tailwindDetected,
1057
+ tailwindEntryPoint,
1058
+ tailwindComponentClasses
1059
+ )
1060
+ },
1061
+ testing: createDoctorFeatureActivations(
1062
+ projectRoot,
1063
+ "testing",
1064
+ summary.testing,
1065
+ projectActiveConfig?.testing ?? [],
1066
+ true
1067
+ ),
1068
+ tools: createDoctorFeatureActivations(
1069
+ projectRoot,
1070
+ "tools",
1071
+ summary.tools,
1072
+ projectActiveConfig?.tools ?? [],
1073
+ true
1074
+ ),
1075
+ typescript: {
1076
+ detected: summary.typescript,
1077
+ enabled: typescriptEnabled,
1078
+ installed: typescriptInstalled,
1079
+ mode: typescriptMode,
1080
+ package: TYPESCRIPT_PACKAGE_NAME,
1081
+ ...typescriptReason ? { reason: typescriptReason } : {},
1082
+ tsconfig
1083
+ }
1084
+ };
1085
+ }));
1086
+ };
1087
+ const formatDoctorActivation = (features) => features.length === 0 ? "none" : features.map((feature) => {
1088
+ const states = [
1089
+ feature.installed ? "I" : "-",
1090
+ feature.detected ? "D" : "-",
1091
+ feature.enabled ? "E" : "-"
1092
+ ].join("");
1093
+ return `${feature.name}[${states}]`;
1094
+ }).join(",");
1095
+ const formatDoctorProjectTable = (projects) => [
1096
+ "",
1097
+ "Per-project activation (I=installed, D=detected, E=enabled):",
1098
+ "Project | Runtime | TypeScript | Tailwind entry | Frameworks | Formats | Libraries | Extensions | Testing | Tools | Ignores",
1099
+ "--- | --- | --- | --- | --- | --- | --- | --- | --- | --- | ---",
1100
+ ...projects.flatMap((project) => {
1101
+ const typescriptStates = [
1102
+ project.typescript.installed ? "I" : "-",
1103
+ project.typescript.detected ? "D" : "-",
1104
+ project.typescript.enabled ? "E" : "-"
1105
+ ].join("");
1106
+ const row = [
1107
+ project.path,
1108
+ project.runtime,
1109
+ `${project.typescript.mode}:${project.typescript.tsconfig ?? "none"}[${typescriptStates}]`,
1110
+ project.tailwind.componentClasses > 0 ? `${project.tailwind.entryPoint ?? "none"} (${project.tailwind.componentClasses} CSS classes)` : project.tailwind.entryPoint ?? "none",
1111
+ formatDoctorActivation(project.frameworks),
1112
+ formatDoctorActivation(project.formats),
1113
+ formatDoctorActivation(project.libraries),
1114
+ formatDoctorActivation(project.extensions),
1115
+ formatDoctorActivation(project.testing),
1116
+ formatDoctorActivation(project.tools),
1117
+ formatList(project.ignores)
1118
+ ].join(" | ");
1119
+ const explanations = [
1120
+ project.typescript.reason,
1121
+ ...project.frameworks.map((feature) => feature.reason),
1122
+ ...project.formats.map((feature) => feature.reason),
1123
+ ...project.libraries.map((feature) => feature.reason),
1124
+ ...project.extensions.map((feature) => feature.reason),
1125
+ ...project.testing.map((feature) => feature.reason),
1126
+ ...project.tools.map((feature) => feature.reason),
1127
+ ...project.inactivePackages.map((item) => `${item.package}: ${item.reason}`)
1128
+ ].filter((reason) => Boolean(reason));
1129
+ return [
1130
+ row,
1131
+ ...explanations.map((reason) => ` ${project.path}: ${reason}`)
1132
+ ];
1133
+ })
1134
+ ];
1135
+ const outputDoctorResult = (json, verbose, configPath, packageManager, summary, installCommand, requiredPackages, issues, warnings, projects, fixes = []) => {
648
1136
  let status = "passed";
649
1137
  if (issues.length > 0) {
650
1138
  status = "failed";
@@ -654,9 +1142,11 @@ const outputDoctorResult = (json, configPath, packageManager, summary, liteInsta
654
1142
  const payload = {
655
1143
  configFile: configPath ? basename(configPath) : null,
656
1144
  fixes,
1145
+ ...installCommand ? { installCommand } : {},
657
1146
  issues,
658
- liteInstallCommand,
659
1147
  packageManager,
1148
+ projects,
1149
+ requiredPackages,
660
1150
  status,
661
1151
  warnings,
662
1152
  workspaceProjects: summary.workspaceProjects
@@ -671,6 +1161,9 @@ const outputDoctorResult = (json, configPath, packageManager, summary, liteInsta
671
1161
  `- Package manager: ${payload.packageManager}`,
672
1162
  `- Config file: ${payload.configFile ?? "none"}`,
673
1163
  `- Workspace projects: ${formatList(summary.workspaceProjects)}`,
1164
+ `- Required packages: ${formatList(requiredPackages)}`,
1165
+ ...installCommand ? [`- Install command: ${installCommand}`] : [],
1166
+ ...verbose ? formatDoctorProjectTable(projects) : [],
674
1167
  ...fixes.length > 0 ? ["", "Fixes applied:", ...fixes.map((fix) => `- ${fix}`)] : [],
675
1168
  ...issues.length > 0 ? ["", "Issues:", ...issues.map((issue) => `- ${issue}`)] : [],
676
1169
  ...warnings.length > 0 ? ["", "Warnings:", ...warnings.map((warning) => `- ${warning}`)] : []
@@ -715,6 +1208,15 @@ const applyDoctorFixes = (cwd, packageJson, configPath, configContent, summary)
715
1208
  fixes.push(`Declared ${packageName}.`);
716
1209
  packageChanged = true;
717
1210
  }
1211
+ const nodeEngine = assessConsumerNodeEngine(cwd, packageJson);
1212
+ if (!nodeEngine.compatible && nodeEngine.targetRange) {
1213
+ updated.engines ??= {};
1214
+ updated.engines.node = nodeEngine.targetRange;
1215
+ packageChanged = true;
1216
+ fixes.push(
1217
+ nodeEngine.consumerRange ? `Changed engines.node from ${nodeEngine.consumerRange} to ${nodeEngine.targetRange}.` : `Declared engines.node as ${nodeEngine.targetRange}.`
1218
+ );
1219
+ }
718
1220
  if (packageChanged) {
719
1221
  const packagePath = join(cwd, "package.json");
720
1222
  const backupPath = createDoctorBackupPath(packagePath);
@@ -741,40 +1243,65 @@ const applyDoctorFixes = (cwd, packageJson, configPath, configContent, summary)
741
1243
  return fixes;
742
1244
  };
743
1245
  const hasV1FrameworkPackageImports = (configContent) => configContent !== null && Object.keys(FRAMEWORK_PACKAGE_TO_KEY).some((packageName) => configContent.includes(packageName));
744
- 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);
1246
+ const handleDoctor = async (cwd = process.cwd(), json = false, liteInstall = false, fix = false, verbose = false) => {
1247
+ const pnpmWorkspaceRoot = findPnpmWorkspaceRoot(cwd);
1248
+ const projectRoot = pnpmWorkspaceRoot ?? cwd;
1249
+ const packageManager = detectPackageManager(projectRoot);
1250
+ const workspaceRoot = packageManager === "pnpm" && pnpmWorkspaceRoot !== void 0;
1251
+ const catalog = workspaceRoot ? getCatalogPreference(readPackageJson(projectRoot)) : false;
1252
+ let configPath = getConfigPathIfPresent(projectRoot);
1253
+ let packageJson = readPackageJson(projectRoot);
749
1254
  let declaredDependencies = getDeclaredDependencyNames(packageJson);
750
- let summary = getInstallProjectSummary(cwd);
751
- let activeConfig = await analyzeEslintConfig(cwd);
1255
+ let summary = getInstallProjectSummary(projectRoot);
1256
+ let activeConfig = await analyzeEslintConfig(projectRoot);
752
1257
  let configContent = configPath ? readFileSync(configPath, "utf8") : null;
753
- let liteInstallPackages = getLiteInstallPackages(summary, declaredDependencies);
754
- let liteInstallCommand = createInstallCommand(packageManager, liteInstallPackages, workspaceRoot);
755
1258
  let hasV1FrameworkImports = hasV1FrameworkPackageImports(configContent);
756
1259
  if (liteInstall) {
1260
+ const liteInstallPackages = getLiteInstallPackages(summary, declaredDependencies);
1261
+ const liteInstallCommand = createInstallCommand(
1262
+ packageManager,
1263
+ addCompatibleConfigVersions(
1264
+ liteInstallPackages,
1265
+ getCompatibleConfigVersion(packageJson, getCliVersion(), pnpmWorkspaceRoot)
1266
+ ),
1267
+ workspaceRoot,
1268
+ catalog
1269
+ );
757
1270
  if (json) {
758
- console.log(JSON.stringify({ command: liteInstallCommand, packageManager, packages: liteInstallPackages }, null, 2));
1271
+ console.log(JSON.stringify({
1272
+ command: liteInstallCommand,
1273
+ packageManager,
1274
+ packages: liteInstallPackages
1275
+ }, null, 2));
759
1276
  } else {
760
1277
  console.log(liteInstallCommand);
761
1278
  }
762
1279
  return;
763
1280
  }
764
- const fixes = fix ? applyDoctorFixes(cwd, packageJson, configPath, configContent, summary) : [];
1281
+ const fixes = fix ? applyDoctorFixes(projectRoot, packageJson, configPath, configContent, summary) : [];
765
1282
  if (fixes.length > 0) {
766
- configPath = getConfigPathIfPresent(cwd);
767
- packageJson = readPackageJson(cwd);
1283
+ configPath = getConfigPathIfPresent(projectRoot);
1284
+ packageJson = readPackageJson(projectRoot);
768
1285
  declaredDependencies = getDeclaredDependencyNames(packageJson);
769
- summary = getInstallProjectSummary(cwd);
770
- activeConfig = await analyzeEslintConfig(cwd);
1286
+ summary = getInstallProjectSummary(projectRoot);
1287
+ activeConfig = await analyzeEslintConfig(projectRoot);
771
1288
  configContent = configPath ? readFileSync(configPath, "utf8") : null;
772
- liteInstallPackages = getLiteInstallPackages(summary, declaredDependencies);
773
- liteInstallCommand = createInstallCommand(packageManager, liteInstallPackages, workspaceRoot);
774
1289
  hasV1FrameworkImports = hasV1FrameworkPackageImports(configContent);
775
1290
  }
1291
+ const explicitFeaturePackages = configContent ? getExplicitConfigFeaturePackages(configContent) : [];
1292
+ const requiredPackages = getInstallPackages(summary, declaredDependencies, explicitFeaturePackages);
1293
+ const installCommand = requiredPackages.length > 0 ? createInstallCommand(
1294
+ packageManager,
1295
+ addCompatibleConfigVersions(
1296
+ requiredPackages,
1297
+ getCompatibleConfigVersion(packageJson, getCliVersion(), pnpmWorkspaceRoot)
1298
+ ),
1299
+ workspaceRoot,
1300
+ catalog
1301
+ ) : void 0;
776
1302
  const { issues, warnings } = buildDoctorDiagnosis(
777
- cwd,
1303
+ projectRoot,
1304
+ packageJson,
778
1305
  configPath,
779
1306
  configContent,
780
1307
  hasV1FrameworkImports,
@@ -782,41 +1309,77 @@ const handleDoctor = async (cwd = process.cwd(), json = false, liteInstall = fal
782
1309
  declaredDependencies,
783
1310
  summary
784
1311
  );
785
- outputDoctorResult(json, configPath, packageManager, summary, liteInstallCommand, issues, warnings, fixes);
1312
+ const projects = await getDoctorProjectActivations(projectRoot);
1313
+ outputDoctorResult(
1314
+ json,
1315
+ verbose,
1316
+ configPath,
1317
+ packageManager,
1318
+ summary,
1319
+ installCommand,
1320
+ requiredPackages,
1321
+ issues,
1322
+ warnings,
1323
+ projects,
1324
+ fixes
1325
+ );
786
1326
  };
787
- const handleInstall = (cwd = process.cwd(), dryRun = false) => {
788
- const packageJson = readPackageJson(cwd);
1327
+ 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);
1328
+ const handleInstall = (cwd = process.cwd(), dryRun = false, runner = spawnSync) => {
1329
+ const pnpmWorkspaceRoot = findPnpmWorkspaceRoot(cwd);
1330
+ const projectRoot = pnpmWorkspaceRoot ?? cwd;
1331
+ const packageJson = readPackageJson(projectRoot);
789
1332
  if (!packageJson) {
790
1333
  console.error("\u274C package.json is missing or invalid.");
791
1334
  process.exitCode = 1;
792
1335
  return;
793
1336
  }
794
- const packageManager = detectPackageManager(cwd);
795
- const configPath = getConfigPathIfPresent(cwd);
1337
+ const packageManager = detectPackageManager(projectRoot);
1338
+ const configPath = getConfigPathIfPresent(projectRoot);
796
1339
  const explicitFeaturePackages = configPath ? getExplicitConfigFeaturePackages(readFileSync(configPath, "utf8")) : [];
797
1340
  const packages = getInstallPackages(
798
- getInstallProjectSummary(cwd),
1341
+ getInstallProjectSummary(projectRoot),
799
1342
  getDeclaredDependencyNames(packageJson),
800
1343
  explicitFeaturePackages
801
1344
  );
802
- const workspaceRoot = packageManager === "pnpm" && existsSync(join(cwd, "pnpm-workspace.yaml"));
1345
+ const workspaceRoot = packageManager === "pnpm" && pnpmWorkspaceRoot !== void 0;
1346
+ const catalog = workspaceRoot ? getCatalogPreference(packageJson) : false;
1347
+ const installPackages = addCompatibleConfigVersions(
1348
+ packages,
1349
+ getCompatibleConfigVersion(packageJson, getCliVersion(), pnpmWorkspaceRoot)
1350
+ );
803
1351
  if (packages.length === 0) {
804
1352
  console.log("\u2705 All packages required by the detected ESLint configuration are already declared.");
805
1353
  return;
806
1354
  }
807
- const installCommand = createInstallCommand(packageManager, packages, workspaceRoot);
1355
+ const installCommand = createInstallCommand(packageManager, installPackages, workspaceRoot, catalog);
808
1356
  if (dryRun) {
809
1357
  console.log(installCommand);
810
1358
  return;
811
1359
  }
812
1360
  console.log(`Installing detected ESLint dependencies:
813
1361
  ${installCommand}`);
814
- const [command, args] = createInstallInvocation(packageManager, packages, workspaceRoot);
815
- const result = spawnSync(command, args, { cwd, stdio: "inherit" });
1362
+ const [command, args] = createInstallInvocation(packageManager, installPackages, workspaceRoot, catalog);
1363
+ const result = runner(command, args, {
1364
+ cwd: projectRoot,
1365
+ encoding: "utf8",
1366
+ maxBuffer: 16 * 1024 * 1024,
1367
+ stdio: "pipe"
1368
+ });
1369
+ const stdout = String(result.stdout ?? "");
1370
+ const stderr = String(result.stderr ?? "");
1371
+ if (stdout) process.stdout.write(stdout);
1372
+ if (stderr) process.stderr.write(stderr);
816
1373
  if (result.error) {
817
1374
  console.error(`\u274C Failed to run ${packageManager}: ${result.error.message}`);
818
1375
  process.exitCode = 1;
819
1376
  } else if (result.status !== 0) {
1377
+ if (packageManager === "pnpm" && isMinimumReleaseAgeFailure(`${stdout}
1378
+ ${stderr}`)) {
1379
+ console.error(
1380
+ `\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.`
1381
+ );
1382
+ }
820
1383
  process.exitCode = result.status ?? 1;
821
1384
  }
822
1385
  };
@@ -943,7 +1506,7 @@ const dispatchCommand = (command, cwd, flags) => {
943
1506
  break;
944
1507
  }
945
1508
  case "doctor": {
946
- handleDoctor(cwd, flags.hasJson, flags.hasLiteInstall, flags.hasFix).catch((error) => {
1509
+ handleDoctor(cwd, flags.hasJson, flags.hasLiteInstall, flags.hasFix, flags.hasVerbose).catch((error) => {
947
1510
  console.error(`\u274C Failed to run doctor: ${String(error)}`);
948
1511
  process.exitCode = 1;
949
1512
  });
@@ -964,6 +1527,25 @@ const dispatchCommand = (command, cwd, flags) => {
964
1527
  }
965
1528
  break;
966
1529
  }
1530
+ case "explain-preset": {
1531
+ if (!flags.positional) {
1532
+ console.error("Missing preset. Usage: basic-eslint explain-preset <preset> [options]");
1533
+ process.exitCode = 1;
1534
+ break;
1535
+ }
1536
+ handleExplainPreset(cwd, flags.positional, {
1537
+ analyzeSource: flags.hasAnalyzeSource,
1538
+ compatibility: flags.hasCompatibility,
1539
+ file: flags.files[0],
1540
+ files: flags.files,
1541
+ json: flags.hasJson,
1542
+ output: flags.output
1543
+ }).catch((error) => {
1544
+ console.error(`\u274C Failed to explain preset: ${String(error)}`);
1545
+ process.exitCode = 1;
1546
+ });
1547
+ break;
1548
+ }
967
1549
  case "generate-skill": {
968
1550
  handleGenerateSkill(cwd, flags.hasForce, {
969
1551
  check: flags.hasCheck,
@@ -1065,9 +1647,9 @@ const getNumericFlagValue = (argv, flag) => {
1065
1647
  };
1066
1648
  const runCli = (argv = process.argv, cwd = process.cwd()) => {
1067
1649
  const command = argv[2];
1068
- const isHelp = command === "--help" || command === "-h";
1650
+ const isHelp = argv.slice(3).some((argument) => argument === "--help" || argument === "-h");
1069
1651
  const isVersion = command === "--version" || command === "-v";
1070
- if (!command || isHelp) {
1652
+ if (!command || command === "--help" || command === "-h") {
1071
1653
  printUsage();
1072
1654
  return;
1073
1655
  }
@@ -1075,10 +1657,19 @@ const runCli = (argv = process.argv, cwd = process.cwd()) => {
1075
1657
  console.log(getCliVersion());
1076
1658
  return;
1077
1659
  }
1660
+ if (isHelp) {
1661
+ if (COMMAND_OPTIONS[command]) printCommandUsage(command);
1662
+ else printUsage();
1663
+ return;
1664
+ }
1665
+ const validation = validateCommandArguments(command, argv.slice(3));
1666
+ if (!validation.ok) return;
1078
1667
  dispatchCommand(command, cwd, {
1079
1668
  concurrency: getFlagValue(argv, "--concurrency"),
1080
1669
  files: getFlagValues(argv, "--file"),
1670
+ hasAnalyzeSource: argv.includes("--analyze-source"),
1081
1671
  hasCheck: argv.includes("--check"),
1672
+ hasCompatibility: argv.includes("--compatibility"),
1082
1673
  hasCreate: argv.includes("--create"),
1083
1674
  hasDryRun: argv.includes("--dry-run"),
1084
1675
  hasExplicit: argv.includes("--explicit"),
@@ -1088,13 +1679,16 @@ const runCli = (argv = process.argv, cwd = process.cwd()) => {
1088
1679
  hasJson: argv.includes("--json"),
1089
1680
  hasLiteInstall: argv.includes("--lite-install"),
1090
1681
  hasPrune: argv.includes("--prune"),
1682
+ hasVerbose: argv.includes("--verbose"),
1091
1683
  hasWithEslintMcp: argv.includes("--with-eslint-mcp"),
1092
1684
  hasWrite: argv.includes("--write"),
1093
1685
  maxDurationMs: getNumericFlagValue(argv, "--max-duration"),
1094
1686
  maxRuleTimeMs: getNumericFlagValue(argv, "--max-rule-time"),
1095
1687
  maxWarnings: getNumericFlagValue(argv, "--max-warnings"),
1688
+ output: getFlagValue(argv, "--output"),
1689
+ positional: validation.positional,
1096
1690
  preset: getFlagValue(argv, "--preset"),
1097
- rule: argv[3] && !argv[3].startsWith("-") ? argv[3] : void 0,
1691
+ rule: command === "explain" ? validation.positional : void 0,
1098
1692
  snapshotPath: getFlagValue(argv, "--snapshot-path"),
1099
1693
  target: getFlagValue(argv, "--to")
1100
1694
  });
@@ -1114,3 +1708,4 @@ export {
1114
1708
  handleUpdate,
1115
1709
  runCli
1116
1710
  };
1711
+ //# sourceMappingURL=cli.js.map