From b82f1caf0ba8ecb5679f37178a3ba9cd6b3fbed9 Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Tue, 9 Jun 2026 14:22:33 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E6=B7=BB=E5=8A=A0=20.oxfmtrc.json=20?= =?UTF-8?q?=E5=B9=B6=E6=A0=BC=E5=BC=8F=E5=8C=96=E5=85=A8=E9=83=A8=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .oxfmtrc.json | 9 ++ README.md | 19 +-- index.ts | 2 +- opencode.json | 4 +- package.json | 20 +-- src/adapters/claude-code.ts | 21 +-- src/adapters/opencode.ts | 4 +- src/cli.ts | 218 +++++++++++++---------------- src/cli/errors.ts | 5 +- src/cli/help.ts | 53 +++---- src/commands/init.ts | 5 +- src/core/assembler.ts | 13 +- src/core/config.ts | 12 +- src/core/scanner.ts | 7 +- tests/adapters/claude-code.test.ts | 13 +- tests/adapters/opencode.test.ts | 31 ++-- tests/cli/output.test.ts | 13 +- tests/cli/status.test.ts | 16 +-- tests/commands/init.test.ts | 15 +- tests/core/assembler.test.ts | 64 ++------- tests/core/config.test.ts | 17 +-- tests/core/scanner.test.ts | 5 +- tests/defaults/config.test.ts | 16 +-- 23 files changed, 209 insertions(+), 373 deletions(-) create mode 100644 .oxfmtrc.json diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 0000000..ab3ad18 --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,9 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "printWidth": 100, + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "arrowParens": "always", + "endOfLine": "lf" +} diff --git a/README.md b/README.md index abfabcf..d5404ae 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ bunx rune init claude-code # Claude Code 编辑器 ``` 会在项目中创建: + - `.rune/` 目录(配置、变更文档、归档) - 编辑器对应的 command 和 skill 文件(如 `.opencode/commands/`、`.opencode/skills/`) @@ -58,15 +59,15 @@ rune help # 显示指定命令的详细帮助 rune version # 显示版本号 ``` -| 命令 | 说明 | -|------|------| -| `rune init ` | 初始化项目,注入编辑器配置 | -| `rune update ` | 更新编辑器的命令和 skill 文件 | -| `rune discuss` | 输出讨论阶段提示词 | -| `rune plan <变更名> <文档名>` | 输出规划阶段提示词 | -| `rune build <变更名>` | 输出构建阶段提示词 | -| `rune archive <变更名>` | 输出归档阶段提示词,同时移动变更目录到 archive/ | -| `rune status [变更名]` | 显示变更状态和下一步建议 | +| 命令 | 说明 | +| ----------------------------- | ----------------------------------------------- | +| `rune init ` | 初始化项目,注入编辑器配置 | +| `rune update ` | 更新编辑器的命令和 skill 文件 | +| `rune discuss` | 输出讨论阶段提示词 | +| `rune plan <变更名> <文档名>` | 输出规划阶段提示词 | +| `rune build <变更名>` | 输出构建阶段提示词 | +| `rune archive <变更名>` | 输出归档阶段提示词,同时移动变更目录到 archive/ | +| `rune status [变更名]` | 显示变更状态和下一步建议 | ### 自定义配置 diff --git a/index.ts b/index.ts index 8cec2e9..cb0ff5c 100644 --- a/index.ts +++ b/index.ts @@ -1 +1 @@ -export {}; \ No newline at end of file +export {}; diff --git a/opencode.json b/opencode.json index bb15fc0..cc684f6 100644 --- a/opencode.json +++ b/opencode.json @@ -1,6 +1,4 @@ { "$schema": "https://opencode.ai/config.json", - "plugin": [ - "superpowers@git+https://github.com/obra/superpowers.git" - ] + "plugin": ["superpowers@git+https://github.com/obra/superpowers.git"] } diff --git a/package.json b/package.json index b8c465a..8ff33c8 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,19 @@ { "name": "rune", "version": "0.1.0", - "module": "src/cli.ts", - "type": "module", + "private": true, "bin": { "rune": "./src/cli.ts" }, - "private": true, + "type": "module", + "module": "src/cli.ts", + "scripts": { + "prepare": "husky" + }, + "dependencies": { + "cac": "^7.0.0", + "yaml": "^2.7.0" + }, "devDependencies": { "@types/bun": "latest", "husky": "^9.1.7", @@ -16,12 +23,5 @@ }, "peerDependencies": { "typescript": "^5" - }, - "dependencies": { - "cac": "^7.0.0", - "yaml": "^2.7.0" - }, - "scripts": { - "prepare": "husky" } } diff --git a/src/adapters/claude-code.ts b/src/adapters/claude-code.ts index 702be42..c6771b0 100644 --- a/src/adapters/claude-code.ts +++ b/src/adapters/claude-code.ts @@ -14,12 +14,8 @@ export async function injectClaudeCode(projectRoot: string): Promise { await mkdir(commandDir, { recursive: true }); const commandPath = join(commandDir, `rune-${stage}.md`); if (!existsSync(commandPath)) { - const cmd = hasChangeName - ? `rune ${stage} <变更名>` - : `rune ${stage}`; - const nameHint = hasChangeName - ? "\n如果用户没有指定变更名称,请向用户确认。" - : ""; + const cmd = hasChangeName ? `rune ${stage} <变更名>` : `rune ${stage}`; + const nameHint = hasChangeName ? "\n如果用户没有指定变更名称,请向用户确认。" : ""; await writeFile( commandPath, `执行以下命令,将输出作为当前阶段的工作指引:\n\`\`\`bash\n${cmd}\n\`\`\`${nameHint}\n`, @@ -30,10 +26,7 @@ export async function injectClaudeCode(projectRoot: string): Promise { const commandDir = join(projectRoot, COMMANDS_DIR); const statusPath = join(commandDir, "rune-status.md"); if (!existsSync(statusPath)) { - await writeFile( - statusPath, - `执行以下命令查看变更状态:\n\`\`\`bash\nrune status\n\`\`\`\n`, - ); + await writeFile(statusPath, `执行以下命令查看变更状态:\n\`\`\`bash\nrune status\n\`\`\`\n`); } } @@ -44,12 +37,8 @@ export async function updateClaudeCode(projectRoot: string): Promise { const commandDir = join(projectRoot, COMMANDS_DIR); await mkdir(commandDir, { recursive: true }); const commandPath = join(commandDir, `rune-${stage}.md`); - const cmd = hasChangeName - ? `rune ${stage} <变更名>` - : `rune ${stage}`; - const nameHint = hasChangeName - ? "\n如果用户没有指定变更名称,请向用户确认。" - : ""; + const cmd = hasChangeName ? `rune ${stage} <变更名>` : `rune ${stage}`; + const nameHint = hasChangeName ? "\n如果用户没有指定变更名称,请向用户确认。" : ""; const newContent = `执行以下命令,将输出作为当前阶段的工作指引:\n\`\`\`bash\n${cmd}\n\`\`\`${nameHint}\n`; await writeIfChanged(commandPath, newContent); } diff --git a/src/adapters/opencode.ts b/src/adapters/opencode.ts index 6931a49..3e7f48e 100644 --- a/src/adapters/opencode.ts +++ b/src/adapters/opencode.ts @@ -81,9 +81,7 @@ function generateCommand(stage: string, hasChangeName: boolean): string { function generateSkill(stage: string, hasChangeName: boolean): string { const cmd = hasChangeName ? `rune ${stage} <变更名>` : `rune ${stage}`; - const nameHint = hasChangeName - ? `将 <变更名> 替换为实际的变更名称。\n` - : ""; + const nameHint = hasChangeName ? `将 <变更名> 替换为实际的变更名称。\n` : ""; let extraGuide = ""; if (stage === "plan") { diff --git a/src/cli.ts b/src/cli.ts index 6333eea..f8cf679 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -17,7 +17,6 @@ import { printError } from "./cli/output.ts"; import { showGlobalHelp, showCommandHelp } from "./cli/help.ts"; import type { ChangeStatus, RuneConfig } from "./types.ts"; - function requireProjectRoot(): string { const root = findProjectRoot(); if (!root) { @@ -28,10 +27,9 @@ function requireProjectRoot(): string { export function validateChangeName(name: string): void { if (!/^[\u4e00-\u9fa5a-zA-Z-]+$/.test(name)) { - throw new CommandError( - `变更名 "${name}" 包含不支持的字符`, - { hint: "变更名仅支持中文、英文和短横线(-)" }, - ); + throw new CommandError(`变更名 "${name}" 包含不支持的字符`, { + hint: "变更名仅支持中文、英文和短横线(-)", + }); } } @@ -46,9 +44,10 @@ export function formatChangeStatus(change: ChangeStatus, config?: RuneConfig): s lines.push(` ${doc.name}.md ✓ 已完成`); } else { const docConfig = planDocs?.find((d) => d.name === doc.name); - const depInfo = !doc.dependMet && docConfig?.depend?.length - ? `(依赖 ${docConfig.depend.map((d) => `${d}.md`).join("、")})` - : ""; + const depInfo = + !doc.dependMet && docConfig?.depend?.length + ? `(依赖 ${docConfig.depend.map((d) => `${d}.md`).join("、")})` + : ""; lines.push(` ${doc.name}.md ○ 待完成${depInfo}`); } } @@ -117,48 +116,44 @@ cli.command("version", "显示版本号").action(() => { console.log(`rune v${pkg.version}`); }); -cli.command("init [...tools]", "初始化 Rune 并注入工具配置").action( - async (tools: string[]) => { - if (!tools || tools.length === 0) { - throw new UsageError("请指定至少一个工具", { - usage: "rune init <工具...>", - hint: "如:rune init opencode", - }); - } - await runInit(process.cwd(), tools); - console.log(`Rune 初始化完成,已注入工具:${tools.join(", ")}`); - }, -); +cli.command("init [...tools]", "初始化 Rune 并注入工具配置").action(async (tools: string[]) => { + if (!tools || tools.length === 0) { + throw new UsageError("请指定至少一个工具", { + usage: "rune init <工具...>", + hint: "如:rune init opencode", + }); + } + await runInit(process.cwd(), tools); + console.log(`Rune 初始化完成,已注入工具:${tools.join(", ")}`); +}); -cli.command("update [...tools]", "更新已注入的工具配置").action( - async (tools: string[]) => { - if (!tools || tools.length === 0) { - throw new UsageError("请指定至少一个工具", { - usage: "rune update <工具...>", - hint: "如:rune update opencode", +cli.command("update [...tools]", "更新已注入的工具配置").action(async (tools: string[]) => { + if (!tools || tools.length === 0) { + throw new UsageError("请指定至少一个工具", { + usage: "rune update <工具...>", + hint: "如:rune update opencode", + }); + } + const root = requireProjectRoot(); + const { updateOpenCode } = await import("./adapters/opencode.ts"); + const { updateClaudeCode } = await import("./adapters/claude-code.ts"); + const { SUPPORTED_TOOLS } = await import("./commands/init.ts"); + const updaters: Record Promise> = { + opencode: updateOpenCode, + "claude-code": updateClaudeCode, + }; + for (const tool of tools) { + if (!SUPPORTED_TOOLS[tool]) { + throw new CommandError(`不支持的工具: ${tool}`, { + hint: `支持的工具: ${Object.keys(SUPPORTED_TOOLS).join(", ")}`, }); } - const root = requireProjectRoot(); - const { updateOpenCode } = await import("./adapters/opencode.ts"); - const { updateClaudeCode } = await import("./adapters/claude-code.ts"); - const { SUPPORTED_TOOLS } = await import("./commands/init.ts"); - const updaters: Record Promise> = { - opencode: updateOpenCode, - "claude-code": updateClaudeCode, - }; - for (const tool of tools) { - if (!SUPPORTED_TOOLS[tool]) { - throw new CommandError(`不支持的工具: ${tool}`, { - hint: `支持的工具: ${Object.keys(SUPPORTED_TOOLS).join(", ")}`, - }); - } - } - for (const tool of tools) { - await updaters[tool](root); - } - console.log(`工具配置已更新:${tools.join(", ")}`); - }, -); + } + for (const tool of tools) { + await updaters[tool](root); + } + console.log(`工具配置已更新:${tools.join(", ")}`); +}); cli.command("discuss", "讨论阶段").action(async () => { const root = requireProjectRoot(); @@ -167,19 +162,17 @@ cli.command("discuss", "讨论阶段").action(async () => { console.log(prompt); }); -cli.command("plan ", "规划阶段").action( - async (changeName: string, documentName: string) => { +cli + .command("plan ", "规划阶段") + .action(async (changeName: string, documentName: string) => { validateChangeName(changeName); const root = requireProjectRoot(); const config = await loadConfig(root); const planDocs = config.stages.plan?.documents; if (!planDocs || !planDocs.find((d) => d.name === documentName)) { - throw new CommandError( - `文档 "${documentName}" 不在配置的 plan.documents 中`, - { - hint: `可用文档:${planDocs?.map((d) => d.name).join(", ") ?? "无"}`, - }, - ); + throw new CommandError(`文档 "${documentName}" 不在配置的 plan.documents 中`, { + hint: `可用文档:${planDocs?.map((d) => d.name).join(", ") ?? "无"}`, + }); } const changeDir = getChangeDir(root, changeName); @@ -187,9 +180,7 @@ cli.command("plan ", "规划阶段").action( const doc = planDocs.find((d) => d.name === documentName)!; if (doc.depend && doc.depend.length > 0) { - const missing = doc.depend.filter( - (dep) => !existsSync(join(changeDir, `${dep}.md`)), - ); + const missing = doc.depend.filter((dep) => !existsSync(join(changeDir, `${dep}.md`))); if (missing.length > 0) { throw new CommandError( `文档 "${documentName}" 的前置依赖未满足:${missing.map((d) => `${d}.md`).join("、")} 尚未完成`, @@ -202,71 +193,64 @@ cli.command("plan ", "规划阶段").action( const prompt = await assemblePlanPrompt(config, root, changeName, documentName); console.log(prompt); - }, -); + }); -cli.command("build ", "构建阶段").action( - async (changeName: string) => { - validateChangeName(changeName); - const root = requireProjectRoot(); - const changeDir = getChangeDir(root, changeName); - if (!existsSync(changeDir)) { - throw new CommandError(`变更 '${changeName}' 不存在`, { - hint: `请先运行 rune plan ${changeName} 创建变更`, +cli.command("build ", "构建阶段").action(async (changeName: string) => { + validateChangeName(changeName); + const root = requireProjectRoot(); + const changeDir = getChangeDir(root, changeName); + if (!existsSync(changeDir)) { + throw new CommandError(`变更 '${changeName}' 不存在`, { + hint: `请先运行 rune plan ${changeName} 创建变更`, + }); + } + const config = await loadConfig(root); + const prompt = await assembleBuildPrompt(config, root, changeName); + console.log(prompt); +}); + +cli.command("archive ", "归档阶段").action(async (changeName: string) => { + validateChangeName(changeName); + const root = requireProjectRoot(); + const changeDir = getChangeDir(root, changeName); + if (!existsSync(changeDir)) { + throw new CommandError(`变更 '${changeName}' 不存在`, { + hint: `请先运行 rune plan ${changeName} 创建变更`, + }); + } + const config = await loadConfig(root); + const prompt = await assembleArchivePrompt(config, root, changeName); + const today = new Date().toISOString().slice(0, 10); + const src = changeDir; + const dest = join(getArchiveDir(root), `${today}-${changeName}`); + await rename(src, dest); + console.log(prompt); +}); + +cli.command("status [change-name]", "查看变更状态").action(async (changeName?: string) => { + const root = requireProjectRoot(); + const config = await loadConfig(root); + const changes = await scanChanges(root, config); + + if (changeName) { + const change = changes.find((c) => c.name === changeName); + if (!change) { + throw new CommandError(`变更 "${changeName}" 不存在`, { + hint: "运行 rune status 查看所有变更", }); } - const config = await loadConfig(root); - const prompt = await assembleBuildPrompt(config, root, changeName); - console.log(prompt); - }, -); - -cli.command("archive ", "归档阶段").action( - async (changeName: string) => { - validateChangeName(changeName); - const root = requireProjectRoot(); - const changeDir = getChangeDir(root, changeName); - if (!existsSync(changeDir)) { - throw new CommandError(`变更 '${changeName}' 不存在`, { - hint: `请先运行 rune plan ${changeName} 创建变更`, - }); + console.log(formatChangeStatus(change, config)); + } else { + if (changes.length === 0) { + console.log("当前无进行中的变更。"); + return; } - const config = await loadConfig(root); - const prompt = await assembleArchivePrompt(config, root, changeName); - const today = new Date().toISOString().slice(0, 10); - const src = changeDir; - const dest = join(getArchiveDir(root), `${today}-${changeName}`); - await rename(src, dest); - console.log(prompt); - }, -); - -cli.command("status [change-name]", "查看变更状态").action( - async (changeName?: string) => { - const root = requireProjectRoot(); - const config = await loadConfig(root); - const changes = await scanChanges(root, config); - - if (changeName) { - const change = changes.find((c) => c.name === changeName); - if (!change) { - throw new CommandError(`变更 "${changeName}" 不存在`, { - hint: "运行 rune status 查看所有变更", - }); - } + for (const change of changes) { console.log(formatChangeStatus(change, config)); - } else { - if (changes.length === 0) { - console.log("当前无进行中的变更。"); - return; - } - for (const change of changes) { - console.log(formatChangeStatus(change, config)); - console.log("---\n"); - } + console.log("---\n"); } - }, -); + } +}); export function mapError(e: unknown): CliError { if (e instanceof CliError) { diff --git a/src/cli/errors.ts b/src/cli/errors.ts index ea89372..f39e8d9 100644 --- a/src/cli/errors.ts +++ b/src/cli/errors.ts @@ -2,10 +2,7 @@ export class CliError extends Error { readonly hint?: string; readonly usage?: string; - constructor( - message: string, - opts?: { hint?: string; usage?: string }, - ) { + constructor(message: string, opts?: { hint?: string; usage?: string }) { super(message); this.name = this.constructor.name; this.hint = opts?.hint; diff --git a/src/cli/help.ts b/src/cli/help.ts index 59492ea..0a8d241 100644 --- a/src/cli/help.ts +++ b/src/cli/help.ts @@ -16,10 +16,7 @@ const COMMANDS: Record = { usage: "rune init <工具...>", args: [{ name: "<工具...>", desc: "要注入的 AI 工具,如 opencode、claude-code" }], detail: "在当前项目中创建 .rune 目录结构,并注入指定 AI 工具的 command 和 skill 配置文件。", - examples: [ - "rune init opencode", - "rune init opencode claude-code", - ], + examples: ["rune init opencode", "rune init opencode claude-code"], }, discuss: { name: "discuss", @@ -36,38 +33,30 @@ const COMMANDS: Record = { description: "规划:生成指定文档的规划提示词", usage: "rune plan ", args: [ - { name: "", desc: "变更名称,如 \"add-login\"" }, - { name: "", desc: "文档名称,如 \"design\"、\"task\"" }, - ], - detail: "生成规划阶段指定文档的提示词。依赖的前置文档必须已完成。可用文档由配置中的 plan.documents 定义。", - examples: [ - "rune plan add-user-auth design", - "rune plan add-user-auth task", + { name: "", desc: '变更名称,如 "add-login"' }, + { name: "", desc: '文档名称,如 "design"、"task"' }, ], + detail: + "生成规划阶段指定文档的提示词。依赖的前置文档必须已完成。可用文档由配置中的 plan.documents 定义。", + examples: ["rune plan add-user-auth design", "rune plan add-user-auth task"], }, build: { name: "build", alias: "build <名称>", description: "构建:生成构建阶段提示词", usage: "rune build ", - args: [{ name: "", desc: "变更名称,如 \"add-login\"" }], + args: [{ name: "", desc: '变更名称,如 "add-login"' }], detail: "生成构建阶段的提示词。变更目录需已存在(通过 rune plan 创建)。", - examples: [ - "rune build add-user-auth", - "rune build fix-memory-leak", - ], + examples: ["rune build add-user-auth", "rune build fix-memory-leak"], }, archive: { name: "archive", alias: "archive <名称>", description: "归档:归档变更并生成提示词", usage: "rune archive ", - args: [{ name: "", desc: "变更名称,如 \"add-login\"" }], + args: [{ name: "", desc: '变更名称,如 "add-login"' }], detail: "将变更目录从 .rune/changes/ 移动到 .rune/archive/,并生成归档阶段提示词。", - examples: [ - "rune archive add-user-auth", - "rune archive fix-memory-leak", - ], + examples: ["rune archive add-user-auth", "rune archive fix-memory-leak"], }, update: { name: "update", @@ -75,11 +64,9 @@ const COMMANDS: Record = { description: "更新:更新已注入的编辑器配置", usage: "rune update <工具...>", args: [{ name: "<工具...>", desc: "要更新的 AI 工具,如 opencode、claude-code" }], - detail: "对比已注入的命令和 skill 文件,与内置版本不一致时覆盖,不存在时新建。用于升级 Rune 后同步编辑器配置。", - examples: [ - "rune update opencode", - "rune update opencode claude-code", - ], + detail: + "对比已注入的命令和 skill 文件,与内置版本不一致时覆盖,不存在时新建。用于升级 Rune 后同步编辑器配置。", + examples: ["rune update opencode", "rune update opencode claude-code"], }, status: { name: "status", @@ -88,10 +75,7 @@ const COMMANDS: Record = { usage: "rune status [change-name]", args: [{ name: "[change-name]", desc: "可选,指定查看的变更名称" }], detail: "展示各文档完成状态、依赖满足情况、规划进度和下一步建议。不传参数则显示所有变更。", - examples: [ - "rune status", - "rune status add-user-auth", - ], + examples: ["rune status", "rune status add-user-auth"], }, }; @@ -118,7 +102,7 @@ export function showGlobalHelp(): string { lines.push("示例:"); lines.push(" rune init opencode 初始化并注入 OpenCode 配置"); lines.push(" rune update opencode 更新 OpenCode 配置"); - lines.push(" rune plan add-login design 规划 \"add-login\" 的设计文档"); + lines.push(' rune plan add-login design 规划 "add-login" 的设计文档'); lines.push(" rune status 查看当前变更状态"); return lines.join("\n"); @@ -128,12 +112,7 @@ export function showCommandHelp(name: string): string | null { const cmd = COMMANDS[name]; if (!cmd) return null; - const lines: string[] = [ - `rune ${cmd.name} — ${cmd.description}`, - "", - "用法:", - ` ${cmd.usage}`, - ]; + const lines: string[] = [`rune ${cmd.name} — ${cmd.description}`, "", "用法:", ` ${cmd.usage}`]; if (cmd.args.length > 0) { lines.push(""); diff --git a/src/commands/init.ts b/src/commands/init.ts index 45e1e3d..c41fee0 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -43,10 +43,7 @@ export const SUPPORTED_TOOLS: Record Promise> = "claude-code": injectClaudeCode, }; -export async function runInit( - projectRoot: string, - tools: string[], -): Promise { +export async function runInit(projectRoot: string, tools: string[]): Promise { for (const tool of tools) { if (!SUPPORTED_TOOLS[tool]) { throw new CommandError(`不支持的工具: ${tool}`, { diff --git a/src/core/assembler.ts b/src/core/assembler.ts index e314c13..12a5188 100644 --- a/src/core/assembler.ts +++ b/src/core/assembler.ts @@ -8,7 +8,8 @@ import { parseTasks } from "./task-parser.ts"; export function assembleDiscussPrompt(config: RuneConfig): string { const discuss = config.stages.discuss; - if (!discuss) throw new CommandError("讨论阶段未配置", { + if (!discuss) + throw new CommandError("讨论阶段未配置", { hint: "请在 .rune/config.yaml 中配置 stages.discuss", }); return discuss.prompt; @@ -21,7 +22,8 @@ export async function assemblePlanPrompt( documentName: string, ): Promise { const plan = config.stages.plan; - if (!plan) throw new CommandError("规划阶段未配置", { + if (!plan) + throw new CommandError("规划阶段未配置", { hint: "请在 .rune/config.yaml 中配置 stages.plan", }); @@ -110,9 +112,7 @@ export async function assembleBuildPrompt( for (const task of pendingTasks) { parts.push(`- [ ] ${task.text}`); } - parts.push( - `\n请从第一个待执行任务开始。完成后更新 ${taskPath} 中的 checkbox。`, - ); + parts.push(`\n请从第一个待执行任务开始。完成后更新 ${taskPath} 中的 checkbox。`); return parts.join("\n"); } @@ -123,7 +123,8 @@ export async function assembleArchivePrompt( changeName: string, ): Promise { const archive = config.stages.archive; - if (!archive) throw new CommandError("归档阶段未配置", { + if (!archive) + throw new CommandError("归档阶段未配置", { hint: "请在 .rune/config.yaml 中配置 stages.archive", }); diff --git a/src/core/config.ts b/src/core/config.ts index 504e96c..77831fd 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -7,9 +7,7 @@ import { ConfigError } from "../cli/errors.ts"; import type { RuneConfig } from "../types.ts"; import { RUNE_DIR, CONFIG_FILE, CHANGES_DIR, ARCHIVE_DIR } from "../types.ts"; -export function findProjectRoot( - startDir: string = process.cwd(), -): string | null { +export function findProjectRoot(startDir: string = process.cwd()): string | null { let dir = startDir; while (true) { if (existsSync(join(dir, RUNE_DIR))) { @@ -49,9 +47,7 @@ export function validateConfig(config: RuneConfig): void { throw new ConfigError(`文档 "${doc.name}" 不能依赖自身`); } if (!docNames.has(dep)) { - throw new ConfigError( - `文档 "${doc.name}" 依赖 "${dep}" 不存在于 plan.documents 中`, - ); + throw new ConfigError(`文档 "${doc.name}" 依赖 "${dep}" 不存在于 plan.documents 中`); } } } @@ -81,9 +77,7 @@ export function validateConfig(config: RuneConfig): void { for (const doc of plan.documents) { path.length = 0; if (hasCycle(doc.name)) { - throw new ConfigError( - `文档间存在循环依赖:${path.join(" → ")}`, - ); + throw new ConfigError(`文档间存在循环依赖:${path.join(" → ")}`); } } } diff --git a/src/core/scanner.ts b/src/core/scanner.ts index 5c339b5..713df68 100644 --- a/src/core/scanner.ts +++ b/src/core/scanner.ts @@ -29,9 +29,7 @@ export async function scanChanges( const fileName = `${docConfig.name}.md`; const completed = mdFiles.has(fileName); const deps = docConfig.depend ?? []; - const dependMet = - deps.length === 0 || - deps.every((dep) => mdFiles.has(`${dep}.md`)); + const dependMet = deps.length === 0 || deps.every((dep) => mdFiles.has(`${dep}.md`)); return { name: docConfig.name, completed, dependMet }; }); } else { @@ -64,8 +62,7 @@ export async function scanChanges( taskProgress, }); } - } catch { - } + } catch {} return results; } diff --git a/tests/adapters/claude-code.test.ts b/tests/adapters/claude-code.test.ts index 5e1c365..e078647 100644 --- a/tests/adapters/claude-code.test.ts +++ b/tests/adapters/claude-code.test.ts @@ -82,9 +82,7 @@ describe("injectClaudeCode", () => { describe("updateClaudeCode", () => { it("文件不存在时创建", async () => { await updateClaudeCode(TMP_DIR); - expect( - existsSync(join(TMP_DIR, ".claude", "commands", "rune-discuss.md")), - ).toBe(true); + expect(existsSync(join(TMP_DIR, ".claude", "commands", "rune-discuss.md"))).toBe(true); }); it("文件存在且内容一致时不覆盖", async () => { @@ -104,10 +102,7 @@ describe("updateClaudeCode", () => { it("文件存在但内容不一致时覆盖", async () => { await injectClaudeCode(TMP_DIR); - await writeFile( - join(TMP_DIR, ".claude", "commands", "rune-discuss.md"), - "旧内容", - ); + await writeFile(join(TMP_DIR, ".claude", "commands", "rune-discuss.md"), "旧内容"); await updateClaudeCode(TMP_DIR); const content = await readFile( @@ -120,8 +115,6 @@ describe("updateClaudeCode", () => { it("更新 status 命令", async () => { await updateClaudeCode(TMP_DIR); - expect( - existsSync(join(TMP_DIR, ".claude", "commands", "rune-status.md")), - ).toBe(true); + expect(existsSync(join(TMP_DIR, ".claude", "commands", "rune-status.md"))).toBe(true); }); }); diff --git a/tests/adapters/opencode.test.ts b/tests/adapters/opencode.test.ts index 9f3d781..5675baa 100644 --- a/tests/adapters/opencode.test.ts +++ b/tests/adapters/opencode.test.ts @@ -24,9 +24,9 @@ describe("injectOpenCode", () => { for (const stage of ["discuss", "plan", "build", "archive"]) { expect(commands).toContain(`rune-${stage}.md`); expect(skills).toContain(`rune-${stage}`); - expect( - existsSync(join(TMP_DIR, ".opencode", "skills", `rune-${stage}`, "SKILL.md")), - ).toBe(true); + expect(existsSync(join(TMP_DIR, ".opencode", "skills", `rune-${stage}`, "SKILL.md"))).toBe( + true, + ); } }); @@ -38,9 +38,7 @@ describe("injectOpenCode", () => { expect(commands).toContain("rune-status.md"); expect(skills).toContain("rune-status"); - expect( - existsSync(join(TMP_DIR, ".opencode", "skills", "rune-status", "SKILL.md")), - ).toBe(true); + expect(existsSync(join(TMP_DIR, ".opencode", "skills", "rune-status", "SKILL.md"))).toBe(true); }); it("command 文件包含 skill 调用指令", async () => { @@ -112,12 +110,8 @@ describe("injectOpenCode", () => { describe("updateOpenCode", () => { it("文件不存在时创建", async () => { await updateOpenCode(TMP_DIR); - expect( - existsSync(join(TMP_DIR, ".opencode", "commands", "rune-discuss.md")), - ).toBe(true); - expect( - existsSync(join(TMP_DIR, ".opencode", "skills", "rune-discuss", "SKILL.md")), - ).toBe(true); + expect(existsSync(join(TMP_DIR, ".opencode", "commands", "rune-discuss.md"))).toBe(true); + expect(existsSync(join(TMP_DIR, ".opencode", "skills", "rune-discuss", "SKILL.md"))).toBe(true); }); it("文件存在且内容一致时不覆盖", async () => { @@ -137,10 +131,7 @@ describe("updateOpenCode", () => { it("文件存在但内容不一致时覆盖", async () => { await injectOpenCode(TMP_DIR); - await writeFile( - join(TMP_DIR, ".opencode", "commands", "rune-discuss.md"), - "旧内容", - ); + await writeFile(join(TMP_DIR, ".opencode", "commands", "rune-discuss.md"), "旧内容"); await updateOpenCode(TMP_DIR); const content = await readFile( @@ -153,11 +144,7 @@ describe("updateOpenCode", () => { it("更新 status 命令和 skill", async () => { await updateOpenCode(TMP_DIR); - expect( - existsSync(join(TMP_DIR, ".opencode", "commands", "rune-status.md")), - ).toBe(true); - expect( - existsSync(join(TMP_DIR, ".opencode", "skills", "rune-status", "SKILL.md")), - ).toBe(true); + expect(existsSync(join(TMP_DIR, ".opencode", "commands", "rune-status.md"))).toBe(true); + expect(existsSync(join(TMP_DIR, ".opencode", "skills", "rune-status", "SKILL.md"))).toBe(true); }); }); diff --git a/tests/cli/output.test.ts b/tests/cli/output.test.ts index 717d7b3..b5996e1 100644 --- a/tests/cli/output.test.ts +++ b/tests/cli/output.test.ts @@ -1,11 +1,6 @@ import { describe, it, expect } from "bun:test"; import { formatError } from "../../src/cli/output.ts"; -import { - UsageError, - ConfigError, - CommandError, - InternalError, -} from "../../src/cli/errors.ts"; +import { UsageError, ConfigError, CommandError, InternalError } from "../../src/cli/errors.ts"; describe("formatError", () => { it("只输出错误行(无 hint/usage)", () => { @@ -25,15 +20,13 @@ describe("formatError", () => { usage: "rune plan ", }); const output = formatError(err); - expect(output).toBe( - "错误: 缺少参数\n\n用法: rune plan ", - ); + expect(output).toBe("错误: 缺少参数\n\n用法: rune plan "); }); it("输出完整格式(错误 + 用法 + 提示)", () => { const err = new UsageError("缺少必填参数 ", { usage: "rune plan ", - hint: "请指定变更名称,如 \"add-login\"", + hint: '请指定变更名称,如 "add-login"', }); const output = formatError(err); expect(output).toBe( diff --git a/tests/cli/status.test.ts b/tests/cli/status.test.ts index 7a99c83..ab716ac 100644 --- a/tests/cli/status.test.ts +++ b/tests/cli/status.test.ts @@ -33,9 +33,7 @@ describe("formatChangeStatus", () => { it("显示文档依赖信息(dependMet 为 false 且 config 中有依赖)", () => { const status = makeStatus({ - documents: [ - { name: "task", completed: false, dependMet: false }, - ], + documents: [{ name: "task", completed: false, dependMet: false }], }); const config: RuneConfig = { stages: { @@ -53,9 +51,7 @@ describe("formatChangeStatus", () => { it("dependMet 为 false 但无 config 时不显示文档依赖信息", () => { const status = makeStatus({ - documents: [ - { name: "task", completed: false, dependMet: false }, - ], + documents: [{ name: "task", completed: false, dependMet: false }], }); const output = formatChangeStatus(status); expect(output).not.toContain("(依赖"); @@ -102,18 +98,14 @@ describe("formatChangeStatus", () => { describe("suggestNextStep", () => { it("规划未完成时返回下一个可规划文档", () => { const status = makeStatus({ - documents: [ - { name: "design", completed: false, dependMet: true }, - ], + documents: [{ name: "design", completed: false, dependMet: true }], }); expect(suggestNextStep(status)).toContain("rune plan test-change design"); }); it("规划未完成且依赖未满足时提示完成前置依赖", () => { const status = makeStatus({ - documents: [ - { name: "design", completed: false, dependMet: false }, - ], + documents: [{ name: "design", completed: false, dependMet: false }], }); expect(suggestNextStep(status)).toBe("完成前置依赖后再规划文档"); }); diff --git a/tests/commands/init.test.ts b/tests/commands/init.test.ts index 7bbe33e..da381d6 100644 --- a/tests/commands/init.test.ts +++ b/tests/commands/init.test.ts @@ -22,10 +22,7 @@ describe("runInit", () => { expect(existsSync(join(TMP_DIR, ".rune"))).toBe(true); expect(existsSync(join(TMP_DIR, ".rune", "config.yaml"))).toBe(true); - const content = await readFile( - join(TMP_DIR, ".rune", "config.yaml"), - "utf-8", - ); + const content = await readFile(join(TMP_DIR, ".rune", "config.yaml"), "utf-8"); expect(content).toContain("# Rune 配置文件"); expect(content).toContain("stages:"); }); @@ -46,16 +43,10 @@ describe("runInit", () => { it("重复 init 不覆盖 config.yaml", async () => { await runInit(TMP_DIR, ["opencode"]); - await writeFile( - join(TMP_DIR, ".rune", "config.yaml"), - "自定义内容", - ); + await writeFile(join(TMP_DIR, ".rune", "config.yaml"), "自定义内容"); await runInit(TMP_DIR, ["opencode"]); - const content = await readFile( - join(TMP_DIR, ".rune", "config.yaml"), - "utf-8", - ); + const content = await readFile(join(TMP_DIR, ".rune", "config.yaml"), "utf-8"); expect(content).toBe("自定义内容"); }); diff --git a/tests/core/assembler.test.ts b/tests/core/assembler.test.ts index 887348a..208a881 100644 --- a/tests/core/assembler.test.ts +++ b/tests/core/assembler.test.ts @@ -38,12 +38,7 @@ describe("assembleDiscussPrompt", () => { describe("assemblePlanPrompt", () => { it("包含指定文档名称和提示词", async () => { - const prompt = await assemblePlanPrompt( - defaultConfig, - TMP_DIR, - "user-auth", - "design", - ); + const prompt = await assemblePlanPrompt(defaultConfig, TMP_DIR, "user-auth", "design"); expect(prompt).toContain("user-auth"); expect(prompt).toContain("design"); expect(prompt).not.toContain("task"); @@ -53,23 +48,13 @@ describe("assemblePlanPrompt", () => { const changeDir = join(TMP_DIR, ".rune", "changes", "user-auth"); await mkdir(changeDir, { recursive: true }); await writeFile(join(changeDir, "design.md"), "# 已有设计"); - const prompt = await assemblePlanPrompt( - defaultConfig, - TMP_DIR, - "user-auth", - "design", - ); + const prompt = await assemblePlanPrompt(defaultConfig, TMP_DIR, "user-auth", "design"); expect(prompt).toContain("已有设计"); expect(prompt).toContain("在此基础上修订"); }); it("替换模板中的 {{change-name}}", async () => { - const prompt = await assemblePlanPrompt( - defaultConfig, - TMP_DIR, - "user-auth", - "design", - ); + const prompt = await assemblePlanPrompt(defaultConfig, TMP_DIR, "user-auth", "design"); expect(prompt).toContain("user-auth 设计文档"); expect(prompt).not.toContain("{{change-name}}"); }); @@ -85,23 +70,13 @@ describe("assemblePlanPrompt", () => { }, }, }; - const prompt = await assemblePlanPrompt( - config, - TMP_DIR, - "user-auth", - "task", - ); + const prompt = await assemblePlanPrompt(config, TMP_DIR, "user-auth", "task"); expect(prompt).toContain("依赖说明"); expect(prompt).toContain("design.md"); }); it("无依赖时不包含依赖说明", async () => { - const prompt = await assemblePlanPrompt( - defaultConfig, - TMP_DIR, - "user-auth", - "design", - ); + const prompt = await assemblePlanPrompt(defaultConfig, TMP_DIR, "user-auth", "design"); expect(prompt).not.toContain("依赖说明"); }); @@ -120,12 +95,7 @@ describe("assemblePlanPrompt", () => { }, }, }; - const prompt = await assemblePlanPrompt( - config, - TMP_DIR, - "user-auth", - "task", - ); + const prompt = await assemblePlanPrompt(config, TMP_DIR, "user-auth", "task"); expect(prompt).toContain("已完成"); }); @@ -154,15 +124,8 @@ describe("assembleBuildPrompt", () => { it("包含待执行任务列表", async () => { const changeDir = join(TMP_DIR, ".rune", "changes", "user-auth"); await mkdir(changeDir, { recursive: true }); - await writeFile( - join(changeDir, "task.md"), - `- [x] 任务一\n- [ ] 任务二\n- [ ] 任务三`, - ); - const prompt = await assembleBuildPrompt( - defaultConfig, - TMP_DIR, - "user-auth", - ); + await writeFile(join(changeDir, "task.md"), `- [x] 任务一\n- [ ] 任务二\n- [ ] 任务三`); + const prompt = await assembleBuildPrompt(defaultConfig, TMP_DIR, "user-auth"); expect(prompt).toContain("任务二"); expect(prompt).toContain("待执行任务"); expect(prompt).toContain("共 2 项"); @@ -171,15 +134,8 @@ describe("assembleBuildPrompt", () => { it("所有任务完成时提示可归档", async () => { const changeDir = join(TMP_DIR, ".rune", "changes", "user-auth"); await mkdir(changeDir, { recursive: true }); - await writeFile( - join(changeDir, "task.md"), - `- [x] 任务一\n- [x] 任务二`, - ); - const prompt = await assembleBuildPrompt( - defaultConfig, - TMP_DIR, - "user-auth", - ); + await writeFile(join(changeDir, "task.md"), `- [x] 任务一\n- [x] 任务二`); + const prompt = await assembleBuildPrompt(defaultConfig, TMP_DIR, "user-auth"); expect(prompt).toContain("已完成"); expect(prompt).toContain("归档"); }); diff --git a/tests/core/config.test.ts b/tests/core/config.test.ts index 1f9f379..fc2b90b 100644 --- a/tests/core/config.test.ts +++ b/tests/core/config.test.ts @@ -91,10 +91,7 @@ describe("loadConfig", () => { it("YAML 解析错误时返回默认配置", async () => { const runeDir = join(TMP_DIR, ".rune"); await mkdir(runeDir, { recursive: true }); - await writeFile( - join(runeDir, "config.yaml"), - `stages: [invalid yaml {{{`, - ); + await writeFile(join(runeDir, "config.yaml"), `stages: [invalid yaml {{{`); const config = await loadConfig(TMP_DIR); expect(config.stages.discuss).toBeDefined(); }); @@ -125,9 +122,7 @@ describe("validateConfig", () => { const config: RuneConfig = { stages: { plan: { - documents: [ - { name: "task", prompt: "生成任务", depend: ["nonexistent"] }, - ], + documents: [{ name: "task", prompt: "生成任务", depend: ["nonexistent"] }], }, }, }; @@ -138,9 +133,7 @@ describe("validateConfig", () => { const config: RuneConfig = { stages: { plan: { - documents: [ - { name: "design", prompt: "生成设计", depend: ["design"] }, - ], + documents: [{ name: "design", prompt: "生成设计", depend: ["design"] }], }, }, }; @@ -170,9 +163,7 @@ describe("validateConfig", () => { const config: RuneConfig = { stages: { plan: { - documents: [ - { name: "design", prompt: "生成设计", depend: [] }, - ], + documents: [{ name: "design", prompt: "生成设计", depend: [] }], }, }, }; diff --git a/tests/core/scanner.test.ts b/tests/core/scanner.test.ts index bd186eb..8e3b56c 100644 --- a/tests/core/scanner.test.ts +++ b/tests/core/scanner.test.ts @@ -25,10 +25,7 @@ describe("scanChanges", () => { const changesDir = join(TMP_DIR, ".rune", "changes"); await mkdir(join(changesDir, "user-auth"), { recursive: true }); await writeFile(join(changesDir, "user-auth", "design.md"), "# 设计"); - await writeFile( - join(changesDir, "user-auth", "task.md"), - `- [x] 任务一\n- [ ] 任务二`, - ); + await writeFile(join(changesDir, "user-auth", "task.md"), `- [x] 任务一\n- [ ] 任务二`); const changes = await scanChanges(TMP_DIR); expect(changes).toHaveLength(1); diff --git a/tests/defaults/config.test.ts b/tests/defaults/config.test.ts index e321421..737710f 100644 --- a/tests/defaults/config.test.ts +++ b/tests/defaults/config.test.ts @@ -25,32 +25,24 @@ describe("defaultConfig", () => { }); it("plan 的 task 文档配置存在", () => { - const taskDoc = defaultConfig.stages.plan!.documents.find( - (d) => d.name === "task", - ); + const taskDoc = defaultConfig.stages.plan!.documents.find((d) => d.name === "task"); expect(taskDoc).toBeDefined(); expect(taskDoc!.prompt).toBeTruthy(); }); it("task 文档依赖 design", () => { - const taskDoc = defaultConfig.stages.plan!.documents.find( - (d) => d.name === "task", - ); + const taskDoc = defaultConfig.stages.plan!.documents.find((d) => d.name === "task"); expect(taskDoc!.depend).toEqual(["design"]); }); it("design 文档有 template", () => { - const designDoc = defaultConfig.stages.plan!.documents.find( - (d) => d.name === "design", - ); + const designDoc = defaultConfig.stages.plan!.documents.find((d) => d.name === "design"); expect(designDoc!.template).toBeTruthy(); expect(designDoc!.template).toContain("{{change-name}}"); }); it("task 文档有 template", () => { - const taskDoc = defaultConfig.stages.plan!.documents.find( - (d) => d.name === "task", - ); + const taskDoc = defaultConfig.stages.plan!.documents.find((d) => d.name === "task"); expect(taskDoc!.template).toBeTruthy(); expect(taskDoc!.template).toContain("- [ ]"); });