@santi020k/eslint-config-basic 3.0.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,9 +1,23 @@
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 { handleExplainPreset } from "./cli-preset.js";
15
+ import {
16
+ handleBaseline,
17
+ handleProfile,
18
+ handleSnapshot,
19
+ handleSnapshotDiff
20
+ } from "./cli-workflows.js";
7
21
  import { detectProjectOptions } from "./index.js";
8
22
  const getDefaultConfigFilename = (cwd) => {
9
23
  const packageJsonPath = join(cwd, "package.json");
@@ -16,15 +30,12 @@ const getDefaultConfigFilename = (cwd) => {
16
30
  }
17
31
  };
18
32
  const resolveConfigPath = (cwd) => {
19
- const existingConfigPath = [
20
- "eslint.config.js",
21
- "eslint.config.mjs"
22
- ].map((filename) => join(cwd, filename)).find((p) => existsSync(p));
33
+ const existingConfigPath = ESLINT_CONFIG_FILENAMES.map((filename) => join(cwd, filename)).find((p) => existsSync(p));
23
34
  return existingConfigPath ?? join(cwd, getDefaultConfigFilename(cwd));
24
35
  };
25
36
  const getFrameworkKeys = (detectedFrameworks) => {
26
37
  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")) {
38
+ if (frameworkKeys.has("next") || frameworkKeys.has("expo") || frameworkKeys.has("react-router") || frameworkKeys.has("tanstack-start") && !frameworkKeys.has("solid")) {
28
39
  frameworkKeys.add("react");
29
40
  }
30
41
  return [...frameworkKeys].sort();
@@ -43,7 +54,7 @@ const FRAMEWORK_PACKAGE_TO_KEY = {
43
54
  "@santi020k/eslint-config-qwik": "qwik",
44
55
  "@santi020k/eslint-config-react": "react",
45
56
  "@santi020k/eslint-config-react-router": "react-router",
46
- "@santi020k/eslint-config-remix": "remix",
57
+ "@santi020k/eslint-config-remix": "react-router",
47
58
  "@santi020k/eslint-config-slidev": "slidev",
48
59
  "@santi020k/eslint-config-solid": "solid",
49
60
  "@santi020k/eslint-config-svelte": "svelte",
@@ -51,19 +62,27 @@ const FRAMEWORK_PACKAGE_TO_KEY = {
51
62
  "@santi020k/eslint-config-vite": "vite",
52
63
  "@santi020k/eslint-config-vue": "vue"
53
64
  };
54
- const FRAMEWORK_KEY_TO_PACKAGE = Object.fromEntries(
55
- Object.entries(FRAMEWORK_PACKAGE_TO_KEY).map(([packageName, framework]) => [framework, packageName])
56
- );
65
+ const FRAMEWORK_KEY_TO_PACKAGE = {
66
+ ...Object.fromEntries(
67
+ Object.entries(FRAMEWORK_PACKAGE_TO_KEY).filter(([packageName]) => packageName !== "@santi020k/eslint-config-remix").map(([packageName, framework]) => [framework, packageName])
68
+ ),
69
+ "react-router": "@santi020k/eslint-config-react-router"
70
+ };
57
71
  const LITE_PACKAGE_NAME = "@santi020k/eslint-config-lite";
58
72
  const INTEGRATIONS_PACKAGE_NAME = "@santi020k/eslint-config-integrations";
59
73
  const ASTRO_DOCTOR_PACKAGE_NAME = "@santi020k/eslint-plugin-astro-doctor";
74
+ const BASIC_PACKAGE_NAME = "@santi020k/eslint-config-basic";
75
+ const FULL_PACKAGE_NAME = "@santi020k/eslint-config-full";
60
76
  const TYPESCRIPT_PACKAGE_NAME = "typescript";
77
+ const CATEGORY_PACKAGES = {
78
+ extensions: "@santi020k/eslint-config-extensions",
79
+ formats: "@santi020k/eslint-config-formats",
80
+ libraries: "@santi020k/eslint-config-libraries",
81
+ testing: "@santi020k/eslint-config-testing",
82
+ tools: "@santi020k/eslint-config-tools"
83
+ };
61
84
  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));
85
+ const configPath = ESLINT_CONFIG_FILENAMES.map((filename) => join(cwd, filename)).find((p) => existsSync(p));
67
86
  return configPath ?? null;
68
87
  };
69
88
  const readPackageJson = (cwd) => {
@@ -113,7 +132,33 @@ const getLiteInstallPackages = (summary, declaredDependencies) => {
113
132
  }
114
133
  return [...packages];
115
134
  };
