@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.
Files changed (155) hide show
  1. package/hub/pipe.mjs +239 -121
  2. package/hub/public/dashboard.html +68 -7
  3. package/hub/server.mjs +2181 -701
  4. package/hub/store-adapter.mjs +101 -37
  5. package/hub/store.mjs +356 -153
  6. package/hub/team/agent-map.json +26 -7
  7. package/hub/team/ansi.mjs +245 -146
  8. package/hub/team/backend.mjs +84 -17
  9. package/hub/team/build-worker-prompt.mjs +42 -0
  10. package/hub/team/check-mcp-hub.mjs +49 -0
  11. package/hub/team/claude-daemon-control.mjs +219 -0
  12. package/hub/team/claude-native-bridge.mjs +784 -0
  13. package/hub/team/claude-session-projection.mjs +65 -0
  14. package/hub/team/cli/commands/attach.mjs +57 -37
  15. package/hub/team/cli/commands/control.mjs +29 -8
  16. package/hub/team/cli/commands/debug.mjs +95 -74
  17. package/hub/team/cli/commands/focus.mjs +80 -53
  18. package/hub/team/cli/commands/interrupt.mjs +18 -6
  19. package/hub/team/cli/commands/kill.mjs +16 -5
  20. package/hub/team/cli/commands/list.mjs +31 -24
  21. package/hub/team/cli/commands/send.mjs +19 -6
  22. package/hub/team/cli/commands/start/index.mjs +154 -33
  23. package/hub/team/cli/commands/start/parse-args.mjs +89 -12
  24. package/hub/team/cli/commands/start/start-headless.mjs +127 -37
  25. package/hub/team/cli/commands/start/start-in-process.mjs +50 -40
  26. package/hub/team/cli/commands/start/start-mux.mjs +122 -73
  27. package/hub/team/cli/commands/start/start-wt.mjs +86 -69
  28. package/hub/team/cli/commands/status.mjs +43 -14
  29. package/hub/team/cli/commands/stop.mjs +11 -4
  30. package/hub/team/cli/commands/task.mjs +8 -3
  31. package/hub/team/cli/commands/tasks.mjs +13 -13
  32. package/hub/team/cli/index.mjs +2 -2
  33. package/hub/team/cli/manifest.mjs +38 -8
  34. package/hub/team/cli/render.mjs +52 -30
  35. package/hub/team/cli/services/attach-fallback.mjs +74 -54
  36. package/hub/team/cli/services/hub-client.mjs +45 -17
  37. package/hub/team/cli/services/member-selector.mjs +37 -30
  38. package/hub/team/cli/services/native-control.mjs +144 -116
  39. package/hub/team/cli/services/runtime-mode.mjs +55 -1
  40. package/hub/team/cli/services/state-store.mjs +66 -9
  41. package/hub/team/cli/services/task-model.mjs +40 -30
  42. package/hub/team/codex-review.mjs +521 -0
  43. package/hub/team/conductor-mesh-bridge.mjs +25 -24
  44. package/hub/team/conductor-registry.mjs +121 -0
  45. package/hub/team/conductor.mjs +1379 -671
  46. package/hub/team/consensus-meta.mjs +176 -0
  47. package/hub/team/dashboard-anchor.mjs +4 -5
  48. package/hub/team/dashboard-layout.mjs +3 -1
  49. package/hub/team/dashboard-open.mjs +21 -119
  50. package/hub/team/dashboard.mjs +76 -28
  51. package/hub/team/event-log.mjs +18 -10
  52. package/hub/team/execution-mode.mjs +241 -0
  53. package/hub/team/extract-completion-payload.mjs +110 -0
  54. package/hub/team/git-preflight.mjs +286 -0
  55. package/hub/team/handoff.mjs +49 -18
  56. package/hub/team/headless-bridge-session.mjs +17 -0
  57. package/hub/team/headless.mjs +2065 -1156
  58. package/hub/team/health-probe.mjs +232 -69
  59. package/hub/team/launcher-template.mjs +23 -13
  60. package/hub/team/lead-control.mjs +91 -4
  61. package/hub/team/mcp-selector.mjs +145 -0
  62. package/hub/team/native-supervisor.mjs +90 -26
  63. package/hub/team/native.mjs +74 -35
  64. package/hub/team/nativeProxy.mjs +184 -113
  65. package/hub/team/notify.mjs +147 -78
  66. package/hub/team/orchestrator.mjs +173 -161
  67. package/hub/team/pane.mjs +43 -15
  68. package/hub/team/process-cleanup.mjs +69 -25
  69. package/hub/team/psmux.mjs +1067 -264
  70. package/hub/team/recovery-store.mjs +149 -0
  71. package/hub/team/remote-probe.mjs +69 -53
  72. package/hub/team/remote-session.mjs +131 -59
  73. package/hub/team/remote-watcher.mjs +62 -34
  74. package/hub/team/retry-state-machine.mjs +267 -0
  75. package/hub/team/routing.mjs +51 -25
  76. package/hub/team/runtime-strategy.mjs +200 -0
  77. package/hub/team/schemas/worker-completion.json +39 -0
  78. package/hub/team/sentinel-capture.mjs +127 -0
  79. package/hub/team/session-sync.mjs +153 -6
  80. package/hub/team/session.mjs +605 -611
  81. package/hub/team/shared.mjs +13 -13
  82. package/hub/team/staleState.mjs +72 -30
  83. package/hub/team/swarm-cli.mjs +390 -0
  84. package/hub/team/swarm-hypervisor.mjs +1526 -198
  85. package/hub/team/swarm-intent.mjs +216 -0
  86. package/hub/team/swarm-locks.mjs +140 -25
  87. package/hub/team/swarm-planner.mjs +121 -21
  88. package/hub/team/swarm-preflight.mjs +317 -0
  89. package/hub/team/swarm-reconciler.mjs +128 -23
  90. package/hub/team/synapse-cli.mjs +458 -0
  91. package/hub/team/synapse-http.mjs +60 -0
  92. package/hub/team/synapse-registry.mjs +329 -0
  93. package/hub/team/terminal-opener.mjs +216 -0
  94. package/hub/team/tui-core.mjs +288 -0
  95. package/hub/team/tui-lite.mjs +234 -146
  96. package/hub/team/tui-remote-adapter.mjs +21 -37
  97. package/hub/team/tui-synapse.mjs +223 -0
  98. package/hub/team/tui-viewer.mjs +104 -45
  99. package/hub/team/tui-widgets.mjs +262 -0
  100. package/hub/team/tui.mjs +936 -449
  101. package/hub/team/worker-completion-validator.mjs +84 -0
  102. package/hub/team/worker-sandbox.mjs +93 -0
  103. package/hub/team/worker-signal.mjs +263 -0
  104. package/hub/team/worker-signal.types.d.ts +52 -0
  105. package/hub/team/worktree-lifecycle.mjs +518 -64
  106. package/hub/team/wt-manager.mjs +297 -26
  107. package/hub/tools.mjs +474 -252
  108. package/hub/tray.mjs +411 -368
  109. package/hub/workers/claude-worker.mjs +162 -118
  110. package/hub/workers/codex-app-server-worker.mjs +1015 -0
  111. package/hub/workers/codex-mcp.mjs +724 -507
  112. package/hub/workers/delegator-mcp.mjs +663 -337
  113. package/hub/workers/factory.mjs +100 -21
  114. package/hub/workers/gemini-worker.mjs +179 -86
  115. package/hub/workers/interface.mjs +23 -2
  116. package/hub/workers/lib/jsonrpc-stdio.mjs +463 -0
  117. package/hub/workers/worker-utils.mjs +22 -16
  118. package/package.json +4 -3
  119. package/scripts/lib/agent-json.mjs +27 -0
  120. package/scripts/lib/async.mjs +174 -0
  121. package/scripts/lib/claudemd-scanner.mjs +223 -0
  122. package/scripts/lib/cli-agy.mjs +62 -0
  123. package/scripts/lib/cli-claude.mjs +78 -0
  124. package/scripts/lib/cli-codex.mjs +199 -0
  125. package/scripts/lib/cli-gemini.mjs +67 -0
  126. package/scripts/lib/codex-recovery.sh +50 -0
  127. package/scripts/lib/context.mjs +67 -0
  128. package/scripts/lib/cross-review-utils.mjs +59 -0
  129. package/scripts/lib/doctor-env-checks.mjs +121 -0
  130. package/scripts/lib/dynamic-route-cli.mjs +107 -0
  131. package/scripts/lib/env-probe.mjs +269 -0
  132. package/scripts/lib/env.mjs +14 -0
  133. package/scripts/lib/gemini-profiles.mjs +119 -0
  134. package/scripts/lib/handoff.mjs +187 -0
  135. package/scripts/lib/hook-utils.mjs +16 -0
  136. package/scripts/lib/hub.mjs +48 -0
  137. package/scripts/lib/keyword-rules.mjs +195 -0
  138. package/scripts/lib/logger.mjs +105 -0
  139. package/scripts/lib/mcp-filter.mjs +883 -0
  140. package/scripts/lib/mcp-gateway-servers.mjs +70 -0
  141. package/scripts/lib/mcp-guard-engine.mjs +2120 -0
  142. package/scripts/lib/mcp-health.mjs +773 -0
  143. package/scripts/lib/mcp-manifest.mjs +89 -0
  144. package/scripts/lib/mcp-server-catalog.mjs +355 -0
  145. package/scripts/lib/pid.mjs +63 -0
  146. package/scripts/lib/process-utils.mjs +26 -0
  147. package/scripts/lib/psmux-info.mjs +179 -0
  148. package/scripts/lib/quota.mjs +47 -0
  149. package/scripts/lib/remote-spawn-transfer.mjs +226 -0
  150. package/scripts/lib/skill-state.mjs +220 -0
  151. package/scripts/lib/skill-template.mjs +375 -0
  152. package/scripts/lib/team.mjs +75 -0
  153. package/scripts/lib/timeout.mjs +66 -0
  154. package/scripts/lib/tmp.mjs +26 -0
  155. 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 'node:child_process';
