mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
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:
parent
1e17b3b3bb
commit
44a6cd7da5
5 changed files with 1050 additions and 0 deletions
342
src/core/project-scanner/DocumentationGenerator.ts
Normal file
342
src/core/project-scanner/DocumentationGenerator.ts
Normal 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
|
||||
}
|
||||
}
|
||||
432
src/core/project-scanner/ProjectScanner.ts
Normal file
432
src/core/project-scanner/ProjectScanner.ts
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
})
|
||||
})
|
||||
89
src/core/project-scanner/__tests__/ProjectScanner.test.ts
Normal file
89
src/core/project-scanner/__tests__/ProjectScanner.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue