diff --git a/package.json b/package.json index ba3c6c6..4bf8931 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "start": "node bin/cc-web.js", "dev": "node bin/cc-web.js --dev", "test": "mocha test/*.test.js", - "release:pr": "bash scripts/release-pr.sh" + "release:pr": "bash scripts/release-pr.sh", + "postinstall": "node scripts/postinstall.js" }, "keywords": [ "claude", diff --git a/scripts/postinstall.js b/scripts/postinstall.js new file mode 100644 index 0000000..7fefdfd --- /dev/null +++ b/scripts/postinstall.js @@ -0,0 +1,36 @@ +#!/usr/bin/env node +/** + * Post-install script: ensure node-pty's spawn-helper binaries are executable. + * + * When npm installs a package, file permissions from the tarball are not always + * preserved on macOS. node-pty's spawn-helper binary must be executable or + * node-pty will throw "posix_spawnp failed" on every pty.spawn() call. + */ +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const prebuildsDirs = [ + path.join(__dirname, '..', 'node_modules', 'node-pty', 'prebuilds'), +]; + +let fixed = 0; +for (const prebuildsDir of prebuildsDirs) { + if (!fs.existsSync(prebuildsDir)) continue; + for (const platform of fs.readdirSync(prebuildsDir)) { + const helperPath = path.join(prebuildsDir, platform, 'spawn-helper'); + if (!fs.existsSync(helperPath)) continue; + const mode = fs.statSync(helperPath).mode; + const execBit = 0o111; + if ((mode & execBit) !== execBit) { + fs.chmodSync(helperPath, mode | execBit); + console.log(`Fixed permissions for ${helperPath}`); + fixed++; + } + } +} + +if (fixed > 0) { + console.log(`postinstall: fixed execute permissions on ${fixed} spawn-helper file(s).`); +} diff --git a/src/claude-bridge.js b/src/claude-bridge.js index c7b4fee..dacf756 100644 --- a/src/claude-bridge.js +++ b/src/claude-bridge.js @@ -9,14 +9,28 @@ class ClaudeBridge { } findClaudeCommand() { + // Dynamically discover all nvm node versions that may have claude installed + const nvmBase = path.join(process.env.HOME || '/', '.nvm', 'versions', 'node'); + const nvmPaths = []; + try { + const versions = fs.readdirSync(nvmBase); + for (const v of versions.reverse()) { + nvmPaths.push(path.join(nvmBase, v, 'bin', 'claude')); + } + } catch (e) { /* nvm not installed */ } + const possibleCommands = [ + // Absolute paths first — more reliable when running under launchd/systemd + // where PATH may not include nvm or user-local bin directories + ...nvmPaths, '/home/ec2-user/.claude/local/claude', - 'claude', - 'claude-code', path.join(process.env.HOME || '/', '.claude', 'local', 'claude'), path.join(process.env.HOME || '/', '.local', 'bin', 'claude'), '/usr/local/bin/claude', - '/usr/bin/claude' + '/usr/bin/claude', + // Short names as last resort (relies on PATH being set correctly) + 'claude', + 'claude-code', ]; for (const cmd of possibleCommands) { @@ -67,8 +81,21 @@ class ClaudeBridge { console.log(`⚠️ WARNING: Skipping permissions with --dangerously-skip-permissions flag`); } - const args = dangerouslySkipPermissions ? ['--dangerously-skip-permissions'] : []; - const claudeProcess = spawn(this.claudeCommand, args, { + // When the resolved claude command is a .js script (e.g. installed via nvm), + // node-pty cannot posix_spawnp a script file directly. Detect this case and + // invoke it via the current node binary instead. + let spawnCmd = this.claudeCommand; + let baseArgs = dangerouslySkipPermissions ? ['--dangerously-skip-permissions'] : []; + try { + const resolvedCmd = fs.realpathSync(this.claudeCommand); + if (resolvedCmd.endsWith('.js')) { + baseArgs = [resolvedCmd, ...baseArgs]; + spawnCmd = process.execPath; + console.log(`Resolved to JS script, spawning via node: ${spawnCmd} ${resolvedCmd}`); + } + } catch (e) { /* use spawnCmd as-is */ } + const args = baseArgs; + const claudeProcess = spawn(spawnCmd, args, { cwd: workingDir, env: { ...process.env, diff --git a/src/public/app.js b/src/public/app.js index b35e564..5148ec3 100644 --- a/src/public/app.js +++ b/src/public/app.js @@ -103,11 +103,9 @@ class ClaudeCodeWebInterface { const firstTabId = this.sessionTabManager.tabs.keys().next().value; console.log('[Init] Switching to tab:', firstTabId); await this.sessionTabManager.switchToTab(firstTabId); - - // Hide overlay completely since we have sessions - console.log('[Init] About to hide overlay'); - this.hideOverlay(); - console.log('[Init] Overlay should be hidden now'); + // Overlay state is handled by the session_joined message handler: + // it hides the overlay when Claude is active, or shows startPrompt when not. + // Do NOT call hideOverlay() here — it would race with and override that logic. } else { console.log('[Init] No sessions found, showing folder browser'); // No sessions - hide loading overlay and show folder picker to create first session