@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.
@@ -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
+ };