feat: CLI 展示文本使用动态命令前缀
This commit is contained in:
104
src/cli.ts
104
src/cli.ts
@@ -15,12 +15,14 @@ import { scanChanges } from "./core/scanner.ts";
|
|||||||
import { UsageError, ConfigError, CommandError, InternalError, CliError } from "./cli/errors.ts";
|
import { UsageError, ConfigError, CommandError, InternalError, CliError } from "./cli/errors.ts";
|
||||||
import { printError } from "./cli/output.ts";
|
import { printError } from "./cli/output.ts";
|
||||||
import { showGlobalHelp, showCommandHelp } from "./cli/help.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";
|
import type { ChangeStatus, RuneConfig } from "./types.ts";
|
||||||
|
|
||||||
function requireProjectRoot(): string {
|
function requireProjectRoot(): string {
|
||||||
const root = findProjectRoot();
|
const root = findProjectRoot();
|
||||||
if (!root) {
|
if (!root) {
|
||||||
throw new ConfigError("当前项目未初始化", { hint: "请先运行 rune init" });
|
const prefix = getPmPrefix();
|
||||||
|
throw new ConfigError("当前项目未初始化", { hint: `请先运行 ${prefix} init` });
|
||||||
}
|
}
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
@@ -66,48 +68,65 @@ export function formatChangeStatus(change: ChangeStatus, config?: RuneConfig): s
|
|||||||
}
|
}
|
||||||
|
|
||||||
lines.push("");
|
lines.push("");
|
||||||
lines.push(` 建议下一步:${suggestNextStep(change)}`);
|
lines.push(` 建议下一步:${suggestNextStep(change, config)}`);
|
||||||
|
|
||||||
return lines.join("\n");
|
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) {
|
if (!change.planCompleted) {
|
||||||
const nextDoc = change.documents.find((d) => !d.completed && d.dependMet);
|
const nextDoc = change.documents.find((d) => !d.completed && d.dependMet);
|
||||||
if (nextDoc) {
|
if (nextDoc) {
|
||||||
return `rune plan ${change.name} ${nextDoc.name}`;
|
return `${prefix} plan ${change.name} ${nextDoc.name}`;
|
||||||
}
|
}
|
||||||
return `完成前置依赖后再规划文档`;
|
return `完成前置依赖后再规划文档`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (change.taskProgress && change.taskProgress.completed < change.taskProgress.total) {
|
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) {
|
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");
|
const cli = cac("rune");
|
||||||
|
|
||||||
cli.command("", "").action(() => {
|
cli.command("", "").action(async () => {
|
||||||
console.log(showGlobalHelp());
|
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) {
|
if (command) {
|
||||||
const output = showCommandHelp(command);
|
const output = showCommandHelp(command, prefix);
|
||||||
if (!output) {
|
if (!output) {
|
||||||
throw new UsageError(`未知命令: ${command}`, {
|
throw new UsageError(`未知命令: ${command}`, {
|
||||||
hint: "运行 rune help 查看所有命令",
|
hint: `运行 ${prefix} help 查看所有命令`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
console.log(output);
|
console.log(output);
|
||||||
} else {
|
} 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[]) => {
|
cli.command("init [...tools]", "初始化 Rune 并注入工具配置").action(async (tools: string[]) => {
|
||||||
|
const prefix = getPmPrefix();
|
||||||
|
const fallbackNote = getFallbackNote();
|
||||||
if (!tools || tools.length === 0) {
|
if (!tools || tools.length === 0) {
|
||||||
throw new UsageError("请指定至少一个工具", {
|
throw new UsageError("请指定至少一个工具", {
|
||||||
usage: "rune init <工具...>",
|
usage: `${prefix} init <工具...>`,
|
||||||
hint: "如:rune init opencode",
|
hint: `如:${prefix} init opencode\n\n${fallbackNote}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await runInit(process.cwd(), tools);
|
await runInit(process.cwd(), tools);
|
||||||
@@ -128,17 +149,19 @@ cli.command("init [...tools]", "初始化 Rune 并注入工具配置").action(as
|
|||||||
});
|
});
|
||||||
|
|
||||||
cli.command("update [...tools]", "更新已注入的工具配置").action(async (tools: string[]) => {
|
cli.command("update [...tools]", "更新已注入的工具配置").action(async (tools: string[]) => {
|
||||||
|
const prefix = getPmPrefix();
|
||||||
|
const fallbackNote = getFallbackNote();
|
||||||
if (!tools || tools.length === 0) {
|
if (!tools || tools.length === 0) {
|
||||||
throw new UsageError("请指定至少一个工具", {
|
throw new UsageError("请指定至少一个工具", {
|
||||||
usage: "rune update <工具...>",
|
usage: `${prefix} update <工具...>`,
|
||||||
hint: "如:rune update opencode",
|
hint: `如:${prefix} update opencode\n\n${fallbackNote}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const root = requireProjectRoot();
|
const root = requireProjectRoot();
|
||||||
const { updateOpenCode } = await import("./adapters/opencode.ts");
|
const { updateOpenCode } = await import("./adapters/opencode.ts");
|
||||||
const { updateClaudeCode } = await import("./adapters/claude-code.ts");
|
const { updateClaudeCode } = await import("./adapters/claude-code.ts");
|
||||||
const { SUPPORTED_TOOLS } = await import("./commands/init.ts");
|
const { SUPPORTED_TOOLS } = await import("./commands/init.ts");
|
||||||
const updaters: Record<string, (root: string) => Promise<void>> = {
|
const updaters: Record<string, (root: string, command?: string) => Promise<void>> = {
|
||||||
opencode: updateOpenCode,
|
opencode: updateOpenCode,
|
||||||
"claude-code": updateClaudeCode,
|
"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) {
|
for (const tool of tools) {
|
||||||
await updaters[tool](root);
|
await updaters[tool](root, command === DEFAULT_PREFIX ? undefined : command);
|
||||||
}
|
}
|
||||||
console.log(`工具配置已更新:${tools.join(", ")}`);
|
console.log(`工具配置已更新:${tools.join(", ")}`);
|
||||||
});
|
});
|
||||||
@@ -185,7 +229,7 @@ cli
|
|||||||
throw new CommandError(
|
throw new CommandError(
|
||||||
`文档 "${documentName}" 的前置依赖未满足:${missing.map((d) => `${d}.md`).join("、")} 尚未完成`,
|
`文档 "${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 <change-name>", "构建阶段").action(async (changeName: str
|
|||||||
const root = requireProjectRoot();
|
const root = requireProjectRoot();
|
||||||
const changeDir = getChangeDir(root, changeName);
|
const changeDir = getChangeDir(root, changeName);
|
||||||
if (!existsSync(changeDir)) {
|
if (!existsSync(changeDir)) {
|
||||||
|
const prefix = getPmPrefix();
|
||||||
throw new CommandError(`变更 '${changeName}' 不存在`, {
|
throw new CommandError(`变更 '${changeName}' 不存在`, {
|
||||||
hint: `请先运行 rune plan ${changeName} 创建变更`,
|
hint: `请先运行 ${prefix} plan ${changeName} 创建变更`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const config = await loadConfig(root);
|
const config = await loadConfig(root);
|
||||||
@@ -214,8 +259,9 @@ cli.command("archive <change-name>", "归档阶段").action(async (changeName: s
|
|||||||
const root = requireProjectRoot();
|
const root = requireProjectRoot();
|
||||||
const changeDir = getChangeDir(root, changeName);
|
const changeDir = getChangeDir(root, changeName);
|
||||||
if (!existsSync(changeDir)) {
|
if (!existsSync(changeDir)) {
|
||||||
|
const prefix = getPmPrefix();
|
||||||
throw new CommandError(`变更 '${changeName}' 不存在`, {
|
throw new CommandError(`变更 '${changeName}' 不存在`, {
|
||||||
hint: `请先运行 rune plan ${changeName} 创建变更`,
|
hint: `请先运行 ${prefix} plan ${changeName} 创建变更`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const config = await loadConfig(root);
|
const config = await loadConfig(root);
|
||||||
@@ -230,13 +276,14 @@ cli.command("archive <change-name>", "归档阶段").action(async (changeName: s
|
|||||||
cli.command("status [change-name]", "查看变更状态").action(async (changeName?: string) => {
|
cli.command("status [change-name]", "查看变更状态").action(async (changeName?: string) => {
|
||||||
const root = requireProjectRoot();
|
const root = requireProjectRoot();
|
||||||
const config = await loadConfig(root);
|
const config = await loadConfig(root);
|
||||||
|
const prefix = getPmPrefix(config);
|
||||||
const changes = await scanChanges(root, config);
|
const changes = await scanChanges(root, config);
|
||||||
|
|
||||||
if (changeName) {
|
if (changeName) {
|
||||||
const change = changes.find((c) => c.name === changeName);
|
const change = changes.find((c) => c.name === changeName);
|
||||||
if (!change) {
|
if (!change) {
|
||||||
throw new CommandError(`变更 "${changeName}" 不存在`, {
|
throw new CommandError(`变更 "${changeName}" 不存在`, {
|
||||||
hint: "运行 rune status 查看所有变更",
|
hint: `运行 ${prefix} status 查看所有变更`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
console.log(formatChangeStatus(change, config));
|
console.log(formatChangeStatus(change, config));
|
||||||
@@ -264,33 +311,34 @@ export function mapError(e: unknown): CliError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function mapCacError(e: Error): CliError | null {
|
function mapCacError(e: Error): CliError | null {
|
||||||
|
const prefix = getPmPrefix();
|
||||||
if (e.message.includes("Unknown option")) {
|
if (e.message.includes("Unknown option")) {
|
||||||
const match = e.message.match(/Unknown option `([^`]+)`/);
|
const match = e.message.match(/Unknown option `([^`]+)`/);
|
||||||
const flag = match ? match[1] : "未知选项";
|
const flag = match ? match[1] : "未知选项";
|
||||||
return new UsageError(`未知选项: ${flag}`, {
|
return new UsageError(`未知选项: ${flag}`, {
|
||||||
hint: "运行 rune help 查看所有命令",
|
hint: `运行 ${prefix} help 查看所有命令`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (e.message.includes("Unknown command")) {
|
if (e.message.includes("Unknown command")) {
|
||||||
const match = e.message.match(/Unknown command `([^`]+)`/);
|
const match = e.message.match(/Unknown command `([^`]+)`/);
|
||||||
const cmd = match ? match[1] : "未知命令";
|
const cmd = match ? match[1] : "未知命令";
|
||||||
return new UsageError(`未知命令: ${cmd}`, {
|
return new UsageError(`未知命令: ${cmd}`, {
|
||||||
hint: "运行 rune help 查看所有命令",
|
hint: `运行 ${prefix} help 查看所有命令`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (e.message.includes("Unused args")) {
|
if (e.message.includes("Unused args")) {
|
||||||
const match = e.message.match(/Unused args: (.+)/);
|
const match = e.message.match(/Unused args: (.+)/);
|
||||||
const args = match ? match[1] : "未知参数";
|
const args = match ? match[1] : "未知参数";
|
||||||
return new UsageError(`未知命令: ${args.replace(/`/g, "")}`, {
|
return new UsageError(`未知命令: ${args.replace(/`/g, "")}`, {
|
||||||
hint: "运行 rune help 查看所有命令",
|
hint: `运行 ${prefix} help 查看所有命令`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (e.message.includes("missing required args")) {
|
if (e.message.includes("missing required args")) {
|
||||||
const match = e.message.match(/command `(\w+)/);
|
const match = e.message.match(/command `(\w+)/);
|
||||||
const cmd = match ? match[1] : "未知命令";
|
const cmd = match ? match[1] : "未知命令";
|
||||||
return new UsageError(`命令 '${cmd}' 缺少必填参数`, {
|
return new UsageError(`命令 '${cmd}' 缺少必填参数`, {
|
||||||
usage: `rune ${cmd} <change-name>`,
|
usage: `${prefix} ${cmd} <change-name>`,
|
||||||
hint: `运行 rune help ${cmd} 查看用法`,
|
hint: `运行 ${prefix} help ${cmd} 查看用法`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { describe, it, expect } from "bun:test";
|
import { describe, it, expect } from "bun:test";
|
||||||
import { UsageError, ConfigError, InternalError } from "../../src/cli/errors.ts";
|
import { UsageError, ConfigError, InternalError } from "../../src/cli/errors.ts";
|
||||||
import { mapError } from "../../src/cli.ts";
|
import { mapError } from "../../src/cli.ts";
|
||||||
|
import { DEFAULT_PREFIX } from "../../src/core/pm.ts";
|
||||||
|
|
||||||
describe("mapError", () => {
|
describe("mapError", () => {
|
||||||
it("CliError 原样返回", () => {
|
it("CliError 原样返回", () => {
|
||||||
@@ -15,7 +16,7 @@ describe("mapError", () => {
|
|||||||
const result = mapError(err);
|
const result = mapError(err);
|
||||||
expect(result).toBeInstanceOf(UsageError);
|
expect(result).toBeInstanceOf(UsageError);
|
||||||
expect(result.message).toBe("未知选项: --badflag");
|
expect(result.message).toBe("未知选项: --badflag");
|
||||||
expect(result.hint).toBe("运行 rune help 查看所有命令");
|
expect(result.hint).toBe(`运行 ${DEFAULT_PREFIX} help 查看所有命令`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Unknown command 转为 UsageError", () => {
|
it("Unknown command 转为 UsageError", () => {
|
||||||
@@ -38,8 +39,8 @@ describe("mapError", () => {
|
|||||||
const result = mapError(err);
|
const result = mapError(err);
|
||||||
expect(result).toBeInstanceOf(UsageError);
|
expect(result).toBeInstanceOf(UsageError);
|
||||||
expect(result.message).toBe("命令 'plan' 缺少必填参数");
|
expect(result.message).toBe("命令 'plan' 缺少必填参数");
|
||||||
expect(result.usage).toBe("rune plan <change-name>");
|
expect(result.usage).toBe(`${DEFAULT_PREFIX} plan <change-name>`);
|
||||||
expect(result.hint).toContain("rune help plan");
|
expect(result.hint).toContain(`${DEFAULT_PREFIX} help plan`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("未知 Error 转为 InternalError", () => {
|
it("未知 Error 转为 InternalError", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user