@santi020k/eslint-config-basic 3.0.0 → 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,19 +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";
59
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";
60
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
+ };
61
83
  const getConfigPathIfPresent = (cwd) => {
62
- const configPath = [
63
- "eslint.config.js",
64
- "eslint.config.mjs",
65
- "eslint.config.cjs"
66
- ].map((filename) => join(cwd, filename)).find((p) => existsSync(p));
84
+ const configPath = ESLINT_CONFIG_FILENAMES.map((filename) => join(cwd, filename)).find((p) => existsSync(p));
67
85
  return configPath ?? null;
68
86
  };
69
87
  const readPackageJson = (cwd) => {
@@ -113,7 +131,33 @@ const getLiteInstallPackages = (summary, declaredDependencies) => {
113
131
  }
114
132
  return [...packages];
115
133
  };
116
- 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) => {
117
161
  const packageList = packages.join(" ");
118
162
  switch (packageManager) {
119
163
  case "bun":
@@ -123,7 +167,19 @@ const createInstallCommand = (packageManager, packages) => {
123
167
  case "yarn":
124
168
  return `yarn add -D ${packageList}`;
125
169
  default:
126
- 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]];
127
183
  }
128
184
  };
129
185
  const detectPackageManager = (cwd) => {
@@ -178,8 +234,40 @@ const detectWorkspaceProjects = (cwd) => {
178
234
  ];
179
235
  return [...new Set(patterns.flatMap((pattern) => expandWorkspacePattern(cwd, pattern)))].sort();
180
236
  };
