@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
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { detectProjectOptions } from "./index.js";
|
|
5
|
+
const AGENT_TARGETS = [
|
|
6
|
+
{
|
|
7
|
+
format: "frontmatter",
|
|
8
|
+
label: ".agent (generic skill format)",
|
|
9
|
+
markerFolder: ".agent",
|
|
10
|
+
skillFile: "eslint-standards.md",
|
|
11
|
+
skillSubdir: "skills"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
format: "frontmatter",
|
|
15
|
+
label: ".agents (generic skill format)",
|
|
16
|
+
markerFolder: ".agents",
|
|
17
|
+
skillFile: "eslint-standards.md",
|
|
18
|
+
skillSubdir: "skills"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
format: "plain",
|
|
22
|
+
label: "Claude Code (.claude/commands)",
|
|
23
|
+
markerFolder: ".claude",
|
|
24
|
+
skillFile: "eslint.md",
|
|
25
|
+
skillSubdir: "commands"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
format: "cursor",
|
|
29
|
+
label: "Cursor (.cursor/rules)",
|
|
30
|
+
markerFolder: ".cursor",
|
|
31
|
+
skillFile: "eslint-standards.mdc",
|
|
32
|
+
skillSubdir: "rules"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
format: "frontmatter",
|
|
36
|
+
label: "Windsurf (.windsurf/rules)",
|
|
37
|
+
markerFolder: ".windsurf",
|
|
38
|
+
skillFile: "eslint-standards.md",
|
|
39
|
+
skillSubdir: "rules"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
format: "plain",
|
|
43
|
+
label: "Copilot (.copilot)",
|
|
44
|
+
markerFolder: ".copilot",
|
|
45
|
+
skillFile: "eslint-standards.md",
|
|
46
|
+
skillSubdir: "instructions"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
format: "plain",
|
|
50
|
+
label: "Aider (.aider)",
|
|
51
|
+
markerFolder: ".aider",
|
|
52
|
+
skillFile: "eslint-standards.md",
|
|
53
|
+
skillSubdir: "."
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
format: "plain",
|
|
57
|
+
label: "Gemini (.gemini)",
|
|
58
|
+
markerFolder: ".gemini",
|
|
59
|
+
skillFile: "styleguide.md",
|
|
60
|
+
skillSubdir: "."
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
format: "plain",
|
|
64
|
+
label: "Cline (.clinerules)",
|
|
65
|
+
markerFolder: ".clinerules",
|
|
66
|
+
skillFile: "eslint-standards.md",
|
|
67
|
+
skillSubdir: "."
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
format: "plain",
|
|
71
|
+
label: "Roo Code (.roo/rules)",
|
|
72
|
+
markerFolder: ".roo",
|
|
73
|
+
skillFile: "eslint-standards.md",
|
|
74
|
+
skillSubdir: "rules"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
format: "kiro",
|
|
78
|
+
label: "Kiro (.kiro/steering)",
|
|
79
|
+
markerFolder: ".kiro",
|
|
80
|
+
skillFile: "eslint-standards.md",
|
|
81
|
+
skillSubdir: "steering"
|
|
82
|
+
}
|
|
83
|
+
];
|
|
84
|
+
const FEATURE_MAP = [
|
|
85
|
+
// TypeScript
|
|
86
|
+
["eslint-config-typescript/", "typescript", "TypeScript"],
|
|
87
|
+
["@typescript-eslint", "typescript", "TypeScript"],
|
|
88
|
+
// Frameworks
|
|
89
|
+
["eslint-config-react/", "frameworks", "React"],
|
|
90
|
+
["eslint-config-next/", "frameworks", "Next.js"],
|
|
91
|
+
["eslint-config-vue/", "frameworks", "Vue"],
|
|
92
|
+
["eslint-config-svelte/", "frameworks", "Svelte"],
|
|
93
|
+
["eslint-config-astro/", "frameworks", "Astro"],
|
|
94
|
+
["eslint-config-angular/", "frameworks", "Angular"],
|
|
95
|
+
["eslint-config-nest/", "frameworks", "NestJS"],
|
|
96
|
+
["eslint-config-hono/", "frameworks", "Hono"],
|
|
97
|
+
["eslint-config-expo/", "frameworks", "Expo"],
|
|
98
|
+
["eslint-config-qwik/", "frameworks", "Qwik"],
|
|
99
|
+
["eslint-config-react-router/", "frameworks", "React Router"],
|
|
100
|
+
["eslint-config-remix/", "frameworks", "Remix"],
|
|
101
|
+
["eslint-config-solid/", "frameworks", "SolidJS"],
|
|
102
|
+
["eslint-config-nuxt/", "frameworks", "Nuxt"],
|
|
103
|
+
["eslint-config-lit/", "frameworks", "Lit"],
|
|
104
|
+
["eslint-config-tanstack-start/", "frameworks", "TanStack Start"],
|
|
105
|
+
["eslint-config-preact/", "frameworks", "Preact"],
|
|
106
|
+
["eslint-config-slidev/", "frameworks", "Slidev"],
|
|
107
|
+
["eslint-config-vite/", "frameworks", "Vite"],
|
|
108
|
+
// Testing
|
|
109
|
+
["integrations/vitest", "testing", "Vitest"],
|
|
110
|
+
["integrations/jest", "testing", "Jest"],
|
|
111
|
+
["eslint-config-integrations/jest-dom", "testing", "Jest DOM"],
|
|
112
|
+
["integrations/playwright", "testing", "Playwright"],
|
|
113
|
+
["integrations/cypress", "testing", "Cypress"],
|
|
114
|
+
["integrations/testing-library", "testing", "Testing Library"],
|
|
115
|
+
// Tools
|
|
116
|
+
["eslint-config/prettier", "tools", "Prettier"],
|
|
117
|
+
["integrations/cspell", "tools", "CSpell"],
|
|
118
|
+
["eslint-config-integrations/jsdoc", "tools", "JSDoc"],
|
|
119
|
+
["integrations/swagger", "tools", "Swagger"],
|
|
120
|
+
["eslint-config-integrations/command", "tools", "Command"],
|
|
121
|
+
["eslint-config-integrations/docker", "tools", "Docker"],
|
|
122
|
+
["eslint-config-integrations/github-actions", "tools", "GitHub Actions"],
|
|
123
|
+
["eslint-config-integrations/nx", "tools", "Nx"],
|
|
124
|
+
["integrations/pnpm/", "tools", "pnpm"],
|
|
125
|
+
// Libraries
|
|
126
|
+
["eslint-config-integrations/ai-sdk", "libraries", "AI SDK"],
|
|
127
|
+
["eslint-config-integrations/mcp", "libraries", "MCP"],
|
|
128
|
+
["eslint-config-integrations/mastra", "libraries", "Mastra"],
|
|
129
|
+
["eslint-config-integrations/openai-agents", "libraries", "OpenAI Agents"],
|
|
130
|
+
["eslint-config-integrations/langchain", "libraries", "LangChain"],
|
|
131
|
+
["eslint-config-integrations/llamaindex", "libraries", "LlamaIndex"],
|
|
132
|
+
["santi020k/tailwind/", "libraries", "Tailwind CSS"],
|
|
133
|
+
["integrations/i18next", "libraries", "i18next"],
|
|
134
|
+
["integrations/stencil", "libraries", "Stencil"],
|
|
135
|
+
["storybook:recommended", "libraries", "Storybook"],
|
|
136
|
+
["eslint-config-integrations/tanstack-query", "libraries", "TanStack Query"],
|
|
137
|
+
["eslint-config-integrations/tanstack-router", "libraries", "TanStack Router"],
|
|
138
|
+
["eslint-config-integrations/autogen", "libraries", "Autogen"],
|
|
139
|
+
["eslint-config-integrations/google-genai", "libraries", "Google GenAI"],
|
|
140
|
+
["eslint-config-integrations/drizzle", "libraries", "Drizzle ORM"],
|
|
141
|
+
["eslint-config-integrations/prisma", "libraries", "Prisma"],
|
|
142
|
+
["eslint-config-integrations/typeorm", "libraries", "TypeORM"],
|
|
143
|
+
["eslint-config-integrations/mikro-orm", "libraries", "MikroORM"],
|
|
144
|
+
["eslint-config-integrations/sequelize", "libraries", "Sequelize"],
|
|
145
|
+
["eslint-config-integrations/zod", "libraries", "Zod"],
|
|
146
|
+
["eslint-config-turbo/", "libraries", "Turbo"],
|
|
147
|
+
// Formats
|
|
148
|
+
["integrations/css/", "formats", "CSS"],
|
|
149
|
+
["integrations/html/", "formats", "HTML"],
|
|
150
|
+
["eslint-config-integrations/package-json", "formats", "Package JSON"],
|
|
151
|
+
["integrations/graphql/", "formats", "GraphQL"],
|
|
152
|
+
["integrations/yaml/", "formats", "YAML"],
|
|
153
|
+
["integrations/jsonc/", "formats", "JSONC"],
|
|
154
|
+
["integrations/markdown", "formats", "Markdown"],
|
|
155
|
+
["eslint-config-mdx/", "formats", "MDX"],
|
|
156
|
+
["integrations/toml/", "formats", "TOML"],
|
|
157
|
+
// Extensions
|
|
158
|
+
["eslint-config/unicorn", "extensions", "Unicorn"],
|
|
159
|
+
["eslint-config/sonarjs", "extensions", "SonarJS"],
|
|
160
|
+
["eslint-config-integrations/security", "extensions", "Security"],
|
|
161
|
+
["eslint-config-integrations/perfectionist", "extensions", "Perfectionist"],
|
|
162
|
+
["integrations/regexp", "extensions", "Regexp"],
|
|
163
|
+
["eslint-config/best-practices", "extensions", "Best Practices"],
|
|
164
|
+
["eslint-config-integrations/a11y/", "extensions", "Accessibility (a11y)"],
|
|
165
|
+
["eslint-config-integrations/biome", "extensions", "Biome"],
|
|
166
|
+
["eslint-config-integrations/import-boundaries", "extensions", "Import Boundaries"],
|
|
167
|
+
["integrations/compat/", "extensions", "Browser Compat"],
|
|
168
|
+
["integrations/de-morgan/", "extensions", "De Morgan"],
|
|
169
|
+
["integrations/depend/", "extensions", "Depend"],
|
|
170
|
+
["integrations/node/", "extensions", "Node.js"],
|
|
171
|
+
["eslint-config-integrations/no-only-tests", "extensions", "No Only Tests"],
|
|
172
|
+
["integrations/oxlint", "extensions", "Oxlint"]
|
|
173
|
+
];
|
|
174
|
+
const DETECTED_FRAMEWORK_LABELS = /* @__PURE__ */ new Map([
|
|
175
|
+
["angular", "Angular"],
|
|
176
|
+
["astro", "Astro"],
|
|
177
|
+
["expo", "Expo"],
|
|
178
|
+
["hono", "Hono"],
|
|
179
|
+
["lit", "Lit"],
|
|
180
|
+
["nest", "NestJS"],
|
|
181
|
+
["next", "Next.js"],
|
|
182
|
+
["nuxt", "Nuxt"],
|
|
183
|
+
["preact", "Preact"],
|
|
184
|
+
["qwik", "Qwik"],
|
|
185
|
+
["react", "React"],
|
|
186
|
+
["react-router", "React Router"],
|
|
187
|
+
["remix", "Remix"],
|
|
188
|
+
["slidev", "Slidev"],
|
|
189
|
+
["solid", "SolidJS"],
|
|
190
|
+
["svelte", "Svelte"],
|
|
191
|
+
["tanstack-start", "TanStack Start"],
|
|
192
|
+
["vite", "Vite"],
|
|
193
|
+
["vue", "Vue"]
|
|
194
|
+
]);
|
|
195
|
+
const FEATURE_LABELS = /* @__PURE__ */ new Map([
|
|
196
|
+
["best-practices", "Best Practices"],
|
|
197
|
+
["compat", "Browser Compat"],
|
|
198
|
+
["cspell", "CSpell"],
|
|
199
|
+
["css", "CSS"],
|
|
200
|
+
["cypress", "Cypress"],
|
|
201
|
+
["de-morgan", "De Morgan"],
|
|
202
|
+
["depend", "Depend"],
|
|
203
|
+
["graphql", "GraphQL"],
|
|
204
|
+
["html", "HTML"],
|
|
205
|
+
["i18next", "i18next"],
|
|
206
|
+
["jest", "Jest"],
|
|
207
|
+
["jsdoc", "JSDoc"],
|
|
208
|
+
["jsonc", "JSONC"],
|
|
209
|
+
["markdown", "Markdown"],
|
|
210
|
+
["mdx", "MDX"],
|
|
211
|
+
["node", "Node.js"],
|
|
212
|
+
["oxlint", "Oxlint"],
|
|
213
|
+
["perfectionist", "Perfectionist"],
|
|
214
|
+
["playwright", "Playwright"],
|
|
215
|
+
["pnpm", "pnpm"],
|
|
216
|
+
["prettier", "Prettier"],
|
|
217
|
+
["regexp", "Regexp"],
|
|
218
|
+
["security", "Security"],
|
|
219
|
+
["sonarjs", "SonarJS"],
|
|
220
|
+
["stencil", "Stencil"],
|
|
221
|
+
["storybook", "Storybook"],
|
|
222
|
+
["swagger", "Swagger"],
|
|
223
|
+
["tailwind", "Tailwind CSS"],
|
|
224
|
+
["tanstack-query", "TanStack Query"],
|
|
225
|
+
["tanstack-router", "TanStack Router"],
|
|
226
|
+
["testing-library", "Testing Library"],
|
|
227
|
+
["toml", "TOML"],
|
|
228
|
+
["unicorn", "Unicorn"],
|
|
229
|
+
["vitest", "Vitest"],
|
|
230
|
+
["yaml", "YAML"]
|
|
231
|
+
]);
|
|
232
|
+
const toFeatureLabel = (value) => FEATURE_LABELS.get(value) ?? value;
|
|
233
|
+
const extractRuleNamespaces = (rules) => Object.keys(rules).filter((key) => key.includes("/")).map((key) => key.slice(0, key.indexOf("/")));
|
|
234
|
+
const collectTokens = (configs) => {
|
|
235
|
+
const tokens = [];
|
|
236
|
+
for (const entry of configs) {
|
|
237
|
+
if (!entry || typeof entry !== "object") continue;
|
|
238
|
+
const cfg = entry;
|
|
239
|
+
if (typeof cfg.name === "string") {
|
|
240
|
+
tokens.push(cfg.name);
|
|
241
|
+
}
|
|
242
|
+
if (cfg.plugins && typeof cfg.plugins === "object") {
|
|
243
|
+
tokens.push(...Object.keys(cfg.plugins));
|
|
244
|
+
}
|
|
245
|
+
if (cfg.rules && typeof cfg.rules === "object") {
|
|
246
|
+
tokens.push(...extractRuleNamespaces(cfg.rules));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return tokens;
|
|
250
|
+
};
|
|
251
|
+
const recordFeature = (features, category, label) => {
|
|
252
|
+
if (category === "typescript") {
|
|
253
|
+
features.typescript = true;
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
switch (category) {
|
|
257
|
+
case "extensions":
|
|
258
|
+
features.extensions.push(label);
|
|
259
|
+
break;
|
|
260
|
+
case "formats":
|
|
261
|
+
features.formats.push(label);
|
|
262
|
+
break;
|
|
263
|
+
case "frameworks":
|
|
264
|
+
features.frameworks.push(label);
|
|
265
|
+
break;
|
|
266
|
+
case "libraries":
|
|
267
|
+
features.libraries.push(label);
|
|
268
|
+
break;
|
|
269
|
+
case "testing":
|
|
270
|
+
features.testing.push(label);
|
|
271
|
+
break;
|
|
272
|
+
case "tools":
|
|
273
|
+
features.tools.push(label);
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
const extractFeatures = (configs, lintCommand, configFile) => {
|
|
278
|
+
const tokens = collectTokens(configs);
|
|
279
|
+
const features = {
|
|
280
|
+
configFile,
|
|
281
|
+
extensions: [],
|
|
282
|
+
formats: [],
|
|
283
|
+
frameworks: [],
|
|
284
|
+
libraries: [],
|
|
285
|
+
lintCommand,
|
|
286
|
+
source: "config-file",
|
|
287
|
+
testing: [],
|
|
288
|
+
tools: [],
|
|
289
|
+
typescript: false
|
|
290
|
+
};
|
|
291
|
+
const seen = /* @__PURE__ */ new Set();
|
|
292
|
+
for (const [pattern, category, label] of FEATURE_MAP) {
|
|
293
|
+
if (seen.has(label)) continue;
|
|
294
|
+
const matched = tokens.some((t) => t.includes(pattern));
|
|
295
|
+
if (!matched) continue;
|
|
296
|
+
seen.add(label);
|
|
297
|
+
recordFeature(features, category, label);
|
|
298
|
+
}
|
|
299
|
+
return features;
|
|
300
|
+
};
|
|
301
|
+
const findEslintConfig = (cwd) => {
|
|
302
|
+
for (const name of ["eslint.config.js", "eslint.config.mjs", "eslint.config.cjs"]) {
|
|
303
|
+
const p = join(cwd, name);
|
|
304
|
+
if (existsSync(p)) return p;
|
|
305
|
+
}
|
|
306
|
+
return null;
|
|
307
|
+
};
|
|
308
|
+
const detectLintCommand = (cwd) => {
|
|
309
|
+
let pkgManager = "npm";
|
|
310
|
+
if (existsSync(join(cwd, "pnpm-lock.yaml")) || existsSync(join(cwd, "pnpm-workspace.yaml"))) {
|
|
311
|
+
pkgManager = "pnpm";
|
|
312
|
+
} else if (existsSync(join(cwd, "yarn.lock"))) {
|
|
313
|
+
pkgManager = "yarn";
|
|
314
|
+
} else if (existsSync(join(cwd, "bun.lockb")) || existsSync(join(cwd, "bun.lock"))) {
|
|
315
|
+
pkgManager = "bun";
|
|
316
|
+
}
|
|
317
|
+
const pkgPath = join(cwd, "package.json");
|
|
318
|
+
try {
|
|
319
|
+
if (existsSync(pkgPath)) {
|
|
320
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
321
|
+
if (pkg.scripts?.lint) return `${pkgManager} run lint`;
|
|
322
|
+
}
|
|
323
|
+
} catch {
|
|
324
|
+
}
|
|
325
|
+
return "npx eslint .";
|
|
326
|
+
};
|
|
327
|
+
const analyzeEslintConfig = async (cwd) => {
|
|
328
|
+
const configPath = findEslintConfig(cwd);
|
|
329
|
+
if (!configPath) return null;
|
|
330
|
+
const lintCommand = detectLintCommand(cwd);
|
|
331
|
+
try {
|
|
332
|
+
const mod = await import(pathToFileURL(resolve(configPath)).href);
|
|
333
|
+
const raw = mod.default ?? mod;
|
|
334
|
+
let configs = null;
|
|
335
|
+
if (Array.isArray(raw)) {
|
|
336
|
+
configs = raw;
|
|
337
|
+
} else if (raw instanceof Promise) {
|
|
338
|
+
configs = await raw;
|
|
339
|
+
}
|
|
340
|
+
if (!Array.isArray(configs)) return null;
|
|
341
|
+
return extractFeatures(configs, lintCommand, configPath);
|
|
342
|
+
} catch {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
const featuresFromDetection = (cwd) => {
|
|
347
|
+
const opts = detectProjectOptions(cwd);
|
|
348
|
+
const lintCommand = detectLintCommand(cwd);
|
|
349
|
+
const frameworks = (opts.detectedFrameworks ?? []).map((f) => DETECTED_FRAMEWORK_LABELS.get(f) ?? f);
|
|
350
|
+
return {
|
|
351
|
+
configFile: null,
|
|
352
|
+
extensions: (opts.extensions ?? []).map(toFeatureLabel),
|
|
353
|
+
formats: (opts.formats ?? []).map(toFeatureLabel),
|
|
354
|
+
frameworks,
|
|
355
|
+
libraries: (opts.libraries ?? []).map(toFeatureLabel),
|
|
356
|
+
lintCommand,
|
|
357
|
+
source: "detection-fallback",
|
|
358
|
+
testing: (opts.testing ?? []).map(toFeatureLabel),
|
|
359
|
+
tools: (opts.tools ?? []).map(toFeatureLabel),
|
|
360
|
+
typescript: opts.typescript === true
|
|
361
|
+
};
|
|
362
|
+
};
|
|
363
|
+
const buildSummaryLines = (features) => {
|
|
364
|
+
const { extensions, formats, frameworks, libraries, testing, tools, typescript } = features;
|
|
365
|
+
const lines = [];
|
|
366
|
+
lines.push(`- **TypeScript**: ${typescript ? "enabled" : "disabled"}`);
|
|
367
|
+
if (frameworks.length > 0) lines.push(`- **Frameworks**: ${frameworks.join(", ")}`);
|
|
368
|
+
if (testing.length > 0) lines.push(`- **Testing**: ${testing.join(", ")}`);
|
|
369
|
+
if (tools.length > 0) lines.push(`- **Tools**: ${tools.join(", ")}`);
|
|
370
|
+
if (libraries.length > 0) lines.push(`- **Libraries**: ${libraries.join(", ")}`);
|
|
371
|
+
if (formats.length > 0) lines.push(`- **Formats**: ${formats.join(", ")}`);
|
|
372
|
+
if (extensions.length > 0) lines.push(`- **Extensions**: ${extensions.join(", ")}`);
|
|
373
|
+
return lines;
|
|
374
|
+
};
|
|
375
|
+
const buildFrameworkSections = (frameworks) => {
|
|
376
|
+
const sections = [];
|
|
377
|
+
if (frameworks.some((f) => f === "React" || f === "Next.js")) {
|
|
378
|
+
sections.push(`
|
|
379
|
+
### React / JSX
|
|
380
|
+
|
|
381
|
+
- Prefer function components over class components
|
|
382
|
+
- Hooks must follow the Rules of Hooks (no conditional calls, no loops)
|
|
383
|
+
- Avoid inline arrow functions in JSX props where it hurts readability
|
|
384
|
+
- Use \`key\` props when rendering lists
|
|
385
|
+
`);
|
|
386
|
+
}
|
|
387
|
+
if (frameworks.includes("Next.js")) {
|
|
388
|
+
sections.push(`
|
|
389
|
+
### Next.js
|
|
390
|
+
|
|
391
|
+
- Follow App Router conventions (Server Components by default, \`"use client"\` when needed)
|
|
392
|
+
- Avoid \`<img>\` \u2014 use \`next/image\`
|
|
393
|
+
- Avoid \`<a>\` for internal navigation \u2014 use \`next/link\`
|
|
394
|
+
`);
|
|
395
|
+
}
|
|
396
|
+
if (frameworks.includes("Vue")) {
|
|
397
|
+
sections.push(`
|
|
398
|
+
### Vue
|
|
399
|
+
|
|
400
|
+
- Prefer Composition API (\`<script setup>\`) over Options API
|
|
401
|
+
- Use single-word or multi-word component names consistently
|
|
402
|
+
`);
|
|
403
|
+
}
|
|
404
|
+
if (frameworks.includes("Svelte")) {
|
|
405
|
+
sections.push(`
|
|
406
|
+
### Svelte
|
|
407
|
+
|
|
408
|
+
- Virtual \`*.svelte/*.ts\` files are handled by the ESLint config \u2014 do not manually adjust \`allowDefaultProject\`
|
|
409
|
+
`);
|
|
410
|
+
}
|
|
411
|
+
if (frameworks.includes("Astro")) {
|
|
412
|
+
sections.push(`
|
|
413
|
+
### Astro
|
|
414
|
+
|
|
415
|
+
- Virtual \`*.astro/*.ts\` files are handled by the ESLint config
|
|
416
|
+
- Prefer Astro components over framework components when no interactivity is needed
|
|
417
|
+
`);
|
|
418
|
+
}
|
|
419
|
+
if (frameworks.includes("Angular")) {
|
|
420
|
+
sections.push(`
|
|
421
|
+
### Angular
|
|
422
|
+
|
|
423
|
+
- Follow the Angular style guide component, directive, and service naming conventions
|
|
424
|
+
- Use standalone components by default; avoid NgModule unless required
|
|
425
|
+
`);
|
|
426
|
+
}
|
|
427
|
+
return sections;
|
|
428
|
+
};
|
|
429
|
+
const wrapWithFormat = (body, format) => {
|
|
430
|
+
if (format === "frontmatter") {
|
|
431
|
+
return `---
|
|
432
|
+
name: eslint-standards
|
|
433
|
+
description: >
|
|
434
|
+
Code quality standards enforced by @santi020k/eslint-config-basic. Follow these
|
|
435
|
+
conventions whenever writing or editing code in this project.
|
|
436
|
+
trigger: always_on
|
|
437
|
+
---
|
|
438
|
+
|
|
439
|
+
${body}`;
|
|
440
|
+
}
|
|
441
|
+
if (format === "cursor") {
|
|
442
|
+
return `---
|
|
443
|
+
description: >
|
|
444
|
+
ESLint code standards for this project. Apply these conventions when writing
|
|
445
|
+
or editing any source file.
|
|
446
|
+
globs:
|
|
447
|
+
- "**/*.ts"
|
|
448
|
+
- "**/*.tsx"
|
|
449
|
+
- "**/*.js"
|
|
450
|
+
- "**/*.jsx"
|
|
451
|
+
- "**/*.vue"
|
|
452
|
+
- "**/*.svelte"
|
|
453
|
+
- "**/*.astro"
|
|
454
|
+
alwaysApply: true
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
${body}`;
|
|
458
|
+
}
|
|
459
|
+
if (format === "kiro") {
|
|
460
|
+
return `---
|
|
461
|
+
inclusion: always
|
|
462
|
+
---
|
|
463
|
+
|
|
464
|
+
${body}`;
|
|
465
|
+
}
|
|
466
|
+
return body;
|
|
467
|
+
};
|
|
468
|
+
const generateSkillContent = (features, format) => {
|
|
469
|
+
const { frameworks, libraries, lintCommand, testing, tools, typescript } = features;
|
|
470
|
+
const summaryLines = buildSummaryLines(features);
|
|
471
|
+
const tsSection = typescript ? `
|
|
472
|
+
### TypeScript
|
|
473
|
+
|
|
474
|
+
- Use explicit type annotations on all exported functions and values
|
|
475
|
+
- Use \`import type\` for type-only imports
|
|
476
|
+
- Avoid \`any\` \u2014 prefer \`unknown\` with type guards
|
|
477
|
+
- Prefer \`const\` over \`let\` whenever the binding is not reassigned
|
|
478
|
+
- Return types must be explicit on exported functions
|
|
479
|
+
` : "";
|
|
480
|
+
const frameworkSections = buildFrameworkSections(frameworks);
|
|
481
|
+
const testingSections = [];
|
|
482
|
+
if (testing.some((t) => t === "Vitest" || t === "Jest")) {
|
|
483
|
+
testingSections.push(`
|
|
484
|
+
### Tests
|
|
485
|
+
|
|
486
|
+
- Name test files \`*.test.ts\` or \`*.spec.ts\`
|
|
487
|
+
- Use \`describe\` blocks to group related cases
|
|
488
|
+
- Avoid testing implementation details \u2014 test observable behaviour
|
|
489
|
+
`);
|
|
490
|
+
}
|
|
491
|
+
if (testing.includes("Playwright")) {
|
|
492
|
+
testingSections.push(`
|
|
493
|
+
### Playwright
|
|
494
|
+
|
|
495
|
+
- Place tests in a dedicated \`e2e/\` or \`tests/\` directory
|
|
496
|
+
- Use \`page.getByRole\` / \`page.getByText\` over CSS selectors where possible
|
|
497
|
+
`);
|
|
498
|
+
}
|
|
499
|
+
const toolSections = [];
|
|
500
|
+
if (tools.includes("Prettier")) {
|
|
501
|
+
toolSections.push(`
|
|
502
|
+
### Formatting
|
|
503
|
+
|
|
504
|
+
- Do not manually format \u2014 Prettier handles all formatting
|
|
505
|
+
- Prettier runs as the **last** ESLint config; never override its rules
|
|
506
|
+
`);
|
|
507
|
+
}
|
|
508
|
+
if (tools.includes("Tailwind CSS") || libraries.includes("Tailwind CSS")) {
|
|
509
|
+
toolSections.push(`
|
|
510
|
+
### Tailwind CSS
|
|
511
|
+
|
|
512
|
+
- Use Tailwind utility classes directly in templates rather than hand-written CSS where possible
|
|
513
|
+
- The \`better-tailwindcss\` plugin enforces ordering; do not disable class-ordering rules
|
|
514
|
+
`);
|
|
515
|
+
}
|
|
516
|
+
const body = `# ESLint Code Standards
|
|
517
|
+
|
|
518
|
+
This project uses [\`@santi020k/eslint-config-basic\`](https://github.com/santi020k/eslint-config-basic) \u2014
|
|
519
|
+
a composable ESLint 10+ Flat Config package.
|
|
520
|
+
|
|
521
|
+
**Always run \`${lintCommand}\` to validate your changes before finishing any task.**
|
|
522
|
+
|
|
523
|
+
## Active Configuration
|
|
524
|
+
|
|
525
|
+
${summaryLines.join("\n")}
|
|
526
|
+
|
|
527
|
+
## Conventions
|
|
528
|
+
${tsSection}${frameworkSections.join("")}${testingSections.join("")}${toolSections.join("")}
|
|
529
|
+
### General
|
|
530
|
+
|
|
531
|
+
- ESLint 10 **Flat Config** format only \u2014 no \`.eslintrc\` files
|
|
532
|
+
- Use \`.js\` extensions on relative imports (ESM requirement)
|
|
533
|
+
- Use \`type\` imports for type-only values; regular imports for runtime values
|
|
534
|
+
|
|
535
|
+
## Verification
|
|
536
|
+
|
|
537
|
+
\`\`\`bash
|
|
538
|
+
${lintCommand}
|
|
539
|
+
\`\`\`
|
|
540
|
+
|
|
541
|
+
If the command is unavailable, fall back to:
|
|
542
|
+
|
|
543
|
+
\`\`\`bash
|
|
544
|
+
npx eslint .
|
|
545
|
+
\`\`\`
|
|
546
|
+
`;
|
|
547
|
+
return wrapWithFormat(body, format);
|
|
548
|
+
};
|
|
549
|
+
const writeSkillFile = (filePath, content, force) => {
|
|
550
|
+
if (existsSync(filePath) && !force) return false;
|
|
551
|
+
mkdirSync(join(filePath, ".."), { recursive: true });
|
|
552
|
+
writeFileSync(filePath, content, "utf-8");
|
|
553
|
+
return true;
|
|
554
|
+
};
|
|
555
|
+
const COPILOT_INSTRUCTIONS_PATH = ".github/copilot-instructions.md";
|
|
556
|
+
const AGENTS_MD_PATH = "AGENTS.md";
|
|
557
|
+
const COPILOT_SECTION_START = "<!-- eslint-standards:start -->";
|
|
558
|
+
const COPILOT_SECTION_END = "<!-- eslint-standards:end -->";
|
|
559
|
+
const buildGuardedSection = (body) => `${COPILOT_SECTION_START}
|
|
560
|
+
${body}
|
|
561
|
+
${COPILOT_SECTION_END}`;
|
|
562
|
+
const handleGuardedSectionFile = (filePath, body, force, check, createIfMissing = false) => {
|
|
563
|
+
const section = buildGuardedSection(body);
|
|
564
|
+
if (!existsSync(filePath)) {
|
|
565
|
+
if (!createIfMissing || check) return null;
|
|
566
|
+
writeFileSync(filePath, `# Agent instructions
|
|
567
|
+
|
|
568
|
+
${section}
|
|
569
|
+
`, "utf-8");
|
|
570
|
+
return "written";
|
|
571
|
+
}
|
|
572
|
+
const existing = readFileSync(filePath, "utf-8");
|
|
573
|
+
if (check) {
|
|
574
|
+
return existing.includes(section) ? "skipped" : "stale";
|
|
575
|
+
}
|
|
576
|
+
if (existing.includes(COPILOT_SECTION_START)) {
|
|
577
|
+
if (!force) return "skipped";
|
|
578
|
+
const updated = existing.replaceAll(
|
|
579
|
+
new RegExp(`${COPILOT_SECTION_START}[\\s\\S]*?${COPILOT_SECTION_END}`, "g"),
|
|
580
|
+
section
|
|
581
|
+
);
|
|
582
|
+
writeFileSync(filePath, updated, "utf-8");
|
|
583
|
+
return "written";
|
|
584
|
+
}
|
|
585
|
+
writeFileSync(filePath, `${existing}
|
|
586
|
+
${section}
|
|
587
|
+
`, "utf-8");
|
|
588
|
+
return "written";
|
|
589
|
+
};
|
|
590
|
+
const resolveSkillOptions = (opts) => ({
|
|
591
|
+
check: opts.check ?? false,
|
|
592
|
+
createAgentsMd: opts.createAgentsMd ?? false,
|
|
593
|
+
cwd: opts.cwd ?? process.cwd(),
|
|
594
|
+
force: opts.force ?? false
|
|
595
|
+
});
|
|
596
|
+
const generateAgentSkills = async (opts = {}) => {
|
|
597
|
+
const { check, createAgentsMd, cwd, force } = resolveSkillOptions(opts);
|
|
598
|
+
const written = [];
|
|
599
|
+
const skipped = [];
|
|
600
|
+
const stale = [];
|
|
601
|
+
const features = await analyzeEslintConfig(cwd) ?? featuresFromDetection(cwd);
|
|
602
|
+
const plainBody = generateSkillContent(features, "plain");
|
|
603
|
+
const recordGuardedResult = (filePath, result) => {
|
|
604
|
+
switch (result) {
|
|
605
|
+
case "skipped":
|
|
606
|
+
skipped.push(filePath);
|
|
607
|
+
break;
|
|
608
|
+
case "stale":
|
|
609
|
+
stale.push(filePath);
|
|
610
|
+
break;
|
|
611
|
+
case "written":
|
|
612
|
+
written.push(filePath);
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
const copilotPath = join(cwd, COPILOT_INSTRUCTIONS_PATH);
|
|
617
|
+
recordGuardedResult(copilotPath, handleGuardedSectionFile(copilotPath, plainBody, force, check));
|
|
618
|
+
const agentsMdPath = join(cwd, AGENTS_MD_PATH);
|
|
619
|
+
recordGuardedResult(agentsMdPath, handleGuardedSectionFile(agentsMdPath, plainBody, force, check, createAgentsMd));
|
|
620
|
+
for (const target of AGENT_TARGETS) {
|
|
621
|
+
const agentFolder = join(cwd, target.markerFolder);
|
|
622
|
+
if (!existsSync(agentFolder)) continue;
|
|
623
|
+
const subdir = target.skillSubdir === "." ? agentFolder : join(agentFolder, target.skillSubdir);
|
|
624
|
+
const filePath = join(subdir, target.skillFile);
|
|
625
|
+
const content = generateSkillContent(features, target.format);
|
|
626
|
+
if (check) {
|
|
627
|
+
const upToDate = existsSync(filePath) && readFileSync(filePath, "utf-8") === content;
|
|
628
|
+
if (upToDate) skipped.push(filePath);
|
|
629
|
+
else stale.push(filePath);
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
const didWrite = writeSkillFile(filePath, content, force);
|
|
633
|
+
if (didWrite) written.push(filePath);
|
|
634
|
+
else skipped.push(filePath);
|
|
635
|
+
}
|
|
636
|
+
return { skipped, stale, written };
|
|
637
|
+
};
|
|
638
|
+
const resolveGenerateSkillFlags = (flags) => ({
|
|
639
|
+
check: flags.check ?? false,
|
|
640
|
+
createAgentsMd: flags.createAgentsMd ?? false
|
|
641
|
+
});
|
|
642
|
+
const outputGenerateSkillResults = (check, result, cwd) => {
|
|
643
|
+
if (check) {
|
|
644
|
+
for (const file of result.skipped) {
|
|
645
|
+
console.log(`\u2705 Up to date: ${file.replace(cwd + "/", "")}`);
|
|
646
|
+
}
|
|
647
|
+
for (const file of result.stale) {
|
|
648
|
+
console.log(`\u274C Stale or missing: ${file.replace(cwd + "/", "")}`);
|
|
649
|
+
}
|
|
650
|
+
if (result.stale.length > 0) {
|
|
651
|
+
console.log(`
|
|
652
|
+
\u26A0\uFE0F ${result.stale.length} skill file(s) are out of date. Run \`generate-skill --force\` to regenerate them.`);
|
|
653
|
+
process.exitCode = 1;
|
|
654
|
+
} else {
|
|
655
|
+
console.log("\n\u{1F389} All agent skill files are up to date!");
|
|
656
|
+
}
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
for (const file of result.written) {
|
|
660
|
+
console.log(`\u2705 Written: ${file.replace(cwd + "/", "")}`);
|
|
661
|
+
}
|
|
662
|
+
for (const file of result.skipped) {
|
|
663
|
+
console.log(`\u23ED\uFE0F Skipped (already exists): ${file.replace(cwd + "/", "")}`);
|
|
664
|
+
}
|
|
665
|
+
if (result.written.length > 0) {
|
|
666
|
+
console.log(`
|
|
667
|
+
\u{1F389} Generated ${result.written.length} skill file(s)!`);
|
|
668
|
+
console.log(" Agents will now follow your project's ESLint standards automatically.");
|
|
669
|
+
}
|
|
670
|
+
if (result.skipped.length > 0) {
|
|
671
|
+
console.log("\n\u{1F4A1} Tip: run with --force to overwrite existing skill files.");
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
const handleGenerateSkill = async (cwd = process.cwd(), force = false, flags = {}) => {
|
|
675
|
+
const { check, createAgentsMd } = resolveGenerateSkillFlags(flags);
|
|
676
|
+
console.log(check ? "\u{1F50D} Checking AI agent skill files..." : "\u{1F50D} Scanning for AI agent folders...");
|
|
677
|
+
const configFile = findEslintConfig(cwd);
|
|
678
|
+
if (configFile) {
|
|
679
|
+
console.log(`\u{1F4C4} Reading config from: ${configFile.replace(cwd + "/", "")}`);
|
|
680
|
+
} else {
|
|
681
|
+
console.log("\u26A0\uFE0F No eslint.config.js found \u2014 falling back to package.json detection.");
|
|
682
|
+
}
|
|
683
|
+
const result = await generateAgentSkills({ check, createAgentsMd, cwd, force });
|
|
684
|
+
if (result.written.length === 0 && result.skipped.length === 0 && result.stale.length === 0) {
|
|
685
|
+
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
|
+
console.log(" Create one of those folders first (or re-run with --create to scaffold AGENTS.md).");
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
outputGenerateSkillResults(check, result, cwd);
|
|
690
|
+
};
|
|
691
|
+
export {
|
|
692
|
+
AGENT_TARGETS,
|
|
693
|
+
analyzeEslintConfig,
|
|
694
|
+
generateAgentSkills,
|
|
695
|
+
generateSkillContent,
|
|
696
|
+
handleGenerateSkill
|
|
697
|
+
};
|