diff --git a/src/services/rules/__tests__/rulesGenerator.test.ts b/src/services/rules/__tests__/rulesGenerator.test.ts new file mode 100644 index 0000000000..29d15cda5b --- /dev/null +++ b/src/services/rules/__tests__/rulesGenerator.test.ts @@ -0,0 +1,100 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" +import * as fs from "fs/promises" +import * as path from "path" +import { createRulesGenerationTaskMessage } from "../rulesGenerator" + +// Mock fs module +vi.mock("fs/promises", () => ({ + mkdir: vi.fn(), +})) + +describe("rulesGenerator", () => { + const mockWorkspacePath = "/test/workspace" + + beforeEach(() => { + vi.clearAllMocks() + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + describe("createRulesGenerationTaskMessage", () => { + it("should create directories when alwaysAllowWriteProtected is true", async () => { + await createRulesGenerationTaskMessage(mockWorkspacePath, ["general"], false, true) + + // Verify mkdir was called for each directory + expect(fs.mkdir).toHaveBeenCalledWith(path.join(mockWorkspacePath, ".roo", "rules"), { recursive: true }) + expect(fs.mkdir).toHaveBeenCalledWith(path.join(mockWorkspacePath, ".roo", "rules-code"), { + recursive: true, + }) + expect(fs.mkdir).toHaveBeenCalledWith(path.join(mockWorkspacePath, ".roo", "rules-architect"), { + recursive: true, + }) + expect(fs.mkdir).toHaveBeenCalledWith(path.join(mockWorkspacePath, ".roo", "rules-debug"), { + recursive: true, + }) + expect(fs.mkdir).toHaveBeenCalledWith(path.join(mockWorkspacePath, ".roo", "rules-docs-extractor"), { + recursive: true, + }) + }) + + it("should NOT create directories when alwaysAllowWriteProtected is false", async () => { + await createRulesGenerationTaskMessage(mockWorkspacePath, ["general"], false, false) + + // Verify mkdir was NOT called + expect(fs.mkdir).not.toHaveBeenCalled() + }) + + it("should include auto-approval note in message when alwaysAllowWriteProtected is true", async () => { + const message = await createRulesGenerationTaskMessage(mockWorkspacePath, ["general"], false, true) + + expect(message).toContain("The directory has already been created for you") + expect(message).toContain("Auto-approval for protected file writes is enabled") + }) + + it("should include manual approval note in message when alwaysAllowWriteProtected is false", async () => { + const message = await createRulesGenerationTaskMessage(mockWorkspacePath, ["general"], false, false) + + expect(message).toContain("Create the necessary directories if they don't exist") + expect(message).toContain("You will need to approve the creation of protected directories and files") + }) + + it("should handle multiple rule types", async () => { + const message = await createRulesGenerationTaskMessage( + mockWorkspacePath, + ["general", "code", "architect"], + false, + true, + ) + + expect(message).toContain(".roo/rules/coding-standards.md") + expect(message).toContain(".roo/rules-code/implementation-rules.md") + expect(message).toContain(".roo/rules-architect/architecture-rules.md") + }) + + it("should include gitignore instructions when addToGitignore is true", async () => { + const message = await createRulesGenerationTaskMessage(mockWorkspacePath, ["general"], true, false) + + expect(message).toContain("Add the generated files to .gitignore") + }) + + it("should include analysis steps for each rule type", async () => { + const message = await createRulesGenerationTaskMessage(mockWorkspacePath, ["code"], false, false) + + // Check that code-specific analysis steps are included + expect(message).toContain("Analyze package.json or equivalent files") + expect(message).toContain("Check for linting and formatting tools") + expect(message).toContain("Examine test files to understand testing patterns") + }) + + it("should include different analysis steps for different rule types", async () => { + const message = await createRulesGenerationTaskMessage(mockWorkspacePath, ["architect"], false, false) + + // Check that architect-specific analysis steps are included + expect(message).toContain("Analyze the overall directory structure") + expect(message).toContain("Identify architectural patterns") + expect(message).toContain("separation of concerns") + }) + }) +}) diff --git a/src/services/rules/rulesGenerator.ts b/src/services/rules/rulesGenerator.ts index 4ea649a02d..3df16a2d99 100644 --- a/src/services/rules/rulesGenerator.ts +++ b/src/services/rules/rulesGenerator.ts @@ -1,205 +1,5 @@ import * as fs from "fs/promises" import * as path from "path" -import { fileExistsAtPath } from "../../utils/fs" - -interface ProjectConfig { - type: "typescript" | "javascript" | "python" | "java" | "go" | "rust" | "unknown" - hasTypeScript: boolean - hasESLint: boolean - hasPrettier: boolean - hasJest: boolean - hasVitest: boolean - hasPytest: boolean - packageManager: "npm" | "yarn" | "pnpm" | "bun" | null - dependencies: string[] - devDependencies: string[] - scripts: Record -} - -/** - * Analyzes the project configuration files to determine project type and tools - */ -async function analyzeProjectConfig(workspacePath: string): Promise { - const config: ProjectConfig = { - type: "unknown", - hasTypeScript: false, - hasESLint: false, - hasPrettier: false, - hasJest: false, - hasVitest: false, - hasPytest: false, - packageManager: null, - dependencies: [], - devDependencies: [], - scripts: {}, - } - - // Check for package.json - const packageJsonPath = path.join(workspacePath, "package.json") - if (await fileExistsAtPath(packageJsonPath)) { - try { - const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf-8")) - - // Determine package manager - if (await fileExistsAtPath(path.join(workspacePath, "yarn.lock"))) { - config.packageManager = "yarn" - } else if (await fileExistsAtPath(path.join(workspacePath, "pnpm-lock.yaml"))) { - config.packageManager = "pnpm" - } else if (await fileExistsAtPath(path.join(workspacePath, "bun.lockb"))) { - config.packageManager = "bun" - } else if (await fileExistsAtPath(path.join(workspacePath, "package-lock.json"))) { - config.packageManager = "npm" - } - - // Extract dependencies - config.dependencies = Object.keys(packageJson.dependencies || {}) - config.devDependencies = Object.keys(packageJson.devDependencies || {}) - config.scripts = packageJson.scripts || {} - - // Check for TypeScript - if ( - config.devDependencies.includes("typescript") || - config.dependencies.includes("typescript") || - (await fileExistsAtPath(path.join(workspacePath, "tsconfig.json"))) - ) { - config.hasTypeScript = true - config.type = "typescript" - } else { - config.type = "javascript" - } - - // Check for testing frameworks - if (config.devDependencies.includes("jest") || config.dependencies.includes("jest")) { - config.hasJest = true - } - if (config.devDependencies.includes("vitest") || config.dependencies.includes("vitest")) { - config.hasVitest = true - } - - // Check for linting/formatting - if (config.devDependencies.includes("eslint") || config.dependencies.includes("eslint")) { - config.hasESLint = true - } - if (config.devDependencies.includes("prettier") || config.dependencies.includes("prettier")) { - config.hasPrettier = true - } - } catch (error) { - console.error("Error parsing package.json:", error) - } - } - - // Check for Python project - if (await fileExistsAtPath(path.join(workspacePath, "requirements.txt"))) { - config.type = "python" - } else if (await fileExistsAtPath(path.join(workspacePath, "pyproject.toml"))) { - config.type = "python" - // Check for pytest - try { - const pyprojectContent = await fs.readFile(path.join(workspacePath, "pyproject.toml"), "utf-8") - if (pyprojectContent.includes("pytest")) { - config.hasPytest = true - } - } catch (error) { - console.error("Error reading pyproject.toml:", error) - } - } else if (await fileExistsAtPath(path.join(workspacePath, "setup.py"))) { - config.type = "python" - } - - // Check for other project types - if (await fileExistsAtPath(path.join(workspacePath, "go.mod"))) { - config.type = "go" - } else if (await fileExistsAtPath(path.join(workspacePath, "Cargo.toml"))) { - config.type = "rust" - } else if (await fileExistsAtPath(path.join(workspacePath, "pom.xml"))) { - config.type = "java" - } - - return config -} - -/** - * Generates a summary of the codebase for LLM analysis - */ -async function generateCodebaseSummary(workspacePath: string, config: ProjectConfig): Promise { - const summary: string[] = [] - - summary.push("## Project Configuration Analysis") - summary.push("") - summary.push(`**Project Type:** ${config.type}`) - summary.push(`**Package Manager:** ${config.packageManager || "None detected"}`) - summary.push("") - - // List key dependencies - if (config.dependencies.length > 0) { - summary.push("### Key Dependencies:") - const keyDeps = config.dependencies.slice(0, 10) - keyDeps.forEach((dep) => summary.push(`- ${dep}`)) - if (config.dependencies.length > 10) { - summary.push(`- ... and ${config.dependencies.length - 10} more`) - } - summary.push("") - } - - // List dev dependencies - if (config.devDependencies.length > 0) { - summary.push("### Development Dependencies:") - const keyDevDeps = config.devDependencies.slice(0, 10) - keyDevDeps.forEach((dep) => summary.push(`- ${dep}`)) - if (config.devDependencies.length > 10) { - summary.push(`- ... and ${config.devDependencies.length - 10} more`) - } - summary.push("") - } - - // List available scripts - const scriptKeys = Object.keys(config.scripts) - if (scriptKeys.length > 0) { - summary.push("### Available Scripts:") - scriptKeys.forEach((script) => summary.push(`- ${script}: ${config.scripts[script]}`)) - summary.push("") - } - - // List detected tools - summary.push("### Detected Tools and Frameworks:") - const tools: string[] = [] - if (config.hasTypeScript) tools.push("TypeScript") - if (config.hasESLint) tools.push("ESLint") - if (config.hasPrettier) tools.push("Prettier") - if (config.hasJest) tools.push("Jest") - if (config.hasVitest) tools.push("Vitest") - if (config.hasPytest) tools.push("Pytest") - - if (tools.length === 0) { - tools.push("No specific tools detected") - } - - tools.forEach((tool) => summary.push(`- ${tool}`)) - summary.push("") - - // Check for existing rules files - const existingRulesFiles: string[] = [] - if (await fileExistsAtPath(path.join(workspacePath, "CLAUDE.md"))) { - existingRulesFiles.push("CLAUDE.md") - } - if (await fileExistsAtPath(path.join(workspacePath, ".cursorrules"))) { - existingRulesFiles.push(".cursorrules") - } - if (await fileExistsAtPath(path.join(workspacePath, ".cursor", "rules"))) { - existingRulesFiles.push(".cursor/rules") - } - if (await fileExistsAtPath(path.join(workspacePath, ".github", "copilot-instructions.md"))) { - existingRulesFiles.push(".github/copilot-instructions.md") - } - - if (existingRulesFiles.length > 0) { - summary.push("## Existing Rules Files:") - existingRulesFiles.forEach((file) => summary.push(`- ${file}`)) - summary.push("") - } - - return summary.join("\n") -} /** * Creates a comprehensive task message for rules generation that can be used with initClineWithTask @@ -210,24 +10,22 @@ export async function createRulesGenerationTaskMessage( addToGitignore: boolean, alwaysAllowWriteProtected: boolean = false, ): Promise { - // Analyze the project to get context - const config = await analyzeProjectConfig(workspacePath) - const codebaseSummary = await generateCodebaseSummary(workspacePath, config) + // Only create directories if auto-approve is enabled + if (alwaysAllowWriteProtected) { + const directoriesToCreate = [ + path.join(workspacePath, ".roo", "rules"), + path.join(workspacePath, ".roo", "rules-code"), + path.join(workspacePath, ".roo", "rules-architect"), + path.join(workspacePath, ".roo", "rules-debug"), + path.join(workspacePath, ".roo", "rules-docs-extractor"), + ] - // Ensure all necessary directories exist at project root - const directoriesToCreate = [ - path.join(workspacePath, ".roo", "rules"), - path.join(workspacePath, ".roo", "rules-code"), - path.join(workspacePath, ".roo", "rules-architect"), - path.join(workspacePath, ".roo", "rules-debug"), - path.join(workspacePath, ".roo", "rules-docs-extractor"), - ] - - for (const dir of directoriesToCreate) { - try { - await fs.mkdir(dir, { recursive: true }) - } catch (error) { - // Directory might already exist, which is fine + for (const dir of directoriesToCreate) { + try { + await fs.mkdir(dir, { recursive: true }) + } catch (error) { + // Directory might already exist, which is fine + } } } @@ -235,6 +33,7 @@ export async function createRulesGenerationTaskMessage( interface RuleInstruction { path: string focus: string + analysisSteps: string[] } const ruleInstructions: RuleInstruction[] = selectedRuleTypes @@ -244,26 +43,61 @@ export async function createRulesGenerationTaskMessage( return { path: ".roo/rules/coding-standards.md", focus: "General coding standards that apply to all modes, including naming conventions, file organization, and general best practices", + analysisSteps: [ + "Examine the project structure and file organization patterns", + "Identify naming conventions for files, functions, variables, and classes", + "Look for general coding patterns and conventions used throughout the codebase", + "Check for any existing documentation or README files that describe project standards", + ], } case "code": return { path: ".roo/rules-code/implementation-rules.md", focus: "Specific rules for code implementation, focusing on syntax patterns, code structure, error handling, testing approaches, and detailed implementation guidelines", + analysisSteps: [ + "Analyze package.json or equivalent files to identify dependencies and build tools", + "Check for linting and formatting tools (ESLint, Prettier, etc.) and their configurations", + "Examine test files to understand testing patterns and frameworks used", + "Look for error handling patterns and logging strategies", + "Identify code style preferences and import/export patterns", + "Check for TypeScript usage and type definition patterns if applicable", + ], } case "architect": return { path: ".roo/rules-architect/architecture-rules.md", focus: "High-level system design rules, focusing on file layout, module organization, architectural patterns, and system-wide design principles", + analysisSteps: [ + "Analyze the overall directory structure and module organization", + "Identify architectural patterns (MVC, microservices, monorepo, etc.)", + "Look for separation of concerns and layering patterns", + "Check for API design patterns and service boundaries", + "Examine how different parts of the system communicate", + ], } case "debug": return { path: ".roo/rules-debug/debugging-rules.md", focus: "Debugging workflow rules, including error investigation approaches, logging strategies, troubleshooting patterns, and debugging best practices", + analysisSteps: [ + "Identify logging frameworks and patterns used in the codebase", + "Look for error handling and exception patterns", + "Check for debugging tools or scripts in the project", + "Analyze test structure for debugging approaches", + "Look for monitoring or observability patterns", + ], } case "docs-extractor": return { path: ".roo/rules-docs-extractor/documentation-rules.md", focus: "Documentation extraction and formatting rules, including documentation style guides, API documentation patterns, and content organization", + analysisSteps: [ + "Check for existing documentation files and their formats", + "Analyze code comments and documentation patterns", + "Look for API documentation tools or generators", + "Identify documentation structure and organization patterns", + "Check for examples or tutorials in the codebase", + ], } default: return null @@ -276,40 +110,40 @@ export async function createRulesGenerationTaskMessage( Your task is to: -1. **Analyze the project structure** - The codebase has been analyzed and here's what was found: +1. **Analyze the project structure** by: +${ruleInstructions.map((rule) => ` - For ${rule.path.split("/").pop()}: ${rule.analysisSteps.join("; ")}`).join("\n")} -${codebaseSummary} - -2. **Create comprehensive rules** that include: - - Build/lint/test commands (especially for running single tests) - - Code style guidelines including imports, formatting, types, naming conventions - - Error handling patterns - - Project-specific conventions and best practices - - File organization patterns +2. **Look for existing rule files** that might provide guidance: + - Check for CLAUDE.md, .cursorrules, .cursor/rules, or .github/copilot-instructions.md + - If found, incorporate and improve upon their content 3. **Generate and save the following rule files**: ${ruleInstructions .map( (rule, index) => ` ${index + 1}. **${rule.path}** - - Focus: ${rule.focus} - - The directory has already been created for you + - Focus: ${rule.focus}${alwaysAllowWriteProtected ? "\n - The directory has already been created for you" : "\n - Create the necessary directories if they don't exist"} - Always overwrite the existing file if it exists - - Use the \`write_to_file\` tool to save the content${alwaysAllowWriteProtected ? "\n - Note: Auto-approval for protected file writes is enabled, so you can write to .roo directories without manual approval" : ""}`, + - Use the \`write_to_file\` tool to save the content${alwaysAllowWriteProtected ? "\n - Note: Auto-approval for protected file writes is enabled, so you can write to .roo directories without manual approval" : "\n - Note: You will need to approve the creation of protected directories and files"}`, ) .join("\n")} -4. **Open the generated file** in the editor for review +4. **Make the rules actionable and specific** by including: + - Build/lint/test commands (especially for running single tests) + - Code style guidelines including imports, formatting, types, naming conventions + - Error handling patterns specific to this project + - Project-specific conventions and best practices + - File organization patterns -The rules should be about 20-30 lines long and focus on the most important guidelines for this specific project. Make them actionable and specific to help AI agents work effectively in this codebase. +5. **Keep rules concise** - aim for 20-30 lines per file, focusing on the most important guidelines -If there are existing rules files (like CLAUDE.md, .cursorrules, .cursor/rules, .github/copilot-instructions.md), incorporate and improve upon them. +6. **Open the generated files** in the editor for review after creation Use the \`safeWriteJson\` utility from \`src/utils/safeWriteJson.ts\` for any JSON file operations to ensure atomic writes. ${ addToGitignore - ? `5. **Add the generated files to .gitignore**: + ? `7. **Add the generated files to .gitignore**: - After generating all rule files, add entries to .gitignore to prevent them from being committed - Add each generated file path to .gitignore (e.g., .roo/rules/coding-standards.md) - If .gitignore doesn't exist, create it diff --git a/webview-ui/src/components/settings/RulesSettings.tsx b/webview-ui/src/components/settings/RulesSettings.tsx index a2d3c3dcde..95e5e928ad 100644 --- a/webview-ui/src/components/settings/RulesSettings.tsx +++ b/webview-ui/src/components/settings/RulesSettings.tsx @@ -1,6 +1,6 @@ import { HTMLAttributes, useState, useEffect } from "react" import { useAppTranslation } from "@/i18n/TranslationContext" -import { FileText, Loader2, AlertTriangle, Info } from "lucide-react" +import { FileText, Loader2, AlertTriangle, Info, Sparkles } from "lucide-react" import { Button } from "@/components/ui/button" import { vscode } from "@/utils/vscode" import { cn } from "@/lib/utils" @@ -156,7 +156,7 @@ export const RulesSettings = ({ className, ...props }: RulesSettingsProps) => { return (
- +
{t("settings:rules.title")}
@@ -164,132 +164,135 @@ export const RulesSettings = ({ className, ...props }: RulesSettingsProps) => {
-
-

{t("settings:rules.description")}

- - {/* Recommendation box */} -
- -
- {t("settings:rules.autoApproveRecommendation")} +
+ {/* Magic Rules Generation subsection */} +
+
+
+ +
{t("settings:rules.magicGeneration.title")}
+
+
+ {t("settings:rules.magicGeneration.description")} +
-
+
+ {/* Recommendation box */} +
+ +
+ {t("settings:rules.autoApproveRecommendation")} +
+
-
-
-

{t("settings:rules.selectTypes")}

-
- {ruleTypes.map((ruleType) => ( -
handleRuleTypeToggle(ruleType.id)} - className={cn( - "relative p-3 rounded-md border cursor-pointer transition-all", - "hover:border-vscode-focusBorder", - ruleType.checked - ? "bg-vscode-list-activeSelectionBackground border-vscode-focusBorder" - : "bg-vscode-editor-background border-vscode-panel-border", - )}> -
-
- {ruleType.label} - {ruleType.exists && ( - - • - - )} -
-
- {ruleType.description} +
+

{t("settings:rules.selectTypes")}

+
+ {ruleTypes.map((ruleType) => ( +
handleRuleTypeToggle(ruleType.id)} + className={cn( + "relative p-3 rounded-md border cursor-pointer transition-all", + "hover:border-vscode-focusBorder", + ruleType.checked + ? "bg-vscode-list-activeSelectionBackground border-vscode-focusBorder" + : "bg-vscode-editor-background border-vscode-panel-border", + )}> +
+
+ {ruleType.label} + {ruleType.exists && ( + + • + + )} +
+
+ {ruleType.description} +
-
- ))} -
-
- - {hasExistingFiles && ( -
- -
-
{t("settings:rules.overwriteWarning")}
-
    - {existingRules.map((rule) => ( -
  • {rule.label}
  • - ))} -
+ ))}
- )} -
- -
- -
- -
- -
-
-
- - + +
+ )} + +
+ +
+ +
+ +
+ +
+
-
- {isGenerating && ( -

- {t("settings:rules.creatingTaskDescription")} -

- )} - - {generationStatus.type === "success" && ( -
-

{t("settings:rules.taskCreated")}

-

{generationStatus.message}

-
- )} - - {generationStatus.type === "error" && ( -
-

{t("settings:rules.error")}

-

- {t("settings:rules.errorDescription", { error: generationStatus.message })} + {isGenerating && ( +

+ {t("settings:rules.creatingTaskDescription")}

-
- )} + )} + + {generationStatus.type === "success" && ( +
+

{t("settings:rules.taskCreated")}

+

{generationStatus.message}

+
+ )} + + {generationStatus.type === "error" && ( +
+

{t("settings:rules.error")}

+

+ {t("settings:rules.errorDescription", { error: generationStatus.message })} +

+
+ )} +
diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index fc58c4f659..be21a3ff3a 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -604,7 +604,11 @@ }, "rules": { "title": "Rules", - "description": "Configure automatic rules generation for your codebase. Rules help AI agents understand your project's conventions and best practices.", + "description": "Rules help AI agents understand your project's conventions and best practices!", + "magicGeneration": { + "title": "Magic Rules Generation", + "description": "Automatically analyze your codebase and generate comprehensive rules tailored to your project's specific needs." + }, "generateButton": "Generate Rules", "generateButtonTooltip": "Create a new task to analyze the codebase and generate rules automatically", "generating": "Creating task...", @@ -649,9 +653,8 @@ "addToGitignore": "Add to .gitignore", "addToGitignoreDescription": "Automatically add the generated rule files to .gitignore to prevent them from being committed to version control", "autoApproveProtected": "Auto-approve protected file writes", - "autoApproveProtectedDescription": "Allow Roo to write to .roo directory without requiring manual approval", + "autoApproveProtectedDescription": "Allow Roo to make and write to .roo directory without requiring manual approval", "autoApproveRecommendation": "For the best experience generating rules, we recommend enabling auto-approve for both read and write operations in the Auto-Approve settings above.", - "apiConfigLabel": "API Configuration", "selectApiConfig": "Select API configuration" }, "promptCaching": {