diff --git a/.roo/package-lock.json b/.roo/package-lock.json new file mode 100644 index 0000000000..40b73f9bf9 --- /dev/null +++ b/.roo/package-lock.json @@ -0,0 +1,62 @@ +{ + "name": "roo-prepare", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "roo-prepare", + "version": "1.0.0", + "dependencies": { + "commander": "^11.1.0" + }, + "bin": { + "prepare": "dist/prepare-cli.js" + }, + "devDependencies": { + "@types/node": "^20.11.0", + "typescript": "^5.3.3" + } + }, + "node_modules/@types/node": { + "version": "20.17.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.30.tgz", + "integrity": "sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/.roo/package.json b/.roo/package.json new file mode 100644 index 0000000000..ca90aef68f --- /dev/null +++ b/.roo/package.json @@ -0,0 +1,21 @@ +{ + "name": "roo-prepare", + "version": "1.0.0", + "description": "Prepare for commit task management system", + "private": true, + "bin": { + "prepare": "./dist/prepare-cli.js" + }, + "scripts": { + "build": "tsc", + "prepare": "npm run build", + "start": "node ./dist/prepare-cli.js" + }, + "dependencies": { + "commander": "^11.1.0" + }, + "devDependencies": { + "@types/node": "^20.11.0", + "typescript": "^5.3.3" + } +} diff --git a/.roo/prepare-cli.ts b/.roo/prepare-cli.ts new file mode 100644 index 0000000000..919f2ba458 --- /dev/null +++ b/.roo/prepare-cli.ts @@ -0,0 +1,148 @@ +#!/usr/bin/env node +import { program } from "commander" +import { taskManager } from "./task-manager" +import * as fs from "fs/promises" +import * as path from "path" +import { execSync } from "child_process" + +program.name("prepare").description("CLI to manage prepare for commit tasks") + +program + .command("create ") + .description("Create a new task") + .requiredOption("-d, --description ", "Task description") + .action(async (taskId: string, options: { description: string }) => { + try { + const initialCommit = execSync("git rev-parse HEAD").toString().trim() + await taskManager.createTask(taskId, options.description, initialCommit) + console.log(`Created task: ${taskId}`) + } catch (error) { + console.error("Failed to create task:", error) + process.exit(1) + } + }) + +program + .command("switch ") + .description("Switch to a different task") + .action(async (taskId: string) => { + try { + await taskManager.switchTask(taskId) + const task = taskManager.getCurrentTask() + console.log(`Switched to task: ${taskId}`) + console.log("Current checkpoint:", task?.active_checkpoint) + if (task?.pending_decisions.length) { + console.log("\nPending decisions:") + task.pending_decisions.forEach((decision, i) => { + console.log(`${i + 1}. ${decision}`) + }) + } + } catch (error) { + console.error("Failed to switch task:", error) + process.exit(1) + } + }) + +program + .command("list") + .description("List all tasks") + .action(async () => { + try { + const tasks = await taskManager.listTasks() + console.log("Available tasks:") + for (const taskId of tasks) { + const content = await fs.readFile(path.join(".roo", "prepare_logs", `${taskId}.json`), "utf-8") + const task = JSON.parse(content) + console.log(`- ${taskId}: ${task.description} (${task.status})`) + } + } catch (error) { + console.error("Failed to list tasks:", error) + process.exit(1) + } + }) + +program + .command("status") + .description("Show current task status") + .action(() => { + const task = taskManager.getCurrentTask() + if (!task) { + console.log("No active task") + return + } + + console.log(`Current task: ${task.task_id}`) + console.log(`Description: ${task.description}`) + console.log(`Status: ${task.status}`) + console.log(`Active checkpoint: ${task.active_checkpoint}`) + + if (task.pending_decisions.length) { + console.log("\nPending decisions:") + task.pending_decisions.forEach((decision, i) => { + console.log(`${i + 1}. ${decision}`) + }) + } + + console.log("\nTest results:") + console.log( + `- Unit tests: ${task.test_results.unit_tests.passing} passing, ${task.test_results.unit_tests.failing} failing`, + ) + console.log(`- Linting: ${task.test_results.linting}`) + console.log(`- Manual testing: ${task.test_results.manual_testing}`) + + console.log("\nRollback info:") + console.log(`Full rollback: ${task.rollback_info.full_rollback}`) + console.log("Partial rollbacks:") + Object.entries(task.rollback_info.partial_rollbacks).forEach(([name, command]) => { + console.log(`- ${name}: ${command}`) + }) + }) + +program + .command("checkpoint") + .description("Create a new checkpoint") + .requiredOption("-d, --description ", "Checkpoint description") + .requiredOption("-c, --component ", "Component being modified") + .requiredOption("--changes ", "List of changes") + .requiredOption("--risks ", "List of risks") + .requiredOption("--feedback ", "Expected user feedback") + .action(async (options) => { + try { + const commitHash = execSync("git rev-parse HEAD").toString().trim() + const task = taskManager.getCurrentTask() + if (!task) throw new Error("No active task") + + const checkpoint = { + id: `${task.task_id}_${task.checkpoints.length + 1}`, + commit_hash: commitHash, + description: options.description, + component: options.component, + changes: options.changes, + risks: options.risks, + expected_feedback: options.feedback, + timestamp: new Date().toISOString(), + } + + await taskManager.addCheckpoint(checkpoint) + console.log(`Created checkpoint: ${checkpoint.id}`) + } catch (error) { + console.error("Failed to create checkpoint:", error) + process.exit(1) + } + }) + +program + .command("decide ") + .description("Resolve a pending decision") + .action(async (index: string) => { + try { + const idx = parseInt(index, 10) - 1 + await taskManager.resolvePendingDecision(idx) + console.log("Decision resolved") + } catch (error) { + console.error("Failed to resolve decision:", error) + process.exit(1) + } + }) + +program.parse() diff --git a/.roo/prepare_logs/PM-CLEANUP-20250412.json b/.roo/prepare_logs/PM-CLEANUP-20250412.json new file mode 100644 index 0000000000..245dff1166 --- /dev/null +++ b/.roo/prepare_logs/PM-CLEANUP-20250412.json @@ -0,0 +1,24 @@ +{ + "task_id": "PM-CLEANUP-20250412", + "active_checkpoint": "", + "status": "in_progress", + "created_at": "2025-04-12T23:57:44.419Z", + "last_accessed": "2025-04-12T23:57:44.420Z", + "description": "Remove unused YamlParser implementation", + "initial_commit": "4417886324a54ad5c058813474b8a57a9859bba0", + "checkpoints": [], + "test_results": { + "unit_tests": { + "passing": 0, + "failing": 0, + "pending": 0 + }, + "linting": "", + "manual_testing": "" + }, + "pending_decisions": [], + "rollback_info": { + "full_rollback": "git reset --hard 4417886324a54ad5c058813474b8a57a9859bba0", + "partial_rollbacks": {} + } +} diff --git a/.roo/prepare_logs/PM-STATE-FIX-20250412.json b/.roo/prepare_logs/PM-STATE-FIX-20250412.json new file mode 100644 index 0000000000..4682d732ef --- /dev/null +++ b/.roo/prepare_logs/PM-STATE-FIX-20250412.json @@ -0,0 +1,63 @@ +{ + "task_id": "PM-STATE-FIX-20250412", + "active_checkpoint": "pm_state_fix_20250412_3", + "status": "in_progress", + "created_at": "2025-04-12T15:44:13-07:00", + "last_accessed": "2025-04-12T15:50:47-07:00", + "description": "Fix package manager state management and refresh issues", + "initial_commit": "4417886324a54ad5c058813474b8a57a9859bba0", + "checkpoints": [ + { + "id": "pm_state_fix_20250412_1", + "commit_hash": "4417886324a54ad5c058813474b8a57a9859bba0", + "description": "UI State Management", + "component": "webview-ui/src/components/package-manager/PackageManagerView.tsx", + "changes": ["Removed premature item clearing", "Fixed state update timing"], + "risks": ["Race conditions between state updates", "Stale data display during refresh"], + "expected_feedback": [ + "Items disappear and reappear during refresh", + "Refresh button gets stuck spinning", + "Old items shown after source changes" + ], + "timestamp": "2025-04-12T15:44:13-07:00" + }, + { + "id": "pm_state_fix_20250412_2", + "commit_hash": "4417886324a54ad5c058813474b8a57a9859bba0", + "description": "Error Handling", + "component": "webview-ui/src/components/package-manager/PackageManagerView.tsx", + "changes": ["Removed client-side error messages", "Improved timeout handling"], + "risks": ["Missing error feedback", "Timeout state confusion"], + "expected_feedback": ["No error message shown on failure", "UI stuck in loading state"], + "timestamp": "2025-04-12T15:45:00-07:00" + }, + { + "id": "pm_state_fix_20250412_3", + "commit_hash": "4417886324a54ad5c058813474b8a57a9859bba0", + "description": "State Reset Logic", + "component": "webview-ui/src/components/package-manager/PackageManagerView.tsx", + "changes": ["Always update items on state change", "Proper timeout cleanup"], + "risks": ["Memory leaks from timeouts", "Inconsistent state after tab switch"], + "expected_feedback": ["Items don't update after source changes", "Refresh button state incorrect"], + "timestamp": "2025-04-12T15:45:30-07:00" + } + ], + "test_results": { + "unit_tests": { + "passing": 1263, + "failing": 0, + "pending": 23 + }, + "linting": "No errors", + "manual_testing": "Confirmed working by user" + }, + "pending_decisions": [], + "rollback_info": { + "full_rollback": "git reset --hard 4417886324a54ad5c058813474b8a57a9859bba0", + "partial_rollbacks": { + "ui_state": "git checkout pm_state_fix_20250412_1", + "error_handling": "git checkout pm_state_fix_20250412_2", + "state_reset": "git checkout pm_state_fix_20250412_3" + } + } +} diff --git a/.roo/prepare_logs/task_4d3f7a2e-8c1b-4f9d-b5e2-9c1d8f3a6b4d.json b/.roo/prepare_logs/task_4d3f7a2e-8c1b-4f9d-b5e2-9c1d8f3a6b4d.json new file mode 100644 index 0000000000..9d3bd3a402 --- /dev/null +++ b/.roo/prepare_logs/task_4d3f7a2e-8c1b-4f9d-b5e2-9c1d8f3a6b4d.json @@ -0,0 +1,56 @@ +{ + "task_id": "4d3f7a2e-8c1b-4f9d-b5e2-9c1d8f3a6b4d", + "description": "Remove unused YamlParser implementation", + "created_at": "2025-04-12T17:07:22-07:00", + "checkpoints": [ + { + "id": "checkpoint_1", + "description": "Initial analysis of YamlParser removal", + "findings": [ + "YamlParser.ts exists but is not imported anywhere", + "No test files are using it", + "No dynamic imports found" + ], + "proposed_changes": [ + "Remove src/services/package-manager/YamlParser.ts", + "Remove any associated test files" + ], + "risks": [ + "Might be used by dynamic imports", + "Could be referenced in package.json", + "Might be part of public API" + ], + "expected_feedback": [ + "Build errors after removal", + "Runtime errors in yaml parsing", + "Missing exports errors" + ] + }, + { + "id": "checkpoint_2", + "description": "Removal of YamlParser files", + "completed_at": "2025-04-12T17:25:29-07:00", + "changes_made": [ + "Removed src/services/package-manager/YamlParser.ts", + "Verified no test file existed to remove" + ], + "verification_steps": ["Confirmed file deletion", "Confirmed no test file present"] + }, + { + "id": "checkpoint_3", + "description": "Test verification", + "completed_at": "2025-04-12T17:27:39-07:00", + "test_results": { + "total": 1286, + "passing": 1263, + "failing": 0, + "pending": 23 + }, + "conclusion": "No regressions detected after removing YamlParser.ts" + } + ], + "current_state": { + "status": "completed", + "summary": "Successfully removed unused YamlParser.ts with no regressions" + } +} diff --git a/.roo/task-manager.ts b/.roo/task-manager.ts new file mode 100644 index 0000000000..67ab2b656f --- /dev/null +++ b/.roo/task-manager.ts @@ -0,0 +1,161 @@ +import * as fs from "fs/promises" +import * as path from "path" + +interface TaskCheckpoint { + id: string + commit_hash: string + description: string + component: string + changes: string[] + risks: string[] + expected_feedback: string[] + timestamp: string +} + +interface TaskContext { + task_id: string + active_checkpoint: string + status: "in_progress" | "completed" | "failed" + created_at: string + last_accessed: string + description: string + initial_commit: string + checkpoints: TaskCheckpoint[] + test_results: { + unit_tests: { + passing: number + failing: number + pending: number + } + linting: string + manual_testing: string + } + pending_decisions: string[] + rollback_info: { + full_rollback: string + partial_rollbacks: Record + } +} + +class TaskManager { + private currentTaskId: string | null = null + private currentContext: TaskContext | null = null + private readonly logsDir = path.join(".roo", "prepare_logs") + + constructor() { + this.ensureLogsDirectory() + } + + private async ensureLogsDirectory() { + try { + await fs.mkdir(this.logsDir, { recursive: true }) + } catch (error) { + console.error("Failed to create logs directory:", error) + } + } + + async switchTask(taskId: string): Promise { + if (this.currentTaskId) { + await this.saveCurrentTask() + } + await this.loadTask(taskId) + } + + private async saveCurrentTask(): Promise { + if (!this.currentTaskId || !this.currentContext) return + + this.currentContext.last_accessed = new Date().toISOString() + const logPath = this.getLogPath(this.currentTaskId) + + await fs.writeFile(logPath, JSON.stringify(this.currentContext, null, 2), "utf-8") + } + + private async loadTask(taskId: string): Promise { + const logPath = this.getLogPath(taskId) + + try { + const content = await fs.readFile(logPath, "utf-8") + this.currentContext = JSON.parse(content) + this.currentTaskId = taskId + } catch (error) { + console.error(`Failed to load task ${taskId}:`, error) + throw error + } + } + + async createTask(taskId: string, description: string, initialCommit: string): Promise { + const newTask: TaskContext = { + task_id: taskId, + active_checkpoint: "", + status: "in_progress", + created_at: new Date().toISOString(), + last_accessed: new Date().toISOString(), + description, + initial_commit: initialCommit, + checkpoints: [], + test_results: { + unit_tests: { passing: 0, failing: 0, pending: 0 }, + linting: "", + manual_testing: "", + }, + pending_decisions: [], + rollback_info: { + full_rollback: `git reset --hard ${initialCommit}`, + partial_rollbacks: {}, + }, + } + + const logPath = this.getLogPath(taskId) + await fs.writeFile(logPath, JSON.stringify(newTask, null, 2), "utf-8") + + this.currentTaskId = taskId + this.currentContext = newTask + } + + async addCheckpoint(checkpoint: TaskCheckpoint): Promise { + if (!this.currentContext) throw new Error("No active task") + + this.currentContext.checkpoints.push(checkpoint) + this.currentContext.active_checkpoint = checkpoint.id + this.currentContext.rollback_info.partial_rollbacks[checkpoint.description.toLowerCase().replace(/\s+/g, "_")] = + `git checkout ${checkpoint.commit_hash}` + + await this.saveCurrentTask() + } + + async updateTestResults(results: TaskContext["test_results"]): Promise { + if (!this.currentContext) throw new Error("No active task") + + this.currentContext.test_results = results + await this.saveCurrentTask() + } + + async addPendingDecision(decision: string): Promise { + if (!this.currentContext) throw new Error("No active task") + + this.currentContext.pending_decisions.push(decision) + await this.saveCurrentTask() + } + + async resolvePendingDecision(index: number): Promise { + if (!this.currentContext) throw new Error("No active task") + + this.currentContext.pending_decisions.splice(index, 1) + await this.saveCurrentTask() + } + + getCurrentTask(): TaskContext | null { + return this.currentContext + } + + private getLogPath(taskId: string): string { + return path.join(this.logsDir, `${taskId}.json`) + } + + async listTasks(): Promise { + const files = await fs.readdir(this.logsDir) + return files.filter((file) => file.endsWith(".json")).map((file) => file.replace(".json", "")) + } +} + +export const taskManager = new TaskManager() diff --git a/.roo/tsconfig.json b/.roo/tsconfig.json new file mode 100644 index 0000000000..6486451502 --- /dev/null +++ b/.roo/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "outDir": "./dist", + "rootDir": ".", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true + }, + "include": ["*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/src/services/package-manager/YamlParser.ts b/src/services/package-manager/YamlParser.ts deleted file mode 100644 index d89624d679..0000000000 --- a/src/services/package-manager/YamlParser.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { XMLParser } from "fast-xml-parser" -import { validateAnyMetadata } from "./schemas" - -/** - * Utility class for parsing and validating YAML content - */ -export class YamlParser { - private static parser = new XMLParser({ - ignoreAttributes: false, - parseAttributeValue: true, - parseTagValue: true, - trimValues: true, - preserveOrder: true, - }) - - /** - * Parse YAML content into an object and validate against schema - * @param content YAML content to parse - * @param validate Whether to validate against schema (default: true) - * @returns Parsed and validated object - * @throws Error if parsing or validation fails - */ - static parse(content: string, validate: boolean = true): T { - if (!content.trim()) { - return {} as T - } - - try { - // Remove comments - const noComments = content.replace(/#[^\n]*/g, "") - - // Handle multi-line strings - const processedContent = this.processMultilineStrings(noComments) - - // Convert YAML to JSON-like structure - const jsonContent = processedContent - // Handle arrays with proper indentation - .replace(/^(\s*)-\s+(?=\S)/gm, (match, indent) => `${indent}array_item: `) - // Handle quoted strings - .replace(/^(\s*)([^:\n]+):\s*(['"])(.*?)\3\s*$/gm, (_, indent, key, quote, value) => { - const safeKey = this.sanitizeKey(key) - return `${indent}${safeKey}: ${value}` - }) - // Handle unquoted key-value pairs - .replace(/^(\s*)([^:\n]+):\s*([^\n]*)$/gm, (_, indent, key, value) => { - const safeKey = this.sanitizeKey(key) - return `${indent}${safeKey}: ${value.trim()}` - }) - - // Parse as XML-like structure - const parsed = this.parser.parse(`${jsonContent}`) - - // Convert array_item markers back to arrays and process nested structures - const result = this.processStructure(parsed.root || {}) - - // Validate against schema if requested - if (validate) { - return validateAnyMetadata(result) as T - } else { - return result as T - } - } catch (error) { - console.error("Failed to parse YAML:", error) - throw new Error(`Failed to parse YAML: ${error instanceof Error ? error.message : String(error)}`) - } - } - - /** - * Process multi-line strings in YAML content - * @param content YAML content - * @returns Processed content - */ - private static processMultilineStrings(content: string): string { - return content.replace(/^(\s*[^:\n]+):\s*\|\s*\n((?:\s+[^\n]*\n?)*)/gm, (_, key, value) => { - const indentLevel = value.match(/^\s+/)?.[0].length || 0 - const processedValue = value - .split("\n") - .map((line: string) => line.slice(indentLevel)) - .join("\n") - .trim() - return `${key}: "${processedValue.replace(/"/g, '\\"')}"` - }) - } - - /** - * Sanitize YAML key for XML compatibility - * @param key Key to sanitize - * @returns Sanitized key - */ - private static sanitizeKey(key: string): string { - return key - .trim() - .replace(/[^\w-]/g, "_") - .replace(/^(\d)/, "_$1") // Prefix numbers with underscore - } - - /** - * Process nested structures and arrays - * @param obj Object to process - * @returns Processed object - */ - private static processStructure(obj: any): any { - if (typeof obj !== "object" || obj === null) { - return obj - } - - if (Array.isArray(obj)) { - return obj.map((item) => this.processStructure(item)) - } - - const result: any = {} - const arrays: { [key: string]: any[] } = {} - - // First pass: collect array items - for (const [key, value] of Object.entries(obj)) { - if (key === "array_item") { - return this.processStructure(value) - } - - const match = key.match(/^(.+?)_(\d+)$/) - if (match) { - const [, baseKey, index] = match - if (!arrays[baseKey]) { - arrays[baseKey] = [] - } - arrays[baseKey][parseInt(index)] = this.processStructure(value) - continue - } - - result[key] = this.processStructure(value) - } - - // Second pass: merge arrays into result - for (const [key, value] of Object.entries(arrays)) { - result[key] = value.filter((item) => item !== undefined) - } - - return result - } -} diff --git a/webview-ui/src/components/package-manager/PackageManagerView.tsx b/webview-ui/src/components/package-manager/PackageManagerView.tsx index 573909e58f..092ee6669a 100644 --- a/webview-ui/src/components/package-manager/PackageManagerView.tsx +++ b/webview-ui/src/components/package-manager/PackageManagerView.tsx @@ -175,8 +175,7 @@ const PackageManagerView: React.FC = ({ onDone }) => { clearTimeout(fetchTimeoutRef.current) } - // Clear items immediately when fetching starts - setItems([]) + // Only set fetching state, don't clear items setIsFetching(true) try { @@ -189,13 +188,10 @@ const PackageManagerView: React.FC = ({ onDone }) => { fetchTimeoutRef.current = setTimeout(() => { console.log("Fetch timeout reached, resetting state") setIsFetching(false) - setItems([]) // Clear items on timeout - vscode.window.showErrorMessage("Package manager items fetch timed out. Please try again.") }, 30000) // 30 second timeout to match server timeout } catch (error) { console.error("Failed to fetch package manager items:", error) setIsFetching(false) - setItems([]) // Clear items on error } }, []) @@ -222,7 +218,6 @@ const PackageManagerView: React.FC = ({ onDone }) => { clearTimeout(fetchTimeoutRef.current) } setIsFetching(false) - setItems([]) // Clear items on error } else { // This is a refresh request fetchPackageManagerItems() @@ -231,8 +226,6 @@ const PackageManagerView: React.FC = ({ onDone }) => { if (message.type === "repositoryRefreshComplete" && message.url) { setRefreshingUrls((prev) => prev.filter((url) => url !== message.url)) - // Trigger a fetch to update items after refresh - fetchPackageManagerItems() } if (message.type === "state" && message.state?.packageManagerItems !== undefined) { @@ -243,6 +236,8 @@ const PackageManagerView: React.FC = ({ onDone }) => { const receivedItems = message.state.packageManagerItems || [] console.log("Received package manager items:", receivedItems.length) + + // Always update items, even if empty setItems([...receivedItems]) setIsFetching(false) }