feat: OpenCode 和 Claude Code 适配器及测试
This commit is contained in:
37
src/adapters/claude-code.ts
Normal file
37
src/adapters/claude-code.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { existsSync } from "node:fs";
|
||||||
|
import { mkdir, writeFile } from "node:fs/promises";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { STAGES } from "../types.ts";
|
||||||
|
|
||||||
|
const COMMANDS_DIR = ".claude/commands";
|
||||||
|
|
||||||
|
export async function injectClaudeCode(projectRoot: string): Promise<void> {
|
||||||
|
for (const stage of STAGES) {
|
||||||
|
const hasChangeName = stage !== "discuss";
|
||||||
|
|
||||||
|
const commandDir = join(projectRoot, COMMANDS_DIR);
|
||||||
|
await mkdir(commandDir, { recursive: true });
|
||||||
|
const commandPath = join(commandDir, `${stage}.md`);
|
||||||
|
if (!existsSync(commandPath)) {
|
||||||
|
const cmd = hasChangeName
|
||||||
|
? `rune ${stage} <变更名>`
|
||||||
|
: `rune ${stage}`;
|
||||||
|
const nameHint = hasChangeName
|
||||||
|
? "\n如果用户没有指定变更名称,请向用户确认。"
|
||||||
|
: "";
|
||||||
|
await writeFile(
|
||||||
|
commandPath,
|
||||||
|
`执行以下命令,将输出作为当前阶段的工作指引:\n\`\`\`bash\n${cmd}\n\`\`\`${nameHint}\n`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
94
src/adapters/opencode.ts
Normal file
94
src/adapters/opencode.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { existsSync } from "node:fs";
|
||||||
|
import { mkdir, writeFile } from "node:fs/promises";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { STAGES } from "../types.ts";
|
||||||
|
|
||||||
|
const COMMANDS_DIR = ".opencode/commands";
|
||||||
|
const SKILLS_DIR = ".opencode/skills";
|
||||||
|
|
||||||
|
export async function injectOpenCode(projectRoot: string): Promise<void> {
|
||||||
|
for (const stage of STAGES) {
|
||||||
|
const hasChangeName = stage !== "discuss";
|
||||||
|
|
||||||
|
const commandDir = join(projectRoot, COMMANDS_DIR);
|
||||||
|
await mkdir(commandDir, { recursive: true });
|
||||||
|
const commandPath = join(commandDir, `${stage}.md`);
|
||||||
|
if (!existsSync(commandPath)) {
|
||||||
|
await writeFile(commandPath, generateCommand(stage, hasChangeName));
|
||||||
|
}
|
||||||
|
|
||||||
|
const skillDir = join(projectRoot, SKILLS_DIR);
|
||||||
|
await mkdir(skillDir, { recursive: true });
|
||||||
|
const skillPath = join(skillDir, `rune-${stage}.md`);
|
||||||
|
if (!existsSync(skillPath)) {
|
||||||
|
await writeFile(skillPath, generateSkill(stage, hasChangeName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const commandDir = join(projectRoot, COMMANDS_DIR);
|
||||||
|
const statusCommandPath = join(commandDir, "rune-status.md");
|
||||||
|
if (!existsSync(statusCommandPath)) {
|
||||||
|
await writeFile(statusCommandPath, generateStatusCommand());
|
||||||
|
}
|
||||||
|
|
||||||
|
const skillDir = join(projectRoot, SKILLS_DIR);
|
||||||
|
const statusSkillPath = join(skillDir, "rune-status.md");
|
||||||
|
if (!existsSync(statusSkillPath)) {
|
||||||
|
await writeFile(statusSkillPath, generateStatusSkill());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateCommand(stage: string, hasChangeName: boolean): string {
|
||||||
|
if (hasChangeName) {
|
||||||
|
return `请调用 rune-${stage} skill 执行 ${stage} 阶段。
|
||||||
|
|
||||||
|
如果用户没有指定变更名称,请向用户确认要操作的变更名称。
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
return `请调用 rune-${stage} skill 执行 ${stage} 阶段。
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateSkill(stage: string, hasChangeName: boolean): string {
|
||||||
|
const cmd = hasChangeName ? `rune ${stage} <变更名>` : `rune ${stage}`;
|
||||||
|
const nameHint = hasChangeName
|
||||||
|
? `将 <变更名> 替换为实际的变更名称。\n`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return `---
|
||||||
|
description: Rune SDD ${stage} 阶段
|
||||||
|
---
|
||||||
|
|
||||||
|
# ${stage} 阶段
|
||||||
|
|
||||||
|
执行以下命令获取工作指引:
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
${cmd}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
${nameHint}将命令输出作为工作指引,执行当前阶段的工作。
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateStatusCommand(): string {
|
||||||
|
return `请调用 rune-status skill 查看当前所有变更状态。
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateStatusSkill(): string {
|
||||||
|
return `---
|
||||||
|
description: 查看所有 Rune 变更状态
|
||||||
|
---
|
||||||
|
|
||||||
|
# 状态查看
|
||||||
|
|
||||||
|
执行以下命令:
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
rune status
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
将命令输出展示给用户。
|
||||||
|
`;
|
||||||
|
}
|
||||||
85
tests/adapters/opencode.test.ts
Normal file
85
tests/adapters/opencode.test.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
||||||
|
import { existsSync } from "node:fs";
|
||||||
|
import { mkdir, rm, readFile, readdir } from "node:fs/promises";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { injectOpenCode } from "../../src/adapters/opencode.ts";
|
||||||
|
|
||||||
|
const TMP_DIR = join(import.meta.dir, "__tmp_opencode_test__");
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await mkdir(TMP_DIR, { recursive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await rm(TMP_DIR, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("injectOpenCode", () => {
|
||||||
|
it("生成 discuss、plan、build、archive 的 command 和 skill 文件", async () => {
|
||||||
|
await injectOpenCode(TMP_DIR);
|
||||||
|
|
||||||
|
const commands = await readdir(join(TMP_DIR, ".opencode", "commands"));
|
||||||
|
const skills = await readdir(join(TMP_DIR, ".opencode", "skills"));
|
||||||
|
|
||||||
|
for (const stage of ["discuss", "plan", "build", "archive"]) {
|
||||||
|
expect(commands).toContain(`${stage}.md`);
|
||||||
|
expect(skills).toContain(`rune-${stage}.md`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("生成 rune-status command 和 skill", async () => {
|
||||||
|
await injectOpenCode(TMP_DIR);
|
||||||
|
|
||||||
|
const commands = await readdir(join(TMP_DIR, ".opencode", "commands"));
|
||||||
|
const skills = await readdir(join(TMP_DIR, ".opencode", "skills"));
|
||||||
|
|
||||||
|
expect(commands).toContain("rune-status.md");
|
||||||
|
expect(skills).toContain("rune-status.md");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("command 文件包含 skill 调用指令", async () => {
|
||||||
|
await injectOpenCode(TMP_DIR);
|
||||||
|
const content = await readFile(
|
||||||
|
join(TMP_DIR, ".opencode", "commands", "discuss.md"),
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
expect(content).toContain("rune-discuss");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skill 文件包含 bash 命令", async () => {
|
||||||
|
await injectOpenCode(TMP_DIR);
|
||||||
|
const content = await readFile(
|
||||||
|
join(TMP_DIR, ".opencode", "skills", "rune-discuss.md"),
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
expect(content).toContain("rune discuss");
|
||||||
|
expect(content).toContain("description");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("plan/build/archive skill 包含变更名称参数提示", async () => {
|
||||||
|
await injectOpenCode(TMP_DIR);
|
||||||
|
|
||||||
|
for (const stage of ["plan", "build", "archive"]) {
|
||||||
|
const content = await readFile(
|
||||||
|
join(TMP_DIR, ".opencode", "skills", `rune-${stage}.md`),
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
expect(content).toContain("变更名");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("重复注入时不覆盖已存在的文件", async () => {
|
||||||
|
await injectOpenCode(TMP_DIR);
|
||||||
|
const originalContent = await readFile(
|
||||||
|
join(TMP_DIR, ".opencode", "commands", "discuss.md"),
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
|
||||||
|
await injectOpenCode(TMP_DIR);
|
||||||
|
const content = await readFile(
|
||||||
|
join(TMP_DIR, ".opencode", "commands", "discuss.md"),
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
expect(content).toBe(originalContent);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user