|
| 1 | +import fs from 'node:fs'; |
| 2 | +import path from 'node:path'; |
| 3 | +import { i18n, localeKeys } from './locale'; |
| 4 | + |
| 5 | +// Codemod for existing projects: add or refresh AGENTS.md / CLAUDE.md so AI |
| 6 | +// coding agents are pointed at the version-matched docs bundled in |
| 7 | +// node_modules/@modern-js/app-tools/docs/. `@modern-js/create` only scaffolds |
| 8 | +// these for new projects; this brings existing projects up to date on upgrade. |
| 9 | +// |
| 10 | +// The Modern.js-managed rules live between these markers. Everything outside |
| 11 | +// them belongs to the user and is never touched, and the block itself is |
| 12 | +// replaced in place on re-run, so this command is idempotent. |
| 13 | +const BEGIN = '<!-- BEGIN:modernjs-agent-rules -->'; |
| 14 | +const END = '<!-- END:modernjs-agent-rules -->'; |
| 15 | +const CLAUDE_IMPORT = '@AGENTS.md'; |
| 16 | + |
| 17 | +// Read the managed block from the create template, so the codemod and the |
| 18 | +// scaffolding share a single source of truth. |
| 19 | +function readManagedBlock(templateDir: string): string { |
| 20 | + const tpl = fs.readFileSync(path.join(templateDir, 'AGENTS.md'), 'utf-8'); |
| 21 | + const begin = tpl.indexOf(BEGIN); |
| 22 | + const end = tpl.indexOf(END); |
| 23 | + if (begin === -1 || end === -1 || end < begin) { |
| 24 | + throw new Error( |
| 25 | + 'template/AGENTS.md is missing the modernjs-agent-rules markers', |
| 26 | + ); |
| 27 | + } |
| 28 | + return tpl.slice(begin, end + END.length); |
| 29 | +} |
| 30 | + |
| 31 | +function report(key: string, file: string): void { |
| 32 | + console.log(i18n.t(key, { file })); |
| 33 | +} |
| 34 | + |
| 35 | +// Create AGENTS.md, refresh the managed block if present, or append it while |
| 36 | +// preserving the user's own content. |
| 37 | +function applyAgentsMd(targetDir: string, block: string): void { |
| 38 | + const file = path.join(targetDir, 'AGENTS.md'); |
| 39 | + if (!fs.existsSync(file)) { |
| 40 | + fs.writeFileSync(file, `${block}\n`, 'utf-8'); |
| 41 | + report(localeKeys.agentsCmd.created, 'AGENTS.md'); |
| 42 | + return; |
| 43 | + } |
| 44 | + |
| 45 | + const content = fs.readFileSync(file, 'utf-8'); |
| 46 | + const begin = content.indexOf(BEGIN); |
| 47 | + const end = content.indexOf(END); |
| 48 | + if (begin !== -1 && end !== -1 && end > begin) { |
| 49 | + const next = |
| 50 | + content.slice(0, begin) + block + content.slice(end + END.length); |
| 51 | + if (next === content) { |
| 52 | + report(localeKeys.agentsCmd.unchanged, 'AGENTS.md'); |
| 53 | + } else { |
| 54 | + fs.writeFileSync(file, next, 'utf-8'); |
| 55 | + report(localeKeys.agentsCmd.updatedBlock, 'AGENTS.md'); |
| 56 | + } |
| 57 | + return; |
| 58 | + } |
| 59 | + |
| 60 | + // No managed block yet: append ours, keeping the existing content intact. |
| 61 | + const base = content.replace(/\s*$/, ''); |
| 62 | + fs.writeFileSync(file, `${base}\n\n${block}\n`, 'utf-8'); |
| 63 | + report(localeKeys.agentsCmd.appendedBlock, 'AGENTS.md'); |
| 64 | +} |
| 65 | + |
| 66 | +// Create CLAUDE.md as an @AGENTS.md import, or add the import to an existing |
| 67 | +// one (Claude Code reads CLAUDE.md, not AGENTS.md, so the bridge is required). |
| 68 | +function applyClaudeMd(targetDir: string): void { |
| 69 | + const file = path.join(targetDir, 'CLAUDE.md'); |
| 70 | + if (!fs.existsSync(file)) { |
| 71 | + fs.writeFileSync(file, `${CLAUDE_IMPORT}\n`, 'utf-8'); |
| 72 | + report(localeKeys.agentsCmd.created, 'CLAUDE.md'); |
| 73 | + return; |
| 74 | + } |
| 75 | + |
| 76 | + const content = fs.readFileSync(file, 'utf-8'); |
| 77 | + if (content.split('\n').some(line => line.trim() === CLAUDE_IMPORT)) { |
| 78 | + report(localeKeys.agentsCmd.unchanged, 'CLAUDE.md'); |
| 79 | + return; |
| 80 | + } |
| 81 | + fs.writeFileSync( |
| 82 | + file, |
| 83 | + `${CLAUDE_IMPORT}\n\n${content.replace(/^\s*/, '')}`, |
| 84 | + 'utf-8', |
| 85 | + ); |
| 86 | + report(localeKeys.agentsCmd.linked, 'CLAUDE.md'); |
| 87 | +} |
| 88 | + |
| 89 | +export function runAgentsMd(templateDir: string, targetDir: string): void { |
| 90 | + if (!fs.existsSync(targetDir)) { |
| 91 | + console.error( |
| 92 | + i18n.t(localeKeys.agentsCmd.targetNotFound, { dir: targetDir }), |
| 93 | + ); |
| 94 | + process.exit(1); |
| 95 | + } |
| 96 | + const block = readManagedBlock(templateDir); |
| 97 | + applyAgentsMd(targetDir, block); |
| 98 | + applyClaudeMd(targetDir); |
| 99 | + console.log(''); |
| 100 | + console.log(i18n.t(localeKeys.agentsCmd.done)); |
| 101 | +} |
0 commit comments