From 2feea7a74f8e73d337cb450aaf394b1aea723c64 Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Tue, 9 Jun 2026 20:29:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20CLI=20=E5=B1=95=E7=A4=BA=E6=96=87?= =?UTF-8?q?=E6=9C=AC=E4=BD=BF=E7=94=A8=E5=8A=A8=E6=80=81=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=E5=89=8D=E7=BC=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/cli.ts | 104 ++++++++++++++++++++++++++---------- tests/cli/map-error.test.ts | 7 +-- 2 files changed, 80 insertions(+), 31 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index f8cf679..ae04374 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -15,12 +15,14 @@ import { scanChanges } from "./core/scanner.ts"; import { UsageError, ConfigError, CommandError, InternalError, CliError } from "./cli/errors.ts"; import { printError } from "./cli/output.ts"; import { showGlobalHelp, showCommandHelp } from "./cli/help.ts"; +import { getPmPrefix, getFallbackNote, DEFAULT_PREFIX, detectCommandPrefix } from "./core/pm.ts"; import type { ChangeStatus, RuneConfig } from "./types.ts"; function requireProjectRoot(): string { const root = findProjectRoot(); if (!root) { - throw new ConfigError("当前项目未初始化", { hint: "请先运行 rune init" }); + const prefix = getPmPrefix(); + throw new ConfigError("当前项目未初始化", { hint: `请先运行 ${prefix} init` }); } return root; } @@ -66,48 +68,65 @@ export function formatChangeStatus(change: ChangeStatus, config?: RuneConfig): s } lines.push(""); - lines.push(` 建议下一步:${suggestNextStep(change)}`); + lines.push(` 建议下一步:${suggestNextStep(change, config)}`); return lines.join("\n"); } -export function suggestNextStep(change: ChangeStatus): string { +export function suggestNextStep(change: ChangeStatus, config?: RuneConfig): string { + const prefix = getPmPrefix(config); if (!change.planCompleted) { const nextDoc = change.documents.find((d) => !d.completed && d.dependMet); if (nextDoc) { - return `rune plan ${change.name} ${nextDoc.name}`; + return `${prefix} plan ${change.name} ${nextDoc.name}`; } return `完成前置依赖后再规划文档`; } if (change.taskProgress && change.taskProgress.completed < change.taskProgress.total) { - return `rune build ${change.name}`; + return `${prefix} build ${change.name}`; } if (change.taskProgress && change.taskProgress.completed === change.taskProgress.total) { - return `rune archive ${change.name}`; + return `${prefix} archive ${change.name}`; } - return `rune build ${change.name}`; + return `${prefix} build ${change.name}`; } const cli = cac("rune"); -cli.command("", "").action(() => { - console.log(showGlobalHelp()); +cli.command("", "").action(async () => { + const root = findProjectRoot(); + let prefix = getPmPrefix(); + if (root) { + try { + const config = await loadConfig(root); + prefix = getPmPrefix(config); + } catch {} + } + console.log(showGlobalHelp(prefix)); }); -cli.command("help [command]", "显示帮助信息").action((command?: string) => { +cli.command("help [command]", "显示帮助信息").action(async (command?: string) => { + const root = findProjectRoot(); + let prefix = getPmPrefix(); + if (root) { + try { + const config = await loadConfig(root); + prefix = getPmPrefix(config); + } catch {} + } if (command) { - const output = showCommandHelp(command); + const output = showCommandHelp(command, prefix); if (!output) { throw new UsageError(`未知命令: ${command}`, { - hint: "运行 rune help 查看所有命令", + hint: `运行 ${prefix} help 查看所有命令`, }); } console.log(output); } else { - console.log(showGlobalHelp()); + console.log(showGlobalHelp(prefix)); } }); @@ -117,10 +136,12 @@ cli.command("version", "显示版本号").action(() => { }); cli.command("init [...tools]", "初始化 Rune 并注入工具配置").action(async (tools: string[]) => { + const prefix = getPmPrefix(); + const fallbackNote = getFallbackNote(); if (!tools || tools.length === 0) { throw new UsageError("请指定至少一个工具", { - usage: "rune init <工具...>", - hint: "如:rune init opencode", + usage: `${prefix} init <工具...>`, + hint: `如:${prefix} init opencode\n\n${fallbackNote}`, }); } await runInit(process.cwd(), tools); @@ -128,17 +149,19 @@ cli.command("init [...tools]", "初始化 Rune 并注入工具配置").action(as }); cli.command("update [...tools]", "更新已注入的工具配置").action(async (tools: string[]) => { + const prefix = getPmPrefix(); + const fallbackNote = getFallbackNote(); if (!tools || tools.length === 0) { throw new UsageError("请指定至少一个工具", { - usage: "rune update <工具...>", - hint: "如:rune update opencode", + usage: `${prefix} update <工具...>`, + hint: `如:${prefix} update opencode\n\n${fallbackNote}`, }); } 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> = { + const updaters: Record Promise> = { opencode: updateOpenCode, "claude-code": updateClaudeCode, }; @@ -149,8 +172,29 @@ cli.command("update [...tools]", "更新已注入的工具配置").action(async }); } } + let config: RuneConfig | undefined; + try { + config = await loadConfig(root); + } catch {} + const command = getPmPrefix(config); + if (!config?.metadata?.command) { + const detected = await detectCommandPrefix(); + if (detected) { + const { readFile, appendFile } = await import("node:fs/promises"); + const yaml = await import("yaml"); + const { join: joinPath } = await import("node:path"); + const configPath = joinPath(root, ".rune", "config.yaml"); + try { + const content = await readFile(configPath, "utf-8"); + const parsed = yaml.parse(content) as { metadata?: { command?: string } } | null; + if (!parsed?.metadata?.command) { + await appendFile(configPath, `\nmetadata:\n command: "${detected}"\n`); + } + } catch {} + } + } for (const tool of tools) { - await updaters[tool](root); + await updaters[tool](root, command === DEFAULT_PREFIX ? undefined : command); } console.log(`工具配置已更新:${tools.join(", ")}`); }); @@ -185,7 +229,7 @@ cli throw new CommandError( `文档 "${documentName}" 的前置依赖未满足:${missing.map((d) => `${d}.md`).join("、")} 尚未完成`, { - hint: `请先完成依赖文档:rune plan ${changeName} ${missing[0]}`, + hint: `请先完成依赖文档:${getPmPrefix(config)} plan ${changeName} ${missing[0]}`, }, ); } @@ -200,8 +244,9 @@ cli.command("build ", "构建阶段").action(async (changeName: str const root = requireProjectRoot(); const changeDir = getChangeDir(root, changeName); if (!existsSync(changeDir)) { + const prefix = getPmPrefix(); throw new CommandError(`变更 '${changeName}' 不存在`, { - hint: `请先运行 rune plan ${changeName} 创建变更`, + hint: `请先运行 ${prefix} plan ${changeName} 创建变更`, }); } const config = await loadConfig(root); @@ -214,8 +259,9 @@ cli.command("archive ", "归档阶段").action(async (changeName: s const root = requireProjectRoot(); const changeDir = getChangeDir(root, changeName); if (!existsSync(changeDir)) { + const prefix = getPmPrefix(); throw new CommandError(`变更 '${changeName}' 不存在`, { - hint: `请先运行 rune plan ${changeName} 创建变更`, + hint: `请先运行 ${prefix} plan ${changeName} 创建变更`, }); } const config = await loadConfig(root); @@ -230,13 +276,14 @@ cli.command("archive ", "归档阶段").action(async (changeName: s cli.command("status [change-name]", "查看变更状态").action(async (changeName?: string) => { const root = requireProjectRoot(); const config = await loadConfig(root); + const prefix = getPmPrefix(config); 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 查看所有变更", + hint: `运行 ${prefix} status 查看所有变更`, }); } console.log(formatChangeStatus(change, config)); @@ -264,33 +311,34 @@ export function mapError(e: unknown): CliError { } function mapCacError(e: Error): CliError | null { + const prefix = getPmPrefix(); if (e.message.includes("Unknown option")) { const match = e.message.match(/Unknown option `([^`]+)`/); const flag = match ? match[1] : "未知选项"; return new UsageError(`未知选项: ${flag}`, { - hint: "运行 rune help 查看所有命令", + hint: `运行 ${prefix} help 查看所有命令`, }); } if (e.message.includes("Unknown command")) { const match = e.message.match(/Unknown command `([^`]+)`/); const cmd = match ? match[1] : "未知命令"; return new UsageError(`未知命令: ${cmd}`, { - hint: "运行 rune help 查看所有命令", + hint: `运行 ${prefix} help 查看所有命令`, }); } if (e.message.includes("Unused args")) { const match = e.message.match(/Unused args: (.+)/); const args = match ? match[1] : "未知参数"; return new UsageError(`未知命令: ${args.replace(/`/g, "")}`, { - hint: "运行 rune help 查看所有命令", + hint: `运行 ${prefix} help 查看所有命令`, }); } if (e.message.includes("missing required args")) { const match = e.message.match(/command `(\w+)/); const cmd = match ? match[1] : "未知命令"; return new UsageError(`命令 '${cmd}' 缺少必填参数`, { - usage: `rune ${cmd} `, - hint: `运行 rune help ${cmd} 查看用法`, + usage: `${prefix} ${cmd} `, + hint: `运行 ${prefix} help ${cmd} 查看用法`, }); } return null; diff --git a/tests/cli/map-error.test.ts b/tests/cli/map-error.test.ts index 149a96f..9518f59 100644 --- a/tests/cli/map-error.test.ts +++ b/tests/cli/map-error.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect } from "bun:test"; import { UsageError, ConfigError, InternalError } from "../../src/cli/errors.ts"; import { mapError } from "../../src/cli.ts"; +import { DEFAULT_PREFIX } from "../../src/core/pm.ts"; describe("mapError", () => { it("CliError 原样返回", () => { @@ -15,7 +16,7 @@ describe("mapError", () => { const result = mapError(err); expect(result).toBeInstanceOf(UsageError); expect(result.message).toBe("未知选项: --badflag"); - expect(result.hint).toBe("运行 rune help 查看所有命令"); + expect(result.hint).toBe(`运行 ${DEFAULT_PREFIX} help 查看所有命令`); }); it("Unknown command 转为 UsageError", () => { @@ -38,8 +39,8 @@ describe("mapError", () => { const result = mapError(err); expect(result).toBeInstanceOf(UsageError); expect(result.message).toBe("命令 'plan' 缺少必填参数"); - expect(result.usage).toBe("rune plan "); - expect(result.hint).toContain("rune help plan"); + expect(result.usage).toBe(`${DEFAULT_PREFIX} plan `); + expect(result.hint).toContain(`${DEFAULT_PREFIX} help plan`); }); it("未知 Error 转为 InternalError", () => {