mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 16: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 { homedir } from 'node:os';
|
||||||
import { ScreenSession, ProcessStats, ScreenSessionWithStats, PersistedRespawnConfig, getErrorMessage } from './types.js';
|
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 */
|
/** Path to persisted screen session metadata */
|
||||||
const SCREENS_FILE = join(homedir(), '.claudeman', 'screens.json');
|
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
|
// 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
|
// 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 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'
|
const cmd = mode === 'claude'
|
||||||
? `${envVars} claude --dangerously-skip-permissions`
|
? `${envVars} ${claudePath} --dangerously-skip-permissions`
|
||||||
: `${envVars} $SHELL`;
|
: `${envVars} $SHELL`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
+61
-2
@@ -16,6 +16,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventEmitter } from 'node:events';
|
import { EventEmitter } from 'node:events';
|
||||||
|
import { execSync } from 'node:child_process';
|
||||||
|
import { existsSync } from 'node:fs';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import * as pty from 'node-pty';
|
import * as pty from 'node-pty';
|
||||||
import { SessionState, SessionStatus, SessionConfig, ScreenSession, RalphTrackerState, RalphTodoItem } from './types.js';
|
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 ANSI_ESCAPE_PATTERN = /\x1b\[[0-9;]*m/g;
|
||||||
const TOKEN_PATTERN = /(\d+(?:\.\d+)?)\s*([kKmM])?\s*tokens/;
|
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)
|
// 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
|
// Fallback to direct PTY if screen is not used
|
||||||
if (!this.ptyProcess) {
|
if (!this.ptyProcess) {
|
||||||
this.ptyProcess = pty.spawn('claude', [
|
const claudePath = resolveClaudePath();
|
||||||
|
this.ptyProcess = pty.spawn(claudePath, [
|
||||||
'--dangerously-skip-permissions'
|
'--dangerously-skip-permissions'
|
||||||
], {
|
], {
|
||||||
name: 'xterm-256color',
|
name: 'xterm-256color',
|
||||||
@@ -1014,7 +1072,8 @@ export class Session extends EventEmitter {
|
|||||||
// Spawn claude in a real PTY
|
// Spawn claude in a real PTY
|
||||||
console.log('[Session] Spawning PTY for claude with prompt:', prompt.substring(0, 50));
|
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',
|
'-p',
|
||||||
'--dangerously-skip-permissions',
|
'--dangerously-skip-permissions',
|
||||||
'--output-format', 'stream-json',
|
'--output-format', 'stream-json',
|
||||||
|
|||||||
Reference in New Issue
Block a user