@triflux/remote 10.74.0 → 10.76.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/hub/team/cli/commands/start/parse-args.mjs +5 -2
- package/hub/team/cli/commands/start/start-headless.mjs +11 -1
- package/hub/team/cli/help.mjs +1 -0
- package/hub/team/cli/services/runtime-mode.mjs +3 -1
- package/hub/team/psmux.mjs +9 -2
- package/hub/team/remote-session.mjs +209 -27
- package/package.json +1 -1
|
@@ -46,7 +46,8 @@ export function parseTeamArgs(args = []) {
|
|
|
46
46
|
let teammateMode = "auto";
|
|
47
47
|
const taskParts = [];
|
|
48
48
|
const assigns = [];
|
|
49
|
-
|
|
49
|
+
// null 이면 start-headless 가 플랫폼으로 정한다: Windows 만 켬 (ADR-0040)
|
|
50
|
+
let autoAttach = null;
|
|
50
51
|
let progressive = true;
|
|
51
52
|
let timeoutSec = 0;
|
|
52
53
|
let verbose = false;
|
|
@@ -82,6 +83,8 @@ export function parseTeamArgs(args = []) {
|
|
|
82
83
|
} else if (current === "--assign" && args[index + 1]) {
|
|
83
84
|
const parsed = parseAssignValue(args[++index]);
|
|
84
85
|
if (parsed) assigns.push(parsed);
|
|
86
|
+
} else if (current === "--auto-attach") {
|
|
87
|
+
autoAttach = true;
|
|
85
88
|
} else if (current === "--no-auto-attach") {
|
|
86
89
|
autoAttach = false;
|
|
87
90
|
} else if (current === "--verbose") {
|
|
@@ -143,7 +146,7 @@ export function parseTeamArgs(args = []) {
|
|
|
143
146
|
p = p[1].toUpperCase() + ":" + p.slice(2);
|
|
144
147
|
}
|
|
145
148
|
cwd = resolve(p);
|
|
146
|
-
} else if (current === "--
|
|
149
|
+
} else if (current === "--dashboard") {
|
|
147
150
|
// 이전 설치 스킬이 넘기는 기본값 플래그는 무시한다.
|
|
148
151
|
} else if (current.startsWith("-")) {
|
|
149
152
|
console.warn(` ⚠ 미인식 플래그 무시: ${current}`);
|
|
@@ -107,13 +107,23 @@ export async function startHeadlessTeam({
|
|
|
107
107
|
);
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
// macOS/Linux 는 claude agents 행이 관전 창이라 호출자 tmux 창을 나누지 않는다.
|
|
111
|
+
// Windows 는 그 행이 없어 WT 탭 attach 를 켠다 (ADR-0040).
|
|
112
|
+
const platform = _deps.platform || process.platform;
|
|
113
|
+
const effectiveAutoAttach = autoAttach ?? platform === "win32";
|
|
114
|
+
if (effectiveAutoAttach && platform !== "win32" && process.env.TMUX_PANE) {
|
|
115
|
+
ok(
|
|
116
|
+
"--auto-attach: 호출자 tmux 창을 아래로 30% 나눠 워커 방에 attach 함 (끄기: --no-auto-attach)",
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
110
120
|
let handle;
|
|
111
121
|
try {
|
|
112
122
|
const run = _deps.runHeadlessInteractive || runHeadlessInteractive;
|
|
113
123
|
handle = await run(sessionId, assignments, {
|
|
114
124
|
timeoutSec: timeoutSec || 900,
|
|
115
125
|
layout,
|
|
116
|
-
autoAttach:
|
|
126
|
+
autoAttach: effectiveAutoAttach,
|
|
117
127
|
leadPane: process.env.TMUX_PANE,
|
|
118
128
|
leadTmux: process.env.TMUX,
|
|
119
129
|
dashboard: !!dashboard,
|
package/hub/team/cli/help.mjs
CHANGED
|
@@ -9,6 +9,7 @@ export function renderTeamHelp() {
|
|
|
9
9
|
${WHITE}tfx multi --agents codex,antigravity --lead claude "작업"${RESET}
|
|
10
10
|
${WHITE}tfx multi --teammate-mode tmux "작업"${RESET}
|
|
11
11
|
${WHITE}tfx multi --teammate-mode headless "작업"${RESET}
|
|
12
|
+
${WHITE}tfx multi --auto-attach "작업"${RESET} ${DIM}(headless: 호출자 tmux 창을 나눠 attach. Windows 는 기본 켬)${RESET}
|
|
12
13
|
${WHITE}tfx multi --layout 1xN "작업"${RESET} ${DIM}(세로 분할 컬럼)${RESET}
|
|
13
14
|
${WHITE}tfx multi --layout Nx1 "작업"${RESET} ${DIM}(가로 분할 스택)${RESET}
|
|
14
15
|
${WHITE}tfx multi --dashboard-layout lite "작업"${RESET} ${DIM}(dashboard-lite 기본 뷰)${RESET}
|
|
@@ -13,12 +13,14 @@ export function normalizeTeammateMode(mode = "auto", deps = {}) {
|
|
|
13
13
|
`지원하지 않는 teammate mode: ${mode}; auto, headless, tmux${platform === "win32" ? ", psmux" : ""}를 사용하세요.`,
|
|
14
14
|
);
|
|
15
15
|
}
|
|
16
|
+
// 비TTY 호출자(파이프, Claude Code Bash 도구, CI)는 대화형 팀에 입력할 수 없고
|
|
17
|
+
// 결과 색인도 headless 에서만 나온다. tmux 안이라도 headless 로 간다 (ADR-0040).
|
|
18
|
+
if (!(deps.isTTY ?? Boolean(process.stdout.isTTY))) return "headless";
|
|
16
19
|
if (env.TMUX) return "tmux";
|
|
17
20
|
const mux = detectMux();
|
|
18
21
|
if (mux === "tmux") return "tmux";
|
|
19
22
|
if (platform === "win32" && mux === "psmux") return "psmux";
|
|
20
23
|
if (platform === "win32" && mux === "git-bash-tmux") return "tmux";
|
|
21
|
-
if (!(deps.isTTY ?? Boolean(process.stdout.isTTY))) return "headless";
|
|
22
24
|
return ensureTmuxOrExit(deps);
|
|
23
25
|
}
|
|
24
26
|
|
package/hub/team/psmux.mjs
CHANGED
|
@@ -997,11 +997,18 @@ function killOrphanMcpProcesses(sessionName) {
|
|
|
997
997
|
function detachAttachedClients(sessionName, waitMs = 750) {
|
|
998
998
|
const attachedCount = getPsmuxSessionAttachedCount(sessionName);
|
|
999
999
|
if (!Number.isFinite(attachedCount) || attachedCount <= 0) return false;
|
|
1000
|
+
// -t 는 클라이언트 이름이다. 세션에 붙은 클라이언트 전부를 떼려면 -s 로 세션을 주고,
|
|
1001
|
+
// tmux 의 접두사 매칭이 같은 접두사의 다른 세션을 떼지 않게 정확한 타깃을 쓴다.
|
|
1000
1002
|
try {
|
|
1001
|
-
psmuxExec(["detach-client", "-
|
|
1003
|
+
psmuxExec(["detach-client", "-s", exactSessionTarget(sessionName)], {
|
|
1004
|
+
stdio: "ignore",
|
|
1005
|
+
});
|
|
1002
1006
|
sleepMs(waitMs);
|
|
1003
1007
|
return true;
|
|
1004
|
-
} catch {
|
|
1008
|
+
} catch (error) {
|
|
1009
|
+
process.stderr.write(
|
|
1010
|
+
`[psmux] detach-client 실패 (${sessionName}): ${error?.message || error}\n`,
|
|
1011
|
+
);
|
|
1005
1012
|
return false;
|
|
1006
1013
|
}
|
|
1007
1014
|
}
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
// Remote session primitives.
|
|
2
2
|
// Pure functions + SSH operations. No psmux, no WT, no CLI arg parsing.
|
|
3
3
|
|
|
4
|
-
import { execFileSync } from "node:child_process";
|
|
5
|
-
import {
|
|
4
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
5
|
+
import {
|
|
6
|
+
chmodSync,
|
|
7
|
+
existsSync,
|
|
8
|
+
lstatSync,
|
|
9
|
+
mkdirSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from "node:fs";
|
|
6
13
|
import { homedir } from "node:os";
|
|
7
14
|
import {
|
|
8
15
|
basename,
|
|
@@ -17,6 +24,45 @@ const REMOTE_ENV_TTL_MS = 86_400_000; // 24h
|
|
|
17
24
|
const REMOTE_STAGE_ROOT = "tfx-remote";
|
|
18
25
|
const SAFE_HOST_RE = /^[a-zA-Z0-9._-]+$/;
|
|
19
26
|
|
|
27
|
+
let sshControlDir = null;
|
|
28
|
+
if (process.platform !== "win32" && typeof process.getuid === "function") {
|
|
29
|
+
// macOS Unix 소켓 경로의 104바이트 제한 때문에 $TMPDIR 대신 /tmp를 쓴다.
|
|
30
|
+
const dir = `/tmp/tfx-cm-${process.getuid()}`;
|
|
31
|
+
try {
|
|
32
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
33
|
+
const stat = lstatSync(dir);
|
|
34
|
+
if (stat.isDirectory() && stat.uid === process.getuid()) {
|
|
35
|
+
chmodSync(dir, 0o700);
|
|
36
|
+
sshControlDir = dir;
|
|
37
|
+
}
|
|
38
|
+
} catch {}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function buildSshOptions({
|
|
42
|
+
interactive = false,
|
|
43
|
+
platform = process.platform,
|
|
44
|
+
} = {}) {
|
|
45
|
+
const options = [
|
|
46
|
+
"-o",
|
|
47
|
+
"ServerAliveInterval=15",
|
|
48
|
+
"-o",
|
|
49
|
+
"ServerAliveCountMax=4",
|
|
50
|
+
];
|
|
51
|
+
if (!interactive)
|
|
52
|
+
options.push("-o", "BatchMode=yes", "-o", "ConnectTimeout=10");
|
|
53
|
+
if (platform !== "win32" && sshControlDir) {
|
|
54
|
+
options.push(
|
|
55
|
+
"-o",
|
|
56
|
+
"ControlMaster=auto",
|
|
57
|
+
"-o",
|
|
58
|
+
`ControlPath=${sshControlDir}/%C`,
|
|
59
|
+
"-o",
|
|
60
|
+
"ControlPersist=60",
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return options;
|
|
64
|
+
}
|
|
65
|
+
|
|
20
66
|
// ── Shell quoting utilities ─────────────────────────────────────
|
|
21
67
|
|
|
22
68
|
export function shellQuote(value) {
|
|
@@ -76,19 +122,107 @@ function parseProbeLines(text) {
|
|
|
76
122
|
function normalizePwshProbeEnv(parsed) {
|
|
77
123
|
if (parsed.shell !== "pwsh" || parsed.os !== "win32") return null;
|
|
78
124
|
if (!parsed.home) return null;
|
|
125
|
+
const psmuxVersion = parsed.psmuxVersion || null;
|
|
126
|
+
const version = psmuxVersion && parseVersion(psmuxVersion);
|
|
79
127
|
return Object.freeze({
|
|
80
128
|
claudePath:
|
|
81
129
|
!parsed.claude || parsed.claude === "notfound" ? null : parsed.claude,
|
|
130
|
+
psmuxPath:
|
|
131
|
+
!parsed.psmux || parsed.psmux === "notfound" ? null : parsed.psmux,
|
|
132
|
+
psmuxVersion,
|
|
133
|
+
// tfx-psmux.md 규칙 5의 3.3.x detach 미지원 범위를 제외한다. 3.4.0은 일반 이름도 정확 매칭한다.
|
|
134
|
+
psmuxSupported: Boolean(
|
|
135
|
+
parsed.psmux &&
|
|
136
|
+
parsed.psmux !== "notfound" &&
|
|
137
|
+
version &&
|
|
138
|
+
compareVersions(version, [3, 4, 0]) >= 0,
|
|
139
|
+
),
|
|
82
140
|
home: parsed.home,
|
|
83
141
|
os: "win32",
|
|
84
142
|
shell: "pwsh",
|
|
85
143
|
});
|
|
86
144
|
}
|
|
87
145
|
|
|
88
|
-
function
|
|
146
|
+
function parseVersion(versionStr) {
|
|
147
|
+
const match = /(\d+)\.(\d+)\.(\d+)/.exec(versionStr);
|
|
148
|
+
if (!match) return null;
|
|
149
|
+
return [
|
|
150
|
+
parseInt(match[1], 10),
|
|
151
|
+
parseInt(match[2], 10),
|
|
152
|
+
parseInt(match[3], 10),
|
|
153
|
+
];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function compareVersions(a, b) {
|
|
157
|
+
for (let i = 0; i < 3; i++) {
|
|
158
|
+
if (a[i] !== b[i]) return a[i] - b[i];
|
|
159
|
+
}
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function probeVersion(binPath) {
|
|
164
|
+
try {
|
|
165
|
+
if (/\.(cmd|bat)$/iu.test(binPath)) {
|
|
166
|
+
// .cmd/.bat → execSync로 shell 경유 (execFileSync EINVAL 회피)
|
|
167
|
+
const out = execSync(`"${binPath}" --version`, {
|
|
168
|
+
encoding: "utf8",
|
|
169
|
+
timeout: 3000,
|
|
170
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
171
|
+
});
|
|
172
|
+
return parseVersion(out);
|
|
173
|
+
}
|
|
174
|
+
const out = execFileSync(binPath, ["--version"], {
|
|
175
|
+
encoding: "utf8",
|
|
176
|
+
timeout: 3000,
|
|
177
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
178
|
+
});
|
|
179
|
+
return parseVersion(out);
|
|
180
|
+
} catch {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function normalizePosixProbeEnv(parsed, localVersions = null) {
|
|
89
186
|
const os =
|
|
90
187
|
parsed.os === "darwin" ? "darwin" : parsed.os === "linux" ? "linux" : null;
|
|
91
|
-
if (!os || !parsed.home)
|
|
188
|
+
if (!os || !parsed.home) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const versions = Object.fromEntries(
|
|
193
|
+
["node", "codex", "agy", "claude", "triflux"].map((name) => [
|
|
194
|
+
name,
|
|
195
|
+
parsed[`${name}Version`] || null,
|
|
196
|
+
]),
|
|
197
|
+
);
|
|
198
|
+
const warnings = [];
|
|
199
|
+
const memoryPressureLevel =
|
|
200
|
+
os === "darwin" ? Number(parsed.memoryPressureLevel) || null : null;
|
|
201
|
+
const memoryFreePct =
|
|
202
|
+
parsed.memoryFreePct && Number.isFinite(Number(parsed.memoryFreePct))
|
|
203
|
+
? Number(parsed.memoryFreePct)
|
|
204
|
+
: null;
|
|
205
|
+
const loadAvgValues = parsed.loadAvg?.split(",").map(Number);
|
|
206
|
+
const loadAvg =
|
|
207
|
+
loadAvgValues?.length === 3 && loadAvgValues.every(Number.isFinite)
|
|
208
|
+
? loadAvgValues
|
|
209
|
+
: null;
|
|
210
|
+
const diskFreeHome = parsed.diskFreeHome ? Number(parsed.diskFreeHome) : null;
|
|
211
|
+
|
|
212
|
+
if (memoryPressureLevel >= 2) {
|
|
213
|
+
warnings.push(`memory pressure level ${memoryPressureLevel}`);
|
|
214
|
+
}
|
|
215
|
+
if (diskFreeHome !== null && diskFreeHome < 5 * 1024 ** 3) {
|
|
216
|
+
warnings.push("home disk free below 5 GiB");
|
|
217
|
+
}
|
|
218
|
+
for (const [name, remote] of Object.entries(versions)) {
|
|
219
|
+
const local = localVersions?.[name];
|
|
220
|
+
const remoteVersion = remote && parseVersion(remote);
|
|
221
|
+
if (local && remoteVersion && compareVersions(local, remoteVersion) !== 0) {
|
|
222
|
+
warnings.push(`${name} version differs`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
92
226
|
return Object.freeze({
|
|
93
227
|
claudePath:
|
|
94
228
|
!parsed.claude || parsed.claude === "notfound" ? null : parsed.claude,
|
|
@@ -96,26 +230,36 @@ function normalizePosixProbeEnv(parsed) {
|
|
|
96
230
|
!parsed.codex || parsed.codex === "notfound" ? null : parsed.codex,
|
|
97
231
|
geminiPath:
|
|
98
232
|
!parsed.gemini || parsed.gemini === "notfound" ? null : parsed.gemini,
|
|
233
|
+
tmuxPath: !parsed.tmux || parsed.tmux === "notfound" ? null : parsed.tmux,
|
|
234
|
+
node: parsed.node || parsed.nodeVersion || null,
|
|
235
|
+
cores: parsed.cores ? Number(parsed.cores) : null,
|
|
236
|
+
ramGb: parsed.ram_gb ? Number(parsed.ram_gb) : null,
|
|
99
237
|
home: parsed.home,
|
|
238
|
+
codexAuthExists: parsed.codexAuthExists === "true",
|
|
239
|
+
diskFreeHome,
|
|
240
|
+
loadAvg,
|
|
241
|
+
memoryFreePct,
|
|
242
|
+
memoryPressureLevel,
|
|
100
243
|
os,
|
|
244
|
+
ready: memoryPressureLevel !== 4,
|
|
101
245
|
shell: parsed.shell === "zsh" ? "zsh" : "bash",
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
ramGb: parsed.ram_gb ? Number(parsed.ram_gb) : null,
|
|
246
|
+
versions,
|
|
247
|
+
warnings,
|
|
105
248
|
});
|
|
106
249
|
}
|
|
107
250
|
|
|
108
|
-
function probeRemoteEnvViaPwsh(host) {
|
|
251
|
+
function probeRemoteEnvViaPwsh(host, execSsh) {
|
|
109
252
|
const command = [
|
|
110
253
|
"Write-Output 'shell=pwsh'",
|
|
111
254
|
'Write-Output "home=$env:USERPROFILE"',
|
|
112
255
|
'if (Test-Path "$env:USERPROFILE\\.local\\bin\\claude.exe") { Write-Output "claude=$env:USERPROFILE\\.local\\bin\\claude.exe" } elseif (Get-Command claude -ErrorAction SilentlyContinue) { Write-Output "claude=$((Get-Command claude).Source)" } else { Write-Output \'claude=notfound\' }',
|
|
256
|
+
'if (Get-Command psmux -ErrorAction SilentlyContinue) { Write-Output "psmux=$((Get-Command psmux).Source)"; $psmuxVersion = psmux -V 2>$null; if ($LASTEXITCODE -eq 0) { Write-Output "psmuxVersion=$psmuxVersion" } } else { Write-Output \'psmux=notfound\' }',
|
|
113
257
|
"Write-Output \"os=$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows) ? 'win32' : 'other')\"",
|
|
114
258
|
].join("; ");
|
|
115
259
|
|
|
116
260
|
try {
|
|
117
|
-
const output =
|
|
118
|
-
[
|
|
261
|
+
const output = execSsh(
|
|
262
|
+
[...buildSshOptions(), host, pwshRemoteCommand(command)],
|
|
119
263
|
{
|
|
120
264
|
encoding: "utf8",
|
|
121
265
|
timeout: 15000,
|
|
@@ -130,7 +274,7 @@ function probeRemoteEnvViaPwsh(host) {
|
|
|
130
274
|
}
|
|
131
275
|
}
|
|
132
276
|
|
|
133
|
-
function probeRemoteEnvViaPosix(host) {
|
|
277
|
+
function probeRemoteEnvViaPosix(host, execSsh, localVersions) {
|
|
134
278
|
const script = [
|
|
135
279
|
'[ -f "$HOME/.zshenv" ] && . "$HOME/.zshenv" 2>/dev/null || true',
|
|
136
280
|
"export PATH=/opt/homebrew/bin:/opt/homebrew/sbin:$HOME/.local/bin:$HOME/.nvm/versions/node/$(ls $HOME/.nvm/versions/node 2>/dev/null | sort -V | tail -1)/bin:$PATH 2>/dev/null",
|
|
@@ -140,22 +284,40 @@ function probeRemoteEnvViaPosix(host) {
|
|
|
140
284
|
"command -v codex >/dev/null 2>&1 && echo codex=$(command -v codex) || echo codex=notfound",
|
|
141
285
|
"command -v gemini >/dev/null 2>&1 && echo gemini=$(command -v gemini) || echo gemini=notfound",
|
|
142
286
|
"echo os=$(uname -s | tr A-Z a-z)",
|
|
143
|
-
"node --version 2>/dev/null && echo node=$(node --version) || echo node=notfound",
|
|
144
287
|
// darwin: sysctl, linux: nproc + /proc/meminfo
|
|
145
288
|
"if [ $(uname -s) = Darwin ]; then echo cores=$(sysctl -n hw.ncpu); echo ram_gb=$(($(sysctl -n hw.memsize) / 1073741824)); else echo cores=$(nproc 2>/dev/null || echo 0); echo ram_gb=$(($(grep MemTotal /proc/meminfo 2>/dev/null | awk '{print $2}') / 1048576)); fi",
|
|
289
|
+
"command -v tmux >/dev/null 2>&1 && echo tmux=$(command -v tmux) || echo tmux=notfound",
|
|
290
|
+
'if [ "$(uname -s)" = Darwin ]; then',
|
|
291
|
+
" printf 'memoryPressureLevel=%s\\n' \"$(sysctl -n kern.memorystatus_vm_pressure_level 2>/dev/null)\"",
|
|
292
|
+
' vm_stat | awk -v total="$(sysctl -n hw.memsize 2>/dev/null)" -v pageSize="$(sysctl -n hw.pagesize 2>/dev/null)" \'/Pages free:|Pages inactive:|Pages speculative:/ {gsub(/[^0-9]/, "", $3); pages += $3} END {if (total > 0 && pageSize > 0) printf "memoryFreePct=%.1f\\n", pages * pageSize / total * 100}\'',
|
|
293
|
+
' sysctl -n vm.loadavg 2>/dev/null | tr -d \'{}\' | awk \'{print "loadAvg=" $1 "," $2 "," $3}\'',
|
|
294
|
+
"else",
|
|
295
|
+
" awk '/MemTotal:/ {total=$2} /MemAvailable:/ {available=$2} END {if (total > 0) printf \"memoryFreePct=%.1f\\n\", available / total * 100}' /proc/meminfo 2>/dev/null",
|
|
296
|
+
' awk \'{print "loadAvg=" $1 "," $2 "," $3}\' /proc/loadavg 2>/dev/null',
|
|
297
|
+
"fi",
|
|
298
|
+
'df -Pk "$HOME" 2>/dev/null | awk \'NR==2 {printf "diskFreeHome=%.0f\\n", $4 * 1024}\'',
|
|
299
|
+
'if [ -f "${CODEX_HOME:-$HOME/.codex}/auth.json" ]; then echo codexAuthExists=true; else echo codexAuthExists=false; fi',
|
|
300
|
+
...(localVersions
|
|
301
|
+
? [
|
|
302
|
+
'probe_version() { command -v "$1" >/dev/null 2>&1 && "$1" --version </dev/null 2>/dev/null | head -n 1; }',
|
|
303
|
+
'for cli in node codex agy claude triflux; do printf \'%sVersion=%s\\n\' "$cli" "$(probe_version "$cli")"; done',
|
|
304
|
+
]
|
|
305
|
+
: []),
|
|
146
306
|
].join("\n");
|
|
147
307
|
|
|
148
308
|
try {
|
|
149
|
-
const output =
|
|
309
|
+
const output = execSsh([...buildSshOptions(), host, "sh"], {
|
|
150
310
|
encoding: "utf8",
|
|
151
311
|
timeout: 15000,
|
|
152
312
|
input: script,
|
|
153
313
|
maxRetries: 2,
|
|
154
314
|
baseDelayMs: 1000,
|
|
155
315
|
});
|
|
156
|
-
return normalizePosixProbeEnv(parseProbeLines(output));
|
|
157
|
-
} catch {
|
|
158
|
-
|
|
316
|
+
return normalizePosixProbeEnv(parseProbeLines(output), localVersions);
|
|
317
|
+
} catch (error) {
|
|
318
|
+
throw new Error(`remote probe failed for ${host}: ${error.message}`, {
|
|
319
|
+
cause: error,
|
|
320
|
+
});
|
|
159
321
|
}
|
|
160
322
|
}
|
|
161
323
|
|
|
@@ -223,22 +385,35 @@ function writeEnvCache(host, env, cacheDir) {
|
|
|
223
385
|
export function probeRemoteEnv(host, opts = {}) {
|
|
224
386
|
validateHost(host);
|
|
225
387
|
const force = opts.force === true;
|
|
388
|
+
const execSsh = opts.execSsh || execSshWithRetry;
|
|
226
389
|
const cacheDir = opts.cacheDir || remoteEnvCacheDir();
|
|
227
390
|
|
|
228
391
|
if (!force) {
|
|
229
392
|
const cached = readEnvCache(host, cacheDir);
|
|
230
|
-
if (
|
|
393
|
+
if (
|
|
394
|
+
isEnvCacheFresh(cached) &&
|
|
395
|
+
cached.env.claudePath &&
|
|
396
|
+
(!opts.osHint ||
|
|
397
|
+
cached.env.os === opts.osHint ||
|
|
398
|
+
(opts.osHint === "windows" && cached.env.os === "win32")) &&
|
|
399
|
+
(cached.env.os === "win32"
|
|
400
|
+
? "psmuxVersion" in cached.env && "psmuxSupported" in cached.env
|
|
401
|
+
: "tmuxPath" in cached.env && "versions" in cached.env)
|
|
402
|
+
)
|
|
403
|
+
return cached.env;
|
|
231
404
|
}
|
|
232
405
|
|
|
233
|
-
const pwshEnv =
|
|
406
|
+
const pwshEnv = ["darwin", "linux"].includes(opts.osHint)
|
|
407
|
+
? null
|
|
408
|
+
: probeRemoteEnvViaPwsh(host, execSsh);
|
|
234
409
|
if (pwshEnv) {
|
|
235
|
-
writeEnvCache(host, pwshEnv, cacheDir);
|
|
410
|
+
if (pwshEnv.claudePath) writeEnvCache(host, pwshEnv, cacheDir);
|
|
236
411
|
return pwshEnv;
|
|
237
412
|
}
|
|
238
413
|
|
|
239
|
-
const posixEnv = probeRemoteEnvViaPosix(host);
|
|
414
|
+
const posixEnv = probeRemoteEnvViaPosix(host, execSsh, opts.localVersions);
|
|
240
415
|
if (posixEnv) {
|
|
241
|
-
writeEnvCache(host, posixEnv, cacheDir);
|
|
416
|
+
if (posixEnv.claudePath) writeEnvCache(host, posixEnv, cacheDir);
|
|
242
417
|
return posixEnv;
|
|
243
418
|
}
|
|
244
419
|
|
|
@@ -303,7 +478,10 @@ export function ensureRemoteStageDir(host, env, remoteStageDir) {
|
|
|
303
478
|
`New-Item -ItemType Directory -Path '${escapePwshSingleQuoted(remoteStageDir)}' -Force | Out-Null`,
|
|
304
479
|
)
|
|
305
480
|
: posixRemoteCommand(`mkdir -p ${shellQuote(remoteStageDir)}`);
|
|
306
|
-
execFileSync("ssh", [host, command], {
|
|
481
|
+
execFileSync("ssh", [...buildSshOptions(), host, command], {
|
|
482
|
+
timeout: 10000,
|
|
483
|
+
stdio: "pipe",
|
|
484
|
+
});
|
|
307
485
|
}
|
|
308
486
|
|
|
309
487
|
/**
|
|
@@ -313,10 +491,14 @@ export function ensureRemoteStageDir(host, env, remoteStageDir) {
|
|
|
313
491
|
* @param {string} remotePath
|
|
314
492
|
*/
|
|
315
493
|
export function uploadFileToRemote(host, localPath, remotePath) {
|
|
316
|
-
execFileSync(
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
494
|
+
execFileSync(
|
|
495
|
+
"scp",
|
|
496
|
+
[...buildSshOptions(), localPath, `${host}:${remotePath}`],
|
|
497
|
+
{
|
|
498
|
+
timeout: 15000,
|
|
499
|
+
stdio: "pipe",
|
|
500
|
+
},
|
|
501
|
+
);
|
|
320
502
|
}
|
|
321
503
|
|
|
322
504
|
/**
|
|
@@ -372,7 +554,7 @@ export function remoteGit(host, env, gitArgs, cwd) {
|
|
|
372
554
|
: posixRemoteCommand(
|
|
373
555
|
`cd ${shellQuote(cwd)} && ${["git", ...gitArgs].map(shellQuote).join(" ")}`,
|
|
374
556
|
);
|
|
375
|
-
return execFileSync("ssh", [host, command], {
|
|
557
|
+
return execFileSync("ssh", [...buildSshOptions(), host, command], {
|
|
376
558
|
encoding: "utf8",
|
|
377
559
|
timeout: 30_000,
|
|
378
560
|
stdio: ["pipe", "pipe", "pipe"],
|