From 01aecafe4c8c1ca7dd2173969b13e186c12111cf Mon Sep 17 00:00:00 2001 From: Qingyu Wang <40660121+colinaaa@users.noreply.github.com> Date: Thu, 9 Oct 2025 17:07:45 +0800 Subject: [PATCH 1/2] feat: support AGENTS.md --- src/index.ts | 115 +++++++++++++++++ template-biome/AGENTS.md | 6 + template-eslint/AGENTS.md | 6 + template-prettier/AGENTS.md | 5 + test/agents.test.ts | 118 ++++++++++++++++++ test/fixtures/agents-md/package.json | 4 + .../agents-md/template-common/AGENTS.md | 10 ++ .../agents-md/template-common/package.json | 4 + .../agents-md/template-vanilla/AGENTS.md | 5 + .../agents-md/template-vanilla/package.json | 4 + 10 files changed, 277 insertions(+) create mode 100644 template-biome/AGENTS.md create mode 100644 template-eslint/AGENTS.md create mode 100644 template-prettier/AGENTS.md create mode 100644 test/agents.test.ts create mode 100644 test/fixtures/agents-md/package.json create mode 100644 test/fixtures/agents-md/template-common/AGENTS.md create mode 100644 test/fixtures/agents-md/template-common/package.json create mode 100644 test/fixtures/agents-md/template-vanilla/AGENTS.md create mode 100644 test/fixtures/agents-md/template-vanilla/package.json diff --git a/src/index.ts b/src/index.ts index b93d8cc..7fe67c2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -254,6 +254,8 @@ export async function create({ }); const packageRoot = path.resolve(__dirname, '..'); + const agentsMdSearchDirs = [srcFolder, commonFolder]; + for (const tool of tools) { const toolFolder = path.join(packageRoot, `template-${tool}`); @@ -275,6 +277,8 @@ export async function create({ isMergePackageJson: true, }); + agentsMdSearchDirs.push(toolFolder); + agentsMdSearchDirs.push(subFolder); continue; } @@ -286,6 +290,8 @@ export async function create({ isMergePackageJson: true, }); + agentsMdSearchDirs.push(toolFolder); + if (tool === 'biome') { await fs.promises.rename( path.join(distFolder, 'biome.json.template'), @@ -294,6 +300,13 @@ export async function create({ } } + const agentsFiles = collectAgentsFiles(agentsMdSearchDirs); + if (agentsFiles.length > 0) { + const mergedAgents = mergeAgentsFiles(agentsFiles); + const agentsPath = path.join(distFolder, 'AGENTS.md'); + fs.writeFileSync(agentsPath, `${mergedAgents}\n`); + } + const nextSteps = noteInformation ? noteInformation : [ @@ -460,3 +473,105 @@ const updatePackageJson = ( fs.writeFileSync(pkgJsonPath, `${JSON.stringify(pkg, null, 2)}\n`); }; + +/** + * Read AGENTS.md files from template directories + */ +function readAgentsFile(filePath: string): string | null { + if (!fs.existsSync(filePath)) { + return null; + } + return fs.readFileSync(filePath, 'utf-8'); +} + +/** + * Parse AGENTS.md content and extract sections + */ +function parseAgentsContent( + content: string, +): Record { + const sections: Record = {}; + const lines = content.split('\n'); + let currentKey = ''; + let currentTitle = ''; + let currentContent: string[] = []; + + for (const line of lines) { + const sectionMatch = line.match(/^##\s+(.+)$/); + if (sectionMatch) { + if (currentKey) { + sections[currentKey] = { + title: currentTitle, + content: currentContent.join('\n').trim(), + }; + } + currentTitle = sectionMatch[1]; + currentKey = sectionMatch[1].toLowerCase(); + currentContent = []; + } else if (currentKey) { + currentContent.push(line); + } + } + + if (currentKey) { + sections[currentKey] = { + title: currentTitle, + content: currentContent.join('\n').trim(), + }; + } + + return sections; +} + +/** + * Merge AGENTS.md files from multiple sources + */ +function mergeAgentsFiles(agentsFiles: string[]): string { + const allSections: Record = {}; + + for (const fileContent of agentsFiles) { + if (!fileContent) continue; + const sections = parseAgentsContent(fileContent); + + for (const [key, section] of Object.entries(sections)) { + if (!allSections[key]) { + allSections[key] = { title: section.title, contents: [] }; + } + if ( + section.content && + !allSections[key].contents.includes(section.content) + ) { + allSections[key].contents.push(section.content); + } + } + } + + const result: string[] = []; + + for (const [, section] of Object.entries(allSections)) { + result.push(`## ${section.title}`); + result.push(''); + for (const content of section.contents) { + result.push(content); + result.push(''); + } + } + + return result.join('\n').trim(); +} + +/** + * Collect AGENTS.md files from template directories + */ +function collectAgentsFiles(agentsMdSearchDirs: string[]): string[] { + const agentsFiles: string[] = []; + + for (const dir of agentsMdSearchDirs) { + const agentsContent = readAgentsFile(path.join(dir, 'AGENTS.md')); + if (agentsContent) { + agentsFiles.push(agentsContent); + } + } + + return agentsFiles; +} diff --git a/template-biome/AGENTS.md b/template-biome/AGENTS.md new file mode 100644 index 0000000..14e06c6 --- /dev/null +++ b/template-biome/AGENTS.md @@ -0,0 +1,6 @@ +## Tools + +### Biome +- Run `npm run lint` to lint your code +- Run `npm run format` to format your code +- Configuration file: `biome.json` \ No newline at end of file diff --git a/template-eslint/AGENTS.md b/template-eslint/AGENTS.md new file mode 100644 index 0000000..3a6bf32 --- /dev/null +++ b/template-eslint/AGENTS.md @@ -0,0 +1,6 @@ +## Tools + +### ESLint +- Run `npm run lint` to lint your code +- Configuration file: `eslint.config.mjs` +- Supports JavaScript and TypeScript \ No newline at end of file diff --git a/template-prettier/AGENTS.md b/template-prettier/AGENTS.md new file mode 100644 index 0000000..8a90b05 --- /dev/null +++ b/template-prettier/AGENTS.md @@ -0,0 +1,5 @@ +## Tools + +### Prettier +- Run `npm run format` to format your code +- Configuration in `.prettierrc` diff --git a/test/agents.test.ts b/test/agents.test.ts new file mode 100644 index 0000000..c2d9367 --- /dev/null +++ b/test/agents.test.ts @@ -0,0 +1,118 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { assert, beforeEach, test } from '@rstest/core'; +import { create } from '../dist/index.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const testDir = path.join(__dirname, 'temp'); +const fixturesDir = path.join(__dirname, 'fixtures', 'agents-md'); + +beforeEach(() => { + // Clean up test directory before each test + if (fs.existsSync(testDir)) { + fs.rmSync(testDir, { recursive: true }); + } + fs.mkdirSync(testDir, { recursive: true }); + + // Store original argv + const originalArgv = process.argv; + + // Return cleanup function + return () => { + // Restore original argv and clean up + process.argv = originalArgv; + if (fs.existsSync(testDir)) { + fs.rmSync(testDir, { recursive: true }); + } + }; +}); + +test('should generate AGENTS.md with no tools selected', async () => { + const projectDir = path.join(testDir, 'no-tools'); + process.argv = ['node', 'test', '--dir', projectDir, '--template', 'vanilla']; + + await create({ + name: 'test', + root: fixturesDir, + templates: ['vanilla'], + getTemplateName: async () => 'vanilla', + mapESLintTemplate: () => null, + }); + + const agentsPath = path.join(projectDir, 'AGENTS.md'); + assert.strictEqual(fs.existsSync(agentsPath), true); + + const content = fs.readFileSync(agentsPath, 'utf-8'); + assert.match(content, /## Template Info/); + assert.match(content, /## Development/); + // template-common has Tools section + assert.match(content, /## Tools/); + assert.match(content, /### Common Tools/); +}); + +test('should generate AGENTS.md with single tool selected', async () => { + const projectDir = path.join(testDir, 'single-tool'); + process.argv = [ + 'node', + 'test', + '--dir', + projectDir, + '--template', + 'vanilla', + '--tools', + 'biome', + ]; + + await create({ + name: 'test', + root: fixturesDir, + templates: ['vanilla'], + getTemplateName: async () => 'vanilla', + mapESLintTemplate: () => null, + }); + + const agentsPath = path.join(projectDir, 'AGENTS.md'); + assert.strictEqual(fs.existsSync(agentsPath), true); + + const content = fs.readFileSync(agentsPath, 'utf-8'); + assert.match(content, /## Template Info/); + assert.match(content, /## Development/); + assert.match(content, /## Tools/); + assert.match(content, /### Common Tools/); // from template-common + assert.match(content, /### Biome/); // from template-biome +}); + +test('should generate AGENTS.md with eslint tool and template mapping', async () => { + const projectDir = path.join(testDir, 'eslint-tool'); + process.argv = [ + 'node', + 'test', + '--dir', + projectDir, + '--template', + 'vanilla', + '--tools', + 'eslint', + ]; + + await create({ + name: 'test', + root: fixturesDir, + templates: ['vanilla'], + getTemplateName: async () => 'vanilla', + mapESLintTemplate: (templateName) => { + if (templateName === 'vanilla') return 'vanilla-ts'; + return null; + }, + }); + + const agentsPath = path.join(projectDir, 'AGENTS.md'); + assert.strictEqual(fs.existsSync(agentsPath), true); + + const content = fs.readFileSync(agentsPath, 'utf-8'); + assert.match(content, /## Template Info/); + assert.match(content, /## Development/); + assert.match(content, /## Tools/); + assert.match(content, /### ESLint/); // from template-eslint/AGENTS.md +}); diff --git a/test/fixtures/agents-md/package.json b/test/fixtures/agents-md/package.json new file mode 100644 index 0000000..f8e753d --- /dev/null +++ b/test/fixtures/agents-md/package.json @@ -0,0 +1,4 @@ +{ + "name": "test-fixtures-agents-md", + "version": "1.0.0" +} diff --git a/test/fixtures/agents-md/template-common/AGENTS.md b/test/fixtures/agents-md/template-common/AGENTS.md new file mode 100644 index 0000000..0507fb9 --- /dev/null +++ b/test/fixtures/agents-md/template-common/AGENTS.md @@ -0,0 +1,10 @@ +## Development + +### Common Development +- Common development instructions +- Available in all templates + +## Tools + +### Common Tools +- Tools that apply to all templates \ No newline at end of file diff --git a/test/fixtures/agents-md/template-common/package.json b/test/fixtures/agents-md/template-common/package.json new file mode 100644 index 0000000..f4138df --- /dev/null +++ b/test/fixtures/agents-md/template-common/package.json @@ -0,0 +1,4 @@ +{ + "name": "test-common", + "version": "1.0.0" +} diff --git a/test/fixtures/agents-md/template-vanilla/AGENTS.md b/test/fixtures/agents-md/template-vanilla/AGENTS.md new file mode 100644 index 0000000..6a8a8c8 --- /dev/null +++ b/test/fixtures/agents-md/template-vanilla/AGENTS.md @@ -0,0 +1,5 @@ +## Template Info + +### Vanilla Template +- This is vanilla template specific content +- Only available in vanilla template \ No newline at end of file diff --git a/test/fixtures/agents-md/template-vanilla/package.json b/test/fixtures/agents-md/template-vanilla/package.json new file mode 100644 index 0000000..65651ce --- /dev/null +++ b/test/fixtures/agents-md/template-vanilla/package.json @@ -0,0 +1,4 @@ +{ + "name": "test-vanilla", + "version": "1.0.0" +} From c3ac5b495bfbb21916dc8c7bc6b35457df9332b9 Mon Sep 17 00:00:00 2001 From: Qingyu Wang <40660121+colinaaa@users.noreply.github.com> Date: Thu, 9 Oct 2025 19:51:11 +0800 Subject: [PATCH 2/2] chore: update prompts Remove prompt for configuration --- template-biome/AGENTS.md | 2 +- template-eslint/AGENTS.md | 3 +-- template-prettier/AGENTS.md | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/template-biome/AGENTS.md b/template-biome/AGENTS.md index 14e06c6..c26c433 100644 --- a/template-biome/AGENTS.md +++ b/template-biome/AGENTS.md @@ -1,6 +1,6 @@ ## Tools ### Biome + - Run `npm run lint` to lint your code - Run `npm run format` to format your code -- Configuration file: `biome.json` \ No newline at end of file diff --git a/template-eslint/AGENTS.md b/template-eslint/AGENTS.md index 3a6bf32..2226fc4 100644 --- a/template-eslint/AGENTS.md +++ b/template-eslint/AGENTS.md @@ -1,6 +1,5 @@ ## Tools ### ESLint + - Run `npm run lint` to lint your code -- Configuration file: `eslint.config.mjs` -- Supports JavaScript and TypeScript \ No newline at end of file diff --git a/template-prettier/AGENTS.md b/template-prettier/AGENTS.md index 8a90b05..7b57953 100644 --- a/template-prettier/AGENTS.md +++ b/template-prettier/AGENTS.md @@ -1,5 +1,5 @@ ## Tools ### Prettier + - Run `npm run format` to format your code -- Configuration in `.prettierrc`