@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/CHANGELOG.md +83 -0
- package/dist/agent-skill-generator.d.ts +11 -2
- package/dist/agent-skill-generator.js +59 -8
- package/dist/cli-advanced.d.ts +23 -0
- package/dist/cli-advanced.js +217 -0
- package/dist/cli-migration.d.ts +26 -0
- package/dist/cli-migration.js +782 -0
- package/dist/cli-workflows.d.ts +63 -0
- package/dist/cli-workflows.js +444 -0
- package/dist/cli.d.ts +5 -4
- package/dist/cli.js +539 -88
- package/dist/frameworks.d.ts +1 -25
- package/dist/frameworks.js +2 -23
- package/dist/index.d.ts +4 -10
- package/dist/index.js +160 -262
- package/dist/integrations.d.ts +6 -1
- package/dist/integrations.js +44 -1
- package/dist/recommended.d.ts +5 -0
- package/dist/recommended.js +5 -0
- package/dist/resolvers.js +1 -6
- package/dist/tailwind.d.ts +6 -0
- package/dist/tailwind.js +25 -0
- package/package.json +137 -31
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,88 @@
|
|
|
1
1
|
# @santi020k/eslint-config-basic
|
|
2
2
|
|
|
3
|
+
## 3.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#99](https://github.com/santi020k/eslint-config-basic/pull/99) [`0f89b89`](https://github.com/santi020k/eslint-config-basic/commit/0f89b89b466732ea582650b4e0baee0c47b0c967) Thanks [@santi020k](https://github.com/santi020k)! - Add an opt-in, lazily loaded Astro Doctor ESLint integration through `Extension.AstroDoctor` or `features: { "astro-doctor": true }`, including the recommended proprietary and official Astro rules, configuration validation, and `basic-eslint doctor` compatibility guidance.
|
|
8
|
+
|
|
9
|
+
- [#99](https://github.com/santi020k/eslint-config-basic/pull/99) [`0f89b89`](https://github.com/santi020k/eslint-config-basic/commit/0f89b89b466732ea582650b4e0baee0c47b0c967) Thanks [@santi020k](https://github.com/santi020k)! - Add a `basic-eslint install` command that detects missing framework, feature
|
|
10
|
+
category, ESLint, and TypeScript dependencies and installs them with the
|
|
11
|
+
project's package manager. Document the command in the v3 setup flow.
|
|
12
|
+
|
|
13
|
+
- [#99](https://github.com/santi020k/eslint-config-basic/pull/99) [`0f89b89`](https://github.com/santi020k/eslint-config-basic/commit/0f89b89b466732ea582650b4e0baee0c47b0c967) Thanks [@santi020k](https://github.com/santi020k)! - Complete the v3 lean dependency boundary and one-line recommended entry point. Framework and feature-pack packages are now optional, dynamically loaded peers with actionable installation errors. Feature factories move from the basic root export to granular extensions, formats, libraries, testing, and tools packages; `@santi020k/eslint-config-integrations` remains as a compatibility aggregate, while the new `@santi020k/eslint-config-full` package provides the batteries-included installation path. Core no longer owns JSX accessibility rules, the CLI writes the one-line configuration, and release checks enforce dependency budgets, packed-consumer compatibility, and lean production-audit boundaries.
|
|
14
|
+
|
|
15
|
+
Remove the v1 compatibility exports (`*Config`, `jsConfig`, `tsConfig`,
|
|
16
|
+
`astroConfig`, `rules`, `loadModule`, and `eslintConfig`) from the aggregate
|
|
17
|
+
v3 API. Remove the deprecated Remix package and `frameworks.remix` option;
|
|
18
|
+
Remix dependencies now resolve through the React Router v7 configuration.
|
|
19
|
+
The Lite package is now a thin compatibility re-export of Basic.
|
|
20
|
+
|
|
21
|
+
- [#99](https://github.com/santi020k/eslint-config-basic/pull/99) [`0f89b89`](https://github.com/santi020k/eslint-config-basic/commit/0f89b89b466732ea582650b4e0baee0c47b0c967) Thanks [@santi020k](https://github.com/santi020k)! - Complete the v3 modular dependency boundary with five category feature packs.
|
|
22
|
+
Each pack owns its plugin dependencies and exposes a self-describing feature
|
|
23
|
+
registry through the shared `ConfigFeature` contract. The lean Basic composer
|
|
24
|
+
loads only registries for selected categories, Full installs every pack, and
|
|
25
|
+
Integrations remains as a compatibility aggregate.
|
|
26
|
+
|
|
27
|
+
Move agent-skill generation from the Basic root export to
|
|
28
|
+
`@santi020k/eslint-config-basic/agent`, keeping Node-oriented tooling out of the
|
|
29
|
+
configuration runtime entry point.
|
|
30
|
+
|
|
31
|
+
- [#99](https://github.com/santi020k/eslint-config-basic/pull/99) [`0f89b89`](https://github.com/santi020k/eslint-config-basic/commit/0f89b89b466732ea582650b4e0baee0c47b0c967) Thanks [@santi020k](https://github.com/santi020k)! - Polish real-world consumer configuration: add one `root` option for detection,
|
|
32
|
+
TypeScript, Tailwind, projects, and `.gitignore`; auto-detect and scope workspace
|
|
33
|
+
packages without leaking root framework dependencies; accept native project
|
|
34
|
+
service options; and syntax-lint TypeScript config files plus explicit
|
|
35
|
+
`untypedFiles` without consumer-side `typescript-eslint` imports.
|
|
36
|
+
|
|
37
|
+
- [#99](https://github.com/santi020k/eslint-config-basic/pull/99) [`0f89b89`](https://github.com/santi020k/eslint-config-basic/commit/0f89b89b466732ea582650b4e0baee0c47b0c967) Thanks [@santi020k](https://github.com/santi020k)! - Drop Node.js 20 support and require Node.js 22.19.0 or newer across the package
|
|
38
|
+
suite. This aligns the published engine contract with the current ESLint,
|
|
39
|
+
Lighthouse, GraphQL, and Unicorn toolchain.
|
|
40
|
+
|
|
41
|
+
- [#99](https://github.com/santi020k/eslint-config-basic/pull/99) [`0f89b89`](https://github.com/santi020k/eslint-config-basic/commit/0f89b89b466732ea582650b4e0baee0c47b0c967) Thanks [@santi020k](https://github.com/santi020k)! - Add v3 lifecycle tooling to the `basic-eslint` CLI: an automated v2-to-v3
|
|
42
|
+
migration planner and writer, incremental strict-mode baselines backed by ESLint
|
|
43
|
+
bulk suppressions, rule-performance profiling with concurrency comparison,
|
|
44
|
+
effective-rule snapshots and diffs, and optional official ESLint MCP
|
|
45
|
+
scaffolding for generated agent standards.
|
|
46
|
+
|
|
47
|
+
- [#99](https://github.com/santi020k/eslint-config-basic/pull/99) [`0f89b89`](https://github.com/santi020k/eslint-config-basic/commit/0f89b89b466732ea582650b4e0baee0c47b0c967) Thanks [@santi020k](https://github.com/santi020k)! - Add v3 project-assistance workflows: safe doctor repairs, explicit init output,
|
|
48
|
+
effective rule/source explanations, package compatibility reporting, lint
|
|
49
|
+
warning and performance budgets, real v2 migration fixtures, and a reusable
|
|
50
|
+
GitHub Action for consumer checks.
|
|
51
|
+
|
|
52
|
+
### Patch Changes
|
|
53
|
+
|
|
54
|
+
- [#99](https://github.com/santi020k/eslint-config-basic/pull/99) [`0f89b89`](https://github.com/santi020k/eslint-config-basic/commit/0f89b89b466732ea582650b4e0baee0c47b0c967) Thanks [@santi020k](https://github.com/santi020k)! - Resolve package-scoped Tailwind and Astro configuration roots, keep lazy framework imports anchored to their declaring package, add shared `projectDefaults` inheritance for monorepos, and move integration host libraries to optional peers.
|
|
55
|
+
|
|
56
|
+
- [#99](https://github.com/santi020k/eslint-config-basic/pull/99) [`0f89b89`](https://github.com/santi020k/eslint-config-basic/commit/0f89b89b466732ea582650b4e0baee0c47b0c967) Thanks [@santi020k](https://github.com/santi020k)! - Deprecate the Lite and Integrations compatibility packages during v3 and
|
|
57
|
+
schedule their removal for v4. Add TypeScript deprecation annotations,
|
|
58
|
+
migration guidance, and post-publish npm registry warnings. Also deprecate the
|
|
59
|
+
`integrations` option alias and `doctor --lite-install` compatibility workflow.
|
|
60
|
+
|
|
61
|
+
- [#99](https://github.com/santi020k/eslint-config-basic/pull/99) [`0f89b89`](https://github.com/santi020k/eslint-config-basic/commit/0f89b89b466732ea582650b4e0baee0c47b0c967) Thanks [@santi020k](https://github.com/santi020k)! - Include explicit feature, preset, and strict config selections in CLI install
|
|
62
|
+
and doctor repair planning, preserve runtime dependency fields and peer metadata
|
|
63
|
+
during v3 migrations, analyze TypeScript ESLint config files during doctor
|
|
64
|
+
checks, and honor dot-prefixed workspace exclusion patterns during project
|
|
65
|
+
detection.
|
|
66
|
+
- Updated dependencies [[`0f89b89`](https://github.com/santi020k/eslint-config-basic/commit/0f89b89b466732ea582650b4e0baee0c47b0c967), [`0f89b89`](https://github.com/santi020k/eslint-config-basic/commit/0f89b89b466732ea582650b4e0baee0c47b0c967), [`0f89b89`](https://github.com/santi020k/eslint-config-basic/commit/0f89b89b466732ea582650b4e0baee0c47b0c967), [`0f89b89`](https://github.com/santi020k/eslint-config-basic/commit/0f89b89b466732ea582650b4e0baee0c47b0c967), [`0f89b89`](https://github.com/santi020k/eslint-config-basic/commit/0f89b89b466732ea582650b4e0baee0c47b0c967), [`0f89b89`](https://github.com/santi020k/eslint-config-basic/commit/0f89b89b466732ea582650b4e0baee0c47b0c967), [`0f89b89`](https://github.com/santi020k/eslint-config-basic/commit/0f89b89b466732ea582650b4e0baee0c47b0c967)]:
|
|
67
|
+
- @santi020k/eslint-config-core@3.1.0
|
|
68
|
+
- @santi020k/eslint-config-typescript@3.1.0
|
|
69
|
+
|
|
70
|
+
## 3.0.0
|
|
71
|
+
|
|
72
|
+
### Major Changes
|
|
73
|
+
|
|
74
|
+
- Make the lean dependency model the default. Framework and integration config
|
|
75
|
+
packages are optional peers instead of hard dependencies.
|
|
76
|
+
- Add `@santi020k/eslint-config-basic/recommended` for a one-line zero-config
|
|
77
|
+
`eslint.config.*` file.
|
|
78
|
+
- Move integration factory exports to
|
|
79
|
+
`@santi020k/eslint-config-integrations`; the new
|
|
80
|
+
`@santi020k/eslint-config-full` package re-exports the batteries-included API.
|
|
81
|
+
- Make `Preset.Basic` dependency-free beyond core and TypeScript support.
|
|
82
|
+
- Make `basic-eslint init` generate the one-line recommended config.
|
|
83
|
+
|
|
84
|
+
See the [v2 to v3 migration guide](https://eslint.santi020k.com/guide/migration-v2-to-v3/).
|
|
85
|
+
|
|
3
86
|
## 2.1.1
|
|
4
87
|
|
|
5
88
|
### Patch Changes
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
declare const ESLINT_CONFIG_FILENAMES: readonly ["eslint.config.js", "eslint.config.mjs", "eslint.config.cjs", "eslint.config.ts", "eslint.config.mts", "eslint.config.cts"];
|
|
1
2
|
interface AgentTarget {
|
|
2
3
|
/** Format variant used when generating content */
|
|
3
4
|
format: 'cursor' | 'frontmatter' | 'kiro' | 'plain';
|
|
@@ -50,6 +51,12 @@ interface GenerateSkillOptions {
|
|
|
50
51
|
* @default false
|
|
51
52
|
*/
|
|
52
53
|
force?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Add the official `eslint --mcp` server to the project root `.mcp.json`.
|
|
56
|
+
* Existing unrelated MCP server entries are preserved.
|
|
57
|
+
* @default false
|
|
58
|
+
*/
|
|
59
|
+
withEslintMcp?: boolean;
|
|
53
60
|
}
|
|
54
61
|
interface GenerateSkillResult {
|
|
55
62
|
skipped: string[];
|
|
@@ -90,7 +97,7 @@ declare const generateSkillContent: (features: EslintConfigFeatures, format: Age
|
|
|
90
97
|
*
|
|
91
98
|
* @example
|
|
92
99
|
* ```ts
|
|
93
|
-
* import { generateAgentSkills } from '@santi020k/eslint-config-basic'
|
|
100
|
+
* import { generateAgentSkills } from '@santi020k/eslint-config-basic/agent'
|
|
94
101
|
*
|
|
95
102
|
* const result = await generateAgentSkills({ cwd: process.cwd() })
|
|
96
103
|
* process.stdout.write(`Written to: ${result.written}\n`)
|
|
@@ -102,7 +109,9 @@ interface HandleGenerateSkillFlags {
|
|
|
102
109
|
check?: boolean;
|
|
103
110
|
/** Create a root `AGENTS.md` when it does not exist */
|
|
104
111
|
createAgentsMd?: boolean;
|
|
112
|
+
/** Add the official ESLint MCP server to `.mcp.json` */
|
|
113
|
+
withEslintMcp?: boolean;
|
|
105
114
|
}
|
|
106
115
|
declare const handleGenerateSkill: (cwd?: string, force?: boolean, flags?: HandleGenerateSkillFlags) => Promise<void>;
|
|
107
116
|
|
|
108
|
-
export { AGENT_TARGETS, type AgentTarget, type EslintConfigFeatures, type GenerateSkillOptions, type GenerateSkillResult, type HandleGenerateSkillFlags, analyzeEslintConfig, generateAgentSkills, generateSkillContent, handleGenerateSkill };
|
|
117
|
+
export { AGENT_TARGETS, type AgentTarget, ESLINT_CONFIG_FILENAMES, type EslintConfigFeatures, type GenerateSkillOptions, type GenerateSkillResult, type HandleGenerateSkillFlags, analyzeEslintConfig, generateAgentSkills, generateSkillContent, handleGenerateSkill };
|
|
@@ -2,6 +2,14 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
import { detectProjectOptions } from "./index.js";
|
|
5
|
+
const ESLINT_CONFIG_FILENAMES = [
|
|
6
|
+
"eslint.config.js",
|
|
7
|
+
"eslint.config.mjs",
|
|
8
|
+
"eslint.config.cjs",
|
|
9
|
+
"eslint.config.ts",
|
|
10
|
+
"eslint.config.mts",
|
|
11
|
+
"eslint.config.cts"
|
|
12
|
+
];
|
|
5
13
|
const AGENT_TARGETS = [
|
|
6
14
|
{
|
|
7
15
|
format: "frontmatter",
|
|
@@ -97,7 +105,6 @@ const FEATURE_MAP = [
|
|
|
97
105
|
["eslint-config-expo/", "frameworks", "Expo"],
|
|
98
106
|
["eslint-config-qwik/", "frameworks", "Qwik"],
|
|
99
107
|
["eslint-config-react-router/", "frameworks", "React Router"],
|
|
100
|
-
["eslint-config-remix/", "frameworks", "Remix"],
|
|
101
108
|
["eslint-config-solid/", "frameworks", "SolidJS"],
|
|
102
109
|
["eslint-config-nuxt/", "frameworks", "Nuxt"],
|
|
103
110
|
["eslint-config-lit/", "frameworks", "Lit"],
|
|
@@ -105,6 +112,9 @@ const FEATURE_MAP = [
|
|
|
105
112
|
["eslint-config-preact/", "frameworks", "Preact"],
|
|
106
113
|
["eslint-config-slidev/", "frameworks", "Slidev"],
|
|
107
114
|
["eslint-config-vite/", "frameworks", "Vite"],
|
|
115
|
+
// Extensions
|
|
116
|
+
["eslint-config-integrations/astro-doctor", "extensions", "Astro Doctor"],
|
|
117
|
+
["astro-doctor/", "extensions", "Astro Doctor"],
|
|
108
118
|
// Testing
|
|
109
119
|
["integrations/vitest", "testing", "Vitest"],
|
|
110
120
|
["integrations/jest", "testing", "Jest"],
|
|
@@ -184,7 +194,6 @@ const DETECTED_FRAMEWORK_LABELS = /* @__PURE__ */ new Map([
|
|
|
184
194
|
["qwik", "Qwik"],
|
|
185
195
|
["react", "React"],
|
|
186
196
|
["react-router", "React Router"],
|
|
187
|
-
["remix", "Remix"],
|
|
188
197
|
["slidev", "Slidev"],
|
|
189
198
|
["solid", "SolidJS"],
|
|
190
199
|
["svelte", "Svelte"],
|
|
@@ -299,7 +308,7 @@ const extractFeatures = (configs, lintCommand, configFile) => {
|
|
|
299
308
|
return features;
|
|
300
309
|
};
|
|
301
310
|
const findEslintConfig = (cwd) => {
|
|
302
|
-
for (const name of
|
|
311
|
+
for (const name of ESLINT_CONFIG_FILENAMES) {
|
|
303
312
|
const p = join(cwd, name);
|
|
304
313
|
if (existsSync(p)) return p;
|
|
305
314
|
}
|
|
@@ -591,10 +600,46 @@ const resolveSkillOptions = (opts) => ({
|
|
|
591
600
|
check: opts.check ?? false,
|
|
592
601
|
createAgentsMd: opts.createAgentsMd ?? false,
|
|
593
602
|
cwd: opts.cwd ?? process.cwd(),
|
|
594
|
-
force: opts.force ?? false
|
|
603
|
+
force: opts.force ?? false,
|
|
604
|
+
withEslintMcp: opts.withEslintMcp ?? false
|
|
595
605
|
});
|
|
606
|
+
const createEslintMcpServer = (cwd) => {
|
|
607
|
+
if (existsSync(join(cwd, "pnpm-lock.yaml")) || existsSync(join(cwd, "pnpm-workspace.yaml"))) {
|
|
608
|
+
return { args: ["exec", "eslint", "--mcp"], command: "pnpm" };
|
|
609
|
+
}
|
|
610
|
+
if (existsSync(join(cwd, "yarn.lock"))) {
|
|
611
|
+
return { args: ["exec", "eslint", "--mcp"], command: "yarn" };
|
|
612
|
+
}
|
|
613
|
+
if (existsSync(join(cwd, "bun.lockb")) || existsSync(join(cwd, "bun.lock"))) {
|
|
614
|
+
return { args: ["eslint", "--mcp"], command: "bunx" };
|
|
615
|
+
}
|
|
616
|
+
return { args: ["eslint", "--mcp"], command: "npx" };
|
|
617
|
+
};
|
|
618
|
+
const handleEslintMcpConfig = (cwd, force, check) => {
|
|
619
|
+
const configPath = join(cwd, ".mcp.json");
|
|
620
|
+
const server = createEslintMcpServer(cwd);
|
|
621
|
+
let config = {};
|
|
622
|
+
if (existsSync(configPath)) {
|
|
623
|
+
try {
|
|
624
|
+
config = JSON.parse(readFileSync(configPath, "utf8"));
|
|
625
|
+
} catch {
|
|
626
|
+
throw new Error(".mcp.json is invalid JSON; fix it before adding the ESLint MCP server.");
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
const current = config.mcpServers?.eslint;
|
|
630
|
+
const isCurrent = JSON.stringify(current) === JSON.stringify(server);
|
|
631
|
+
if (check) return isCurrent ? "skipped" : "stale";
|
|
632
|
+
if (current && !force) return "skipped";
|
|
633
|
+
config.mcpServers = {
|
|
634
|
+
...config.mcpServers,
|
|
635
|
+
eslint: server
|
|
636
|
+
};
|
|
637
|
+
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
638
|
+
`);
|
|
639
|
+
return "written";
|
|
640
|
+
};
|
|
596
641
|
const generateAgentSkills = async (opts = {}) => {
|
|
597
|
-
const { check, createAgentsMd, cwd, force } = resolveSkillOptions(opts);
|
|
642
|
+
const { check, createAgentsMd, cwd, force, withEslintMcp } = resolveSkillOptions(opts);
|
|
598
643
|
const written = [];
|
|
599
644
|
const skipped = [];
|
|
600
645
|
const stale = [];
|
|
@@ -617,6 +662,10 @@ const generateAgentSkills = async (opts = {}) => {
|
|
|
617
662
|
recordGuardedResult(copilotPath, handleGuardedSectionFile(copilotPath, plainBody, force, check));
|
|
618
663
|
const agentsMdPath = join(cwd, AGENTS_MD_PATH);
|
|
619
664
|
recordGuardedResult(agentsMdPath, handleGuardedSectionFile(agentsMdPath, plainBody, force, check, createAgentsMd));
|
|
665
|
+
if (withEslintMcp) {
|
|
666
|
+
const eslintMcpPath = join(cwd, ".mcp.json");
|
|
667
|
+
recordGuardedResult(eslintMcpPath, handleEslintMcpConfig(cwd, force, check));
|
|
668
|
+
}
|
|
620
669
|
for (const target of AGENT_TARGETS) {
|
|
621
670
|
const agentFolder = join(cwd, target.markerFolder);
|
|
622
671
|
if (!existsSync(agentFolder)) continue;
|
|
@@ -637,7 +686,8 @@ const generateAgentSkills = async (opts = {}) => {
|
|
|
637
686
|
};
|
|
638
687
|
const resolveGenerateSkillFlags = (flags) => ({
|
|
639
688
|
check: flags.check ?? false,
|
|
640
|
-
createAgentsMd: flags.createAgentsMd ?? false
|
|
689
|
+
createAgentsMd: flags.createAgentsMd ?? false,
|
|
690
|
+
withEslintMcp: flags.withEslintMcp ?? false
|
|
641
691
|
});
|
|
642
692
|
const outputGenerateSkillResults = (check, result, cwd) => {
|
|
643
693
|
if (check) {
|
|
@@ -672,7 +722,7 @@ const outputGenerateSkillResults = (check, result, cwd) => {
|
|
|
672
722
|
}
|
|
673
723
|
};
|
|
674
724
|
const handleGenerateSkill = async (cwd = process.cwd(), force = false, flags = {}) => {
|
|
675
|
-
const { check, createAgentsMd } = resolveGenerateSkillFlags(flags);
|
|
725
|
+
const { check, createAgentsMd, withEslintMcp } = resolveGenerateSkillFlags(flags);
|
|
676
726
|
console.log(check ? "\u{1F50D} Checking AI agent skill files..." : "\u{1F50D} Scanning for AI agent folders...");
|
|
677
727
|
const configFile = findEslintConfig(cwd);
|
|
678
728
|
if (configFile) {
|
|
@@ -680,7 +730,7 @@ const handleGenerateSkill = async (cwd = process.cwd(), force = false, flags = {
|
|
|
680
730
|
} else {
|
|
681
731
|
console.log("\u26A0\uFE0F No eslint.config.js found \u2014 falling back to package.json detection.");
|
|
682
732
|
}
|
|
683
|
-
const result = await generateAgentSkills({ check, createAgentsMd, cwd, force });
|
|
733
|
+
const result = await generateAgentSkills({ check, createAgentsMd, cwd, force, withEslintMcp });
|
|
684
734
|
if (result.written.length === 0 && result.skipped.length === 0 && result.stale.length === 0) {
|
|
685
735
|
console.log("\n\u26A0\uFE0F No agent folders found (.agent, .agents, .claude, .cursor, .windsurf, .copilot, .aider, .gemini, .clinerules, .roo, .kiro) and no AGENTS.md.");
|
|
686
736
|
console.log(" Create one of those folders first (or re-run with --create to scaffold AGENTS.md).");
|
|
@@ -690,6 +740,7 @@ const handleGenerateSkill = async (cwd = process.cwd(), force = false, flags = {
|
|
|
690
740
|
};
|
|
691
741
|
export {
|
|
692
742
|
AGENT_TARGETS,
|
|
743
|
+
ESLINT_CONFIG_FILENAMES,
|
|
693
744
|
analyzeEslintConfig,
|
|
694
745
|
generateAgentSkills,
|
|
695
746
|
generateSkillContent,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
interface ExplainRuleOptions {
|
|
2
|
+
file?: string;
|
|
3
|
+
json?: boolean;
|
|
4
|
+
rule: string;
|
|
5
|
+
}
|
|
6
|
+
declare const handleExplainRule: (cwd: string | undefined, options: ExplainRuleOptions) => Promise<void>;
|
|
7
|
+
declare const createCompatibilityReport: (cwd?: string) => {
|
|
8
|
+
compatible: boolean;
|
|
9
|
+
packages: {
|
|
10
|
+
declared: string;
|
|
11
|
+
issues: string[];
|
|
12
|
+
name: string;
|
|
13
|
+
resolved: string | null;
|
|
14
|
+
}[];
|
|
15
|
+
runtime: {
|
|
16
|
+
eslint: string | null;
|
|
17
|
+
node: string;
|
|
18
|
+
typescript: string | null;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
declare const handleCompatibility: (cwd?: string, json?: boolean) => void;
|
|
22
|
+
|
|
23
|
+
export { type ExplainRuleOptions, createCompatibilityReport, handleCompatibility, handleExplainRule };
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
const CONFIG_FILENAMES = [
|
|
6
|
+
"eslint.config.js",
|
|
7
|
+
"eslint.config.mjs",
|
|
8
|
+
"eslint.config.cjs",
|
|
9
|
+
"eslint.config.ts",
|
|
10
|
+
"eslint.config.mts",
|
|
11
|
+
"eslint.config.cts"
|
|
12
|
+
];
|
|
13
|
+
const RULE_PACKAGE_HINTS = {
|
|
14
|
+
"@angular-eslint": "@santi020k/eslint-config-angular",
|
|
15
|
+
"@eslint-react": "@santi020k/eslint-config-react",
|
|
16
|
+
"@next/next": "@santi020k/eslint-config-next",
|
|
17
|
+
"@typescript-eslint": "@santi020k/eslint-config-typescript",
|
|
18
|
+
astro: "@santi020k/eslint-config-astro",
|
|
19
|
+
cypress: "@santi020k/eslint-config-testing",
|
|
20
|
+
jest: "@santi020k/eslint-config-testing",
|
|
21
|
+
"jsonc": "@santi020k/eslint-config-formats",
|
|
22
|
+
playwright: "@santi020k/eslint-config-testing",
|
|
23
|
+
react: "@santi020k/eslint-config-react",
|
|
24
|
+
"testing-library": "@santi020k/eslint-config-testing",
|
|
25
|
+
unicorn: "@santi020k/eslint-config-extensions",
|
|
26
|
+
vue: "@santi020k/eslint-config-vue"
|
|
27
|
+
};
|
|
28
|
+
const semver = createRequire(import.meta.url)("semver");
|
|
29
|
+
const readJson = (filePath) => {
|
|
30
|
+
if (!existsSync(filePath)) return null;
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const findConfigPath = (cwd) => CONFIG_FILENAMES.map((file) => join(cwd, file)).find((file) => existsSync(file)) ?? null;
|
|
38
|
+
const flattenConfig = async (value) => {
|
|
39
|
+
const resolved = await value;
|
|
40
|
+
if (Array.isArray(resolved)) {
|
|
41
|
+
const nested = await Promise.all(resolved.map((entry) => flattenConfig(entry)));
|
|
42
|
+
return nested.flat();
|
|
43
|
+
}
|
|
44
|
+
if (resolved && typeof resolved === "object") return [resolved];
|
|
45
|
+
return [];
|
|
46
|
+
};
|
|
47
|
+
const loadConfigEntries = async (configPath) => {
|
|
48
|
+
if (!configPath) return [];
|
|
49
|
+
try {
|
|
50
|
+
const loaded = await import(`${pathToFileURL(configPath).href}?basic-eslint-explain=${Date.now()}`);
|
|
51
|
+
if (!loaded || typeof loaded !== "object" || !("default" in loaded)) return [];
|
|
52
|
+
return await flattenConfig(loaded.default);
|
|
53
|
+
} catch {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const loadProjectEslint = (cwd) => {
|
|
58
|
+
const projectRequire = createRequire(join(cwd, "package.json"));
|
|
59
|
+
const eslintModule = projectRequire("eslint");
|
|
60
|
+
if (!eslintModule.ESLint) throw new Error("The installed eslint package does not expose the ESLint API.");
|
|
61
|
+
return new eslintModule.ESLint({ cwd });
|
|
62
|
+
};
|
|
63
|
+
const getRuleSeverity = (ruleValue) => Array.isArray(ruleValue) ? ruleValue[0] : ruleValue;
|
|
64
|
+
const isRuleEnabled = (ruleValue) => {
|
|
65
|
+
const severity = getRuleSeverity(ruleValue);
|
|
66
|
+
return severity !== 0 && severity !== "off";
|
|
67
|
+
};
|
|
68
|
+
const getPackageHint = (rule) => {
|
|
69
|
+
const prefix = rule.includes("/") ? rule.slice(0, rule.indexOf("/")) : rule;
|
|
70
|
+
const scopedPrefix = rule.startsWith("@") ? rule.split("/").slice(0, 2).join("/") : prefix;
|
|
71
|
+
return RULE_PACKAGE_HINTS[scopedPrefix] ?? RULE_PACKAGE_HINTS[prefix] ?? null;
|
|
72
|
+
};
|
|
73
|
+
const handleExplainRule = async (cwd = process.cwd(), options) => {
|
|
74
|
+
const configPath = findConfigPath(cwd);
|
|
75
|
+
const file = options.file ?? "eslint.config.js";
|
|
76
|
+
const eslint = loadProjectEslint(cwd);
|
|
77
|
+
const effective = await eslint.calculateConfigForFile(file);
|
|
78
|
+
const effectiveValue = effective?.rules?.[options.rule];
|
|
79
|
+
const entries = await loadConfigEntries(configPath);
|
|
80
|
+
const sources = entries.flatMap((entry, index) => {
|
|
81
|
+
if (!entry.rules || !Object.hasOwn(entry.rules, options.rule)) return [];
|
|
82
|
+
return [{
|
|
83
|
+
files: entry.files ?? null,
|
|
84
|
+
index,
|
|
85
|
+
name: entry.name ?? `anonymous config #${index + 1}`,
|
|
86
|
+
value: entry.rules[options.rule]
|
|
87
|
+
}];
|
|
88
|
+
});
|
|
89
|
+
const payload = {
|
|
90
|
+
configFile: configPath ? basename(configPath) : null,
|
|
91
|
+
effective: effectiveValue ?? null,
|
|
92
|
+
enabled: effectiveValue !== void 0 && isRuleEnabled(effectiveValue),
|
|
93
|
+
file,
|
|
94
|
+
packageHint: getPackageHint(options.rule),
|
|
95
|
+
rule: options.rule,
|
|
96
|
+
severity: effectiveValue === void 0 ? null : getRuleSeverity(effectiveValue),
|
|
97
|
+
sources
|
|
98
|
+
};
|
|
99
|
+
if (options.json) {
|
|
100
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
101
|
+
} else {
|
|
102
|
+
console.log([
|
|
103
|
+
`ESLint rule explanation: ${payload.rule}`,
|
|
104
|
+
`- File: ${payload.file}`,
|
|
105
|
+
`- Config: ${payload.configFile ?? "none"}`,
|
|
106
|
+
`- Effective value: ${JSON.stringify(payload.effective)}`,
|
|
107
|
+
`- Enabled: ${payload.enabled ? "yes" : "no"}`,
|
|
108
|
+
`- Likely package: ${payload.packageHint ?? "core or custom config"}`,
|
|
109
|
+
`- Defining entries: ${payload.sources.length}`
|
|
110
|
+
].join("\n"));
|
|
111
|
+
for (const source of payload.sources) {
|
|
112
|
+
console.log(` - ${source.name}: ${JSON.stringify(source.value)}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (effectiveValue === void 0) process.exitCode = 1;
|
|
116
|
+
};
|
|
117
|
+
const getDependencyVersions = (manifest) => {
|
|
118
|
+
const fields = ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"];
|
|
119
|
+
const dependencies = {};
|
|
120
|
+
for (const field of fields) {
|
|
121
|
+
const values = manifest?.[field];
|
|
122
|
+
if (!values || typeof values !== "object" || Array.isArray(values)) continue;
|
|
123
|
+
for (const [name, version] of Object.entries(values)) {
|
|
124
|
+
if (typeof version === "string") dependencies[name] = version;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return dependencies;
|
|
128
|
+
};
|
|
129
|
+
const readResolvedManifest = (projectRequire, packageName) => {
|
|
130
|
+
try {
|
|
131
|
+
let directory = dirname(projectRequire.resolve(packageName));
|
|
132
|
+
for (let depth = 0; depth < 6; depth++) {
|
|
133
|
+
const manifestPath = join(directory, "package.json");
|
|
134
|
+
const manifest = readJson(manifestPath);
|
|
135
|
+
if (manifest?.name === packageName) return manifest;
|
|
136
|
+
directory = dirname(directory);
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
};
|
|
143
|
+
const readWorkspaceManifest = (cwd, packageName) => {
|
|
144
|
+
const packagesPath = join(cwd, "packages");
|
|
145
|
+
if (!existsSync(packagesPath)) return null;
|
|
146
|
+
for (const entry of readdirSync(packagesPath, { withFileTypes: true })) {
|
|
147
|
+
if (!entry.isDirectory()) continue;
|
|
148
|
+
const manifest = readJson(join(packagesPath, entry.name, "package.json"));
|
|
149
|
+
if (manifest?.name === packageName) return manifest;
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
};
|
|
153
|
+
const createCompatibilityReport = (cwd = process.cwd()) => {
|
|
154
|
+
const rootManifest = readJson(join(cwd, "package.json"));
|
|
155
|
+
const declared = getDependencyVersions(rootManifest);
|
|
156
|
+
const projectRequire = createRequire(join(cwd, "package.json"));
|
|
157
|
+
const configPackages = Object.keys(declared).filter((name) => name.startsWith("@santi020k/eslint-config-")).sort();
|
|
158
|
+
const runtimeVersions = {
|
|
159
|
+
eslint: readResolvedManifest(projectRequire, "eslint")?.version ?? null,
|
|
160
|
+
node: process.versions.node,
|
|
161
|
+
typescript: readResolvedManifest(projectRequire, "typescript")?.version ?? null
|
|
162
|
+
};
|
|
163
|
+
const packages = configPackages.map((name) => {
|
|
164
|
+
const manifest = readResolvedManifest(projectRequire, name) ?? readWorkspaceManifest(cwd, name);
|
|
165
|
+
const issues = [];
|
|
166
|
+
if (!manifest) {
|
|
167
|
+
issues.push("declared but not installed");
|
|
168
|
+
} else {
|
|
169
|
+
const nodeRange = manifest.engines?.node;
|
|
170
|
+
if (nodeRange && !semver.satisfies(runtimeVersions.node, nodeRange)) {
|
|
171
|
+
issues.push(`requires Node ${nodeRange}`);
|
|
172
|
+
}
|
|
173
|
+
for (const peer of ["eslint", "typescript"]) {
|
|
174
|
+
const range = manifest.peerDependencies?.[peer];
|
|
175
|
+
const version = runtimeVersions[peer];
|
|
176
|
+
if (range && version && !semver.satisfies(version, range)) {
|
|
177
|
+
issues.push(`requires ${peer} ${range}, resolved ${version}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
declared: declared[name],
|
|
183
|
+
issues,
|
|
184
|
+
name,
|
|
185
|
+
resolved: manifest?.version ?? null
|
|
186
|
+
};
|
|
187
|
+
});
|
|
188
|
+
return {
|
|
189
|
+
compatible: packages.every((item) => item.issues.length === 0),
|
|
190
|
+
packages,
|
|
191
|
+
runtime: runtimeVersions
|
|
192
|
+
};
|
|
193
|
+
};
|
|
194
|
+
const handleCompatibility = (cwd = process.cwd(), json = false) => {
|
|
195
|
+
const report = createCompatibilityReport(cwd);
|
|
196
|
+
if (json) {
|
|
197
|
+
console.log(JSON.stringify(report, null, 2));
|
|
198
|
+
} else {
|
|
199
|
+
console.log([
|
|
200
|
+
`ESLint compatibility: ${report.compatible ? "compatible" : "issues found"}`,
|
|
201
|
+
`- Node: ${report.runtime.node}`,
|
|
202
|
+
`- ESLint: ${report.runtime.eslint ?? "not installed"}`,
|
|
203
|
+
`- TypeScript: ${report.runtime.typescript ?? "not installed"}`
|
|
204
|
+
].join("\n"));
|
|
205
|
+
for (const item of report.packages) {
|
|
206
|
+
console.log(
|
|
207
|
+
`- ${item.name}: ${item.resolved ?? "not installed"}` + (item.issues.length > 0 ? ` (${item.issues.join("; ")})` : "")
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (!report.compatible) process.exitCode = 1;
|
|
212
|
+
};
|
|
213
|
+
export {
|
|
214
|
+
createCompatibilityReport,
|
|
215
|
+
handleCompatibility,
|
|
216
|
+
handleExplainRule
|
|
217
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
interface V3MigrationContext {
|
|
2
|
+
declaredConfigPackages?: string[];
|
|
3
|
+
extensions: string[];
|
|
4
|
+
formats: string[];
|
|
5
|
+
frameworks: string[];
|
|
6
|
+
libraries: string[];
|
|
7
|
+
packageManager: string;
|
|
8
|
+
testing: string[];
|
|
9
|
+
tools: string[];
|
|
10
|
+
typescript: boolean;
|
|
11
|
+
}
|
|
12
|
+
interface V3MigrationOptions {
|
|
13
|
+
check?: boolean;
|
|
14
|
+
full?: boolean;
|
|
15
|
+
json?: boolean;
|
|
16
|
+
write?: boolean;
|
|
17
|
+
}
|
|
18
|
+
declare const getExplicitConfigFeaturePackages: (content: string) => string[];
|
|
19
|
+
declare const migrateConfigToV3: (content: string, mode: "full" | "lean") => {
|
|
20
|
+
changes: string[];
|
|
21
|
+
content: string;
|
|
22
|
+
featurePackages: string[];
|
|
23
|
+
};
|
|
24
|
+
declare const handleMigrateV3: (cwd: string, context: V3MigrationContext, options?: V3MigrationOptions) => void;
|
|
25
|
+
|
|
26
|
+
export { type V3MigrationContext, type V3MigrationOptions, getExplicitConfigFeaturePackages, handleMigrateV3, migrateConfigToV3 };
|