181
- const createConfigContent = (cwd) => {
182
- const configContent = "export { default } from '@santi020k/eslint-config-basic/recommended'\n";
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");
183
271
  return {
184
272
  configContent,
185
273
  configPath: resolveConfigPath(cwd)
@@ -193,6 +281,7 @@ const getProjectSummary = (cwd) => {
193
281
  const options = detectProjectOptions(cwd);
194
282
  const workspaceProjects = detectWorkspaceProjects(cwd);
195
283
  return {
284
+ detectedProjects: Object.keys(options.projects ?? {}).sort(),
196
285
  extensions: options.extensions ?? [],
197
286
  formats: options.formats ?? [],
198
287
  frameworks: getFrameworkKeys(options.detectedFrameworks),
@@ -206,6 +295,30 @@ const getProjectSummary = (cwd) => {
206
295
  workspaceProjects
207
296
  };
208
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
+ };
209
322
  const createStandardsContent = (cwd) => {
210
323
  const summary = getProjectSummary(cwd);
211
324
  return [
@@ -229,9 +342,9 @@ const createStandardsContent = (cwd) => {
229
342
  "## Recommended Config",
230
343
  "",
231
344
  "```js",
232
- "import { eslintConfig } from '@santi020k/eslint-config-basic'",
345
+ "import { defineConfig } from '@santi020k/eslint-config-basic'",
233
346
  "",
234
- "export default await eslintConfig()",
347
+ "export default await defineConfig()",
235
348
  "```",
236
349
  "",
237
350
  "Use `basic-eslint explain` when you want to inspect what the zero-config setup detects."
@@ -256,23 +369,43 @@ const printUsage = () => {
256
369
  " update Regenerate eslint.config.js/mjs from detection",
257
370
  " explain Print detected v3 config inputs",
258
371
  " inspect Print detected inputs and active config features",
372
+ " install Install missing packages for detected v3 features",
259
373
  " doctor Check project setup for common v3 adoption issues",
374
+ " compatibility Check runtime and config package compatibility",
260
375
  " docs Generate ESLINT_STANDARDS.md from detection",
261
- " 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",
262
381
  " generate-skill Generate AI agent standards files",
263
382
  "",
264
383
  "Options:",
265
384
  " --force Overwrite existing generated skill sections/files",
266
- " --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",
267
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",
268
393
  " --json Print JSON for commands that support it",
269
- " --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",
270
403
  " --write Apply safe migrations for commands that support it",
271
404
  " --help, -h Show this help message",
272
405
  " --version, -v Show CLI version"
273
406
  ].join("\n"));
274
407
  };
275
- const handleInit = (cwd = process.cwd(), check = false) => {
408
+ const handleInit = (cwd = process.cwd(), check = false, explicit = false) => {
276
409
  const configPath = resolveConfigPath(cwd);
277
410
  if (check) {
278
411
  const exists = existsSync(configPath);
@@ -289,7 +422,7 @@ const handleInit = (cwd = process.cwd(), check = false) => {
289
422
  return;
290
423
  }
291
424
  console.log("\u{1F50D} Detecting project settings...");
292
- const { configContent } = createConfigContent(cwd);
425
+ const { configContent } = createConfigContent(cwd, explicit);
293
426
  writeFileSync(configPath, configContent);
294
427
  console.log(`\u2705 Created ${basename(configPath)} with auto-detected settings!`);
295
428
  console.log("\u{1F680} Ready to lint!");
@@ -429,7 +562,9 @@ const validateConfigContent = (configContent, hasV1FrameworkImports, declaredDep
429
562
  if (hasV1FrameworkImports) {
430
563
  warnings.push("Config still imports v1 framework packages. Run `basic-eslint migrate --write` or switch to framework booleans.");
431
564
  }
432
- 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)) {
433
568
  warnings.push("Workspace packages were detected, but the root config does not use `projects` scoping.");
434
569
  }
435
570
  if (configContent.includes(LITE_PACKAGE_NAME)) {
@@ -509,7 +644,7 @@ const buildDoctorDiagnosis = (cwd, configPath, configContent, hasV1FrameworkImpo
509
644
  checkDuplicateEslint(cwd, warnings);
510
645
  return { issues, warnings };
511
646
  };
512
- const outputDoctorResult = (json, configPath, packageManager, summary, liteInstallCommand, issues, warnings) => {
647
+ const outputDoctorResult = (json, configPath, packageManager, summary, liteInstallCommand, issues, warnings, fixes = []) => {
513
648
  let status = "passed";
514
649
  if (issues.length > 0) {
515
650
  status = "failed";
@@ -518,6 +653,7 @@ const outputDoctorResult = (json, configPath, packageManager, summary, liteInsta
518
653
  }
519
654
  const payload = {
520
655
  configFile: configPath ? basename(configPath) : null,
656
+ fixes,
521
657
  issues,
522
658
  liteInstallCommand,
523
659
  packageManager,
@@ -535,22 +671,88 @@ const outputDoctorResult = (json, configPath, packageManager, summary, liteInsta
535
671
  `- Package manager: ${payload.packageManager}`,
536
672
  `- Config file: ${payload.configFile ?? "none"}`,
537
673
  `- Workspace projects: ${formatList(summary.workspaceProjects)}`,
674
+ ...fixes.length > 0 ? ["", "Fixes applied:", ...fixes.map((fix) => `- ${fix}`)] : [],
538
675
  ...issues.length > 0 ? ["", "Issues:", ...issues.map((issue) => `- ${issue}`)] : [],
539
676
  ...warnings.length > 0 ? ["", "Warnings:", ...warnings.map((warning) => `- ${warning}`)] : []
540
677
  ].join("\n"));
541
678
  if (issues.length > 0) process.exitCode = 1;
542
679
  };
543
- const handleDoctor = async (cwd = process.cwd(), json = false, liteInstall = false) => {
544
- const configPath = getConfigPathIfPresent(cwd);
545
- const packageJson = readPackageJson(cwd);
546
- const declaredDependencies = getDeclaredDependencyNames(packageJson);
547
- const summary = getProjectSummary(cwd);
548
- const activeConfig = await analyzeEslintConfig(cwd);
549
- 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) => {
550
745
  const packageManager = detectPackageManager(cwd);
551
- const liteInstallPackages = getLiteInstallPackages(summary, declaredDependencies);
552
- const liteInstallCommand = createInstallCommand(packageManager, liteInstallPackages);
553
- 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);
554
756
  if (liteInstall) {
555
757
  if (json) {
556
758
  console.log(JSON.stringify({ command: liteInstallCommand, packageManager, packages: liteInstallPackages }, null, 2));
@@ -559,8 +761,64 @@ const handleDoctor = async (cwd = process.cwd(), json = false, liteInstall = fal
559
761
  }
560
762
  return;
561
763
  }
562
- const { issues, warnings } = buildDoctorDiagnosis(cwd, configPath, configContent, hasV1FrameworkImports, activeConfig, declaredDependencies, summary);
563
- 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
+ }
564
822
  };
565
823
  const handleDocs = (cwd = process.cwd()) => {
566
824
  const outputPath = join(cwd, "ESLINT_STANDARDS.md");
@@ -574,9 +832,9 @@ const migrateConfigContent = (content) => {
574
832
  }
575
833
  const frameworks = getFrameworkKeys(importedFrameworks);
576
834
  const migrated = [
577
- "import { eslintConfig } from '@santi020k/eslint-config-basic'",
835
+ "import { defineConfig } from '@santi020k/eslint-config-basic'",
578
836
  "",
579
- "export default await eslintConfig({",
837
+ "export default await defineConfig({",
580
838
  " frameworks: {",
581
839
  ` ${frameworks.map((key) => `${toPropertyKey(key)}: true`).join(",\n ")}`,
582
840
  " }",
@@ -609,14 +867,32 @@ const processConfigMigration = (configPath, write, suggestions) => {
609
867
  }
610
868
  return applied;
611
869
  };
612
- 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
+ }
613
889
  const configPath = getConfigPathIfPresent(cwd) ?? join(cwd, getDefaultConfigFilename(cwd));
614
890
  const suggestions = [
615
891
  "v1 to v2 migration suggestions:",
616
892
  "- Install only @santi020k/eslint-config-basic for the public config API.",
617
893
  "- Replace framework imports with bundled booleans such as frameworks: { react: true, next: true }.",
618
894
  "- Remove app-level @santi020k/eslint-config-react/next/vue/etc. config package imports.",
619
- "- Try eslintConfig() first; v3 auto-detects supported frameworks and integrations.",
895
+ "- Try defineConfig() first; v3 auto-detects supported frameworks and integrations.",
620
896
  "- Use basic-eslint explain to review what v3 detects before committing the migration."
621
897
  ];
622
898
  const applied = processConfigMigration(configPath, write, suggestions);
@@ -634,30 +910,73 @@ const handleMigrate = (cwd = process.cwd(), write = false, json = false) => {
634
910
  };
635
911
  const dispatchCommand = (command, cwd, flags) => {
636
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
+ }
637
941
  case "docs": {
638
942
  handleDocs(cwd);
639
943
  break;
640
944
  }
641
945
  case "doctor": {
642
- handleDoctor(cwd, flags.hasJson, flags.hasLiteInstall).catch((error) => {
946
+ handleDoctor(cwd, flags.hasJson, flags.hasLiteInstall, flags.hasFix).catch((error) => {
643
947
  console.error(`\u274C Failed to run doctor: ${String(error)}`);
644
948
  process.exitCode = 1;
645
949
  });
646
950
  break;
647
951
  }
648
952
  case "explain": {
649
- 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
+ }
650
965
  break;
651
966
  }
652
967
  case "generate-skill": {
653
- 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) => {
654
973
  console.error(`\u274C Failed to generate skill files: ${String(error)}`);
655
974
  process.exitCode = 1;
656
975
  });
657
976
  break;
658
977
  }
659
978
  case "init": {
660
- handleInit(cwd, flags.hasCheck);
979
+ handleInit(cwd, flags.hasCheck, flags.hasExplicit);
661
980
  break;
662
981
  }
663
982
  case "inspect": {
@@ -667,8 +986,52 @@ const dispatchCommand = (command, cwd, flags) => {
667
986
  });
668
987
  break;
669
988
  }
989
+ case "install": {
990
+ handleInstall(cwd, flags.hasDryRun);
991
+ break;
992
+ }
670
993
  case "migrate": {
671
- 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
+ });
672
1035
  break;
673
1036
  }
674
1037
  case "update": {
@@ -682,6 +1045,24 @@ const dispatchCommand = (command, cwd, flags) => {
682
1045
  }
683
1046
  }
684
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
+ };
685
1066
  const runCli = (argv = process.argv, cwd = process.cwd()) => {
686
1067
  const command = argv[2];
687
1068
  const isHelp = command === "--help" || command === "-h";
@@ -695,12 +1076,27 @@ const runCli = (argv = process.argv, cwd = process.cwd()) => {
695
1076
  return;
696
1077
  }
697
1078
  dispatchCommand(command, cwd, {
1079
+ concurrency: getFlagValue(argv, "--concurrency"),
1080
+ files: getFlagValues(argv, "--file"),
698
1081
  hasCheck: argv.includes("--check"),
699
1082
  hasCreate: argv.includes("--create"),
1083
+ hasDryRun: argv.includes("--dry-run"),
1084
+ hasExplicit: argv.includes("--explicit"),
1085
+ hasFix: argv.includes("--fix"),
700
1086
  hasForce: argv.includes("--force"),
1087
+ hasFull: argv.includes("--full"),
701
1088
  hasJson: argv.includes("--json"),
702
1089
  hasLiteInstall: argv.includes("--lite-install"),
703
- 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")
704
1100
  });
705
1101
  };
706
1102
  if (process.argv[1] && (process.argv[1] === fileURLToPath(import.meta.url) || process.argv[1].endsWith("cli.js") || process.argv[1].endsWith("cli.ts"))) {
@@ -713,6 +1109,7 @@ export {
713
1109
  handleExplain,
714
1110
  handleInit,
715
1111
  handleInspect,
1112
+ handleInstall,
716
1113
  handleMigrate,
717
1114
  handleUpdate,
718
1115
  runCli