- Bug修复: formatChangeStatus 使用实际配置而非 defaultConfig - 统一 assembler 中所有错误抛出为 CommandError - 提取 writeIfChanged 到 adapters/utils.ts,消除 claude-code/opencode 重复代码 - 导出 SUPPORTED_TOOLS,cli.ts update 命令复用同一工具注册表 - 提取 mapError/mapCacError 函数,支持单元测试 - 补充 claude-code 适配器测试(10 个用例) - 补充 validateChangeName、formatChangeStatus、suggestNextStep、mapError 单元测试(18 个用例) - 共新增 3 个测试文件,测试从 96 增至 133,全部通过
158 lines
5.0 KiB
TypeScript
158 lines
5.0 KiB
TypeScript
import { existsSync } from "node:fs";
|
||
import { readFile } from "node:fs/promises";
|
||
import { join } from "node:path";
|
||
import type { RuneConfig } from "../types.ts";
|
||
import { CommandError } from "../cli/errors.ts";
|
||
import { getChangeDir } from "./config.ts";
|
||
import { parseTasks } from "./task-parser.ts";
|
||
|
||
export function assembleDiscussPrompt(config: RuneConfig): string {
|
||
const discuss = config.stages.discuss;
|
||
if (!discuss) throw new CommandError("讨论阶段未配置", {
|
||
hint: "请在 .rune/config.yaml 中配置 stages.discuss",
|
||
});
|
||
return discuss.prompt;
|
||
}
|
||
|
||
export async function assemblePlanPrompt(
|
||
config: RuneConfig,
|
||
projectRoot: string,
|
||
changeName: string,
|
||
documentName: string,
|
||
): Promise<string> {
|
||
const plan = config.stages.plan;
|
||
if (!plan) throw new CommandError("规划阶段未配置", {
|
||
hint: "请在 .rune/config.yaml 中配置 stages.plan",
|
||
});
|
||
|
||
const doc = plan.documents.find((d) => d.name === documentName);
|
||
if (!doc) {
|
||
throw new CommandError(`文档 "${documentName}" 不在配置的 plan.documents 中`, {
|
||
hint: `可用文档:${plan.documents.map((d) => d.name).join(", ")}`,
|
||
});
|
||
}
|
||
|
||
const changeDir = getChangeDir(projectRoot, changeName);
|
||
const parts: string[] = [];
|
||
|
||
parts.push(`# 规划阶段:${changeName}`);
|
||
parts.push("");
|
||
parts.push(`## 文档:${doc.name}.md`);
|
||
parts.push(doc.prompt);
|
||
|
||
const docPath = join(changeDir, `${doc.name}.md`);
|
||
if (existsSync(docPath)) {
|
||
const existing = await readFile(docPath, "utf-8");
|
||
parts.push(`\n### 已有内容(请在此基础上修订):\n${existing}`);
|
||
}
|
||
|
||
if (doc.template) {
|
||
const rendered = doc.template.replace(/\{\{change-name\}\}/g, changeName);
|
||
parts.push(`\n### 格式模板:\n${rendered}`);
|
||
}
|
||
|
||
if (doc.depend && doc.depend.length > 0) {
|
||
parts.push("\n---\n");
|
||
parts.push("## 依赖说明\n");
|
||
parts.push("本文档依赖以下前置文档:");
|
||
|
||
for (const dep of doc.depend) {
|
||
const depPath = join(changeDir, `${dep}.md`);
|
||
const depCompleted = existsSync(depPath);
|
||
const status = depCompleted ? "已完成" : "未完成";
|
||
parts.push(`- ${dep}.md(${status})`);
|
||
}
|
||
|
||
parts.push("\n请先阅读已完成的前置文档,确保内容一致。");
|
||
}
|
||
|
||
parts.push(`\n请将文档写入目录:${changeDir}`);
|
||
return parts.join("\n");
|
||
}
|
||
|
||
export async function assembleBuildPrompt(
|
||
config: RuneConfig,
|
||
projectRoot: string,
|
||
changeName: string,
|
||
): Promise<string> {
|
||
const build = config.stages.build;
|
||
if (!build) {
|
||
throw new CommandError("构建阶段未配置", {
|
||
hint: "请在 .rune/config.yaml 中配置 stages.build",
|
||
});
|
||
}
|
||
|
||
const changeDir = getChangeDir(projectRoot, changeName);
|
||
const taskPath = join(changeDir, "task.md");
|
||
|
||
let taskContent: string;
|
||
try {
|
||
taskContent = await readFile(taskPath, "utf-8");
|
||
} catch {
|
||
throw new CommandError(`变更 "${changeName}" 尚未完成规划,task.md 不存在`, {
|
||
hint: `请先完成规划阶段:rune plan ${changeName} 生成所有规划文档`,
|
||
});
|
||
}
|
||
|
||
const tasks = parseTasks(taskContent);
|
||
const pendingTasks = tasks.filter((t) => !t.checked);
|
||
|
||
if (pendingTasks.length === 0) {
|
||
return `所有任务已完成。变更 "${changeName}" 可以归档。`;
|
||
}
|
||
|
||
const parts: string[] = [];
|
||
parts.push(`# 构建阶段:${changeName}\n`);
|
||
parts.push(build.prompt);
|
||
parts.push(`\n## 任务列表\n`);
|
||
parts.push(taskContent);
|
||
parts.push(`\n## 待执行任务(共 ${pendingTasks.length} 项)`);
|
||
for (const task of pendingTasks) {
|
||
parts.push(`- [ ] ${task.text}`);
|
||
}
|
||
parts.push(
|
||
`\n请从第一个待执行任务开始。完成后更新 ${taskPath} 中的 checkbox。`,
|
||
);
|
||
|
||
return parts.join("\n");
|
||
}
|
||
|
||
export async function assembleArchivePrompt(
|
||
config: RuneConfig,
|
||
projectRoot: string,
|
||
changeName: string,
|
||
): Promise<string> {
|
||
const archive = config.stages.archive;
|
||
if (!archive) throw new CommandError("归档阶段未配置", {
|
||
hint: "请在 .rune/config.yaml 中配置 stages.archive",
|
||
});
|
||
|
||
const changeDir = getChangeDir(projectRoot, changeName);
|
||
const taskPath = join(changeDir, "task.md");
|
||
|
||
const parts: string[] = [];
|
||
parts.push(`# 归档阶段:${changeName}\n`);
|
||
|
||
try {
|
||
const taskContent = await readFile(taskPath, "utf-8");
|
||
const tasks = parseTasks(taskContent);
|
||
const incompleteTasks = tasks.filter((t) => !t.checked);
|
||
if (incompleteTasks.length > 0) {
|
||
parts.push("## ⚠️ 警告:存在未完成的任务\n");
|
||
parts.push(`以下 ${incompleteTasks.length} 个任务尚未完成:`);
|
||
for (const t of incompleteTasks) {
|
||
parts.push(`- [ ] ${t.text}`);
|
||
}
|
||
parts.push("");
|
||
parts.push("请询问用户是否确认在任务未全部完成的情况下归档。");
|
||
parts.push("如用户确认,则继续归档;否则中止并返回构建阶段。");
|
||
parts.push("");
|
||
}
|
||
} catch {
|
||
// task.md 不存在时不追加警告
|
||
}
|
||
|
||
parts.push(archive.prompt);
|
||
return parts.join("\n");
|
||
}
|