feat: implement /init command for project documentation generation

- Add /init command handler in Task.ts that intercepts the command
- Create ProjectScanner module to analyze project structure and technologies
- Create DocumentationGenerator to generate comprehensive ROO.md file
- Add unit tests for both ProjectScanner and DocumentationGenerator
- Integrate with existing RooIgnore patterns for file filtering

Closes #6153
This commit is contained in:
Roo Code 2025-07-24 07:21:27 +00:00
parent 1e17b3b3bb
commit 44a6cd7da5
5 changed files with 1050 additions and 0 deletions

View file

@ -0,0 +1,342 @@
import { ProjectInfo, Technology, ConfigFile, DirectoryInfo } from "./ProjectScanner"
export class DocumentationGenerator {
async generateDocumentation(projectInfo: ProjectInfo): Promise<string> {
const sections: string[] = []
// Header
sections.push(`# ${projectInfo.name}`)
sections.push("")
sections.push(projectInfo.description)
sections.push("")
// Project Overview
sections.push("## Project Overview")
sections.push("")
sections.push(
`This document provides a comprehensive overview of the **${projectInfo.name}** project structure, technologies, and setup instructions. This file is automatically generated by Roo Code's \`/init\` command to help maintain consistent project documentation.`,
)
sections.push("")
// Table of Contents
sections.push("## Table of Contents")
sections.push("")
sections.push("- [Technologies](#technologies)")
sections.push("- [Project Structure](#project-structure)")
sections.push("- [Configuration Files](#configuration-files)")
sections.push("- [Dependencies](#dependencies)")
sections.push("- [Available Scripts](#available-scripts)")
sections.push("- [Development Setup](#development-setup)")
sections.push("- [Architecture Patterns](#architecture-patterns)")
if (projectInfo.gitInfo.hasGit) {
sections.push("- [Git Information](#git-information)")
}
sections.push("")
// Technologies
sections.push("## Technologies")
sections.push("")
sections.push("This project uses the following technologies:")
sections.push("")
const techByType = this.groupTechnologiesByType(projectInfo.technologies)
for (const [type, techs] of Object.entries(techByType)) {
sections.push(`### ${this.formatTechType(type)}`)
sections.push("")
for (const tech of techs) {
const version = tech.version ? ` (${tech.version})` : ""
const config = tech.configFile ? ` - Config: \`${tech.configFile}\`` : ""
sections.push(`- **${tech.name}**${version}${config}`)
}
sections.push("")
}
// Project Structure
sections.push("## Project Structure")
sections.push("")
sections.push("```")
sections.push(this.generateTreeStructure(projectInfo.structure.directories))
sections.push("```")
sections.push("")
// File statistics
sections.push("### File Statistics")
sections.push("")
sections.push(`- Total files: ${projectInfo.structure.fileCount}`)
sections.push(`- File types:`)
const sortedFileTypes = Object.entries(projectInfo.structure.fileTypes)
.sort(([, a], [, b]) => b - a)
.slice(0, 10)
for (const [ext, count] of sortedFileTypes) {
const extName = ext || "(no extension)"
sections.push(` - ${extName}: ${count} files`)
}
sections.push("")
// Configuration Files
sections.push("## Configuration Files")
sections.push("")
sections.push("The following configuration files are present in the project:")
sections.push("")
const configByType = this.groupConfigsByType(projectInfo.configFiles)
for (const [type, configs] of Object.entries(configByType)) {
sections.push(`### ${this.formatConfigType(type)}`)
sections.push("")
for (const config of configs) {
sections.push(`- \`${config.path}\``)
}
sections.push("")
}
// Dependencies
if (
Object.keys(projectInfo.dependencies.production).length > 0 ||
Object.keys(projectInfo.dependencies.development).length > 0
) {
sections.push("## Dependencies")
sections.push("")
if (Object.keys(projectInfo.dependencies.production).length > 0) {
sections.push("### Production Dependencies")
sections.push("")
sections.push("```json")
sections.push(JSON.stringify(projectInfo.dependencies.production, null, 2))
sections.push("```")
sections.push("")
}
if (Object.keys(projectInfo.dependencies.development).length > 0) {
sections.push("### Development Dependencies")
sections.push("")
sections.push("```json")
sections.push(JSON.stringify(projectInfo.dependencies.development, null, 2))
sections.push("```")
sections.push("")
}
}
// Available Scripts
if (Object.keys(projectInfo.scripts).length > 0) {
sections.push("## Available Scripts")
sections.push("")
sections.push("The following scripts are available:")
sections.push("")
for (const [name, command] of Object.entries(projectInfo.scripts)) {
if (command === "Makefile target") {
sections.push(`- \`${name}\` - Makefile target`)
} else {
sections.push(`- \`npm run ${name}\` - ${command}`)
}
}
sections.push("")
}
// Development Setup
sections.push("## Development Setup")
sections.push("")
sections.push("To set up this project for development:")
sections.push("")
const setupSteps = this.generateSetupSteps(projectInfo)
setupSteps.forEach((step, index) => {
sections.push(`${index + 1}. ${step}`)
})
sections.push("")
// Architecture Patterns
if (
projectInfo.patterns.architecture ||
projectInfo.patterns.testingFramework ||
projectInfo.patterns.buildTool ||
projectInfo.patterns.cicd
) {
sections.push("## Architecture Patterns")
sections.push("")
if (projectInfo.patterns.architecture) {
sections.push(`- **Architecture**: ${projectInfo.patterns.architecture}`)
}
if (projectInfo.patterns.testingFramework) {
sections.push(`- **Testing Framework**: ${projectInfo.patterns.testingFramework}`)
}
if (projectInfo.patterns.buildTool) {
sections.push(`- **Build Tool**: ${projectInfo.patterns.buildTool}`)
}
if (projectInfo.patterns.packageManager) {
sections.push(`- **Package Manager**: ${projectInfo.patterns.packageManager}`)
}
if (projectInfo.patterns.cicd && projectInfo.patterns.cicd.length > 0) {
sections.push(`- **CI/CD**: ${projectInfo.patterns.cicd.join(", ")}`)
}
sections.push("")
}
// Git Information
if (projectInfo.gitInfo.hasGit) {
sections.push("## Git Information")
sections.push("")
if (projectInfo.gitInfo.branch) {
sections.push(`- **Current Branch**: ${projectInfo.gitInfo.branch}`)
}
if (projectInfo.gitInfo.remote) {
sections.push(`- **Remote Repository**: ${projectInfo.gitInfo.remote}`)
}
sections.push("")
}
// Footer
sections.push("---")
sections.push("")
sections.push(`*This documentation was automatically generated by Roo Code on ${new Date().toISOString()}*`)
sections.push("")
return sections.join("\n")
}
private groupTechnologiesByType(technologies: Technology[]): Record<string, Technology[]> {
const grouped: Record<string, Technology[]> = {}
for (const tech of technologies) {
if (!grouped[tech.type]) {
grouped[tech.type] = []
}
grouped[tech.type].push(tech)
}
return grouped
}
private groupConfigsByType(configs: ConfigFile[]): Record<string, ConfigFile[]> {
const grouped: Record<string, ConfigFile[]> = {}
for (const config of configs) {
if (!grouped[config.type]) {
grouped[config.type] = []
}
grouped[config.type].push(config)
}
return grouped
}
private formatTechType(type: string): string {
const typeMap: Record<string, string> = {
language: "Languages",
framework: "Frameworks",
tool: "Tools",
database: "Databases",
service: "Services",
}
return typeMap[type] || type
}
private formatConfigType(type: string): string {
const typeMap: Record<string, string> = {
npm: "NPM Configuration",
typescript: "TypeScript Configuration",
eslint: "ESLint Configuration",
prettier: "Prettier Configuration",
webpack: "Webpack Configuration",
vite: "Vite Configuration",
jest: "Jest Configuration",
vitest: "Vitest Configuration",
git: "Git Configuration",
docker: "Docker Configuration",
environment: "Environment Configuration",
python: "Python Configuration",
go: "Go Configuration",
rust: "Rust Configuration",
}
return typeMap[type] || type
}
private generateTreeStructure(directories: DirectoryInfo[], prefix: string = ""): string {
const lines: string[] = []
// Add root indicator
if (prefix === "") {
lines.push(".")
}
// Sort directories by name
const sorted = directories.sort((a, b) => a.name.localeCompare(b.name))
for (let i = 0; i < sorted.length; i++) {
const dir = sorted[i]
const isLast = i === sorted.length - 1
const connector = isLast ? "└── " : "├── "
const extension = isLast ? " " : "│ "
lines.push(`${prefix}${connector}${dir.name}/`)
// Add file count if directory has files
if (dir.fileCount > 0) {
lines.push(`${prefix}${extension} (${dir.fileCount} files)`)
}
}
return lines.join("\n")
}
private generateSetupSteps(projectInfo: ProjectInfo): string[] {
const steps: string[] = []
// Clone repository if git remote exists
if (projectInfo.gitInfo.remote) {
steps.push(`Clone the repository: \`git clone ${projectInfo.gitInfo.remote}\``)
steps.push(`Navigate to the project directory: \`cd ${projectInfo.name}\``)
}
// Install dependencies based on package manager
if (projectInfo.patterns.packageManager) {
const pm = projectInfo.patterns.packageManager
if (pm === "npm") {
steps.push("Install dependencies: `npm install`")
} else if (pm === "yarn") {
steps.push("Install dependencies: `yarn install`")
} else if (pm === "pnpm") {
steps.push("Install dependencies: `pnpm install`")
}
} else if (projectInfo.technologies.some((t) => t.name === "Node.js")) {
steps.push("Install dependencies: `npm install`")
}
// Python setup
if (projectInfo.technologies.some((t) => t.name === "Python")) {
steps.push("Create a virtual environment: `python -m venv venv`")
steps.push("Activate the virtual environment:")
steps.push(" - On Windows: `venv\\Scripts\\activate`")
steps.push(" - On macOS/Linux: `source venv/bin/activate`")
if (projectInfo.configFiles.some((c) => c.name === "requirements.txt")) {
steps.push("Install Python dependencies: `pip install -r requirements.txt`")
}
}
// Go setup
if (projectInfo.technologies.some((t) => t.name === "Go")) {
steps.push("Download Go dependencies: `go mod download`")
}
// Rust setup
if (projectInfo.technologies.some((t) => t.name === "Rust")) {
steps.push("Build the project: `cargo build`")
}
// Environment setup
if (projectInfo.configFiles.some((c) => c.name === ".env.example")) {
steps.push("Copy the example environment file: `cp .env.example .env`")
steps.push("Update the `.env` file with your local configuration")
}
// Add common development scripts
if (projectInfo.scripts["dev"]) {
steps.push("Start the development server: `npm run dev`")
} else if (projectInfo.scripts["start"]) {
steps.push("Start the development server: `npm run start`")
}
if (projectInfo.scripts["test"]) {
steps.push("Run tests: `npm run test`")
}
return steps
}
}

