@santi020k/eslint-config-basic 1.6.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.
@@ -4,53 +4,81 @@ import { pathToFileURL } from "node:url";
4
4
  import { detectProjectOptions } from "./index.js";
5
5
  const AGENT_TARGETS = [
6
6
  {
7
+ format: "frontmatter",
7
8
  label: ".agent (generic skill format)",
8
9
  markerFolder: ".agent",
9
- skillSubdir: "skills",
10
10
  skillFile: "eslint-standards.md",
11
- format: "frontmatter"
11
+ skillSubdir: "skills"
12
12
  },
13
13
  {
14
+ format: "frontmatter",
14
15
  label: ".agents (generic skill format)",
15
16
  markerFolder: ".agents",
16
- skillSubdir: "skills",
17
17
  skillFile: "eslint-standards.md",
18
- format: "frontmatter"
18
+ skillSubdir: "skills"
19
19
  },
20
20
  {
21
+ format: "plain",
21
22
  label: "Claude Code (.claude/commands)",
22
23
  markerFolder: ".claude",
23
- skillSubdir: "commands",
24
24
  skillFile: "eslint.md",
25
- format: "plain"
25
+ skillSubdir: "commands"
26
26
  },
27
27
  {
28
+ format: "cursor",
28
29
  label: "Cursor (.cursor/rules)",
29
30
  markerFolder: ".cursor",
30
- skillSubdir: "rules",
31
31
  skillFile: "eslint-standards.mdc",
32
- format: "cursor"
32
+ skillSubdir: "rules"
33
33
  },
34
34
  {
35
+ format: "frontmatter",
35
36
  label: "Windsurf (.windsurf/rules)",
36
37
  markerFolder: ".windsurf",
37
- skillSubdir: "rules",
38
38
  skillFile: "eslint-standards.md",
39
- format: "frontmatter"
39
+ skillSubdir: "rules"
40
40
  },
41
41
  {
42
+ format: "plain",
42
43
  label: "Copilot (.copilot)",
43
44
  markerFolder: ".copilot",
44
- skillSubdir: "instructions",
45
45
  skillFile: "eslint-standards.md",
46
- format: "plain"
46
+ skillSubdir: "instructions"
47
47
  },
48
48
  {
49
+ format: "plain",
49
50
  label: "Aider (.aider)",
50
51
  markerFolder: ".aider",
51
- skillSubdir: ".",
52
52
  skillFile: "eslint-standards.md",
53
- format: "plain"
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"
54
82
  }
55
83
  ];
