refactor: remove CommonJS launchers and use claude command directly

Remove local Claude launcher scripts and refactor to invoke the global
claude command from PATH. This simplifies the launcher logic and removes
the need to maintain separate CommonJS entry points.

Changes:
- Delete cli/scripts/claude_local_launcher.cjs, claude_remote_launcher.cjs,
  and claude_version_utils.cjs
- Update claudeLocal to spawn 'claude' command directly instead of using
  a launcher script
- Remove runtimePath dependency and launcher existence checks
- Add DISABLE_AUTOUPDATER environment variable for local mode
- Improve error messages when claude command is not found
- Add shell option for Windows compatibility in process spawning
- Remove unused imports from claudeLocal and index files
This commit is contained in:
weishu
2025-12-23 10:21:50 +08:00
parent f132a7dc79
commit 88cce9c28d
8 changed files with 28 additions and 462 deletions
-7
View File
@@ -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());
-16
View File
@@ -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());
-403
View File
@@ -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/<version>
* - 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
};
+22 -17
View File
@@ -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();
}
});
});
+3 -5
View File
@@ -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
+3 -5
View File
@@ -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) {
-3
View File
@@ -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')
];
-6
View File
@@ -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),