View file

@ -0,0 +1,432 @@
import * as path from "path"
import * as fs from "fs/promises"
import { listFiles } from "../../services/glob/list-files"
import { RooIgnoreController } from "../ignore/RooIgnoreController"
export interface ProjectInfo {
name: string
description: string
rootPath: string
structure: ProjectStructure
technologies: Technology[]
configFiles: ConfigFile[]
dependencies: Dependencies
scripts: Scripts
gitInfo: GitInfo
patterns: ProjectPatterns
}
export interface ProjectStructure {
directories: DirectoryInfo[]
fileCount: number
totalSize: number
fileTypes: Record<string, number>
}
export interface DirectoryInfo {
name: string
path: string
fileCount: number
subdirectories: string[]
}
export interface Technology {
name: string
version?: string
configFile?: string
type: "language" | "framework" | "tool" | "database" | "service"
}
export interface ConfigFile {
name: string
path: string
type: string
content?: any
}
export interface Dependencies {
production: Record<string, string>
development: Record<string, string>
peer?: Record<string, string>
}
export interface Scripts {
[key: string]: string
}
export interface GitInfo {
hasGit: boolean
branch?: string
remote?: string
lastCommit?: string
}
export interface ProjectPatterns {
architecture?: string
testingFramework?: string
buildTool?: string
packageManager?: string
cicd?: string[]
}
export class ProjectScanner {
constructor(
private readonly rootPath: string,
private readonly rooIgnoreController?: RooIgnoreController,
) {}
async scanProject(): Promise<ProjectInfo> {
const [files, _] = await listFiles(this.rootPath, true, 10000)
// Filter files through rooIgnoreController if available
const filteredFiles = this.rooIgnoreController
? files.filter((file) => {
// Check if the rooIgnoreController would filter this path
const filtered = this.rooIgnoreController!.filterPaths([file])
return filtered.includes(file)
})
: files
const projectInfo: ProjectInfo = {
name: await this.detectProjectName(),
description: await this.detectProjectDescription(),
rootPath: this.rootPath,
structure: await this.analyzeStructure(filteredFiles),
technologies: await this.detectTechnologies(filteredFiles),
configFiles: await this.findConfigFiles(filteredFiles),
dependencies: await this.analyzeDependencies(),
scripts: await this.analyzeScripts(),
gitInfo: await this.analyzeGitInfo(),
patterns: await this.detectPatterns(filteredFiles),
}
return projectInfo
}
private async detectProjectName(): Promise<string> {
// Try to get name from package.json
try {
const packageJsonPath = path.join(this.rootPath, "package.json")
const content = await fs.readFile(packageJsonPath, "utf-8")
const packageJson = JSON.parse(content)
if (packageJson.name) return packageJson.name
} catch {}
// Try to get name from pyproject.toml
try {
const pyprojectPath = path.join(this.rootPath, "pyproject.toml")
const content = await fs.readFile(pyprojectPath, "utf-8")
const match = content.match(/name\s*=\s*"([^"]+)"/)
if (match) return match[1]
} catch {}
// Default to directory name
return path.basename(this.rootPath)
}
private async detectProjectDescription(): Promise<string> {
// Try to get description from package.json
try {
const packageJsonPath = path.join(this.rootPath, "package.json")
const content = await fs.readFile(packageJsonPath, "utf-8")
const packageJson = JSON.parse(content)
if (packageJson.description) return packageJson.description
} catch {}
// Try to get description from README
try {
const readmePath = path.join(this.rootPath, "README.md")
const content = await fs.readFile(readmePath, "utf-8")
const lines = content.split("\n")
// Get first non-header, non-empty line
for (const line of lines) {
const trimmed = line.trim()
if (trimmed && !trimmed.startsWith("#") && !trimmed.startsWith("![")) {
return trimmed.substring(0, 200)
}
}
} catch {}
return "No description available"
}
private async analyzeStructure(files: string[]): Promise<ProjectStructure> {
const directories = new Map<string, DirectoryInfo>()
const fileTypes: Record<string, number> = {}
let totalSize = 0
for (const file of files) {
if (file.endsWith("/")) {
// It's a directory
const dirPath = file.slice(0, -1)
const dirName = path.basename(dirPath)
const parentDir = path.dirname(dirPath)
if (!directories.has(dirPath)) {
directories.set(dirPath, {
name: dirName,
path: dirPath,
fileCount: 0,
subdirectories: [],
})
}
if (parentDir !== "." && directories.has(parentDir)) {
directories.get(parentDir)!.subdirectories.push(dirName)
}
} else {
// It's a file
const ext = path.extname(file).toLowerCase()
fileTypes[ext] = (fileTypes[ext] || 0) + 1
const dir = path.dirname(file)
if (directories.has(dir)) {
directories.get(dir)!.fileCount++
}
}
}
// Get top-level directories
const topLevelDirs = Array.from(directories.values())
.filter((dir) => path.dirname(dir.path) === ".")
.sort((a, b) => a.name.localeCompare(b.name))
return {
directories: topLevelDirs,
fileCount: files.filter((f) => !f.endsWith("/")).length,
totalSize,
fileTypes,
}
}
private async detectTechnologies(files: string[]): Promise<Technology[]> {
const technologies: Technology[] = []
const fileSet = new Set(files)
// Node.js / JavaScript
if (fileSet.has("package.json")) {
technologies.push({ name: "Node.js", type: "language", configFile: "package.json" })
// Check for specific frameworks
try {
const content = await fs.readFile(path.join(this.rootPath, "package.json"), "utf-8")
const pkg = JSON.parse(content)
const deps = { ...pkg.dependencies, ...pkg.devDependencies }
if (deps.react) technologies.push({ name: "React", version: deps.react, type: "framework" })
if (deps.vue) technologies.push({ name: "Vue", version: deps.vue, type: "framework" })
if (deps.angular) technologies.push({ name: "Angular", version: deps.angular, type: "framework" })
if (deps.express) technologies.push({ name: "Express", version: deps.express, type: "framework" })
if (deps.next) technologies.push({ name: "Next.js", version: deps.next, type: "framework" })
if (deps.typescript)
technologies.push({ name: "TypeScript", version: deps.typescript, type: "language" })
if (deps.jest || deps.vitest || deps.mocha) {
const testFramework = deps.jest ? "Jest" : deps.vitest ? "Vitest" : "Mocha"
technologies.push({ name: testFramework, type: "tool" })
}
} catch {}
}
// Python
if (fileSet.has("requirements.txt") || fileSet.has("pyproject.toml") || fileSet.has("setup.py")) {
technologies.push({ name: "Python", type: "language" })
// Check for frameworks
try {
const reqPath = path.join(this.rootPath, "requirements.txt")
const content = await fs.readFile(reqPath, "utf-8")
if (content.includes("django")) technologies.push({ name: "Django", type: "framework" })
if (content.includes("flask")) technologies.push({ name: "Flask", type: "framework" })
if (content.includes("fastapi")) technologies.push({ name: "FastAPI", type: "framework" })
} catch {}
}
// Go
if (fileSet.has("go.mod")) {
technologies.push({ name: "Go", type: "language", configFile: "go.mod" })
}
// Rust
if (fileSet.has("Cargo.toml")) {
technologies.push({ name: "Rust", type: "language", configFile: "Cargo.toml" })
}
// Docker
if (fileSet.has("Dockerfile") || fileSet.has("docker-compose.yml")) {
technologies.push({ name: "Docker", type: "tool" })
}
return technologies
}
private async findConfigFiles(files: string[]): Promise<ConfigFile[]> {
const configFiles: ConfigFile[] = []
const configPatterns = [
{ pattern: "package.json", type: "npm" },
{ pattern: "tsconfig.json", type: "typescript" },
{ pattern: ".eslintrc", type: "eslint" },
{ pattern: ".prettierrc", type: "prettier" },
{ pattern: "webpack.config.js", type: "webpack" },
{ pattern: "vite.config", type: "vite" },
{ pattern: "jest.config", type: "jest" },
{ pattern: "vitest.config", type: "vitest" },
{ pattern: ".gitignore", type: "git" },
{ pattern: "Dockerfile", type: "docker" },
{ pattern: "docker-compose", type: "docker" },
{ pattern: ".env.example", type: "environment" },
{ pattern: "requirements.txt", type: "python" },
{ pattern: "pyproject.toml", type: "python" },
{ pattern: "go.mod", type: "go" },
{ pattern: "Cargo.toml", type: "rust" },
]
for (const file of files) {
const basename = path.basename(file)
for (const { pattern, type } of configPatterns) {
if (basename.includes(pattern)) {
configFiles.push({
name: basename,
path: file,
type,
})
}
}
}
return configFiles
}
private async analyzeDependencies(): Promise<Dependencies> {
const dependencies: Dependencies = {
production: {},
development: {},
}
// Try to read package.json
try {
const packageJsonPath = path.join(this.rootPath, "package.json")
const content = await fs.readFile(packageJsonPath, "utf-8")
const pkg = JSON.parse(content)
dependencies.production = pkg.dependencies || {}
dependencies.development = pkg.devDependencies || {}
dependencies.peer = pkg.peerDependencies
} catch {}
return dependencies
}
private async analyzeScripts(): Promise<Scripts> {
const scripts: Scripts = {}
// Try to read package.json scripts
try {
const packageJsonPath = path.join(this.rootPath, "package.json")
const content = await fs.readFile(packageJsonPath, "utf-8")
const pkg = JSON.parse(content)
if (pkg.scripts) {
Object.assign(scripts, pkg.scripts)
}
} catch {}
// Try to read Makefile
try {
const makefilePath = path.join(this.rootPath, "Makefile")
const content = await fs.readFile(makefilePath, "utf-8")
const lines = content.split("\n")
for (const line of lines) {
const match = line.match(/^([a-zA-Z0-9_-]+):/)
if (match) {
scripts[`make ${match[1]}`] = "Makefile target"
}
}
} catch {}
return scripts
}
private async analyzeGitInfo(): Promise<GitInfo> {
const gitInfo: GitInfo = {
hasGit: false,
}
try {
// Check if .git directory exists
await fs.access(path.join(this.rootPath, ".git"))
gitInfo.hasGit = true
// Try to read current branch
try {
const headContent = await fs.readFile(path.join(this.rootPath, ".git", "HEAD"), "utf-8")
const match = headContent.match(/ref: refs\/heads\/(.+)/)
if (match) {
gitInfo.branch = match[1].trim()
}
} catch {}
// Try to read remote
try {
const configContent = await fs.readFile(path.join(this.rootPath, ".git", "config"), "utf-8")
const remoteMatch = configContent.match(/url = (.+)/)
if (remoteMatch) {
gitInfo.remote = remoteMatch[1].trim()
}
} catch {}
} catch {}
return gitInfo
}
private async detectPatterns(files: string[]): Promise<ProjectPatterns> {
const patterns: ProjectPatterns = {}
const fileSet = new Set(files)
// Detect architecture
if (fileSet.has("src/") && (fileSet.has("src/components/") || fileSet.has("src/pages/"))) {
patterns.architecture = "Component-based"
} else if (fileSet.has("app/") && fileSet.has("app/models/")) {
patterns.architecture = "MVC"
} else if (fileSet.has("src/") && fileSet.has("src/domain/")) {
patterns.architecture = "Domain-driven"
}
// Detect testing framework
if (fileSet.has("jest.config.js") || fileSet.has("jest.config.ts")) {
patterns.testingFramework = "Jest"
} else if (fileSet.has("vitest.config.js") || fileSet.has("vitest.config.ts")) {
patterns.testingFramework = "Vitest"
} else if (fileSet.has(".mocharc.json")) {
patterns.testingFramework = "Mocha"
}
// Detect build tool
if (fileSet.has("webpack.config.js")) {
patterns.buildTool = "Webpack"
} else if (fileSet.has("vite.config.js") || fileSet.has("vite.config.ts")) {
patterns.buildTool = "Vite"
} else if (fileSet.has("rollup.config.js")) {
patterns.buildTool = "Rollup"
}
// Detect package manager
if (fileSet.has("package-lock.json")) {
patterns.packageManager = "npm"
} else if (fileSet.has("yarn.lock")) {
patterns.packageManager = "yarn"
} else if (fileSet.has("pnpm-lock.yaml")) {
patterns.packageManager = "pnpm"
}
// Detect CI/CD
const cicd: string[] = []
if (fileSet.has(".github/workflows/")) cicd.push("GitHub Actions")
if (fileSet.has(".gitlab-ci.yml")) cicd.push("GitLab CI")
if (fileSet.has(".circleci/")) cicd.push("CircleCI")
if (fileSet.has("Jenkinsfile")) cicd.push("Jenkins")
if (cicd.length > 0) patterns.cicd = cicd
return patterns
}
}

