mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 14:25:51 +00:00
feat: 实现 SSH Remote — 本地 REPL + 远端工具执行
SSH Remote 允许在本地运行交互式 REPL,同时将工具调用(Bash、文件读写等) 通过 SSH 隧道转发到远程主机执行。 核心模块: - SSHSessionManager: NDJSON 双向通信、权限转发、指数退避重连 - SSHAuthProxy: 本地认证代理 + SSH -R 反向端口转发,nonce 验证 - SSHProbe: 远端主机平台/架构/已有二进制探测 - SSHDeploy: 远端二进制部署(scp) - createSSHSession: 会话编排(probe → deploy → spawn → attach) 新增选项: - --remote-bin: 跳过 probe/deploy,使用自定义远端二进制 - ANTHROPIC_AUTH_NONCE: API 请求认证 nonce header 包含 17 个单元测试和完整文档。
This commit is contained in:
99
src/ssh/SSHProbe.ts
Normal file
99
src/ssh/SSHProbe.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { logForDebugging } from 'src/utils/debug.js'
|
||||
|
||||
const PROBE_TIMEOUT_MS = 15_000
|
||||
|
||||
export interface ProbeResult {
|
||||
hasBinary: boolean
|
||||
remoteVersion: string | null
|
||||
remotePlatform: 'linux' | 'darwin'
|
||||
remoteArch: 'x64' | 'arm64'
|
||||
defaultCwd: string
|
||||
binaryPath: string | null
|
||||
}
|
||||
|
||||
export class SSHProbeError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message)
|
||||
this.name = 'SSHProbeError'
|
||||
}
|
||||
}
|
||||
|
||||
export async function probeRemote(
|
||||
host: string,
|
||||
onProgress?: (msg: string) => void,
|
||||
): Promise<ProbeResult> {
|
||||
onProgress?.('Probing remote host…')
|
||||
|
||||
const proc = Bun.spawn(
|
||||
[
|
||||
'ssh',
|
||||
'-o',
|
||||
'BatchMode=yes',
|
||||
'-o',
|
||||
'ConnectTimeout=10',
|
||||
host,
|
||||
'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',
|
||||
],
|
||||
{ stdin: 'ignore', stdout: 'pipe', stderr: 'pipe' },
|
||||
)
|
||||
|
||||
const result = await Promise.race([
|
||||
proc.exited,
|
||||
new Promise<never>((_, reject) =>
|
||||
setTimeout(
|
||||
() =>
|
||||
reject(
|
||||
new SSHProbeError(
|
||||
`SSH probe timed out after ${PROBE_TIMEOUT_MS / 1000}s`,
|
||||
),
|
||||
),
|
||||
PROBE_TIMEOUT_MS,
|
||||
),
|
||||
),
|
||||
])
|
||||
|
||||
const stdout = await new Response(proc.stdout).text()
|
||||
const stderr = await new Response(proc.stderr).text()
|
||||
|
||||
if (result !== 0) {
|
||||
const detail = stderr.trim() || `exit code ${result}`
|
||||
throw new SSHProbeError(`SSH probe failed: ${detail}`)
|
||||
}
|
||||
|
||||
const lines = stdout
|
||||
.split('\n')
|
||||
.map(l => l.trim())
|
||||
.filter(Boolean)
|
||||
logForDebugging(`[SSHProbe] raw lines: ${JSON.stringify(lines)}`)
|
||||
|
||||
const unameIdx = lines.findIndex(l => /^(Linux|Darwin)\s/.test(l))
|
||||
if (unameIdx === -1) {
|
||||
throw new SSHProbeError(
|
||||
'Could not detect remote platform (uname output missing)',
|
||||
)
|
||||
}
|
||||
|
||||
const binaryPath = unameIdx >= 2 ? lines[unameIdx - 2] || null : null
|
||||
const versionLine = unameIdx >= 1 ? lines[unameIdx - 1] || null : null
|
||||
const remoteVersion =
|
||||
versionLine && /^\d+\.\d+/.test(versionLine) ? versionLine : null
|
||||
const hasBinary = binaryPath !== null && binaryPath.startsWith('/')
|
||||
const defaultCwd = lines[unameIdx + 1] || '/'
|
||||
|
||||
const [osName, arch] = lines[unameIdx]!.split(/\s+/)
|
||||
|
||||
const remotePlatform = osName === 'Darwin' ? 'darwin' : 'linux'
|
||||
const remoteArch: 'x64' | 'arm64' =
|
||||
arch === 'aarch64' || arch === 'arm64' ? 'arm64' : 'x64'
|
||||
|
||||
onProgress?.(`Detected ${remotePlatform}/${remoteArch}`)
|
||||
|
||||
return {
|
||||
hasBinary: hasBinary && remoteVersion !== null,
|
||||
remoteVersion,
|
||||
remotePlatform,
|
||||
remoteArch,
|
||||
defaultCwd,
|
||||
binaryPath: hasBinary ? binaryPath : null,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user