@santi020k/eslint-config-basic 2.1.1 → 3.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/dist/cli.js CHANGED
@@ -1,9 +1,22 @@
1
1
  #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
2
3
  import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
3
4
  import { createRequire } from "node:module";
4
5
  import { basename, dirname, join } from "node:path";
5
6
  import { fileURLToPath } from "node:url";
6
- import { analyzeEslintConfig, handleGenerateSkill } from "./agent-skill-generator.js";
7
+ import {
8
+ analyzeEslintConfig,
9
+ ESLINT_CONFIG_FILENAMES,
10
+ handleGenerateSkill
11
+ } from "./agent-skill-generator.js";
12
+ import { handleCompatibility, handleExplainRule } from "./cli-advanced.js";
13
+ import { getExplicitConfigFeaturePackages, handleMigrateV3 } from "./cli-migration.js";
14
+ import {
15
+ handleBaseline,
16
+ handleProfile,
17
+ handleSnapshot,
18
+ handleSnapshotDiff
19
+ } from "./cli-workflows.js";
7
20
  import { detectProjectOptions } from "./index.js";
8
21
  const getDefaultConfigFilename = (cwd) => {
9
22
  const packageJsonPath = join(cwd, "package.json");
@@ -16,15 +29,12 @@ const getDefaultConfigFilename = (cwd) => {
16
29
  }
17
30
  };
18
31
  const resolveConfigPath = (cwd) => {
19
- const existingConfigPath = [
20
- "eslint.config.js",
21
- "eslint.config.mjs"
22
- ].map((filename) => join(cwd, filename)).find((p) => existsSync(p));
32
+ const existingConfigPath = ESLINT_CONFIG_FILENAMES.map((filename) => join(cwd, filename)).find((p) => existsSync(p));
23
33
  return existingConfigPath ?? join(cwd, getDefaultConfigFilename(cwd));
24
34
  };