6
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
7
- import { basename, join, posix as posixPath, win32 as win32Path } from 'node:path';
8
- import { execSshWithRetry } from '@triflux/core/hub/lib/ssh-retry.mjs';
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 = 'tfx-remote';
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, '``').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 !== 'pwsh' || parsed.os !== 'win32') return null;
63
+ if (parsed.shell !== "pwsh" || parsed.os !== "win32") return null;
59
64
  if (!parsed.home) return null;
60
65
  return Object.freeze({
61
- claudePath: (!parsed.claude || parsed.claude === 'notfound') ? null : parsed.claude,
66
+ claudePath:
67
+ !parsed.claude || parsed.claude === "notfound" ? null : parsed.claude,
62
68
  home: parsed.home,
63
- os: 'win32',
64
- shell: 'pwsh',
69
+ os: "win32",
70
+ shell: "pwsh",
65
71
  });
66
72
  }
67
73
 
68
74
  function normalizePosixProbeEnv(parsed) {
69
- const os = parsed.os === 'darwin' ? 'darwin' : parsed.os === 'linux' ? 'linux' : null;
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: (!parsed.claude || parsed.claude === 'notfound') ? null : parsed.claude,
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 === 'zsh' ? 'zsh' : 'bash',
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
- 'Write-Output "os=$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows) ? \'win32\' : \'other\')"',
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([host, 'pwsh', '-NoProfile', '-Command', command], {
89
- encoding: 'utf8', timeout: 15000, stdio: ['pipe', 'pipe', 'pipe'],
90
- maxRetries: 2, baseDelayMs: 1000,
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
- 'echo shell=$(basename $SHELL)',
101
- 'echo home=$HOME',
102
- 'command -v claude >/dev/null 2>&1 && echo claude=$(command -v claude) || echo claude=notfound',
103
- 'echo os=$(uname -s | tr A-Z a-z)',
104
- ].join('\n');
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, 'sh'], {
108
- encoding: 'utf8', timeout: 15000, input: script,
109
- maxRetries: 2, baseDelayMs: 1000,
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, 'utf8'));
128
- return parsed && typeof parsed === 'object' ? parsed : null;
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
- && typeof entry.cachedAt === 'number'
138
- && entry.env
139
- && (Date.now() - entry.cachedAt) < REMOTE_ENV_TTL_MS,
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(getEnvCachePath(host, cacheDir), JSON.stringify({ cachedAt: Date.now(), env }, null, 2), 'utf8');
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('.omc', 'state', 'remote-env');
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) { writeEnvCache(host, pwshEnv, cacheDir); return pwshEnv; }
204
+ if (pwshEnv) {
205
+ writeEnvCache(host, pwshEnv, cacheDir);
206
+ return pwshEnv;
207
+ }
170
208
 
