From bae0f78825ccbaae23e80f30065079b31d23f12d Mon Sep 17 00:00:00 2001 From: Roo Code Date: Fri, 18 Jul 2025 17:33:23 +0000 Subject: [PATCH] fix: comprehensive Windows support for Claude Code integration - Add cmd.exe wrapper for Windows command execution to handle PowerShell scripts - Improve response parsing with better error handling and validation - Add comprehensive Windows-specific error messages for common issues - Add tests for Windows command execution scenarios - Addresses issues mentioned in PR comments about Linux-specific commands and parsing problems --- .../claude-code/__tests__/run.spec.ts | 104 ++++++++++++++++++ src/integrations/claude-code/run.ts | 75 +++++++++++-- 2 files changed, 168 insertions(+), 11 deletions(-) diff --git a/src/integrations/claude-code/__tests__/run.spec.ts b/src/integrations/claude-code/__tests__/run.spec.ts index a538e45214..2809528372 100644 --- a/src/integrations/claude-code/__tests__/run.spec.ts +++ b/src/integrations/claude-code/__tests__/run.spec.ts @@ -492,4 +492,108 @@ describe("runClaudeCode", () => { const systemPromptArg = args[systemPromptIndex + 1] expect(systemPromptArg).not.toMatch(/^@/) }) + + test("should use cmd.exe wrapper on Windows for command execution", async () => { + const { runClaudeCode } = await import("../run") + + // Mock process.platform to simulate Windows + const originalPlatform = process.platform + Object.defineProperty(process, "platform", { + value: "win32", + }) + + const options = { + systemPrompt: "You are a helpful assistant", + messages: [{ role: "user" as const, content: "Hello" }], + path: "claude", + } + + const generator = runClaudeCode(options) + + // Consume at least one item to trigger process spawn + await generator.next() + + // Clean up the generator + await generator.return(undefined) + + // Verify execa was called with cmd.exe wrapper on Windows + const [command, args] = mockExeca.mock.calls[0] + expect(command).toBe("cmd.exe") + expect(args).toEqual(expect.arrayContaining(["/c", "claude"])) + expect(args).toContain("-p") + + // Restore original platform + Object.defineProperty(process, "platform", { + value: originalPlatform, + }) + }) + + test("should not double-wrap cmd.exe on Windows", async () => { + const { runClaudeCode } = await import("../run") + + // Mock process.platform to simulate Windows + const originalPlatform = process.platform + Object.defineProperty(process, "platform", { + value: "win32", + }) + + const options = { + systemPrompt: "You are a helpful assistant", + messages: [{ role: "user" as const, content: "Hello" }], + path: "cmd.exe", // Already cmd.exe + } + + const generator = runClaudeCode(options) + + // Consume at least one item to trigger process spawn + await generator.next() + + // Clean up the generator + await generator.return(undefined) + + // Verify execa was called without double-wrapping + const [command, args] = mockExeca.mock.calls[0] + expect(command).toBe("cmd.exe") + expect(args).not.toContain("/c") // Should not have /c since it's already cmd.exe + + // Restore original platform + Object.defineProperty(process, "platform", { + value: originalPlatform, + }) + }) + + test("should not use cmd.exe wrapper on non-Windows platforms", async () => { + const { runClaudeCode } = await import("../run") + + // Mock process.platform to simulate Linux/macOS + const originalPlatform = process.platform + Object.defineProperty(process, "platform", { + value: "linux", + }) + + const options = { + systemPrompt: "You are a helpful assistant", + messages: [{ role: "user" as const, content: "Hello" }], + path: "claude", + } + + const generator = runClaudeCode(options) + + // Consume at least one item to trigger process spawn + await generator.next() + + // Clean up the generator + await generator.return(undefined) + + // Verify execa was called directly without cmd.exe wrapper + const [command, args] = mockExeca.mock.calls[0] + expect(command).toBe("claude") + expect(args).not.toContain("/c") + expect(args).toContain("-p") + + // Restore original platform + Object.defineProperty(process, "platform", { + value: originalPlatform, + }) + }) }) diff --git a/src/integrations/claude-code/run.ts b/src/integrations/claude-code/run.ts index 564e5a7e2d..a3f400de6b 100644 --- a/src/integrations/claude-code/run.ts +++ b/src/integrations/claude-code/run.ts @@ -82,9 +82,24 @@ export async function* runClaudeCode( const { exitCode } = await process if (exitCode !== null && exitCode !== 0) { const errorOutput = processState.error?.message || processState.stderrLogs?.trim() - throw new Error( - `Claude Code process exited with code ${exitCode}.${errorOutput ? ` Error output: ${errorOutput}` : ""}`, - ) + + // Provide more specific error messages for common Windows issues + let errorMessage = `Claude Code process exited with code ${exitCode}` + + if (errorOutput) { + // Check for common Windows-specific errors + if (errorOutput.includes("ENAMETOOLONG") || errorOutput.includes("command line too long")) { + errorMessage += ". This appears to be a Windows command line length issue. Try using a shorter system prompt or ensure the Claude CLI is properly installed." + } else if (errorOutput.includes("is not recognized as an internal or external command")) { + errorMessage += ". The 'claude' command was not found. Please ensure the Claude CLI is installed and available in your PATH." + } else if (errorOutput.includes("Access is denied") || errorOutput.includes("EACCES")) { + errorMessage += ". Access denied. Please check file permissions and ensure the Claude CLI has proper execution rights." + } else { + errorMessage += `. Error output: ${errorOutput}` + } + } + + throw new Error(errorMessage) } } finally { rl.close() @@ -140,7 +155,7 @@ async function runProcess({ // Check if system prompt is too long for command line const useTempFileForSystemPrompt = systemPrompt.length > MAX_COMMAND_LINE_LENGTH - const args = ["-p"] + const baseArgs = ["-p"] let tempFileCleanup: TempFileCleanup | null = null // Handle system prompt - use temp file for long prompts, command line for short ones @@ -153,7 +168,7 @@ async function runProcess({ try { await vscode.workspace.fs.writeFile(vscode.Uri.file(tempFilePath), Buffer.from(systemPrompt, "utf8")) - args.push("--system-prompt", `@${tempFilePath}`) + baseArgs.push("--system-prompt", `@${tempFilePath}`) tempFileCleanup = { filePath: tempFilePath, @@ -171,10 +186,10 @@ async function runProcess({ } } else { // Use command line argument for short system prompts - args.push("--system-prompt", systemPrompt) + baseArgs.push("--system-prompt", systemPrompt) } - args.push( + baseArgs.push( "--verbose", "--output-format", "stream-json", @@ -186,9 +201,21 @@ async function runProcess({ ) if (modelId) { - args.push("--model", modelId) + baseArgs.push("--model", modelId) } + // On Windows, wrap commands with cmd.exe to handle non-exe executables like npx.ps1 + // This is necessary for node version managers (fnm, nvm-windows, volta) that implement + // commands as PowerShell scripts rather than executables. + // This pattern is used in McpHub.ts for MCP servers and resolves Windows execution issues. + const isWindows = process.platform === "win32" + + // Check if command is already cmd.exe to avoid double-wrapping + const isAlreadyWrapped = claudePath.toLowerCase() === "cmd.exe" || claudePath.toLowerCase() === "cmd" + + const command = isWindows && !isAlreadyWrapped ? "cmd.exe" : claudePath + const args = isWindows && !isAlreadyWrapped ? ["/c", claudePath, ...baseArgs] : baseArgs + const env: Record = { ...process.env, // Use the configured value, or the environment variable, or default to CLAUDE_CODE_DEFAULT_MAX_OUTPUT_TOKENS @@ -198,7 +225,7 @@ async function runProcess({ CLAUDE_CODE_DEFAULT_MAX_OUTPUT_TOKENS.toString(), } - const child = execa(claudePath, args, { + const child = execa(command, args, { stdin: "pipe", stdout: "pipe", stderr: "pipe", @@ -260,9 +287,35 @@ function parseChunk(data: string, processState: ProcessState) { function attemptParseChunk(data: string): ClaudeCodeMessage | null { try { - return JSON.parse(data) + // Trim whitespace and ensure we have valid JSON + const trimmedData = data.trim() + if (!trimmedData) { + return null + } + + // Check if the data looks like JSON (starts with { and ends with }) + if (!trimmedData.startsWith("{") || !trimmedData.endsWith("}")) { + console.warn("Received non-JSON data from Claude Code:", trimmedData.substring(0, 100)) + return null + } + + const parsed = JSON.parse(trimmedData) + + // Validate that the parsed object has the expected structure + if (!parsed || typeof parsed !== "object" || !parsed.type) { + console.warn("Parsed data missing required 'type' field:", parsed) + return null + } + + return parsed as ClaudeCodeMessage } catch (error) { - console.error("Error parsing chunk:", error, data.length) + // Log more detailed error information for debugging + console.error("Error parsing chunk from Claude Code:", { + error: error instanceof Error ? error.message : String(error), + dataLength: data.length, + dataPreview: data.substring(0, 200), + dataEnding: data.length > 200 ? data.substring(data.length - 50) : "", + }) return null } }