@santi020k/eslint-config-basic 1.5.0 → 2.0.0-beta.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 +171 -0
- package/README.md +4 -22
- package/dist/agent-skill-generator.d.ts +108 -0
- package/dist/agent-skill-generator.js +697 -0
- package/dist/cli.d.ts +20 -2
- package/dist/cli.js +603 -31
- package/dist/compose.d.ts +4 -2
- package/dist/compose.js +19 -8
- package/dist/frameworks.d.ts +58 -0
- package/dist/frameworks.js +106 -0
- package/dist/index.d.ts +12 -6
- package/dist/index.js +711 -104
- package/dist/integrations.d.ts +1 -0
- package/dist/integrations.js +5 -0
- package/dist/lazy.d.ts +3 -0
- package/dist/lazy.js +19 -0
- package/dist/resolvers.d.ts +5 -2
- package/dist/resolvers.js +60 -23
- package/package.json +46 -64
- package/dist/optionals.d.ts +0 -13
- package/dist/optionals.js +0 -71
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
-
import {
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { basename, dirname, join } from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { analyzeEslintConfig, handleGenerateSkill } from "./agent-skill-generator.js";
|
|
5
7
|
import { detectProjectOptions } from "./index.js";
|
|
6
8
|
const getDefaultConfigFilename = (cwd) => {
|
|
7
9
|
const packageJsonPath = join(cwd, "package.json");
|
|
@@ -17,48 +19,303 @@ const resolveConfigPath = (cwd) => {
|
|
|
17
19
|
const existingConfigPath = [
|
|
18
20
|
"eslint.config.js",
|
|
19
21
|
"eslint.config.mjs"
|
|
20
|
-
].map((filename) => join(cwd, filename)).find(existsSync);
|
|
22
|
+
].map((filename) => join(cwd, filename)).find((p) => existsSync(p));
|
|
21
23
|
return existingConfigPath ?? join(cwd, getDefaultConfigFilename(cwd));
|
|
22
24
|
};
|
|
23
25
|
const getFrameworkKeys = (detectedFrameworks) => {
|
|
24
26
|
const frameworkKeys = new Set(detectedFrameworks ?? []);
|
|
25
|
-
if (frameworkKeys.has("next") || frameworkKeys.has("expo")) {
|
|
27
|
+
if (frameworkKeys.has("next") || frameworkKeys.has("expo") || frameworkKeys.has("remix") || frameworkKeys.has("react-router") || frameworkKeys.has("tanstack-start") && !frameworkKeys.has("solid")) {
|
|
26
28
|
frameworkKeys.add("react");
|
|
27
29
|
}
|
|
28
30
|
return [...frameworkKeys].sort();
|
|
29
31
|
};
|
|
32
|
+
const toPropertyKey = (key) => /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : `'${key}'`;
|
|
33
|
+
const FRAMEWORK_PACKAGE_TO_KEY = {
|
|
34
|
+
"@santi020k/eslint-config-angular": "angular",
|
|
35
|
+
"@santi020k/eslint-config-astro": "astro",
|
|
36
|
+
"@santi020k/eslint-config-expo": "expo",
|
|
37
|
+
"@santi020k/eslint-config-hono": "hono",
|
|
38
|
+
"@santi020k/eslint-config-lit": "lit",
|
|
39
|
+
"@santi020k/eslint-config-nest": "nest",
|
|
40
|
+
"@santi020k/eslint-config-next": "next",
|
|
41
|
+
"@santi020k/eslint-config-nuxt": "nuxt",
|
|
42
|
+
"@santi020k/eslint-config-preact": "preact",
|
|
43
|
+
"@santi020k/eslint-config-qwik": "qwik",
|
|
44
|
+
"@santi020k/eslint-config-react": "react",
|
|
45
|
+
"@santi020k/eslint-config-react-router": "react-router",
|
|
46
|
+
"@santi020k/eslint-config-remix": "remix",
|
|
47
|
+
"@santi020k/eslint-config-slidev": "slidev",
|
|
48
|
+
"@santi020k/eslint-config-solid": "solid",
|
|
49
|
+
"@santi020k/eslint-config-svelte": "svelte",
|
|
50
|
+
"@santi020k/eslint-config-tanstack-start": "tanstack-start",
|
|
51
|
+
"@santi020k/eslint-config-vite": "vite",
|
|
52
|
+
"@santi020k/eslint-config-vue": "vue"
|
|
53
|
+
};
|
|
54
|
+
const FRAMEWORK_KEY_TO_PACKAGE = Object.fromEntries(
|
|
55
|
+
Object.entries(FRAMEWORK_PACKAGE_TO_KEY).map(([packageName, framework]) => [framework, packageName])
|
|
56
|
+
);
|
|
57
|
+
const LITE_PACKAGE_NAME = "@santi020k/eslint-config-lite";
|
|
58
|
+
const INTEGRATIONS_PACKAGE_NAME = "@santi020k/eslint-config-integrations";
|
|
59
|
+
const TYPESCRIPT_PACKAGE_NAME = "typescript";
|
|
60
|
+
const getConfigPathIfPresent = (cwd) => {
|
|
61
|
+
const configPath = [
|
|
62
|
+
"eslint.config.js",
|
|
63
|
+
"eslint.config.mjs",
|
|
64
|
+
"eslint.config.cjs"
|
|
65
|
+
].map((filename) => join(cwd, filename)).find((p) => existsSync(p));
|
|
66
|
+
return configPath ?? null;
|
|
67
|
+
};
|
|
68
|
+
const readPackageJson = (cwd) => {
|
|
69
|
+
const packageJsonPath = join(cwd, "package.json");
|
|
70
|
+
if (!existsSync(packageJsonPath)) return null;
|
|
71
|
+
try {
|
|
72
|
+
return JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
const getDeclaredDependencyNames = (packageJson) => {
|
|
78
|
+
const dependencyFields = [
|
|
79
|
+
"dependencies",
|
|
80
|
+
"devDependencies",
|
|
81
|
+
"peerDependencies",
|
|
82
|
+
"optionalDependencies"
|
|
83
|
+
];
|
|
84
|
+
const names = /* @__PURE__ */ new Set();
|
|
85
|
+
for (const field of dependencyFields) {
|
|
86
|
+
const dependencies = packageJson?.[field];
|
|
87
|
+
if (!dependencies || typeof dependencies !== "object" || Array.isArray(dependencies)) continue;
|
|
88
|
+
for (const name of Object.keys(dependencies)) {
|
|
89
|
+
names.add(name);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return names;
|
|
93
|
+
};
|
|
94
|
+
const needsIntegrationPackage = (summary) => summary.libraries.length > 0 || summary.testing.length > 0 || summary.formats.length > 0 || summary.tools.length > 0 || summary.extensions.some((extension) => extension !== "boundaries");
|
|
95
|
+
const hasPresetAll = (configContent) => Boolean(
|
|
96
|
+
configContent && (/\bPreset\.All\b/.test(configContent) || /preset\s*:\s*['"]all['"]/.test(configContent))
|
|
97
|
+
);
|
|
98
|
+
const getLiteInstallPackages = (summary, declaredDependencies) => {
|
|
99
|
+
const packages = /* @__PURE__ */ new Set([
|
|
100
|
+
"eslint",
|
|
101
|
+
LITE_PACKAGE_NAME
|
|
102
|
+
]);
|
|
103
|
+
for (const framework of summary.frameworks) {
|
|
104
|
+
const packageName = FRAMEWORK_KEY_TO_PACKAGE[framework];
|
|
105
|
+
if (packageName) packages.add(packageName);
|
|
106
|
+
}
|
|
107
|
+
if (needsIntegrationPackage(summary)) {
|
|
108
|
+
packages.add(INTEGRATIONS_PACKAGE_NAME);
|
|
109
|
+
}
|
|
110
|
+
if (summary.typescript && !declaredDependencies.has(TYPESCRIPT_PACKAGE_NAME)) {
|
|
111
|
+
packages.add(TYPESCRIPT_PACKAGE_NAME);
|
|
112
|
+
}
|
|
113
|
+
return [...packages];
|
|
114
|
+
};
|
|
115
|
+
const createInstallCommand = (packageManager, packages) => {
|
|
116
|
+
const packageList = packages.join(" ");
|
|
117
|
+
switch (packageManager) {
|
|
118
|
+
case "bun":
|
|
119
|
+
return `bun add -d ${packageList}`;
|
|
120
|
+
case "npm":
|
|
121
|
+
return `npm install -D ${packageList}`;
|
|
122
|
+
case "yarn":
|
|
123
|
+
return `yarn add -D ${packageList}`;
|
|
124
|
+
default:
|
|
125
|
+
return `pnpm add -D ${packageList}`;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
const detectPackageManager = (cwd) => {
|
|
129
|
+
if (existsSync(join(cwd, "pnpm-lock.yaml")) || existsSync(join(cwd, "pnpm-workspace.yaml"))) return "pnpm";
|
|
130
|
+
if (existsSync(join(cwd, "yarn.lock"))) return "yarn";
|
|
131
|
+
if (existsSync(join(cwd, "bun.lockb")) || existsSync(join(cwd, "bun.lock"))) return "bun";
|
|
132
|
+
return "npm";
|
|
133
|
+
};
|
|
134
|
+
const hasLintScript = (cwd) => {
|
|
135
|
+
const packageJson = readPackageJson(cwd);
|
|
136
|
+
return Boolean(packageJson?.scripts?.lint);
|
|
137
|
+
};
|
|
138
|
+
const getWorkspacePatternsFromPackageJson = (packageJson) => {
|
|
139
|
+
const workspaces = packageJson?.workspaces;
|
|
140
|
+
if (Array.isArray(workspaces)) {
|
|
141
|
+
return workspaces.filter((value) => typeof value === "string");
|
|
142
|
+
}
|
|
143
|
+
if (workspaces && typeof workspaces === "object" && Array.isArray(workspaces.packages)) {
|
|
144
|
+
return workspaces.packages.filter((value) => typeof value === "string");
|
|
145
|
+
}
|
|
146
|
+
return [];
|
|
147
|
+
};
|
|
148
|
+
const getWorkspacePatternsFromPnpm = (cwd) => {
|
|
149
|
+
const workspacePath = join(cwd, "pnpm-workspace.yaml");
|
|
150
|
+
if (!existsSync(workspacePath)) return [];
|
|
151
|
+
const content = readFileSync(workspacePath, "utf8");
|
|
152
|
+
const patterns = [];
|
|
153
|
+
let inPackages = false;
|
|
154
|
+
for (const line of content.split(/\r?\n/)) {
|
|
155
|
+
if (/^packages:\s*$/.test(line)) {
|
|
156
|
+
inPackages = true;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (inPackages && /^\S/.test(line)) break;
|
|
160
|
+
const match = inPackages ? /^\s*-\s*['"]?([^'"]+)['"]?\s*$/.exec(line) : null;
|
|
161
|
+
if (match?.[1] && !match[1].startsWith("!")) patterns.push(match[1]);
|
|
162
|
+
}
|
|
163
|
+
return patterns;
|
|
164
|
+
};
|
|
165
|
+
const expandWorkspacePattern = (cwd, pattern) => {
|
|
166
|
+
const normalized = pattern.replace(/\/$/, "");
|
|
167
|
+
if (!normalized.endsWith("/*")) return [];
|
|
168
|
+
const base = normalized.slice(0, -2);
|
|
169
|
+
const basePath = join(cwd, base);
|
|
170
|
+
if (!existsSync(basePath)) return [];
|
|
171
|
+
return readdirSync(basePath, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => `${base}/${entry.name}`).filter((projectPath) => existsSync(join(cwd, projectPath, "package.json")));
|
|
172
|
+
};
|
|
173
|
+
const detectWorkspaceProjects = (cwd) => {
|
|
174
|
+
const patterns = [
|
|
175
|
+
...getWorkspacePatternsFromPackageJson(readPackageJson(cwd)),
|
|
176
|
+
...getWorkspacePatternsFromPnpm(cwd)
|
|
177
|
+
];
|
|
178
|
+
return [...new Set(patterns.flatMap((pattern) => expandWorkspacePattern(cwd, pattern)))].sort();
|
|
179
|
+
};
|
|
180
|
+
const getDetectionValues = (options) => ({
|
|
181
|
+
extensions: options.extensions ?? [],
|
|
182
|
+
formats: options.formats ?? [],
|
|
183
|
+
libraries: options.libraries ?? [],
|
|
184
|
+
runtime: options.runtime ?? "universal",
|
|
185
|
+
settings: options.settings ?? [],
|
|
186
|
+
testing: options.testing ?? [],
|
|
187
|
+
tools: options.tools ?? [],
|
|
188
|
+
typescript: options.typescript ?? false
|
|
189
|
+
});
|
|
30
190
|
const createConfigContent = (cwd) => {
|
|
31
191
|
const options = detectProjectOptions(cwd);
|
|
192
|
+
const values = getDetectionValues(options);
|
|
32
193
|
const frameworkKeys = getFrameworkKeys(options.detectedFrameworks);
|
|
33
194
|
const imports = ["import { eslintConfig } from '@santi020k/eslint-config-basic'"];
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
});
|
|
195
|
+
const presetLine = options.preset && options.preset !== "basic" ? ` preset: ${JSON.stringify(options.preset)},
|
|
196
|
+
` : "";
|
|
37
197
|
const configContent = `${imports.join("\n")}
|
|
38
198
|
|
|
39
|
-
export default eslintConfig({
|
|
40
|
-
typescript: ${JSON.stringify(
|
|
199
|
+
export default await eslintConfig({
|
|
200
|
+
${presetLine} typescript: ${JSON.stringify(values.typescript)},
|
|
41
201
|
frameworks: {
|
|
42
|
-
${frameworkKeys.map((key) => `${key}:
|
|
202
|
+
${frameworkKeys.map((key) => `${toPropertyKey(key)}: true`).join(",\n ")}
|
|
43
203
|
},
|
|
44
|
-
libraries: ${JSON.stringify(
|
|
45
|
-
testing: ${JSON.stringify(
|
|
46
|
-
formats: ${JSON.stringify(
|
|
47
|
-
tools: ${JSON.stringify(
|
|
48
|
-
extensions: ${JSON.stringify(
|
|
49
|
-
runtime: ${JSON.stringify(
|
|
50
|
-
settings: ${JSON.stringify(
|
|
204
|
+
libraries: ${JSON.stringify(values.libraries, null, 2)},
|
|
205
|
+
testing: ${JSON.stringify(values.testing, null, 2)},
|
|
206
|
+
formats: ${JSON.stringify(values.formats, null, 2)},
|
|
207
|
+
tools: ${JSON.stringify(values.tools, null, 2)},
|
|
208
|
+
extensions: ${JSON.stringify(values.extensions, null, 2)},
|
|
209
|
+
runtime: ${JSON.stringify(values.runtime)},
|
|
210
|
+
settings: ${JSON.stringify(values.settings, null, 2)}
|
|
51
211
|
})
|
|
52
212
|
`;
|
|
53
213
|
return {
|
|
54
|
-
|
|
55
|
-
|
|
214
|
+
configContent,
|
|
215
|
+
configPath: resolveConfigPath(cwd)
|
|
56
216
|
};
|
|
57
217
|
};
|
|
58
|
-
const
|
|
218
|
+
const formatList = (values) => {
|
|
219
|
+
if (!values?.length) return "none";
|
|
220
|
+
return values.join(", ");
|
|
221
|
+
};
|
|
222
|
+
const getProjectSummary = (cwd) => {
|
|
223
|
+
const options = detectProjectOptions(cwd);
|
|
224
|
+
const workspaceProjects = detectWorkspaceProjects(cwd);
|
|
225
|
+
return {
|
|
226
|
+
extensions: options.extensions ?? [],
|
|
227
|
+
formats: options.formats ?? [],
|
|
228
|
+
frameworks: getFrameworkKeys(options.detectedFrameworks),
|
|
229
|
+
libraries: options.libraries ?? [],
|
|
230
|
+
nextMode: options.nextMode ?? "n/a",
|
|
231
|
+
preset: options.preset ?? "basic",
|
|
232
|
+
runtime: options.runtime ?? "universal",
|
|
233
|
+
testing: options.testing ?? [],
|
|
234
|
+
tools: options.tools ?? [],
|
|
235
|
+
typescript: Boolean(options.typescript),
|
|
236
|
+
workspaceProjects
|
|
237
|
+
};
|
|
238
|
+
};
|
|
239
|
+
const createStandardsContent = (cwd) => {
|
|
240
|
+
const summary = getProjectSummary(cwd);
|
|
241
|
+
return [
|
|
242
|
+
"# ESLint Standards",
|
|
243
|
+
"",
|
|
244
|
+
"This document was generated from the active project dependencies detected by `@santi020k/eslint-config-basic`.",
|
|
245
|
+
"",
|
|
246
|
+
"## Active Profile",
|
|
247
|
+
"",
|
|
248
|
+
`- TypeScript: ${summary.typescript ? "enabled" : "disabled"}`,
|
|
249
|
+
`- Preset: ${summary.preset}`,
|
|
250
|
+
`- Runtime: ${summary.runtime}`,
|
|
251
|
+
`- Next.js mode: ${summary.nextMode}`,
|
|
252
|
+
`- Frameworks: ${formatList(summary.frameworks)}`,
|
|
253
|
+
`- Libraries: ${formatList(summary.libraries)}`,
|
|
254
|
+
`- Testing: ${formatList(summary.testing)}`,
|
|
255
|
+
`- Formats: ${formatList(summary.formats)}`,
|
|
256
|
+
`- Tools: ${formatList(summary.tools)}`,
|
|
257
|
+
`- Extensions: ${formatList(summary.extensions)}`,
|
|
258
|
+
"",
|
|
259
|
+
"## Recommended Config",
|
|
260
|
+
"",
|
|
261
|
+
"```js",
|
|
262
|
+
"import { eslintConfig } from '@santi020k/eslint-config-basic'",
|
|
263
|
+
"",
|
|
264
|
+
"export default await eslintConfig()",
|
|
265
|
+
"```",
|
|
266
|
+
"",
|
|
267
|
+
"Use `basic-eslint explain` when you want to inspect what the zero-config setup detects."
|
|
268
|
+
].join("\n");
|
|
269
|
+
};
|
|
270
|
+
const getCliVersion = () => {
|
|
271
|
+
const cliDir = dirname(fileURLToPath(import.meta.url));
|
|
272
|
+
const packageJsonPath = join(cliDir, "..", "package.json");
|
|
273
|
+
try {
|
|
274
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
275
|
+
return packageJson.version ?? "unknown";
|
|
276
|
+
} catch {
|
|
277
|
+
return "unknown";
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
const printUsage = () => {
|
|
281
|
+
console.log([
|
|
282
|
+
"Usage: basic-eslint <command> [options]",
|
|
283
|
+
"",
|
|
284
|
+
"Commands:",
|
|
285
|
+
" init Create eslint.config.js/mjs if missing",
|
|
286
|
+
" update Regenerate eslint.config.js/mjs from detection",
|
|
287
|
+
" explain Print detected v2 config inputs",
|
|
288
|
+
" inspect Print detected inputs and active config features",
|
|
289
|
+
" doctor Check project setup for common v2 adoption issues",
|
|
290
|
+
" docs Generate ESLINT_STANDARDS.md from detection",
|
|
291
|
+
" migrate Report v1-to-v2 migration suggestions",
|
|
292
|
+
" generate-skill Generate AI agent standards files",
|
|
293
|
+
"",
|
|
294
|
+
"Options:",
|
|
295
|
+
" --force Overwrite existing generated skill sections/files",
|
|
296
|
+
" --check init: verify config exists; generate-skill: verify files are up to date",
|
|
297
|
+
" --create generate-skill: scaffold a root AGENTS.md when missing",
|
|
298
|
+
" --json Print JSON for commands that support it",
|
|
299
|
+
" --lite-install doctor: print the detected lite install command",
|
|
300
|
+
" --write Apply safe migrations for commands that support it",
|
|
301
|
+
" --help, -h Show this help message",
|
|
302
|
+
" --version, -v Show CLI version"
|
|
303
|
+
].join("\n"));
|
|
304
|
+
};
|
|
305
|
+
const handleInit = (cwd = process.cwd(), check = false) => {
|
|
59
306
|
const configPath = resolveConfigPath(cwd);
|
|
307
|
+
if (check) {
|
|
308
|
+
const exists = existsSync(configPath);
|
|
309
|
+
console.log(JSON.stringify({
|
|
310
|
+
configFile: exists ? basename(configPath) : null,
|
|
311
|
+
exists,
|
|
312
|
+
ok: exists
|
|
313
|
+
}, null, 2));
|
|
314
|
+
if (!exists) process.exitCode = 1;
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
60
317
|
if (existsSync(configPath)) {
|
|
61
|
-
console.
|
|
318
|
+
console.error(`\u26A0\uFE0F ${basename(configPath)} already exists. Skipping...`);
|
|
62
319
|
return;
|
|
63
320
|
}
|
|
64
321
|
console.log("\u{1F50D} Detecting project settings...");
|
|
@@ -69,25 +326,340 @@ const handleInit = (cwd = process.cwd()) => {
|
|
|
69
326
|
};
|
|
70
327
|
const handleUpdate = (cwd = process.cwd()) => {
|
|
71
328
|
console.log("\u{1F50D} Detecting project settings...");
|
|
72
|
-
const {
|
|
329
|
+
const { configContent, configPath } = createConfigContent(cwd);
|
|
330
|
+
if (existsSync(configPath)) {
|
|
331
|
+
const backupPath = `${configPath}.bak`;
|
|
332
|
+
const existing = readFileSync(configPath, "utf8");
|
|
333
|
+
writeFileSync(backupPath, existing);
|
|
334
|
+
console.log(`\u{1F4E6} Backed up existing config to ${basename(backupPath)}`);
|
|
335
|
+
}
|
|
73
336
|
writeFileSync(configPath, configContent);
|
|
74
337
|
console.log(`\u2705 Updated ${basename(configPath)} with auto-detected settings!`);
|
|
75
338
|
console.log("\u{1F680} Ready to lint!");
|
|
76
339
|
};
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
if (
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
340
|
+
const handleExplain = (cwd = process.cwd(), json = false) => {
|
|
341
|
+
const summary = getProjectSummary(cwd);
|
|
342
|
+
if (json) {
|
|
343
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
console.log([
|
|
347
|
+
"ESLint Basic detected configuration:",
|
|
348
|
+
`- TypeScript: ${summary.typescript ? "enabled" : "disabled"}`,
|
|
349
|
+
`- Preset: ${summary.preset}`,
|
|
350
|
+
`- Runtime: ${summary.runtime}`,
|
|
351
|
+
`- Next.js mode: ${summary.nextMode}`,
|
|
352
|
+
`- Frameworks: ${formatList(summary.frameworks)}`,
|
|
353
|
+
`- Libraries: ${formatList(summary.libraries)}`,
|
|
354
|
+
`- Testing: ${formatList(summary.testing)}`,
|
|
355
|
+
`- Formats: ${formatList(summary.formats)}`,
|
|
356
|
+
`- Tools: ${formatList(summary.tools)}`,
|
|
357
|
+
`- Extensions: ${formatList(summary.extensions)}`
|
|
358
|
+
].join("\n"));
|
|
359
|
+
};
|
|
360
|
+
const handleInspect = async (cwd = process.cwd(), json = false) => {
|
|
361
|
+
const summary = getProjectSummary(cwd);
|
|
362
|
+
const activeConfig = await analyzeEslintConfig(cwd);
|
|
363
|
+
const payload = {
|
|
364
|
+
activeConfig,
|
|
365
|
+
detected: summary,
|
|
366
|
+
packageManager: detectPackageManager(cwd)
|
|
367
|
+
};
|
|
368
|
+
if (json) {
|
|
369
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
console.log([
|
|
373
|
+
"ESLint Basic inspection:",
|
|
374
|
+
`- Package manager: ${payload.packageManager}`,
|
|
375
|
+
`- Config source: ${activeConfig?.source ?? "not found"}`,
|
|
376
|
+
`- Config file: ${activeConfig?.configFile ?? "none"}`,
|
|
377
|
+
`- TypeScript: ${summary.typescript ? "enabled" : "disabled"}`,
|
|
378
|
+
`- Preset: ${summary.preset}`,
|
|
379
|
+
`- Runtime: ${summary.runtime}`,
|
|
380
|
+
`- Frameworks: ${formatList(summary.frameworks)}`,
|
|
381
|
+
`- Libraries: ${formatList(summary.libraries)}`,
|
|
382
|
+
`- Testing: ${formatList(summary.testing)}`,
|
|
383
|
+
`- Formats: ${formatList(summary.formats)}`,
|
|
384
|
+
`- Tools: ${formatList(summary.tools)}`,
|
|
385
|
+
`- Extensions: ${formatList(summary.extensions)}`,
|
|
386
|
+
`- Workspace projects: ${formatList(summary.workspaceProjects)}`
|
|
387
|
+
].join("\n"));
|
|
388
|
+
};
|
|
389
|
+
const findDuplicateEslint = (cwd = process.cwd()) => {
|
|
390
|
+
try {
|
|
391
|
+
const projectRequire = createRequire(join(cwd, "package.json"));
|
|
392
|
+
const projectEslintPkgPath = projectRequire.resolve("eslint/package.json");
|
|
393
|
+
const coreMainPath = projectRequire.resolve("@santi020k/eslint-config-core");
|
|
394
|
+
const coreRequire = createRequire(coreMainPath);
|
|
395
|
+
const coreEslintPkgPath = coreRequire.resolve("eslint/package.json");
|
|
396
|
+
if (projectEslintPkgPath === coreEslintPkgPath) return null;
|
|
397
|
+
const projectVersion = JSON.parse(readFileSync(projectEslintPkgPath, "utf8")).version ?? "unknown";
|
|
398
|
+
const configVersion = JSON.parse(readFileSync(coreEslintPkgPath, "utf8")).version ?? "unknown";
|
|
399
|
+
if (projectVersion === configVersion) return null;
|
|
400
|
+
return { configVersion, projectVersion };
|
|
401
|
+
} catch {
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
const validateConfigContent = (configContent, hasV1FrameworkImports, declaredDependencies, summary) => {
|
|
406
|
+
const warnings = [];
|
|
407
|
+
if (hasV1FrameworkImports) {
|
|
408
|
+
warnings.push("Config still imports v1 framework packages. Run `basic-eslint migrate --write` or switch to framework booleans.");
|
|
409
|
+
}
|
|
410
|
+
if (summary.workspaceProjects.length > 0 && !configContent.includes("projects:")) {
|
|
411
|
+
warnings.push("Workspace packages were detected, but the root config does not use `projects` scoping.");
|
|
412
|
+
}
|
|
413
|
+
if (configContent.includes(LITE_PACKAGE_NAME)) {
|
|
414
|
+
const missingFrameworkPackages = summary.frameworks.map((framework) => FRAMEWORK_KEY_TO_PACKAGE[framework]).filter((packageName) => Boolean(packageName)).filter((packageName) => !declaredDependencies.has(packageName));
|
|
415
|
+
if (missingFrameworkPackages.length > 0) {
|
|
416
|
+
warnings.push(
|
|
417
|
+
`Lite config detected ${formatList(summary.frameworks)}, but these framework config packages are not declared: ${missingFrameworkPackages.join(", ")}. Install them or disable those frameworks.`
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
if (needsIntegrationPackage(summary) && !declaredDependencies.has(INTEGRATIONS_PACKAGE_NAME)) {
|
|
421
|
+
warnings.push(
|
|
422
|
+
`Lite config detected optional integrations, but ${INTEGRATIONS_PACKAGE_NAME} is not declared. Install it or remove the selected libraries, testing tools, formats, tools, and extension integrations.`
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
if (hasPresetAll(configContent)) {
|
|
426
|
+
warnings.push(
|
|
427
|
+
"Lite config uses Preset.All, which enables every optional integration and reduces the dependency benefits of lite. Prefer enabling only the integrations this project uses."
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return warnings;
|
|
432
|
+
};
|
|
433
|
+
const buildDoctorDiagnosis = (cwd, configPath, configContent, hasV1FrameworkImports, activeConfig, declaredDependencies, summary) => {
|
|
434
|
+
const issues = [];
|
|
435
|
+
const warnings = [];
|
|
436
|
+
if (!readPackageJson(cwd)) {
|
|
437
|
+
issues.push("package.json is missing or invalid.");
|
|
438
|
+
}
|
|
439
|
+
if (!configPath) {
|
|
440
|
+
warnings.push("No eslint.config.js/mjs/cjs file found. Run `basic-eslint init` to create one.");
|
|
441
|
+
} else if (!activeConfig && !hasV1FrameworkImports) {
|
|
442
|
+
issues.push(`${basename(configPath)} could not be loaded. Run ESLint directly to see the import error.`);
|
|
443
|
+
}
|
|
444
|
+
if (!hasLintScript(cwd)) {
|
|
445
|
+
warnings.push("No `lint` script found in package.json.");
|
|
446
|
+
}
|
|
447
|
+
if (configContent) {
|
|
448
|
+
warnings.push(...validateConfigContent(configContent, hasV1FrameworkImports, declaredDependencies, summary));
|
|
449
|
+
}
|
|
450
|
+
const duplicateEslint = findDuplicateEslint(cwd);
|
|
451
|
+
if (duplicateEslint) {
|
|
452
|
+
warnings.push(
|
|
453
|
+
`Two ESLint copies are installed: the project resolves ${duplicateEslint.projectVersion} while the config packages resolve ${duplicateEslint.configVersion}. Only ESLint 10 is supported, and parallel copies can apply different rule behavior between your editor and CLI. Align the project's eslint version with the config's (or dedupe via your package manager).`
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
return { issues, warnings };
|
|
457
|
+
};
|
|
458
|
+
const outputDoctorResult = (json, configPath, packageManager, summary, liteInstallCommand, issues, warnings) => {
|
|
459
|
+
let status = "passed";
|
|
460
|
+
if (issues.length > 0) {
|
|
461
|
+
status = "failed";
|
|
462
|
+
} else if (warnings.length > 0) {
|
|
463
|
+
status = "passed with warnings";
|
|
464
|
+
}
|
|
465
|
+
const payload = {
|
|
466
|
+
configFile: configPath ? basename(configPath) : null,
|
|
467
|
+
issues,
|
|
468
|
+
liteInstallCommand,
|
|
469
|
+
packageManager,
|
|
470
|
+
status,
|
|
471
|
+
warnings,
|
|
472
|
+
workspaceProjects: summary.workspaceProjects
|
|
473
|
+
};
|
|
474
|
+
if (json) {
|
|
475
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
476
|
+
if (issues.length > 0) process.exitCode = 1;
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
console.log([
|
|
480
|
+
`ESLint Basic doctor: ${status}`,
|
|
481
|
+
`- Package manager: ${payload.packageManager}`,
|
|
482
|
+
`- Config file: ${payload.configFile ?? "none"}`,
|
|
483
|
+
`- Workspace projects: ${formatList(summary.workspaceProjects)}`,
|
|
484
|
+
...issues.length > 0 ? ["", "Issues:", ...issues.map((issue) => `- ${issue}`)] : [],
|
|
485
|
+
...warnings.length > 0 ? ["", "Warnings:", ...warnings.map((warning) => `- ${warning}`)] : []
|
|
486
|
+
].join("\n"));
|
|
487
|
+
if (issues.length > 0) process.exitCode = 1;
|
|
488
|
+
};
|
|
489
|
+
const handleDoctor = async (cwd = process.cwd(), json = false, liteInstall = false) => {
|
|
490
|
+
const configPath = getConfigPathIfPresent(cwd);
|
|
491
|
+
const packageJson = readPackageJson(cwd);
|
|
492
|
+
const declaredDependencies = getDeclaredDependencyNames(packageJson);
|
|
493
|
+
const summary = getProjectSummary(cwd);
|
|
494
|
+
const activeConfig = await analyzeEslintConfig(cwd);
|
|
495
|
+
const configContent = configPath ? readFileSync(configPath, "utf8") : null;
|
|
496
|
+
const packageManager = detectPackageManager(cwd);
|
|
497
|
+
const liteInstallPackages = getLiteInstallPackages(summary, declaredDependencies);
|
|
498
|
+
const liteInstallCommand = createInstallCommand(packageManager, liteInstallPackages);
|
|
499
|
+
const hasV1FrameworkImports = configContent ? Object.keys(FRAMEWORK_PACKAGE_TO_KEY).some((packageName) => configContent.includes(packageName)) : false;
|
|
500
|
+
if (liteInstall) {
|
|
501
|
+
if (json) {
|
|
502
|
+
console.log(JSON.stringify({ command: liteInstallCommand, packageManager, packages: liteInstallPackages }, null, 2));
|
|
503
|
+
} else {
|
|
504
|
+
console.log(liteInstallCommand);
|
|
505
|
+
}
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
const { issues, warnings } = buildDoctorDiagnosis(cwd, configPath, configContent, hasV1FrameworkImports, activeConfig, declaredDependencies, summary);
|
|
509
|
+
outputDoctorResult(json, configPath, packageManager, summary, liteInstallCommand, issues, warnings);
|
|
510
|
+
};
|
|
511
|
+
const handleDocs = (cwd = process.cwd()) => {
|
|
512
|
+
const outputPath = join(cwd, "ESLINT_STANDARDS.md");
|
|
513
|
+
writeFileSync(outputPath, createStandardsContent(cwd));
|
|
514
|
+
console.log("\u2705 Generated ESLINT_STANDARDS.md");
|
|
515
|
+
};
|
|
516
|
+
const migrateConfigContent = (content) => {
|
|
517
|
+
const importedFrameworks = Object.entries(FRAMEWORK_PACKAGE_TO_KEY).filter(([packageName]) => content.includes(packageName)).map(([, framework]) => framework);
|
|
518
|
+
if (importedFrameworks.length === 0) {
|
|
519
|
+
return { changed: false, content, frameworks: [] };
|
|
520
|
+
}
|
|
521
|
+
const frameworks = getFrameworkKeys(importedFrameworks);
|
|
522
|
+
const migrated = [
|
|
523
|
+
"import { eslintConfig } from '@santi020k/eslint-config-basic'",
|
|
524
|
+
"",
|
|
525
|
+
"export default await eslintConfig({",
|
|
526
|
+
" frameworks: {",
|
|
527
|
+
` ${frameworks.map((key) => `${toPropertyKey(key)}: true`).join(",\n ")}`,
|
|
528
|
+
" }",
|
|
529
|
+
"})",
|
|
530
|
+
""
|
|
531
|
+
].join("\n");
|
|
532
|
+
return { changed: true, content: migrated, frameworks };
|
|
533
|
+
};
|
|
534
|
+
const processConfigMigration = (configPath, write, suggestions) => {
|
|
535
|
+
const applied = [];
|
|
536
|
+
if (!existsSync(configPath)) {
|
|
537
|
+
if (write) suggestions.push("- No ESLint config file was found to rewrite.");
|
|
538
|
+
return applied;
|
|
539
|
+
}
|
|
540
|
+
const configContent = readFileSync(configPath, "utf8");
|
|
541
|
+
if (configContent.includes("@santi020k/eslint-config-") && !configContent.includes("frameworks:")) {
|
|
542
|
+
suggestions.push("- This config appears to import v1 framework packages; replace those imports with framework booleans.");
|
|
543
|
+
}
|
|
544
|
+
if (!write) return applied;
|
|
545
|
+
const result = migrateConfigContent(configContent);
|
|
546
|
+
if (result.changed) {
|
|
547
|
+
const backupPath = `${configPath}.bak`;
|
|
548
|
+
writeFileSync(backupPath, configContent);
|
|
549
|
+
writeFileSync(configPath, result.content);
|
|
550
|
+
const message = `Rewrote ${basename(configPath)} with v2 framework booleans: ${result.frameworks.join(", ")}. Original backed up to ${basename(backupPath)}.`;
|
|
551
|
+
applied.push(message);
|
|
552
|
+
suggestions.push(`- ${message}`);
|
|
83
553
|
} else {
|
|
84
|
-
|
|
554
|
+
suggestions.push("- No v1 framework imports were found to rewrite.");
|
|
85
555
|
}
|
|
556
|
+
return applied;
|
|
557
|
+
};
|
|
558
|
+
const handleMigrate = (cwd = process.cwd(), write = false, json = false) => {
|
|
559
|
+
const configPath = getConfigPathIfPresent(cwd) ?? join(cwd, getDefaultConfigFilename(cwd));
|
|
560
|
+
const suggestions = [
|
|
561
|
+
"v1 to v2 migration suggestions:",
|
|
562
|
+
"- Install only @santi020k/eslint-config-basic for the public config API.",
|
|
563
|
+
"- Replace framework imports with bundled booleans such as frameworks: { react: true, next: true }.",
|
|
564
|
+
"- Remove app-level @santi020k/eslint-config-react/next/vue/etc. config package imports.",
|
|
565
|
+
"- Try eslintConfig() first; v2 auto-detects supported frameworks and integrations.",
|
|
566
|
+
"- Use basic-eslint explain to review what v2 detects before committing the migration."
|
|
567
|
+
];
|
|
568
|
+
const applied = processConfigMigration(configPath, write, suggestions);
|
|
569
|
+
if (json) {
|
|
570
|
+
console.log(JSON.stringify({
|
|
571
|
+
applied,
|
|
572
|
+
configFile: existsSync(configPath) ? basename(configPath) : null,
|
|
573
|
+
suggestions: suggestions.slice(1),
|
|
574
|
+
title: suggestions[0],
|
|
575
|
+
write
|
|
576
|
+
}, null, 2));
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
console.log(suggestions.join("\n"));
|
|
580
|
+
};
|
|
581
|
+
const dispatchCommand = (command, cwd, flags) => {
|
|
582
|
+
switch (command) {
|
|
583
|
+
case "docs": {
|
|
584
|
+
handleDocs(cwd);
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
case "doctor": {
|
|
588
|
+
handleDoctor(cwd, flags.hasJson, flags.hasLiteInstall).catch((error) => {
|
|
589
|
+
console.error(`\u274C Failed to run doctor: ${String(error)}`);
|
|
590
|
+
process.exitCode = 1;
|
|
591
|
+
});
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
case "explain": {
|
|
595
|
+
handleExplain(cwd, flags.hasJson);
|
|
596
|
+
break;
|
|
597
|
+
}
|
|
598
|
+
case "generate-skill": {
|
|
599
|
+
handleGenerateSkill(cwd, flags.hasForce, { check: flags.hasCheck, createAgentsMd: flags.hasCreate }).catch((error) => {
|
|
600
|
+
console.error(`\u274C Failed to generate skill files: ${String(error)}`);
|
|
601
|
+
process.exitCode = 1;
|
|
602
|
+
});
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
case "init": {
|
|
606
|
+
handleInit(cwd, flags.hasCheck);
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
609
|
+
case "inspect": {
|
|
610
|
+
handleInspect(cwd, flags.hasJson).catch((error) => {
|
|
611
|
+
console.error(`\u274C Failed to inspect project: ${String(error)}`);
|
|
612
|
+
process.exitCode = 1;
|
|
613
|
+
});
|
|
614
|
+
break;
|
|
615
|
+
}
|
|
616
|
+
case "migrate": {
|
|
617
|
+
handleMigrate(cwd, flags.hasWrite, flags.hasJson);
|
|
618
|
+
break;
|
|
619
|
+
}
|
|
620
|
+
case "update": {
|
|
621
|
+
handleUpdate(cwd);
|
|
622
|
+
break;
|
|
623
|
+
}
|
|
624
|
+
default: {
|
|
625
|
+
console.error(`Unknown command: ${command}`);
|
|
626
|
+
printUsage();
|
|
627
|
+
process.exitCode = 1;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
const runCli = (argv = process.argv, cwd = process.cwd()) => {
|
|
632
|
+
const command = argv[2];
|
|
633
|
+
const isHelp = command === "--help" || command === "-h";
|
|
634
|
+
const isVersion = command === "--version" || command === "-v";
|
|
635
|
+
if (!command || isHelp) {
|
|
636
|
+
printUsage();
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
if (isVersion) {
|
|
640
|
+
console.log(getCliVersion());
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
dispatchCommand(command, cwd, {
|
|
644
|
+
hasCheck: argv.includes("--check"),
|
|
645
|
+
hasCreate: argv.includes("--create"),
|
|
646
|
+
hasForce: argv.includes("--force"),
|
|
647
|
+
hasJson: argv.includes("--json"),
|
|
648
|
+
hasLiteInstall: argv.includes("--lite-install"),
|
|
649
|
+
hasWrite: argv.includes("--write")
|
|
650
|
+
});
|
|
86
651
|
};
|
|
87
652
|
if (process.argv[1] && (process.argv[1] === fileURLToPath(import.meta.url) || process.argv[1].endsWith("cli.js") || process.argv[1].endsWith("cli.ts"))) {
|
|
88
|
-
|
|
653
|
+
runCli();
|
|
89
654
|
}
|
|
90
655
|
export {
|
|
656
|
+
findDuplicateEslint,
|
|
657
|
+
handleDocs,
|
|
658
|
+
handleDoctor,
|
|
659
|
+
handleExplain,
|
|
91
660
|
handleInit,
|
|
92
|
-
|
|
661
|
+
handleInspect,
|
|
662
|
+
handleMigrate,
|
|
663
|
+
handleUpdate,
|
|
664
|
+
runCli
|
|
93
665
|
};
|