diff --git a/cli/scripts/claude_local_launcher.cjs b/cli/scripts/claude_local_launcher.cjs deleted file mode 100644 index e4e55647..00000000 --- a/cli/scripts/claude_local_launcher.cjs +++ /dev/null @@ -1,7 +0,0 @@ -// Disable autoupdater -process.env.DISABLE_AUTOUPDATER = '1'; - -// Import global Claude Code CLI -const { getClaudeCliPath, runClaudeCli } = require('./claude_version_utils.cjs'); - -runClaudeCli(getClaudeCliPath()); diff --git a/cli/scripts/claude_remote_launcher.cjs b/cli/scripts/claude_remote_launcher.cjs deleted file mode 100644 index a581ca97..00000000 --- a/cli/scripts/claude_remote_launcher.cjs +++ /dev/null @@ -1,16 +0,0 @@ -// Intercept setTimeout for the Claude Code SDK -const originalSetTimeout = global.setTimeout; - -global.setTimeout = function(callback, delay, ...args) { - // Just wrap and call the original setTimeout - return originalSetTimeout(callback, delay, ...args); -}; - -// Preserve setTimeout properties -Object.defineProperty(global.setTimeout, 'name', { value: 'setTimeout' }); -Object.defineProperty(global.setTimeout, 'length', { value: originalSetTimeout.length }); - -// Import global Claude Code CLI -const { getClaudeCliPath, runClaudeCli } = require('./claude_version_utils.cjs'); - -runClaudeCli(getClaudeCliPath()); \ No newline at end of file diff --git a/cli/scripts/claude_version_utils.cjs b/cli/scripts/claude_version_utils.cjs deleted file mode 100644 index 087e897c..00000000 --- a/cli/scripts/claude_version_utils.cjs +++ /dev/null @@ -1,403 +0,0 @@ -/** - * Shared utilities for finding and resolving Claude Code CLI path - * Used by both local and remote launchers - * - * Supports multiple installation methods: - * 1. npm global: npm install -g @anthropic-ai/claude-code - * 2. Homebrew: brew install claude-code - * 3. Native installer: - * - macOS/Linux: curl -fsSL https://claude.ai/install.sh | bash - * - PowerShell: irm https://claude.ai/install.ps1 | iex - * - Windows CMD: curl -fsSL https://claude.ai/install.cmd | cmd - * 4. PATH fallback: bun, pnpm, or any other package manager - */ - -const { execSync } = require('child_process'); -const path = require('path'); -const fs = require('fs'); -const os = require('os'); - -/** - * Safely resolve symlink or return path if it exists - * @param {string} filePath - Path to resolve - * @returns {string|null} Resolved path or null if not found - */ -function resolvePathSafe(filePath) { - if (!fs.existsSync(filePath)) return null; - try { - return fs.realpathSync(filePath); - } catch (e) { - // Symlink resolution failed, return original path - return filePath; - } -} - -/** - * Find path to npm globally installed Claude Code CLI - * @returns {string|null} Path to cli.js or null if not found - */ -function findNpmGlobalCliPath() { - try { - const globalRoot = execSync('npm root -g', { encoding: 'utf8' }).trim(); - const globalCliPath = path.join(globalRoot, '@anthropic-ai', 'claude-code', 'cli.js'); - if (fs.existsSync(globalCliPath)) { - return globalCliPath; - } - } catch (e) { - // npm root -g failed - } - return null; -} - -/** - * Find path to Homebrew installed Claude Code CLI - * @returns {string|null} Path to cli.js or binary, or null if not found - */ -function findHomebrewCliPath() { - if (process.platform !== 'darwin' && process.platform !== 'linux') { - return null; - } - - // Try to get Homebrew prefix via command first - let brewPrefix = null; - try { - brewPrefix = execSync('brew --prefix 2>/dev/null', { encoding: 'utf8' }).trim(); - } catch (e) { - // brew command not in PATH, try standard locations - } - - // Standard Homebrew locations to check - const possiblePrefixes = []; - if (brewPrefix) { - possiblePrefixes.push(brewPrefix); - } - - // Add standard locations based on platform - if (process.platform === 'darwin') { - // macOS: Intel (/usr/local) or Apple Silicon (/opt/homebrew) - possiblePrefixes.push('/opt/homebrew', '/usr/local'); - } else if (process.platform === 'linux') { - // Linux: system-wide or user installation - const homeDir = os.homedir(); - possiblePrefixes.push('/home/linuxbrew/.linuxbrew', path.join(homeDir, '.linuxbrew')); - } - - // Check each possible prefix - for (const prefix of possiblePrefixes) { - if (!fs.existsSync(prefix)) { - continue; - } - - // Homebrew installs claude-code as a Cask (binary) in Caskroom - const caskroomPath = path.join(prefix, 'Caskroom', 'claude-code'); - if (fs.existsSync(caskroomPath)) { - const found = findLatestVersionBinary(caskroomPath, 'claude'); - if (found) return found; - } - - // Also check Cellar (for formula installations, though claude-code is usually a Cask) - const cellarPath = path.join(prefix, 'Cellar', 'claude-code'); - if (fs.existsSync(cellarPath)) { - // Cellar has different structure - check for cli.js in libexec - const entries = fs.readdirSync(cellarPath); - if (entries.length > 0) { - const sorted = entries.sort((a, b) => compareVersions(b, a)); - const latestVersion = sorted[0]; - const cliPath = path.join(cellarPath, latestVersion, 'libexec', 'lib', 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js'); - if (fs.existsSync(cliPath)) { - return cliPath; - } - } - } - - // Check bin directory for symlink (most reliable) - const binPath = path.join(prefix, 'bin', 'claude'); - const resolvedBinPath = resolvePathSafe(binPath); - if (resolvedBinPath) return resolvedBinPath; - } - - return null; -} - -/** - * Find path to native installer Claude Code CLI - * - * Installation locations: - * - macOS/Linux: ~/.local/bin/claude (symlink) -> ~/.local/share/claude/versions/ - * - Windows: %LOCALAPPDATA%\Claude\ or %USERPROFILE%\.claude\ - * - Legacy: ~/.claude/local/ - * - * @returns {string|null} Path to cli.js or binary, or null if not found - */ -function findNativeInstallerCliPath() { - const homeDir = os.homedir(); - - // Windows-specific locations - if (process.platform === 'win32') { - const localAppData = process.env.LOCALAPPDATA || path.join(homeDir, 'AppData', 'Local'); - - // Check %LOCALAPPDATA%\Claude\ - const windowsClaudePath = path.join(localAppData, 'Claude'); - if (fs.existsSync(windowsClaudePath)) { - // Check for versions directory - const versionsDir = path.join(windowsClaudePath, 'versions'); - if (fs.existsSync(versionsDir)) { - const found = findLatestVersionBinary(versionsDir); - if (found) return found; - } - - // Check for claude.exe directly - const exePath = path.join(windowsClaudePath, 'claude.exe'); - if (fs.existsSync(exePath)) { - return exePath; - } - - // Check for cli.js - const cliPath = path.join(windowsClaudePath, 'cli.js'); - if (fs.existsSync(cliPath)) { - return cliPath; - } - } - - // Check %USERPROFILE%\.claude\ (alternative Windows location) - const dotClaudePath = path.join(homeDir, '.claude'); - if (fs.existsSync(dotClaudePath)) { - const versionsDir = path.join(dotClaudePath, 'versions'); - if (fs.existsSync(versionsDir)) { - const found = findLatestVersionBinary(versionsDir); - if (found) return found; - } - - const exePath = path.join(dotClaudePath, 'claude.exe'); - if (fs.existsSync(exePath)) { - return exePath; - } - } - } - - // Check ~/.local/bin/claude symlink (most common location on macOS/Linux) - const localBinPath = path.join(homeDir, '.local', 'bin', 'claude'); - const resolvedLocalBinPath = resolvePathSafe(localBinPath); - if (resolvedLocalBinPath) return resolvedLocalBinPath; - - // Check ~/.local/share/claude/versions/ (native installer location) - const versionsDir = path.join(homeDir, '.local', 'share', 'claude', 'versions'); - if (fs.existsSync(versionsDir)) { - const found = findLatestVersionBinary(versionsDir); - if (found) return found; - } - - // Check ~/.claude/local/ (older installation method) - const nativeBasePath = path.join(homeDir, '.claude', 'local'); - if (fs.existsSync(nativeBasePath)) { - // Look for the cli.js in the node_modules structure - const cliPath = path.join(nativeBasePath, 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js'); - if (fs.existsSync(cliPath)) { - return cliPath; - } - - // Alternative: direct cli.js in the installation - const directCliPath = path.join(nativeBasePath, 'cli.js'); - if (fs.existsSync(directCliPath)) { - return directCliPath; - } - } - - return null; -} - -/** - * Helper to find the latest version binary in a versions directory - * @param {string} versionsDir - Path to versions directory - * @param {string} [binaryName] - Optional binary name to look for inside version directory - * @returns {string|null} Path to binary or null - */ -function findLatestVersionBinary(versionsDir, binaryName = null) { - try { - const entries = fs.readdirSync(versionsDir); - if (entries.length === 0) return null; - - // Sort using semver comparison (descending) - const sorted = entries.sort((a, b) => compareVersions(b, a)); - const latestVersion = sorted[0]; - const versionPath = path.join(versionsDir, latestVersion); - - // Check if it's a file (binary) or directory - const stat = fs.statSync(versionPath); - if (stat.isFile()) { - return versionPath; - } else if (stat.isDirectory()) { - // If specific binary name provided, check for it - if (binaryName) { - const binaryPath = path.join(versionPath, binaryName); - if (fs.existsSync(binaryPath)) { - return binaryPath; - } - } - // Check for executable or cli.js inside directory - const exePath = path.join(versionPath, process.platform === 'win32' ? 'claude.exe' : 'claude'); - if (fs.existsSync(exePath)) { - return exePath; - } - const cliPath = path.join(versionPath, 'cli.js'); - if (fs.existsSync(cliPath)) { - return cliPath; - } - } - } catch (e) { - // Directory read failed - } - return null; -} - -/** - * Find path to Claude Code CLI using system PATH lookup (which/where) - * This serves as a fallback for non-standard installations (e.g., bun, pnpm) - * @returns {string|null} Path to claude binary, or null if not found - */ -function findWhichCliPath() { - try { - const cmd = process.platform === 'win32' ? 'where claude' : 'which claude'; - const result = execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim(); - // which/where may return multiple lines, take the first - const firstPath = result.split('\n')[0].trim(); - if (firstPath && fs.existsSync(firstPath)) { - return resolvePathSafe(firstPath) || firstPath; - } - } catch (e) { - // which/where command failed (claude not in PATH) - } - return null; -} - -/** - * Find path to globally installed Claude Code CLI - * Checks multiple installation methods in order of preference: - * 1. npm global (highest priority) - * 2. Homebrew - * 3. Native installer - * @returns {{path: string, source: string}|null} Path and source, or null if not found - */ -function findGlobalClaudeCliPath() { - // Check npm global first (highest priority) - const npmPath = findNpmGlobalCliPath(); - if (npmPath) return { path: npmPath, source: 'npm' }; - - // Check Homebrew installation - const homebrewPath = findHomebrewCliPath(); - if (homebrewPath) return { path: homebrewPath, source: 'Homebrew' }; - - // Check native installer - const nativePath = findNativeInstallerCliPath(); - if (nativePath) return { path: nativePath, source: 'native installer' }; - - // Fallback: use which/where to find claude in PATH - const whichPath = findWhichCliPath(); - if (whichPath) return { path: whichPath, source: 'PATH' }; - - return null; -} - -/** - * Get version from Claude Code package.json - * @param {string} cliPath - Path to cli.js - * @returns {string|null} Version string or null - */ -function getVersion(cliPath) { - try { - const pkgPath = path.join(path.dirname(cliPath), 'package.json'); - if (fs.existsSync(pkgPath)) { - const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); - return pkg.version; - } - } catch (e) {} - return null; -} - -/** - * Compare semver versions - * @param {string} a - First version - * @param {string} b - Second version - * @returns {number} 1 if a > b, -1 if a < b, 0 if equal - */ -function compareVersions(a, b) { - if (!a || !b) return 0; - const partsA = a.split('.').map(Number); - const partsB = b.split('.').map(Number); - for (let i = 0; i < 3; i++) { - if (partsA[i] > partsB[i]) return 1; - if (partsA[i] < partsB[i]) return -1; - } - return 0; -} - -/** - * Get the CLI path to use (global installation) - * @returns {string} Path to cli.js - * @throws {Error} If no global installation found - */ -function getClaudeCliPath() { - const result = findGlobalClaudeCliPath(); - if (!result) { - console.error('\n\x1b[1m\x1b[33mClaude Code is not installed\x1b[0m\n'); - console.error('Please install Claude Code using one of these methods:\n'); - console.error('\x1b[1mOption 1 - npm (recommended, highest priority):\x1b[0m'); - console.error(' \x1b[36mnpm install -g @anthropic-ai/claude-code\x1b[0m\n'); - console.error('\x1b[1mOption 2 - Homebrew (macOS/Linux):\x1b[0m'); - console.error(' \x1b[36mbrew install claude-code\x1b[0m\n'); - console.error('\x1b[1mOption 3 - Native installer:\x1b[0m'); - console.error(' \x1b[90mmacOS/Linux:\x1b[0m \x1b[36mcurl -fsSL https://claude.ai/install.sh | bash\x1b[0m'); - console.error(' \x1b[90mPowerShell:\x1b[0m \x1b[36mirm https://claude.ai/install.ps1 | iex\x1b[0m'); - console.error(' \x1b[90mWindows CMD:\x1b[0m \x1b[36mcurl -fsSL https://claude.ai/install.cmd -o install.cmd && install.cmd && del install.cmd\x1b[0m\n'); - console.error('\x1b[90mNote: If multiple installations exist, npm takes priority.\x1b[0m\n'); - process.exit(1); - } - - const version = getVersion(result.path); - const versionStr = version ? ` v${version}` : ''; - console.error(`\x1b[90mUsing Claude Code${versionStr} from ${result.source}\x1b[0m`); - - return result.path; -} - -/** - * Run Claude CLI, handling both JavaScript and binary files - * @param {string} cliPath - Path to CLI (from getClaudeCliPath) - */ -function runClaudeCli(cliPath) { - const { pathToFileURL } = require('url'); - const { spawn } = require('child_process'); - - // Check if it's a JavaScript file (.js or .cjs) or a binary file - const isJsFile = cliPath.endsWith('.js') || cliPath.endsWith('.cjs'); - - if (isJsFile) { - // JavaScript file - use import to keep interceptors working - const importUrl = pathToFileURL(cliPath).href; - import(importUrl); - } else { - // Binary file (e.g., Homebrew installation) - spawn directly - // Note: Interceptors won't work with binary files, but that's acceptable - // as binary files are self-contained and don't need interception - const args = process.argv.slice(2); - const child = spawn(cliPath, args, { - stdio: 'inherit', - env: process.env - }); - child.on('exit', (code) => { - process.exit(code || 0); - }); - } -} - -module.exports = { - findGlobalClaudeCliPath, - findNpmGlobalCliPath, - findHomebrewCliPath, - findNativeInstallerCliPath, - getVersion, - compareVersions, - getClaudeCliPath, - runClaudeCli -}; - diff --git a/cli/src/claude/claudeLocal.ts b/cli/src/claude/claudeLocal.ts index 0519c768..50703417 100644 --- a/cli/src/claude/claudeLocal.ts +++ b/cli/src/claude/claudeLocal.ts @@ -1,18 +1,12 @@ import { spawn } from "node:child_process"; -import { resolve, join } from "node:path"; -import { mkdirSync, existsSync } from "node:fs"; +import { mkdirSync } from "node:fs"; import { randomUUID } from "node:crypto"; import { logger } from "@/ui/logger"; import { claudeCheckSession } from "./utils/claudeCheckSession"; import { getProjectPath } from "./utils/path"; -import { runtimePath } from "@/projectPath"; import { systemPrompt } from "./utils/systemPrompt"; import { withBunRuntimeEnv } from "@/utils/bunRuntime"; - -// Get Claude CLI path from project root -export const claudeCliPath = resolve(join(runtimePath(), 'scripts', 'claude_local_launcher.cjs')) - export async function claudeLocal(opts: { abort: AbortSignal, sessionId: string | null, @@ -79,37 +73,48 @@ export async function claudeLocal(opts: { args.push(...opts.claudeArgs) } - if (!claudeCliPath || !existsSync(claudeCliPath)) { - throw new Error('Claude local launcher not found. Please ensure HAPI_PROJECT_ROOT is set correctly for development.'); - } - // Prepare environment variables // Note: Local mode uses global Claude installation with --session-id flag const env = { ...process.env, + DISABLE_AUTOUPDATER: '1', ...opts.claudeEnvVars } - logger.debug(`[ClaudeLocal] Spawning launcher: ${claudeCliPath}`); + logger.debug('[ClaudeLocal] Spawning claude'); logger.debug(`[ClaudeLocal] Args: ${JSON.stringify(args)}`); - const child = spawn(process.execPath, [claudeCliPath, ...args], { + const child = spawn('claude', args, { stdio: ['inherit', 'inherit', 'inherit'], signal: opts.abort, cwd: opts.path, env: withBunRuntimeEnv(env), + shell: process.platform === 'win32' }); + let settled = false; + const finalize = (error?: Error) => { + if (settled) { + return; + } + settled = true; + if (error) { + reject(error); + } else { + r(); + } + }; child.on('error', (error) => { - // Ignore + const message = error instanceof Error ? error.message : String(error); + finalize(new Error(`Failed to spawn claude: ${message}. Is Claude installed and on PATH?`)); }); child.on('exit', (code, signal) => { if (signal === 'SIGTERM' && opts.abort.aborted) { // Normal termination due to abort signal - r(); + finalize(); } else if (signal) { - reject(new Error(`Process terminated with signal: ${signal}`)); + finalize(new Error(`Process terminated with signal: ${signal}`)); } else { - r(); + finalize(); } }); }); diff --git a/cli/src/claude/claudeRemote.ts b/cli/src/claude/claudeRemote.ts index e23cd878..23dfff74 100644 --- a/cli/src/claude/claudeRemote.ts +++ b/cli/src/claude/claudeRemote.ts @@ -1,8 +1,7 @@ import { EnhancedMode, PermissionMode } from "./loop"; import { query, type QueryOptions as Options, type SDKMessage, type SDKSystemMessage, AbortError, SDKUserMessage } from '@/claude/sdk' import { claudeCheckSession } from "./utils/claudeCheckSession"; -import { join, resolve } from 'node:path'; -import { runtimePath } from "@/projectPath"; +import { join } from 'node:path'; import { parseSpecialCommand } from "@/parsers/specialCommands"; import { logger } from "@/lib"; import { PushableAsyncIterable } from "@/utils/PushableAsyncIterable"; @@ -74,6 +73,7 @@ export async function claudeRemote(opts: { process.env[key] = value; }); } + process.env.DISABLE_AUTOUPDATER = '1'; // Get initial message const initial = await opts.nextMessage(); @@ -121,9 +121,7 @@ export async function claudeRemote(opts: { canCallTool: (toolName: string, input: unknown, options: { signal: AbortSignal }) => opts.canCallTool(toolName, input, mode, options), executable: process.execPath, abort: opts.signal, - pathToClaudeCodeExecutable: (() => { - return resolve(join(runtimePath(), 'scripts', 'claude_remote_launcher.cjs')); - })(), + pathToClaudeCodeExecutable: 'claude', } // Track thinking state diff --git a/cli/src/index.ts b/cli/src/index.ts index cf3c6636..4dc6f19f 100755 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -25,7 +25,6 @@ import { listDaemonSessions, stopDaemonSession } from './daemon/controlClient' import { handleAuthCommand } from './commands/auth' import { handleConnectCommand } from './commands/connect' import { spawnHappyCLI } from './utils/spawnHappyCLI' -import { claudeCliPath } from './claude/claudeLocal' import { execFileSync } from 'node:child_process' import { initializeToken } from './ui/tokenInit' import { ensureRuntimeAssets } from './runtime/assets' @@ -391,12 +390,11 @@ ${chalk.bold.cyan('Claude Code Options (from `claude --help`):')} `) // Run claude --help and display its output - // Use execFileSync with the current Node executable for cross-platform compatibility try { const claudeHelp = execFileSync( - process.execPath, - [claudeCliPath, '--help'], - { encoding: 'utf8', env: withBunRuntimeEnv() } + 'claude', + ['--help'], + { encoding: 'utf8', env: withBunRuntimeEnv(), shell: process.platform === 'win32' } ) console.log(claudeHelp) } catch (e) { diff --git a/cli/src/runtime/assets.ts b/cli/src/runtime/assets.ts index e0723f05..321907db 100644 --- a/cli/src/runtime/assets.ts +++ b/cli/src/runtime/assets.ts @@ -116,9 +116,6 @@ function unpackTools(runtimeRoot: string): void { function runtimeAssetsReady(runtimeRoot: string): boolean { const requiredScripts = [ - join(runtimeRoot, 'scripts', 'claude_local_launcher.cjs'), - join(runtimeRoot, 'scripts', 'claude_remote_launcher.cjs'), - join(runtimeRoot, 'scripts', 'claude_version_utils.cjs'), join(runtimeRoot, 'scripts', 'ripgrep_launcher.cjs') ]; diff --git a/cli/src/runtime/embeddedAssets.bun.ts b/cli/src/runtime/embeddedAssets.bun.ts index d7042f1c..9e826bef 100644 --- a/cli/src/runtime/embeddedAssets.bun.ts +++ b/cli/src/runtime/embeddedAssets.bun.ts @@ -1,8 +1,5 @@ import { feature } from 'bun:bundle'; -import claudeLocalLauncher from '../../scripts/claude_local_launcher.cjs' assert { type: 'file' }; -import claudeRemoteLauncher from '../../scripts/claude_remote_launcher.cjs' assert { type: 'file' }; -import claudeVersionUtils from '../../scripts/claude_version_utils.cjs' assert { type: 'file' }; import ripgrepLauncher from '../../scripts/ripgrep_launcher.cjs' assert { type: 'file' }; import difftasticArchiveLicense from '../../tools/archives/difftastic-LICENSE' assert { type: 'file' }; @@ -23,9 +20,6 @@ function asset(relativePath: string, sourcePath: string): EmbeddedAsset { } const COMMON_ASSETS: EmbeddedAsset[] = [ - asset('scripts/claude_local_launcher.cjs', claudeLocalLauncher), - asset('scripts/claude_remote_launcher.cjs', claudeRemoteLauncher), - asset('scripts/claude_version_utils.cjs', claudeVersionUtils), asset('scripts/ripgrep_launcher.cjs', ripgrepLauncher), asset('tools/archives/difftastic-LICENSE', difftasticArchiveLicense), asset('tools/archives/ripgrep-LICENSE', ripgrepArchiveLicense),