mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
fix(windows): use absolute path with shell:false for Claude spawn (#143)
On Windows, spawning Claude with shell: true causes the process to exit immediately with code 1. This happens because shell: true invokes cmd.exe /c claude <args>, and cmd.exe's PATH resolution differs from direct process creation, leading to environment inconsistencies. This fix: - Adds findWindowsClaudePath() to locate claude.exe absolute path - Changes spawn to use absolute path with shell: false on Windows - Maintains Unix behavior (command name works fine with shell: false) - Adds HAPI_CLAUDE_PATH env var for user override Tested on Windows 11 with Claude Code v2.1.29 and hapi v0.15.0. via [HAPI](https://hapi.run) Co-authored-by: HAPI <noreply@hapi.run>
This commit is contained in:
@@ -9,6 +9,7 @@ import { withBunRuntimeEnv } from "@/utils/bunRuntime";
|
||||
import { spawnWithAbort } from "@/utils/spawnWithAbort";
|
||||
import { getHapiBlobsDir } from "@/constants/uploadPaths";
|
||||
import { stripNewlinesForWindowsShellArg } from "@/utils/shellEscape";
|
||||
import { getDefaultClaudeCodePath } from "./sdk/utils";
|
||||
|
||||
export async function claudeLocal(opts: {
|
||||
abort: AbortSignal,
|
||||
@@ -84,11 +85,15 @@ export async function claudeLocal(opts: {
|
||||
|
||||
logger.debug(`[ClaudeLocal] Spawning claude with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Get Claude executable path (absolute path on Windows for shell: false)
|
||||
const claudeCommand = getDefaultClaudeCodePath();
|
||||
logger.debug(`[ClaudeLocal] Using claude executable: ${claudeCommand}`);
|
||||
|
||||
// Spawn the process
|
||||
try {
|
||||
process.stdin.pause();
|
||||
await spawnWithAbort({
|
||||
command: 'claude',
|
||||
command: claudeCommand,
|
||||
args,
|
||||
cwd: opts.path,
|
||||
env: withBunRuntimeEnv(env, { allowBunBeBun: false }),
|
||||
@@ -98,7 +103,7 @@ export async function claudeLocal(opts: {
|
||||
installHint: 'Claude CLI',
|
||||
includeCause: true,
|
||||
logExit: true,
|
||||
shell: process.platform === 'win32'
|
||||
shell: false // Use absolute path, no shell needed
|
||||
});
|
||||
} finally {
|
||||
cleanupMcpConfig?.();
|
||||
|
||||
@@ -347,8 +347,9 @@ export function query(config: {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
signal: config.options?.abort,
|
||||
env: spawnEnv,
|
||||
// Use shell on Windows for command resolution
|
||||
shell: process.platform === 'win32'
|
||||
// Use shell: false with absolute path from getDefaultClaudeCodePath()
|
||||
// This avoids cmd.exe resolution issues on Windows
|
||||
shell: false
|
||||
}) as ChildProcessWithoutNullStreams
|
||||
|
||||
// Handle stdin
|
||||
|
||||
+63
-20
@@ -8,19 +8,64 @@ import { execSync } from 'node:child_process'
|
||||
import { homedir } from 'node:os'
|
||||
import { logger } from '@/ui/logger'
|
||||
|
||||
/**
|
||||
* Find Claude executable path on Windows.
|
||||
* Returns absolute path to claude.exe for use with shell: false
|
||||
*/
|
||||
function findWindowsClaudePath(): string | null {
|
||||
const homeDir = homedir()
|
||||
const path = require('node:path')
|
||||
|
||||
// Known installation paths for Claude on Windows
|
||||
const candidates = [
|
||||
path.join(homeDir, '.local', 'bin', 'claude.exe'),
|
||||
path.join(homeDir, 'AppData', 'Local', 'Programs', 'claude', 'claude.exe'),
|
||||
path.join(homeDir, 'AppData', 'Local', 'Microsoft', 'WinGet', 'Packages', 'Anthropic.claude-code_Microsoft.Winget.Source_8wekyb3d8bbwe', 'claude.exe'),
|
||||
]
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (existsSync(candidate)) {
|
||||
logger.debug(`[Claude SDK] Found Windows claude.exe at: ${candidate}`)
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
|
||||
// Try 'where claude' to find in PATH
|
||||
try {
|
||||
const result = execSync('where claude.exe', {
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
cwd: homeDir
|
||||
}).trim().split('\n')[0].trim()
|
||||
if (result && existsSync(result)) {
|
||||
logger.debug(`[Claude SDK] Found Windows claude.exe via where: ${result}`)
|
||||
return result
|
||||
}
|
||||
} catch {
|
||||
// where didn't find it
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to find globally installed Claude CLI
|
||||
* Returns 'claude' if the command works globally (preferred method for reliability)
|
||||
* Falls back to which/where to get actual path on Unix systems
|
||||
* On Windows: Returns absolute path to claude.exe (for shell: false)
|
||||
* On Unix: Returns 'claude' if command works, or actual path via which
|
||||
* Runs from home directory to avoid local cwd side effects
|
||||
*/
|
||||
function findGlobalClaudePath(): string | null {
|
||||
const homeDir = homedir()
|
||||
|
||||
// PRIMARY: Check if 'claude' command works directly from home dir
|
||||
|
||||
// Windows: Always return absolute path for shell: false compatibility
|
||||
if (process.platform === 'win32') {
|
||||
return findWindowsClaudePath()
|
||||
}
|
||||
|
||||
// Unix: Check if 'claude' command works directly from home dir
|
||||
try {
|
||||
execSync('claude --version', {
|
||||
encoding: 'utf8',
|
||||
execSync('claude --version', {
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
cwd: homeDir
|
||||
})
|
||||
@@ -31,22 +76,20 @@ function findGlobalClaudePath(): string | null {
|
||||
}
|
||||
|
||||
// FALLBACK for Unix: try which to get actual path
|
||||
if (process.platform !== 'win32') {
|
||||
try {
|
||||
const result = execSync('which claude', {
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
cwd: homeDir
|
||||
}).trim()
|
||||
if (result && existsSync(result)) {
|
||||
logger.debug(`[Claude SDK] Found global claude path via which: ${result}`)
|
||||
return result
|
||||
}
|
||||
} catch {
|
||||
// which didn't find it
|
||||
try {
|
||||
const result = execSync('which claude', {
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
cwd: homeDir
|
||||
}).trim()
|
||||
if (result && existsSync(result)) {
|
||||
logger.debug(`[Claude SDK] Found global claude path via which: ${result}`)
|
||||
return result
|
||||
}
|
||||
} catch {
|
||||
// which didn't find it
|
||||
}
|
||||
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user