diff --git a/src/adapters/opencode.ts b/src/adapters/opencode.ts index 8e0d95f..96a08c8 100644 --- a/src/adapters/opencode.ts +++ b/src/adapters/opencode.ts @@ -17,9 +17,9 @@ export async function injectOpenCode(projectRoot: string): Promise { await writeFile(commandPath, generateCommand(stage, hasChangeName)); } - const skillDir = join(projectRoot, SKILLS_DIR); - await mkdir(skillDir, { recursive: true }); - const skillPath = join(skillDir, `rune-${stage}.md`); + const skillStageDir = join(projectRoot, SKILLS_DIR, `rune-${stage}`); + await mkdir(skillStageDir, { recursive: true }); + const skillPath = join(skillStageDir, "SKILL.md"); if (!existsSync(skillPath)) { await writeFile(skillPath, generateSkill(stage, hasChangeName)); } @@ -31,8 +31,9 @@ export async function injectOpenCode(projectRoot: string): Promise { await writeFile(statusCommandPath, generateStatusCommand()); } - const skillDir = join(projectRoot, SKILLS_DIR); - const statusSkillPath = join(skillDir, "rune-status.md"); + const statusSkillDir = join(projectRoot, SKILLS_DIR, "rune-status"); + await mkdir(statusSkillDir, { recursive: true }); + const statusSkillPath = join(statusSkillDir, "SKILL.md"); if (!existsSync(statusSkillPath)) { await writeFile(statusSkillPath, generateStatusSkill()); } @@ -54,9 +55,16 @@ function generateSkill(stage: string, hasChangeName: boolean): string { const nameHint = hasChangeName ? `将 <变更名> 替换为实际的变更名称。\n` : ""; + const descriptionMap: Record = { + discuss: "Use when 需要进入 SDD 讨论阶段,自由讨论需求和架构方案", + plan: "Use when 需要进入 SDD 规划阶段,生成设计文档和任务清单", + build: "Use when 需要进入 SDD 构建阶段,按任务清单逐步实现变更", + archive: "Use when 需要进入 SDD 归档阶段,确认变更完成并归档", + }; return `--- -description: Rune SDD ${stage} 阶段 +name: rune-${stage} +description: ${descriptionMap[stage] ?? `Use when 需要执行 SDD ${stage} 阶段`} --- # ${stage} 阶段 @@ -78,7 +86,8 @@ function generateStatusCommand(): string { function generateStatusSkill(): string { return `--- -description: 查看所有 Rune 变更状态 +name: rune-status +description: Use when 需要查看当前所有 Rune 变更的状态和任务进度 --- # 状态查看 diff --git a/src/cli.ts b/src/cli.ts index dcad0d7..07e4847 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -149,6 +149,13 @@ function handleError(e: unknown): never { printError(new UsageError(`未知命令: ${args.replace(/`/g, "")}`, { hint: "运行 rune help 查看所有命令", })); + } else if (e instanceof Error && e.message.includes("missing required args")) { + const match = e.message.match(/command `(\w+)/); + const cmd = match ? match[1] : "未知命令"; + printError(new UsageError(`命令 '${cmd}' 缺少必填参数`, { + usage: `rune ${cmd} `, + hint: `运行 rune help ${cmd} 查看用法`, + })); } else { printError(new InternalError()); } diff --git a/src/commands/init.ts b/src/commands/init.ts index 88aaca9..631c42e 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -6,6 +6,37 @@ import { injectOpenCode } from "../adapters/opencode.ts"; import { injectClaudeCode } from "../adapters/claude-code.ts"; import { CommandError } from "../cli/errors.ts"; +const CONFIG_TEMPLATE = `# Rune 配置文件 +# +# 未配置的阶段将使用内置默认配置。 +# 阶段顺序:discuss -> plan -> build -> archive +# +# 可配置阶段: +# discuss - 讨论阶段:自由讨论需求和架构 +# plan - 规划阶段:生成设计文档和任务清单 +# build - 构建阶段:按任务清单逐步实现 +# archive - 归档阶段:确认完成并归档变更 +# +# 示例 - 自定义讨论阶段提示词: +# stages: +# discuss: +# prompt: | +# 你是一位资深软件架构师... +# +# 示例 - 自定义规划阶段的文档模板: +# stages: +# plan: +# documents: +# - name: design +# prompt: 生成设计文档 +# template: | +# # {{change-name}} 设计文档 +# - name: task +# prompt: 生成任务清单 +# template: | +# # {{change-name}} 任务清单 +`; + const SUPPORTED_TOOLS: Record Promise> = { opencode: injectOpenCode, "claude-code": injectClaudeCode, @@ -30,7 +61,7 @@ export async function runInit( const configPath = join(runeDir, CONFIG_FILE); if (!existsSync(configPath)) { - await writeFile(configPath, "stages: {}\n", "utf-8"); + await writeFile(configPath, CONFIG_TEMPLATE, "utf-8"); } for (const tool of tools) { diff --git a/tests/adapters/opencode.test.ts b/tests/adapters/opencode.test.ts index 69bff63..ad345f1 100644 --- a/tests/adapters/opencode.test.ts +++ b/tests/adapters/opencode.test.ts @@ -23,7 +23,10 @@ describe("injectOpenCode", () => { for (const stage of ["discuss", "plan", "build", "archive"]) { expect(commands).toContain(`rune-${stage}.md`); - expect(skills).toContain(`rune-${stage}.md`); + expect(skills).toContain(`rune-${stage}`); + expect( + existsSync(join(TMP_DIR, ".opencode", "skills", `rune-${stage}`, "SKILL.md")), + ).toBe(true); } }); @@ -34,7 +37,10 @@ describe("injectOpenCode", () => { const skills = await readdir(join(TMP_DIR, ".opencode", "skills")); expect(commands).toContain("rune-status.md"); - expect(skills).toContain("rune-status.md"); + expect(skills).toContain("rune-status"); + expect( + existsSync(join(TMP_DIR, ".opencode", "skills", "rune-status", "SKILL.md")), + ).toBe(true); }); it("command 文件包含 skill 调用指令", async () => { @@ -49,11 +55,12 @@ describe("injectOpenCode", () => { it("skill 文件包含 bash 命令", async () => { await injectOpenCode(TMP_DIR); const content = await readFile( - join(TMP_DIR, ".opencode", "skills", "rune-discuss.md"), + join(TMP_DIR, ".opencode", "skills", "rune-discuss", "SKILL.md"), "utf-8", ); expect(content).toContain("rune discuss"); expect(content).toContain("description"); + expect(content).toContain("name: rune-discuss"); }); it("plan/build/archive skill 包含变更名称参数提示", async () => { @@ -61,7 +68,7 @@ describe("injectOpenCode", () => { for (const stage of ["plan", "build", "archive"]) { const content = await readFile( - join(TMP_DIR, ".opencode", "skills", `rune-${stage}.md`), + join(TMP_DIR, ".opencode", "skills", `rune-${stage}`, "SKILL.md"), "utf-8", ); expect(content).toContain("变更名"); diff --git a/tests/commands/init.test.ts b/tests/commands/init.test.ts index 3053d76..7bbe33e 100644 --- a/tests/commands/init.test.ts +++ b/tests/commands/init.test.ts @@ -26,7 +26,8 @@ describe("runInit", () => { join(TMP_DIR, ".rune", "config.yaml"), "utf-8", ); - expect(content.trim()).toBe("stages: {}"); + expect(content).toContain("# Rune 配置文件"); + expect(content).toContain("stages:"); }); it("创建 .rune/changes 和 .rune/archive 目录", async () => { @@ -40,7 +41,7 @@ describe("runInit", () => { await runInit(TMP_DIR, ["opencode"]); expect(existsSync(join(TMP_DIR, ".opencode", "commands", "rune-discuss.md"))).toBe(true); - expect(existsSync(join(TMP_DIR, ".opencode", "skills", "rune-discuss.md"))).toBe(true); + expect(existsSync(join(TMP_DIR, ".opencode", "skills", "rune-discuss", "SKILL.md"))).toBe(true); }); it("重复 init 不覆盖 config.yaml", async () => {