( host: string, onProgress?: (msg: string) => void, )
| 19 | } |
| 20 | |
| 21 | export async function probeRemote( |
| 22 | host: string, |
| 23 | onProgress?: (msg: string) => void, |
| 24 | ): Promise<ProbeResult> { |
| 25 | onProgress?.('Probing remote host…') |
| 26 | |
| 27 | const proc = Bun.spawn( |
| 28 | [ |
| 29 | 'ssh', |
| 30 | '-o', |
| 31 | 'BatchMode=yes', |
| 32 | '-o', |
| 33 | 'ConnectTimeout=10', |
| 34 | host, |
| 35 | 'CLAUDE_BIN=$(test -x "$HOME/.local/bin/claude" && echo "$HOME/.local/bin/claude" || command -v claude 2>/dev/null); echo "$CLAUDE_BIN"; $CLAUDE_BIN --version 2>/dev/null; uname -sm; pwd', |
| 36 | ], |
| 37 | { stdin: 'ignore', stdout: 'pipe', stderr: 'pipe' }, |
| 38 | ) |
| 39 | |
| 40 | const result = await Promise.race([ |
| 41 | proc.exited, |
| 42 | new Promise<never>((_, reject) => |
| 43 | setTimeout( |
| 44 | () => |
| 45 | reject( |
| 46 | new SSHProbeError( |
| 47 | `SSH probe timed out after ${PROBE_TIMEOUT_MS / 1000}s`, |
| 48 | ), |
| 49 | ), |
| 50 | PROBE_TIMEOUT_MS, |
| 51 | ), |
| 52 | ), |
| 53 | ]) |
| 54 | |
| 55 | const stdout = await new Response(proc.stdout).text() |
| 56 | const stderr = await new Response(proc.stderr).text() |
| 57 | |
| 58 | if (result !== 0) { |
| 59 | const detail = stderr.trim() || `exit code ${result}` |
| 60 | throw new SSHProbeError(`SSH probe failed: ${detail}`) |
| 61 | } |
| 62 | |
| 63 | const lines = stdout |
| 64 | .split('\n') |
| 65 | .map(l => l.trim()) |
| 66 | .filter(Boolean) |
| 67 | logForDebugging(`[SSHProbe] raw lines: ${JSON.stringify(lines)}`) |
| 68 | |
| 69 | const unameIdx = lines.findIndex(l => /^(Linux|Darwin)\s/.test(l)) |
| 70 | if (unameIdx === -1) { |
| 71 | throw new SSHProbeError( |
| 72 | 'Could not detect remote platform (uname output missing)', |
| 73 | ) |
| 74 | } |
| 75 | |
| 76 | const binaryPath = unameIdx >= 2 ? lines[unameIdx - 2] || null : null |
| 77 | const versionLine = unameIdx >= 1 ? lines[unameIdx - 1] || null : null |
| 78 | const remoteVersion = |
no test coverage detected