View file

@ -0,0 +1,126 @@
import { describe, it, expect } from "vitest"
import { DocumentationGenerator } from "../DocumentationGenerator"
import { ProjectInfo } from "../ProjectScanner"
describe("DocumentationGenerator", () => {
const mockProjectInfo: ProjectInfo = {
name: "test-project",
description: "A test project for unit testing",
rootPath: "/test/project",
structure: {
directories: [
{
name: "src",
path: "src",
fileCount: 5,
subdirectories: ["components", "utils"],
},
{
name: "tests",
path: "tests",
fileCount: 3,
subdirectories: [],
},
],
fileCount: 15,
totalSize: 0,
fileTypes: {
".ts": 8,
".tsx": 4,
".json": 2,
".md": 1,
},
},
technologies: [
{ name: "Node.js", type: "language", configFile: "package.json" },
{ name: "TypeScript", type: "language", version: "^5.0.0" },
{ name: "React", type: "framework", version: "^18.0.0" },
{ name: "Jest", type: "tool" },
],
configFiles: [
{ name: "package.json", path: "package.json", type: "npm" },
{ name: "tsconfig.json", path: "tsconfig.json", type: "typescript" },
{ name: ".gitignore", path: ".gitignore", type: "git" },
],
dependencies: {
production: {
react: "^18.0.0",
"react-dom": "^18.0.0",
},
development: {
typescript: "^5.0.0",
jest: "^29.0.0",
},
},
scripts: {
start: "react-scripts start",
build: "react-scripts build",
test: "jest",
},
gitInfo: {
hasGit: true,
branch: "main",
remote: "https://github.com/user/test-project.git",
},
patterns: {
architecture: "Component-based",
testingFramework: "Jest",
buildTool: "Webpack",
packageManager: "npm",
cicd: ["GitHub Actions"],
},
}
it("should generate documentation with all sections", async () => {
const generator = new DocumentationGenerator()
const documentation = await generator.generateDocumentation(mockProjectInfo)
// Check that the documentation contains expected sections
expect(documentation).toContain("# test-project")
expect(documentation).toContain("A test project for unit testing")
expect(documentation).toContain("## Technologies")
expect(documentation).toContain("## Project Structure")
expect(documentation).toContain("## Configuration Files")
expect(documentation).toContain("## Dependencies")
expect(documentation).toContain("## Available Scripts")
expect(documentation).toContain("## Development Setup")
expect(documentation).toContain("## Architecture Patterns")
expect(documentation).toContain("## Git Information")
})
it("should include technology details", async () => {
const generator = new DocumentationGenerator()
const documentation = await generator.generateDocumentation(mockProjectInfo)
expect(documentation).toContain("**Node.js**")
expect(documentation).toContain("**TypeScript** (^5.0.0)")
expect(documentation).toContain("**React** (^18.0.0)")
expect(documentation).toContain("**Jest**")
})
it("should include file statistics", async () => {
const generator = new DocumentationGenerator()
const documentation = await generator.generateDocumentation(mockProjectInfo)
expect(documentation).toContain("Total files: 15")
expect(documentation).toContain(".ts: 8 files")
expect(documentation).toContain(".tsx: 4 files")
})
it("should include setup instructions", async () => {
const generator = new DocumentationGenerator()
const documentation = await generator.generateDocumentation(mockProjectInfo)
expect(documentation).toContain("Clone the repository: `git clone https://github.com/user/test-project.git`")
expect(documentation).toContain("Install dependencies: `npm install`")
expect(documentation).toContain("Start the development server: `npm run start`")
expect(documentation).toContain("Run tests: `npm run test`")
})
it("should include timestamp", async () => {
const generator = new DocumentationGenerator()
const documentation = await generator.generateDocumentation(mockProjectInfo)
expect(documentation).toContain("This documentation was automatically generated by Roo Code on")
})
})

