|
| 1 | +import * as fs from "fs/promises" |
| 2 | +import * as path from "path" |
| 3 | +import * as vscode from "vscode" |
| 4 | +import { fileExistsAtPath } from "../../utils/fs" |
| 5 | +import { getProjectRooDirectoryForCwd } from "../roo-config/index" |
| 6 | + |
| 7 | +interface ProjectConfig { |
| 8 | + type: "typescript" | "javascript" | "python" | "java" | "go" | "rust" | "unknown" |
| 9 | + hasTypeScript: boolean |
| 10 | + hasESLint: boolean |
| 11 | + hasPrettier: boolean |
| 12 | + hasJest: boolean |
| 13 | + hasVitest: boolean |
| 14 | + hasPytest: boolean |
| 15 | + packageManager: "npm" | "yarn" | "pnpm" | "bun" | null |
| 16 | + dependencies: string[] |
| 17 | + devDependencies: string[] |
| 18 | + scripts: Record<string, string> |
| 19 | +} |
| 20 | + |
| 21 | +/** |
| 22 | + * Analyzes the project configuration files to determine project type and tools |
| 23 | + */ |
| 24 | +async function analyzeProjectConfig(workspacePath: string): Promise<ProjectConfig> { |
| 25 | + const config: ProjectConfig = { |
| 26 | + type: "unknown", |
| 27 | + hasTypeScript: false, |
| 28 | + hasESLint: false, |
| 29 | + hasPrettier: false, |
| 30 | + hasJest: false, |
| 31 | + hasVitest: false, |
| 32 | + hasPytest: false, |
| 33 | + packageManager: null, |
| 34 | + dependencies: [], |
| 35 | + devDependencies: [], |
| 36 | + scripts: {}, |
| 37 | + } |
| 38 | + |
| 39 | + // Check for package.json |
| 40 | + const packageJsonPath = path.join(workspacePath, "package.json") |
| 41 | + if (await fileExistsAtPath(packageJsonPath)) { |
| 42 | + try { |
| 43 | + const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf-8")) |
| 44 | + |
| 45 | + // Determine package manager |
| 46 | + if (await fileExistsAtPath(path.join(workspacePath, "yarn.lock"))) { |
| 47 | + config.packageManager = "yarn" |
| 48 | + } else if (await fileExistsAtPath(path.join(workspacePath, "pnpm-lock.yaml"))) { |
| 49 | + config.packageManager = "pnpm" |
| 50 | + } else if (await fileExistsAtPath(path.join(workspacePath, "bun.lockb"))) { |
| 51 | + config.packageManager = "bun" |
| 52 | + } else if (await fileExistsAtPath(path.join(workspacePath, "package-lock.json"))) { |
| 53 | + config.packageManager = "npm" |
| 54 | + } |
| 55 | + |
| 56 | + // Extract dependencies |
| 57 | + config.dependencies = Object.keys(packageJson.dependencies || {}) |
| 58 | + config.devDependencies = Object.keys(packageJson.devDependencies || {}) |
| 59 | + config.scripts = packageJson.scripts || {} |
| 60 | + |
| 61 | + // Check for specific tools |
| 62 | + const allDeps = [...config.dependencies, ...config.devDependencies] |
| 63 | + config.hasTypeScript = |
| 64 | + allDeps.includes("typescript") || (await fileExistsAtPath(path.join(workspacePath, "tsconfig.json"))) |
| 65 | + config.hasESLint = |
| 66 | + allDeps.includes("eslint") || |
| 67 | + (await fileExistsAtPath(path.join(workspacePath, ".eslintrc.js"))) || |
| 68 | + (await fileExistsAtPath(path.join(workspacePath, ".eslintrc.json"))) |
| 69 | + config.hasPrettier = |
| 70 | + allDeps.includes("prettier") || (await fileExistsAtPath(path.join(workspacePath, ".prettierrc"))) |
| 71 | + config.hasJest = allDeps.includes("jest") |
| 72 | + config.hasVitest = allDeps.includes("vitest") |
| 73 | + |
| 74 | + // Determine project type |
| 75 | + if (config.hasTypeScript) { |
| 76 | + config.type = "typescript" |
| 77 | + } else { |
| 78 | + config.type = "javascript" |
| 79 | + } |
| 80 | + } catch (error) { |
| 81 | + console.error("Error parsing package.json:", error) |
| 82 | + } |
| 83 | + } |
| 84 | + |
| 85 | + // Check for Python project |
| 86 | + if ( |
| 87 | + (await fileExistsAtPath(path.join(workspacePath, "pyproject.toml"))) || |
| 88 | + (await fileExistsAtPath(path.join(workspacePath, "setup.py"))) || |
| 89 | + (await fileExistsAtPath(path.join(workspacePath, "requirements.txt"))) |
| 90 | + ) { |
| 91 | + config.type = "python" |
| 92 | + config.hasPytest = |
| 93 | + (await fileExistsAtPath(path.join(workspacePath, "pytest.ini"))) || |
| 94 | + (await fileExistsAtPath(path.join(workspacePath, "pyproject.toml"))) |
| 95 | + } |
| 96 | + |
| 97 | + // Check for other project types |
| 98 | + if (await fileExistsAtPath(path.join(workspacePath, "go.mod"))) { |
| 99 | + config.type = "go" |
| 100 | + } else if (await fileExistsAtPath(path.join(workspacePath, "Cargo.toml"))) { |
| 101 | + config.type = "rust" |
| 102 | + } else if ( |
| 103 | + (await fileExistsAtPath(path.join(workspacePath, "pom.xml"))) || |
| 104 | + (await fileExistsAtPath(path.join(workspacePath, "build.gradle"))) |
| 105 | + ) { |
| 106 | + config.type = "java" |
| 107 | + } |
| 108 | + |
| 109 | + return config |
| 110 | +} |
| 111 | + |
| 112 | +/** |
| 113 | + * Generates rules content based on project analysis |
| 114 | + */ |
| 115 | +function generateRulesContent(config: ProjectConfig, workspacePath: string): string { |
| 116 | + const sections: string[] = [] |
| 117 | + |
| 118 | + // Header |
| 119 | + sections.push("# Project Rules") |
| 120 | + sections.push("") |
| 121 | + sections.push(`Generated on: ${new Date().toISOString()}`) |
| 122 | + sections.push(`Project type: ${config.type}`) |
| 123 | + sections.push("") |
| 124 | + |
| 125 | + // Build and Development |
| 126 | + sections.push("## Build and Development") |
| 127 | + sections.push("") |
| 128 | + |
| 129 | + if (config.packageManager) { |
| 130 | + sections.push(`- Package manager: ${config.packageManager}`) |
| 131 | + sections.push(`- Install dependencies: \`${config.packageManager} install\``) |
| 132 | + |
| 133 | + if (config.scripts.build) { |
| 134 | + sections.push(`- Build command: \`${config.packageManager} run build\``) |
| 135 | + } |
| 136 | + if (config.scripts.test) { |
| 137 | + sections.push(`- Test command: \`${config.packageManager} run test\``) |
| 138 | + } |
| 139 | + if (config.scripts.dev || config.scripts.start) { |
| 140 | + const devScript = config.scripts.dev || config.scripts.start |
| 141 | + sections.push( |
| 142 | + `- Development server: \`${config.packageManager} run ${config.scripts.dev ? "dev" : "start"}\``, |
| 143 | + ) |
| 144 | + } |
| 145 | + } |
| 146 | + |
| 147 | + sections.push("") |
| 148 | + |
| 149 | + // Code Style and Linting |
| 150 | + sections.push("## Code Style and Linting") |
| 151 | + sections.push("") |
| 152 | + |
| 153 | + if (config.hasESLint) { |
| 154 | + sections.push("- ESLint is configured for this project") |
| 155 | + sections.push("- Run linting: `npm run lint` (if configured)") |
| 156 | + sections.push("- Follow ESLint rules and fix any linting errors before committing") |
| 157 | + } |
| 158 | + |
| 159 | + if (config.hasPrettier) { |
| 160 | + sections.push("- Prettier is configured for code formatting") |
| 161 | + sections.push("- Format code before committing") |
| 162 | + sections.push("- Run formatting: `npm run format` (if configured)") |
| 163 | + } |
| 164 | + |
| 165 | + if (config.hasTypeScript) { |
| 166 | + sections.push("- TypeScript is used in this project") |
| 167 | + sections.push("- Ensure all TypeScript errors are resolved before committing") |
| 168 | + sections.push("- Use proper type annotations and avoid `any` types") |
| 169 | + sections.push("- Run type checking: `npm run type-check` or `tsc --noEmit`") |
| 170 | + } |
| 171 | + |
| 172 | + sections.push("") |
| 173 | + |
| 174 | + // Testing |
| 175 | + sections.push("## Testing") |
| 176 | + sections.push("") |
| 177 | + |
| 178 | + if (config.hasJest || config.hasVitest) { |
| 179 | + const testFramework = config.hasVitest ? "Vitest" : "Jest" |
| 180 | + sections.push(`- ${testFramework} is used for testing`) |
| 181 | + sections.push("- Write tests for new features and bug fixes") |
| 182 | + sections.push("- Ensure all tests pass before committing") |
| 183 | + sections.push(`- Run tests: \`${config.packageManager || "npm"} run test\``) |
| 184 | + |
| 185 | + if (config.hasVitest) { |
| 186 | + sections.push("- Vitest specific: Test files should use `.test.ts` or `.spec.ts` extensions") |
| 187 | + sections.push("- The `describe`, `test`, `it` functions are globally available") |
| 188 | + } |
| 189 | + } |
| 190 | + |
| 191 | + if (config.hasPytest && config.type === "python") { |
| 192 | + sections.push("- Pytest is used for testing") |
| 193 | + sections.push("- Write tests in `test_*.py` or `*_test.py` files") |
| 194 | + sections.push("- Run tests: `pytest`") |
| 195 | + } |
| 196 | + |
| 197 | + sections.push("") |
| 198 | + |
| 199 | + // Project Structure |
| 200 | + sections.push("## Project Structure") |
| 201 | + sections.push("") |
| 202 | + sections.push("- Follow the existing project structure and naming conventions") |
| 203 | + sections.push("- Place new files in appropriate directories") |
| 204 | + sections.push("- Use consistent file naming (kebab-case, camelCase, or PascalCase as per project convention)") |
| 205 | + |
| 206 | + sections.push("") |
| 207 | + |
| 208 | + // Language-specific rules |
| 209 | + if (config.type === "typescript" || config.type === "javascript") { |
| 210 | + sections.push("## JavaScript/TypeScript Guidelines") |
| 211 | + sections.push("") |
| 212 | + sections.push("- Use ES6+ syntax (const/let, arrow functions, destructuring, etc.)") |
| 213 | + sections.push("- Prefer functional programming patterns where appropriate") |
| 214 | + sections.push("- Handle errors properly with try/catch blocks") |
| 215 | + sections.push("- Use async/await for asynchronous operations") |
| 216 | + sections.push("- Follow existing import/export patterns") |
| 217 | + sections.push("") |
| 218 | + } |
| 219 | + |
| 220 | + if (config.type === "python") { |
| 221 | + sections.push("## Python Guidelines") |
| 222 | + sections.push("") |
| 223 | + sections.push("- Follow PEP 8 style guide") |
| 224 | + sections.push("- Use type hints where appropriate") |
| 225 | + sections.push("- Write docstrings for functions and classes") |
| 226 | + sections.push("- Use virtual environments for dependency management") |
| 227 | + sections.push("") |
| 228 | + } |
| 229 | + |
| 230 | + // General Best Practices |
| 231 | + sections.push("## General Best Practices") |
| 232 | + sections.push("") |
| 233 | + sections.push("- Write clear, self-documenting code") |
| 234 | + sections.push("- Add comments for complex logic") |
| 235 | + sections.push("- Keep functions small and focused") |
| 236 | + sections.push("- Follow DRY (Don't Repeat Yourself) principle") |
| 237 | + sections.push("- Handle edge cases and errors gracefully") |
| 238 | + sections.push("- Write meaningful commit messages") |
| 239 | + sections.push("") |
| 240 | + |
| 241 | + // Dependencies |
| 242 | + if (config.dependencies.length > 0 || config.devDependencies.length > 0) { |
| 243 | + sections.push("## Key Dependencies") |
| 244 | + sections.push("") |
| 245 | + |
| 246 | + // List some key dependencies |
| 247 | + const keyDeps = [...config.dependencies, ...config.devDependencies] |
| 248 | + .filter((dep) => !dep.startsWith("@types/")) |
| 249 | + .slice(0, 10) |
| 250 | + |
| 251 | + keyDeps.forEach((dep) => { |
| 252 | + sections.push(`- ${dep}`) |
| 253 | + }) |
| 254 | + |
| 255 | + sections.push("") |
| 256 | + } |
| 257 | + |
| 258 | + return sections.join("\n") |
| 259 | +} |
| 260 | + |
| 261 | +/** |
| 262 | + * Generates rules for the workspace and saves them to a file |
| 263 | + */ |
| 264 | +export async function generateRulesForWorkspace(workspacePath: string): Promise<string> { |
| 265 | + // Analyze the project |
| 266 | + const config = await analyzeProjectConfig(workspacePath) |
| 267 | + |
| 268 | + // Generate rules content |
| 269 | + const rulesContent = generateRulesContent(config, workspacePath) |
| 270 | + |
| 271 | + // Ensure .roo/rules directory exists |
| 272 | + const rooDir = getProjectRooDirectoryForCwd(workspacePath) |
| 273 | + const rulesDir = path.join(rooDir, "rules") |
| 274 | + await fs.mkdir(rulesDir, { recursive: true }) |
| 275 | + |
| 276 | + // Generate filename with timestamp |
| 277 | + const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, -5) |
| 278 | + const rulesFileName = `generated-rules-${timestamp}.md` |
| 279 | + const rulesPath = path.join(rulesDir, rulesFileName) |
| 280 | + |
| 281 | + // Write rules file |
| 282 | + await fs.writeFile(rulesPath, rulesContent, "utf-8") |
| 283 | + |
| 284 | + // Open the file in VSCode |
| 285 | + const doc = await vscode.workspace.openTextDocument(rulesPath) |
| 286 | + await vscode.window.showTextDocument(doc) |
| 287 | + |
| 288 | + return rulesPath |
| 289 | +} |
0 commit comments