120 lines
3.6 KiB
TypeScript
120 lines
3.6 KiB
TypeScript
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<AgentResult> {
|
|
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<AgentResult> {
|
|
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<AgentResult> {
|
|
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();
|
|
}
|