25
35
  const getFrameworkKeys = (detectedFrameworks) => {
26
36
  const frameworkKeys = new Set(detectedFrameworks ?? []);
27
- if (frameworkKeys.has("next") || frameworkKeys.has("expo") || frameworkKeys.has("remix") || frameworkKeys.has("react-router") || frameworkKeys.has("tanstack-start") && !frameworkKeys.has("solid")) {
37
+ if (frameworkKeys.has("next") || frameworkKeys.has("expo") || frameworkKeys.has("react-router") || frameworkKeys.has("tanstack-start") && !frameworkKeys.has("solid")) {
28
38
  frameworkKeys.add("react");
29
39
  }
30
40
  return [...frameworkKeys].sort();
@@ -43,7 +53,7 @@ const FRAMEWORK_PACKAGE_TO_KEY = {
43
53
  "@santi020k/eslint-config-qwik": "qwik",
44
54
  "@santi020k/eslint-config-react": "react",
45
55
  "@santi020k/eslint-config-react-router": "react-router",
46
- "@santi020k/eslint-config-remix": "remix",
56
+ "@santi020k/eslint-config-remix": "react-router",
47
57
  "@santi020k/eslint-config-slidev": "slidev",
48
58
  "@santi020k/eslint-config-solid": "solid",
49
59
  "@santi020k/eslint-config-svelte": "svelte",
@@ -51,18 +61,27 @@ const FRAMEWORK_PACKAGE_TO_KEY = {
51
61
  "@santi020k/eslint-config-vite": "vite",
52
62
  "@santi020k/eslint-config-vue": "vue"
53
63
  };
54
- const FRAMEWORK_KEY_TO_PACKAGE = Object.fromEntries(
55
- Object.entries(FRAMEWORK_PACKAGE_TO_KEY).map(([packageName, framework]) => [framework, packageName])
56
- );
64
+ const FRAMEWORK_KEY_TO_PACKAGE = {
65
+ ...Object.fromEntries(
66
+ Object.entries(FRAMEWORK_PACKAGE_TO_KEY).filter(([packageName]) => packageName !== "@santi020k/eslint-config-remix").map(([packageName, framework]) => [framework, packageName])
67
+ ),
68
+ "react-router": "@santi020k/eslint-config-react-router"
69
+ };
57
70
  const LITE_PACKAGE_NAME = "@santi020k/eslint-config-lite";
58
71
  const INTEGRATIONS_PACKAGE_NAME = "@santi020k/eslint-config-integrations";
72
+ const ASTRO_DOCTOR_PACKAGE_NAME = "@santi020k/eslint-plugin-astro-doctor";
73
+ const BASIC_PACKAGE_NAME = "@santi020k/eslint-config-basic";
74
+ const FULL_PACKAGE_NAME = "@santi020k/eslint-config-full";
59
75
  const TYPESCRIPT_PACKAGE_NAME = "typescript";
76
+ const CATEGORY_PACKAGES = {
77
+ extensions: "@santi020k/eslint-config-extensions",
78
+ formats: "@santi020k/eslint-config-formats",
79
+ libraries: "@santi020k/eslint-config-libraries",
80
+ testing: "@santi020k/eslint-config-testing",
81
+ tools: "@santi020k/eslint-config-tools"
82
+ };
60
83
  const getConfigPathIfPresent = (cwd) => {
61
- const configPath = [
62
- "eslint.config.js",
63
- "eslint.config.mjs",
64
- "eslint.config.cjs"
65
- ].map((filename) => join(cwd, filename)).find((p) => existsSync(p));
84
+ const configPath = ESLINT_CONFIG_FILENAMES.map((filename) => join(cwd, filename)).find((p) => existsSync(p));
66
85
  return configPath ?? null;
67
86
  };
68
87
  const readPackageJson = (cwd) => {
@@ -112,7 +131,33 @@ const getLiteInstallPackages = (summary, declaredDependencies) => {
112
131
  }
113
132
  return [...packages];
114
133
  };
115
- const createInstallCommand = (packageManager, packages) => {
134
+ const getInstallPackages = (summary, declaredDependencies, explicitFeaturePackages = []) => {
135
+ const usesFullPackage = declaredDependencies.has(FULL_PACKAGE_NAME);
136
+ const packages = /* @__PURE__ */ new Set(["eslint"]);
137
+ if (!usesFullPackage) packages.add(BASIC_PACKAGE_NAME);
138
+ if (!usesFullPackage) {
139
+ for (const framework of summary.frameworks) {
140
+ const packageName = FRAMEWORK_KEY_TO_PACKAGE[framework];
141
+ if (packageName) packages.add(packageName);
142
+ }
143
+ const categorySelections = [
144
+ [summary.extensions, CATEGORY_PACKAGES.extensions],
145
+ [summary.formats, CATEGORY_PACKAGES.formats],
146
+ [summary.libraries, CATEGORY_PACKAGES.libraries],
147
+ [summary.testing, CATEGORY_PACKAGES.testing],
148
+ [summary.tools, CATEGORY_PACKAGES.tools]
149
+ ];
150
+ for (const [features, packageName] of categorySelections) {
151
+ if (features.length > 0) packages.add(packageName);
152
+ }
153
+ for (const packageName of explicitFeaturePackages) {
154
+ packages.add(packageName);
155
+ }
156
+ }
157
+ if (summary.typescript) packages.add(TYPESCRIPT_PACKAGE_NAME);
158
+ return [...packages].filter((packageName) => !declaredDependencies.has(packageName));
159
+ };
160
+ const createInstallCommand = (packageManager, packages, workspaceRoot = false) => {
116
161
  const packageList = packages.join(" ");
117
162
  switch (packageManager) {
118
163
  case "bun":
@@ -122,7 +167,19 @@ const createInstallCommand = (packageManager, packages) => {
122
167
  case "yarn":
123
168
  return `yarn add -D ${packageList}`;
124
169
  default:
125
- return `pnpm add -D ${packageList}`;
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]];
126
183
  }
127
184
  };
128
185
  const detectPackageManager = (cwd) => {
@@ -177,39 +234,40 @@ const detectWorkspaceProjects = (cwd) => {
177
234
  ];
178
235
  return [...new Set(patterns.flatMap((pattern) => expandWorkspacePattern(cwd, pattern)))].sort();
179
236
  };
180
- const getDetectionValues = (options) => ({
181
- extensions: options.extensions ?? [],
182
- formats: options.formats ?? [],
183
- libraries: options.libraries ?? [],
184
- runtime: options.runtime ?? "universal",
185
- settings: options.settings ?? [],
186
- testing: options.testing ?? [],
187
- tools: options.tools ?? [],
188
- typescript: options.typescript ?? false
189
- });
190
- const createConfigContent = (cwd) => {
191
- const options = detectProjectOptions(cwd);
192
- const values = getDetectionValues(options);
193
- const frameworkKeys = getFrameworkKeys(options.detectedFrameworks);
194
- const imports = ["import { eslintConfig } from '@santi020k/eslint-config-basic'"];
195
- const presetLine = options.preset && options.preset !== "basic" ? ` preset: ${JSON.stringify(options.preset)},
196
- ` : "";
197
- const configContent = `${imports.join("\n")}
198
-
199
- export default await eslintConfig({
200
- ${presetLine} typescript: ${JSON.stringify(values.typescript)},
201
- frameworks: {
202
- ${frameworkKeys.map((key) => `${toPropertyKey(key)}: true`).join(",\n ")}
203
- },
204
- libraries: ${JSON.stringify(values.libraries, null, 2)},
205
- testing: ${JSON.stringify(values.testing, null, 2)},
206
- formats: ${JSON.stringify(values.formats, null, 2)},
207
- tools: ${JSON.stringify(values.tools, null, 2)},
208
- extensions: ${JSON.stringify(values.extensions, null, 2)},
209
- runtime: ${JSON.stringify(values.runtime)},
210
- settings: ${JSON.stringify(values.settings, null, 2)}
211
- })
212
- `;
237
+ const createExplicitOptions = (cwd) => {
238
+ const summary = getProjectSummary(cwd);
239
+ 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
+ ];
247
+ if (summary.typescript) options.push(" typescript: true,");
248
+ if (summary.frameworks.length > 0) {
249
+ options.push(
250
+ " frameworks: {",
251
+ ...summary.frameworks.map((framework) => ` ${toPropertyKey(framework)}: true,`),
252
+ " },"
253
+ );
254
+ }
255
+ for (const [category, values] of optionalCategories) {
256
+ if (values.length > 0) {
257
+ options.push(` ${category}: ${JSON.stringify(values)},`);
258
+ }
259
+ }
260
+ return options;
261
+ };
262
+ const createConfigContent = (cwd, explicit = false) => {
263
+ const explicitOptions = explicit ? createExplicitOptions(cwd) : [];
264
+ const invocation = explicitOptions.length > 0 ? ["export default defineConfig({", ...explicitOptions, "})"] : ["export default defineConfig()"];
265
+ const configContent = [
266
+ "import { defineConfig } from '@santi020k/eslint-config-basic'",
267
+ "",
268
+ ...invocation,
269
+ ""
270
+ ].join("\n");
213
271
  return {
214
272
  configContent,
215
273
  configPath: resolveConfigPath(cwd)
@@ -223,6 +281,7 @@ const getProjectSummary = (cwd) => {
223
281
  const options = detectProjectOptions(cwd);
224
282
  const workspaceProjects = detectWorkspaceProjects(cwd);
225
283
  return {
284
+ detectedProjects: Object.keys(options.projects ?? {}).sort(),
226
285
  extensions: options.extensions ?? [],
227
286
  formats: options.formats ?? [],
228
287
  frameworks: getFrameworkKeys(options.detectedFrameworks),
@@ -236,6 +295,30 @@ const getProjectSummary = (cwd) => {
236
295
  workspaceProjects
237
296
  };
238
297
  };
298
+ const getInstallProjectSummary = (cwd) => {
299
+ const rootSummary = getProjectSummary(cwd);
300
+ const projectSummaries = rootSummary.detectedProjects.map((projectPath) => getProjectSummary(join(cwd, projectPath)));
301
+ const summaries = [rootSummary, ...projectSummaries];
302
+ return {
303
+ ...rootSummary,
304
+ extensions: [...new Set(summaries.flatMap((summary) => summary.extensions))],
305
+ formats: [...new Set(summaries.flatMap((summary) => summary.formats))],
306
+ frameworks: [...new Set(summaries.flatMap((summary) => summary.frameworks))].sort(),
307
+ libraries: [...new Set(summaries.flatMap((summary) => summary.libraries))],
308
+ testing: [...new Set(summaries.flatMap((summary) => summary.testing))],
309
+ tools: [...new Set(summaries.flatMap((summary) => summary.tools))],
310
+ typescript: summaries.some((summary) => summary.typescript)
311
+ };
312
+ };
313
+ const getWorkspaceDeclaredConfigPackages = (cwd, summary) => {
314
+ const packages = /* @__PURE__ */ new Set();
315
+ for (const projectPath of summary.detectedProjects) {
316
+ for (const packageName of getDeclaredDependencyNames(readPackageJson(join(cwd, projectPath)))) {
317
+ if (packageName.startsWith("@santi020k/eslint-config-")) packages.add(packageName);
318
+ }
319
+ }
320
+ return [...packages];
321
+ };
239
322
  const createStandardsContent = (cwd) => {
240
323
  const summary = getProjectSummary(cwd);
241
324
  return [
@@ -259,9 +342,9 @@ const createStandardsContent = (cwd) => {
259
342
  "## Recommended Config",
260
343
  "",
261
344
  "```js",
262
- "import { eslintConfig } from '@santi020k/eslint-config-basic'",
345
+ "import { defineConfig } from '@santi020k/eslint-config-basic'",
263
346
  "",
264
- "export default await eslintConfig()",
347
+ "export default await defineConfig()",
265
348
  "```",
266
349
  "",
267
350
  "Use `basic-eslint explain` when you want to inspect what the zero-config setup detects."
@@ -284,25 +367,45 @@ const printUsage = () => {
284
367
  "Commands:",
285
368
  " init Create eslint.config.js/mjs if missing",
286
369
  " update Regenerate eslint.config.js/mjs from detection",
287
- " explain Print detected v2 config inputs",
370
+ " explain Print detected v3 config inputs",
288
371
  " inspect Print detected inputs and active config features",
289
- " doctor Check project setup for common v2 adoption issues",
372
+ " install Install missing packages for detected v3 features",
373
+ " doctor Check project setup for common v3 adoption issues",
374
+ " compatibility Check runtime and config package compatibility",
290
375
  " docs Generate ESLINT_STANDARDS.md from detection",
291
- " migrate Report v1-to-v2 migration suggestions",
376
+ " migrate Plan or apply v1-to-v2 and v2-to-v3 migrations",
377
+ " baseline Suppress existing violations for incremental adoption",
378
+ " profile Compare ESLint performance and report slow rules",
379
+ " snapshot Save the effective rules for representative files",
380
+ " diff Compare effective rules with the saved snapshot",
292
381
  " generate-skill Generate AI agent standards files",
293
382
  "",
294
383
  "Options:",
295
384
  " --force Overwrite existing generated skill sections/files",
296
- " --check init: verify config exists; generate-skill: verify files are up to date",
385
+ " --check Verify generated files, migrations, or snapshots without writing",
386
+ " --concurrency profile: off, auto, or a worker count",
297
387
  " --create generate-skill: scaffold a root AGENTS.md when missing",
388
+ " --dry-run install: print the detected install command without running it",
389
+ " --explicit init: write detected settings explicitly",
390
+ " --file profile/snapshot/diff: representative file or lint target (repeatable)",
391
+ " --fix doctor: safely repair generated config and package metadata",
392
+ " --full migrate --to v3: choose the batteries-included package",
298
393
  " --json Print JSON for commands that support it",
299
- " --lite-install doctor: print the detected lite install command",
394
+ " --lite-install doctor: print the deprecated v2 Lite install command (removed in v4)",
395
+ " --max-duration profile: maximum duration in milliseconds",
396
+ " --max-rule-time profile: maximum time for the slowest rule in milliseconds",
397
+ " --max-warnings profile: maximum warning count",
398
+ " --preset baseline: enable ci or pedantic strict mode before suppressing",
399
+ " --prune baseline: remove suppressions for resolved violations",
400
+ " --snapshot-path snapshot/diff: override .eslint-config-snapshot.json",
401
+ " --to migrate: target version (v2 or v3)",
402
+ " --with-eslint-mcp generate-skill: scaffold the official ESLint MCP server",
300
403
  " --write Apply safe migrations for commands that support it",
301
404
  " --help, -h Show this help message",
302
405
  " --version, -v Show CLI version"
303
406
  ].join("\n"));
304
407
  };
305
- const handleInit = (cwd = process.cwd(), check = false) => {
408
+ const handleInit = (cwd = process.cwd(), check = false, explicit = false) => {
306
409
  const configPath = resolveConfigPath(cwd);
307
410
  if (check) {
308
411
  const exists = existsSync(configPath);
@@ -319,7 +422,7 @@ const handleInit = (cwd = process.cwd(), check = false) => {
319
422
  return;
320
423
  }
321
424
  console.log("\u{1F50D} Detecting project settings...");
322
- const { configContent } = createConfigContent(cwd);
425
+ const { configContent } = createConfigContent(cwd, explicit);
323
426
  writeFileSync(configPath, configContent);
324
427
  console.log(`\u2705 Created ${basename(configPath)} with auto-detected settings!`);
325
428
  console.log("\u{1F680} Ready to lint!");
@@ -386,6 +489,58 @@ const handleInspect = async (cwd = process.cwd(), json = false) => {
386
489
  `- Workspace projects: ${formatList(summary.workspaceProjects)}`
387
490
  ].join("\n"));
388
491
  };
492
+ const semver = createRequire(import.meta.url)("semver");
493
+ const readPackageMetadataFromMain = (mainPath, packageName) => {
494
+ let directory = dirname(mainPath);
495
+ for (let depth = 0; depth < 4; depth++) {
496
+ const packagePath = join(directory, "package.json");
497
+ if (existsSync(packagePath)) {
498
+ const metadata = JSON.parse(readFileSync(packagePath, "utf8"));
499
+ if (metadata.name === packageName) return metadata;
500
+ }
501
+ directory = dirname(directory);
502
+ }
503
+ return null;
504
+ };
505
+ const resolvePackageMetadata = (cwd, packageName) => {
506
+ try {
507
+ const projectRequire = createRequire(join(cwd, "package.json"));
508
+ try {
509
+ return readPackageMetadataFromMain(projectRequire.resolve(packageName), packageName);
510
+ } catch {
511
+ const integrationsMain = projectRequire.resolve(INTEGRATIONS_PACKAGE_NAME);
512
+ const integrationsRequire = createRequire(integrationsMain);
513
+ return readPackageMetadataFromMain(integrationsRequire.resolve(packageName), packageName);
514
+ }
515
+ } catch {
516
+ return null;
517
+ }
518
+ };
519
+ const getAstroDoctorNodeWarning = (metadata) => {
520
+ const nodeRange = metadata.engines?.node;
521
+ if (!nodeRange || semver.satisfies(process.versions.node, nodeRange)) return null;
522
+ return `Astro Doctor ${metadata.version ?? ""} requires Node ${nodeRange}, but the current runtime is ${process.versions.node}.`;
523
+ };
524
+ const getAstroDoctorEslintWarning = (metadata, eslintMetadata) => {
525
+ const eslintRange = metadata.peerDependencies?.eslint;
526
+ const eslintVersion = eslintMetadata?.version;
527
+ if (!eslintRange || !eslintVersion || semver.satisfies(eslintVersion, eslintRange)) return null;
528
+ return `Astro Doctor ${metadata.version ?? ""} requires ESLint ${eslintRange}, but this project resolves ${eslintVersion}.`;
529
+ };
530
+ const getAstroDoctorCompatibilityWarnings = (cwd, enabled) => {
531
+ if (!enabled) return [];
532
+ const pluginMetadata = resolvePackageMetadata(cwd, ASTRO_DOCTOR_PACKAGE_NAME);
533
+ if (!pluginMetadata) {
534
+ return [
535
+ `Astro Doctor is enabled, but ${ASTRO_DOCTOR_PACKAGE_NAME} could not be resolved. Reinstall dependencies or disable the \`astro-doctor\` feature.`
536
+ ];
537
+ }
538
+ const eslintMetadata = resolvePackageMetadata(cwd, "eslint");
539
+ return [
540
+ getAstroDoctorNodeWarning(pluginMetadata),
541
+ getAstroDoctorEslintWarning(pluginMetadata, eslintMetadata)
542
+ ].filter((warning) => warning !== null);
543
+ };
389
544
  const findDuplicateEslint = (cwd = process.cwd()) => {
390
545
  try {
391
546
  const projectRequire = createRequire(join(cwd, "package.json"));
@@ -407,7 +562,9 @@ const validateConfigContent = (configContent, hasV1FrameworkImports, declaredDep
407
562
  if (hasV1FrameworkImports) {
408
563
  warnings.push("Config still imports v1 framework packages. Run `basic-eslint migrate --write` or switch to framework booleans.");
409
564
  }
410
- if (summary.workspaceProjects.length > 0 && !configContent.includes("projects:")) {
565
+ const usesBasicComposer = /from\s*['"]@santi020k\/eslint-config-(?:basic|full|lite)['"]/.test(configContent) && /\b(?:defineConfig|eslintConfig)\s*\(/.test(configContent);
566
+ const hasDetectedProjectScopes = summary.workspaceProjects.every((project) => summary.detectedProjects.includes(project));
567
+ if (summary.workspaceProjects.length > 0 && !configContent.includes("projects:") && !(usesBasicComposer && hasDetectedProjectScopes)) {
411
568
  warnings.push("Workspace packages were detected, but the root config does not use `projects` scoping.");
412
569
  }
413
570
  if (configContent.includes(LITE_PACKAGE_NAME)) {
@@ -430,7 +587,7 @@ const validateConfigContent = (configContent, hasV1FrameworkImports, declaredDep
430
587
  }
431
588
  return warnings;
432
589
  };
433
- const buildDoctorDiagnosis = (cwd, configPath, configContent, hasV1FrameworkImports, activeConfig, declaredDependencies, summary) => {
590
+ const validateProjectSetup = (cwd, configPath, activeConfig, hasV1FrameworkImports) => {
434
591
  const issues = [];
435
592
  const warnings = [];
436
593
  if (!readPackageJson(cwd)) {
@@ -444,18 +601,50 @@ const buildDoctorDiagnosis = (cwd, configPath, configContent, hasV1FrameworkImpo
444
601
  if (!hasLintScript(cwd)) {
445
602
  warnings.push("No `lint` script found in package.json.");
446
603
  }
447
- if (configContent) {
448
- warnings.push(...validateConfigContent(configContent, hasV1FrameworkImports, declaredDependencies, summary));
604
+ return { issues, warnings };
605
+ };
606
+ const isAstroDoctorConfigured = (activeConfig, configContent) => [
607
+ activeConfig?.extensions.includes("Astro Doctor") ?? false,
608
+ configContent?.includes("astro-doctor") ?? false,
609
+ configContent?.includes("AstroDoctor") ?? false
610
+ ].includes(true);
611
+ const isAstroConfigured = (activeConfig, summary) => [
612
+ activeConfig?.frameworks.includes("Astro") ?? false,
613
+ summary.frameworks.includes("astro")
614
+ ].some(Boolean);
615
+ const checkAstroDoctorStatus = (activeConfig, configContent, summary, warnings) => {
616
+ const astroDoctorEnabled = isAstroDoctorConfigured(activeConfig, configContent);
617
+ const astroEnabled = isAstroConfigured(activeConfig, summary);
618
+ if (astroDoctorEnabled && !astroEnabled) {
619
+ warnings.push(
620
+ "Astro Doctor is enabled without Astro. Enable `frameworks: { astro: true }` or remove the `astro-doctor` feature."
621
+ );
622
+ } else if (astroEnabled && !astroDoctorEnabled) {
623
+ warnings.push(
624
+ 'Astro was detected without Astro Doctor. Enable `features: { "astro-doctor": true }` for additional Astro diagnostics.'
625
+ );
449
626
  }
627
+ return astroDoctorEnabled;
628
+ };
629
+ const checkDuplicateEslint = (cwd, warnings) => {
450
630
  const duplicateEslint = findDuplicateEslint(cwd);
451
631
  if (duplicateEslint) {
452
632
  warnings.push(
453
633
  `Two ESLint copies are installed: the project resolves ${duplicateEslint.projectVersion} while the config packages resolve ${duplicateEslint.configVersion}. Only ESLint 10 is supported, and parallel copies can apply different rule behavior between your editor and CLI. Align the project's eslint version with the config's (or dedupe via your package manager).`
454
634
  );
455
635
  }
636
+ };
637
+ const buildDoctorDiagnosis = (cwd, configPath, configContent, hasV1FrameworkImports, activeConfig, declaredDependencies, summary) => {
638
+ const { issues, warnings } = validateProjectSetup(cwd, configPath, activeConfig, hasV1FrameworkImports);
639
+ if (configContent) {
640
+ warnings.push(...validateConfigContent(configContent, hasV1FrameworkImports, declaredDependencies, summary));
641
+ }
642
+ const astroDoctorEnabled = checkAstroDoctorStatus(activeConfig, configContent, summary, warnings);
643
+ warnings.push(...getAstroDoctorCompatibilityWarnings(cwd, astroDoctorEnabled));
644
+ checkDuplicateEslint(cwd, warnings);
456
645
  return { issues, warnings };
457
646
  };
458
- const outputDoctorResult = (json, configPath, packageManager, summary, liteInstallCommand, issues, warnings) => {
647
+ const outputDoctorResult = (json, configPath, packageManager, summary, liteInstallCommand, issues, warnings, fixes = []) => {
459
648
  let status = "passed";
460
649
  if (issues.length > 0) {
461
650
  status = "failed";
@@ -464,6 +653,7 @@ const outputDoctorResult = (json, configPath, packageManager, summary, liteInsta
464
653
  }
465
654
  const payload = {
466
655
  configFile: configPath ? basename(configPath) : null,
656
+ fixes,
467
657
  issues,
468
658
  liteInstallCommand,
469
659
  packageManager,
@@ -481,22 +671,88 @@ const outputDoctorResult = (json, configPath, packageManager, summary, liteInsta
481
671
  `- Package manager: ${payload.packageManager}`,
482
672
  `- Config file: ${payload.configFile ?? "none"}`,
483
673
  `- Workspace projects: ${formatList(summary.workspaceProjects)}`,
674
+ ...fixes.length > 0 ? ["", "Fixes applied:", ...fixes.map((fix) => `- ${fix}`)] : [],
484
675
  ...issues.length > 0 ? ["", "Issues:", ...issues.map((issue) => `- ${issue}`)] : [],
485
676
  ...warnings.length > 0 ? ["", "Warnings:", ...warnings.map((warning) => `- ${warning}`)] : []
486
677
  ].join("\n"));
487
678
  if (issues.length > 0) process.exitCode = 1;
488
679
  };
489
- const handleDoctor = async (cwd = process.cwd(), json = false, liteInstall = false) => {
490
- const configPath = getConfigPathIfPresent(cwd);
491
- const packageJson = readPackageJson(cwd);
492
- const declaredDependencies = getDeclaredDependencyNames(packageJson);
493
- const summary = getProjectSummary(cwd);
494
- const activeConfig = await analyzeEslintConfig(cwd);
495
- const configContent = configPath ? readFileSync(configPath, "utf8") : null;
680
+ const createDoctorBackupPath = (filePath) => {
681
+ const preferred = `${filePath}.doctor.bak`;
682
+ if (!existsSync(preferred)) return preferred;
683
+ let index = 2;
684
+ while (existsSync(`${preferred}.${index}`)) index++;
685
+ return `${preferred}.${index}`;
686
+ };
687
+ const getDoctorDependencyVersion = (packageName) => {
688
+ if (packageName === "eslint") return "^10.0.0";
689
+ if (packageName === TYPESCRIPT_PACKAGE_NAME) return "^5.0.0";
690
+ return "^3.0.0";
691
+ };
692
+ const applyDoctorFixes = (cwd, packageJson, configPath, configContent, summary) => {
693
+ const fixes = [];
694
+ if (packageJson) {
695
+ const updated = structuredClone(packageJson);
696
+ let packageChanged = false;
697
+ updated.scripts ??= {};
698
+ if (!updated.scripts.lint) {
699
+ updated.scripts.lint = "eslint .";
700
+ packageChanged = true;
701
+ fixes.push("Added the `lint` script.");
702
+ }
703
+ updated.devDependencies ??= {};
704
+ const declared = getDeclaredDependencyNames(packageJson);
705
+ const explicitFeaturePackages = configContent ? getExplicitConfigFeaturePackages(configContent) : [];
706
+ const missingPackages = getInstallPackages(summary, declared, explicitFeaturePackages);
707
+ const missingDependencies = Object.fromEntries(
708
+ missingPackages.map((packageName) => [packageName, getDoctorDependencyVersion(packageName)])
709
+ );
710
+ updated.devDependencies = {
711
+ ...updated.devDependencies,
712
+ ...missingDependencies
713
+ };
714
+ for (const packageName of missingPackages) {
715
+ fixes.push(`Declared ${packageName}.`);
716
+ packageChanged = true;
717
+ }
718
+ if (packageChanged) {
719
+ const packagePath = join(cwd, "package.json");
720
+ const backupPath = createDoctorBackupPath(packagePath);
721
+ writeFileSync(backupPath, `${JSON.stringify(packageJson, null, 2)}
722
+ `);
723
+ writeFileSync(packagePath, `${JSON.stringify(updated, null, 2)}
724
+ `);
725
+ fixes.push(`Backed up package.json to ${basename(backupPath)}.`);
726
+ }
727
+ }
728
+ if (!configPath) {
729
+ const generated = createConfigContent(cwd);
730
+ writeFileSync(generated.configPath, generated.configContent);
731
+ fixes.push(`Created ${basename(generated.configPath)}.`);
732
+ } else if (configContent && Object.keys(FRAMEWORK_PACKAGE_TO_KEY).some((packageName) => configContent.includes(packageName))) {
733
+ const result = migrateConfigContent(configContent);
734
+ if (result.changed) {
735
+ const backupPath = createDoctorBackupPath(configPath);
736
+ writeFileSync(backupPath, configContent);
737
+ writeFileSync(configPath, result.content);
738
+ fixes.push(`Migrated v1 framework imports and backed up the config to ${basename(backupPath)}.`);
739
+ }
740
+ }
741
+ return fixes;
742
+ };
743
+ 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) => {
496
745
  const packageManager = detectPackageManager(cwd);
497
- const liteInstallPackages = getLiteInstallPackages(summary, declaredDependencies);
498
- const liteInstallCommand = createInstallCommand(packageManager, liteInstallPackages);
499
- const hasV1FrameworkImports = configContent ? Object.keys(FRAMEWORK_PACKAGE_TO_KEY).some((packageName) => configContent.includes(packageName)) : false;
746
+ const workspaceRoot = packageManager === "pnpm" && existsSync(join(cwd, "pnpm-workspace.yaml"));
747
+ let configPath = getConfigPathIfPresent(cwd);
748
+ let packageJson = readPackageJson(cwd);
749
+ let declaredDependencies = getDeclaredDependencyNames(packageJson);
750
+ let summary = getInstallProjectSummary(cwd);
751
+ let activeConfig = await analyzeEslintConfig(cwd);
752
+ let configContent = configPath ? readFileSync(configPath, "utf8") : null;
753
+ let liteInstallPackages = getLiteInstallPackages(summary, declaredDependencies);
754
+ let liteInstallCommand = createInstallCommand(packageManager, liteInstallPackages, workspaceRoot);
755
+ let hasV1FrameworkImports = hasV1FrameworkPackageImports(configContent);
500
756
  if (liteInstall) {
501
757
  if (json) {
502
758
  console.log(JSON.stringify({ command: liteInstallCommand, packageManager, packages: liteInstallPackages }, null, 2));
@@ -505,8 +761,64 @@ const handleDoctor = async (cwd = process.cwd(), json = false, liteInstall = fal
505
761
  }
506
762
  return;
507
763
  }
508
- const { issues, warnings } = buildDoctorDiagnosis(cwd, configPath, configContent, hasV1FrameworkImports, activeConfig, declaredDependencies, summary);
509
- outputDoctorResult(json, configPath, packageManager, summary, liteInstallCommand, issues, warnings);
764
+ const fixes = fix ? applyDoctorFixes(cwd, packageJson, configPath, configContent, summary) : [];
765
+ if (fixes.length > 0) {
766
+ configPath = getConfigPathIfPresent(cwd);
767
+ packageJson = readPackageJson(cwd);
768
+ declaredDependencies = getDeclaredDependencyNames(packageJson);
769
+ summary = getInstallProjectSummary(cwd);
770
+ activeConfig = await analyzeEslintConfig(cwd);
771
+ configContent = configPath ? readFileSync(configPath, "utf8") : null;
772
+ liteInstallPackages = getLiteInstallPackages(summary, declaredDependencies);
773
+ liteInstallCommand = createInstallCommand(packageManager, liteInstallPackages, workspaceRoot);
774
+ hasV1FrameworkImports = hasV1FrameworkPackageImports(configContent);
775
+ }
776
+ const { issues, warnings } = buildDoctorDiagnosis(
777
+ cwd,
778
+ configPath,
779
+ configContent,
780
+ hasV1FrameworkImports,
781
+ activeConfig,
782
+ declaredDependencies,
783
+ summary
784
+ );
785
+ outputDoctorResult(json, configPath, packageManager, summary, liteInstallCommand, issues, warnings, fixes);
786
+ };
787
+ const handleInstall = (cwd = process.cwd(), dryRun = false) => {
788
+ const packageJson = readPackageJson(cwd);
789
+ if (!packageJson) {
790
+ console.error("\u274C package.json is missing or invalid.");
791
+ process.exitCode = 1;
792
+ return;
793
+ }
794
+ const packageManager = detectPackageManager(cwd);
795
+ const configPath = getConfigPathIfPresent(cwd);
796
+ const explicitFeaturePackages = configPath ? getExplicitConfigFeaturePackages(readFileSync(configPath, "utf8")) : [];
797
+ const packages = getInstallPackages(
798
+ getInstallProjectSummary(cwd),
799
+ getDeclaredDependencyNames(packageJson),
800
+ explicitFeaturePackages
801
+ );
802
+ const workspaceRoot = packageManager === "pnpm" && existsSync(join(cwd, "pnpm-workspace.yaml"));
803
+ if (packages.length === 0) {
804
+ console.log("\u2705 All packages required by the detected ESLint configuration are already declared.");
805
+ return;
806
+ }
807
+ const installCommand = createInstallCommand(packageManager, packages, workspaceRoot);
808
+ if (dryRun) {
809
+ console.log(installCommand);
810
+ return;
811
+ }
812
+ console.log(`Installing detected ESLint dependencies:
813
+ ${installCommand}`);
814
+ const [command, args] = createInstallInvocation(packageManager, packages, workspaceRoot);
815
+ const result = spawnSync(command, args, { cwd, stdio: "inherit" });
816
+ if (result.error) {
817
+ console.error(`\u274C Failed to run ${packageManager}: ${result.error.message}`);
818
+ process.exitCode = 1;
819
+ } else if (result.status !== 0) {
820
+ process.exitCode = result.status ?? 1;
821
+ }
510
822
  };
511
823
  const handleDocs = (cwd = process.cwd()) => {
512
824
  const outputPath = join(cwd, "ESLINT_STANDARDS.md");
@@ -520,9 +832,9 @@ const migrateConfigContent = (content) => {
520
832
  }
521
833
  const frameworks = getFrameworkKeys(importedFrameworks);
522
834
  const migrated = [
523
- "import { eslintConfig } from '@santi020k/eslint-config-basic'",
835
+ "import { defineConfig } from '@santi020k/eslint-config-basic'",
524
836
  "",
525
- "export default await eslintConfig({",
837
+ "export default await defineConfig({",
526
838
  " frameworks: {",
527
839
  ` ${frameworks.map((key) => `${toPropertyKey(key)}: true`).join(",\n ")}`,
528
840
  " }",
@@ -547,7 +859,7 @@ const processConfigMigration = (configPath, write, suggestions) => {
547
859
  const backupPath = `${configPath}.bak`;
548
860
  writeFileSync(backupPath, configContent);
549
861
  writeFileSync(configPath, result.content);
550
- const message = `Rewrote ${basename(configPath)} with v2 framework booleans: ${result.frameworks.join(", ")}. Original backed up to ${basename(backupPath)}.`;
862
+ const message = `Rewrote ${basename(configPath)} with framework booleans: ${result.frameworks.join(", ")}. Original backed up to ${basename(backupPath)}.`;
551
863
  applied.push(message);
552
864
  suggestions.push(`- ${message}`);
553
865
  } else {
@@ -555,15 +867,33 @@ const processConfigMigration = (configPath, write, suggestions) => {
555
867
  }
556
868
  return applied;
557
869
  };
558
- const handleMigrate = (cwd = process.cwd(), write = false, json = false) => {
870
+ const handleMigrate = (cwd = process.cwd(), write = false, json = false, target = "v2", check = false, full = false) => {
871
+ if (target === "v3" || target === "3") {
872
+ const summary = getInstallProjectSummary(cwd);
873
+ handleMigrateV3(cwd, {
874
+ declaredConfigPackages: getWorkspaceDeclaredConfigPackages(cwd, summary),
875
+ extensions: summary.extensions,
876
+ formats: summary.formats,
877
+ frameworks: summary.frameworks,
878
+ libraries: summary.libraries,
879
+ packageManager: detectPackageManager(cwd),
880
+ testing: summary.testing,
881
+ tools: summary.tools,
882
+ typescript: summary.typescript
883
+ }, { check, full, json, write });
884
+ return;
885
+ }
886
+ if (target !== "v2" && target !== "2") {
887
+ throw new Error(`Unsupported migration target "${target}". Use --to v2 or --to v3.`);
888
+ }
559
889
  const configPath = getConfigPathIfPresent(cwd) ?? join(cwd, getDefaultConfigFilename(cwd));
560
890
  const suggestions = [
561
891
  "v1 to v2 migration suggestions:",
562
892
  "- Install only @santi020k/eslint-config-basic for the public config API.",
563
893
  "- Replace framework imports with bundled booleans such as frameworks: { react: true, next: true }.",
564
894
  "- Remove app-level @santi020k/eslint-config-react/next/vue/etc. config package imports.",
565
- "- Try eslintConfig() first; v2 auto-detects supported frameworks and integrations.",
566
- "- Use basic-eslint explain to review what v2 detects before committing the migration."
895
+ "- Try defineConfig() first; v3 auto-detects supported frameworks and integrations.",
896
+ "- Use basic-eslint explain to review what v3 detects before committing the migration."
567
897
  ];
568
898
  const applied = processConfigMigration(configPath, write, suggestions);
569
899
  if (json) {
@@ -580,30 +910,73 @@ const handleMigrate = (cwd = process.cwd(), write = false, json = false) => {
580
910
  };
581
911
  const dispatchCommand = (command, cwd, flags) => {
582
912
  switch (command) {
913
+ case "baseline": {
914
+ try {
915
+ handleBaseline(cwd, {
916
+ json: flags.hasJson,
917
+ preset: flags.preset,
918
+ prune: flags.hasPrune
919
+ });
920
+ } catch (error) {
921
+ console.error(`\u274C Failed to create ESLint baseline: ${String(error)}`);
922
+ process.exitCode = 1;
923
+ }
924
+ break;
925
+ }
926
+ case "compatibility": {
927
+ handleCompatibility(cwd, flags.hasJson);
928
+ break;
929
+ }
930
+ case "diff": {
931
+ handleSnapshotDiff(cwd, {
932
+ files: flags.files,
933
+ json: flags.hasJson,
934
+ snapshotPath: flags.snapshotPath
935
+ }).catch((error) => {
936
+ console.error(`\u274C Failed to diff ESLint configuration: ${String(error)}`);
937
+ process.exitCode = 1;
938
+ });
939
+ break;
940
+ }
583
941
  case "docs": {
584
942
  handleDocs(cwd);
585
943
  break;
586
944
  }
587
945
  case "doctor": {
588
- handleDoctor(cwd, flags.hasJson, flags.hasLiteInstall).catch((error) => {
946
+ handleDoctor(cwd, flags.hasJson, flags.hasLiteInstall, flags.hasFix).catch((error) => {
589
947
  console.error(`\u274C Failed to run doctor: ${String(error)}`);
590
948
  process.exitCode = 1;
591
949
  });
592
950
  break;
593
951
  }
594
952
  case "explain": {
595
- handleExplain(cwd, flags.hasJson);
953
+ if (flags.rule) {
954
+ handleExplainRule(cwd, {
955
+ file: flags.files[0],
956
+ json: flags.hasJson,
957
+ rule: flags.rule
958
+ }).catch((error) => {
959
+ console.error(`\u274C Failed to explain ESLint rule: ${String(error)}`);
960
+ process.exitCode = 1;
961
+ });
962
+ } else {
963
+ handleExplain(cwd, flags.hasJson);
964
+ }
596
965
  break;
597
966
  }
598
967
  case "generate-skill": {
599
- handleGenerateSkill(cwd, flags.hasForce, { check: flags.hasCheck, createAgentsMd: flags.hasCreate }).catch((error) => {
968
+ handleGenerateSkill(cwd, flags.hasForce, {
969
+ check: flags.hasCheck,
970
+ createAgentsMd: flags.hasCreate,
971
+ withEslintMcp: flags.hasWithEslintMcp
972
+ }).catch((error) => {
600
973
  console.error(`\u274C Failed to generate skill files: ${String(error)}`);
601
974
  process.exitCode = 1;
602
975
  });
603
976
  break;
604
977
  }
605
978
  case "init": {
606
- handleInit(cwd, flags.hasCheck);
979
+ handleInit(cwd, flags.hasCheck, flags.hasExplicit);
607
980
  break;
608
981
  }
609
982
  case "inspect": {
@@ -613,8 +986,52 @@ const dispatchCommand = (command, cwd, flags) => {
613
986
  });
614
987
  break;
615
988
  }
989
+ case "install": {
990
+ handleInstall(cwd, flags.hasDryRun);
991
+ break;
992
+ }
616
993
  case "migrate": {
617
- handleMigrate(cwd, flags.hasWrite, flags.hasJson);
994
+ try {
995
+ handleMigrate(
996
+ cwd,
997
+ flags.hasWrite,
998
+ flags.hasJson,
999
+ flags.target ?? "v2",
1000
+ flags.hasCheck,
1001
+ flags.hasFull
1002
+ );
1003
+ } catch (error) {
1004
+ console.error(`\u274C Failed to migrate ESLint configuration: ${String(error)}`);
1005
+ process.exitCode = 1;
1006
+ }
1007
+ break;
1008
+ }
1009
+ case "profile": {
1010
+ try {
1011
+ handleProfile(cwd, {
1012
+ concurrency: flags.concurrency,
1013
+ files: flags.files,
1014
+ json: flags.hasJson,
1015
+ maxDurationMs: flags.maxDurationMs,
1016
+ maxRuleTimeMs: flags.maxRuleTimeMs,
1017
+ maxWarnings: flags.maxWarnings
1018
+ });
1019
+ } catch (error) {
1020
+ console.error(`\u274C Failed to profile ESLint: ${String(error)}`);
1021
+ process.exitCode = 1;
1022
+ }
1023
+ break;
1024
+ }
1025
+ case "snapshot": {
1026
+ handleSnapshot(cwd, {
1027
+ check: flags.hasCheck,
1028
+ files: flags.files,
1029
+ json: flags.hasJson,
1030
+ snapshotPath: flags.snapshotPath
1031
+ }).catch((error) => {
1032
+ console.error(`\u274C Failed to snapshot ESLint configuration: ${String(error)}`);
1033
+ process.exitCode = 1;
1034
+ });
618
1035
  break;
619
1036
  }
620
1037
  case "update": {
@@ -628,6 +1045,24 @@ const dispatchCommand = (command, cwd, flags) => {
628
1045
  }
629
1046
  }
630
1047
  };
1048
+ const getFlagValue = (argv, flag) => {
1049
+ const inline = argv.find((argument) => argument.startsWith(`${flag}=`));
1050
+ if (inline) return inline.slice(flag.length + 1);
1051
+ const index = argv.indexOf(flag);
1052
+ return index >= 0 ? argv[index + 1] : void 0;
1053
+ };
1054
+ const getFlagValues = (argv, flag) => argv.flatMap((argument, index) => {
1055
+ if (argument.startsWith(`${flag}=`)) return [argument.slice(flag.length + 1)];
1056
+ if (argument === flag && argv[index + 1]) return [argv[index + 1]];
1057
+ return [];
1058
+ });
1059
+ const getNumericFlagValue = (argv, flag) => {
1060
+ const value = getFlagValue(argv, flag);
1061
+ const present = argv.includes(flag) || argv.some((argument) => argument.startsWith(`${flag}=`));
1062
+ if (!present) return void 0;
1063
+ if (value === void 0 || value.trim() === "" || value.startsWith("--")) return Number.NaN;
1064
+ return Number(value);
1065
+ };
631
1066
  const runCli = (argv = process.argv, cwd = process.cwd()) => {
632
1067
  const command = argv[2];
633
1068
  const isHelp = command === "--help" || command === "-h";
@@ -641,12 +1076,27 @@ const runCli = (argv = process.argv, cwd = process.cwd()) => {
641
1076
  return;
642
1077
  }
643
1078
  dispatchCommand(command, cwd, {
1079
+ concurrency: getFlagValue(argv, "--concurrency"),
1080
+ files: getFlagValues(argv, "--file"),
644
1081
  hasCheck: argv.includes("--check"),
645
1082
  hasCreate: argv.includes("--create"),
1083
+ hasDryRun: argv.includes("--dry-run"),
1084
+ hasExplicit: argv.includes("--explicit"),
1085
+ hasFix: argv.includes("--fix"),
646
1086
  hasForce: argv.includes("--force"),
1087
+ hasFull: argv.includes("--full"),
647
1088
  hasJson: argv.includes("--json"),
648
1089
  hasLiteInstall: argv.includes("--lite-install"),
649
- hasWrite: argv.includes("--write")
1090
+ hasPrune: argv.includes("--prune"),
1091
+ hasWithEslintMcp: argv.includes("--with-eslint-mcp"),
1092
+ hasWrite: argv.includes("--write"),
1093
+ maxDurationMs: getNumericFlagValue(argv, "--max-duration"),
1094
+ maxRuleTimeMs: getNumericFlagValue(argv, "--max-rule-time"),
1095
+ maxWarnings: getNumericFlagValue(argv, "--max-warnings"),
1096
+ preset: getFlagValue(argv, "--preset"),
1097
+ rule: argv[3] && !argv[3].startsWith("-") ? argv[3] : void 0,
1098
+ snapshotPath: getFlagValue(argv, "--snapshot-path"),
1099
+ target: getFlagValue(argv, "--to")
650
1100
  });
651
1101
  };
652
1102
  if (process.argv[1] && (process.argv[1] === fileURLToPath(import.meta.url) || process.argv[1].endsWith("cli.js") || process.argv[1].endsWith("cli.ts"))) {
@@ -659,6 +1109,7 @@ export {
659
1109
  handleExplain,
660
1110
  handleInit,
661
1111
  handleInspect,
1112
+ handleInstall,
662
1113
  handleMigrate,
663
1114
  handleUpdate,
664
1115
  runCli