fix: CLI缺少参数友好提示、config.yaml注释模板、skill目录结构规范
This commit is contained in:
@@ -17,9 +17,9 @@ export async function injectOpenCode(projectRoot: string): Promise<void> {
|
|||||||
await writeFile(commandPath, generateCommand(stage, hasChangeName));
|
await writeFile(commandPath, generateCommand(stage, hasChangeName));
|
||||||
}
|
}
|
||||||
|
|
||||||
const skillDir = join(projectRoot, SKILLS_DIR);
|
const skillStageDir = join(projectRoot, SKILLS_DIR, `rune-${stage}`);
|
||||||
await mkdir(skillDir, { recursive: true });
|
await mkdir(skillStageDir, { recursive: true });
|
||||||
const skillPath = join(skillDir, `rune-${stage}.md`);
|
const skillPath = join(skillStageDir, "SKILL.md");
|
||||||
if (!existsSync(skillPath)) {
|
if (!existsSync(skillPath)) {
|
||||||
await writeFile(skillPath, generateSkill(stage, hasChangeName));
|
await writeFile(skillPath, generateSkill(stage, hasChangeName));
|
||||||
}
|
}
|
||||||
@@ -31,8 +31,9 @@ export async function injectOpenCode(projectRoot: string): Promise<void> {
|
|||||||
await writeFile(statusCommandPath, generateStatusCommand());
|
await writeFile(statusCommandPath, generateStatusCommand());
|
||||||
}
|
}
|
||||||
|
|
||||||
const skillDir = join(projectRoot, SKILLS_DIR);
|
const statusSkillDir = join(projectRoot, SKILLS_DIR, "rune-status");
|
||||||
const statusSkillPath = join(skillDir, "rune-status.md");
|
await mkdir(statusSkillDir, { recursive: true });
|
||||||
|
const statusSkillPath = join(statusSkillDir, "SKILL.md");
|
||||||
if (!existsSync(statusSkillPath)) {
|
if (!existsSync(statusSkillPath)) {
|
||||||
await writeFile(statusSkillPath, generateStatusSkill());
|
await writeFile(statusSkillPath, generateStatusSkill());
|
||||||
}
|
}
|
||||||
@@ -54,9 +55,16 @@ function generateSkill(stage: string, hasChangeName: boolean): string {
|
|||||||
const nameHint = hasChangeName
|
const nameHint = hasChangeName
|
||||||
? `将 <变更名> 替换为实际的变更名称。\n`
|
? `将 <变更名> 替换为实际的变更名称。\n`
|
||||||
: "";
|
: "";
|
||||||
|
const descriptionMap: Record<string, string> = {
|
||||||
|
discuss: "Use when 需要进入 SDD 讨论阶段,自由讨论需求和架构方案",
|
||||||
|
plan: "Use when 需要进入 SDD 规划阶段,生成设计文档和任务清单",
|
||||||
|
build: "Use when 需要进入 SDD 构建阶段,按任务清单逐步实现变更",
|
||||||
|
archive: "Use when 需要进入 SDD 归档阶段,确认变更完成并归档",
|
||||||
|
};
|
||||||
|
|
||||||
return `---
|
return `---
|
||||||
description: Rune SDD ${stage} 阶段
|
name: rune-${stage}
|
||||||
|
description: ${descriptionMap[stage] ?? `Use when 需要执行 SDD ${stage} 阶段`}
|
||||||
---
|
---
|
||||||
|
|
||||||
# ${stage} 阶段
|
# ${stage} 阶段
|
||||||
@@ -78,7 +86,8 @@ function generateStatusCommand(): string {
|
|||||||
|
|
||||||
function generateStatusSkill(): string {
|
function generateStatusSkill(): string {
|
||||||
return `---
|
return `---
|
||||||
description: 查看所有 Rune 变更状态
|
name: rune-status
|
||||||
|
description: Use when 需要查看当前所有 Rune 变更的状态和任务进度
|
||||||
---
|
---
|
||||||
|
|
||||||
# 状态查看
|
# 状态查看
|
||||||
|
|||||||
@@ -149,6 +149,13 @@ function handleError(e: unknown): never {
|
|||||||
printError(new UsageError(`未知命令: ${args.replace(/`/g, "")}`, {
|
printError(new UsageError(`未知命令: ${args.replace(/`/g, "")}`, {
|
||||||
hint: "运行 rune help 查看所有命令",
|
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} <change-name>`,
|
||||||
|
hint: `运行 rune help ${cmd} 查看用法`,
|
||||||
|
}));
|
||||||
} else {
|
} else {
|
||||||
printError(new InternalError());
|
printError(new InternalError());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,37 @@ import { injectOpenCode } from "../adapters/opencode.ts";
|
|||||||
import { injectClaudeCode } from "../adapters/claude-code.ts";
|
import { injectClaudeCode } from "../adapters/claude-code.ts";
|
||||||
import { CommandError } from "../cli/errors.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<string, (root: string) => Promise<void>> = {
|
const SUPPORTED_TOOLS: Record<string, (root: string) => Promise<void>> = {
|
||||||
opencode: injectOpenCode,
|
opencode: injectOpenCode,
|
||||||
"claude-code": injectClaudeCode,
|
"claude-code": injectClaudeCode,
|
||||||
@@ -30,7 +61,7 @@ export async function runInit(
|
|||||||
|
|
||||||
const configPath = join(runeDir, CONFIG_FILE);
|
const configPath = join(runeDir, CONFIG_FILE);
|
||||||
if (!existsSync(configPath)) {
|
if (!existsSync(configPath)) {
|
||||||
await writeFile(configPath, "stages: {}\n", "utf-8");
|
await writeFile(configPath, CONFIG_TEMPLATE, "utf-8");
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const tool of tools) {
|
for (const tool of tools) {
|
||||||
|
|||||||
@@ -23,7 +23,10 @@ describe("injectOpenCode", () => {
|
|||||||
|
|
||||||
for (const stage of ["discuss", "plan", "build", "archive"]) {
|
for (const stage of ["discuss", "plan", "build", "archive"]) {
|
||||||
expect(commands).toContain(`rune-${stage}.md`);
|
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"));
|
const skills = await readdir(join(TMP_DIR, ".opencode", "skills"));
|
||||||
|
|
||||||
expect(commands).toContain("rune-status.md");
|
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 () => {
|
it("command 文件包含 skill 调用指令", async () => {
|
||||||
@@ -49,11 +55,12 @@ describe("injectOpenCode", () => {
|
|||||||
it("skill 文件包含 bash 命令", async () => {
|
it("skill 文件包含 bash 命令", async () => {
|
||||||
await injectOpenCode(TMP_DIR);
|
await injectOpenCode(TMP_DIR);
|
||||||
const content = await readFile(
|
const content = await readFile(
|
||||||
join(TMP_DIR, ".opencode", "skills", "rune-discuss.md"),
|
join(TMP_DIR, ".opencode", "skills", "rune-discuss", "SKILL.md"),
|
||||||
"utf-8",
|
"utf-8",
|
||||||
);
|
);
|
||||||
expect(content).toContain("rune discuss");
|
expect(content).toContain("rune discuss");
|
||||||
expect(content).toContain("description");
|
expect(content).toContain("description");
|
||||||
|
expect(content).toContain("name: rune-discuss");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("plan/build/archive skill 包含变更名称参数提示", async () => {
|
it("plan/build/archive skill 包含变更名称参数提示", async () => {
|
||||||
@@ -61,7 +68,7 @@ describe("injectOpenCode", () => {
|
|||||||
|
|
||||||
for (const stage of ["plan", "build", "archive"]) {
|
for (const stage of ["plan", "build", "archive"]) {
|
||||||
const content = await readFile(
|
const content = await readFile(
|
||||||
join(TMP_DIR, ".opencode", "skills", `rune-${stage}.md`),
|
join(TMP_DIR, ".opencode", "skills", `rune-${stage}`, "SKILL.md"),
|
||||||
"utf-8",
|
"utf-8",
|
||||||
);
|
);
|
||||||
expect(content).toContain("变更名");
|
expect(content).toContain("变更名");
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ describe("runInit", () => {
|
|||||||
join(TMP_DIR, ".rune", "config.yaml"),
|
join(TMP_DIR, ".rune", "config.yaml"),
|
||||||
"utf-8",
|
"utf-8",
|
||||||
);
|
);
|
||||||
expect(content.trim()).toBe("stages: {}");
|
expect(content).toContain("# Rune 配置文件");
|
||||||
|
expect(content).toContain("stages:");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("创建 .rune/changes 和 .rune/archive 目录", async () => {
|
it("创建 .rune/changes 和 .rune/archive 目录", async () => {
|
||||||
@@ -40,7 +41,7 @@ describe("runInit", () => {
|
|||||||
await runInit(TMP_DIR, ["opencode"]);
|
await runInit(TMP_DIR, ["opencode"]);
|
||||||
|
|
||||||
expect(existsSync(join(TMP_DIR, ".opencode", "commands", "rune-discuss.md"))).toBe(true);
|
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 () => {
|
it("重复 init 不覆盖 config.yaml", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user