#!/usr/bin/env bun 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 { assembleDiscussPrompt, assemblePlanPrompt, assembleBuildPrompt, 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"; import type { ChangeStatus, RuneConfig } from "./types.ts"; function requireProjectRoot(): string { const root = findProjectRoot(); if (!root) { throw new ConfigError("当前项目未初始化", { hint: "请先运行 rune init" }); } return root; } export function validateChangeName(name: string): void { if (!/^[\u4e00-\u9fa5a-zA-Z-]+$/.test(name)) { throw new CommandError(`变更名 "${name}" 包含不支持的字符`, { hint: "变更名仅支持中文、英文和短横线(-)", }); } } export function formatChangeStatus(change: ChangeStatus, config?: RuneConfig): string { const lines: string[] = []; lines.push(`变更:${change.name}`); lines.push(" 规划阶段:"); const planDocs = config?.stages.plan?.documents; for (const doc of change.documents) { if (doc.completed) { lines.push(` ${doc.name}.md ✓ 已完成`); } else { const docConfig = planDocs?.find((d) => d.name === doc.name); const depInfo = !doc.dependMet && docConfig?.depend?.length ? `(依赖 ${docConfig.depend.map((d) => `${d}.md`).join("、")})` : ""; lines.push(` ${doc.name}.md ○ 待完成${depInfo}`); } } const completedCount = change.documents.filter((d) => d.completed).length; lines.push(` 规划进度:${completedCount}/${change.documents.length} 文档已完成`); if (change.buildUnlocked) { lines.push(" 构建阶段:已解锁"); } else { lines.push(" 构建阶段:未解锁(需完成规划)"); } if (change.taskProgress) { lines.push(` 任务进度:${change.taskProgress.completed}/${change.taskProgress.total} 已完成`); } lines.push(""); lines.push(` 建议下一步:${suggestNextStep(change)}`); return lines.join("\n"); } export function suggestNextStep(change: ChangeStatus): string { if (!change.planCompleted) { const nextDoc = change.documents.find((d) => !d.completed && d.dependMet); if (nextDoc) { return `rune plan ${change.name} ${nextDoc.name}`; } return `完成前置依赖后再规划文档`; } if (change.taskProgress && change.taskProgress.completed < change.taskProgress.total) { return `rune build ${change.name}`; } if (change.taskProgress && change.taskProgress.completed === change.taskProgress.total) { return `rune archive ${change.name}`; } return `rune build ${change.name}`; } 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) { throw new UsageError("请指定至少一个工具", { usage: "rune init <工具...>", hint: "如:rune init opencode", }); } await runInit(process.cwd(), tools); console.log(`Rune 初始化完成,已注入工具:${tools.join(", ")}`); }); 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 { SUPPORTED_TOOLS } = await import("./commands/init.ts"); const updaters: Record Promise> = { opencode: updateOpenCode, "claude-code": updateClaudeCode, }; for (const tool of tools) { if (!SUPPORTED_TOOLS[tool]) { throw new CommandError(`不支持的工具: ${tool}`, { hint: `支持的工具: ${Object.keys(SUPPORTED_TOOLS).join(", ")}`, }); } } for (const tool of tools) { await updaters[tool](root); } console.log(`工具配置已更新:${tools.join(", ")}`); }); cli.command("discuss", "讨论阶段").action(async () => { const root = requireProjectRoot(); const config = await loadConfig(root); const prompt = assembleDiscussPrompt(config); console.log(prompt); }); cli .command("plan ", "规划阶段") .action(async (changeName: string, documentName: string) => { validateChangeName(changeName); const root = requireProjectRoot(); const config = await loadConfig(root); const planDocs = config.stages.plan?.documents; if (!planDocs || !planDocs.find((d) => d.name === documentName)) { throw new CommandError(`文档 "${documentName}" 不在配置的 plan.documents 中`, { hint: `可用文档:${planDocs?.map((d) => d.name).join(", ") ?? "无"}`, }); } const changeDir = getChangeDir(root, changeName); await mkdir(changeDir, { recursive: true }); const doc = planDocs.find((d) => d.name === documentName)!; if (doc.depend && doc.depend.length > 0) { const missing = doc.depend.filter((dep) => !existsSync(join(changeDir, `${dep}.md`))); if (missing.length > 0) { throw new CommandError( `文档 "${documentName}" 的前置依赖未满足:${missing.map((d) => `${d}.md`).join("、")} 尚未完成`, { hint: `请先完成依赖文档:rune plan ${changeName} ${missing[0]}`, }, ); } } const prompt = await assemblePlanPrompt(config, root, changeName, documentName); console.log(prompt); }); cli.command("build ", "构建阶段").action(async (changeName: string) => { validateChangeName(changeName); 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) => { validateChangeName(changeName); 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 assembleArchivePrompt(config, root, changeName); const today = new Date().toISOString().slice(0, 10); const src = changeDir; const dest = join(getArchiveDir(root), `${today}-${changeName}`); await rename(src, dest); console.log(prompt); }); cli.command("status [change-name]", "查看变更状态").action(async (changeName?: string) => { const root = requireProjectRoot(); const config = await loadConfig(root); 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 查看所有变更", }); } console.log(formatChangeStatus(change, config)); } else { if (changes.length === 0) { console.log("当前无进行中的变更。"); return; } for (const change of changes) { console.log(formatChangeStatus(change, config)); console.log("---\n"); } } }); export function mapError(e: unknown): CliError { if (e instanceof CliError) { return e; } if (e instanceof Error) { const err = mapCacError(e); if (err) return err; } return new InternalError(); } function mapCacError(e: Error): CliError | null { 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 查看所有命令", }); } 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 查看所有命令", }); } 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 查看所有命令", }); } 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} 查看用法`, }); } return null; } function handleError(e: unknown): never { printError(mapError(e)); } process.on("unhandledRejection", (e) => { handleError(e); }); try { cli.parse(); } catch (e) { handleError(e); }