From c4511ca8257430ae7439286ec399a6348d05d64a Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Mon, 8 Jun 2026 22:40:22 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20CLI=20=E5=85=A5=E5=8F=A3=E9=87=8D?= =?UTF-8?q?=E6=9E=84=EF=BC=8C=E7=BB=9F=E4=B8=80=E9=94=99=E8=AF=AF=E5=A4=84?= =?UTF-8?q?=E7=90=86=E5=92=8C=20help=20=E8=BE=93=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/cli.ts | 142 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 102 insertions(+), 40 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index a2ee160..dcad0d7 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -2,6 +2,7 @@ import { cac } from "cac"; import { join } from "node:path"; import { mkdir, rename } from "node:fs/promises"; +import { readFileSync, existsSync } from "node:fs"; import { runInit } from "./commands/init.ts"; import { findProjectRoot, loadConfig, getChangeDir, getArchiveDir } from "./core/config.ts"; import { @@ -11,14 +12,50 @@ import { assembleArchivePrompt, } from "./core/assembler.ts"; 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"; + +function requireProjectRoot(): string { + const root = findProjectRoot(); + if (!root) { + throw new ConfigError("当前项目未初始化", { hint: "请先运行 rune init" }); + } + return root; +} const cli = cac("rune"); +cli.command("", "").action(() => { + console.log(showGlobalHelp()); +}); + +cli.command("help [command]", "显示帮助信息").action((command?: string) => { + if (command) { + const output = showCommandHelp(command); + if (!output) { + throw new UsageError(`未知命令: ${command}`, { + hint: "运行 rune help 查看所有命令", + }); + } + console.log(output); + } else { + console.log(showGlobalHelp()); + } +}); + +cli.command("version", "显示版本号").action(() => { + const pkg = JSON.parse(readFileSync(join(import.meta.dir, "../package.json"), "utf-8")); + console.log(`rune v${pkg.version}`); +}); + cli.command("init [...tools]", "初始化 Rune 并注入工具配置").action( async (tools: string[]) => { if (!tools || tools.length === 0) { - console.error("请指定至少一个工具,如:rune init opencode"); - process.exit(1); + throw new UsageError("请指定至少一个工具", { + usage: "rune init <工具...>", + hint: "如:rune init opencode", + }); } await runInit(process.cwd(), tools); console.log(`Rune 初始化完成,已注入工具:${tools.join(", ")}`); @@ -26,66 +63,58 @@ cli.command("init [...tools]", "初始化 Rune 并注入工具配置").action( ); cli.command("discuss", "讨论阶段").action(async () => { - const root = findProjectRoot(); - if (!root) { - console.error("未找到 .rune 目录,请先运行 rune init"); - process.exit(1); - } + const root = requireProjectRoot(); const config = await loadConfig(root); const prompt = assembleDiscussPrompt(config); console.log(prompt); }); -cli - .command("plan ", "规划阶段") - .action(async (changeName: string) => { - const root = findProjectRoot(); - if (!root) { - console.error("未找到 .rune 目录,请先运行 rune init"); - process.exit(1); - } +cli.command("plan ", "规划阶段").action( + async (changeName: string) => { + const root = requireProjectRoot(); await mkdir(getChangeDir(root, changeName), { recursive: true }); const config = await loadConfig(root); const prompt = await assemblePlanPrompt(config, root, changeName); console.log(prompt); - }); + }, +); -cli - .command("build ", "构建阶段") - .action(async (changeName: string) => { - const root = findProjectRoot(); - if (!root) { - console.error("未找到 .rune 目录,请先运行 rune init"); - process.exit(1); +cli.command("build ", "构建阶段").action( + async (changeName: string) => { + const root = requireProjectRoot(); + const changeDir = getChangeDir(root, changeName); + if (!existsSync(changeDir)) { + throw new CommandError(`变更 '${changeName}' 不存在`, { + hint: `请先运行 rune plan ${changeName} 创建变更`, + }); } const config = await loadConfig(root); const prompt = await assembleBuildPrompt(config, root, changeName); console.log(prompt); - }); + }, +); -cli - .command("archive ", "归档阶段") - .action(async (changeName: string) => { - const root = findProjectRoot(); - if (!root) { - console.error("未找到 .rune 目录,请先运行 rune init"); - process.exit(1); +cli.command("archive ", "归档阶段").action( + async (changeName: string) => { + const root = requireProjectRoot(); + const changeDir = getChangeDir(root, changeName); + if (!existsSync(changeDir)) { + throw new CommandError(`变更 '${changeName}' 不存在`, { + hint: `请先运行 rune plan ${changeName} 创建变更`, + }); } const config = await loadConfig(root); const prompt = assembleArchivePrompt(config, changeName); const today = new Date().toISOString().slice(0, 10); - const src = getChangeDir(root, changeName); + const src = changeDir; const dest = join(getArchiveDir(root), `${today}-${changeName}`); await rename(src, dest); console.log(prompt); - }); + }, +); cli.command("status", "查看变更状态").action(async () => { - const root = findProjectRoot(); - if (!root) { - console.error("未找到 .rune 目录,请先运行 rune init"); - process.exit(1); - } + const root = requireProjectRoot(); const changes = await scanChanges(root); if (changes.length === 0) { console.log("当前无进行中的变更。"); @@ -99,5 +128,38 @@ cli.command("status", "查看变更状态").action(async () => { } }); -cli.help(); -cli.parse(); +function handleError(e: unknown): never { + if (e instanceof CliError) { + printError(e); + } else if (e instanceof Error && e.message.includes("Unknown option")) { + const match = e.message.match(/Unknown option `([^`]+)`/); + const flag = match ? match[1] : "未知选项"; + printError(new UsageError(`未知选项: ${flag}`, { + hint: "运行 rune help 查看所有命令", + })); + } else if (e instanceof Error && e.message.includes("Unknown command")) { + const match = e.message.match(/Unknown command `([^`]+)`/); + const cmd = match ? match[1] : "未知命令"; + printError(new UsageError(`未知命令: ${cmd}`, { + hint: "运行 rune help 查看所有命令", + })); + } else if (e instanceof Error && e.message.includes("Unused args")) { + const match = e.message.match(/Unused args: (.+)/); + const args = match ? match[1] : "未知参数"; + printError(new UsageError(`未知命令: ${args.replace(/`/g, "")}`, { + hint: "运行 rune help 查看所有命令", + })); + } else { + printError(new InternalError()); + } +} + +process.on("unhandledRejection", (e) => { + handleError(e); +}); + +try { + cli.parse(); +} catch (e) { + handleError(e); +}