From 1a23bfa43026f15de68d6b57465fb00607fefe53 Mon Sep 17 00:00:00 2001 From: weishu Date: Wed, 24 Dec 2025 13:20:06 +0800 Subject: [PATCH] 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. --- cli/README.md | 2 - cli/src/claude/claudeLocal.ts | 2 +- cli/src/claude/claudeRemote.ts | 1 - cli/src/claude/sdk/query.ts | 18 +++------ cli/src/claude/sdk/types.ts | 2 - cli/src/claude/sdk/utils.ts | 68 +++------------------------------- cli/src/utils/bunRuntime.ts | 23 +++++++++++- 7 files changed, 34 insertions(+), 82 deletions(-) diff --git a/cli/README.md b/cli/README.md index 40b71f6d..a5f46589 100644 --- a/cli/README.md +++ b/cli/README.md @@ -44,8 +44,6 @@ Optional: - `HAPI_EXPERIMENTAL` - enable experimental features (true/1/yes) - `HAPI_HTTP_MCP_URL` - default MCP target for `hapi mcp` - `HAPI_CLAUDE_PATH` - path to a specific `claude` executable -- `HAPI_USE_BUNDLED_CLAUDE` - set to 1 to prefer node_modules claude -- `HAPI_USE_GLOBAL_CLAUDE` - set to 1 to prefer global claude ## Requirements - Claude CLI installed and logged in (`claude` on PATH). diff --git a/cli/src/claude/claudeLocal.ts b/cli/src/claude/claudeLocal.ts index 5bd70e07..63db54d9 100644 --- a/cli/src/claude/claudeLocal.ts +++ b/cli/src/claude/claudeLocal.ts @@ -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; diff --git a/cli/src/claude/claudeRemote.ts b/cli/src/claude/claudeRemote.ts index 023b4c65..329e174f 100644 --- a/cli/src/claude/claudeRemote.ts +++ b/cli/src/claude/claudeRemote.ts @@ -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, diff --git a/cli/src/claude/sdk/query.ts b/cli/src/claude/sdk/query.ts index 3aa9dc0b..816a1bed 100644 --- a/cli/src/claude/sdk/query.ts +++ b/cli/src/claude/sdk/query.ts @@ -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 diff --git a/cli/src/claude/sdk/types.ts b/cli/src/claude/sdk/types.ts index f0730cb2..a6d11563 100644 --- a/cli/src/claude/sdk/types.ts +++ b/cli/src/claude/sdk/types.ts @@ -161,8 +161,6 @@ export interface QueryOptions { customSystemPrompt?: string cwd?: string disallowedTools?: string[] - executable?: string - executableArgs?: string[] maxTurns?: number mcpServers?: Record pathToClaudeCodeExecutable?: string diff --git a/cli/src/claude/sdk/utils.ts b/cli/src/claude/sdk/utils.ts index 8b525dc7..c8541233 100644 --- a/cli/src/claude/sdk/utils.ts +++ b/cli/src/claude/sdk/utils.ts @@ -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() -} \ No newline at end of file +} diff --git a/cli/src/utils/bunRuntime.ts b/cli/src/utils/bunRuntime.ts index 0c1bd43b..eb86101d 100644 --- a/cli/src/utils/bunRuntime.ts +++ b/cli/src/utils/bunRuntime.ts @@ -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'