Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
36 changes: 36 additions & 0 deletions scripts/postinstall.js
Original file line number Diff line number Diff line change
@@ -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).`);
}
37 changes: 32 additions & 5 deletions src/claude-bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 3 additions & 5 deletions src/public/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down