chore: 添加 .oxfmtrc.json 并格式化全部代码
This commit is contained in:
9
.oxfmtrc.json
Normal file
9
.oxfmtrc.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
||||||
|
"printWidth": 100,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": false,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"arrowParens": "always",
|
||||||
|
"endOfLine": "lf"
|
||||||
|
}
|
||||||
19
README.md
19
README.md
@@ -18,6 +18,7 @@ bunx rune init claude-code # Claude Code 编辑器
|
|||||||
```
|
```
|
||||||
|
|
||||||
会在项目中创建:
|
会在项目中创建:
|
||||||
|
|
||||||
- `.rune/` 目录(配置、变更文档、归档)
|
- `.rune/` 目录(配置、变更文档、归档)
|
||||||
- 编辑器对应的 command 和 skill 文件(如 `.opencode/commands/`、`.opencode/skills/`)
|
- 编辑器对应的 command 和 skill 文件(如 `.opencode/commands/`、`.opencode/skills/`)
|
||||||
|
|
||||||
@@ -58,15 +59,15 @@ rune help <command> # 显示指定命令的详细帮助
|
|||||||
rune version # 显示版本号
|
rune version # 显示版本号
|
||||||
```
|
```
|
||||||
|
|
||||||
| 命令 | 说明 |
|
| 命令 | 说明 |
|
||||||
|------|------|
|
| ----------------------------- | ----------------------------------------------- |
|
||||||
| `rune init <tool>` | 初始化项目,注入编辑器配置 |
|
| `rune init <tool>` | 初始化项目,注入编辑器配置 |
|
||||||
| `rune update <tool>` | 更新编辑器的命令和 skill 文件 |
|
| `rune update <tool>` | 更新编辑器的命令和 skill 文件 |
|
||||||
| `rune discuss` | 输出讨论阶段提示词 |
|
| `rune discuss` | 输出讨论阶段提示词 |
|
||||||
| `rune plan <变更名> <文档名>` | 输出规划阶段提示词 |
|
| `rune plan <变更名> <文档名>` | 输出规划阶段提示词 |
|
||||||
| `rune build <变更名>` | 输出构建阶段提示词 |
|
| `rune build <变更名>` | 输出构建阶段提示词 |
|
||||||
| `rune archive <变更名>` | 输出归档阶段提示词,同时移动变更目录到 archive/ |
|
| `rune archive <变更名>` | 输出归档阶段提示词,同时移动变更目录到 archive/ |
|
||||||
| `rune status [变更名]` | 显示变更状态和下一步建议 |
|
| `rune status [变更名]` | 显示变更状态和下一步建议 |
|
||||||
|
|
||||||
### 自定义配置
|
### 自定义配置
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://opencode.ai/config.json",
|
"$schema": "https://opencode.ai/config.json",
|
||||||
"plugin": [
|
"plugin": ["superpowers@git+https://github.com/obra/superpowers.git"]
|
||||||
"superpowers@git+https://github.com/obra/superpowers.git"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
20
package.json
20
package.json
@@ -1,12 +1,19 @@
|
|||||||
{
|
{
|
||||||
"name": "rune",
|
"name": "rune",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"module": "src/cli.ts",
|
"private": true,
|
||||||
"type": "module",
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"rune": "./src/cli.ts"
|
"rune": "./src/cli.ts"
|
||||||
},
|
},
|
||||||
"private": true,
|
"type": "module",
|
||||||
|
"module": "src/cli.ts",
|
||||||
|
"scripts": {
|
||||||
|
"prepare": "husky"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"cac": "^7.0.0",
|
||||||
|
"yaml": "^2.7.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
@@ -16,12 +23,5 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"cac": "^7.0.0",
|
|
||||||
"yaml": "^2.7.0"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"prepare": "husky"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,12 +14,8 @@ export async function injectClaudeCode(projectRoot: string): Promise<void> {
|
|||||||
await mkdir(commandDir, { recursive: true });
|
await mkdir(commandDir, { recursive: true });
|
||||||
const commandPath = join(commandDir, `rune-${stage}.md`);
|
const commandPath = join(commandDir, `rune-${stage}.md`);
|
||||||
if (!existsSync(commandPath)) {
|
if (!existsSync(commandPath)) {
|
||||||
const cmd = hasChangeName
|
const cmd = hasChangeName ? `rune ${stage} <变更名>` : `rune ${stage}`;
|
||||||
? `rune ${stage} <变更名>`
|
const nameHint = hasChangeName ? "\n如果用户没有指定变更名称,请向用户确认。" : "";
|
||||||
: `rune ${stage}`;
|
|
||||||
const nameHint = hasChangeName
|
|
||||||
? "\n如果用户没有指定变更名称,请向用户确认。"
|
|
||||||
: "";
|
|
||||||
await writeFile(
|
await writeFile(
|
||||||
commandPath,
|
commandPath,
|
||||||
`执行以下命令,将输出作为当前阶段的工作指引:\n\`\`\`bash\n${cmd}\n\`\`\`${nameHint}\n`,
|
`执行以下命令,将输出作为当前阶段的工作指引:\n\`\`\`bash\n${cmd}\n\`\`\`${nameHint}\n`,
|
||||||
@@ -30,10 +26,7 @@ export async function injectClaudeCode(projectRoot: string): Promise<void> {
|
|||||||
const commandDir = join(projectRoot, COMMANDS_DIR);
|
const commandDir = join(projectRoot, COMMANDS_DIR);
|
||||||
const statusPath = join(commandDir, "rune-status.md");
|
const statusPath = join(commandDir, "rune-status.md");
|
||||||
if (!existsSync(statusPath)) {
|
if (!existsSync(statusPath)) {
|
||||||
await writeFile(
|
await writeFile(statusPath, `执行以下命令查看变更状态:\n\`\`\`bash\nrune status\n\`\`\`\n`);
|
||||||
statusPath,
|
|
||||||
`执行以下命令查看变更状态:\n\`\`\`bash\nrune status\n\`\`\`\n`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,12 +37,8 @@ export async function updateClaudeCode(projectRoot: string): Promise<void> {
|
|||||||
const commandDir = join(projectRoot, COMMANDS_DIR);
|
const commandDir = join(projectRoot, COMMANDS_DIR);
|
||||||
await mkdir(commandDir, { recursive: true });
|
await mkdir(commandDir, { recursive: true });
|
||||||
const commandPath = join(commandDir, `rune-${stage}.md`);
|
const commandPath = join(commandDir, `rune-${stage}.md`);
|
||||||
const cmd = hasChangeName
|
const cmd = hasChangeName ? `rune ${stage} <变更名>` : `rune ${stage}`;
|
||||||
? `rune ${stage} <变更名>`
|
const nameHint = hasChangeName ? "\n如果用户没有指定变更名称,请向用户确认。" : "";
|
||||||
: `rune ${stage}`;
|
|
||||||
const nameHint = hasChangeName
|
|
||||||
? "\n如果用户没有指定变更名称,请向用户确认。"
|
|
||||||
: "";
|
|
||||||
const newContent = `执行以下命令,将输出作为当前阶段的工作指引:\n\`\`\`bash\n${cmd}\n\`\`\`${nameHint}\n`;
|
const newContent = `执行以下命令,将输出作为当前阶段的工作指引:\n\`\`\`bash\n${cmd}\n\`\`\`${nameHint}\n`;
|
||||||
await writeIfChanged(commandPath, newContent);
|
await writeIfChanged(commandPath, newContent);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,9 +81,7 @@ function generateCommand(stage: string, hasChangeName: boolean): string {
|
|||||||
|
|
||||||
function generateSkill(stage: string, hasChangeName: boolean): string {
|
function generateSkill(stage: string, hasChangeName: boolean): string {
|
||||||
const cmd = hasChangeName ? `rune ${stage} <变更名>` : `rune ${stage}`;
|
const cmd = hasChangeName ? `rune ${stage} <变更名>` : `rune ${stage}`;
|
||||||
const nameHint = hasChangeName
|
const nameHint = hasChangeName ? `将 <变更名> 替换为实际的变更名称。\n` : "";
|
||||||
? `将 <变更名> 替换为实际的变更名称。\n`
|
|
||||||
: "";
|
|
||||||
|
|
||||||
let extraGuide = "";
|
let extraGuide = "";
|
||||||
if (stage === "plan") {
|
if (stage === "plan") {
|
||||||
|
|||||||
218
src/cli.ts
218
src/cli.ts
@@ -17,7 +17,6 @@ import { printError } from "./cli/output.ts";
|
|||||||
import { showGlobalHelp, showCommandHelp } from "./cli/help.ts";
|
import { showGlobalHelp, showCommandHelp } from "./cli/help.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) {
|
||||||
@@ -28,10 +27,9 @@ function requireProjectRoot(): string {
|
|||||||
|
|
||||||
export function validateChangeName(name: string): void {
|
export function validateChangeName(name: string): void {
|
||||||
if (!/^[\u4e00-\u9fa5a-zA-Z-]+$/.test(name)) {
|
if (!/^[\u4e00-\u9fa5a-zA-Z-]+$/.test(name)) {
|
||||||
throw new CommandError(
|
throw new CommandError(`变更名 "${name}" 包含不支持的字符`, {
|
||||||
`变更名 "${name}" 包含不支持的字符`,
|
hint: "变更名仅支持中文、英文和短横线(-)",
|
||||||
{ hint: "变更名仅支持中文、英文和短横线(-)" },
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,9 +44,10 @@ export function formatChangeStatus(change: ChangeStatus, config?: RuneConfig): s
|
|||||||
lines.push(` ${doc.name}.md ✓ 已完成`);
|
lines.push(` ${doc.name}.md ✓ 已完成`);
|
||||||
} else {
|
} else {
|
||||||
const docConfig = planDocs?.find((d) => d.name === doc.name);
|
const docConfig = planDocs?.find((d) => d.name === doc.name);
|
||||||
const depInfo = !doc.dependMet && docConfig?.depend?.length
|
const depInfo =
|
||||||
? `(依赖 ${docConfig.depend.map((d) => `${d}.md`).join("、")})`
|
!doc.dependMet && docConfig?.depend?.length
|
||||||
: "";
|
? `(依赖 ${docConfig.depend.map((d) => `${d}.md`).join("、")})`
|
||||||
|
: "";
|
||||||
lines.push(` ${doc.name}.md ○ 待完成${depInfo}`);
|
lines.push(` ${doc.name}.md ○ 待完成${depInfo}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,48 +116,44 @@ cli.command("version", "显示版本号").action(() => {
|
|||||||
console.log(`rune v${pkg.version}`);
|
console.log(`rune v${pkg.version}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
cli.command("init [...tools]", "初始化 Rune 并注入工具配置").action(
|
cli.command("init [...tools]", "初始化 Rune 并注入工具配置").action(async (tools: string[]) => {
|
||||||
async (tools: string[]) => {
|
if (!tools || tools.length === 0) {
|
||||||
if (!tools || tools.length === 0) {
|
throw new UsageError("请指定至少一个工具", {
|
||||||
throw new UsageError("请指定至少一个工具", {
|
usage: "rune init <工具...>",
|
||||||
usage: "rune init <工具...>",
|
hint: "如:rune init opencode",
|
||||||
hint: "如:rune init opencode",
|
});
|
||||||
});
|
}
|
||||||
}
|
await runInit(process.cwd(), tools);
|
||||||
await runInit(process.cwd(), tools);
|
console.log(`Rune 初始化完成,已注入工具:${tools.join(", ")}`);
|
||||||
console.log(`Rune 初始化完成,已注入工具:${tools.join(", ")}`);
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
cli.command("update [...tools]", "更新已注入的工具配置").action(
|
cli.command("update [...tools]", "更新已注入的工具配置").action(async (tools: string[]) => {
|
||||||
async (tools: string[]) => {
|
if (!tools || tools.length === 0) {
|
||||||
if (!tools || tools.length === 0) {
|
throw new UsageError("请指定至少一个工具", {
|
||||||
throw new UsageError("请指定至少一个工具", {
|
usage: "rune update <工具...>",
|
||||||
usage: "rune update <工具...>",
|
hint: "如:rune update opencode",
|
||||||
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<string, (root: string) => Promise<void>> = {
|
||||||
|
opencode: updateOpenCode,
|
||||||
|
"claude-code": updateClaudeCode,
|
||||||
|
};
|
||||||
|
for (const tool of tools) {
|
||||||
|
if (!SUPPORTED_TOOLS[tool]) {
|
||||||
|
throw new CommandError(`不支持的工具: ${tool}`, {
|
||||||
|
hint: `支持的工具: ${Object.keys(SUPPORTED_TOOLS).join(", ")}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const root = requireProjectRoot();
|
}
|
||||||
const { updateOpenCode } = await import("./adapters/opencode.ts");
|
for (const tool of tools) {
|
||||||
const { updateClaudeCode } = await import("./adapters/claude-code.ts");
|
await updaters[tool](root);
|
||||||
const { SUPPORTED_TOOLS } = await import("./commands/init.ts");
|
}
|
||||||
const updaters: Record<string, (root: string) => Promise<void>> = {
|
console.log(`工具配置已更新:${tools.join(", ")}`);
|
||||||
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 () => {
|
cli.command("discuss", "讨论阶段").action(async () => {
|
||||||
const root = requireProjectRoot();
|
const root = requireProjectRoot();
|
||||||
@@ -167,19 +162,17 @@ cli.command("discuss", "讨论阶段").action(async () => {
|
|||||||
console.log(prompt);
|
console.log(prompt);
|
||||||
});
|
});
|
||||||
|
|
||||||
cli.command("plan <change-name> <document-name>", "规划阶段").action(
|
cli
|
||||||
async (changeName: string, documentName: string) => {
|
.command("plan <change-name> <document-name>", "规划阶段")
|
||||||
|
.action(async (changeName: string, documentName: string) => {
|
||||||
validateChangeName(changeName);
|
validateChangeName(changeName);
|
||||||
const root = requireProjectRoot();
|
const root = requireProjectRoot();
|
||||||
const config = await loadConfig(root);
|
const config = await loadConfig(root);
|
||||||
const planDocs = config.stages.plan?.documents;
|
const planDocs = config.stages.plan?.documents;
|
||||||
if (!planDocs || !planDocs.find((d) => d.name === documentName)) {
|
if (!planDocs || !planDocs.find((d) => d.name === documentName)) {
|
||||||
throw new CommandError(
|
throw new CommandError(`文档 "${documentName}" 不在配置的 plan.documents 中`, {
|
||||||
`文档 "${documentName}" 不在配置的 plan.documents 中`,
|
hint: `可用文档:${planDocs?.map((d) => d.name).join(", ") ?? "无"}`,
|
||||||
{
|
});
|
||||||
hint: `可用文档:${planDocs?.map((d) => d.name).join(", ") ?? "无"}`,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const changeDir = getChangeDir(root, changeName);
|
const changeDir = getChangeDir(root, changeName);
|
||||||
@@ -187,9 +180,7 @@ cli.command("plan <change-name> <document-name>", "规划阶段").action(
|
|||||||
|
|
||||||
const doc = planDocs.find((d) => d.name === documentName)!;
|
const doc = planDocs.find((d) => d.name === documentName)!;
|
||||||
if (doc.depend && doc.depend.length > 0) {
|
if (doc.depend && doc.depend.length > 0) {
|
||||||
const missing = doc.depend.filter(
|
const missing = doc.depend.filter((dep) => !existsSync(join(changeDir, `${dep}.md`)));
|
||||||
(dep) => !existsSync(join(changeDir, `${dep}.md`)),
|
|
||||||
);
|
|
||||||
if (missing.length > 0) {
|
if (missing.length > 0) {
|
||||||
throw new CommandError(
|
throw new CommandError(
|
||||||
`文档 "${documentName}" 的前置依赖未满足:${missing.map((d) => `${d}.md`).join("、")} 尚未完成`,
|
`文档 "${documentName}" 的前置依赖未满足:${missing.map((d) => `${d}.md`).join("、")} 尚未完成`,
|
||||||
@@ -202,71 +193,64 @@ cli.command("plan <change-name> <document-name>", "规划阶段").action(
|
|||||||
|
|
||||||
const prompt = await assemblePlanPrompt(config, root, changeName, documentName);
|
const prompt = await assemblePlanPrompt(config, root, changeName, documentName);
|
||||||
console.log(prompt);
|
console.log(prompt);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
cli.command("build <change-name>", "构建阶段").action(
|
cli.command("build <change-name>", "构建阶段").action(async (changeName: string) => {
|
||||||
async (changeName: string) => {
|
validateChangeName(changeName);
|
||||||
validateChangeName(changeName);
|
const root = requireProjectRoot();
|
||||||
const root = requireProjectRoot();
|
const changeDir = getChangeDir(root, changeName);
|
||||||
const changeDir = getChangeDir(root, changeName);
|
if (!existsSync(changeDir)) {
|
||||||
if (!existsSync(changeDir)) {
|
throw new CommandError(`变更 '${changeName}' 不存在`, {
|
||||||
throw new CommandError(`变更 '${changeName}' 不存在`, {
|
hint: `请先运行 rune plan ${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) => {
|
||||||
|
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 查看所有变更",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const config = await loadConfig(root);
|
console.log(formatChangeStatus(change, config));
|
||||||
const prompt = await assembleBuildPrompt(config, root, changeName);
|
} else {
|
||||||
console.log(prompt);
|
if (changes.length === 0) {
|
||||||
},
|
console.log("当前无进行中的变更。");
|
||||||
);
|
return;
|
||||||
|
|
||||||
cli.command("archive <change-name>", "归档阶段").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);
|
for (const change of changes) {
|
||||||
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));
|
console.log(formatChangeStatus(change, config));
|
||||||
} else {
|
console.log("---\n");
|
||||||
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 {
|
export function mapError(e: unknown): CliError {
|
||||||
if (e instanceof CliError) {
|
if (e instanceof CliError) {
|
||||||
|
|||||||
@@ -2,10 +2,7 @@ export class CliError extends Error {
|
|||||||
readonly hint?: string;
|
readonly hint?: string;
|
||||||
readonly usage?: string;
|
readonly usage?: string;
|
||||||
|
|
||||||
constructor(
|
constructor(message: string, opts?: { hint?: string; usage?: string }) {
|
||||||
message: string,
|
|
||||||
opts?: { hint?: string; usage?: string },
|
|
||||||
) {
|
|
||||||
super(message);
|
super(message);
|
||||||
this.name = this.constructor.name;
|
this.name = this.constructor.name;
|
||||||
this.hint = opts?.hint;
|
this.hint = opts?.hint;
|
||||||
|
|||||||
@@ -16,10 +16,7 @@ const COMMANDS: Record<string, CommandHelpDef> = {
|
|||||||
usage: "rune init <工具...>",
|
usage: "rune init <工具...>",
|
||||||
args: [{ name: "<工具...>", desc: "要注入的 AI 工具,如 opencode、claude-code" }],
|
args: [{ name: "<工具...>", desc: "要注入的 AI 工具,如 opencode、claude-code" }],
|
||||||
detail: "在当前项目中创建 .rune 目录结构,并注入指定 AI 工具的 command 和 skill 配置文件。",
|
detail: "在当前项目中创建 .rune 目录结构,并注入指定 AI 工具的 command 和 skill 配置文件。",
|
||||||
examples: [
|
examples: ["rune init opencode", "rune init opencode claude-code"],
|
||||||
"rune init opencode",
|
|
||||||
"rune init opencode claude-code",
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
discuss: {
|
discuss: {
|
||||||
name: "discuss",
|
name: "discuss",
|
||||||
@@ -36,38 +33,30 @@ const COMMANDS: Record<string, CommandHelpDef> = {
|
|||||||
description: "规划:生成指定文档的规划提示词",
|
description: "规划:生成指定文档的规划提示词",
|
||||||
usage: "rune plan <change-name> <document-name>",
|
usage: "rune plan <change-name> <document-name>",
|
||||||
args: [
|
args: [
|
||||||
{ name: "<change-name>", desc: "变更名称,如 \"add-login\"" },
|
{ name: "<change-name>", desc: '变更名称,如 "add-login"' },
|
||||||
{ name: "<document-name>", desc: "文档名称,如 \"design\"、\"task\"" },
|
{ name: "<document-name>", desc: '文档名称,如 "design"、"task"' },
|
||||||
],
|
|
||||||
detail: "生成规划阶段指定文档的提示词。依赖的前置文档必须已完成。可用文档由配置中的 plan.documents 定义。",
|
|
||||||
examples: [
|
|
||||||
"rune plan add-user-auth design",
|
|
||||||
"rune plan add-user-auth task",
|
|
||||||
],
|
],
|
||||||
|
detail:
|
||||||
|
"生成规划阶段指定文档的提示词。依赖的前置文档必须已完成。可用文档由配置中的 plan.documents 定义。",
|
||||||
|
examples: ["rune plan add-user-auth design", "rune plan add-user-auth task"],
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
name: "build",
|
name: "build",
|
||||||
alias: "build <名称>",
|
alias: "build <名称>",
|
||||||
description: "构建:生成构建阶段提示词",
|
description: "构建:生成构建阶段提示词",
|
||||||
usage: "rune build <change-name>",
|
usage: "rune build <change-name>",
|
||||||
args: [{ name: "<change-name>", desc: "变更名称,如 \"add-login\"" }],
|
args: [{ name: "<change-name>", desc: '变更名称,如 "add-login"' }],
|
||||||
detail: "生成构建阶段的提示词。变更目录需已存在(通过 rune plan 创建)。",
|
detail: "生成构建阶段的提示词。变更目录需已存在(通过 rune plan 创建)。",
|
||||||
examples: [
|
examples: ["rune build add-user-auth", "rune build fix-memory-leak"],
|
||||||
"rune build add-user-auth",
|
|
||||||
"rune build fix-memory-leak",
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
archive: {
|
archive: {
|
||||||
name: "archive",
|
name: "archive",
|
||||||
alias: "archive <名称>",
|
alias: "archive <名称>",
|
||||||
description: "归档:归档变更并生成提示词",
|
description: "归档:归档变更并生成提示词",
|
||||||
usage: "rune archive <change-name>",
|
usage: "rune archive <change-name>",
|
||||||
args: [{ name: "<change-name>", desc: "变更名称,如 \"add-login\"" }],
|
args: [{ name: "<change-name>", desc: '变更名称,如 "add-login"' }],
|
||||||
detail: "将变更目录从 .rune/changes/ 移动到 .rune/archive/,并生成归档阶段提示词。",
|
detail: "将变更目录从 .rune/changes/ 移动到 .rune/archive/,并生成归档阶段提示词。",
|
||||||
examples: [
|
examples: ["rune archive add-user-auth", "rune archive fix-memory-leak"],
|
||||||
"rune archive add-user-auth",
|
|
||||||
"rune archive fix-memory-leak",
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
name: "update",
|
name: "update",
|
||||||
@@ -75,11 +64,9 @@ const COMMANDS: Record<string, CommandHelpDef> = {
|
|||||||
description: "更新:更新已注入的编辑器配置",
|
description: "更新:更新已注入的编辑器配置",
|
||||||
usage: "rune update <工具...>",
|
usage: "rune update <工具...>",
|
||||||
args: [{ name: "<工具...>", desc: "要更新的 AI 工具,如 opencode、claude-code" }],
|
args: [{ name: "<工具...>", desc: "要更新的 AI 工具,如 opencode、claude-code" }],
|
||||||
detail: "对比已注入的命令和 skill 文件,与内置版本不一致时覆盖,不存在时新建。用于升级 Rune 后同步编辑器配置。",
|
detail:
|
||||||
examples: [
|
"对比已注入的命令和 skill 文件,与内置版本不一致时覆盖,不存在时新建。用于升级 Rune 后同步编辑器配置。",
|
||||||
"rune update opencode",
|
examples: ["rune update opencode", "rune update opencode claude-code"],
|
||||||
"rune update opencode claude-code",
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
name: "status",
|
name: "status",
|
||||||
@@ -88,10 +75,7 @@ const COMMANDS: Record<string, CommandHelpDef> = {
|
|||||||
usage: "rune status [change-name]",
|
usage: "rune status [change-name]",
|
||||||
args: [{ name: "[change-name]", desc: "可选,指定查看的变更名称" }],
|
args: [{ name: "[change-name]", desc: "可选,指定查看的变更名称" }],
|
||||||
detail: "展示各文档完成状态、依赖满足情况、规划进度和下一步建议。不传参数则显示所有变更。",
|
detail: "展示各文档完成状态、依赖满足情况、规划进度和下一步建议。不传参数则显示所有变更。",
|
||||||
examples: [
|
examples: ["rune status", "rune status add-user-auth"],
|
||||||
"rune status",
|
|
||||||
"rune status add-user-auth",
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -118,7 +102,7 @@ export function showGlobalHelp(): string {
|
|||||||
lines.push("示例:");
|
lines.push("示例:");
|
||||||
lines.push(" rune init opencode 初始化并注入 OpenCode 配置");
|
lines.push(" rune init opencode 初始化并注入 OpenCode 配置");
|
||||||
lines.push(" rune update opencode 更新 OpenCode 配置");
|
lines.push(" rune update opencode 更新 OpenCode 配置");
|
||||||
lines.push(" rune plan add-login design 规划 \"add-login\" 的设计文档");
|
lines.push(' rune plan add-login design 规划 "add-login" 的设计文档');
|
||||||
lines.push(" rune status 查看当前变更状态");
|
lines.push(" rune status 查看当前变更状态");
|
||||||
|
|
||||||
return lines.join("\n");
|
return lines.join("\n");
|
||||||
@@ -128,12 +112,7 @@ export function showCommandHelp(name: string): string | null {
|
|||||||
const cmd = COMMANDS[name];
|
const cmd = COMMANDS[name];
|
||||||
if (!cmd) return null;
|
if (!cmd) return null;
|
||||||
|
|
||||||
const lines: string[] = [
|
const lines: string[] = [`rune ${cmd.name} — ${cmd.description}`, "", "用法:", ` ${cmd.usage}`];
|
||||||
`rune ${cmd.name} — ${cmd.description}`,
|
|
||||||
"",
|
|
||||||
"用法:",
|
|
||||||
` ${cmd.usage}`,
|
|
||||||
];
|
|
||||||
|
|
||||||
if (cmd.args.length > 0) {
|
if (cmd.args.length > 0) {
|
||||||
lines.push("");
|
lines.push("");
|
||||||
|
|||||||
@@ -43,10 +43,7 @@ export const SUPPORTED_TOOLS: Record<string, (root: string) => Promise<void>> =
|
|||||||
"claude-code": injectClaudeCode,
|
"claude-code": injectClaudeCode,
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function runInit(
|
export async function runInit(projectRoot: string, tools: string[]): Promise<void> {
|
||||||
projectRoot: string,
|
|
||||||
tools: string[],
|
|
||||||
): Promise<void> {
|
|
||||||
for (const tool of tools) {
|
for (const tool of tools) {
|
||||||
if (!SUPPORTED_TOOLS[tool]) {
|
if (!SUPPORTED_TOOLS[tool]) {
|
||||||
throw new CommandError(`不支持的工具: ${tool}`, {
|
throw new CommandError(`不支持的工具: ${tool}`, {
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import { parseTasks } from "./task-parser.ts";
|
|||||||
|
|
||||||
export function assembleDiscussPrompt(config: RuneConfig): string {
|
export function assembleDiscussPrompt(config: RuneConfig): string {
|
||||||
const discuss = config.stages.discuss;
|
const discuss = config.stages.discuss;
|
||||||
if (!discuss) throw new CommandError("讨论阶段未配置", {
|
if (!discuss)
|
||||||
|
throw new CommandError("讨论阶段未配置", {
|
||||||
hint: "请在 .rune/config.yaml 中配置 stages.discuss",
|
hint: "请在 .rune/config.yaml 中配置 stages.discuss",
|
||||||
});
|
});
|
||||||
return discuss.prompt;
|
return discuss.prompt;
|
||||||
@@ -21,7 +22,8 @@ export async function assemblePlanPrompt(
|
|||||||
documentName: string,
|
documentName: string,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const plan = config.stages.plan;
|
const plan = config.stages.plan;
|
||||||
if (!plan) throw new CommandError("规划阶段未配置", {
|
if (!plan)
|
||||||
|
throw new CommandError("规划阶段未配置", {
|
||||||
hint: "请在 .rune/config.yaml 中配置 stages.plan",
|
hint: "请在 .rune/config.yaml 中配置 stages.plan",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -110,9 +112,7 @@ export async function assembleBuildPrompt(
|
|||||||
for (const task of pendingTasks) {
|
for (const task of pendingTasks) {
|
||||||
parts.push(`- [ ] ${task.text}`);
|
parts.push(`- [ ] ${task.text}`);
|
||||||
}
|
}
|
||||||
parts.push(
|
parts.push(`\n请从第一个待执行任务开始。完成后更新 ${taskPath} 中的 checkbox。`);
|
||||||
`\n请从第一个待执行任务开始。完成后更新 ${taskPath} 中的 checkbox。`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return parts.join("\n");
|
return parts.join("\n");
|
||||||
}
|
}
|
||||||
@@ -123,7 +123,8 @@ export async function assembleArchivePrompt(
|
|||||||
changeName: string,
|
changeName: string,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const archive = config.stages.archive;
|
const archive = config.stages.archive;
|
||||||
if (!archive) throw new CommandError("归档阶段未配置", {
|
if (!archive)
|
||||||
|
throw new CommandError("归档阶段未配置", {
|
||||||
hint: "请在 .rune/config.yaml 中配置 stages.archive",
|
hint: "请在 .rune/config.yaml 中配置 stages.archive",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ import { ConfigError } from "../cli/errors.ts";
|
|||||||
import type { RuneConfig } from "../types.ts";
|
import type { RuneConfig } from "../types.ts";
|
||||||
import { RUNE_DIR, CONFIG_FILE, CHANGES_DIR, ARCHIVE_DIR } from "../types.ts";
|
import { RUNE_DIR, CONFIG_FILE, CHANGES_DIR, ARCHIVE_DIR } from "../types.ts";
|
||||||
|
|
||||||
export function findProjectRoot(
|
export function findProjectRoot(startDir: string = process.cwd()): string | null {
|
||||||
startDir: string = process.cwd(),
|
|
||||||
): string | null {
|
|
||||||
let dir = startDir;
|
let dir = startDir;
|
||||||
while (true) {
|
while (true) {
|
||||||
if (existsSync(join(dir, RUNE_DIR))) {
|
if (existsSync(join(dir, RUNE_DIR))) {
|
||||||
@@ -49,9 +47,7 @@ export function validateConfig(config: RuneConfig): void {
|
|||||||
throw new ConfigError(`文档 "${doc.name}" 不能依赖自身`);
|
throw new ConfigError(`文档 "${doc.name}" 不能依赖自身`);
|
||||||
}
|
}
|
||||||
if (!docNames.has(dep)) {
|
if (!docNames.has(dep)) {
|
||||||
throw new ConfigError(
|
throw new ConfigError(`文档 "${doc.name}" 依赖 "${dep}" 不存在于 plan.documents 中`);
|
||||||
`文档 "${doc.name}" 依赖 "${dep}" 不存在于 plan.documents 中`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,9 +77,7 @@ export function validateConfig(config: RuneConfig): void {
|
|||||||
for (const doc of plan.documents) {
|
for (const doc of plan.documents) {
|
||||||
path.length = 0;
|
path.length = 0;
|
||||||
if (hasCycle(doc.name)) {
|
if (hasCycle(doc.name)) {
|
||||||
throw new ConfigError(
|
throw new ConfigError(`文档间存在循环依赖:${path.join(" → ")}`);
|
||||||
`文档间存在循环依赖:${path.join(" → ")}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,9 +29,7 @@ export async function scanChanges(
|
|||||||
const fileName = `${docConfig.name}.md`;
|
const fileName = `${docConfig.name}.md`;
|
||||||
const completed = mdFiles.has(fileName);
|
const completed = mdFiles.has(fileName);
|
||||||
const deps = docConfig.depend ?? [];
|
const deps = docConfig.depend ?? [];
|
||||||
const dependMet =
|
const dependMet = deps.length === 0 || deps.every((dep) => mdFiles.has(`${dep}.md`));
|
||||||
deps.length === 0 ||
|
|
||||||
deps.every((dep) => mdFiles.has(`${dep}.md`));
|
|
||||||
return { name: docConfig.name, completed, dependMet };
|
return { name: docConfig.name, completed, dependMet };
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -64,8 +62,7 @@ export async function scanChanges(
|
|||||||
taskProgress,
|
taskProgress,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {}
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,9 +82,7 @@ describe("injectClaudeCode", () => {
|
|||||||
describe("updateClaudeCode", () => {
|
describe("updateClaudeCode", () => {
|
||||||
it("文件不存在时创建", async () => {
|
it("文件不存在时创建", async () => {
|
||||||
await updateClaudeCode(TMP_DIR);
|
await updateClaudeCode(TMP_DIR);
|
||||||
expect(
|
expect(existsSync(join(TMP_DIR, ".claude", "commands", "rune-discuss.md"))).toBe(true);
|
||||||
existsSync(join(TMP_DIR, ".claude", "commands", "rune-discuss.md")),
|
|
||||||
).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("文件存在且内容一致时不覆盖", async () => {
|
it("文件存在且内容一致时不覆盖", async () => {
|
||||||
@@ -104,10 +102,7 @@ describe("updateClaudeCode", () => {
|
|||||||
|
|
||||||
it("文件存在但内容不一致时覆盖", async () => {
|
it("文件存在但内容不一致时覆盖", async () => {
|
||||||
await injectClaudeCode(TMP_DIR);
|
await injectClaudeCode(TMP_DIR);
|
||||||
await writeFile(
|
await writeFile(join(TMP_DIR, ".claude", "commands", "rune-discuss.md"), "旧内容");
|
||||||
join(TMP_DIR, ".claude", "commands", "rune-discuss.md"),
|
|
||||||
"旧内容",
|
|
||||||
);
|
|
||||||
|
|
||||||
await updateClaudeCode(TMP_DIR);
|
await updateClaudeCode(TMP_DIR);
|
||||||
const content = await readFile(
|
const content = await readFile(
|
||||||
@@ -120,8 +115,6 @@ describe("updateClaudeCode", () => {
|
|||||||
|
|
||||||
it("更新 status 命令", async () => {
|
it("更新 status 命令", async () => {
|
||||||
await updateClaudeCode(TMP_DIR);
|
await updateClaudeCode(TMP_DIR);
|
||||||
expect(
|
expect(existsSync(join(TMP_DIR, ".claude", "commands", "rune-status.md"))).toBe(true);
|
||||||
existsSync(join(TMP_DIR, ".claude", "commands", "rune-status.md")),
|
|
||||||
).toBe(true);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ describe("injectOpenCode", () => {
|
|||||||
for (const stage of ["discuss", "plan", "build", "archive"]) {
|
for (const stage of ["discuss", "plan", "build", "archive"]) {
|
||||||
expect(commands).toContain(`rune-${stage}.md`);
|
expect(commands).toContain(`rune-${stage}.md`);
|
||||||
expect(skills).toContain(`rune-${stage}`);
|
expect(skills).toContain(`rune-${stage}`);
|
||||||
expect(
|
expect(existsSync(join(TMP_DIR, ".opencode", "skills", `rune-${stage}`, "SKILL.md"))).toBe(
|
||||||
existsSync(join(TMP_DIR, ".opencode", "skills", `rune-${stage}`, "SKILL.md")),
|
true,
|
||||||
).toBe(true);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -38,9 +38,7 @@ describe("injectOpenCode", () => {
|
|||||||
|
|
||||||
expect(commands).toContain("rune-status.md");
|
expect(commands).toContain("rune-status.md");
|
||||||
expect(skills).toContain("rune-status");
|
expect(skills).toContain("rune-status");
|
||||||
expect(
|
expect(existsSync(join(TMP_DIR, ".opencode", "skills", "rune-status", "SKILL.md"))).toBe(true);
|
||||||
existsSync(join(TMP_DIR, ".opencode", "skills", "rune-status", "SKILL.md")),
|
|
||||||
).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("command 文件包含 skill 调用指令", async () => {
|
it("command 文件包含 skill 调用指令", async () => {
|
||||||
@@ -112,12 +110,8 @@ describe("injectOpenCode", () => {
|
|||||||
describe("updateOpenCode", () => {
|
describe("updateOpenCode", () => {
|
||||||
it("文件不存在时创建", async () => {
|
it("文件不存在时创建", async () => {
|
||||||
await updateOpenCode(TMP_DIR);
|
await updateOpenCode(TMP_DIR);
|
||||||
expect(
|
expect(existsSync(join(TMP_DIR, ".opencode", "commands", "rune-discuss.md"))).toBe(true);
|
||||||
existsSync(join(TMP_DIR, ".opencode", "commands", "rune-discuss.md")),
|
expect(existsSync(join(TMP_DIR, ".opencode", "skills", "rune-discuss", "SKILL.md"))).toBe(true);
|
||||||
).toBe(true);
|
|
||||||
expect(
|
|
||||||
existsSync(join(TMP_DIR, ".opencode", "skills", "rune-discuss", "SKILL.md")),
|
|
||||||
).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("文件存在且内容一致时不覆盖", async () => {
|
it("文件存在且内容一致时不覆盖", async () => {
|
||||||
@@ -137,10 +131,7 @@ describe("updateOpenCode", () => {
|
|||||||
|
|
||||||
it("文件存在但内容不一致时覆盖", async () => {
|
it("文件存在但内容不一致时覆盖", async () => {
|
||||||
await injectOpenCode(TMP_DIR);
|
await injectOpenCode(TMP_DIR);
|
||||||
await writeFile(
|
await writeFile(join(TMP_DIR, ".opencode", "commands", "rune-discuss.md"), "旧内容");
|
||||||
join(TMP_DIR, ".opencode", "commands", "rune-discuss.md"),
|
|
||||||
"旧内容",
|
|
||||||
);
|
|
||||||
|
|
||||||
await updateOpenCode(TMP_DIR);
|
await updateOpenCode(TMP_DIR);
|
||||||
const content = await readFile(
|
const content = await readFile(
|
||||||
@@ -153,11 +144,7 @@ describe("updateOpenCode", () => {
|
|||||||
|
|
||||||
it("更新 status 命令和 skill", async () => {
|
it("更新 status 命令和 skill", async () => {
|
||||||
await updateOpenCode(TMP_DIR);
|
await updateOpenCode(TMP_DIR);
|
||||||
expect(
|
expect(existsSync(join(TMP_DIR, ".opencode", "commands", "rune-status.md"))).toBe(true);
|
||||||
existsSync(join(TMP_DIR, ".opencode", "commands", "rune-status.md")),
|
expect(existsSync(join(TMP_DIR, ".opencode", "skills", "rune-status", "SKILL.md"))).toBe(true);
|
||||||
).toBe(true);
|
|
||||||
expect(
|
|
||||||
existsSync(join(TMP_DIR, ".opencode", "skills", "rune-status", "SKILL.md")),
|
|
||||||
).toBe(true);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
import { describe, it, expect } from "bun:test";
|
import { describe, it, expect } from "bun:test";
|
||||||
import { formatError } from "../../src/cli/output.ts";
|
import { formatError } from "../../src/cli/output.ts";
|
||||||
import {
|
import { UsageError, ConfigError, CommandError, InternalError } from "../../src/cli/errors.ts";
|
||||||
UsageError,
|
|
||||||
ConfigError,
|
|
||||||
CommandError,
|
|
||||||
InternalError,
|
|
||||||
} from "../../src/cli/errors.ts";
|
|
||||||
|
|
||||||
describe("formatError", () => {
|
describe("formatError", () => {
|
||||||
it("只输出错误行(无 hint/usage)", () => {
|
it("只输出错误行(无 hint/usage)", () => {
|
||||||
@@ -25,15 +20,13 @@ describe("formatError", () => {
|
|||||||
usage: "rune plan <change-name>",
|
usage: "rune plan <change-name>",
|
||||||
});
|
});
|
||||||
const output = formatError(err);
|
const output = formatError(err);
|
||||||
expect(output).toBe(
|
expect(output).toBe("错误: 缺少参数\n\n用法: rune plan <change-name>");
|
||||||
"错误: 缺少参数\n\n用法: rune plan <change-name>",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("输出完整格式(错误 + 用法 + 提示)", () => {
|
it("输出完整格式(错误 + 用法 + 提示)", () => {
|
||||||
const err = new UsageError("缺少必填参数 <change-name>", {
|
const err = new UsageError("缺少必填参数 <change-name>", {
|
||||||
usage: "rune plan <change-name>",
|
usage: "rune plan <change-name>",
|
||||||
hint: "请指定变更名称,如 \"add-login\"",
|
hint: '请指定变更名称,如 "add-login"',
|
||||||
});
|
});
|
||||||
const output = formatError(err);
|
const output = formatError(err);
|
||||||
expect(output).toBe(
|
expect(output).toBe(
|
||||||
|
|||||||
@@ -33,9 +33,7 @@ describe("formatChangeStatus", () => {
|
|||||||
|
|
||||||
it("显示文档依赖信息(dependMet 为 false 且 config 中有依赖)", () => {
|
it("显示文档依赖信息(dependMet 为 false 且 config 中有依赖)", () => {
|
||||||
const status = makeStatus({
|
const status = makeStatus({
|
||||||
documents: [
|
documents: [{ name: "task", completed: false, dependMet: false }],
|
||||||
{ name: "task", completed: false, dependMet: false },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
const config: RuneConfig = {
|
const config: RuneConfig = {
|
||||||
stages: {
|
stages: {
|
||||||
@@ -53,9 +51,7 @@ describe("formatChangeStatus", () => {
|
|||||||
|
|
||||||
it("dependMet 为 false 但无 config 时不显示文档依赖信息", () => {
|
it("dependMet 为 false 但无 config 时不显示文档依赖信息", () => {
|
||||||
const status = makeStatus({
|
const status = makeStatus({
|
||||||
documents: [
|
documents: [{ name: "task", completed: false, dependMet: false }],
|
||||||
{ name: "task", completed: false, dependMet: false },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
const output = formatChangeStatus(status);
|
const output = formatChangeStatus(status);
|
||||||
expect(output).not.toContain("(依赖");
|
expect(output).not.toContain("(依赖");
|
||||||
@@ -102,18 +98,14 @@ describe("formatChangeStatus", () => {
|
|||||||
describe("suggestNextStep", () => {
|
describe("suggestNextStep", () => {
|
||||||
it("规划未完成时返回下一个可规划文档", () => {
|
it("规划未完成时返回下一个可规划文档", () => {
|
||||||
const status = makeStatus({
|
const status = makeStatus({
|
||||||
documents: [
|
documents: [{ name: "design", completed: false, dependMet: true }],
|
||||||
{ name: "design", completed: false, dependMet: true },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
expect(suggestNextStep(status)).toContain("rune plan test-change design");
|
expect(suggestNextStep(status)).toContain("rune plan test-change design");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("规划未完成且依赖未满足时提示完成前置依赖", () => {
|
it("规划未完成且依赖未满足时提示完成前置依赖", () => {
|
||||||
const status = makeStatus({
|
const status = makeStatus({
|
||||||
documents: [
|
documents: [{ name: "design", completed: false, dependMet: false }],
|
||||||
{ name: "design", completed: false, dependMet: false },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
expect(suggestNextStep(status)).toBe("完成前置依赖后再规划文档");
|
expect(suggestNextStep(status)).toBe("完成前置依赖后再规划文档");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,10 +22,7 @@ describe("runInit", () => {
|
|||||||
expect(existsSync(join(TMP_DIR, ".rune"))).toBe(true);
|
expect(existsSync(join(TMP_DIR, ".rune"))).toBe(true);
|
||||||
expect(existsSync(join(TMP_DIR, ".rune", "config.yaml"))).toBe(true);
|
expect(existsSync(join(TMP_DIR, ".rune", "config.yaml"))).toBe(true);
|
||||||
|
|
||||||
const content = await readFile(
|
const content = await readFile(join(TMP_DIR, ".rune", "config.yaml"), "utf-8");
|
||||||
join(TMP_DIR, ".rune", "config.yaml"),
|
|
||||||
"utf-8",
|
|
||||||
);
|
|
||||||
expect(content).toContain("# Rune 配置文件");
|
expect(content).toContain("# Rune 配置文件");
|
||||||
expect(content).toContain("stages:");
|
expect(content).toContain("stages:");
|
||||||
});
|
});
|
||||||
@@ -46,16 +43,10 @@ describe("runInit", () => {
|
|||||||
|
|
||||||
it("重复 init 不覆盖 config.yaml", async () => {
|
it("重复 init 不覆盖 config.yaml", async () => {
|
||||||
await runInit(TMP_DIR, ["opencode"]);
|
await runInit(TMP_DIR, ["opencode"]);
|
||||||
await writeFile(
|
await writeFile(join(TMP_DIR, ".rune", "config.yaml"), "自定义内容");
|
||||||
join(TMP_DIR, ".rune", "config.yaml"),
|
|
||||||
"自定义内容",
|
|
||||||
);
|
|
||||||
|
|
||||||
await runInit(TMP_DIR, ["opencode"]);
|
await runInit(TMP_DIR, ["opencode"]);
|
||||||
const content = await readFile(
|
const content = await readFile(join(TMP_DIR, ".rune", "config.yaml"), "utf-8");
|
||||||
join(TMP_DIR, ".rune", "config.yaml"),
|
|
||||||
"utf-8",
|
|
||||||
);
|
|
||||||
expect(content).toBe("自定义内容");
|
expect(content).toBe("自定义内容");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -38,12 +38,7 @@ describe("assembleDiscussPrompt", () => {
|
|||||||
|
|
||||||
describe("assemblePlanPrompt", () => {
|
describe("assemblePlanPrompt", () => {
|
||||||
it("包含指定文档名称和提示词", async () => {
|
it("包含指定文档名称和提示词", async () => {
|
||||||
const prompt = await assemblePlanPrompt(
|
const prompt = await assemblePlanPrompt(defaultConfig, TMP_DIR, "user-auth", "design");
|
||||||
defaultConfig,
|
|
||||||
TMP_DIR,
|
|
||||||
"user-auth",
|
|
||||||
"design",
|
|
||||||
);
|
|
||||||
expect(prompt).toContain("user-auth");
|
expect(prompt).toContain("user-auth");
|
||||||
expect(prompt).toContain("design");
|
expect(prompt).toContain("design");
|
||||||
expect(prompt).not.toContain("task");
|
expect(prompt).not.toContain("task");
|
||||||
@@ -53,23 +48,13 @@ describe("assemblePlanPrompt", () => {
|
|||||||
const changeDir = join(TMP_DIR, ".rune", "changes", "user-auth");
|
const changeDir = join(TMP_DIR, ".rune", "changes", "user-auth");
|
||||||
await mkdir(changeDir, { recursive: true });
|
await mkdir(changeDir, { recursive: true });
|
||||||
await writeFile(join(changeDir, "design.md"), "# 已有设计");
|
await writeFile(join(changeDir, "design.md"), "# 已有设计");
|
||||||
const prompt = await assemblePlanPrompt(
|
const prompt = await assemblePlanPrompt(defaultConfig, TMP_DIR, "user-auth", "design");
|
||||||
defaultConfig,
|
|
||||||
TMP_DIR,
|
|
||||||
"user-auth",
|
|
||||||
"design",
|
|
||||||
);
|
|
||||||
expect(prompt).toContain("已有设计");
|
expect(prompt).toContain("已有设计");
|
||||||
expect(prompt).toContain("在此基础上修订");
|
expect(prompt).toContain("在此基础上修订");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("替换模板中的 {{change-name}}", async () => {
|
it("替换模板中的 {{change-name}}", async () => {
|
||||||
const prompt = await assemblePlanPrompt(
|
const prompt = await assemblePlanPrompt(defaultConfig, TMP_DIR, "user-auth", "design");
|
||||||
defaultConfig,
|
|
||||||
TMP_DIR,
|
|
||||||
"user-auth",
|
|
||||||
"design",
|
|
||||||
);
|
|
||||||
expect(prompt).toContain("user-auth 设计文档");
|
expect(prompt).toContain("user-auth 设计文档");
|
||||||
expect(prompt).not.toContain("{{change-name}}");
|
expect(prompt).not.toContain("{{change-name}}");
|
||||||
});
|
});
|
||||||
@@ -85,23 +70,13 @@ describe("assemblePlanPrompt", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const prompt = await assemblePlanPrompt(
|
const prompt = await assemblePlanPrompt(config, TMP_DIR, "user-auth", "task");
|
||||||
config,
|
|
||||||
TMP_DIR,
|
|
||||||
"user-auth",
|
|
||||||
"task",
|
|
||||||
);
|
|
||||||
expect(prompt).toContain("依赖说明");
|
expect(prompt).toContain("依赖说明");
|
||||||
expect(prompt).toContain("design.md");
|
expect(prompt).toContain("design.md");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("无依赖时不包含依赖说明", async () => {
|
it("无依赖时不包含依赖说明", async () => {
|
||||||
const prompt = await assemblePlanPrompt(
|
const prompt = await assemblePlanPrompt(defaultConfig, TMP_DIR, "user-auth", "design");
|
||||||
defaultConfig,
|
|
||||||
TMP_DIR,
|
|
||||||
"user-auth",
|
|
||||||
"design",
|
|
||||||
);
|
|
||||||
expect(prompt).not.toContain("依赖说明");
|
expect(prompt).not.toContain("依赖说明");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -120,12 +95,7 @@ describe("assemblePlanPrompt", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const prompt = await assemblePlanPrompt(
|
const prompt = await assemblePlanPrompt(config, TMP_DIR, "user-auth", "task");
|
||||||
config,
|
|
||||||
TMP_DIR,
|
|
||||||
"user-auth",
|
|
||||||
"task",
|
|
||||||
);
|
|
||||||
expect(prompt).toContain("已完成");
|
expect(prompt).toContain("已完成");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -154,15 +124,8 @@ describe("assembleBuildPrompt", () => {
|
|||||||
it("包含待执行任务列表", async () => {
|
it("包含待执行任务列表", async () => {
|
||||||
const changeDir = join(TMP_DIR, ".rune", "changes", "user-auth");
|
const changeDir = join(TMP_DIR, ".rune", "changes", "user-auth");
|
||||||
await mkdir(changeDir, { recursive: true });
|
await mkdir(changeDir, { recursive: true });
|
||||||
await writeFile(
|
await writeFile(join(changeDir, "task.md"), `- [x] 任务一\n- [ ] 任务二\n- [ ] 任务三`);
|
||||||
join(changeDir, "task.md"),
|
const prompt = await assembleBuildPrompt(defaultConfig, TMP_DIR, "user-auth");
|
||||||
`- [x] 任务一\n- [ ] 任务二\n- [ ] 任务三`,
|
|
||||||
);
|
|
||||||
const prompt = await assembleBuildPrompt(
|
|
||||||
defaultConfig,
|
|
||||||
TMP_DIR,
|
|
||||||
"user-auth",
|
|
||||||
);
|
|
||||||
expect(prompt).toContain("任务二");
|
expect(prompt).toContain("任务二");
|
||||||
expect(prompt).toContain("待执行任务");
|
expect(prompt).toContain("待执行任务");
|
||||||
expect(prompt).toContain("共 2 项");
|
expect(prompt).toContain("共 2 项");
|
||||||
@@ -171,15 +134,8 @@ describe("assembleBuildPrompt", () => {
|
|||||||
it("所有任务完成时提示可归档", async () => {
|
it("所有任务完成时提示可归档", async () => {
|
||||||
const changeDir = join(TMP_DIR, ".rune", "changes", "user-auth");
|
const changeDir = join(TMP_DIR, ".rune", "changes", "user-auth");
|
||||||
await mkdir(changeDir, { recursive: true });
|
await mkdir(changeDir, { recursive: true });
|
||||||
await writeFile(
|
await writeFile(join(changeDir, "task.md"), `- [x] 任务一\n- [x] 任务二`);
|
||||||
join(changeDir, "task.md"),
|
const prompt = await assembleBuildPrompt(defaultConfig, TMP_DIR, "user-auth");
|
||||||
`- [x] 任务一\n- [x] 任务二`,
|
|
||||||
);
|
|
||||||
const prompt = await assembleBuildPrompt(
|
|
||||||
defaultConfig,
|
|
||||||
TMP_DIR,
|
|
||||||
"user-auth",
|
|
||||||
);
|
|
||||||
expect(prompt).toContain("已完成");
|
expect(prompt).toContain("已完成");
|
||||||
expect(prompt).toContain("归档");
|
expect(prompt).toContain("归档");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -91,10 +91,7 @@ describe("loadConfig", () => {
|
|||||||
it("YAML 解析错误时返回默认配置", async () => {
|
it("YAML 解析错误时返回默认配置", async () => {
|
||||||
const runeDir = join(TMP_DIR, ".rune");
|
const runeDir = join(TMP_DIR, ".rune");
|
||||||
await mkdir(runeDir, { recursive: true });
|
await mkdir(runeDir, { recursive: true });
|
||||||
await writeFile(
|
await writeFile(join(runeDir, "config.yaml"), `stages: [invalid yaml {{{`);
|
||||||
join(runeDir, "config.yaml"),
|
|
||||||
`stages: [invalid yaml {{{`,
|
|
||||||
);
|
|
||||||
const config = await loadConfig(TMP_DIR);
|
const config = await loadConfig(TMP_DIR);
|
||||||
expect(config.stages.discuss).toBeDefined();
|
expect(config.stages.discuss).toBeDefined();
|
||||||
});
|
});
|
||||||
@@ -125,9 +122,7 @@ describe("validateConfig", () => {
|
|||||||
const config: RuneConfig = {
|
const config: RuneConfig = {
|
||||||
stages: {
|
stages: {
|
||||||
plan: {
|
plan: {
|
||||||
documents: [
|
documents: [{ name: "task", prompt: "生成任务", depend: ["nonexistent"] }],
|
||||||
{ name: "task", prompt: "生成任务", depend: ["nonexistent"] },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -138,9 +133,7 @@ describe("validateConfig", () => {
|
|||||||
const config: RuneConfig = {
|
const config: RuneConfig = {
|
||||||
stages: {
|
stages: {
|
||||||
plan: {
|
plan: {
|
||||||
documents: [
|
documents: [{ name: "design", prompt: "生成设计", depend: ["design"] }],
|
||||||
{ name: "design", prompt: "生成设计", depend: ["design"] },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -170,9 +163,7 @@ describe("validateConfig", () => {
|
|||||||
const config: RuneConfig = {
|
const config: RuneConfig = {
|
||||||
stages: {
|
stages: {
|
||||||
plan: {
|
plan: {
|
||||||
documents: [
|
documents: [{ name: "design", prompt: "生成设计", depend: [] }],
|
||||||
{ name: "design", prompt: "生成设计", depend: [] },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,10 +25,7 @@ describe("scanChanges", () => {
|
|||||||
const changesDir = join(TMP_DIR, ".rune", "changes");
|
const changesDir = join(TMP_DIR, ".rune", "changes");
|
||||||
await mkdir(join(changesDir, "user-auth"), { recursive: true });
|
await mkdir(join(changesDir, "user-auth"), { recursive: true });
|
||||||
await writeFile(join(changesDir, "user-auth", "design.md"), "# 设计");
|
await writeFile(join(changesDir, "user-auth", "design.md"), "# 设计");
|
||||||
await writeFile(
|
await writeFile(join(changesDir, "user-auth", "task.md"), `- [x] 任务一\n- [ ] 任务二`);
|
||||||
join(changesDir, "user-auth", "task.md"),
|
|
||||||
`- [x] 任务一\n- [ ] 任务二`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const changes = await scanChanges(TMP_DIR);
|
const changes = await scanChanges(TMP_DIR);
|
||||||
expect(changes).toHaveLength(1);
|
expect(changes).toHaveLength(1);
|
||||||
|
|||||||
@@ -25,32 +25,24 @@ describe("defaultConfig", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("plan 的 task 文档配置存在", () => {
|
it("plan 的 task 文档配置存在", () => {
|
||||||
const taskDoc = defaultConfig.stages.plan!.documents.find(
|
const taskDoc = defaultConfig.stages.plan!.documents.find((d) => d.name === "task");
|
||||||
(d) => d.name === "task",
|
|
||||||
);
|
|
||||||
expect(taskDoc).toBeDefined();
|
expect(taskDoc).toBeDefined();
|
||||||
expect(taskDoc!.prompt).toBeTruthy();
|
expect(taskDoc!.prompt).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("task 文档依赖 design", () => {
|
it("task 文档依赖 design", () => {
|
||||||
const taskDoc = defaultConfig.stages.plan!.documents.find(
|
const taskDoc = defaultConfig.stages.plan!.documents.find((d) => d.name === "task");
|
||||||
(d) => d.name === "task",
|
|
||||||
);
|
|
||||||
expect(taskDoc!.depend).toEqual(["design"]);
|
expect(taskDoc!.depend).toEqual(["design"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("design 文档有 template", () => {
|
it("design 文档有 template", () => {
|
||||||
const designDoc = defaultConfig.stages.plan!.documents.find(
|
const designDoc = defaultConfig.stages.plan!.documents.find((d) => d.name === "design");
|
||||||
(d) => d.name === "design",
|
|
||||||
);
|
|
||||||
expect(designDoc!.template).toBeTruthy();
|
expect(designDoc!.template).toBeTruthy();
|
||||||
expect(designDoc!.template).toContain("{{change-name}}");
|
expect(designDoc!.template).toContain("{{change-name}}");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("task 文档有 template", () => {
|
it("task 文档有 template", () => {
|
||||||
const taskDoc = defaultConfig.stages.plan!.documents.find(
|
const taskDoc = defaultConfig.stages.plan!.documents.find((d) => d.name === "task");
|
||||||
(d) => d.name === "task",
|
|
||||||
);
|
|
||||||
expect(taskDoc!.template).toBeTruthy();
|
expect(taskDoc!.template).toBeTruthy();
|
||||||
expect(taskDoc!.template).toContain("- [ ]");
|
expect(taskDoc!.template).toContain("- [ ]");
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user