56
84
  const FEATURE_MAP = [
@@ -68,85 +96,141 @@ const FEATURE_MAP = [
68
96
  ["eslint-config-hono/", "frameworks", "Hono"],
69
97
  ["eslint-config-expo/", "frameworks", "Expo"],
70
98
  ["eslint-config-qwik/", "frameworks", "Qwik"],
99
+ ["eslint-config-react-router/", "frameworks", "React Router"],
71
100
  ["eslint-config-remix/", "frameworks", "Remix"],
72
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"],
73
108
  // Testing
74
- ["optionals/vitest", "testing", "Vitest"],
75
- ["optionals/jest", "testing", "Jest"],
76
- ["optionals/playwright", "testing", "Playwright"],
77
- ["optionals/cypress", "testing", "Cypress"],
78
- ["optionals/testing-library", "testing", "Testing Library"],
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"],
79
115
  // Tools
80
116
  ["eslint-config/prettier", "tools", "Prettier"],
81
- ["optionals/cspell", "tools", "CSpell"],
82
- ["eslint-config-optionals/jsdoc", "tools", "JSDoc"],
83
- ["optionals/swagger", "tools", "Swagger"],
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"],
84
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"],
85
132
  ["santi020k/tailwind/", "libraries", "Tailwind CSS"],
86
- ["optionals/i18next", "libraries", "i18next"],
87
- ["optionals/stencil", "libraries", "Stencil"],
88
- ["optionals/storybook", "libraries", "Storybook"],
89
- ["eslint-config-optionals/tanstack-query", "libraries", "TanStack Query"],
90
- ["eslint-config-optionals/tanstack-router", "libraries", "TanStack Router"],
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"],
91
147
  // Formats
92
- ["optionals/graphql/", "formats", "GraphQL"],
93
- ["optionals/yaml/", "formats", "YAML"],
94
- ["optionals/jsonc/", "formats", "JSONC"],
95
- ["optionals/markdown", "formats", "Markdown"],
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"],
96
155
  ["eslint-config-mdx/", "formats", "MDX"],
97
- ["optionals/toml/", "formats", "TOML"],
156
+ ["integrations/toml/", "formats", "TOML"],
98
157
  // Extensions
99
158
  ["eslint-config/unicorn", "extensions", "Unicorn"],
100
159
  ["eslint-config/sonarjs", "extensions", "SonarJS"],
101
- ["eslint-config-optionals/security", "extensions", "Security"],
102
- ["eslint-config-optionals/perfectionist", "extensions", "Perfectionist"],
103
- ["optionals/regexp", "extensions", "Regexp"],
104
- ["eslint-config/best-practices", "extensions", "Best Practices"]
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"]
105
173
  ];
106
- const DETECTED_FRAMEWORK_LABELS = {
107
- angular: "Angular",
108
- astro: "Astro",
109
- expo: "Expo",
110
- hono: "Hono",
111
- nest: "NestJS",
112
- next: "Next.js",
113
- qwik: "Qwik",
114
- react: "React",
115
- remix: "Remix",
116
- solid: "SolidJS",
117
- svelte: "Svelte",
118
- vue: "Vue"
119
- };
120
- const FEATURE_LABELS = {
121
- "best-practices": "Best Practices",
122
- cspell: "CSpell",
123
- cypress: "Cypress",
124
- graphql: "GraphQL",
125
- i18next: "i18next",
126
- jest: "Jest",
127
- jsdoc: "JSDoc",
128
- jsonc: "JSONC",
129
- markdown: "Markdown",
130
- mdx: "MDX",
131
- perfectionist: "Perfectionist",
132
- playwright: "Playwright",
133
- prettier: "Prettier",
134
- regexp: "Regexp",
135
- security: "Security",
136
- sonarjs: "SonarJS",
137
- stencil: "Stencil",
138
- storybook: "Storybook",
139
- swagger: "Swagger",
140
- tailwind: "Tailwind CSS",
141
- "tanstack-query": "TanStack Query",
142
- "tanstack-router": "TanStack Router",
143
- "testing-library": "Testing Library",
144
- toml: "TOML",
145
- unicorn: "Unicorn",
146
- vitest: "Vitest",
147
- yaml: "YAML"
148
- };
149
- const toFeatureLabel = (value) => FEATURE_LABELS[value] ?? value;
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("/")));
150
234
  const collectTokens = (configs) => {
151
235
  const tokens = [];
152
236
  for (const entry of configs) {
@@ -159,27 +243,50 @@ const collectTokens = (configs) => {
159
243
  tokens.push(...Object.keys(cfg.plugins));
160
244
  }
161
245
  if (cfg.rules && typeof cfg.rules === "object") {
162
- for (const key of Object.keys(cfg.rules)) {
163
- const slash = key.indexOf("/");
164
- if (slash > -1) tokens.push(key.slice(0, slash));
165
- }
246
+ tokens.push(...extractRuleNamespaces(cfg.rules));
166
247
  }
167
248
  }
168
249
  return tokens;
169
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
+ };
170
277
  const extractFeatures = (configs, lintCommand, configFile) => {
171
278
  const tokens = collectTokens(configs);
172
279
  const features = {
173
- typescript: false,
280
+ configFile,
281
+ extensions: [],
282
+ formats: [],
174
283
  frameworks: [],
175
- testing: [],
176
- tools: [],
177
284
  libraries: [],
178
- formats: [],
179
- extensions: [],
180
285
  lintCommand,
181
- configFile,
182
- source: "config-file"
286
+ source: "config-file",
287
+ testing: [],
288
+ tools: [],
289
+ typescript: false
183
290
  };
184
291
  const seen = /* @__PURE__ */ new Set();
185
292
  for (const [pattern, category, label] of FEATURE_MAP) {
@@ -187,11 +294,7 @@ const extractFeatures = (configs, lintCommand, configFile) => {
187
294
  const matched = tokens.some((t) => t.includes(pattern));
188
295
  if (!matched) continue;
189
296
  seen.add(label);
190
- if (category === "typescript") {
191
- features.typescript = true;
192
- } else {
193
- features[category].push(label);
194
- }
297
+ recordFeature(features, category, label);
195
298
  }
196
299
  return features;
197
300
  };
@@ -243,42 +346,36 @@ const analyzeEslintConfig = async (cwd) => {
243
346
  const featuresFromDetection = (cwd) => {
244
347
  const opts = detectProjectOptions(cwd);
245
348
  const lintCommand = detectLintCommand(cwd);
246
- const frameworks = (opts.detectedFrameworks ?? []).map((f) => DETECTED_FRAMEWORK_LABELS[f] ?? f);
349
+ const frameworks = (opts.detectedFrameworks ?? []).map((f) => DETECTED_FRAMEWORK_LABELS.get(f) ?? f);
247
350
  return {
248
- typescript: opts.typescript === true,
351
+ configFile: null,
352
+ extensions: (opts.extensions ?? []).map(toFeatureLabel),
353
+ formats: (opts.formats ?? []).map(toFeatureLabel),
249
354
  frameworks,
250
- testing: (opts.testing ?? []).map(toFeatureLabel),
251
- tools: (opts.tools ?? []).map(toFeatureLabel),
252
355
  libraries: (opts.libraries ?? []).map(toFeatureLabel),
253
- formats: (opts.formats ?? []).map(toFeatureLabel),
254
- extensions: (opts.extensions ?? []).map(toFeatureLabel),
255
356
  lintCommand,
256
- configFile: null,
257
- source: "detection-fallback"
357
+ source: "detection-fallback",
358
+ testing: (opts.testing ?? []).map(toFeatureLabel),
359
+ tools: (opts.tools ?? []).map(toFeatureLabel),
360
+ typescript: opts.typescript === true
258
361
  };
259
362
  };
260
- const generateSkillContent = (features, format) => {
261
- const { typescript, frameworks, testing, tools, libraries, formats, extensions, lintCommand } = features;
262
- const summaryLines = [];
263
- summaryLines.push(`- **TypeScript**: ${typescript ? "enabled" : "disabled"}`);
264
- if (frameworks.length > 0) summaryLines.push(`- **Frameworks**: ${frameworks.join(", ")}`);
265
- if (testing.length > 0) summaryLines.push(`- **Testing**: ${testing.join(", ")}`);
266
- if (tools.length > 0) summaryLines.push(`- **Tools**: ${tools.join(", ")}`);
267
- if (libraries.length > 0) summaryLines.push(`- **Libraries**: ${libraries.join(", ")}`);
268
- if (formats.length > 0) summaryLines.push(`- **Formats**: ${formats.join(", ")}`);
269
- if (extensions.length > 0) summaryLines.push(`- **Extensions**: ${extensions.join(", ")}`);
270
- const tsSection = typescript ? `
271
- ### TypeScript
272
-
273
- - Use explicit type annotations on all exported functions and values
274
- - Use \`import type\` for type-only imports
275
- - Avoid \`any\` \u2014 prefer \`unknown\` with type guards
276
- - Prefer \`const\` over \`let\` whenever the binding is not reassigned
277
- - Return types must be explicit on exported functions
278
- ` : "";
279
- const frameworkSections = [];
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 = [];
280
377
  if (frameworks.some((f) => f === "React" || f === "Next.js")) {
281
- frameworkSections.push(`
378
+ sections.push(`
282
379
  ### React / JSX
283
380
 
284
381
  - Prefer function components over class components
@@ -288,7 +385,7 @@ const generateSkillContent = (features, format) => {
288
385
  `);
289
386
  }
290
387
  if (frameworks.includes("Next.js")) {
291
- frameworkSections.push(`
388
+ sections.push(`
292
389
  ### Next.js
293
390
 
294
391
  - Follow App Router conventions (Server Components by default, \`"use client"\` when needed)
@@ -297,7 +394,7 @@ const generateSkillContent = (features, format) => {
297
394
  `);
298
395
  }
299
396
  if (frameworks.includes("Vue")) {
300
- frameworkSections.push(`
397
+ sections.push(`
301
398
  ### Vue
302
399
 
303
400
  - Prefer Composition API (\`<script setup>\`) over Options API
@@ -305,14 +402,14 @@ const generateSkillContent = (features, format) => {
305
402
  `);
306
403
  }
307
404
  if (frameworks.includes("Svelte")) {
308
- frameworkSections.push(`
405
+ sections.push(`
309
406
  ### Svelte
310
407
 
311
408
  - Virtual \`*.svelte/*.ts\` files are handled by the ESLint config \u2014 do not manually adjust \`allowDefaultProject\`
312
409
  `);
313
410
  }
314
411
  if (frameworks.includes("Astro")) {
315
- frameworkSections.push(`
412
+ sections.push(`
316
413
  ### Astro
317
414
 
318
415
  - Virtual \`*.astro/*.ts\` files are handled by the ESLint config
@@ -320,13 +417,67 @@ const generateSkillContent = (features, format) => {
320
417
  `);
321
418
  }
322
419
  if (frameworks.includes("Angular")) {
323
- frameworkSections.push(`
420
+ sections.push(`
324
421
  ### Angular
325
422
 
326
423
  - Follow the Angular style guide component, directive, and service naming conventions
327
424
  - Use standalone components by default; avoid NgModule unless required
328
425
  `);
329
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);
330
481
  const testingSections = [];
331
482
  if (testing.some((t) => t === "Vitest" || t === "Jest")) {
332
483
  testingSections.push(`
@@ -364,7 +515,8 @@ const generateSkillContent = (features, format) => {
364
515
  }
365
516
  const body = `# ESLint Code Standards
366
517
 
367
- This project uses [\`@santi020k/eslint-config-basic\`](https://github.com/santi020k/eslint-config-basic) \u2014 a composable ESLint 9/10+ Flat Config package.
518
+ This project uses [\`@santi020k/eslint-config-basic\`](https://github.com/santi020k/eslint-config-basic) \u2014
519
+ a composable ESLint 10+ Flat Config package.
368
520
 
369
521
  **Always run \`${lintCommand}\` to validate your changes before finishing any task.**
370
522
 
@@ -376,7 +528,7 @@ ${summaryLines.join("\n")}
376
528
  ${tsSection}${frameworkSections.join("")}${testingSections.join("")}${toolSections.join("")}
377
529
  ### General
378
530
 
379
- - ESLint 9/10 **Flat Config** format only \u2014 no \`.eslintrc\` files
531
+ - ESLint 10 **Flat Config** format only \u2014 no \`.eslintrc\` files
380
532
  - Use \`.js\` extensions on relative imports (ESM requirement)
381
533
  - Use \`type\` imports for type-only values; regular imports for runtime values
382
534
 
@@ -392,36 +544,7 @@ If the command is unavailable, fall back to:
392
544
  npx eslint .
393
545
  \`\`\`
394
546
  `;
395
- if (format === "frontmatter") {
396
- return `---
397
- name: eslint-standards
398
- description: >
399
- Code quality standards enforced by @santi020k/eslint-config-basic. Follow these
400
- conventions whenever writing or editing code in this project.
401
- trigger: always_on
402
- ---
403
-
404
- ${body}`;
405
- }
406
- if (format === "cursor") {
407
- return `---
408
- description: >
409
- ESLint code standards for this project. Apply these conventions when writing
410
- or editing any source file.
411
- globs:
412
- - "**/*.ts"
413
- - "**/*.tsx"
414
- - "**/*.js"
415
- - "**/*.jsx"
416
- - "**/*.vue"
417
- - "**/*.svelte"
418
- - "**/*.astro"
419
- alwaysApply: true
420
- ---
421
-
422
- ${body}`;
423
- }
424
- return body;
547
+ return wrapWithFormat(body, format);
425
548
  };
426
549
  const writeSkillFile = (filePath, content, force) => {
427
550
  if (existsSync(filePath) && !force) return false;
@@ -430,70 +553,107 @@ const writeSkillFile = (filePath, content, force) => {
430
553
  return true;
431
554
  };
432
555
  const COPILOT_INSTRUCTIONS_PATH = ".github/copilot-instructions.md";
556
+ const AGENTS_MD_PATH = "AGENTS.md";
433
557
  const COPILOT_SECTION_START = "<!-- eslint-standards:start -->";
434
558
  const COPILOT_SECTION_END = "<!-- eslint-standards:end -->";
435
- const handleCopilotInstructions = (cwd, body, force) => {
436
- const filePath = join(cwd, COPILOT_INSTRUCTIONS_PATH);
437
- if (!existsSync(filePath)) return null;
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
+ }
438
572
  const existing = readFileSync(filePath, "utf-8");
573
+ if (check) {
574
+ return existing.includes(section) ? "skipped" : "stale";
575
+ }
439
576
  if (existing.includes(COPILOT_SECTION_START)) {
440
577
  if (!force) return "skipped";
441
- const updated = existing.replace(
578
+ const updated = existing.replaceAll(
442
579
  new RegExp(`${COPILOT_SECTION_START}[\\s\\S]*?${COPILOT_SECTION_END}`, "g"),
443
- `${COPILOT_SECTION_START}
444
- ${body}
445
- ${COPILOT_SECTION_END}`
580
+ section
446
581
  );
447
582
  writeFileSync(filePath, updated, "utf-8");
448
583
  return "written";
449
584
  }
450
- writeFileSync(
451
- filePath,
452
- `${existing}
453
- ${COPILOT_SECTION_START}
454
- ${body}
455
- ${COPILOT_SECTION_END}
456
- `,
457
- "utf-8"
458
- );
585
+ writeFileSync(filePath, `${existing}
586
+ ${section}
587
+ `, "utf-8");
459
588
  return "written";
460
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
+ });
461
596
  const generateAgentSkills = async (opts = {}) => {
462
- const cwd = opts.cwd ?? process.cwd();
463
- const force = opts.force ?? false;
597
+ const { check, createAgentsMd, cwd, force } = resolveSkillOptions(opts);
464
598
  const written = [];
465
599
  const skipped = [];
600
+ const stale = [];
466
601
  const features = await analyzeEslintConfig(cwd) ?? featuresFromDetection(cwd);
467
602
  const plainBody = generateSkillContent(features, "plain");
468
- const copilotResult = handleCopilotInstructions(cwd, plainBody, force);
469
- if (copilotResult === "written") written.push(join(cwd, COPILOT_INSTRUCTIONS_PATH));
470
- else if (copilotResult === "skipped") skipped.push(join(cwd, COPILOT_INSTRUCTIONS_PATH));
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));
471
620
  for (const target of AGENT_TARGETS) {
472
621
  const agentFolder = join(cwd, target.markerFolder);
473
622
  if (!existsSync(agentFolder)) continue;
474
623
  const subdir = target.skillSubdir === "." ? agentFolder : join(agentFolder, target.skillSubdir);
475
624
  const filePath = join(subdir, target.skillFile);
476
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
+ }
477
632
  const didWrite = writeSkillFile(filePath, content, force);
478
633
  if (didWrite) written.push(filePath);
479
634
  else skipped.push(filePath);
480
635
  }
481
- return { written, skipped };
636
+ return { skipped, stale, written };
482
637
  };
483
- const handleGenerateSkill = async (cwd = process.cwd(), force = false) => {
484
- console.log("\u{1F50D} Scanning for AI agent folders...");
485
- const configFile = findEslintConfig(cwd);
486
- if (configFile) {
487
- console.log(`\u{1F4C4} Reading config from: ${configFile.replace(cwd + "/", "")}`);
488
- } else {
489
- console.log("\u26A0\uFE0F No eslint.config.js found \u2014 falling back to package.json detection.");
490
- }
491
- const result = await generateAgentSkills({ cwd, force });
492
- if (result.written.length === 0 && result.skipped.length === 0) {
493
- console.log(
494
- "\n\u26A0\uFE0F No agent folders found (.agent, .agents, .claude, .cursor, .windsurf, .copilot, .aider)."
495
- );
496
- console.log(" Create one of those folders first, then re-run this command.");
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
+ }
497
657
  return;
498
658
  }
499
659
  for (const file of result.written) {
@@ -511,6 +671,23 @@ const handleGenerateSkill = async (cwd = process.cwd(), force = false) => {
511
671
  console.log("\n\u{1F4A1} Tip: run with --force to overwrite existing skill files.");
512
672
  }
513
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
+ };
514
691
  export {
515
692
  AGENT_TARGETS,
516
693
  analyzeEslintConfig,