116
- const createInstallCommand = (packageManager, packages) => {
135
+ const getInstallPackages = (summary, declaredDependencies, explicitFeaturePackages = []) => {
136
+ const usesFullPackage = declaredDependencies.has(FULL_PACKAGE_NAME);
137
+ const packages = /* @__PURE__ */ new Set(["eslint"]);
138
+ if (!usesFullPackage) packages.add(BASIC_PACKAGE_NAME);
139
+ if (!usesFullPackage) {
140
+ for (const framework of summary.frameworks) {
141
+ const packageName = FRAMEWORK_KEY_TO_PACKAGE[framework];
142
+ if (packageName) packages.add(packageName);
143
+ }
144
+ const categorySelections = [
145
+ [summary.extensions, CATEGORY_PACKAGES.extensions],
146
+ [summary.formats, CATEGORY_PACKAGES.formats],
147
+ [summary.libraries, CATEGORY_PACKAGES.libraries],
148
+ [summary.testing, CATEGORY_PACKAGES.testing],
149
+ [summary.tools, CATEGORY_PACKAGES.tools]
150
+ ];
151
+ for (const [features, packageName] of categorySelections) {
152
+ if (features.length > 0) packages.add(packageName);
153
+ }
154
+ for (const packageName of explicitFeaturePackages) {
155
+ packages.add(packageName);
156
+ }
157
+ }
158
+ if (summary.typescript) packages.add(TYPESCRIPT_PACKAGE_NAME);
159
+ return [...packages].filter((packageName) => !declaredDependencies.has(packageName));
160
+ };
161
+ const createInstallCommand = (packageManager, packages, workspaceRoot = false, catalog = false) => {
117
162
  const packageList = packages.join(" ");
118
163
  switch (packageManager) {
119
164
  case "bun":
@@ -123,15 +168,146 @@ const createInstallCommand = (packageManager, packages) => {
123
168
  case "yarn":
124
169
  return `yarn add -D ${packageList}`;
125
170
  default:
126
- return `pnpm add -D ${packageList}`;
171
+ return `pnpm add -D${workspaceRoot ? " --workspace-root" : ""}` + (catalog === true ? " --save-catalog" : "") + `${typeof catalog === "string" ? ` --save-catalog-name=${catalog}` : ""} ${packageList}`;
172
+ }
173
+ };
174
+ const createInstallInvocation = (packageManager, packages, workspaceRoot = false, catalog = false) => {
175
+ switch (packageManager) {
176
+ case "bun":
177
+ return ["bun", ["add", "-d", ...packages]];
178
+ case "npm":
179
+ return ["npm", ["install", "-D", ...packages]];
180
+ case "yarn":
181
+ return ["yarn", ["add", "-D", ...packages]];
182
+ default:
183
+ return ["pnpm", [
184
+ "add",
185
+ "-D",
186
+ ...workspaceRoot ? ["--workspace-root"] : [],
187
+ ...catalog === true ? ["--save-catalog"] : [],
188
+ ...typeof catalog === "string" ? [`--save-catalog-name=${catalog}`] : [],
189
+ ...packages
190
+ ]];
191
+ }
192
+ };
193
+ const findPnpmWorkspaceRoot = (cwd) => {
194
+ let current = cwd;
195
+ for (; ; ) {
196
+ if (existsSync(join(current, "pnpm-workspace.yaml"))) return current;
197
+ const parent = dirname(current);
198
+ if (parent === current) return void 0;
199
+ current = parent;
127
200
  }
128
201
  };
129
202
  const detectPackageManager = (cwd) => {
130
- if (existsSync(join(cwd, "pnpm-lock.yaml")) || existsSync(join(cwd, "pnpm-workspace.yaml"))) return "pnpm";
203
+ if (findPnpmWorkspaceRoot(cwd) || existsSync(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
131
204
  if (existsSync(join(cwd, "yarn.lock"))) return "yarn";
132
205
  if (existsSync(join(cwd, "bun.lockb")) || existsSync(join(cwd, "bun.lock"))) return "bun";
133
206
  return "npm";
134
207
  };
208
+ const getCatalogPreference = (packageJson) => {
209
+ const dependencyFields = ["devDependencies", "dependencies"];
210
+ const dependencyRecords = [];
211
+ const parseCatalog = (value) => {
212
+ if (typeof value !== "string" || !value.startsWith("catalog:")) return false;
213
+ const name = value.slice("catalog:".length);
214
+ return name && name !== "default" ? name : true;
215
+ };
216
+ for (const field of dependencyFields) {
217
+ const dependencies = packageJson?.[field];
218
+ if (!dependencies || typeof dependencies !== "object" || Array.isArray(dependencies)) continue;
219
+ dependencyRecords.push(dependencies);
220
+ }
221
+ for (const dependencies of dependencyRecords) {
222
+ const basicCatalog = parseCatalog(Reflect.get(dependencies, BASIC_PACKAGE_NAME));
223
+ if (basicCatalog) return basicCatalog;
224
+ }
225
+ for (const dependencies of dependencyRecords) {
226
+ for (const value of Object.values(dependencies)) {
227
+ const catalog = parseCatalog(value);
228
+ if (catalog) return catalog;
229
+ }
230
+ }
231
+ return false;
232
+ };
233
+ const parseYamlMappingLine = (line) => {
234
+ const match = /^(\s*)(?:"([^"]+)"|'([^']+)'|([^'"].*?))\s*:\s*(.*?)\s*$/.exec(line);
235
+ if (!match) return void 0;
236
+ const value = match[5].replace(/\s+#.*$/, "").trim();
237
+ const key = match[2] || match[3] || match[4];
238
+ return {
239
+ indent: match[1].length,
240
+ key: key.trim(),
241
+ value: /^(['"]).*\1$/.test(value) ? value.slice(1, -1) : value
242
+ };
243
+ };
244
+ const getCatalogVersion = (workspaceRoot, packageName, catalog) => {
245
+ const workspacePath = join(workspaceRoot, "pnpm-workspace.yaml");
246
+ if (!existsSync(workspacePath)) return void 0;
247
+ const lines = readFileSync(workspacePath, "utf8").split(/\r?\n/);
248
+ let sectionIndex = -1;
249
+ let sectionIndent = -1;
250
+ if (catalog === true) {
251
+ sectionIndex = lines.findIndex((line) => {
252
+ const mapping = parseYamlMappingLine(line);
253
+ return mapping?.indent === 0 && mapping.key === "catalog" && mapping.value === "";
254
+ });
255
+ sectionIndent = 0;
256
+ } else {
257
+ const catalogsIndex = lines.findIndex((line) => {
258
+ const mapping = parseYamlMappingLine(line);
259
+ return mapping?.indent === 0 && mapping.key === "catalogs" && mapping.value === "";
260
+ });
261
+ if (catalogsIndex >= 0) {
262
+ for (let index = catalogsIndex + 1; index < lines.length; index++) {
263
+ const mapping = parseYamlMappingLine(lines.at(index) ?? "");
264
+ if (!mapping) continue;
265
+ if (mapping.indent === 0) break;
266
+ if (mapping.key === catalog && mapping.value === "") {
267
+ sectionIndex = index;
268
+ sectionIndent = mapping.indent;
269
+ break;
270
+ }
271
+ }
272
+ }
273
+ }
274
+ if (sectionIndex < 0) return void 0;
275
+ for (let index = sectionIndex + 1; index < lines.length; index++) {
276
+ const mapping = parseYamlMappingLine(lines.at(index) ?? "");
277
+ if (!mapping) continue;
278
+ if (mapping.indent <= sectionIndent) break;
279
+ if (mapping.key === packageName) return mapping.value || void 0;
280
+ }
281
+ return void 0;
282
+ };
283
+ const getCompatibleConfigVersion = (packageJson, workspaceRoot) => {
284
+ const dependencyFields = ["devDependencies", "dependencies", "peerDependencies"];
285
+ let basicSpec;
286
+ for (const field of dependencyFields) {
287
+ const dependencies = packageJson?.[field];
288
+ if (!dependencies || typeof dependencies !== "object" || Array.isArray(dependencies)) continue;
289
+ const value = Object.entries(dependencies).find(([packageName]) => packageName === BASIC_PACKAGE_NAME)?.[1];
290
+ if (typeof value === "string") {
291
+ basicSpec = value;
292
+ break;
293
+ }
294
+ }
295
+ const catalogName = basicSpec?.startsWith("catalog:") ? basicSpec.slice("catalog:".length) : void 0;
296
+ let selectedCatalog;
297
+ if (catalogName !== void 0) {
298
+ selectedCatalog = catalogName && catalogName !== "default" ? catalogName : true;
299
+ }
300
+ const resolvedSpec = selectedCatalog && workspaceRoot ? getCatalogVersion(workspaceRoot, BASIC_PACKAGE_NAME, selectedCatalog) : basicSpec;
301
+ const match = /(\d+)\.(\d+)\.(\d+)/.exec(resolvedSpec ?? getCliVersion());
302
+ return match ? `^${match[1]}.${match[2]}.${match[3]}` : "^3.1.0";
303
+ };
304
+ const addCompatibleConfigVersions = (packages, version) => {
305
+ const major = /\d+/.exec(version)?.[0];
306
+ const companionVersion = major ? `^${major}.0.0` : version;
307
+ return packages.map(
308
+ (packageName) => packageName.startsWith("@santi020k/eslint-config-") && packageName !== LITE_PACKAGE_NAME && packageName !== INTEGRATIONS_PACKAGE_NAME ? `${packageName}@${packageName === BASIC_PACKAGE_NAME ? version : companionVersion}` : packageName
309
+ );
310
+ };
135
311
  const hasLintScript = (cwd) => {
136
312
  const packageJson = readPackageJson(cwd);
137
313
  return Boolean(packageJson?.scripts?.lint);
@@ -178,8 +354,42 @@ const detectWorkspaceProjects = (cwd) => {
178
354
  ];
179
355
  return [...new Set(patterns.flatMap((pattern) => expandWorkspacePattern(cwd, pattern)))].sort();
180
356
  };
181
- const createConfigContent = (cwd) => {
182
- const configContent = "export { default } from '@santi020k/eslint-config-basic/recommended'\n";
357
+ const createExplicitOptions = (cwd) => {
358
+ const summary = getProjectSummary(cwd);
359
+ const options = [];
360
+ const features = [.../* @__PURE__ */ new Set([
361
+ ...summary.extensions,
362
+ ...summary.formats,
363
+ ...summary.libraries,
364
+ ...summary.testing,
365
+ ...summary.tools
366
+ ])].sort();
367
+ if (summary.typescript) options.push(" typescript: true,");
368
+ if (summary.frameworks.length > 0) {
369
+ options.push(
370
+ " frameworks: {",
371
+ ...summary.frameworks.map((framework) => ` ${toPropertyKey(framework)}: true,`),
372
+ " },"
373
+ );
374
+ }
375
+ if (features.length > 0) {
376
+ options.push(
377
+ " features: {",
378
+ ...features.map((feature) => ` ${toPropertyKey(feature)}: true,`),
379
+ " },"
380
+ );
381
+ }
382
+ return options;
383
+ };
384
+ const createConfigContent = (cwd, explicit = false) => {
385
+ const explicitOptions = explicit ? createExplicitOptions(cwd) : [];
386
+ const invocation = explicitOptions.length > 0 ? ["export default defineConfig({", ...explicitOptions, "})"] : ["export default defineConfig()"];
387
+ const configContent = [
388
+ "import { defineConfig } from '@santi020k/eslint-config-basic'",
389
+ "",
390
+ ...invocation,
391
+ ""
392
+ ].join("\n");
183
393
  return {
184
394
  configContent,
185
395
  configPath: resolveConfigPath(cwd)
@@ -193,6 +403,7 @@ const getProjectSummary = (cwd) => {
193
403
  const options = detectProjectOptions(cwd);
194
404
  const workspaceProjects = detectWorkspaceProjects(cwd);
195
405
  return {
406
+ detectedProjects: Object.keys(options.projects ?? {}).sort(),
196
407
  extensions: options.extensions ?? [],
197
408
  formats: options.formats ?? [],
198
409
  frameworks: getFrameworkKeys(options.detectedFrameworks),
@@ -206,6 +417,34 @@ const getProjectSummary = (cwd) => {
206
417
  workspaceProjects
207
418
  };
208
419
  };
420
+ const getInstallProjectSummary = (cwd) => {
421
+ const rootSummary = getProjectSummary(cwd);
422
+ const projectPaths = [.../* @__PURE__ */ new Set([
423
+ ...rootSummary.detectedProjects,
424
+ ...rootSummary.workspaceProjects
425
+ ])];
426
+ const projectSummaries = projectPaths.map((projectPath) => getProjectSummary(join(cwd, projectPath)));
427
+ const summaries = [rootSummary, ...projectSummaries];
428
+ return {
429
+ ...rootSummary,
430
+ extensions: [...new Set(summaries.flatMap((summary) => summary.extensions))],
431
+ formats: [...new Set(summaries.flatMap((summary) => summary.formats))],
432
+ frameworks: [...new Set(summaries.flatMap((summary) => summary.frameworks))].sort(),
433
+ libraries: [...new Set(summaries.flatMap((summary) => summary.libraries))],
434
+ testing: [...new Set(summaries.flatMap((summary) => summary.testing))],
435
+ tools: [...new Set(summaries.flatMap((summary) => summary.tools))],
436
+ typescript: summaries.some((summary) => summary.typescript)
437
+ };
438
+ };
439
+ const getWorkspaceDeclaredConfigPackages = (cwd, summary) => {
440
+ const packages = /* @__PURE__ */ new Set();
441
+ for (const projectPath of summary.detectedProjects) {
442
+ for (const packageName of getDeclaredDependencyNames(readPackageJson(join(cwd, projectPath)))) {
443
+ if (packageName.startsWith("@santi020k/eslint-config-")) packages.add(packageName);
444
+ }
445
+ }
446
+ return [...packages];
447
+ };
209
448
  const createStandardsContent = (cwd) => {
210
449
  const summary = getProjectSummary(cwd);
211
450
  return [
@@ -229,9 +468,9 @@ const createStandardsContent = (cwd) => {
229
468
  "## Recommended Config",
230
469
  "",
231
470
  "```js",
232
- "import { eslintConfig } from '@santi020k/eslint-config-basic'",
471
+ "import { defineConfig } from '@santi020k/eslint-config-basic'",
233
472
  "",
234
- "export default await eslintConfig()",
473
+ "export default await defineConfig()",
235
474
  "```",
236
475
  "",
237
476
  "Use `basic-eslint explain` when you want to inspect what the zero-config setup detects."
@@ -255,24 +494,135 @@ const printUsage = () => {
255
494
  " init Create eslint.config.js/mjs if missing",
256
495
  " update Regenerate eslint.config.js/mjs from detection",
257
496
  " explain Print detected v3 config inputs",
497
+ " explain-preset Compare the current config with a preset",
258
498
  " inspect Print detected inputs and active config features",
499
+ " install Install missing packages for detected v3 features",
259
500
  " doctor Check project setup for common v3 adoption issues",
501
+ " compatibility Check runtime and config package compatibility",
260
502
  " docs Generate ESLINT_STANDARDS.md from detection",
261
- " migrate Report v1-to-v2 migration suggestions",
503
+ " migrate Plan or apply v1-to-v2 and v2-to-v3 migrations",
504
+ " baseline Suppress existing violations for incremental adoption",
505
+ " profile Compare ESLint performance and report slow rules",
506
+ " snapshot Save the effective rules for representative files",
507
+ " diff Compare effective rules with the saved snapshot",
262
508
  " generate-skill Generate AI agent standards files",
263
509
  "",
264
510
  "Options:",
265
511
  " --force Overwrite existing generated skill sections/files",
266
- " --check init: verify config exists; generate-skill: verify files are up to date",
512
+ " --check Verify generated files, migrations, or snapshots without writing",
513
+ " --concurrency profile: off, auto, or a worker count",
267
514
  " --create generate-skill: scaffold a root AGENTS.md when missing",
515
+ " --dry-run install: print the detected install command without running it",
516
+ " --explicit init: write detected settings explicitly",
517
+ " --file explain/profile/snapshot/diff: representative file or lint target (repeatable)",
518
+ " --fix doctor: safely repair generated config and package metadata",
519
+ " --full migrate --to v3: choose the batteries-included package",
268
520
  " --json Print JSON for commands that support it",
269
- " --lite-install doctor: print the detected lite install command",
521
+ " --lite-install doctor: print the deprecated v2 Lite install command (removed in v4)",
522
+ " --max-duration profile: maximum duration in milliseconds",
523
+ " --max-rule-time profile: maximum time for the slowest rule in milliseconds",
524
+ " --max-warnings profile: maximum warning count",
525
+ " --compatibility explain-preset: write a temporary compatibility override",
526
+ " --output explain-preset: compatibility override path",
527
+ " --preset baseline: enable ci or pedantic strict mode before suppressing",
528
+ " --prune baseline: remove suppressions for resolved violations",
529
+ " --snapshot-path snapshot/diff: override .eslint-config-snapshot.json",
530
+ " --to migrate: target version (v2 or v3)",
531
+ " --with-eslint-mcp generate-skill: scaffold the official ESLint MCP server",
270
532
  " --write Apply safe migrations for commands that support it",
271
533
  " --help, -h Show this help message",
272
534
  " --version, -v Show CLI version"
273
535
  ].join("\n"));
274
536
  };
275
- const handleInit = (cwd = process.cwd(), check = false) => {
537
+ const COMMAND_OPTIONS = {
538
+ baseline: ["--json", "--preset", "--prune"],
539
+ compatibility: ["--json"],
540
+ diff: ["--file", "--json", "--snapshot-path"],
541
+ docs: [],
542
+ doctor: ["--fix", "--json", "--lite-install"],
543
+ explain: ["--file", "--json"],
544
+ "explain-preset": ["--compatibility", "--file", "--json", "--output"],
545
+ "generate-skill": ["--check", "--create", "--force", "--with-eslint-mcp"],
546
+ init: ["--check", "--explicit"],
547
+ inspect: ["--json"],
548
+ install: ["--dry-run"],
549
+ migrate: ["--check", "--full", "--json", "--to", "--write"],
550
+ profile: ["--concurrency", "--file", "--json", "--max-duration", "--max-rule-time", "--max-warnings"],
551
+ snapshot: ["--check", "--file", "--json", "--snapshot-path"],
552
+ update: []
553
+ };
554
+ const VALUE_OPTIONS = /* @__PURE__ */ new Set([
555
+ "--concurrency",
556
+ "--file",
557
+ "--max-duration",
558
+ "--max-rule-time",
559
+ "--max-warnings",
560
+ "--output",
561
+ "--preset",
562
+ "--snapshot-path",
563
+ "--to"
564
+ ]);
565
+ const printCommandUsage = (command) => {
566
+ let positional = "";
567
+ if (command === "explain") positional = " [rule]";
568
+ else if (command === "explain-preset") positional = " <preset>";
569
+ const options = COMMAND_OPTIONS[command] ?? [];
570
+ console.log([
571
+ "Usage: basic-eslint <command> [options]",
572
+ "",
573
+ `Usage: basic-eslint ${command}${positional}${options.length > 0 ? " [options]" : ""}`,
574
+ ...options.length > 0 ? ["", "Options:", ...options.map((option) => ` ${option}`)] : []
575
+ ].join("\n"));
576
+ };
577
+ const validateCommandArguments = (command, args) => {
578
+ const allowed = COMMAND_OPTIONS[command];
579
+ if (!allowed) return { ok: true };
580
+ const allowedOptions = new Set(allowed);
581
+ const positionals = [];
582
+ for (let index = 0; index < args.length; index++) {
583
+ const argument = args[index];
584
+ if (!argument.startsWith("-")) {
585
+ positionals.push(argument);
586
+ continue;
587
+ }
588
+ const option = argument.includes("=") ? argument.slice(0, argument.indexOf("=")) : argument;
589
+ if (!allowedOptions.has(option)) {
590
+ console.error(`Unknown option for ${command}: ${option}`);
591
+ process.exitCode = 1;
592
+ return { ok: false };
593
+ }
594
+ if (argument.includes("=") && !VALUE_OPTIONS.has(option)) {
595
+ console.error(`Option ${option} does not accept a value.`);
596
+ process.exitCode = 1;
597
+ return { ok: false };
598
+ }
599
+ if (VALUE_OPTIONS.has(option) && !argument.includes("=")) {
600
+ const value = args[index + 1];
601
+ if (!value || value.startsWith("-")) {
602
+ console.error(
603
+ ["--max-duration", "--max-rule-time", "--max-warnings"].includes(option) ? `${option} must be a non-negative number.` : `Option ${option} requires a value.`
604
+ );
605
+ process.exitCode = 1;
606
+ return { ok: false };
607
+ }
608
+ index++;
609
+ } else if (VALUE_OPTIONS.has(option) && argument.endsWith("=")) {
610
+ console.error(
611
+ ["--max-duration", "--max-rule-time", "--max-warnings"].includes(option) ? `${option} must be a non-negative number.` : `Option ${option} requires a value.`
612
+ );
613
+ process.exitCode = 1;
614
+ return { ok: false };
615
+ }
616
+ }
617
+ const maximumPositionals = command === "explain" || command === "explain-preset" ? 1 : 0;
618
+ if (positionals.length > maximumPositionals) {
619
+ console.error(`Unexpected argument for ${command}: ${positionals[maximumPositionals]}`);
620
+ process.exitCode = 1;
621
+ return { ok: false };
622
+ }
623
+ return { ok: true, positional: positionals[0] };
624
+ };
625
+ const handleInit = (cwd = process.cwd(), check = false, explicit = false) => {
276
626
  const configPath = resolveConfigPath(cwd);
277
627
  if (check) {
278
628
  const exists = existsSync(configPath);
@@ -289,7 +639,7 @@ const handleInit = (cwd = process.cwd(), check = false) => {
289
639
  return;
290
640
  }
291
641
  console.log("\u{1F50D} Detecting project settings...");
292
- const { configContent } = createConfigContent(cwd);
642
+ const { configContent } = createConfigContent(cwd, explicit);
293
643
  writeFileSync(configPath, configContent);
294
644
  console.log(`\u2705 Created ${basename(configPath)} with auto-detected settings!`);
295
645
  console.log("\u{1F680} Ready to lint!");
@@ -429,7 +779,9 @@ const validateConfigContent = (configContent, hasV1FrameworkImports, declaredDep
429
779
  if (hasV1FrameworkImports) {
430
780
  warnings.push("Config still imports v1 framework packages. Run `basic-eslint migrate --write` or switch to framework booleans.");
431
781
  }
432
- if (summary.workspaceProjects.length > 0 && !configContent.includes("projects:")) {
782
+ const usesBasicComposer = /from\s*['"]@santi020k\/eslint-config-(?:basic|full|lite)['"]/.test(configContent) && /\b(?:defineConfig|eslintConfig)\s*\(/.test(configContent);
783
+ const hasDetectedProjectScopes = summary.workspaceProjects.every((project) => summary.detectedProjects.includes(project));
784
+ if (summary.workspaceProjects.length > 0 && !configContent.includes("projects:") && !(usesBasicComposer && hasDetectedProjectScopes)) {
433
785
  warnings.push("Workspace packages were detected, but the root config does not use `projects` scoping.");
434
786
  }
435
787
  if (configContent.includes(LITE_PACKAGE_NAME)) {
@@ -450,6 +802,16 @@ const validateConfigContent = (configContent, hasV1FrameworkImports, declaredDep
450
802
  );
451
803
  }
452
804
  }
805
+ const missingPackages = getInstallPackages(
806
+ summary,
807
+ declaredDependencies,
808
+ getExplicitConfigFeaturePackages(configContent)
809
+ );
810
+ if (missingPackages.length > 0) {
811
+ warnings.push(
812
+ `The active configuration requires undeclared packages: ${missingPackages.join(", ")}. Run \`basic-eslint install\` to add the modular v3 dependency set.`
813
+ );
814
+ }
453
815
  return warnings;
454
816
  };
455
817
  const validateProjectSetup = (cwd, configPath, activeConfig, hasV1FrameworkImports) => {
@@ -509,7 +871,7 @@ const buildDoctorDiagnosis = (cwd, configPath, configContent, hasV1FrameworkImpo
509
871
  checkDuplicateEslint(cwd, warnings);
510
872
  return { issues, warnings };
511
873
  };
512
- const outputDoctorResult = (json, configPath, packageManager, summary, liteInstallCommand, issues, warnings) => {
874
+ const outputDoctorResult = (json, configPath, packageManager, summary, installCommand, requiredPackages, issues, warnings, fixes = []) => {
513
875
  let status = "passed";
514
876
  if (issues.length > 0) {
515
877
  status = "failed";
@@ -518,9 +880,11 @@ const outputDoctorResult = (json, configPath, packageManager, summary, liteInsta
518
880
  }
519
881
  const payload = {
520
882
  configFile: configPath ? basename(configPath) : null,
883
+ fixes,
884
+ ...installCommand ? { installCommand } : {},
521
885
  issues,
522
- liteInstallCommand,
523
886
  packageManager,
887
+ requiredPackages,
524
888
  status,
525
889
  warnings,
526
890
  workspaceProjects: summary.workspaceProjects
@@ -535,23 +899,102 @@ const outputDoctorResult = (json, configPath, packageManager, summary, liteInsta
535
899
  `- Package manager: ${payload.packageManager}`,
536
900
  `- Config file: ${payload.configFile ?? "none"}`,
537
901
  `- Workspace projects: ${formatList(summary.workspaceProjects)}`,
902
+ `- Required packages: ${formatList(requiredPackages)}`,
903
+ ...installCommand ? [`- Install command: ${installCommand}`] : [],
904
+ ...fixes.length > 0 ? ["", "Fixes applied:", ...fixes.map((fix) => `- ${fix}`)] : [],
538
905
  ...issues.length > 0 ? ["", "Issues:", ...issues.map((issue) => `- ${issue}`)] : [],
539
906
  ...warnings.length > 0 ? ["", "Warnings:", ...warnings.map((warning) => `- ${warning}`)] : []
540
907
  ].join("\n"));
541
908
  if (issues.length > 0) process.exitCode = 1;
542
909
  };
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;
550
- 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;
910
+ const createDoctorBackupPath = (filePath) => {
911
+ const preferred = `${filePath}.doctor.bak`;
912
+ if (!existsSync(preferred)) return preferred;
913
+ let index = 2;
914
+ while (existsSync(`${preferred}.${index}`)) index++;
915
+ return `${preferred}.${index}`;
916
+ };
917
+ const getDoctorDependencyVersion = (packageName) => {
918
+ if (packageName === "eslint") return "^10.0.0";
919
+ if (packageName === TYPESCRIPT_PACKAGE_NAME) return "^5.0.0";
920
+ return "^3.0.0";
921
+ };
922
+ const applyDoctorFixes = (cwd, packageJson, configPath, configContent, summary) => {
923
+ const fixes = [];
924
+ if (packageJson) {
925
+ const updated = structuredClone(packageJson);
926
+ let packageChanged = false;
927
+ updated.scripts ??= {};
928
+ if (!updated.scripts.lint) {
929
+ updated.scripts.lint = "eslint .";
930
+ packageChanged = true;
931
+ fixes.push("Added the `lint` script.");
932
+ }
933
+ updated.devDependencies ??= {};
934
+ const declared = getDeclaredDependencyNames(packageJson);
935
+ const explicitFeaturePackages = configContent ? getExplicitConfigFeaturePackages(configContent) : [];
936
+ const missingPackages = getInstallPackages(summary, declared, explicitFeaturePackages);
937
+ const missingDependencies = Object.fromEntries(
938
+ missingPackages.map((packageName) => [packageName, getDoctorDependencyVersion(packageName)])
939
+ );
940
+ updated.devDependencies = {
941
+ ...updated.devDependencies,
942
+ ...missingDependencies
943
+ };
944
+ for (const packageName of missingPackages) {
945
+ fixes.push(`Declared ${packageName}.`);
946
+ packageChanged = true;
947
+ }
948
+ if (packageChanged) {
949
+ const packagePath = join(cwd, "package.json");
950
+ const backupPath = createDoctorBackupPath(packagePath);
951
+ writeFileSync(backupPath, `${JSON.stringify(packageJson, null, 2)}
952
+ `);
953
+ writeFileSync(packagePath, `${JSON.stringify(updated, null, 2)}
954
+ `);
955
+ fixes.push(`Backed up package.json to ${basename(backupPath)}.`);
956
+ }
957
+ }
958
+ if (!configPath) {
959
+ const generated = createConfigContent(cwd);
960
+ writeFileSync(generated.configPath, generated.configContent);
961
+ fixes.push(`Created ${basename(generated.configPath)}.`);
962
+ } else if (configContent && Object.keys(FRAMEWORK_PACKAGE_TO_KEY).some((packageName) => configContent.includes(packageName))) {
963
+ const result = migrateConfigContent(configContent);
964
+ if (result.changed) {
965
+ const backupPath = createDoctorBackupPath(configPath);
966
+ writeFileSync(backupPath, configContent);
967
+ writeFileSync(configPath, result.content);
968
+ fixes.push(`Migrated v1 framework imports and backed up the config to ${basename(backupPath)}.`);
969
+ }
970
+ }
971
+ return fixes;
972
+ };
973
+ const hasV1FrameworkPackageImports = (configContent) => configContent !== null && Object.keys(FRAMEWORK_PACKAGE_TO_KEY).some((packageName) => configContent.includes(packageName));
974
+ const handleDoctor = async (cwd = process.cwd(), json = false, liteInstall = false, fix = false) => {
975
+ const pnpmWorkspaceRoot = findPnpmWorkspaceRoot(cwd);
976
+ const projectRoot = pnpmWorkspaceRoot ?? cwd;
977
+ const packageManager = detectPackageManager(projectRoot);
978
+ const workspaceRoot = packageManager === "pnpm" && pnpmWorkspaceRoot !== void 0;
979
+ const catalog = workspaceRoot ? getCatalogPreference(readPackageJson(projectRoot)) : false;
980
+ let configPath = getConfigPathIfPresent(projectRoot);
981
+ let packageJson = readPackageJson(projectRoot);
982
+ let declaredDependencies = getDeclaredDependencyNames(packageJson);
983
+ let summary = getInstallProjectSummary(projectRoot);
984
+ let activeConfig = await analyzeEslintConfig(projectRoot);
985
+ let configContent = configPath ? readFileSync(configPath, "utf8") : null;
986
+ let hasV1FrameworkImports = hasV1FrameworkPackageImports(configContent);
554
987
  if (liteInstall) {
988
+ const liteInstallPackages = getLiteInstallPackages(summary, declaredDependencies);
989
+ const liteInstallCommand = createInstallCommand(
990
+ packageManager,
991
+ addCompatibleConfigVersions(
992
+ liteInstallPackages,
993
+ getCompatibleConfigVersion(packageJson, pnpmWorkspaceRoot)
994
+ ),
995
+ workspaceRoot,
996
+ catalog
997
+ );
555
998
  if (json) {
556
999
  console.log(JSON.stringify({ command: liteInstallCommand, packageManager, packages: liteInstallPackages }, null, 2));
557
1000
  } else {
@@ -559,8 +1002,106 @@ const handleDoctor = async (cwd = process.cwd(), json = false, liteInstall = fal
559
1002
  }
560
1003
  return;
561
1004
  }
562
- const { issues, warnings } = buildDoctorDiagnosis(cwd, configPath, configContent, hasV1FrameworkImports, activeConfig, declaredDependencies, summary);
563
- outputDoctorResult(json, configPath, packageManager, summary, liteInstallCommand, issues, warnings);
1005
+ const fixes = fix ? applyDoctorFixes(projectRoot, packageJson, configPath, configContent, summary) : [];
1006
+ if (fixes.length > 0) {
1007
+ configPath = getConfigPathIfPresent(projectRoot);
1008
+ packageJson = readPackageJson(projectRoot);
1009
+ declaredDependencies = getDeclaredDependencyNames(packageJson);
1010
+ summary = getInstallProjectSummary(projectRoot);
1011
+ activeConfig = await analyzeEslintConfig(projectRoot);
1012
+ configContent = configPath ? readFileSync(configPath, "utf8") : null;
1013
+ hasV1FrameworkImports = hasV1FrameworkPackageImports(configContent);
1014
+ }
1015
+ const explicitFeaturePackages = configContent ? getExplicitConfigFeaturePackages(configContent) : [];
1016
+ const requiredPackages = getInstallPackages(summary, declaredDependencies, explicitFeaturePackages);
1017
+ const installCommand = requiredPackages.length > 0 ? createInstallCommand(
1018
+ packageManager,
1019
+ addCompatibleConfigVersions(
1020
+ requiredPackages,
1021
+ getCompatibleConfigVersion(packageJson, pnpmWorkspaceRoot)
1022
+ ),
1023
+ workspaceRoot,
1024
+ catalog
1025
+ ) : void 0;
1026
+ const { issues, warnings } = buildDoctorDiagnosis(
1027
+ projectRoot,
1028
+ configPath,
1029
+ configContent,
1030
+ hasV1FrameworkImports,
1031
+ activeConfig,
1032
+ declaredDependencies,
1033
+ summary
1034
+ );
1035
+ outputDoctorResult(
1036
+ json,
1037
+ configPath,
1038
+ packageManager,
1039
+ summary,
1040
+ installCommand,
1041
+ requiredPackages,
1042
+ issues,
1043
+ warnings,
1044
+ fixes
1045
+ );
1046
+ };
1047
+ const isMinimumReleaseAgeFailure = (output) => /minimum[\s_-]*release[\s_-]*age/i.test(output) || /release (?:is )?(?:too young|not old enough)/i.test(output) || /blocked by (?:the )?minimum age/i.test(output);
1048
+ const handleInstall = (cwd = process.cwd(), dryRun = false, runner = spawnSync) => {
1049
+ const pnpmWorkspaceRoot = findPnpmWorkspaceRoot(cwd);
1050
+ const projectRoot = pnpmWorkspaceRoot ?? cwd;
1051
+ const packageJson = readPackageJson(projectRoot);
1052
+ if (!packageJson) {
1053
+ console.error("\u274C package.json is missing or invalid.");
1054
+ process.exitCode = 1;
1055
+ return;
1056
+ }
1057
+ const packageManager = detectPackageManager(projectRoot);
1058
+ const configPath = getConfigPathIfPresent(projectRoot);
1059
+ const explicitFeaturePackages = configPath ? getExplicitConfigFeaturePackages(readFileSync(configPath, "utf8")) : [];
1060
+ const packages = getInstallPackages(
1061
+ getInstallProjectSummary(projectRoot),
1062
+ getDeclaredDependencyNames(packageJson),
1063
+ explicitFeaturePackages
1064
+ );
1065
+ const workspaceRoot = packageManager === "pnpm" && pnpmWorkspaceRoot !== void 0;
1066
+ const catalog = workspaceRoot ? getCatalogPreference(packageJson) : false;
1067
+ const installPackages = addCompatibleConfigVersions(
1068
+ packages,
1069
+ getCompatibleConfigVersion(packageJson, pnpmWorkspaceRoot)
1070
+ );
1071
+ if (packages.length === 0) {
1072
+ console.log("\u2705 All packages required by the detected ESLint configuration are already declared.");
1073
+ return;
1074
+ }
1075
+ const installCommand = createInstallCommand(packageManager, installPackages, workspaceRoot, catalog);
1076
+ if (dryRun) {
1077
+ console.log(installCommand);
1078
+ return;
1079
+ }
1080
+ console.log(`Installing detected ESLint dependencies:
1081
+ ${installCommand}`);
1082
+ const [command, args] = createInstallInvocation(packageManager, installPackages, workspaceRoot, catalog);
1083
+ const result = runner(command, args, {
1084
+ cwd: projectRoot,
1085
+ encoding: "utf8",
1086
+ maxBuffer: 16 * 1024 * 1024,
1087
+ stdio: "pipe"
1088
+ });
1089
+ const stdout = String(result.stdout ?? "");
1090
+ const stderr = String(result.stderr ?? "");
1091
+ if (stdout) process.stdout.write(stdout);
1092
+ if (stderr) process.stderr.write(stderr);
1093
+ if (result.error) {
1094
+ console.error(`\u274C Failed to run ${packageManager}: ${result.error.message}`);
1095
+ process.exitCode = 1;
1096
+ } else if (result.status !== 0) {
1097
+ if (packageManager === "pnpm" && isMinimumReleaseAgeFailure(`${stdout}
1098
+ ${stderr}`)) {
1099
+ console.error(
1100
+ `\u274C pnpm minimumReleaseAge temporarily blocks the compatible ESLint config release (${installPackages.join(", ")}). Keep the generated compatible range and either wait for the policy window or add the exact release to minimumReleaseAgeExclude in pnpm-workspace.yaml.`
1101
+ );
1102
+ }
1103
+ process.exitCode = result.status ?? 1;
1104
+ }
564
1105
  };
565
1106
  const handleDocs = (cwd = process.cwd()) => {
566
1107
  const outputPath = join(cwd, "ESLINT_STANDARDS.md");
@@ -574,9 +1115,9 @@ const migrateConfigContent = (content) => {
574
1115
  }
575
1116
  const frameworks = getFrameworkKeys(importedFrameworks);
576
1117
  const migrated = [
577
- "import { eslintConfig } from '@santi020k/eslint-config-basic'",
1118
+ "import { defineConfig } from '@santi020k/eslint-config-basic'",
578
1119
  "",
579
- "export default await eslintConfig({",
1120
+ "export default await defineConfig({",
580
1121
  " frameworks: {",
581
1122
  ` ${frameworks.map((key) => `${toPropertyKey(key)}: true`).join(",\n ")}`,
582
1123
  " }",
@@ -609,14 +1150,32 @@ const processConfigMigration = (configPath, write, suggestions) => {
609
1150
  }
610
1151
  return applied;
611
1152
  };
612
- const handleMigrate = (cwd = process.cwd(), write = false, json = false) => {
1153
+ const handleMigrate = (cwd = process.cwd(), write = false, json = false, target = "v2", check = false, full = false) => {
1154
+ if (target === "v3" || target === "3") {
1155
+ const summary = getInstallProjectSummary(cwd);
1156
+ handleMigrateV3(cwd, {
1157
+ declaredConfigPackages: getWorkspaceDeclaredConfigPackages(cwd, summary),
1158
+ extensions: summary.extensions,
1159
+ formats: summary.formats,
1160
+ frameworks: summary.frameworks,
1161
+ libraries: summary.libraries,
1162
+ packageManager: detectPackageManager(cwd),
1163
+ testing: summary.testing,
1164
+ tools: summary.tools,
1165
+ typescript: summary.typescript
1166
+ }, { check, full, json, write });
1167
+ return;
1168
+ }
1169
+ if (target !== "v2" && target !== "2") {
1170
+ throw new Error(`Unsupported migration target "${target}". Use --to v2 or --to v3.`);
1171
+ }
613
1172
  const configPath = getConfigPathIfPresent(cwd) ?? join(cwd, getDefaultConfigFilename(cwd));
614
1173
  const suggestions = [
615
1174
  "v1 to v2 migration suggestions:",
616
1175
  "- Install only @santi020k/eslint-config-basic for the public config API.",
617
1176
  "- Replace framework imports with bundled booleans such as frameworks: { react: true, next: true }.",
618
1177
  "- Remove app-level @santi020k/eslint-config-react/next/vue/etc. config package imports.",
619
- "- Try eslintConfig() first; v3 auto-detects supported frameworks and integrations.",
1178
+ "- Try defineConfig() first; v3 auto-detects supported frameworks and integrations.",
620
1179
  "- Use basic-eslint explain to review what v3 detects before committing the migration."
621
1180
  ];
622
1181
  const applied = processConfigMigration(configPath, write, suggestions);
@@ -634,30 +1193,90 @@ const handleMigrate = (cwd = process.cwd(), write = false, json = false) => {
634
1193
  };
635
1194
  const dispatchCommand = (command, cwd, flags) => {
636
1195
  switch (command) {
1196
+ case "baseline": {
1197
+ try {
1198
+ handleBaseline(cwd, {
1199
+ json: flags.hasJson,
1200
+ preset: flags.preset,
1201
+ prune: flags.hasPrune
1202
+ });
1203
+ } catch (error) {
1204
+ console.error(`\u274C Failed to create ESLint baseline: ${String(error)}`);
1205
+ process.exitCode = 1;
1206
+ }
1207
+ break;
1208
+ }
1209
+ case "compatibility": {
1210
+ handleCompatibility(cwd, flags.hasJson);
1211
+ break;
1212
+ }
1213
+ case "diff": {
1214
+ handleSnapshotDiff(cwd, {
1215
+ files: flags.files,
1216
+ json: flags.hasJson,
1217
+ snapshotPath: flags.snapshotPath
1218
+ }).catch((error) => {
1219
+ console.error(`\u274C Failed to diff ESLint configuration: ${String(error)}`);
1220
+ process.exitCode = 1;
1221
+ });
1222
+ break;
1223
+ }
637
1224
  case "docs": {
638
1225
  handleDocs(cwd);
639
1226
  break;
640
1227
  }
641
1228
  case "doctor": {
642
- handleDoctor(cwd, flags.hasJson, flags.hasLiteInstall).catch((error) => {
1229
+ handleDoctor(cwd, flags.hasJson, flags.hasLiteInstall, flags.hasFix).catch((error) => {
643
1230
  console.error(`\u274C Failed to run doctor: ${String(error)}`);
644
1231
  process.exitCode = 1;
645
1232
  });
646
1233
  break;
647
1234
  }
648
1235
  case "explain": {
649
- handleExplain(cwd, flags.hasJson);
1236
+ if (flags.rule) {
1237
+ handleExplainRule(cwd, {
1238
+ file: flags.files[0],
1239
+ json: flags.hasJson,
1240
+ rule: flags.rule
1241
+ }).catch((error) => {
1242
+ console.error(`\u274C Failed to explain ESLint rule: ${String(error)}`);
1243
+ process.exitCode = 1;
1244
+ });
1245
+ } else {
1246
+ handleExplain(cwd, flags.hasJson);
1247
+ }
1248
+ break;
1249
+ }
1250
+ case "explain-preset": {
1251
+ if (!flags.positional) {
1252
+ console.error("Missing preset. Usage: basic-eslint explain-preset <preset> [options]");
1253
+ process.exitCode = 1;
1254
+ break;
1255
+ }
1256
+ handleExplainPreset(cwd, flags.positional, {
1257
+ compatibility: flags.hasCompatibility,
1258
+ file: flags.files[0],
1259
+ json: flags.hasJson,
1260
+ output: flags.output
1261
+ }).catch((error) => {
1262
+ console.error(`\u274C Failed to explain preset: ${String(error)}`);
1263
+ process.exitCode = 1;
1264
+ });
650
1265
  break;
651
1266
  }
652
1267
  case "generate-skill": {
653
- handleGenerateSkill(cwd, flags.hasForce, { check: flags.hasCheck, createAgentsMd: flags.hasCreate }).catch((error) => {
1268
+ handleGenerateSkill(cwd, flags.hasForce, {
1269
+ check: flags.hasCheck,
1270
+ createAgentsMd: flags.hasCreate,
1271
+ withEslintMcp: flags.hasWithEslintMcp
1272
+ }).catch((error) => {
654
1273
  console.error(`\u274C Failed to generate skill files: ${String(error)}`);
655
1274
  process.exitCode = 1;
656
1275
  });
657
1276
  break;
658
1277
  }
659
1278
  case "init": {
660
- handleInit(cwd, flags.hasCheck);
1279
+ handleInit(cwd, flags.hasCheck, flags.hasExplicit);
661
1280
  break;
662
1281
  }
663
1282
  case "inspect": {
@@ -667,8 +1286,52 @@ const dispatchCommand = (command, cwd, flags) => {
667
1286
  });
668
1287
  break;
669
1288
  }
1289
+ case "install": {
1290
+ handleInstall(cwd, flags.hasDryRun);
1291
+ break;
1292
+ }
670
1293
  case "migrate": {
671
- handleMigrate(cwd, flags.hasWrite, flags.hasJson);
1294
+ try {
1295
+ handleMigrate(
1296
+ cwd,
1297
+ flags.hasWrite,
1298
+ flags.hasJson,
1299
+ flags.target ?? "v2",
1300
+ flags.hasCheck,
1301
+ flags.hasFull
1302
+ );
1303
+ } catch (error) {
1304
+ console.error(`\u274C Failed to migrate ESLint configuration: ${String(error)}`);
1305
+ process.exitCode = 1;
1306
+ }
1307
+ break;
1308
+ }
1309
+ case "profile": {
1310
+ try {
1311
+ handleProfile(cwd, {
1312
+ concurrency: flags.concurrency,
1313
+ files: flags.files,
1314
+ json: flags.hasJson,
1315
+ maxDurationMs: flags.maxDurationMs,
1316
+ maxRuleTimeMs: flags.maxRuleTimeMs,
1317
+ maxWarnings: flags.maxWarnings
1318
+ });
1319
+ } catch (error) {
1320
+ console.error(`\u274C Failed to profile ESLint: ${String(error)}`);
1321
+ process.exitCode = 1;
1322
+ }
1323
+ break;
1324
+ }
1325
+ case "snapshot": {
1326
+ handleSnapshot(cwd, {
1327
+ check: flags.hasCheck,
1328
+ files: flags.files,
1329
+ json: flags.hasJson,
1330
+ snapshotPath: flags.snapshotPath
1331
+ }).catch((error) => {
1332
+ console.error(`\u274C Failed to snapshot ESLint configuration: ${String(error)}`);
1333
+ process.exitCode = 1;
1334
+ });
672
1335
  break;
673
1336
  }
674
1337
  case "update": {
@@ -682,11 +1345,29 @@ const dispatchCommand = (command, cwd, flags) => {
682
1345
  }
683
1346
  }
684
1347
  };
1348
+ const getFlagValue = (argv, flag) => {
1349
+ const inline = argv.find((argument) => argument.startsWith(`${flag}=`));
1350
+ if (inline) return inline.slice(flag.length + 1);
1351
+ const index = argv.indexOf(flag);
1352
+ return index >= 0 ? argv[index + 1] : void 0;
1353
+ };
1354
+ const getFlagValues = (argv, flag) => argv.flatMap((argument, index) => {
1355
+ if (argument.startsWith(`${flag}=`)) return [argument.slice(flag.length + 1)];
1356
+ if (argument === flag && argv[index + 1]) return [argv[index + 1]];
1357
+ return [];
1358
+ });
1359
+ const getNumericFlagValue = (argv, flag) => {
1360
+ const value = getFlagValue(argv, flag);
1361
+ const present = argv.includes(flag) || argv.some((argument) => argument.startsWith(`${flag}=`));
1362
+ if (!present) return void 0;
1363
+ if (value === void 0 || value.trim() === "" || value.startsWith("--")) return Number.NaN;
1364
+ return Number(value);
1365
+ };
685
1366
  const runCli = (argv = process.argv, cwd = process.cwd()) => {
686
1367
  const command = argv[2];
687
- const isHelp = command === "--help" || command === "-h";
1368
+ const isHelp = argv.slice(3).some((argument) => argument === "--help" || argument === "-h");
688
1369
  const isVersion = command === "--version" || command === "-v";
689
- if (!command || isHelp) {
1370
+ if (!command || command === "--help" || command === "-h") {
690
1371
  printUsage();
691
1372
  return;
692
1373
  }
@@ -694,13 +1375,38 @@ const runCli = (argv = process.argv, cwd = process.cwd()) => {
694
1375
  console.log(getCliVersion());
695
1376
  return;
696
1377
  }
1378
+ if (isHelp) {
1379
+ if (COMMAND_OPTIONS[command]) printCommandUsage(command);
1380
+ else printUsage();
1381
+ return;
1382
+ }
1383
+ const validation = validateCommandArguments(command, argv.slice(3));
1384
+ if (!validation.ok) return;
697
1385
  dispatchCommand(command, cwd, {
1386
+ concurrency: getFlagValue(argv, "--concurrency"),
1387
+ files: getFlagValues(argv, "--file"),
698
1388
  hasCheck: argv.includes("--check"),
1389
+ hasCompatibility: argv.includes("--compatibility"),
699
1390
  hasCreate: argv.includes("--create"),
1391
+ hasDryRun: argv.includes("--dry-run"),
1392
+ hasExplicit: argv.includes("--explicit"),
1393
+ hasFix: argv.includes("--fix"),
700
1394
  hasForce: argv.includes("--force"),
1395
+ hasFull: argv.includes("--full"),
701
1396
  hasJson: argv.includes("--json"),
702
1397
  hasLiteInstall: argv.includes("--lite-install"),
703
- hasWrite: argv.includes("--write")
1398
+ hasPrune: argv.includes("--prune"),
1399
+ hasWithEslintMcp: argv.includes("--with-eslint-mcp"),
1400
+ hasWrite: argv.includes("--write"),
1401
+ maxDurationMs: getNumericFlagValue(argv, "--max-duration"),
1402
+ maxRuleTimeMs: getNumericFlagValue(argv, "--max-rule-time"),
1403
+ maxWarnings: getNumericFlagValue(argv, "--max-warnings"),
1404
+ output: getFlagValue(argv, "--output"),
1405
+ positional: validation.positional,
1406
+ preset: getFlagValue(argv, "--preset"),
1407
+ rule: command === "explain" ? validation.positional : void 0,
1408
+ snapshotPath: getFlagValue(argv, "--snapshot-path"),
1409
+ target: getFlagValue(argv, "--to")
704
1410
  });
705
1411
  };
706
1412
  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 +1419,7 @@ export {
713
1419
  handleExplain,
714
1420
  handleInit,
715
1421
  handleInspect,
1422
+ handleInstall,
716
1423
  handleMigrate,
717
1424
  handleUpdate,
718
1425
  runCli