refactor: CLI 入口重构,统一错误处理和 help 输出

This commit is contained in:
2026-06-08 22:40:22 +08:00
parent c63912dc0d
commit c4511ca825

View File

@@ -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 <change-name>", "规划阶段")
.action(async (changeName: string) => {
const root = findProjectRoot();
if (!root) {
console.error("未找到 .rune 目录,请先运行 rune init");
process.exit(1);
}
cli.command("plan <change-name>", "规划阶段").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 <change-name>", "构建阶段")
.action(async (changeName: string) => {
const root = findProjectRoot();
if (!root) {
console.error("未找到 .rune 目录,请先运行 rune init");
process.exit(1);
cli.command("build <change-name>", "构建阶段").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 <change-name>", "归档阶段")
.action(async (changeName: string) => {
const root = findProjectRoot();
if (!root) {
console.error("未找到 .rune 目录,请先运行 rune init");
process.exit(1);
cli.command("archive <change-name>", "归档阶段").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);
}