refactor: remove custom executable support and simplify Claude CLI resolution

Remove support for custom executable paths and bundled Claude versions, including
deprecated environment variables HAPI_USE_BUNDLED_CLAUDE and HAPI_USE_GLOBAL_CLAUDE.
This simplifies the codebase to rely only on the global Claude CLI installation.

Updates Claude Code spawning logic to remove special handling for .js/.cjs files,
adds configurable BUN_BE_BUN environment variable handling, and streamlines path
resolution to throw an error when Claude Code CLI is not found on PATH.
This commit is contained in:
weishu
2025-12-24 13:20:06 +08:00
parent 8f804c57bf
commit 1a23bfa430
7 changed files with 34 additions and 82 deletions
+1 -1
View File
@@ -80,7 +80,7 @@ export async function claudeLocal(opts: {
stdio: ['inherit', 'inherit', 'inherit'],
signal: opts.abort,
cwd: opts.path,
env: withBunRuntimeEnv(env),
env: withBunRuntimeEnv(env, { allowBunBeBun: false }),
shell: process.platform === 'win32'
});
let settled = false;
-1
View File
@@ -120,7 +120,6 @@ export async function claudeRemote(opts: {
allowedTools: initial.mode.allowedTools ? initial.mode.allowedTools.concat(opts.allowedTools) : opts.allowedTools,
disallowedTools: initial.mode.disallowedTools,
canCallTool: (toolName: string, input: unknown, options: { signal: AbortSignal }) => opts.canCallTool(toolName, input, mode, options),
executable: process.execPath,
abort: opts.signal,
pathToClaudeCodeExecutable: 'claude',
settingsPath: opts.hookSettingsPath,
+6 -12
View File
@@ -263,8 +263,6 @@ export function query(config: {
customSystemPrompt,
cwd,
disallowedTools = [],
executable = process.execPath,
executableArgs = [],
maxTurns,
mcpServers,
pathToClaudeCodeExecutable = getDefaultClaudeCodePath(),
@@ -323,10 +321,8 @@ export function query(config: {
}
// Determine how to spawn Claude Code
// - If it's a .js/.cjs file → spawn(current runtime, [path, ...args])
// - If it's just 'claude' command → spawn('claude', args) with shell on Windows
// - If it's a full path to binary → spawn(path, args)
const isJsFile = pathToClaudeCodeExecutable.endsWith('.js') || pathToClaudeCodeExecutable.endsWith('.cjs')
// - If it's a full path to binary or script → spawn(path, args)
const isCommandOnly = pathToClaudeCodeExecutable === 'claude'
// Validate executable path (skip for command-only mode)
@@ -334,15 +330,13 @@ export function query(config: {
throw new ReferenceError(`Claude Code executable not found at ${pathToClaudeCodeExecutable}. Is options.pathToClaudeCodeExecutable set?`)
}
const spawnCommand = isJsFile ? executable : pathToClaudeCodeExecutable
const spawnArgs = isJsFile
? [...executableArgs, pathToClaudeCodeExecutable, ...args]
: args
const spawnCommand = pathToClaudeCodeExecutable
const spawnArgs = args
// Spawn Claude Code process
// Use clean env for global claude to avoid local node_modules/.bin taking precedence
const baseEnv = isCommandOnly ? getCleanEnv() : process.env
const spawnEnv = withBunRuntimeEnv(baseEnv)
const spawnEnv = withBunRuntimeEnv(baseEnv, { allowBunBeBun: false })
logDebug(`Spawning Claude Code process: ${spawnCommand} ${spawnArgs.join(' ')} (using ${isCommandOnly ? 'clean' : 'normal'} env)`)
const child = spawn(spawnCommand, spawnArgs, {
@@ -350,8 +344,8 @@ export function query(config: {
stdio: ['pipe', 'pipe', 'pipe'],
signal: config.options?.abort,
env: spawnEnv,
// Use shell on Windows for global binaries and command-only mode
shell: !isJsFile && process.platform === 'win32'
// Use shell on Windows for command resolution
shell: process.platform === 'win32'
}) as ChildProcessWithoutNullStreams
// Handle stdin
-2
View File
@@ -161,8 +161,6 @@ export interface QueryOptions {
customSystemPrompt?: string
cwd?: string
disallowedTools?: string[]
executable?: string
executableArgs?: string[]
maxTurns?: number
mcpServers?: Record<string, unknown>
pathToClaudeCodeExecutable?: string
+5 -63
View File
@@ -3,41 +3,11 @@
* Provides helper functions for path resolution and logging
*/
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { existsSync, readFileSync } from 'node:fs'
import { existsSync } from 'node:fs'
import { execSync } from 'node:child_process'
import { homedir } from 'node:os'
import { logger } from '@/ui/logger'
/**
* Get the directory path of the current module
*/
const __filename = fileURLToPath(import.meta.url)
const __dirname = join(__filename, '..')
/**
* Get version of globally installed claude
* Runs from home directory with clean PATH to avoid picking up local node_modules/.bin
*/
function getGlobalClaudeVersion(): string | null {
try {
const cleanEnv = getCleanEnv()
const output = execSync('claude --version', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
cwd: homedir(),
env: cleanEnv
}).trim()
// Output format: "2.0.54 (Claude Code)" or similar
const match = output.match(/(\d+\.\d+\.\d+)/)
logger.debug(`[Claude SDK] Global claude --version output: ${output}`)
return match ? match[1] : null
} catch {
return null
}
}
/**
* Create a clean environment without local node_modules/.bin in PATH
* This ensures we find the global claude, not the local one
@@ -114,51 +84,23 @@ function findGlobalClaudePath(): string | null {
}
/**
* Get default path to Claude Code executable
* Compares global and bundled versions, uses the newer one
*
* Get default path to Claude Code executable.
*
* Environment variables:
* - HAPI_CLAUDE_PATH: Force a specific path to claude executable
* - HAPI_USE_BUNDLED_CLAUDE=1: Force use of node_modules version (skip global search)
* - HAPI_USE_GLOBAL_CLAUDE=1: Force use of global version (if available)
*/
export function getDefaultClaudeCodePath(): string {
const nodeModulesPath = join(__dirname, '..', '..', '..', 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js')
// Allow explicit override via env var
if (process.env.HAPI_CLAUDE_PATH) {
logger.debug(`[Claude SDK] Using HAPI_CLAUDE_PATH: ${process.env.HAPI_CLAUDE_PATH}`)
return process.env.HAPI_CLAUDE_PATH
}
// Force bundled version if requested
if (process.env.HAPI_USE_BUNDLED_CLAUDE === '1') {
logger.debug(`[Claude SDK] Forced bundled version: ${nodeModulesPath}`)
return nodeModulesPath
}
// Find global claude
const globalPath = findGlobalClaudePath()
// No global claude found - use bundled
if (!globalPath) {
logger.debug(`[Claude SDK] No global claude found, using bundled: ${nodeModulesPath}`)
return nodeModulesPath
throw new Error('Claude Code CLI not found on PATH. Install Claude Code or set HAPI_CLAUDE_PATH.')
}
// Compare versions and use the newer one
const globalVersion = getGlobalClaudeVersion()
logger.debug(`[Claude SDK] Global version: ${globalVersion || 'unknown'}`)
// If we can't determine versions, prefer global (user's choice to install it)
if (!globalVersion) {
logger.debug(`[Claude SDK] Cannot compare versions, using global: ${globalPath}`)
return globalPath
}
return globalPath
}
@@ -185,4 +127,4 @@ export async function streamToStdin(
stdin.write(JSON.stringify(message) + '\n')
}
stdin.end()
}
}
+22 -1
View File
@@ -1,4 +1,21 @@
export function withBunRuntimeEnv(env: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
export type BunRuntimeEnvOptions = {
allowBunBeBun?: boolean;
};
function stripBunBeBun(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
if (!('BUN_BE_BUN' in env)) {
return env;
}
const copy = { ...env };
delete copy.BUN_BE_BUN;
return copy;
}
export function withBunRuntimeEnv(
env: NodeJS.ProcessEnv = process.env,
options: BunRuntimeEnvOptions = {}
): NodeJS.ProcessEnv {
const bunRuntime = (globalThis as typeof globalThis & { Bun?: { isCompiled?: boolean } }).Bun;
const argv1 = process.argv[1] ?? '';
const isCompiled = Boolean(bunRuntime?.isCompiled) || argv1.includes('$bunfs');
@@ -7,6 +24,10 @@ export function withBunRuntimeEnv(env: NodeJS.ProcessEnv = process.env): NodeJS.
return env;
}
if (options.allowBunBeBun === false) {
return stripBunBeBun(env);
}
return {
...env,
BUN_BE_BUN: '1'