@santi020k/eslint-config-basic 1.4.0 → 1.6.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 CHANGED
@@ -1,5 +1,59 @@
1
1
  # @santi020k/eslint-config-basic
2
2
 
3
+ ## 1.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#85](https://github.com/santi020k/eslint-config-basic/pull/85) [`664693e`](https://github.com/santi020k/eslint-config-basic/commit/664693ef8edeb51239bf4dc2c75148203bcf41a6) Thanks [@santi020k](https://github.com/santi020k)! - Add `basic-eslint generate-skill` CLI command and `generateAgentSkills()` API.
8
+
9
+ The new command scans a project for AI coding-assistant agent folders (`.agent`, `.agents`, `.claude`, `.cursor`, `.windsurf`, `.copilot`, `.aider`) and writes a tailored ESLint standards skill file into each one. The generated file describes the active configuration (TypeScript, frameworks, testing, tools, etc.) and instructs the agent to follow the project's lint conventions automatically.
10
+
11
+ A companion GitHub Actions workflow (`.github/workflows/agent-skills.yml`) is included so projects can auto-regenerate skill files whenever the ESLint config changes.
12
+
13
+ **Usage:**
14
+
15
+ ```bash
16
+ npx basic-eslint generate-skill # write skill files (skip existing)
17
+ npx basic-eslint generate-skill --force # overwrite all skill files
18
+ ```
19
+
20
+ **Programmatic API:**
21
+
22
+ ```ts
23
+ import { generateAgentSkills } from "@santi020k/eslint-config-basic";
24
+
25
+ const { written, skipped } = generateAgentSkills({
26
+ cwd: process.cwd(),
27
+ force: true,
28
+ });
29
+ ```
30
+
31
+ ### Patch Changes
32
+
33
+ - [#85](https://github.com/santi020k/eslint-config-basic/pull/85) [`664693e`](https://github.com/santi020k/eslint-config-basic/commit/664693ef8edeb51239bf4dc2c75148203bcf41a6) Thanks [@santi020k](https://github.com/santi020k)! - Load optional integrations lazily so projects can import the base config without installing unrelated integration peers such as Storybook, GraphQL, Stencil, Cypress, or Testing Library.
34
+
35
+ Also include the generated agent-skill API entry in the basic package build output so published packages do not reference a missing `agent-skill-generator.js` file.
36
+
37
+ - Updated dependencies [[`664693e`](https://github.com/santi020k/eslint-config-basic/commit/664693ef8edeb51239bf4dc2c75148203bcf41a6)]:
38
+ - @santi020k/eslint-config-optionals@1.6.0
39
+ - @santi020k/eslint-config-core@1.6.0
40
+ - @santi020k/eslint-config-typescript@1.6.0
41
+
42
+ ## 1.5.0
43
+
44
+ ### Minor Changes
45
+
46
+ - [#82](https://github.com/santi020k/eslint-config-basic/pull/82) [`af844f6`](https://github.com/santi020k/eslint-config-basic/commit/af844f663f8403d679467e0dd2a7251e8f3d4bcd) Thanks [@santi020k](https://github.com/santi020k)! - - Add first-class Hono support with Worker runtime globals, package detection, and a playground for Cloudflare Workers-style projects.
47
+ - Lower the Node.js engine requirement to `>=22.18.0` in `package.json` and `.nvmrc`, and validate CI against Node.js 22.
48
+ - Update `README.md` with a landing pointer to the canonical docs site.
49
+
50
+ ### Patch Changes
51
+
52
+ - Updated dependencies [[`af844f6`](https://github.com/santi020k/eslint-config-basic/commit/af844f663f8403d679467e0dd2a7251e8f3d4bcd)]:
53
+ - @santi020k/eslint-config-core@1.5.0
54
+ - @santi020k/eslint-config-optionals@1.5.0
55
+ - @santi020k/eslint-config-typescript@1.5.0
56
+
3
57
  ## 1.4.0
4
58
 
5
59
  ### Minor Changes
@@ -0,0 +1,86 @@
1
+ interface AgentTarget {
2
+ /** Human-readable label for logging */
3
+ label: string;
4
+ /** Folder that must exist in cwd to be considered "present" */
5
+ markerFolder: string;
6
+ /** Subdirectory inside the agent folder where the skill file is placed */
7
+ skillSubdir: string;
8
+ /** File name for the generated skill */
9
+ skillFile: string;
10
+ /** Format variant used when generating content */
11
+ format: 'frontmatter' | 'cursor' | 'plain';
12
+ }
13
+ /**
14
+ * Features extracted from the project's actual `eslint.config.js`.
15
+ * All arrays hold display-friendly labels (e.g. `'TypeScript'`, `'React'`).
16
+ */
17
+ interface EslintConfigFeatures {
18
+ typescript: boolean;
19
+ frameworks: string[];
20
+ testing: string[];
21
+ tools: string[];
22
+ libraries: string[];
23
+ formats: string[];
24
+ extensions: string[];
25
+ /** The lint command found in the project's package.json scripts, or a sensible default */
26
+ lintCommand: string;
27
+ /** Path to the config file that was loaded, or null when falling back to detection */
28
+ configFile: string | null;
29
+ /** Whether features came from the real config file or from package.json detection */
30
+ source: 'config-file' | 'detection-fallback';
31
+ }
32
+ interface GenerateSkillResult {
33
+ written: string[];
34
+ skipped: string[];
35
+ }
36
+ interface GenerateSkillOptions {
37
+ /** Working directory — defaults to process.cwd() */
38
+ cwd?: string;
39
+ /**
40
+ * Overwrite files that already exist.
41
+ * @default false
42
+ */
43
+ force?: boolean;
44
+ }
45
+ /**
46
+ * All AI coding-assistant agent folders that are probed for.
47
+ * Add new entries here as new agents emerge — the generator picks them up
48
+ * automatically on the next run.
49
+ */
50
+ declare const AGENT_TARGETS: AgentTarget[];
51
+ /**
52
+ * Loads the user's `eslint.config.js` (or `.mjs` / `.cjs`) via dynamic
53
+ * `import()` and derives features from the resulting flat-config array.
54
+ *
55
+ * Returns `null` when no config file is found or it cannot be imported.
56
+ */
57
+ declare const analyzeEslintConfig: (cwd: string) => Promise<EslintConfigFeatures | null>;
58
+ /**
59
+ * Builds the skill document body from the project's {@link EslintConfigFeatures}.
60
+ * Three format variants are produced:
61
+ *
62
+ * - `frontmatter` — YAML front-matter + Markdown (`.agent`, `.agents`, `.windsurf`)
63
+ * - `cursor` — Cursor MDC front-matter + Markdown
64
+ * - `plain` — pure Markdown, no front-matter (Claude Code, Copilot, Aider)
65
+ */
66
+ declare const generateSkillContent: (features: EslintConfigFeatures, format: AgentTarget["format"]) => string;
67
+ /**
68
+ * Detects which AI agent folders exist in the project, reads the actual
69
+ * `eslint.config.js` to understand what is configured, and writes a tailored
70
+ * ESLint standards skill file into each found folder.
71
+ *
72
+ * Falls back to package.json auto-detection when the config file cannot be
73
+ * dynamically imported (e.g. when running outside a built environment).
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * import { generateAgentSkills } from '@santi020k/eslint-config-basic'
78
+ *
79
+ * const result = await generateAgentSkills({ cwd: process.cwd() })
80
+ * console.log('Written to:', result.written)
81
+ * ```
82
+ */
83
+ declare const generateAgentSkills: (opts?: GenerateSkillOptions) => Promise<GenerateSkillResult>;
84
+ declare const handleGenerateSkill: (cwd?: string, force?: boolean) => Promise<void>;
85
+
86
+ export { AGENT_TARGETS, type AgentTarget, type EslintConfigFeatures, type GenerateSkillOptions, type GenerateSkillResult, analyzeEslintConfig, generateAgentSkills, generateSkillContent, handleGenerateSkill };
@@ -0,0 +1,520 @@
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
+ label: ".agent (generic skill format)",
8
+ markerFolder: ".agent",
9
+ skillSubdir: "skills",
10
+ skillFile: "eslint-standards.md",
11
+ format: "frontmatter"
12
+ },
13
+ {
14
+ label: ".agents (generic skill format)",
15
+ markerFolder: ".agents",
16
+ skillSubdir: "skills",
17
+ skillFile: "eslint-standards.md",
18
+ format: "frontmatter"
19
+ },
20
+ {
21
+ label: "Claude Code (.claude/commands)",
22
+ markerFolder: ".claude",
23
+ skillSubdir: "commands",
24
+ skillFile: "eslint.md",
25
+ format: "plain"
26
+ },
27
+ {
28
+ label: "Cursor (.cursor/rules)",
29
+ markerFolder: ".cursor",
30
+ skillSubdir: "rules",
31
+ skillFile: "eslint-standards.mdc",
32
+ format: "cursor"
33
+ },
34
+ {
35
+ label: "Windsurf (.windsurf/rules)",
36
+ markerFolder: ".windsurf",
37
+ skillSubdir: "rules",
38
+ skillFile: "eslint-standards.md",
39
+ format: "frontmatter"
40
+ },
41
+ {
42
+ label: "Copilot (.copilot)",
43
+ markerFolder: ".copilot",
44
+ skillSubdir: "instructions",
45
+ skillFile: "eslint-standards.md",
46
+ format: "plain"
47
+ },
48
+ {
49
+ label: "Aider (.aider)",
50
+ markerFolder: ".aider",
51
+ skillSubdir: ".",
52
+ skillFile: "eslint-standards.md",
53
+ format: "plain"
54
+ }
55
+ ];
56
+ const FEATURE_MAP = [
57
+ // TypeScript
58
+ ["eslint-config-typescript/", "typescript", "TypeScript"],
59
+ ["@typescript-eslint", "typescript", "TypeScript"],
60
+ // Frameworks
61
+ ["eslint-config-react/", "frameworks", "React"],
62
+ ["eslint-config-next/", "frameworks", "Next.js"],
63
+ ["eslint-config-vue/", "frameworks", "Vue"],
64
+ ["eslint-config-svelte/", "frameworks", "Svelte"],
65
+ ["eslint-config-astro/", "frameworks", "Astro"],
66
+ ["eslint-config-angular/", "frameworks", "Angular"],
67
+ ["eslint-config-nest/", "frameworks", "NestJS"],
68
+ ["eslint-config-hono/", "frameworks", "Hono"],
69
+ ["eslint-config-expo/", "frameworks", "Expo"],
70
+ ["eslint-config-qwik/", "frameworks", "Qwik"],
71
+ ["eslint-config-remix/", "frameworks", "Remix"],
72
+ ["eslint-config-solid/", "frameworks", "SolidJS"],
73
+ // 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"],
79
+ // Tools
80
+ ["eslint-config/prettier", "tools", "Prettier"],
81
+ ["optionals/cspell", "tools", "CSpell"],
82
+ ["eslint-config-optionals/jsdoc", "tools", "JSDoc"],
83
+ ["optionals/swagger", "tools", "Swagger"],
84
+ // Libraries
85
+ ["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"],
91
+ // Formats
92
+ ["optionals/graphql/", "formats", "GraphQL"],
93
+ ["optionals/yaml/", "formats", "YAML"],
94
+ ["optionals/jsonc/", "formats", "JSONC"],
95
+ ["optionals/markdown", "formats", "Markdown"],
96
+ ["eslint-config-mdx/", "formats", "MDX"],
97
+ ["optionals/toml/", "formats", "TOML"],
98
+ // Extensions
99
+ ["eslint-config/unicorn", "extensions", "Unicorn"],
100
+ ["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"]
105
+ ];
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;
150
+ const collectTokens = (configs) => {
151
+ const tokens = [];
152
+ for (const entry of configs) {
153
+ if (!entry || typeof entry !== "object") continue;
154
+ const cfg = entry;
155
+ if (typeof cfg.name === "string") {
156
+ tokens.push(cfg.name);
157
+ }
158
+ if (cfg.plugins && typeof cfg.plugins === "object") {
159
+ tokens.push(...Object.keys(cfg.plugins));
160
+ }
161
+ 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
+ }
166
+ }
167
+ }
168
+ return tokens;
169
+ };
170
+ const extractFeatures = (configs, lintCommand, configFile) => {
171
+ const tokens = collectTokens(configs);
172
+ const features = {
173
+ typescript: false,
174
+ frameworks: [],
175
+ testing: [],
176
+ tools: [],
177
+ libraries: [],
178
+ formats: [],
179
+ extensions: [],
180
+ lintCommand,
181
+ configFile,
182
+ source: "config-file"
183
+ };
184
+ const seen = /* @__PURE__ */ new Set();
185
+ for (const [pattern, category, label] of FEATURE_MAP) {
186
+ if (seen.has(label)) continue;
187
+ const matched = tokens.some((t) => t.includes(pattern));
188
+ if (!matched) continue;
189
+ seen.add(label);
190
+ if (category === "typescript") {
191
+ features.typescript = true;
192
+ } else {
193
+ features[category].push(label);
194
+ }
195
+ }
196
+ return features;
197
+ };
198
+ const findEslintConfig = (cwd) => {
199
+ for (const name of ["eslint.config.js", "eslint.config.mjs", "eslint.config.cjs"]) {
200
+ const p = join(cwd, name);
201
+ if (existsSync(p)) return p;
202
+ }
203
+ return null;
204
+ };
205
+ const detectLintCommand = (cwd) => {
206
+ let pkgManager = "npm";
207
+ if (existsSync(join(cwd, "pnpm-lock.yaml")) || existsSync(join(cwd, "pnpm-workspace.yaml"))) {
208
+ pkgManager = "pnpm";
209
+ } else if (existsSync(join(cwd, "yarn.lock"))) {
210
+ pkgManager = "yarn";
211
+ } else if (existsSync(join(cwd, "bun.lockb")) || existsSync(join(cwd, "bun.lock"))) {
212
+ pkgManager = "bun";
213
+ }
214
+ const pkgPath = join(cwd, "package.json");
215
+ try {
216
+ if (existsSync(pkgPath)) {
217
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
218
+ if (pkg.scripts?.lint) return `${pkgManager} run lint`;
219
+ }
220
+ } catch {
221
+ }
222
+ return "npx eslint .";
223
+ };
224
+ const analyzeEslintConfig = async (cwd) => {
225
+ const configPath = findEslintConfig(cwd);
226
+ if (!configPath) return null;
227
+ const lintCommand = detectLintCommand(cwd);
228
+ try {
229
+ const mod = await import(pathToFileURL(resolve(configPath)).href);
230
+ const raw = mod.default ?? mod;
231
+ let configs = null;
232
+ if (Array.isArray(raw)) {
233
+ configs = raw;
234
+ } else if (raw instanceof Promise) {
235
+ configs = await raw;
236
+ }
237
+ if (!Array.isArray(configs)) return null;
238
+ return extractFeatures(configs, lintCommand, configPath);
239
+ } catch {
240
+ return null;
241
+ }
242
+ };
243
+ const featuresFromDetection = (cwd) => {
244
+ const opts = detectProjectOptions(cwd);
245
+ const lintCommand = detectLintCommand(cwd);
246
+ const frameworks = (opts.detectedFrameworks ?? []).map((f) => DETECTED_FRAMEWORK_LABELS[f] ?? f);
247
+ return {
248
+ typescript: opts.typescript === true,
249
+ frameworks,
250
+ testing: (opts.testing ?? []).map(toFeatureLabel),
251
+ tools: (opts.tools ?? []).map(toFeatureLabel),
252
+ libraries: (opts.libraries ?? []).map(toFeatureLabel),
253
+ formats: (opts.formats ?? []).map(toFeatureLabel),
254
+ extensions: (opts.extensions ?? []).map(toFeatureLabel),
255
+ lintCommand,
256
+ configFile: null,
257
+ source: "detection-fallback"
258
+ };
259
+ };
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 = [];
280
+ if (frameworks.some((f) => f === "React" || f === "Next.js")) {
281
+ frameworkSections.push(`
282
+ ### React / JSX
283
+
284
+ - Prefer function components over class components
285
+ - Hooks must follow the Rules of Hooks (no conditional calls, no loops)
286
+ - Avoid inline arrow functions in JSX props where it hurts readability
287
+ - Use \`key\` props when rendering lists
288
+ `);
289
+ }
290
+ if (frameworks.includes("Next.js")) {
291
+ frameworkSections.push(`
292
+ ### Next.js
293
+
294
+ - Follow App Router conventions (Server Components by default, \`"use client"\` when needed)
295
+ - Avoid \`<img>\` \u2014 use \`next/image\`
296
+ - Avoid \`<a>\` for internal navigation \u2014 use \`next/link\`
297
+ `);
298
+ }
299
+ if (frameworks.includes("Vue")) {
300
+ frameworkSections.push(`
301
+ ### Vue
302
+
303
+ - Prefer Composition API (\`<script setup>\`) over Options API
304
+ - Use single-word or multi-word component names consistently
305
+ `);
306
+ }
307
+ if (frameworks.includes("Svelte")) {
308
+ frameworkSections.push(`
309
+ ### Svelte
310
+
311
+ - Virtual \`*.svelte/*.ts\` files are handled by the ESLint config \u2014 do not manually adjust \`allowDefaultProject\`
312
+ `);
313
+ }
314
+ if (frameworks.includes("Astro")) {
315
+ frameworkSections.push(`
316
+ ### Astro
317
+
318
+ - Virtual \`*.astro/*.ts\` files are handled by the ESLint config
319
+ - Prefer Astro components over framework components when no interactivity is needed
320
+ `);
321
+ }
322
+ if (frameworks.includes("Angular")) {
323
+ frameworkSections.push(`
324
+ ### Angular
325
+
326
+ - Follow the Angular style guide component, directive, and service naming conventions
327
+ - Use standalone components by default; avoid NgModule unless required
328
+ `);
329
+ }
330
+ const testingSections = [];
331
+ if (testing.some((t) => t === "Vitest" || t === "Jest")) {
332
+ testingSections.push(`
333
+ ### Tests
334
+
335
+ - Name test files \`*.test.ts\` or \`*.spec.ts\`
336
+ - Use \`describe\` blocks to group related cases
337
+ - Avoid testing implementation details \u2014 test observable behaviour
338
+ `);
339
+ }
340
+ if (testing.includes("Playwright")) {
341
+ testingSections.push(`
342
+ ### Playwright
343
+
344
+ - Place tests in a dedicated \`e2e/\` or \`tests/\` directory
345
+ - Use \`page.getByRole\` / \`page.getByText\` over CSS selectors where possible
346
+ `);
347
+ }
348
+ const toolSections = [];
349
+ if (tools.includes("Prettier")) {
350
+ toolSections.push(`
351
+ ### Formatting
352
+
353
+ - Do not manually format \u2014 Prettier handles all formatting
354
+ - Prettier runs as the **last** ESLint config; never override its rules
355
+ `);
356
+ }
357
+ if (tools.includes("Tailwind CSS") || libraries.includes("Tailwind CSS")) {
358
+ toolSections.push(`
359
+ ### Tailwind CSS
360
+
361
+ - Use Tailwind utility classes directly in templates rather than hand-written CSS where possible
362
+ - The \`better-tailwindcss\` plugin enforces ordering; do not disable class-ordering rules
363
+ `);
364
+ }
365
+ const body = `# ESLint Code Standards
366
+
367
+ This project uses [\`@santi020k/eslint-config-basic\`](https://github.com/santi020k/eslint-config-basic) \u2014 a composable ESLint 9/10+ Flat Config package.
368
+
369
+ **Always run \`${lintCommand}\` to validate your changes before finishing any task.**
370
+
371
+ ## Active Configuration
372
+
373
+ ${summaryLines.join("\n")}
374
+
375
+ ## Conventions
376
+ ${tsSection}${frameworkSections.join("")}${testingSections.join("")}${toolSections.join("")}
377
+ ### General
378
+
379
+ - ESLint 9/10 **Flat Config** format only \u2014 no \`.eslintrc\` files
380
+ - Use \`.js\` extensions on relative imports (ESM requirement)
381
+ - Use \`type\` imports for type-only values; regular imports for runtime values
382
+
383
+ ## Verification
384
+
385
+ \`\`\`bash
386
+ ${lintCommand}
387
+ \`\`\`
388
+
389
+ If the command is unavailable, fall back to:
390
+
391
+ \`\`\`bash
392
+ npx eslint .
393
+ \`\`\`
394
+ `;
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;
425
+ };
426
+ const writeSkillFile = (filePath, content, force) => {
427
+ if (existsSync(filePath) && !force) return false;
428
+ mkdirSync(join(filePath, ".."), { recursive: true });
429
+ writeFileSync(filePath, content, "utf-8");
430
+ return true;
431
+ };
432
+ const COPILOT_INSTRUCTIONS_PATH = ".github/copilot-instructions.md";
433
+ const COPILOT_SECTION_START = "<!-- eslint-standards:start -->";
434
+ 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;
438
+ const existing = readFileSync(filePath, "utf-8");
439
+ if (existing.includes(COPILOT_SECTION_START)) {
440
+ if (!force) return "skipped";
441
+ const updated = existing.replace(
442
+ new RegExp(`${COPILOT_SECTION_START}[\\s\\S]*?${COPILOT_SECTION_END}`, "g"),
443
+ `${COPILOT_SECTION_START}
444
+ ${body}
445
+ ${COPILOT_SECTION_END}`
446
+ );
447
+ writeFileSync(filePath, updated, "utf-8");
448
+ return "written";
449
+ }
450
+ writeFileSync(
451
+ filePath,
452
+ `${existing}
453
+ ${COPILOT_SECTION_START}
454
+ ${body}
455
+ ${COPILOT_SECTION_END}
456
+ `,
457
+ "utf-8"
458
+ );
459
+ return "written";
460
+ };
461
+ const generateAgentSkills = async (opts = {}) => {
462
+ const cwd = opts.cwd ?? process.cwd();
463
+ const force = opts.force ?? false;
464
+ const written = [];
465
+ const skipped = [];
466
+ const features = await analyzeEslintConfig(cwd) ?? featuresFromDetection(cwd);
467
+ 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));
471
+ for (const target of AGENT_TARGETS) {
472
+ const agentFolder = join(cwd, target.markerFolder);
473
+ if (!existsSync(agentFolder)) continue;
474
+ const subdir = target.skillSubdir === "." ? agentFolder : join(agentFolder, target.skillSubdir);
475
+ const filePath = join(subdir, target.skillFile);
476
+ const content = generateSkillContent(features, target.format);
477
+ const didWrite = writeSkillFile(filePath, content, force);
478
+ if (didWrite) written.push(filePath);
479
+ else skipped.push(filePath);
480
+ }
481
+ return { written, skipped };
482
+ };
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.");
497
+ return;
498
+ }
499
+ for (const file of result.written) {
500
+ console.log(`\u2705 Written: ${file.replace(cwd + "/", "")}`);
501
+ }
502
+ for (const file of result.skipped) {
503
+ console.log(`\u23ED\uFE0F Skipped (already exists): ${file.replace(cwd + "/", "")}`);
504
+ }
505
+ if (result.written.length > 0) {
506
+ console.log(`
507
+ \u{1F389} Generated ${result.written.length} skill file(s)!`);
508
+ console.log(" Agents will now follow your project's ESLint standards automatically.");
509
+ }
510
+ if (result.skipped.length > 0) {
511
+ console.log("\n\u{1F4A1} Tip: run with --force to overwrite existing skill files.");
512
+ }
513
+ };
514
+ export {
515
+ AGENT_TARGETS,
516
+ analyzeEslintConfig,
517
+ generateAgentSkills,
518
+ generateSkillContent,
519
+ handleGenerateSkill
520
+ };
package/dist/cli.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { basename, join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
+ import { handleGenerateSkill } from "./agent-skill-generator.js";
5
6
  import { detectProjectOptions } from "./index.js";
6
7
  const getDefaultConfigFilename = (cwd) => {
7
8
  const packageJsonPath = join(cwd, "package.json");
@@ -76,12 +77,18 @@ const handleUpdate = (cwd = process.cwd()) => {
76
77
  };
77
78
  const run = () => {
78
79
  const command = process.argv[2];
80
+ const hasForce = process.argv.includes("--force");
79
81
  if (command === "init") {
80
82
  handleInit();
81
83
  } else if (command === "update") {
82
84
  handleUpdate();
85
+ } else if (command === "generate-skill") {
86
+ handleGenerateSkill(process.cwd(), hasForce).catch((err) => {
87
+ console.error("\u274C Failed to generate skill files:", err);
88
+ process.exitCode = 1;
89
+ });
83
90
  } else {
84
- console.log("Usage: basic-eslint <init|update>");
91
+ console.log("Usage: basic-eslint <init|update|generate-skill> [--force]");
85
92
  }
86
93
  };
87
94
  if (process.argv[1] && (process.argv[1] === fileURLToPath(import.meta.url) || process.argv[1].endsWith("cli.js") || process.argv[1].endsWith("cli.ts"))) {
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { EslintConfigOptions, FlatConfigArray } from '@santi020k/eslint-config-core';
2
2
  export { DetectedFrameworkName, EslintConfigOptions, Extension, FlatConfigArray, Format, ImportedFramework, Library, NextMode, Preset, ReactConfigKeys, Runtime, Setting, Testing, Tool, coreConfig, createCoreConfig, detectProjectOptions, getGlobalsForRuntime, gitignore, hasReactConfig, jsConfig } from '@santi020k/eslint-config-core';
3
+ export { AGENT_TARGETS, AgentTarget, GenerateSkillOptions, GenerateSkillResult, generateAgentSkills, generateSkillContent } from './agent-skill-generator.js';
3
4
  export { tsConfig, typescriptConfig } from '@santi020k/eslint-config-typescript';
4
5
  export { bestPractices, cspell, cypress, graphql, i18next, jest, jsdoc, jsonc, markdown, mdx, perfectionist, playwright, prettier, regexp, security, sonarjs, stencil, storybook, swagger, tailwind, tanstackQuery, tanstackRouter, testingLibrary, toml, unicorn, vitest, yaml } from '@santi020k/eslint-config-optionals';
5
6
 
package/dist/index.js CHANGED
@@ -11,6 +11,11 @@ import {
11
11
  Setting
12
12
  } from "@santi020k/eslint-config-core";
13
13
  import { createTypescriptConfig } from "@santi020k/eslint-config-typescript";
14
+ import {
15
+ AGENT_TARGETS,
16
+ generateAgentSkills,
17
+ generateSkillContent
18
+ } from "./agent-skill-generator.js";
14
19
  import {
15
20
  coreConfig as coreConfig2,
16
21
  createCoreConfig as createCoreConfig2,
@@ -104,6 +109,7 @@ const eslintConfig = (options) => {
104
109
  });
105
110
  const expoParam = resolveFramework("expo", frameworks.expo);
106
111
  const nestParam = resolveFramework("nest", frameworks.nest);
112
+ const honoParam = resolveFramework("hono", frameworks.hono, { runtime });
107
113
  const vueParam = resolveFramework("vue", frameworks.vue);
108
114
  const svelteParam = resolveFramework("svelte", frameworks.svelte);
109
115
  const solidParam = resolveFramework("solid", frameworks.solid);
@@ -130,6 +136,7 @@ const eslintConfig = (options) => {
130
136
  ...nextParam,
131
137
  ...expoParam,
132
138
  ...nestParam,
139
+ ...honoParam,
133
140
  ...vueParam,
134
141
  ...svelteParam,
135
142
  ...solidParam,
@@ -164,6 +171,7 @@ const eslintConfig = (options) => {
164
171
  return applyStrictMode(configs, strict);
165
172
  };
166
173
  export {
174
+ AGENT_TARGETS,
167
175
  Extension,
168
176
  Format,
169
177
  Library,
@@ -181,6 +189,8 @@ export {
181
189
  cypress,
182
190
  detectProjectOptions2 as detectProjectOptions,
183
191
  eslintConfig,
192
+ generateAgentSkills,
193
+ generateSkillContent,
184
194
  getGlobalsForRuntime,
185
195
  gitignore2 as gitignore,
186
196
  graphql,
package/dist/resolvers.js CHANGED
@@ -39,6 +39,11 @@ const resolvePreset = (preset) => {
39
39
  typescript: true,
40
40
  runtime: Runtime.Browser
41
41
  };
42
+ case Preset.Worker:
43
+ return {
44
+ typescript: true,
45
+ runtime: Runtime.Worker
46
+ };
42
47
  case Preset.All:
43
48
  return {
44
49
  typescript: true,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@santi020k/eslint-config-basic",
3
- "version": "1.4.0",
4
- "description": "A composable ESLint 9/10+ Flat Config package for JavaScript, TypeScript, React, Next.js, Vue, Svelte, Solid, Angular, Astro, Expo, NestJS, Qwik, and Remix.",
3
+ "version": "1.6.0",
4
+ "description": "A composable ESLint 9/10+ Flat Config package for JavaScript, TypeScript, React, Next.js, Vue, Svelte, Solid, Angular, Astro, Expo, NestJS, Hono, Qwik, and Remix.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.js",
@@ -23,22 +23,23 @@
23
23
  "dependencies": {
24
24
  "@typescript-eslint/utils": "^8.59.0",
25
25
  "eslint": "^10.2.1",
26
- "@santi020k/eslint-config-core": "^1.4.0",
27
- "@santi020k/eslint-config-optionals": "^1.4.0",
28
- "@santi020k/eslint-config-typescript": "^1.4.0"
26
+ "@santi020k/eslint-config-core": "^1.6.0",
27
+ "@santi020k/eslint-config-optionals": "^1.6.0",
28
+ "@santi020k/eslint-config-typescript": "^1.6.0"
29
29
  },
30
30
  "peerDependencies": {
31
- "@santi020k/eslint-config-angular": "^1.4.0",
32
- "@santi020k/eslint-config-astro": "^1.4.0",
33
- "@santi020k/eslint-config-expo": "^1.4.0",
34
- "@santi020k/eslint-config-nest": "^1.4.0",
35
- "@santi020k/eslint-config-next": "^1.4.0",
36
- "@santi020k/eslint-config-qwik": "^1.4.0",
37
- "@santi020k/eslint-config-react": "^1.4.0",
38
- "@santi020k/eslint-config-remix": "^1.4.0",
39
- "@santi020k/eslint-config-solid": "^1.4.0",
40
- "@santi020k/eslint-config-svelte": "^1.4.0",
41
- "@santi020k/eslint-config-vue": "^1.4.0"
31
+ "@santi020k/eslint-config-angular": "^1.6.0",
32
+ "@santi020k/eslint-config-astro": "^1.6.0",
33
+ "@santi020k/eslint-config-expo": "^1.6.0",
34
+ "@santi020k/eslint-config-hono": "^1.6.0",
35
+ "@santi020k/eslint-config-nest": "^1.6.0",
36
+ "@santi020k/eslint-config-next": "^1.6.0",
37
+ "@santi020k/eslint-config-qwik": "^1.6.0",
38
+ "@santi020k/eslint-config-react": "^1.6.0",
39
+ "@santi020k/eslint-config-remix": "^1.6.0",
40
+ "@santi020k/eslint-config-solid": "^1.6.0",
41
+ "@santi020k/eslint-config-svelte": "^1.6.0",
42
+ "@santi020k/eslint-config-vue": "^1.6.0"
42
43
  },
43
44
  "peerDependenciesMeta": {
44
45
  "@santi020k/eslint-config-angular": {
@@ -50,6 +51,9 @@
50
51
  "@santi020k/eslint-config-expo": {
51
52
  "optional": true
52
53
  },
54
+ "@santi020k/eslint-config-hono": {
55
+ "optional": true
56
+ },
53
57
  "@santi020k/eslint-config-nest": {
54
58
  "optional": true
55
59
  },
@@ -84,7 +88,7 @@
84
88
  "access": "public"
85
89
  },
86
90
  "engines": {
87
- "node": ">=24.0.0"
91
+ "node": ">=22.18.0"
88
92
  },
89
93
  "license": "MIT",
90
94
  "author": "santi020k",
package/dist/cli.d.ts DELETED
@@ -1,4 +0,0 @@
1
- declare const handleInit: (cwd?: string) => void;
2
- declare const handleUpdate: (cwd?: string) => void;
3
-
4
- export { handleInit, handleUpdate };