View file

@ -0,0 +1,89 @@
import { describe, it, expect, vi, beforeEach } from "vitest"
import * as fs from "fs/promises"
import * as path from "path"
import { ProjectScanner } from "../ProjectScanner"
// Mock the listFiles function
vi.mock("../../../services/glob/list-files", () => ({
listFiles: vi.fn().mockResolvedValue([["package.json", "src/", "src/index.ts", "README.md", ".gitignore"], false]),
}))
// Mock fs/promises
vi.mock("fs/promises", () => ({
readFile: vi.fn(),
}))
describe("ProjectScanner", () => {
const mockRootPath = "/test/project"
let scanner: ProjectScanner
beforeEach(() => {
vi.clearAllMocks()
scanner = new ProjectScanner(mockRootPath)
})
describe("scanProject", () => {
it("should detect project name from package.json", async () => {
vi.mocked(fs.readFile).mockImplementation(async (filePath) => {
if (filePath === path.join(mockRootPath, "package.json")) {
return JSON.stringify({
name: "test-project",
description: "A test project",
dependencies: {
react: "^18.0.0",
typescript: "^5.0.0",
},
})
}
throw new Error("File not found")
})
const result = await scanner.scanProject()
expect(result.name).toBe("test-project")
expect(result.description).toBe("A test project")
expect(result.rootPath).toBe(mockRootPath)
})
it("should detect technologies from package.json", async () => {
vi.mocked(fs.readFile).mockImplementation(async (filePath) => {
if (filePath === path.join(mockRootPath, "package.json")) {
return JSON.stringify({
name: "test-project",
dependencies: {
react: "^18.0.0",
express: "^4.18.0",
},
devDependencies: {
typescript: "^5.0.0",
jest: "^29.0.0",
},
})
}
throw new Error("File not found")
})
const result = await scanner.scanProject()
const techNames = result.technologies.map((t) => t.name)
expect(techNames).toContain("Node.js")
expect(techNames).toContain("React")
expect(techNames).toContain("Express")
expect(techNames).toContain("TypeScript")
expect(techNames).toContain("Jest")
})
it("should analyze project structure", async () => {
vi.mocked(fs.readFile).mockImplementation(async () => {
throw new Error("File not found")
})
const result = await scanner.scanProject()
expect(result.structure.fileCount).toBe(4) // Excluding directories
expect(result.structure.fileTypes[".json"]).toBe(1)
expect(result.structure.fileTypes[".ts"]).toBe(1)
expect(result.structure.fileTypes[".md"]).toBe(1)
})
})
})

