Skip to content

Commit e8e57d5

Browse files
authored
feat: project wiki (#476)
1 parent eb3fe1c commit e8e57d5

15 files changed

+8682
-0
lines changed
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { getCommands, getCommand } from "../services/command/commands"
2+
import { ensureProjectWikiCommandExists } from "../core/tools/helpers/projectWikiHelpers"
3+
import { projectWikiCommandName } from "../core/tools/helpers/projectWikiHelpers"
4+
5+
describe("Project Wiki Command Integration", () => {
6+
const testCwd = process.cwd()
7+
8+
describe("动态命令初始化", () => {
9+
it("应该能够初始化 project-wiki 命令而不抛出错误", async () => {
10+
// 测试 ensureProjectWikiCommandExists 函数
11+
await expect(ensureProjectWikiCommandExists()).resolves.not.toThrow()
12+
})
13+
14+
it("getCommands() 应该包含 project-wiki 命令", async () => {
15+
// 确保命令已初始化
16+
await ensureProjectWikiCommandExists()
17+
18+
// 获取所有命令
19+
const commands = await getCommands(testCwd)
20+
21+
// 验证命令列表是数组
22+
expect(Array.isArray(commands)).toBe(true)
23+
24+
// 查找 project-wiki 命令
25+
const projectWikiCommand = commands.find((cmd) => cmd.name === projectWikiCommandName)
26+
27+
// 验证 project-wiki 命令存在
28+
expect(projectWikiCommand).toBeDefined()
29+
30+
if (projectWikiCommand) {
31+
expect(projectWikiCommand.name).toBe(projectWikiCommandName)
32+
expect(projectWikiCommand.source).toBe("global")
33+
expect(typeof projectWikiCommand.content).toBe("string")
34+
expect(projectWikiCommand.content.length).toBeGreaterThan(0)
35+
expect(projectWikiCommand.filePath).toContain("project-wiki.md")
36+
}
37+
})
38+
39+
it("getCommand() 应该能够获取 project-wiki 命令", async () => {
40+
// 确保命令已初始化
41+
await ensureProjectWikiCommandExists()
42+
43+
// 获取特定命令
44+
const command = await getCommand(testCwd, projectWikiCommandName)
45+
46+
// 验证命令存在且正确
47+
expect(command).toBeDefined()
48+
expect(command?.name).toBe(projectWikiCommandName)
49+
expect(command?.source).toBe("global")
50+
expect(typeof command?.content).toBe("string")
51+
expect(command?.content.length).toBeGreaterThan(0)
52+
})
53+
})
54+
55+
describe("错误处理机制", () => {
56+
it("即使 ensureProjectWikiCommandExists 失败,getCommands 也应该正常工作", async () => {
57+
// 这个测试验证错误隔离机制
58+
// 即使动态命令初始化失败,其他命令仍应正常工作
59+
const commands = await getCommands(testCwd)
60+
61+
// 应该返回数组(可能为空,但不应该抛出错误)
62+
expect(Array.isArray(commands)).toBe(true)
63+
})
64+
65+
it("应该能够处理重复的命令初始化调用", async () => {
66+
// 多次调用应该不会出错
67+
await expect(ensureProjectWikiCommandExists()).resolves.not.toThrow()
68+
await expect(ensureProjectWikiCommandExists()).resolves.not.toThrow()
69+
await expect(ensureProjectWikiCommandExists()).resolves.not.toThrow()
70+
})
71+
})
72+
73+
describe("命令内容验证", () => {
74+
it("project-wiki 命令应该包含预期的内容结构", async () => {
75+
await ensureProjectWikiCommandExists()
76+
const command = await getCommand(testCwd, projectWikiCommandName)
77+
78+
expect(command).toBeDefined()
79+
if (command) {
80+
// 验证命令内容包含预期的关键词(中文内容)
81+
const content = command.content
82+
expect(content).toContain("项目")
83+
expect(content).toContain("wiki")
84+
expect(content).toContain("分析")
85+
}
86+
})
87+
})
88+
})
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { describe, it, expect, vi, beforeEach } from "vitest"
2+
import { ensureProjectWikiCommandExists } from "../helpers/projectWikiHelpers"
3+
import { promises as fs } from "fs"
4+
import * as path from "path"
5+
import * as os from "os"
6+
7+
// Mock fs module
8+
vi.mock("fs")
9+
const mockedFs = vi.mocked(fs)
10+
11+
describe("projectWikiHelpers", () => {
12+
const globalCommandsDir = path.join(os.homedir(), ".roo", "commands")
13+
const projectWikiFile = path.join(globalCommandsDir, "project-wiki.md")
14+
const subTaskDir = path.join(globalCommandsDir, "subtasks")
15+
16+
beforeEach(() => {
17+
vi.clearAllMocks()
18+
})
19+
20+
it("should successfully create wiki command files", async () => {
21+
// Mock file system operations
22+
mockedFs.mkdir.mockResolvedValue(undefined)
23+
mockedFs.access.mockRejectedValue(new Error("File not found"))
24+
mockedFs.rm.mockResolvedValue(undefined)
25+
mockedFs.writeFile.mockResolvedValue(undefined)
26+
mockedFs.readdir.mockResolvedValue([
27+
"01_Project_Overview_Analysis.md",
28+
"02_Overall_Architecture_Analysis.md",
29+
] as any)
30+
31+
// Execute function
32+
await expect(ensureProjectWikiCommandExists()).resolves.not.toThrow()
33+
34+
// Verify calls
35+
expect(mockedFs.mkdir).toHaveBeenCalledWith(globalCommandsDir, { recursive: true })
36+
expect(mockedFs.writeFile).toHaveBeenCalledTimes(10) // 1 main file + 9 subtask files
37+
})
38+
39+
it("should skip creation when files already exist", async () => {
40+
// Mock existing files
41+
mockedFs.mkdir.mockResolvedValue(undefined)
42+
mockedFs.access.mockResolvedValue(undefined)
43+
mockedFs.stat.mockResolvedValue({
44+
isDirectory: () => true,
45+
} as any)
46+
mockedFs.readdir.mockResolvedValue(["01_Project_Overview_Analysis.md"] as any)
47+
48+
// Execute function
49+
await expect(ensureProjectWikiCommandExists()).resolves.not.toThrow()
50+
51+
// Verify no write operations were called
52+
expect(mockedFs.writeFile).not.toHaveBeenCalled()
53+
})
54+
55+
it("should generate all wiki files correctly", async () => {
56+
// Read the modified projectWikiHelpers.ts file to test the generateWikiFiles function
57+
// generateWikiFiles is an internal function and not exported, so we test ensureProjectWikiCommandExists instead
58+
// This will indirectly test the functionality of generateWikiFiles
59+
expect(true).toBe(true) // Placeholder test
60+
})
61+
})
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import { promises as fs } from "fs"
2+
import * as path from "path"
3+
import * as os from "os"
4+
import { PROJECT_WIKI_TEMPLATE } from "./wiki-prompts/project-wiki"
5+
import { PROJECT_OVERVIEW_ANALYSIS_TEMPLATE } from "./wiki-prompts/subtasks/01_Project_Overview_Analysis"
6+
import { OVERALL_ARCHITECTURE_ANALYSIS_TEMPLATE } from "./wiki-prompts/subtasks/02_Overall_Architecture_Analysis"
7+
import { SERVICE_DEPENDENCIES_ANALYSIS_TEMPLATE } from "./wiki-prompts/subtasks/03_Service_Dependencies_Analysis"
8+
import { DATA_FLOW_INTEGRATION_ANALYSIS_TEMPLATE } from "./wiki-prompts/subtasks/04_Data_Flow_Integration_Analysis"
9+
import { SERVICE_ANALYSIS_TEMPLATE } from "./wiki-prompts/subtasks/05_Service_Analysis_Template"
10+
import { DATABASE_SCHEMA_ANALYSIS_TEMPLATE } from "./wiki-prompts/subtasks/06_Database_Schema_Analysis"
11+
import { API_INTERFACE_ANALYSIS_TEMPLATE } from "./wiki-prompts/subtasks/07_API_Interface_Analysis"
12+
import { DEPLOY_ANALYSIS_TEMPLATE } from "./wiki-prompts/subtasks/08_Deploy_Analysis"
13+
import { PROJECT_RULES_GENERATION_TEMPLATE } from "./wiki-prompts/subtasks/09_Project_Rules_Generation"
14+
import { ILogger, createLogger } from "../../../utils/logger"
15+
16+
// Safely get home directory
17+
function getHomeDir(): string {
18+
const homeDir = os.homedir()
19+
if (!homeDir) {
20+
throw new Error("Unable to determine home directory")
21+
}
22+
return homeDir
23+
}
24+
25+
// Get global commands directory path
26+
function getGlobalCommandsDir(): string {
27+
return path.join(getHomeDir(), ".roo", "commands")
28+
}
29+
30+
export const projectWikiCommandName = "project-wiki"
31+
export const projectWikiCommandDescription = `Analyze project deeply and generate a comprehensive project wiki.`
32+
33+
const logger: ILogger = createLogger("ProjectWikiHelpers")
34+
35+
// Unified error handling function, preserving stack information
36+
function formatError(error: unknown): string {
37+
if (error instanceof Error) {
38+
return error.stack || error.message
39+
}
40+
return String(error)
41+
}
42+
43+
const mainFileName: string = projectWikiCommandName + ".md"
44+
// Template data mapping
45+
const TEMPLATES = {
46+
[mainFileName]: PROJECT_WIKI_TEMPLATE,
47+
"01_Project_Overview_Analysis.md": PROJECT_OVERVIEW_ANALYSIS_TEMPLATE,
48+
"02_Overall_Architecture_Analysis.md": OVERALL_ARCHITECTURE_ANALYSIS_TEMPLATE,
49+
"03_Service_Dependencies_Analysis.md": SERVICE_DEPENDENCIES_ANALYSIS_TEMPLATE,
50+
"04_Data_Flow_Integration_Analysis.md": DATA_FLOW_INTEGRATION_ANALYSIS_TEMPLATE,
51+
"05_Service_Analysis_Template.md": SERVICE_ANALYSIS_TEMPLATE,
52+
"06_Database_Schema_Analysis.md": DATABASE_SCHEMA_ANALYSIS_TEMPLATE,
53+
"07_API_Interface_Analysis.md": API_INTERFACE_ANALYSIS_TEMPLATE,
54+
"08_Deploy_Analysis.md": DEPLOY_ANALYSIS_TEMPLATE,
55+
"09_Project_Rules_Generation.md": PROJECT_RULES_GENERATION_TEMPLATE,
56+
}
57+
58+
export async function ensureProjectWikiCommandExists() {
59+
const startTime = Date.now()
60+
logger.info("[projectWikiHelpers] Starting ensureProjectWikiCommandExists...")
61+
62+
try {
63+
const globalCommandsDir = getGlobalCommandsDir()
64+
await fs.mkdir(globalCommandsDir, { recursive: true })
65+
66+
const projectWikiFile = path.join(globalCommandsDir, `${projectWikiCommandName}.md`)
67+
const subTaskDir = path.join(globalCommandsDir, "subtasks")
68+
69+
// Check if setup is needed
70+
const needsSetup = await checkIfSetupNeeded(projectWikiFile, subTaskDir)
71+
if (!needsSetup) {
72+
logger.info("[projectWikiHelpers] project-wiki command already exists")
73+
return
74+
}
75+
76+
logger.info("[projectWikiHelpers] Setting up project-wiki command...")
77+
78+
// Clean up existing files
79+
await Promise.allSettled([
80+
fs.rm(projectWikiFile, { force: true }),
81+
fs.rm(subTaskDir, { recursive: true, force: true }),
82+
])
83+
84+
// Generate Wiki files
85+
await generateWikiCommandFiles(projectWikiFile, subTaskDir)
86+
87+
const duration = Date.now() - startTime
88+
logger.info(`[projectWikiHelpers] project-wiki command setup completed in ${duration}ms`)
89+
} catch (error) {
90+
const errorMsg = formatError(error)
91+
throw new Error(`Failed to setup project-wiki command: ${errorMsg}`)
92+
}
93+
}
94+
95+
// Optimized file checking logic, using Promise.allSettled to improve performance
96+
async function checkIfSetupNeeded(projectWikiFile: string, subTaskDir: string): Promise<boolean> {
97+
try {
98+
const [mainFileResult, subDirResult] = await Promise.allSettled([
99+
fs.access(projectWikiFile, fs.constants.F_OK | fs.constants.R_OK | fs.constants.W_OK),
100+
fs.stat(subTaskDir),
101+
])
102+
103+
// If main file doesn't exist, setup is needed
104+
if (mainFileResult.status === "rejected") {
105+
logger.info("[projectWikiHelpers] projectWikiFile not accessible:", formatError(mainFileResult.reason))
106+
return true
107+
}
108+
109+
// If subtask directory doesn't exist or is not a directory, setup is needed
110+
if (subDirResult.status === "rejected") {
111+
logger.info("[projectWikiHelpers] subTaskDir not accessible:", formatError(subDirResult.reason))
112+
return true
113+
}
114+
115+
if (!subDirResult.value.isDirectory()) {
116+
logger.info("[projectWikiHelpers] subTaskDir exists but is not a directory")
117+
return true
118+
}
119+
120+
// Check if subtask directory has .md files
121+
const subTaskFiles = await fs.readdir(subTaskDir)
122+
const mdFiles = subTaskFiles.filter((file) => file.endsWith(".md"))
123+
return mdFiles.length === 0
124+
} catch (error) {
125+
logger.error("[projectWikiHelpers] Error checking setup status:", formatError(error))
126+
return true
127+
}
128+
}
129+
130+
// Generate Wiki files
131+
async function generateWikiCommandFiles(projectWikiFile: string, subTaskDir: string): Promise<void> {
132+
try {
133+
// Generate main file
134+
const mainTemplate = TEMPLATES[mainFileName]
135+
if (!mainTemplate) {
136+
throw new Error("Main template not found")
137+
}
138+
139+
await fs.writeFile(projectWikiFile, mainTemplate, "utf-8")
140+
logger.info(`[projectWikiHelpers] Generated main wiki file: ${projectWikiFile}`)
141+
142+
// Create subtask directory
143+
await fs.mkdir(subTaskDir, { recursive: true })
144+
145+
// Generate subtask files
146+
const subTaskFiles = Object.keys(TEMPLATES).filter((file) => file !== mainFileName)
147+
const generateResults = await Promise.allSettled(
148+
subTaskFiles.map(async (file) => {
149+
const template = TEMPLATES[file as keyof typeof TEMPLATES]
150+
if (!template) {
151+
throw new Error(`Template not found for file: ${file}`)
152+
}
153+
154+
const targetFile = path.join(subTaskDir, file)
155+
await fs.writeFile(targetFile, template, "utf-8")
156+
return file
157+
}),
158+
)
159+
160+
// Count generation results
161+
const successful = generateResults.filter((result) => result.status === "fulfilled")
162+
const failed = generateResults.filter((result) => result.status === "rejected")
163+
164+
logger.info(`[projectWikiHelpers] Successfully generated ${successful.length} subtask files`)
165+
166+
if (failed.length > 0) {
167+
logger.warn(`[projectWikiHelpers] Failed to generate ${failed.length} subtask files:`)
168+
failed.forEach((result, index) => {
169+
if (result.status === "rejected") {
170+
logger.warn(` - ${subTaskFiles[generateResults.indexOf(result)]}: ${formatError(result.reason)}`)
171+
}
172+
})
173+
}
174+
} catch (error) {
175+
const errorMsg = formatError(error)
176+
throw new Error(`Failed to generate wiki files: ${errorMsg}`)
177+
}
178+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import * as os from "os"
2+
import * as path from "path"
3+
import { WIKI_OUTPUT_DIR } from "./subtasks/constants"
4+
5+
const subtaskDir = path.join(os.homedir(), ".roo", "commands", "subtasks") + path.sep
6+
export const PROJECT_WIKI_TEMPLATE = `---
7+
description: "深度分析项目,生成技术文档"
8+
---
9+
您是一位专业的技术作家和软件架构师。
10+
以下每个文件中的内容都是一个任务,按顺序作为指令严格逐个执行:
11+
12+
[项目概览分析](${subtaskDir}01_Project_Overview_Analysis.md)
13+
[整体架构分析](${subtaskDir}02_Overall_Architecture_Analysis.md)
14+
[服务依赖分析](${subtaskDir}03_Service_Dependencies_Analysis.md)
15+
[数据流分析](${subtaskDir}04_Data_Flow_Integration_Analysis.md)
16+
[服务模块分析](${subtaskDir}05_Service_Analysis_Template.md)
17+
[数据库分析](${subtaskDir}06_Database_Schema_Analysis.md)
18+
[API分析](${subtaskDir}07_API_Interface_Analysis.md)
19+
[部署分析](${subtaskDir}08_Deploy_Analysis.md)
20+
[Rues生成](${subtaskDir}09_Project_Rules_Generation.md)
21+
注意:
22+
1、如果未发现上述文件,直接退出报错即可,禁止自作主张!
23+
2、一切以实际项目为准,禁止无中生有!
24+
3、最终产物是${WIKI_OUTPUT_DIR}目录下的若干个技术文档,生成完成后,为它们在${WIKI_OUTPUT_DIR}目录下创建一个index.md 文件,内容是前面技术文档的简洁目录,index.md控制20行左右;`

0 commit comments

Comments
 (0)