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..c26c433 --- /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 diff --git a/template-eslint/AGENTS.md b/template-eslint/AGENTS.md new file mode 100644 index 0000000..2226fc4 --- /dev/null +++ b/template-eslint/AGENTS.md @@ -0,0 +1,5 @@ +## Tools + +### ESLint + +- Run `npm run lint` to lint your code diff --git a/template-prettier/AGENTS.md b/template-prettier/AGENTS.md new file mode 100644 index 0000000..7b57953 --- /dev/null +++ b/template-prettier/AGENTS.md @@ -0,0 +1,5 @@ +## Tools + +### Prettier + +- Run `npm run format` to format your code 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" +}