feat: 新增 rune update 命令用于更新编辑器配置
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { existsSync } from "node:fs";
|
import { existsSync } from "node:fs";
|
||||||
import { mkdir, writeFile } from "node:fs/promises";
|
import { mkdir, writeFile, readFile } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { STAGES } from "../types.ts";
|
import { STAGES } from "../types.ts";
|
||||||
|
|
||||||
@@ -35,3 +35,36 @@ export async function injectClaudeCode(projectRoot: string): Promise<void> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateClaudeCode(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, `rune-${stage}.md`);
|
||||||
|
const cmd = hasChangeName
|
||||||
|
? `rune ${stage} <变更名>`
|
||||||
|
: `rune ${stage}`;
|
||||||
|
const nameHint = hasChangeName
|
||||||
|
? "\n如果用户没有指定变更名称,请向用户确认。"
|
||||||
|
: "";
|
||||||
|
const newContent = `执行以下命令,将输出作为当前阶段的工作指引:\n\`\`\`bash\n${cmd}\n\`\`\`${nameHint}\n`;
|
||||||
|
await writeIfChangedClaude(commandPath, newContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusPath = join(projectRoot, COMMANDS_DIR, "rune-status.md");
|
||||||
|
await writeIfChangedClaude(statusPath, `执行以下命令查看变更状态:\n\`\`\`bash\nrune status\n\`\`\`\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeIfChangedClaude(filePath: string, newContent: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const existing = await readFile(filePath, "utf-8");
|
||||||
|
if (existing === newContent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 文件不存在,创建
|
||||||
|
}
|
||||||
|
await writeFile(filePath, newContent);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { existsSync } from "node:fs";
|
import { existsSync } from "node:fs";
|
||||||
import { mkdir, writeFile } from "node:fs/promises";
|
import { mkdir, writeFile, readFile } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { STAGES } from "../types.ts";
|
import { STAGES } from "../types.ts";
|
||||||
|
|
||||||
@@ -39,6 +39,45 @@ export async function injectOpenCode(projectRoot: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateOpenCode(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, `rune-${stage}.md`);
|
||||||
|
const newCommand = generateCommand(stage, hasChangeName);
|
||||||
|
await writeIfChanged(commandPath, newCommand);
|
||||||
|
|
||||||
|
const skillStageDir = join(projectRoot, SKILLS_DIR, `rune-${stage}`);
|
||||||
|
await mkdir(skillStageDir, { recursive: true });
|
||||||
|
const skillPath = join(skillStageDir, "SKILL.md");
|
||||||
|
const newSkill = generateSkill(stage, hasChangeName);
|
||||||
|
await writeIfChanged(skillPath, newSkill);
|
||||||
|
}
|
||||||
|
|
||||||
|
const commandDir = join(projectRoot, COMMANDS_DIR);
|
||||||
|
const statusCommandPath = join(commandDir, "rune-status.md");
|
||||||
|
await writeIfChanged(statusCommandPath, generateStatusCommand());
|
||||||
|
|
||||||
|
const statusSkillDir = join(projectRoot, SKILLS_DIR, "rune-status");
|
||||||
|
await mkdir(statusSkillDir, { recursive: true });
|
||||||
|
const statusSkillPath = join(statusSkillDir, "SKILL.md");
|
||||||
|
await writeIfChanged(statusSkillPath, generateStatusSkill());
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeIfChanged(filePath: string, newContent: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const existing = await readFile(filePath, "utf-8");
|
||||||
|
if (existing === newContent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 文件不存在,创建
|
||||||
|
}
|
||||||
|
await writeFile(filePath, newContent);
|
||||||
|
}
|
||||||
|
|
||||||
function generateCommand(stage: string, hasChangeName: boolean): string {
|
function generateCommand(stage: string, hasChangeName: boolean): string {
|
||||||
if (hasChangeName) {
|
if (hasChangeName) {
|
||||||
return `请调用 rune-${stage} skill 执行 ${stage} 阶段。
|
return `请调用 rune-${stage} skill 执行 ${stage} 阶段。
|
||||||
|
|||||||
29
src/cli.ts
29
src/cli.ts
@@ -129,6 +129,35 @@ cli.command("init [...tools]", "初始化 Rune 并注入工具配置").action(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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 validators: Record<string, (root: string) => Promise<void>> = {
|
||||||
|
opencode: updateOpenCode,
|
||||||
|
"claude-code": updateClaudeCode,
|
||||||
|
};
|
||||||
|
for (const tool of tools) {
|
||||||
|
if (!validators[tool]) {
|
||||||
|
throw new CommandError(`不支持的工具: ${tool}`, {
|
||||||
|
hint: `支持的工具: ${Object.keys(validators).join(", ")}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const tool of tools) {
|
||||||
|
await validators[tool](root);
|
||||||
|
}
|
||||||
|
console.log(`工具配置已更新:${tools.join(", ")}`);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
cli.command("discuss", "讨论阶段").action(async () => {
|
cli.command("discuss", "讨论阶段").action(async () => {
|
||||||
const root = requireProjectRoot();
|
const root = requireProjectRoot();
|
||||||
const config = await loadConfig(root);
|
const config = await loadConfig(root);
|
||||||
|
|||||||
@@ -69,6 +69,18 @@ const COMMANDS: Record<string, CommandHelpDef> = {
|
|||||||
"rune archive fix-memory-leak",
|
"rune archive fix-memory-leak",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
update: {
|
||||||
|
name: "update",
|
||||||
|
alias: "update <工具...>",
|
||||||
|
description: "更新:更新已注入的编辑器配置",
|
||||||
|
usage: "rune update <工具...>",
|
||||||
|
args: [{ name: "<工具...>", desc: "要更新的 AI 工具,如 opencode、claude-code" }],
|
||||||
|
detail: "对比已注入的命令和 skill 文件,与内置版本不一致时覆盖,不存在时新建。用于升级 Rune 后同步编辑器配置。",
|
||||||
|
examples: [
|
||||||
|
"rune update opencode",
|
||||||
|
"rune update opencode claude-code",
|
||||||
|
],
|
||||||
|
},
|
||||||
status: {
|
status: {
|
||||||
name: "status",
|
name: "status",
|
||||||
alias: "status [变更]",
|
alias: "status [变更]",
|
||||||
@@ -105,6 +117,7 @@ export function showGlobalHelp(): string {
|
|||||||
lines.push("");
|
lines.push("");
|
||||||
lines.push("示例:");
|
lines.push("示例:");
|
||||||
lines.push(" rune init opencode 初始化并注入 OpenCode 配置");
|
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 查看当前变更状态");
|
lines.push(" rune status 查看当前变更状态");
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
|||||||
import { existsSync } from "node:fs";
|
import { existsSync } from "node:fs";
|
||||||
import { mkdir, rm, readFile, readdir } from "node:fs/promises";
|
import { mkdir, rm, readFile, readdir } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { injectOpenCode } from "../../src/adapters/opencode.ts";
|
import { writeFile } from "node:fs/promises";
|
||||||
|
import { injectOpenCode, updateOpenCode } from "../../src/adapters/opencode.ts";
|
||||||
|
|
||||||
const TMP_DIR = join(import.meta.dir, "__tmp_opencode_test__");
|
const TMP_DIR = join(import.meta.dir, "__tmp_opencode_test__");
|
||||||
|
|
||||||
@@ -108,3 +109,56 @@ describe("injectOpenCode", () => {
|
|||||||
expect(content).toBe(originalContent);
|
expect(content).toBe(originalContent);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("文件存在且内容一致时不覆盖", async () => {
|
||||||
|
await injectOpenCode(TMP_DIR);
|
||||||
|
const originalContent = await readFile(
|
||||||
|
join(TMP_DIR, ".opencode", "commands", "rune-discuss.md"),
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
|
||||||
|
await updateOpenCode(TMP_DIR);
|
||||||
|
const content = await readFile(
|
||||||
|
join(TMP_DIR, ".opencode", "commands", "rune-discuss.md"),
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
expect(content).toBe(originalContent);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("文件存在但内容不一致时覆盖", async () => {
|
||||||
|
await injectOpenCode(TMP_DIR);
|
||||||
|
await writeFile(
|
||||||
|
join(TMP_DIR, ".opencode", "commands", "rune-discuss.md"),
|
||||||
|
"旧内容",
|
||||||
|
);
|
||||||
|
|
||||||
|
await updateOpenCode(TMP_DIR);
|
||||||
|
const content = await readFile(
|
||||||
|
join(TMP_DIR, ".opencode", "commands", "rune-discuss.md"),
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
expect(content).not.toBe("旧内容");
|
||||||
|
expect(content).toContain("rune-discuss");
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user