mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-03 05:59:43 +02:00
fix: resolve claude CLI absolute path to prevent execvp failures
When the web server runs in environments where `claude` isn't in PATH (e.g., systemd services, non-login shells), node-pty's execvp(3) fails with "No such file or directory". Fix by resolving the absolute path to the claude binary using `which` with fallback to common installation locations (~/.local/bin, /usr/local/bin, etc.). The resolved path is cached for performance. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+49
-1
@@ -21,6 +21,53 @@ import { dirname, join } from 'node:path';
|
||||
import { homedir } from 'node:os';
|
||||
import { ScreenSession, ProcessStats, ScreenSessionWithStats, PersistedRespawnConfig, getErrorMessage } from './types.js';
|
||||
|
||||
// ============================================================================
|
||||
// Claude CLI Path Resolution
|
||||
// ============================================================================
|
||||
|
||||
/** Common installation paths for the Claude CLI binary */
|
||||
const CLAUDE_COMMON_PATHS = [
|
||||
`${homedir()}/.local/bin/claude`,
|
||||
`${homedir()}/.claude/local/claude`,
|
||||
'/usr/local/bin/claude',
|
||||
'/usr/bin/claude',
|
||||
`${homedir()}/.npm-global/bin/claude`,
|
||||
`${homedir()}/bin/claude`,
|
||||
];
|
||||
|
||||
/** Cached resolved path to the claude binary */
|
||||
let _resolvedClaudePath: string | null = null;
|
||||
|
||||
/**
|
||||
* Resolves the absolute path to the `claude` CLI binary.
|
||||
* Uses `which` first, then falls back to common installation locations.
|
||||
*/
|
||||
function resolveClaudePath(): string {
|
||||
if (_resolvedClaudePath) return _resolvedClaudePath;
|
||||
|
||||
try {
|
||||
const result = execSync('which claude', { encoding: 'utf-8', timeout: 5000 }).trim();
|
||||
if (result && existsSync(result)) {
|
||||
_resolvedClaudePath = result;
|
||||
return _resolvedClaudePath;
|
||||
}
|
||||
} catch {
|
||||
// `which` failed, try common paths
|
||||
}
|
||||
|
||||
for (const p of CLAUDE_COMMON_PATHS) {
|
||||
if (existsSync(p)) {
|
||||
_resolvedClaudePath = p;
|
||||
return _resolvedClaudePath;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'Claude CLI not found. Ensure `claude` is installed and available in PATH. ' +
|
||||
'Checked: PATH lookup and common paths: ' + CLAUDE_COMMON_PATHS.join(', ')
|
||||
);
|
||||
}
|
||||
|
||||
/** Path to persisted screen session metadata */
|
||||
const SCREENS_FILE = join(homedir(), '.claudeman', 'screens.json');
|
||||
|
||||
@@ -169,8 +216,9 @@ export class ScreenManager extends EventEmitter {
|
||||
// Set CLAUDEMAN_SCREEN=1 so Claude sessions know they're running in Claudeman
|
||||
// This helps prevent Claude from attempting to kill its own screen session
|
||||
const envVars = `CLAUDEMAN_SCREEN=1 CLAUDEMAN_SESSION_ID=${sessionId} CLAUDEMAN_SCREEN_NAME=${screenName} CLAUDEMAN_API_URL=${process.env.CLAUDEMAN_API_URL || 'http://localhost:3000'}`;
|
||||
const claudePath = resolveClaudePath();
|
||||
const cmd = mode === 'claude'
|
||||
? `${envVars} claude --dangerously-skip-permissions`
|
||||
? `${envVars} ${claudePath} --dangerously-skip-permissions`
|
||||
: `${envVars} $SHELL`;
|
||||
|
||||
try {
|
||||
|
||||
+61
-2
@@ -16,6 +16,8 @@
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import * as pty from 'node-pty';
|
||||
import { SessionState, SessionStatus, SessionConfig, ScreenSession, RalphTrackerState, RalphTodoItem } from './types.js';
|
||||
@@ -61,6 +63,61 @@ const FOCUS_ESCAPE_FILTER = /\x1b\[\?1004[hl]|\x1b\[[IO]/g;
|
||||
const ANSI_ESCAPE_PATTERN = /\x1b\[[0-9;]*m/g;
|
||||
const TOKEN_PATTERN = /(\d+(?:\.\d+)?)\s*([kKmM])?\s*tokens/;
|
||||
|
||||
// ============================================================================
|
||||
// Claude CLI Path Resolution
|
||||
// ============================================================================
|
||||
|
||||
/** Common installation paths for the Claude CLI binary */
|
||||
const CLAUDE_COMMON_PATHS = [
|
||||
`${process.env.HOME}/.local/bin/claude`,
|
||||
`${process.env.HOME}/.claude/local/claude`,
|
||||
'/usr/local/bin/claude',
|
||||
'/usr/bin/claude',
|
||||
`${process.env.HOME}/.npm-global/bin/claude`,
|
||||
`${process.env.HOME}/bin/claude`,
|
||||
];
|
||||
|
||||
/** Cached resolved path to the claude binary */
|
||||
let _resolvedClaudePath: string | null = null;
|
||||
|
||||
/**
|
||||
* Resolves the absolute path to the `claude` CLI binary.
|
||||
*
|
||||
* Uses `which` to find the binary in PATH first, then falls back to common
|
||||
* installation locations. The result is cached for subsequent calls.
|
||||
*
|
||||
* @throws {Error} If claude CLI cannot be found in PATH or common locations
|
||||
*/
|
||||
function resolveClaudePath(): string {
|
||||
if (_resolvedClaudePath) return _resolvedClaudePath;
|
||||
|
||||
// Try `which` first (respects PATH)
|
||||
try {
|
||||
const result = execSync('which claude', { encoding: 'utf-8', timeout: 5000 }).trim();
|
||||
if (result && existsSync(result)) {
|
||||
_resolvedClaudePath = result;
|
||||
console.log('[Session] Resolved claude CLI path:', _resolvedClaudePath);
|
||||
return _resolvedClaudePath;
|
||||
}
|
||||
} catch {
|
||||
// `which` failed, try common paths
|
||||
}
|
||||
|
||||
// Check common installation paths
|
||||
for (const p of CLAUDE_COMMON_PATHS) {
|
||||
if (existsSync(p)) {
|
||||
_resolvedClaudePath = p;
|
||||
console.log('[Session] Found claude CLI at common path:', _resolvedClaudePath);
|
||||
return _resolvedClaudePath;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'Claude CLI not found. Ensure `claude` is installed and available in PATH. ' +
|
||||
'Checked: PATH lookup and common paths: ' + CLAUDE_COMMON_PATHS.join(', ')
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Buffer Accumulator (reduces GC pressure from string concatenation)
|
||||
// ============================================================================
|
||||
@@ -736,7 +793,8 @@ export class Session extends EventEmitter {
|
||||
|
||||
// Fallback to direct PTY if screen is not used
|
||||
if (!this.ptyProcess) {
|
||||
this.ptyProcess = pty.spawn('claude', [
|
||||
const claudePath = resolveClaudePath();
|
||||
this.ptyProcess = pty.spawn(claudePath, [
|
||||
'--dangerously-skip-permissions'
|
||||
], {
|
||||
name: 'xterm-256color',
|
||||
@@ -1014,7 +1072,8 @@ export class Session extends EventEmitter {
|
||||
// Spawn claude in a real PTY
|
||||
console.log('[Session] Spawning PTY for claude with prompt:', prompt.substring(0, 50));
|
||||
|
||||
this.ptyProcess = pty.spawn('claude', [
|
||||
const claudePath = resolveClaudePath();
|
||||
this.ptyProcess = pty.spawn(claudePath, [
|
||||
'-p',
|
||||
'--dangerously-skip-permissions',
|
||||
'--output-format', 'stream-json',
|
||||
|
||||
Reference in New Issue
Block a user