diff --git a/tests/agent/tier1-command.ts b/tests/agent/tier1-command.ts new file mode 100644 index 0000000..d76fd4f --- /dev/null +++ b/tests/agent/tier1-command.ts @@ -0,0 +1,119 @@ +import { mkdir, writeFile, readFile, rename } from "node:fs/promises"; +import { join } from "node:path"; +import type { RuneConfig, DocumentConfig } from "../../src/types.ts"; +import type { AgentRunner, AgentResult } from "./runner.ts"; +import { getChangeDir, getArchiveDir } from "../../src/core/config.ts"; +import { parseTasks } from "../../src/core/task-parser.ts"; + +export class CommandLevelRunner implements AgentRunner { + readonly tier = 1; + + async runPlan( + projectDir: string, + changeName: string, + docName: string, + config: RuneConfig, + ): Promise { + const changeDir = getChangeDir(projectDir, changeName); + await mkdir(changeDir, { recursive: true }); + + const planStage = config.stages.plan; + if (!planStage) { + throw new Error("plan 阶段未配置"); + } + + const docConfig = planStage.documents.find((d) => d.name === docName); + if (!docConfig) { + throw new Error(`文档 "${docName}" 未在 plan.documents 中配置`); + } + + const content = this.renderDocument(docConfig, changeName); + const filePath = join(changeDir, `${docName}.md`); + await writeFile(filePath, content); + + return { files: [`${docName}.md`] }; + } + + async runBuild( + projectDir: string, + changeName: string, + _config: RuneConfig, + ): Promise { + const changeDir = getChangeDir(projectDir, changeName); + const taskPath = join(changeDir, "task.md"); + + let taskContent: string; + try { + taskContent = await readFile(taskPath, "utf-8"); + } catch { + throw new Error(`变更 "${changeName}" 的 task.md 不存在,请先完成规划`); + } + + const tasks = parseTasks(taskContent); + const pending = tasks.filter((t) => !t.checked); + + if (pending.length === 0) { + return { files: [] }; + } + + const files: string[] = []; + for (const task of pending) { + const oldLine = `- [ ] ${task.text}`; + const newLine = `- [x] ${task.text}`; + taskContent = taskContent.replace(oldLine, newLine); + const implFile = `${task.text + .replace(/[^a-zA-Z\u4e00-\u9fa5]+/g, "-") + .replace(/^-|-$/g, "") + .toLowerCase()}.ts`; + await writeFile(join(changeDir, implFile), `// ${task.text}\n`); + files.push(implFile); + } + + await writeFile(taskPath, taskContent); + files.push("task.md"); + + return { files }; + } + + async runArchive( + projectDir: string, + changeName: string, + _config: RuneConfig, + ): Promise { + const changeDir = getChangeDir(projectDir, changeName); + const taskPath = join(changeDir, "task.md"); + + try { + const taskContent = await readFile(taskPath, "utf-8"); + const tasks = parseTasks(taskContent); + const pending = tasks.filter((t) => !t.checked); + + if (pending.length > 0) { + throw new Error(`变更 "${changeName}" 存在 ${pending.length} 个未完成任务,无法归档`); + } + } catch (e) { + if (e instanceof Error && e.message.includes("未完成任务")) { + throw e; + } + } + + const today = new Date().toISOString().slice(0, 10); + const archiveDir = getArchiveDir(projectDir); + await mkdir(archiveDir, { recursive: true }); + const dest = join(archiveDir, `${today}-${changeName}`); + await rename(changeDir, dest); + + return { files: [] }; + } + + private renderDocument(doc: DocumentConfig, changeName: string): string { + if (doc.template) { + return doc.template.replace(/\{\{change-name\}\}/g, changeName) + "\n"; + } + return `# ${doc.name}\n\n${doc.prompt}\n`; + } +} + +export function createRunner(): AgentRunner { + return new CommandLevelRunner(); +}