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(); }