refactor: CLI 入口重构,统一错误处理和 help 输出
This commit is contained in:
142
src/cli.ts
142
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 <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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user