@triflux/remote 10.73.0 → 10.75.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 +195 -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) {
|
|
@@ -85,10 +131,86 @@ function normalizePwshProbeEnv(parsed) {
|
|
|
85
131
|
});
|
|
86
132
|
}
|
|
87
133
|
|
|
88
|
-
function
|
|
134
|
+
function parseVersion(versionStr) {
|
|
135
|
+
const match = /(\d+)\.(\d+)\.(\d+)/.exec(versionStr);
|
|
136
|
+
if (!match) return null;
|
|
137
|
+
return [
|
|
138
|
+
parseInt(match[1], 10),
|
|
139
|
+
parseInt(match[2], 10),
|
|
140
|
+
parseInt(match[3], 10),
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function compareVersions(a, b) {
|
|
145
|
+
for (let i = 0; i < 3; i++) {
|
|
146
|
+
if (a[i] !== b[i]) return a[i] - b[i];
|
|
147
|
+
}
|
|
148
|
+
return 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function probeVersion(binPath) {
|
|
152
|
+
try {
|
|
153
|
+
if (/\.(cmd|bat)$/iu.test(binPath)) {
|
|
154
|
+
// .cmd/.bat → execSync로 shell 경유 (execFileSync EINVAL 회피)
|
|
155
|
+
const out = execSync(`"${binPath}" --version`, {
|
|
156
|
+
encoding: "utf8",
|
|
157
|
+
timeout: 3000,
|
|
158
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
159
|
+
});
|
|
160
|
+
return parseVersion(out);
|
|
161
|
+
}
|
|
162
|
+
const out = execFileSync(binPath, ["--version"], {
|
|
163
|
+
encoding: "utf8",
|
|
164
|
+
timeout: 3000,
|
|
165
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
166
|
+
});
|
|
167
|
+
return parseVersion(out);
|
|
168
|
+
} catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function normalizePosixProbeEnv(parsed, localVersions = null) {
|
|
89
174
|
const os =
|
|
90
175
|
parsed.os === "darwin" ? "darwin" : parsed.os === "linux" ? "linux" : null;
|
|
91
|
-
if (!os || !parsed.home)
|
|
176
|
+
if (!os || !parsed.home) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const versions = Object.fromEntries(
|
|
181
|
+
["node", "codex", "agy", "claude", "triflux"].map((name) => [
|
|
182
|
+
name,
|
|
183
|
+
parsed[`${name}Version`] || null,
|
|
184
|
+
]),
|
|
185
|
+
);
|
|
186
|
+
const warnings = [];
|
|
187
|
+
const memoryPressureLevel =
|
|
188
|
+
os === "darwin" ? Number(parsed.memoryPressureLevel) || null : null;
|
|
189
|
+
const memoryFreePct =
|
|
190
|
+
parsed.memoryFreePct && Number.isFinite(Number(parsed.memoryFreePct))
|
|
191
|
+
? Number(parsed.memoryFreePct)
|
|
192
|
+
: null;
|
|
193
|
+
const loadAvgValues = parsed.loadAvg?.split(",").map(Number);
|
|
194
|
+
const loadAvg =
|
|
195
|
+
loadAvgValues?.length === 3 && loadAvgValues.every(Number.isFinite)
|
|
196
|
+
? loadAvgValues
|
|
197
|
+
: null;
|
|
198
|
+
const diskFreeHome = parsed.diskFreeHome ? Number(parsed.diskFreeHome) : null;
|
|
199
|
+
|
|
200
|
+
if (memoryPressureLevel >= 2) {
|
|
201
|
+
warnings.push(`memory pressure level ${memoryPressureLevel}`);
|
|
202
|
+
}
|
|
203
|
+
if (diskFreeHome !== null && diskFreeHome < 5 * 1024 ** 3) {
|
|
204
|
+
warnings.push("home disk free below 5 GiB");
|
|
205
|
+
}
|
|
206
|
+
for (const [name, remote] of Object.entries(versions)) {
|
|
207
|
+
const local = localVersions?.[name];
|
|
208
|
+
const remoteVersion = remote && parseVersion(remote);
|
|
209
|
+
if (local && remoteVersion && compareVersions(local, remoteVersion) !== 0) {
|
|
210
|
+
warnings.push(`${name} version differs`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
92
214
|
return Object.freeze({
|
|
93
215
|
claudePath:
|
|
94
216
|
!parsed.claude || parsed.claude === "notfound" ? null : parsed.claude,
|
|
@@ -96,16 +218,25 @@ function normalizePosixProbeEnv(parsed) {
|
|
|
96
218
|
!parsed.codex || parsed.codex === "notfound" ? null : parsed.codex,
|
|
97
219
|
geminiPath:
|
|
98
220
|
!parsed.gemini || parsed.gemini === "notfound" ? null : parsed.gemini,
|
|
221
|
+
tmuxPath: !parsed.tmux || parsed.tmux === "notfound" ? null : parsed.tmux,
|
|
222
|
+
node: parsed.node || parsed.nodeVersion || null,
|
|
223
|
+
cores: parsed.cores ? Number(parsed.cores) : null,
|
|
224
|
+
ramGb: parsed.ram_gb ? Number(parsed.ram_gb) : null,
|
|
99
225
|
home: parsed.home,
|
|
226
|
+
codexAuthExists: parsed.codexAuthExists === "true",
|
|
227
|
+
diskFreeHome,
|
|
228
|
+
loadAvg,
|
|
229
|
+
memoryFreePct,
|
|
230
|
+
memoryPressureLevel,
|
|
100
231
|
os,
|
|
232
|
+
ready: memoryPressureLevel !== 4,
|
|
101
233
|
shell: parsed.shell === "zsh" ? "zsh" : "bash",
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
ramGb: parsed.ram_gb ? Number(parsed.ram_gb) : null,
|
|
234
|
+
versions,
|
|
235
|
+
warnings,
|
|
105
236
|
});
|
|
106
237
|
}
|
|
107
238
|
|
|
108
|
-
function probeRemoteEnvViaPwsh(host) {
|
|
239
|
+
function probeRemoteEnvViaPwsh(host, execSsh) {
|
|
109
240
|
const command = [
|
|
110
241
|
"Write-Output 'shell=pwsh'",
|
|
111
242
|
'Write-Output "home=$env:USERPROFILE"',
|
|
@@ -114,8 +245,8 @@ function probeRemoteEnvViaPwsh(host) {
|
|
|
114
245
|
].join("; ");
|
|
115
246
|
|
|
116
247
|
try {
|
|
117
|
-
const output =
|
|
118
|
-
[host, "pwsh", "-NoProfile", "-Command", command],
|
|
248
|
+
const output = execSsh(
|
|
249
|
+
[...buildSshOptions(), host, "pwsh", "-NoProfile", "-Command", command],
|
|
119
250
|
{
|
|
120
251
|
encoding: "utf8",
|
|
121
252
|
timeout: 15000,
|
|
@@ -130,7 +261,7 @@ function probeRemoteEnvViaPwsh(host) {
|
|
|
130
261
|
}
|
|
131
262
|
}
|
|
132
263
|
|
|
133
|
-
function probeRemoteEnvViaPosix(host) {
|
|
264
|
+
function probeRemoteEnvViaPosix(host, execSsh, localVersions) {
|
|
134
265
|
const script = [
|
|
135
266
|
'[ -f "$HOME/.zshenv" ] && . "$HOME/.zshenv" 2>/dev/null || true',
|
|
136
267
|
"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 +271,40 @@ function probeRemoteEnvViaPosix(host) {
|
|
|
140
271
|
"command -v codex >/dev/null 2>&1 && echo codex=$(command -v codex) || echo codex=notfound",
|
|
141
272
|
"command -v gemini >/dev/null 2>&1 && echo gemini=$(command -v gemini) || echo gemini=notfound",
|
|
142
273
|
"echo os=$(uname -s | tr A-Z a-z)",
|
|
143
|
-
"node --version 2>/dev/null && echo node=$(node --version) || echo node=notfound",
|
|
144
274
|
// darwin: sysctl, linux: nproc + /proc/meminfo
|
|
145
275
|
"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",
|
|
276
|
+
"command -v tmux >/dev/null 2>&1 && echo tmux=$(command -v tmux) || echo tmux=notfound",
|
|
277
|
+
'if [ "$(uname -s)" = Darwin ]; then',
|
|
278
|
+
" printf 'memoryPressureLevel=%s\\n' \"$(sysctl -n kern.memorystatus_vm_pressure_level 2>/dev/null)\"",
|
|
279
|
+
' 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}\'',
|
|
280
|
+
' sysctl -n vm.loadavg 2>/dev/null | tr -d \'{}\' | awk \'{print "loadAvg=" $1 "," $2 "," $3}\'',
|
|
281
|
+
"else",
|
|
282
|
+
" awk '/MemTotal:/ {total=$2} /MemAvailable:/ {available=$2} END {if (total > 0) printf \"memoryFreePct=%.1f\\n\", available / total * 100}' /proc/meminfo 2>/dev/null",
|
|
283
|
+
' awk \'{print "loadAvg=" $1 "," $2 "," $3}\' /proc/loadavg 2>/dev/null',
|
|
284
|
+
"fi",
|
|
285
|
+
'df -Pk "$HOME" 2>/dev/null | awk \'NR==2 {printf "diskFreeHome=%.0f\\n", $4 * 1024}\'',
|
|
286
|
+
'if [ -f "${CODEX_HOME:-$HOME/.codex}/auth.json" ]; then echo codexAuthExists=true; else echo codexAuthExists=false; fi',
|
|
287
|
+
...(localVersions
|
|
288
|
+
? [
|
|
289
|
+
'probe_version() { command -v "$1" >/dev/null 2>&1 && "$1" --version </dev/null 2>/dev/null | head -n 1; }',
|
|
290
|
+
'for cli in node codex agy claude triflux; do printf \'%sVersion=%s\\n\' "$cli" "$(probe_version "$cli")"; done',
|
|
291
|
+
]
|
|
292
|
+
: []),
|
|
146
293
|
].join("\n");
|
|
147
294
|
|
|
148
295
|
try {
|
|
149
|
-
const output =
|
|
296
|
+
const output = execSsh([...buildSshOptions(), host, "sh"], {
|
|
150
297
|
encoding: "utf8",
|
|
151
298
|
timeout: 15000,
|
|
152
299
|
input: script,
|
|
153
300
|
maxRetries: 2,
|
|
154
301
|
baseDelayMs: 1000,
|
|
155
302
|
});
|
|
156
|
-
return normalizePosixProbeEnv(parseProbeLines(output));
|
|
157
|
-
} catch {
|
|
158
|
-
|
|
303
|
+
return normalizePosixProbeEnv(parseProbeLines(output), localVersions);
|
|
304
|
+
} catch (error) {
|
|
305
|
+
throw new Error(`remote probe failed for ${host}: ${error.message}`, {
|
|
306
|
+
cause: error,
|
|
307
|
+
});
|
|
159
308
|
}
|
|
160
309
|
}
|
|
161
310
|
|
|
@@ -223,22 +372,34 @@ function writeEnvCache(host, env, cacheDir) {
|
|
|
223
372
|
export function probeRemoteEnv(host, opts = {}) {
|
|
224
373
|
validateHost(host);
|
|
225
374
|
const force = opts.force === true;
|
|
375
|
+
const execSsh = opts.execSsh || execSshWithRetry;
|
|
226
376
|
const cacheDir = opts.cacheDir || remoteEnvCacheDir();
|
|
227
377
|
|
|
228
378
|
if (!force) {
|
|
229
379
|
const cached = readEnvCache(host, cacheDir);
|
|
230
|
-
if (
|
|
380
|
+
if (
|
|
381
|
+
isEnvCacheFresh(cached) &&
|
|
382
|
+
cached.env.claudePath &&
|
|
383
|
+
(!opts.osHint ||
|
|
384
|
+
cached.env.os === opts.osHint ||
|
|
385
|
+
(opts.osHint === "windows" && cached.env.os === "win32")) &&
|
|
386
|
+
(cached.env.os === "win32" ||
|
|
387
|
+
("tmuxPath" in cached.env && "versions" in cached.env))
|
|
388
|
+
)
|
|
389
|
+
return cached.env;
|
|
231
390
|
}
|
|
232
391
|
|
|
233
|
-
const pwshEnv =
|
|
392
|
+
const pwshEnv = ["darwin", "linux"].includes(opts.osHint)
|
|
393
|
+
? null
|
|
394
|
+
: probeRemoteEnvViaPwsh(host, execSsh);
|
|
234
395
|
if (pwshEnv) {
|
|
235
|
-
writeEnvCache(host, pwshEnv, cacheDir);
|
|
396
|
+
if (pwshEnv.claudePath) writeEnvCache(host, pwshEnv, cacheDir);
|
|
236
397
|
return pwshEnv;
|
|
237
398
|
}
|
|
238
399
|
|
|
239
|
-
const posixEnv = probeRemoteEnvViaPosix(host);
|
|
400
|
+
const posixEnv = probeRemoteEnvViaPosix(host, execSsh, opts.localVersions);
|
|
240
401
|
if (posixEnv) {
|
|
241
|
-
writeEnvCache(host, posixEnv, cacheDir);
|
|
402
|
+
if (posixEnv.claudePath) writeEnvCache(host, posixEnv, cacheDir);
|
|
242
403
|
return posixEnv;
|
|
243
404
|
}
|
|
244
405
|
|
|
@@ -303,7 +464,10 @@ export function ensureRemoteStageDir(host, env, remoteStageDir) {
|
|
|
303
464
|
`New-Item -ItemType Directory -Path '${escapePwshSingleQuoted(remoteStageDir)}' -Force | Out-Null`,
|
|
304
465
|
)
|
|
305
466
|
: posixRemoteCommand(`mkdir -p ${shellQuote(remoteStageDir)}`);
|
|
306
|
-
execFileSync("ssh", [host, command], {
|
|
467
|
+
execFileSync("ssh", [...buildSshOptions(), host, command], {
|
|
468
|
+
timeout: 10000,
|
|
469
|
+
stdio: "pipe",
|
|
470
|
+
});
|
|
307
471
|
}
|
|
308
472
|
|
|
309
473
|
/**
|
|
@@ -313,10 +477,14 @@ export function ensureRemoteStageDir(host, env, remoteStageDir) {
|
|
|
313
477
|
* @param {string} remotePath
|
|
314
478
|
*/
|
|
315
479
|
export function uploadFileToRemote(host, localPath, remotePath) {
|
|
316
|
-
execFileSync(
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
480
|
+
execFileSync(
|
|
481
|
+
"scp",
|
|
482
|
+
[...buildSshOptions(), localPath, `${host}:${remotePath}`],
|
|
483
|
+
{
|
|
484
|
+
timeout: 15000,
|
|
485
|
+
stdio: "pipe",
|
|
486
|
+
},
|
|
487
|
+
);
|
|
320
488
|
}
|
|
321
489
|
|
|
322
490
|
/**
|
|
@@ -372,7 +540,7 @@ export function remoteGit(host, env, gitArgs, cwd) {
|
|
|
372
540
|
: posixRemoteCommand(
|
|
373
541
|
`cd ${shellQuote(cwd)} && ${["git", ...gitArgs].map(shellQuote).join(" ")}`,
|
|
374
542
|
);
|
|
375
|
-
return execFileSync("ssh", [host, command], {
|
|
543
|
+
return execFileSync("ssh", [...buildSshOptions(), host, command], {
|
|
376
544
|
encoding: "utf8",
|
|
377
545
|
timeout: 30_000,
|
|
378
546
|
stdio: ["pipe", "pipe", "pipe"],
|