View file

@ -1,5 +1,6 @@
import * as path from "path"
import * as vscode from "vscode"
import * as fs from "fs/promises"
import os from "os"
import crypto from "crypto"
import EventEmitter from "events"
@ -745,6 +746,12 @@ export class Task extends EventEmitter<ClineEvents> {
this.apiConversationHistory = []
await this.providerRef.deref()?.postStateToWebview()
// Check if the task is the /init command
if (task?.trim() === "/init") {
await this.handleInitCommand()
return
}
await this.say("text", task, images)
this.isInitialized = true
@ -761,6 +768,60 @@ export class Task extends EventEmitter<ClineEvents> {
])
}
private async handleInitCommand(): Promise<void> {
this.isInitialized = true
try {
// Notify user that we're starting the project scan
await this.say("text", "🔍 Scanning project structure and analyzing codebase...")
// Import the project scanner module
const { ProjectScanner } = await import("../project-scanner/ProjectScanner")
const { DocumentationGenerator } = await import("../project-scanner/DocumentationGenerator")
// Create scanner instance
const scanner = new ProjectScanner(this.cwd, this.rooIgnoreController)
// Scan the project
await this.say("text", "📊 Analyzing project structure...")
const projectInfo = await scanner.scanProject()
// Generate documentation
await this.say("text", "📝 Generating ROO.md documentation...")
const generator = new DocumentationGenerator()
const documentation = await generator.generateDocumentation(projectInfo)
// Write ROO.md file using the file system
const rooMdPath = path.join(this.cwd, "ROO.md")
await this.say("text", `✍️ Writing documentation to ${rooMdPath}...`)
// Write the file directly
await fs.writeFile(rooMdPath, documentation, "utf-8")
// Provide summary to user
await this.say(
"text",
`✅ Project initialization complete!
I've created a ROO.md file with comprehensive documentation about your project:
- Project structure and organization
- Detected technologies and frameworks
- Key configuration files
- Development setup instructions
- Important patterns and conventions
The ROO.md file will help me better understand your project context in future conversations.`,
)
// Mark task as complete by completing the task loop
this.abort = true
} catch (error) {
await this.say("error", `Failed to initialize project: ${error.message}`)
console.error("Error in handleInitCommand:", error)
this.abort = true
}
}
public async resumePausedTask(lastMessage: string) {
// Release this Cline instance from paused state.
this.isPaused = false