171
209
  const posixEnv = probeRemoteEnvViaPosix(host);
172
- if (posixEnv) { writeEnvCache(host, posixEnv, cacheDir); return 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 === 'win32') {
195
- const winDir = requestedDir.replace(/\//g, '\\');
196
- if (winDir === '~') return env.home;
197
- if (/^~[\\/]/u.test(winDir)) return win32Path.join(env.home, winDir.slice(2));
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 === '~') return env.home;
203
- if (requestedDir.startsWith('~/')) return posixPath.join(env.home, requestedDir.slice(2));
204
- if (requestedDir.startsWith('/')) return requestedDir;
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 === 'win32') {
270
+ if (env.os === "win32") {
228
271
  const safePath = escapePwshSingleQuoted(remoteStageDir);
229
- execFileSync('ssh', [host, 'pwsh', '-NoProfile', '-Command', `New-Item -ItemType Directory -Path '${safePath}' -Force | Out-Null`], { timeout: 10000, stdio: 'pipe' });
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('ssh', [host, 'sh', '-lc', `mkdir -p ${shellQuote(remoteStageDir)}`], { timeout: 10000, stdio: 'pipe' });
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('scp', [localPath, `${host}:${remotePath}`], { timeout: 15000, stdio: 'pipe' });
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 = ['git', ...gitArgs].map((a) => shellQuote(a)).join(' ');
346
+ const gitCmd = ["git", ...gitArgs].map((a) => shellQuote(a)).join(" ");
287
347
 
288
- if (env.os === 'win32') {
348
+ if (env.os === "win32") {
289
349
  const cdPath = escapePwshSingleQuoted(cwd);
290
350
  const command = `Set-Location '${cdPath}'; ${gitCmd}`;
291
- return execFileSync('ssh', [host, 'pwsh', '-NoProfile', '-Command', command], {
292
- encoding: 'utf8', timeout: 30_000, stdio: ['pipe', 'pipe', 'pipe'],
293
- }).trim();
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('ssh', [host, 'sh', '-lc', `cd ${shellQuote(cwd)} && ${gitCmd}`], {
297
- encoding: 'utf8', timeout: 30_000, stdio: ['pipe', 'pipe', 'pipe'],
298
- }).trim();
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 shellQuote(value, os) {
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]) => [sessionName, freezeSessionRecord(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(String(captured || "").matchAll(COMPLETION_TOKEN_RE));
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 = match[2] == null ? null : Number.parseInt(match[2], 10);
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((sessionName) => Boolean(sessionName) && sessionName.startsWith(sessionPrefix));
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 || "").replace(/\r/g, "").trimEnd(),
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 state === REMOTE_WATCHER_STATES.COMPLETED || state === REMOTE_WATCHER_STATES.FAILED;
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(eventName, Object.freeze({
256
- exitCode: nextRecord.exitCode,
257
- host: nextRecord.host,
258
- inputWaitPattern: nextRecord.inputWaitPattern,
259
- output: nextRecord.lastOutput,
260
- paneTarget: nextRecord.paneTarget,
261
- probeLevel: nextRecord.lastProbeLevel,
262
- promptIdlePattern: nextRecord.promptIdlePattern,
263
- reason: nextRecord.reason,
264
- sessionName: nextRecord.sessionName,
265
- state: nextRecord.state,
266
- ts: now,
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 = previousRecord.hasActivity || hasMeaningfulActivity(captured);
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
- && hasActivity
297
- && (!inputWait.detected || inputWait.pattern === BARE_INPUT_WAIT_PATTERN)
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
- && previousRecord.state === nextRecord.state
330
- && previousRecord.reason === nextRecord.reason
331
- && previousRecord.exitCode === nextRecord.exitCode
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(sessionName, config);
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({ eventName: "sessionFailed", record: failedRecord });
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(status.sessions)) {
433
+ for (const [sessionName, previousRecord] of Object.entries(
434
+ status.sessions,
435
+ )) {
411
436
  if (activeSet.has(sessionName)) continue;
412
- const { eventName, nextRecord } = markMissingSession(previousRecord, now);
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 });