fix(remote): bootstrap PATH consistently for remote tmux (johoja12/Codeman#15)

This commit is contained in:
Saqeb Akhter authored and Codeman maintainer committed 2026-10-24 14:52:26 +02:00
1 parent 002de32a08
commit 5b9445de9e
8 files changed
+223 -19

No files matched your search

+20 -3
View File
@@ -89,6 +89,19 @@ export function remoteLoginShellCommand(command: string): string {
return `exec ${REMOTE_LOGIN_SHELL} -i -l -c ${shellescape(command)}`;
}
/**
* Remote tmux runs in sshd's non-interactive shell, which may omit Homebrew and
* per-user binaries. Keep every remote tmux operation on the same PATH. Quoting
* the entire assignment preserves spaces in HOME/inherited PATH under dash too.
* CLI commands still use their existing login-shell/configuration handling.
*/
export function withRemoteTmuxPath(command: string): string {
return (
'export PATH="$HOME/.opencode/bin:$HOME/.local/bin:$HOME/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH"; ' +
command
);
}
/**
* The CLI text a location overlay should launch for `mode`, or null when this build has no
* entry for it. `overlays.<location>.command` when the entry names one, otherwise the bare
@@ -228,7 +241,11 @@ export function buildRemoteTmuxCheckCommand(
host: Pick<RemoteHost, 'username' | 'host' | 'port'> & RemoteSshOptions
): string {
// ConnectTimeout is now a default of buildSshConnectionArgs (shared with the launch).
return [...buildSshConnectionArgs(host), remoteSshTarget(host), "'command -v tmux'"].join(' ');
return [
...buildSshConnectionArgs(host),
remoteSshTarget(host),
shellescape(withRemoteTmuxPath('command -v tmux')),
].join(' ');
}
export interface RemoteTmuxCheckResult {
@@ -368,7 +385,7 @@ export function buildRemoteSessionAliveCommand(
): string {
const [ssh, ...connectionArgs] = buildSshConnectionArgs(host);
const remoteCmd = `tmux -L codeman-remote has-session -t ${shellescape(remoteSessionName)} 2>/dev/null`;
return [ssh, ...connectionArgs, remoteSshTarget(host), shellescape(remoteCmd)].join(' ');
return [ssh, ...connectionArgs, remoteSshTarget(host), shellescape(withRemoteTmuxPath(remoteCmd))].join(' ');
}
/**
@@ -453,7 +470,7 @@ export function buildRemoteListSessionsCommand(
// separators (tmux expands them); `2>/dev/null` is inside the quoted command.
const remoteCmd =
'tmux -L codeman list-sessions -F "#{session_name}\\t#{session_attached}\\t#{session_created}\\t#{session_windows}" 2>/dev/null';
parts.push(remoteSshTarget(host), shellescape(remoteCmd));
parts.push(remoteSshTarget(host), shellescape(withRemoteTmuxPath(remoteCmd)));
return parts.join(' ');
}
+8 -2
View File
@@ -48,7 +48,13 @@ import { exec } from 'node:child_process';
import { randomBytes } from 'node:crypto';
import { promisify } from 'node:util';
import type { SessionRemote } from './types/session.js';
import { buildSshConnectionArgs, classifyRemoteAliveExit, remoteSshTarget, shellescape } from './remote-hosts.js';
import {
buildSshConnectionArgs,
classifyRemoteAliveExit,
remoteSshTarget,
shellescape,
withRemoteTmuxPath,
} from './remote-hosts.js';
import { RemoteSshSlotTimeoutError, runWithRemoteSshLimit } from './remote-ssh-limiter.js';
import { remoteTmuxLocation } from './tmux-manager.js';
@@ -153,7 +159,7 @@ export function buildRemoteScrollbackCommand(
marker: string
): string {
const { socket, sessionName } = remoteTmuxLocation(remote, sessionId);
const script = buildRemoteScrollbackScript(socket, sessionName, historyLines, tailBytes, marker);
const script = withRemoteTmuxPath(buildRemoteScrollbackScript(socket, sessionName, historyLines, tailBytes, marker));
// `exec` so a timeout kill reaches ssh itself, not only the local `sh -c`.
return ['exec', ...buildSshConnectionArgs(remote), remoteSshTarget(remote), shellescape(script)].join(' ');
}
+18 -3
View File
@@ -84,6 +84,7 @@ import {
remoteLoginShellCommand,
remoteSshTarget,
remoteTmuxSessionAlive,
withRemoteTmuxPath,
} from './remote-hosts.js';
import {
buildDockerBaseArgs,
@@ -1381,7 +1382,14 @@ export function buildRemoteLaunchCommand(options: {
// `ssh -o BatchMode=yes` (preserving the historical token order), then the rest
// of the connection args, then the target and the quoted tmux invocation.
const [ssh, batchMode, ...connectionArgs] = buildSshConnectionArgs(remote);
const sshParts = [ssh, batchMode, '-t', ...connectionArgs, remoteSshTarget(remote), shellescape(tmuxInvocation)];
const sshParts = [
ssh,
batchMode,
'-t',
...connectionArgs,
remoteSshTarget(remote),
shellescape(withRemoteTmuxPath(tmuxInvocation)),
];
return sshParts.join(' ');
}
@@ -1398,7 +1406,7 @@ export function buildRemoteKillCommand(options: { remote: SessionRemote; session
const remoteName = remoteTmuxSessionName(sessionId);
const killCmd = `tmux -L ${REMOTE_TMUX_SOCKET} kill-session -t ${shellescape(remoteName)}`;
const [ssh, ...connectionArgs] = buildSshConnectionArgs(remote);
return [ssh, ...connectionArgs, remoteSshTarget(remote), shellescape(killCmd)].join(' ');
return [ssh, ...connectionArgs, remoteSshTarget(remote), shellescape(withRemoteTmuxPath(killCmd))].join(' ');
}
// ========== Docker cases (COD-Docker) ==========
@@ -1777,7 +1785,14 @@ export function resolveDockerLaunchOptions(
export function buildRemoteAttachCommand(remote: SessionRemote, remoteSessionName: string): string {
const tmuxInvocation = `tmux -L codeman attach -t ${shellescape(remoteSessionName)}`;
const [ssh, batchMode, ...connectionArgs] = buildSshConnectionArgs(remote);
const sshParts = [ssh, batchMode, '-t', ...connectionArgs, remoteSshTarget(remote), shellescape(tmuxInvocation)];
const sshParts = [
ssh,
batchMode,
'-t',
...connectionArgs,
remoteSshTarget(remote),
shellescape(withRemoteTmuxPath(tmuxInvocation)),
];
return sshParts.join(' ');
}