From 705687de173a41fed595eee5ef5d338bcce99c04 Mon Sep 17 00:00:00 2001 From: A <258483684+la14-1@users.noreply.github.com> Date: Mon, 9 Mar 2026 16:26:49 -0700 Subject: [PATCH] fix: persist npm-global PATH to .profile/.bash_profile/.bashrc for SSH reconnect (#2399) After SSH reconnect, agent commands (openclaw, codex, kilocode, junie) were not found because PATH was only written to ~/.bashrc, which is not sourced by login shells. Login shells (used by SSH) source ~/.profile or ~/.bash_profile instead. Changes: - Write .spawnrc sourcing to ~/.profile and ~/.bash_profile in addition to ~/.bashrc and ~/.zshrc (orchestrate.ts) - Write npm-global PATH export to ~/.profile and ~/.bash_profile for all npm-installed agents: OpenClaw, Codex, Kilo Code, Junie (agent-setup.ts) - Write Claude Code PATH to ~/.profile and ~/.bash_profile (agent-setup.ts) - Write OpenCode PATH to ~/.profile and ~/.bash_profile (agent-setup.ts) - Extract NPM_GLOBAL_PATH_PERSIST constant to DRY up repeated shell snippets - Fix e2e provision.sh to also write .spawnrc sourcing to login shell configs - Bump CLI version to 0.15.32 Fixes #2394 Agent: code-health Co-authored-by: B <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.5 --- packages/cli/src/shared/agent-setup.ts | 33 +++++++++++++++----------- packages/cli/src/shared/orchestrate.ts | 5 ++-- sh/e2e/lib/provision.sh | 3 ++- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/packages/cli/src/shared/agent-setup.ts b/packages/cli/src/shared/agent-setup.ts index 542768e3..eb612f28 100644 --- a/packages/cli/src/shared/agent-setup.ts +++ b/packages/cli/src/shared/agent-setup.ts @@ -97,7 +97,7 @@ async function installClaudeCode(runner: CloudRunner): Promise { logStep("Installing Claude Code..."); const claudePath = "$HOME/.npm-global/bin:$HOME/.claude/local/bin:$HOME/.local/bin:$HOME/.bun/bin:$HOME/.n/bin"; - const pathSetup = `for rc in ~/.bashrc ~/.zshrc; do grep -q '.claude/local/bin' "$rc" 2>/dev/null || printf '\\n# Claude Code PATH\\nexport PATH="$HOME/.claude/local/bin:$HOME/.local/bin:$HOME/.bun/bin:$PATH"\\n' >> "$rc"; done`; + const pathSetup = `for rc in ~/.bashrc ~/.profile ~/.bash_profile ~/.zshrc; do grep -q '.claude/local/bin' "$rc" 2>/dev/null || printf '\\n# Claude Code PATH\\nexport PATH="$HOME/.claude/local/bin:$HOME/.local/bin:$HOME/.bun/bin:$PATH"\\n' >> "$rc"; done`; const finalize = `claude install --force 2>/dev/null || true; ${pathSetup}`; const script = [ @@ -534,7 +534,7 @@ async function ensureSwapSpace(runner: CloudRunner, sizeMb = 1024): Promise&2; exit 1; fi && tar xzf /tmp/opencode-install/oc.tar.gz -C /tmp/opencode-install && mv /tmp/opencode-install/opencode "$HOME/.opencode/bin/" && rm -rf /tmp/opencode-install && grep -q ".opencode/bin" "$HOME/.bashrc" 2>/dev/null || echo \'export PATH="$HOME/.opencode/bin:$PATH"\' >> "$HOME/.bashrc"; grep -q ".opencode/bin" "$HOME/.zshrc" 2>/dev/null || echo \'export PATH="$HOME/.opencode/bin:$PATH"\' >> "$HOME/.zshrc" 2>/dev/null; export PATH="$HOME/.opencode/bin:$PATH"'; + return 'OC_ARCH=$(uname -m); case "$OC_ARCH" in aarch64) OC_ARCH=arm64;; x86_64) OC_ARCH=x64;; esac; OC_OS=$(uname -s | tr A-Z a-z); mkdir -p /tmp/opencode-install "$HOME/.opencode/bin" && curl --proto \'=https\' -fsSL -o /tmp/opencode-install/oc.tar.gz "https://github.com/sst/opencode/releases/latest/download/opencode-${OC_OS}-${OC_ARCH}.tar.gz" && if tar -tzf /tmp/opencode-install/oc.tar.gz | grep -qE \'(^/|\\.\\.)\'; then echo "Tarball contains unsafe paths" >&2; exit 1; fi && tar xzf /tmp/opencode-install/oc.tar.gz -C /tmp/opencode-install && mv /tmp/opencode-install/opencode "$HOME/.opencode/bin/" && rm -rf /tmp/opencode-install && for _rc in "$HOME/.bashrc" "$HOME/.profile" "$HOME/.bash_profile"; do grep -q ".opencode/bin" "$_rc" 2>/dev/null || echo \'export PATH="$HOME/.opencode/bin:$PATH"\' >> "$_rc"; done; { [ ! -f "$HOME/.zshrc" ] || grep -q ".opencode/bin" "$HOME/.zshrc" 2>/dev/null || echo \'export PATH="$HOME/.opencode/bin:$PATH"\' >> "$HOME/.zshrc"; }; export PATH="$HOME/.opencode/bin:$PATH"'; } // ─── npm prefix helper ──────────────────────────────────────────────────────── @@ -557,6 +557,19 @@ const NPM_PREFIX_SETUP = 'mkdir -p ~/.npm-global/bin; _NPM_G_FLAGS="--prefix $HOME/.npm-global"; fi; ' + 'export PATH="$HOME/.npm-global/bin:$PATH"'; +/** + * Shell snippet that persists ~/.npm-global/bin in PATH across all shell config + * files: ~/.bashrc, ~/.profile, ~/.bash_profile, and ~/.zshrc. + * Login shells (SSH reconnect) source ~/.profile or ~/.bash_profile, not ~/.bashrc, + * so writing to ~/.bashrc alone is insufficient. + */ +const NPM_GLOBAL_PATH_PERSIST = + "for _rc in ~/.bashrc ~/.profile ~/.bash_profile; do " + + "grep -qF '.npm-global/bin' \"$_rc\" 2>/dev/null || " + + 'echo \'export PATH="$HOME/.npm-global/bin:$PATH"\' >> "$_rc"; done; ' + + "{ [ ! -f ~/.zshrc ] || grep -qF '.npm-global/bin' ~/.zshrc 2>/dev/null || " + + "echo 'export PATH=\"$HOME/.npm-global/bin:$PATH\"' >> ~/.zshrc; }"; + // ─── Default Agent Definitions ─────────────────────────────────────────────── const ZEROCLAW_INSTALL_URL = @@ -590,9 +603,7 @@ function createAgents(runner: CloudRunner): Record { installAgent( runner, "Codex CLI", - `${NPM_PREFIX_SETUP} && npm install -g \${_NPM_G_FLAGS} @openai/codex && ` + - "{ grep -qF '.npm-global/bin' ~/.bashrc 2>/dev/null || echo 'export PATH=\"$HOME/.npm-global/bin:$PATH\"' >> ~/.bashrc; } && " + - "{ [ ! -f ~/.zshrc ] || grep -qF '.npm-global/bin' ~/.zshrc 2>/dev/null || echo 'export PATH=\"$HOME/.npm-global/bin:$PATH\"' >> ~/.zshrc; }", + `${NPM_PREFIX_SETUP} && npm install -g \${_NPM_G_FLAGS} @openai/codex && ${NPM_GLOBAL_PATH_PERSIST}`, ), envVars: (apiKey) => [ `OPENROUTER_API_KEY=${apiKey}`, @@ -610,9 +621,7 @@ function createAgents(runner: CloudRunner): Record { await installAgent( runner, "openclaw", - `source ~/.bashrc 2>/dev/null; ${NPM_PREFIX_SETUP} && npm install -g \${_NPM_G_FLAGS} openclaw && ` + - "{ grep -qF '.npm-global/bin' ~/.bashrc 2>/dev/null || echo 'export PATH=\"$HOME/.npm-global/bin:$PATH\"' >> ~/.bashrc; } && " + - "{ [ ! -f ~/.zshrc ] || grep -qF '.npm-global/bin' ~/.zshrc 2>/dev/null || echo 'export PATH=\"$HOME/.npm-global/bin:$PATH\"' >> ~/.zshrc; }", + `source ~/.bashrc 2>/dev/null; ${NPM_PREFIX_SETUP} && npm install -g \${_NPM_G_FLAGS} openclaw && ${NPM_GLOBAL_PATH_PERSIST}`, ); }, envVars: (apiKey) => [ @@ -647,9 +656,7 @@ function createAgents(runner: CloudRunner): Record { installAgent( runner, "Kilo Code", - `${NPM_PREFIX_SETUP} && npm install -g \${_NPM_G_FLAGS} @kilocode/cli && ` + - "{ grep -qF '.npm-global/bin' ~/.bashrc 2>/dev/null || echo 'export PATH=\"$HOME/.npm-global/bin:$PATH\"' >> ~/.bashrc; } && " + - "{ [ ! -f ~/.zshrc ] || grep -qF '.npm-global/bin' ~/.zshrc 2>/dev/null || echo 'export PATH=\"$HOME/.npm-global/bin:$PATH\"' >> ~/.zshrc; }", + `${NPM_PREFIX_SETUP} && npm install -g \${_NPM_G_FLAGS} @kilocode/cli && ${NPM_GLOBAL_PATH_PERSIST}`, ), envVars: (apiKey) => [ `OPENROUTER_API_KEY=${apiKey}`, @@ -711,9 +718,7 @@ function createAgents(runner: CloudRunner): Record { installAgent( runner, "Junie", - `${NPM_PREFIX_SETUP} && npm install -g \${_NPM_G_FLAGS} @jetbrains/junie-cli && ` + - "{ grep -qF '.npm-global/bin' ~/.bashrc 2>/dev/null || echo 'export PATH=\"$HOME/.npm-global/bin:$PATH\"' >> ~/.bashrc; } && " + - "{ [ ! -f ~/.zshrc ] || grep -qF '.npm-global/bin' ~/.zshrc 2>/dev/null || echo 'export PATH=\"$HOME/.npm-global/bin:$PATH\"' >> ~/.zshrc; }", + `${NPM_PREFIX_SETUP} && npm install -g \${_NPM_G_FLAGS} @jetbrains/junie-cli && ${NPM_GLOBAL_PATH_PERSIST}`, ), envVars: (apiKey) => [ `JUNIE_OPENROUTER_API_KEY=${apiKey}`, diff --git a/packages/cli/src/shared/orchestrate.ts b/packages/cli/src/shared/orchestrate.ts index f930d797..8c3ca511 100644 --- a/packages/cli/src/shared/orchestrate.ts +++ b/packages/cli/src/shared/orchestrate.ts @@ -147,8 +147,9 @@ export async function runOrchestration( wrapSshCall( cloud.runner.runServer( `printf '%s' '${envB64}' | base64 -d > ~/.spawnrc && chmod 600 ~/.spawnrc; ` + - `grep -q 'source ~/.spawnrc' ~/.bashrc 2>/dev/null || echo '[ -f ~/.spawnrc ] && source ~/.spawnrc' >> ~/.bashrc; ` + - `grep -q 'source ~/.spawnrc' ~/.zshrc 2>/dev/null || echo '[ -f ~/.spawnrc ] && source ~/.spawnrc' >> ~/.zshrc`, + "for _rc in ~/.bashrc ~/.profile ~/.bash_profile ~/.zshrc; do " + + `grep -q 'source ~/.spawnrc' "$_rc" 2>/dev/null || echo '[ -f ~/.spawnrc ] && source ~/.spawnrc' >> "$_rc"; ` + + "done", ), ), 2, diff --git a/sh/e2e/lib/provision.sh b/sh/e2e/lib/provision.sh index 9dce8037..a549fa7d 100644 --- a/sh/e2e/lib/provision.sh +++ b/sh/e2e/lib/provision.sh @@ -224,7 +224,8 @@ CLOUD_ENV fi if printf '%s' "${env_b64}" | cloud_exec "${app_name}" "base64 -d > ~/.spawnrc && chmod 600 ~/.spawnrc && \ - grep -q 'source ~/.spawnrc' ~/.bashrc 2>/dev/null || printf '%s\n' '[ -f ~/.spawnrc ] && source ~/.spawnrc' >> ~/.bashrc" >/dev/null 2>&1; then + for _rc in ~/.bashrc ~/.profile ~/.bash_profile; do \ + grep -q 'source ~/.spawnrc' \"\$_rc\" 2>/dev/null || printf '%s\n' '[ -f ~/.spawnrc ] && source ~/.spawnrc' >> \"\$_rc\"; done" >/dev/null 2>&1; then log_ok "Manual .spawnrc created successfully" else log_err "Failed to create manual .spawnrc"