@triflux/remote 10.0.1 → 10.25.1
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/pipe.mjs +239 -121
- package/hub/public/dashboard.html +68 -7
- package/hub/server.mjs +2181 -701
- package/hub/store-adapter.mjs +101 -37
- package/hub/store.mjs +356 -153
- package/hub/team/agent-map.json +26 -7
- package/hub/team/ansi.mjs +245 -146
- package/hub/team/backend.mjs +84 -17
- package/hub/team/build-worker-prompt.mjs +42 -0
- package/hub/team/check-mcp-hub.mjs +49 -0
- package/hub/team/claude-daemon-control.mjs +219 -0
- package/hub/team/claude-native-bridge.mjs +784 -0
- package/hub/team/claude-session-projection.mjs +65 -0
- package/hub/team/cli/commands/attach.mjs +57 -37
- package/hub/team/cli/commands/control.mjs +29 -8
- package/hub/team/cli/commands/debug.mjs +95 -74
- package/hub/team/cli/commands/focus.mjs +80 -53
- package/hub/team/cli/commands/interrupt.mjs +18 -6
- package/hub/team/cli/commands/kill.mjs +16 -5
- package/hub/team/cli/commands/list.mjs +31 -24
- package/hub/team/cli/commands/send.mjs +19 -6
- package/hub/team/cli/commands/start/index.mjs +154 -33
- package/hub/team/cli/commands/start/parse-args.mjs +89 -12
- package/hub/team/cli/commands/start/start-headless.mjs +127 -37
- package/hub/team/cli/commands/start/start-in-process.mjs +50 -40
- package/hub/team/cli/commands/start/start-mux.mjs +122 -73
- package/hub/team/cli/commands/start/start-wt.mjs +86 -69
- package/hub/team/cli/commands/status.mjs +43 -14
- package/hub/team/cli/commands/stop.mjs +11 -4
- package/hub/team/cli/commands/task.mjs +8 -3
- package/hub/team/cli/commands/tasks.mjs +13 -13
- package/hub/team/cli/index.mjs +2 -2
- package/hub/team/cli/manifest.mjs +38 -8
- package/hub/team/cli/render.mjs +52 -30
- package/hub/team/cli/services/attach-fallback.mjs +74 -54
- package/hub/team/cli/services/hub-client.mjs +45 -17
- package/hub/team/cli/services/member-selector.mjs +37 -30
- package/hub/team/cli/services/native-control.mjs +144 -116
- package/hub/team/cli/services/runtime-mode.mjs +55 -1
- package/hub/team/cli/services/state-store.mjs +66 -9
- package/hub/team/cli/services/task-model.mjs +40 -30
- package/hub/team/codex-review.mjs +521 -0
- package/hub/team/conductor-mesh-bridge.mjs +25 -24
- package/hub/team/conductor-registry.mjs +121 -0
- package/hub/team/conductor.mjs +1379 -671
- package/hub/team/consensus-meta.mjs +176 -0
- package/hub/team/dashboard-anchor.mjs +4 -5
- package/hub/team/dashboard-layout.mjs +3 -1
- package/hub/team/dashboard-open.mjs +21 -119
- package/hub/team/dashboard.mjs +76 -28
- package/hub/team/event-log.mjs +18 -10
- package/hub/team/execution-mode.mjs +241 -0
- package/hub/team/extract-completion-payload.mjs +110 -0
- package/hub/team/git-preflight.mjs +286 -0
- package/hub/team/handoff.mjs +49 -18
- package/hub/team/headless-bridge-session.mjs +17 -0
- package/hub/team/headless.mjs +2065 -1156
- package/hub/team/health-probe.mjs +232 -69
- package/hub/team/launcher-template.mjs +23 -13
- package/hub/team/lead-control.mjs +91 -4
- package/hub/team/mcp-selector.mjs +145 -0
- package/hub/team/native-supervisor.mjs +90 -26
- package/hub/team/native.mjs +74 -35
- package/hub/team/nativeProxy.mjs +184 -113
- package/hub/team/notify.mjs +147 -78
- package/hub/team/orchestrator.mjs +173 -161
- package/hub/team/pane.mjs +43 -15
- package/hub/team/process-cleanup.mjs +69 -25
- package/hub/team/psmux.mjs +1067 -264
- package/hub/team/recovery-store.mjs +149 -0
- package/hub/team/remote-probe.mjs +69 -53
- package/hub/team/remote-session.mjs +131 -59
- package/hub/team/remote-watcher.mjs +62 -34
- package/hub/team/retry-state-machine.mjs +267 -0
- package/hub/team/routing.mjs +51 -25
- package/hub/team/runtime-strategy.mjs +200 -0
- package/hub/team/schemas/worker-completion.json +39 -0
- package/hub/team/sentinel-capture.mjs +127 -0
- package/hub/team/session-sync.mjs +153 -6
- package/hub/team/session.mjs +605 -611
- package/hub/team/shared.mjs +13 -13
- package/hub/team/staleState.mjs +72 -30
- package/hub/team/swarm-cli.mjs +390 -0
- package/hub/team/swarm-hypervisor.mjs +1526 -198
- package/hub/team/swarm-intent.mjs +216 -0
- package/hub/team/swarm-locks.mjs +140 -25
- package/hub/team/swarm-planner.mjs +121 -21
- package/hub/team/swarm-preflight.mjs +317 -0
- package/hub/team/swarm-reconciler.mjs +128 -23
- package/hub/team/synapse-cli.mjs +458 -0
- package/hub/team/synapse-http.mjs +60 -0
- package/hub/team/synapse-registry.mjs +329 -0
- package/hub/team/terminal-opener.mjs +216 -0
- package/hub/team/tui-core.mjs +288 -0
- package/hub/team/tui-lite.mjs +234 -146
- package/hub/team/tui-remote-adapter.mjs +21 -37
- package/hub/team/tui-synapse.mjs +223 -0
- package/hub/team/tui-viewer.mjs +104 -45
- package/hub/team/tui-widgets.mjs +262 -0
- package/hub/team/tui.mjs +936 -449
- package/hub/team/worker-completion-validator.mjs +84 -0
- package/hub/team/worker-sandbox.mjs +93 -0
- package/hub/team/worker-signal.mjs +263 -0
- package/hub/team/worker-signal.types.d.ts +52 -0
- package/hub/team/worktree-lifecycle.mjs +518 -64
- package/hub/team/wt-manager.mjs +297 -26
- package/hub/tools.mjs +474 -252
- package/hub/tray.mjs +411 -368
- package/hub/workers/claude-worker.mjs +162 -118
- package/hub/workers/codex-app-server-worker.mjs +1015 -0
- package/hub/workers/codex-mcp.mjs +724 -507
- package/hub/workers/delegator-mcp.mjs +663 -337
- package/hub/workers/factory.mjs +100 -21
- package/hub/workers/gemini-worker.mjs +179 -86
- package/hub/workers/interface.mjs +23 -2
- package/hub/workers/lib/jsonrpc-stdio.mjs +463 -0
- package/hub/workers/worker-utils.mjs +22 -16
- package/package.json +4 -3
- package/scripts/lib/agent-json.mjs +27 -0
- package/scripts/lib/async.mjs +174 -0
- package/scripts/lib/claudemd-scanner.mjs +223 -0
- package/scripts/lib/cli-agy.mjs +62 -0
- package/scripts/lib/cli-claude.mjs +78 -0
- package/scripts/lib/cli-codex.mjs +199 -0
- package/scripts/lib/cli-gemini.mjs +67 -0
- package/scripts/lib/codex-recovery.sh +50 -0
- package/scripts/lib/context.mjs +67 -0
- package/scripts/lib/cross-review-utils.mjs +59 -0
- package/scripts/lib/doctor-env-checks.mjs +121 -0
- package/scripts/lib/dynamic-route-cli.mjs +107 -0
- package/scripts/lib/env-probe.mjs +269 -0
- package/scripts/lib/env.mjs +14 -0
- package/scripts/lib/gemini-profiles.mjs +119 -0
- package/scripts/lib/handoff.mjs +187 -0
- package/scripts/lib/hook-utils.mjs +16 -0
- package/scripts/lib/hub.mjs +48 -0
- package/scripts/lib/keyword-rules.mjs +195 -0
- package/scripts/lib/logger.mjs +105 -0
- package/scripts/lib/mcp-filter.mjs +883 -0
- package/scripts/lib/mcp-gateway-servers.mjs +70 -0
- package/scripts/lib/mcp-guard-engine.mjs +2120 -0
- package/scripts/lib/mcp-health.mjs +773 -0
- package/scripts/lib/mcp-manifest.mjs +89 -0
- package/scripts/lib/mcp-server-catalog.mjs +355 -0
- package/scripts/lib/pid.mjs +63 -0
- package/scripts/lib/process-utils.mjs +26 -0
- package/scripts/lib/psmux-info.mjs +179 -0
- package/scripts/lib/quota.mjs +47 -0
- package/scripts/lib/remote-spawn-transfer.mjs +226 -0
- package/scripts/lib/skill-state.mjs +220 -0
- package/scripts/lib/skill-template.mjs +375 -0
- package/scripts/lib/team.mjs +75 -0
- package/scripts/lib/timeout.mjs +66 -0
- package/scripts/lib/tmp.mjs +26 -0
- package/scripts/lib/toml.mjs +113 -0
|
@@ -2,13 +2,18 @@
|
|
|
2
2
|
// Extracted from scripts/remote-spawn.mjs for reuse by swarm-hypervisor.
|
|
3
3
|
// Pure functions + SSH operations. No psmux, no WT, no CLI arg parsing.
|
|
4
4
|
|
|
5
|
-
import { execFileSync } from
|
|
6
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from
|
|
7
|
-
import {
|
|
8
|
-
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import {
|
|
8
|
+
basename,
|
|
9
|
+
join,
|
|
10
|
+
posix as posixPath,
|
|
11
|
+
win32 as win32Path,
|
|
12
|
+
} from "node:path";
|
|
13
|
+
import { execSshWithRetry } from "@triflux/core/hub/lib/ssh-retry.mjs";
|
|
9
14
|
|
|
10
15
|
const REMOTE_ENV_TTL_MS = 86_400_000; // 24h
|
|
11
|
-
const REMOTE_STAGE_ROOT =
|
|
16
|
+
const REMOTE_STAGE_ROOT = "tfx-remote";
|
|
12
17
|
const SAFE_HOST_RE = /^[a-zA-Z0-9._-]+$/;
|
|
13
18
|
|
|
14
19
|
// ── Shell quoting utilities ─────────────────────────────────────
|
|
@@ -22,11 +27,11 @@ export function escapePwshSingleQuoted(value) {
|
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
export function escapePwshDoubleQuoted(value) {
|
|
25
|
-
return String(value).replace(/`/g,
|
|
30
|
+
return String(value).replace(/`/g, "``").replace(/"/g, '`"');
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
function normalizeCommandPath(value) {
|
|
29
|
-
return String(value).replace(/\\/g,
|
|
34
|
+
return String(value).replace(/\\/g, "/");
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
// ── Validation ──────────────────────────────────────────────────
|
|
@@ -47,7 +52,7 @@ function parseProbeLines(text) {
|
|
|
47
52
|
.map((line) => line.trim())
|
|
48
53
|
.filter(Boolean)
|
|
49
54
|
.map((line) => {
|
|
50
|
-
const idx = line.indexOf(
|
|
55
|
+
const idx = line.indexOf("=");
|
|
51
56
|
return idx === -1 ? null : [line.slice(0, idx), line.slice(idx + 1)];
|
|
52
57
|
})
|
|
53
58
|
.filter(Boolean),
|
|
@@ -55,24 +60,34 @@ function parseProbeLines(text) {
|
|
|
55
60
|
}
|
|
56
61
|
|
|
57
62
|
function normalizePwshProbeEnv(parsed) {
|
|
58
|
-
if (parsed.shell !==
|
|
63
|
+
if (parsed.shell !== "pwsh" || parsed.os !== "win32") return null;
|
|
59
64
|
if (!parsed.home) return null;
|
|
60
65
|
return Object.freeze({
|
|
61
|
-
claudePath:
|
|
66
|
+
claudePath:
|
|
67
|
+
!parsed.claude || parsed.claude === "notfound" ? null : parsed.claude,
|
|
62
68
|
home: parsed.home,
|
|
63
|
-
os:
|
|
64
|
-
shell:
|
|
69
|
+
os: "win32",
|
|
70
|
+
shell: "pwsh",
|
|
65
71
|
});
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
function normalizePosixProbeEnv(parsed) {
|
|
69
|
-
const os =
|
|
75
|
+
const os =
|
|
76
|
+
parsed.os === "darwin" ? "darwin" : parsed.os === "linux" ? "linux" : null;
|
|
70
77
|
if (!os || !parsed.home) return null;
|
|
71
78
|
return Object.freeze({
|
|
72
|
-
claudePath:
|
|
79
|
+
claudePath:
|
|
80
|
+
!parsed.claude || parsed.claude === "notfound" ? null : parsed.claude,
|
|
81
|
+
codexPath:
|
|
82
|
+
!parsed.codex || parsed.codex === "notfound" ? null : parsed.codex,
|
|
83
|
+
geminiPath:
|
|
84
|
+
!parsed.gemini || parsed.gemini === "notfound" ? null : parsed.gemini,
|
|
73
85
|
home: parsed.home,
|
|
74
86
|
os,
|
|
75
|
-
shell: parsed.shell ===
|
|
87
|
+
shell: parsed.shell === "zsh" ? "zsh" : "bash",
|
|
88
|
+
node: parsed.node || null,
|
|
89
|
+
cores: parsed.cores ? Number(parsed.cores) : null,
|
|
90
|
+
ramGb: parsed.ram_gb ? Number(parsed.ram_gb) : null,
|
|
76
91
|
});
|
|
77
92
|
}
|
|
78
93
|
|
|
@@ -81,14 +96,20 @@ function probeRemoteEnvViaPwsh(host) {
|
|
|
81
96
|
"Write-Output 'shell=pwsh'",
|
|
82
97
|
'Write-Output "home=$env:USERPROFILE"',
|
|
83
98
|
'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\' }',
|
|
84
|
-
|
|
85
|
-
].join(
|
|
99
|
+
"Write-Output \"os=$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows) ? 'win32' : 'other')\"",
|
|
100
|
+
].join("; ");
|
|
86
101
|
|
|
87
102
|
try {
|
|
88
|
-
const output = execSshWithRetry(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
103
|
+
const output = execSshWithRetry(
|
|
104
|
+
[host, "pwsh", "-NoProfile", "-Command", command],
|
|
105
|
+
{
|
|
106
|
+
encoding: "utf8",
|
|
107
|
+
timeout: 15000,
|
|
108
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
109
|
+
maxRetries: 2,
|
|
110
|
+
baseDelayMs: 1000,
|
|
111
|
+
},
|
|
112
|
+
);
|
|
92
113
|
return normalizePwshProbeEnv(parseProbeLines(output));
|
|
93
114
|
} catch {
|
|
94
115
|
return null;
|
|
@@ -97,16 +118,26 @@ function probeRemoteEnvViaPwsh(host) {
|
|
|
97
118
|
|
|
98
119
|
function probeRemoteEnvViaPosix(host) {
|
|
99
120
|
const script = [
|
|
100
|
-
'
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
121
|
+
'[ -f "$HOME/.zshenv" ] && . "$HOME/.zshenv" 2>/dev/null || true',
|
|
122
|
+
"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",
|
|
123
|
+
"echo shell=$(basename $SHELL)",
|
|
124
|
+
"echo home=$HOME",
|
|
125
|
+
"command -v claude >/dev/null 2>&1 && echo claude=$(command -v claude) || echo claude=notfound",
|
|
126
|
+
"command -v codex >/dev/null 2>&1 && echo codex=$(command -v codex) || echo codex=notfound",
|
|
127
|
+
"command -v gemini >/dev/null 2>&1 && echo gemini=$(command -v gemini) || echo gemini=notfound",
|
|
128
|
+
"echo os=$(uname -s | tr A-Z a-z)",
|
|
129
|
+
"node --version 2>/dev/null && echo node=$(node --version) || echo node=notfound",
|
|
130
|
+
// darwin: sysctl, linux: nproc + /proc/meminfo
|
|
131
|
+
"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",
|
|
132
|
+
].join("\n");
|
|
105
133
|
|
|
106
134
|
try {
|
|
107
|
-
const output = execSshWithRetry([host,
|
|
108
|
-
encoding:
|
|
109
|
-
|
|
135
|
+
const output = execSshWithRetry([host, "sh"], {
|
|
136
|
+
encoding: "utf8",
|
|
137
|
+
timeout: 15000,
|
|
138
|
+
input: script,
|
|
139
|
+
maxRetries: 2,
|
|
140
|
+
baseDelayMs: 1000,
|
|
110
141
|
});
|
|
111
142
|
return normalizePosixProbeEnv(parseProbeLines(output));
|
|
112
143
|
} catch {
|
|
@@ -124,8 +155,8 @@ function readEnvCache(host, cacheDir) {
|
|
|
124
155
|
const cachePath = getEnvCachePath(host, cacheDir);
|
|
125
156
|
if (!existsSync(cachePath)) return null;
|
|
126
157
|
try {
|
|
127
|
-
const parsed = JSON.parse(readFileSync(cachePath,
|
|
128
|
-
return parsed && typeof parsed ===
|
|
158
|
+
const parsed = JSON.parse(readFileSync(cachePath, "utf8"));
|
|
159
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
129
160
|
} catch {
|
|
130
161
|
return null;
|
|
131
162
|
}
|
|
@@ -133,16 +164,20 @@ function readEnvCache(host, cacheDir) {
|
|
|
133
164
|
|
|
134
165
|
function isEnvCacheFresh(entry) {
|
|
135
166
|
return Boolean(
|
|
136
|
-
entry
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
167
|
+
entry &&
|
|
168
|
+
typeof entry.cachedAt === "number" &&
|
|
169
|
+
entry.env &&
|
|
170
|
+
Date.now() - entry.cachedAt < REMOTE_ENV_TTL_MS,
|
|
140
171
|
);
|
|
141
172
|
}
|
|
142
173
|
|
|
143
174
|
function writeEnvCache(host, env, cacheDir) {
|
|
144
175
|
mkdirSync(cacheDir, { recursive: true });
|
|
145
|
-
writeFileSync(
|
|
176
|
+
writeFileSync(
|
|
177
|
+
getEnvCachePath(host, cacheDir),
|
|
178
|
+
JSON.stringify({ cachedAt: Date.now(), env }, null, 2),
|
|
179
|
+
"utf8",
|
|
180
|
+
);
|
|
146
181
|
}
|
|
147
182
|
|
|
148
183
|
/**
|
|
@@ -158,7 +193,7 @@ function writeEnvCache(host, env, cacheDir) {
|
|
|
158
193
|
export function probeRemoteEnv(host, opts = {}) {
|
|
159
194
|
validateHost(host);
|
|
160
195
|
const force = opts.force === true;
|
|
161
|
-
const cacheDir = opts.cacheDir || join(
|
|
196
|
+
const cacheDir = opts.cacheDir || join(".omc", "state", "remote-env");
|
|
162
197
|
|
|
163
198
|
if (!force) {
|
|
164
199
|
const cached = readEnvCache(host, cacheDir);
|
|
@@ -166,10 +201,16 @@ export function probeRemoteEnv(host, opts = {}) {
|
|
|
166
201
|
}
|
|
167
202
|
|
|
168
203
|
const pwshEnv = probeRemoteEnvViaPwsh(host);
|
|
169
|
-
if (pwshEnv) {
|
|
204
|
+
if (pwshEnv) {
|
|
205
|
+
writeEnvCache(host, pwshEnv, cacheDir);
|
|
206
|
+
return pwshEnv;
|
|
207
|
+
}
|
|
170
208
|
|
|
171
209
|
const posixEnv = probeRemoteEnvViaPosix(host);
|
|
172
|
-
if (posixEnv) {
|
|
210
|
+
if (posixEnv) {
|
|
211
|
+
writeEnvCache(host, posixEnv, cacheDir);
|
|
212
|
+
return posixEnv;
|
|
213
|
+
}
|
|
173
214
|
|
|
174
215
|
throw new Error(`remote probe failed for ${host}`);
|
|
175
216
|
}
|
|
@@ -177,7 +218,7 @@ export function probeRemoteEnv(host, opts = {}) {
|
|
|
177
218
|
// ── Remote directory resolution ─────────────────────────────────
|
|
178
219
|
|
|
179
220
|
function isWindowsAbsolutePath(value) {
|
|
180
|
-
return /^[a-zA-Z]:[\\/]/u.test(value) || value.startsWith(
|
|
221
|
+
return /^[a-zA-Z]:[\\/]/u.test(value) || value.startsWith("\\\\");
|
|
181
222
|
}
|
|
182
223
|
|
|
183
224
|
/**
|
|
@@ -191,17 +232,19 @@ function isWindowsAbsolutePath(value) {
|
|
|
191
232
|
export function resolveRemoteDir(dir, env) {
|
|
192
233
|
const requestedDir = dir || env.home;
|
|
193
234
|
|
|
194
|
-
if (env.os ===
|
|
195
|
-
const winDir = requestedDir.replace(/\//g,
|
|
196
|
-
if (winDir ===
|
|
197
|
-
if (/^~[\\/]/u.test(winDir))
|
|
235
|
+
if (env.os === "win32") {
|
|
236
|
+
const winDir = requestedDir.replace(/\//g, "\\");
|
|
237
|
+
if (winDir === "~") return env.home;
|
|
238
|
+
if (/^~[\\/]/u.test(winDir))
|
|
239
|
+
return win32Path.join(env.home, winDir.slice(2));
|
|
198
240
|
if (isWindowsAbsolutePath(winDir)) return winDir;
|
|
199
241
|
return win32Path.join(env.home, winDir);
|
|
200
242
|
}
|
|
201
243
|
|
|
202
|
-
if (requestedDir ===
|
|
203
|
-
if (requestedDir.startsWith(
|
|
204
|
-
|
|
244
|
+
if (requestedDir === "~") return env.home;
|
|
245
|
+
if (requestedDir.startsWith("~/"))
|
|
246
|
+
return posixPath.join(env.home, requestedDir.slice(2));
|
|
247
|
+
if (requestedDir.startsWith("/")) return requestedDir;
|
|
205
248
|
return posixPath.join(env.home, requestedDir);
|
|
206
249
|
}
|
|
207
250
|
|
|
@@ -224,12 +267,26 @@ export function resolveRemoteStageDir(env, stageId) {
|
|
|
224
267
|
* @param {string} remoteStageDir
|
|
225
268
|
*/
|
|
226
269
|
export function ensureRemoteStageDir(host, env, remoteStageDir) {
|
|
227
|
-
if (env.os ===
|
|
270
|
+
if (env.os === "win32") {
|
|
228
271
|
const safePath = escapePwshSingleQuoted(remoteStageDir);
|
|
229
|
-
execFileSync(
|
|
272
|
+
execFileSync(
|
|
273
|
+
"ssh",
|
|
274
|
+
[
|
|
275
|
+
host,
|
|
276
|
+
"pwsh",
|
|
277
|
+
"-NoProfile",
|
|
278
|
+
"-Command",
|
|
279
|
+
`New-Item -ItemType Directory -Path '${safePath}' -Force | Out-Null`,
|
|
280
|
+
],
|
|
281
|
+
{ timeout: 10000, stdio: "pipe" },
|
|
282
|
+
);
|
|
230
283
|
return;
|
|
231
284
|
}
|
|
232
|
-
execFileSync(
|
|
285
|
+
execFileSync(
|
|
286
|
+
"ssh",
|
|
287
|
+
[host, "sh", "-lc", `mkdir -p ${shellQuote(remoteStageDir)}`],
|
|
288
|
+
{ timeout: 10000, stdio: "pipe" },
|
|
289
|
+
);
|
|
233
290
|
}
|
|
234
291
|
|
|
235
292
|
/**
|
|
@@ -239,7 +296,10 @@ export function ensureRemoteStageDir(host, env, remoteStageDir) {
|
|
|
239
296
|
* @param {string} remotePath
|
|
240
297
|
*/
|
|
241
298
|
export function uploadFileToRemote(host, localPath, remotePath) {
|
|
242
|
-
execFileSync(
|
|
299
|
+
execFileSync("scp", [localPath, `${host}:${remotePath}`], {
|
|
300
|
+
timeout: 15000,
|
|
301
|
+
stdio: "pipe",
|
|
302
|
+
});
|
|
243
303
|
}
|
|
244
304
|
|
|
245
305
|
/**
|
|
@@ -283,17 +343,29 @@ export function stageRemotePromptFiles(host, env, transferCandidates, stageId) {
|
|
|
283
343
|
* @returns {string} stdout
|
|
284
344
|
*/
|
|
285
345
|
export function remoteGit(host, env, gitArgs, cwd) {
|
|
286
|
-
const gitCmd = [
|
|
346
|
+
const gitCmd = ["git", ...gitArgs].map((a) => shellQuote(a)).join(" ");
|
|
287
347
|
|
|
288
|
-
if (env.os ===
|
|
348
|
+
if (env.os === "win32") {
|
|
289
349
|
const cdPath = escapePwshSingleQuoted(cwd);
|
|
290
350
|
const command = `Set-Location '${cdPath}'; ${gitCmd}`;
|
|
291
|
-
return execFileSync(
|
|
292
|
-
|
|
293
|
-
|
|
351
|
+
return execFileSync(
|
|
352
|
+
"ssh",
|
|
353
|
+
[host, "pwsh", "-NoProfile", "-Command", command],
|
|
354
|
+
{
|
|
355
|
+
encoding: "utf8",
|
|
356
|
+
timeout: 30_000,
|
|
357
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
358
|
+
},
|
|
359
|
+
).trim();
|
|
294
360
|
}
|
|
295
361
|
|
|
296
|
-
return execFileSync(
|
|
297
|
-
|
|
298
|
-
|
|
362
|
+
return execFileSync(
|
|
363
|
+
"ssh",
|
|
364
|
+
[host, "sh", "-lc", `cd ${shellQuote(cwd)} && ${gitCmd}`],
|
|
365
|
+
{
|
|
366
|
+
encoding: "utf8",
|
|
367
|
+
timeout: 30_000,
|
|
368
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
369
|
+
},
|
|
370
|
+
).trim();
|
|
299
371
|
}
|
|
@@ -9,9 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
import { execFileSync } from "node:child_process";
|
|
11
11
|
import { EventEmitter } from "node:events";
|
|
12
|
-
|
|
12
|
+
import { detectHostOs, shellQuoteForHost } from "@triflux/core/hub/lib/ssh-command.mjs";
|
|
13
13
|
import { detectInputWait, PROBE_LEVELS } from "./health-probe.mjs";
|
|
14
|
-
import { shellQuoteForHost, detectHostOs } from "@triflux/core/hub/lib/ssh-command.mjs";
|
|
15
14
|
|
|
16
15
|
export const REMOTE_WATCHER_STATES = Object.freeze({
|
|
17
16
|
WATCHING: "watching",
|
|
@@ -40,7 +39,7 @@ const PROMPT_IDLE_PATTERNS = Object.freeze([
|
|
|
40
39
|
]);
|
|
41
40
|
|
|
42
41
|
/** @deprecated shellQuoteForHost(value, os) 사용 권장 — OS-aware 쿼팅 */
|
|
43
|
-
function
|
|
42
|
+
function _shellQuote(value, os) {
|
|
44
43
|
if (os) return shellQuoteForHost(value, os);
|
|
45
44
|
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
46
45
|
}
|
|
@@ -62,7 +61,10 @@ function freezeStatus(status) {
|
|
|
62
61
|
Object.fromEntries(
|
|
63
62
|
Object.entries(status.sessions || {})
|
|
64
63
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
65
|
-
.map(([sessionName, record]) => [
|
|
64
|
+
.map(([sessionName, record]) => [
|
|
65
|
+
sessionName,
|
|
66
|
+
freezeSessionRecord(record),
|
|
67
|
+
]),
|
|
66
68
|
),
|
|
67
69
|
);
|
|
68
70
|
|
|
@@ -117,7 +119,9 @@ function hasMeaningfulActivity(captured) {
|
|
|
117
119
|
}
|
|
118
120
|
|
|
119
121
|
function detectCompletion(captured) {
|
|
120
|
-
const matches = Array.from(
|
|
122
|
+
const matches = Array.from(
|
|
123
|
+
String(captured || "").matchAll(COMPLETION_TOKEN_RE),
|
|
124
|
+
);
|
|
121
125
|
const match = matches.at(-1);
|
|
122
126
|
if (!match) {
|
|
123
127
|
return {
|
|
@@ -128,7 +132,8 @@ function detectCompletion(captured) {
|
|
|
128
132
|
};
|
|
129
133
|
}
|
|
130
134
|
|
|
131
|
-
const parsedExitCode =
|
|
135
|
+
const parsedExitCode =
|
|
136
|
+
match[2] == null ? null : Number.parseInt(match[2], 10);
|
|
132
137
|
return {
|
|
133
138
|
detected: true,
|
|
134
139
|
exitCode: Number.isFinite(parsedExitCode) ? parsedExitCode : null,
|
|
@@ -143,7 +148,10 @@ function parseSessionNames(rawOutput, sessionPrefix) {
|
|
|
143
148
|
.map((line) => line.trim())
|
|
144
149
|
.filter(Boolean)
|
|
145
150
|
.map((line) => line.split(":")[0]?.trim())
|
|
146
|
-
.filter(
|
|
151
|
+
.filter(
|
|
152
|
+
(sessionName) =>
|
|
153
|
+
Boolean(sessionName) && sessionName.startsWith(sessionPrefix),
|
|
154
|
+
);
|
|
147
155
|
}
|
|
148
156
|
|
|
149
157
|
function buildExecOptions(config) {
|
|
@@ -187,7 +195,9 @@ function captureSpawnSession(sessionName, config) {
|
|
|
187
195
|
);
|
|
188
196
|
return {
|
|
189
197
|
paneTarget,
|
|
190
|
-
captured: String(output || "")
|
|
198
|
+
captured: String(output || "")
|
|
199
|
+
.replace(/\r/g, "")
|
|
200
|
+
.trimEnd(),
|
|
191
201
|
};
|
|
192
202
|
}
|
|
193
203
|
|
|
@@ -221,7 +231,10 @@ function createSessionRecord(sessionName, config, now) {
|
|
|
221
231
|
}
|
|
222
232
|
|
|
223
233
|
function isTerminalState(state) {
|
|
224
|
-
return
|
|
234
|
+
return (
|
|
235
|
+
state === REMOTE_WATCHER_STATES.COMPLETED ||
|
|
236
|
+
state === REMOTE_WATCHER_STATES.FAILED
|
|
237
|
+
);
|
|
225
238
|
}
|
|
226
239
|
|
|
227
240
|
export function createRemoteWatcher(opts = {}) {
|
|
@@ -252,26 +265,30 @@ export function createRemoteWatcher(opts = {}) {
|
|
|
252
265
|
}
|
|
253
266
|
|
|
254
267
|
function emitSessionEvent(eventName, nextRecord, now) {
|
|
255
|
-
emitter.emit(
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
+
emitter.emit(
|
|
269
|
+
eventName,
|
|
270
|
+
Object.freeze({
|
|
271
|
+
exitCode: nextRecord.exitCode,
|
|
272
|
+
host: nextRecord.host,
|
|
273
|
+
inputWaitPattern: nextRecord.inputWaitPattern,
|
|
274
|
+
output: nextRecord.lastOutput,
|
|
275
|
+
paneTarget: nextRecord.paneTarget,
|
|
276
|
+
probeLevel: nextRecord.lastProbeLevel,
|
|
277
|
+
promptIdlePattern: nextRecord.promptIdlePattern,
|
|
278
|
+
reason: nextRecord.reason,
|
|
279
|
+
sessionName: nextRecord.sessionName,
|
|
280
|
+
state: nextRecord.state,
|
|
281
|
+
ts: now,
|
|
282
|
+
}),
|
|
283
|
+
);
|
|
268
284
|
}
|
|
269
285
|
|
|
270
286
|
function classifySession(previousRecord, captured, now) {
|
|
271
287
|
const completion = detectCompletion(captured);
|
|
272
288
|
const inputWait = detectInputWait(captured);
|
|
273
289
|
const promptIdle = detectPromptIdle(captured);
|
|
274
|
-
const hasActivity =
|
|
290
|
+
const hasActivity =
|
|
291
|
+
previousRecord.hasActivity || hasMeaningfulActivity(captured);
|
|
275
292
|
|
|
276
293
|
let nextState = REMOTE_WATCHER_STATES.WATCHING;
|
|
277
294
|
let reason = "watching";
|
|
@@ -292,9 +309,9 @@ export function createRemoteWatcher(opts = {}) {
|
|
|
292
309
|
eventName = "sessionCompleted";
|
|
293
310
|
}
|
|
294
311
|
} else if (
|
|
295
|
-
promptIdle.detected
|
|
296
|
-
&&
|
|
297
|
-
|
|
312
|
+
promptIdle.detected &&
|
|
313
|
+
hasActivity &&
|
|
314
|
+
(!inputWait.detected || inputWait.pattern === BARE_INPUT_WAIT_PATTERN)
|
|
298
315
|
) {
|
|
299
316
|
nextState = REMOTE_WATCHER_STATES.COMPLETED;
|
|
300
317
|
reason = "prompt_idle";
|
|
@@ -325,10 +342,10 @@ export function createRemoteWatcher(opts = {}) {
|
|
|
325
342
|
};
|
|
326
343
|
|
|
327
344
|
if (
|
|
328
|
-
eventName
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
345
|
+
eventName &&
|
|
346
|
+
previousRecord.state === nextRecord.state &&
|
|
347
|
+
previousRecord.reason === nextRecord.reason &&
|
|
348
|
+
previousRecord.exitCode === nextRecord.exitCode
|
|
332
349
|
) {
|
|
333
350
|
return { eventName: null, nextRecord };
|
|
334
351
|
}
|
|
@@ -375,7 +392,10 @@ export function createRemoteWatcher(opts = {}) {
|
|
|
375
392
|
: createSessionRecord(sessionName, config, now);
|
|
376
393
|
|
|
377
394
|
try {
|
|
378
|
-
const { paneTarget, captured } = captureSpawnSession(
|
|
395
|
+
const { paneTarget, captured } = captureSpawnSession(
|
|
396
|
+
sessionName,
|
|
397
|
+
config,
|
|
398
|
+
);
|
|
379
399
|
const { eventName, nextRecord } = classifySession(
|
|
380
400
|
{ ...previousRecord, paneTarget },
|
|
381
401
|
captured,
|
|
@@ -396,7 +416,10 @@ export function createRemoteWatcher(opts = {}) {
|
|
|
396
416
|
state: REMOTE_WATCHER_STATES.FAILED,
|
|
397
417
|
};
|
|
398
418
|
nextSessions[sessionName] = failedRecord;
|
|
399
|
-
queuedEvents.push({
|
|
419
|
+
queuedEvents.push({
|
|
420
|
+
eventName: "sessionFailed",
|
|
421
|
+
record: failedRecord,
|
|
422
|
+
});
|
|
400
423
|
setStatus({
|
|
401
424
|
...status,
|
|
402
425
|
lastError: toErrorRecord(error),
|
|
@@ -407,9 +430,14 @@ export function createRemoteWatcher(opts = {}) {
|
|
|
407
430
|
}
|
|
408
431
|
}
|
|
409
432
|
|
|
410
|
-
for (const [sessionName, previousRecord] of Object.entries(
|
|
433
|
+
for (const [sessionName, previousRecord] of Object.entries(
|
|
434
|
+
status.sessions,
|
|
435
|
+
)) {
|
|
411
436
|
if (activeSet.has(sessionName)) continue;
|
|
412
|
-
const { eventName, nextRecord } = markMissingSession(
|
|
437
|
+
const { eventName, nextRecord } = markMissingSession(
|
|
438
|
+
previousRecord,
|
|
439
|
+
now,
|
|
440
|
+
);
|
|
413
441
|
nextSessions[sessionName] = nextRecord;
|
|
414
442
|
if (eventName) {
|
|
415
443
|
queuedEvents.push({ eventName, record: nextRecord });
|