triflux 10.3.4 → 10.7.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/LICENSE +21 -21
- package/bin/tfx-doctor-tui.mjs +1 -1
- package/bin/tfx-doctor.mjs +6 -1
- package/bin/tfx-profile.mjs +1 -1
- package/bin/tfx-setup-tui.mjs +1 -1
- package/bin/tfx-setup.mjs +6 -1
- package/bin/triflux.mjs +2396 -1140
- package/hooks/agent-route-guard.mjs +12 -8
- package/hooks/cross-review-tracker.mjs +21 -8
- package/hooks/error-context.mjs +19 -7
- package/hooks/hook-adaptive-collector.mjs +18 -16
- package/hooks/hook-manager.mjs +93 -32
- package/hooks/hook-orchestrator.mjs +108 -24
- package/hooks/hook-registry.json +11 -0
- package/hooks/keyword-rules.json +6 -10
- package/hooks/lib/resolve-root.mjs +1 -1
- package/hooks/mcp-config-watcher.mjs +6 -2
- package/hooks/pipeline-stop.mjs +3 -6
- package/hooks/safety-guard.mjs +99 -28
- package/hooks/session-start-fast.mjs +143 -0
- package/hooks/subagent-verifier.mjs +5 -4
- package/hub/account-broker.mjs +256 -60
- package/hub/adaptive-diagnostic.mjs +75 -48
- package/hub/adaptive-inject.mjs +95 -57
- package/hub/adaptive-memory.mjs +156 -42
- package/hub/adaptive.mjs +60 -31
- package/hub/assign-callbacks.mjs +67 -30
- package/hub/bridge.mjs +0 -1
- package/hub/cli-adapter-base.mjs +200 -48
- package/hub/codex-adapter.mjs +76 -96
- package/hub/codex-compat.mjs +3 -3
- package/hub/codex-preflight.mjs +63 -37
- package/hub/delegator/contracts.mjs +19 -23
- package/hub/delegator/index.mjs +3 -3
- package/hub/delegator/service.mjs +88 -64
- package/hub/delegator/tool-definitions.mjs +5 -5
- package/hub/fullcycle.mjs +33 -17
- package/hub/gemini-adapter.mjs +69 -94
- package/hub/hitl.mjs +89 -30
- package/hub/intent.mjs +161 -38
- package/hub/lib/cache-guard.mjs +43 -17
- package/hub/lib/mcp-response-cache.mjs +66 -32
- package/hub/lib/memory-store.mjs +285 -111
- package/hub/lib/path-utils.mjs +35 -37
- package/hub/lib/process-utils.mjs +106 -37
- package/hub/lib/spawn-trace.mjs +527 -0
- package/hub/lib/ssh-command.mjs +34 -4
- package/hub/lib/ssh-retry.mjs +5 -1
- package/hub/lib/uuidv7.mjs +4 -3
- package/hub/memory-doctor.mjs +266 -106
- package/hub/middleware/request-logger.mjs +61 -34
- package/hub/paths.mjs +9 -9
- package/hub/pipeline/gates/confidence.mjs +34 -15
- package/hub/pipeline/gates/consensus.mjs +27 -15
- package/hub/pipeline/gates/index.mjs +7 -3
- package/hub/pipeline/gates/selfcheck.mjs +57 -19
- package/hub/pipeline/index.mjs +77 -42
- package/hub/pipeline/state.mjs +10 -10
- package/hub/pipeline/transitions.mjs +40 -23
- package/hub/platform.mjs +57 -48
- package/hub/promote-penalties.mjs +25 -7
- package/hub/quality/deslop.mjs +70 -49
- package/hub/research.mjs +32 -25
- package/hub/router.mjs +240 -107
- package/hub/routing/complexity.mjs +132 -29
- package/hub/routing/index.mjs +17 -12
- package/hub/routing/q-learning.mjs +76 -28
- package/hub/server.mjs +4 -4
- package/hub/session-fingerprint.mjs +126 -60
- package/hub/state.mjs +84 -43
- package/hub/store-adapter.mjs +59 -26
- package/hub/store.mjs +356 -153
- package/hub/team/agent-map.json +22 -7
- package/hub/team/ansi.mjs +186 -122
- package/hub/team/backend.mjs +28 -10
- package/hub/team/cli/commands/attach.mjs +29 -9
- package/hub/team/cli/commands/control.mjs +29 -8
- package/hub/team/cli/commands/debug.mjs +32 -11
- package/hub/team/cli/commands/focus.mjs +38 -11
- 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 +11 -4
- package/hub/team/cli/commands/send.mjs +19 -6
- package/hub/team/cli/commands/start/index.mjs +154 -31
- package/hub/team/cli/commands/start/parse-args.mjs +38 -11
- package/hub/team/cli/commands/start/start-headless.mjs +112 -36
- package/hub/team/cli/commands/start/start-in-process.mjs +12 -2
- package/hub/team/cli/commands/start/start-mux.mjs +70 -21
- package/hub/team/cli/commands/start/start-wt.mjs +29 -12
- 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 +1 -1
- package/hub/team/cli/index.mjs +2 -2
- package/hub/team/cli/manifest.mjs +38 -8
- package/hub/team/cli/render.mjs +30 -8
- package/hub/team/cli/services/attach-fallback.mjs +31 -11
- package/hub/team/cli/services/hub-client.mjs +42 -14
- package/hub/team/cli/services/member-selector.mjs +11 -4
- package/hub/team/cli/services/native-control.mjs +48 -21
- package/hub/team/cli/services/runtime-mode.mjs +2 -1
- package/hub/team/cli/services/state-store.mjs +25 -8
- package/hub/team/cli/services/task-model.mjs +16 -6
- package/hub/team/conductor-mesh-bridge.mjs +24 -23
- package/hub/team/conductor.mjs +8 -4
- package/hub/team/dashboard-anchor.mjs +4 -5
- package/hub/team/dashboard-layout.mjs +3 -1
- package/hub/team/dashboard-open.mjs +41 -21
- package/hub/team/dashboard.mjs +76 -28
- package/hub/team/event-log.mjs +18 -10
- package/hub/team/handoff.mjs +31 -15
- package/hub/team/headless.mjs +2 -1
- package/hub/team/health-probe.mjs +69 -54
- package/hub/team/launcher-template.mjs +16 -13
- package/hub/team/native-supervisor.mjs +65 -21
- package/hub/team/native.mjs +74 -35
- package/hub/team/nativeProxy.mjs +184 -113
- package/hub/team/notify.mjs +119 -76
- package/hub/team/orchestrator.mjs +9 -4
- package/hub/team/pane.mjs +12 -7
- package/hub/team/process-cleanup.mjs +25 -16
- package/hub/team/psmux.mjs +491 -201
- package/hub/team/remote-probe.mjs +68 -52
- package/hub/team/remote-session.mjs +117 -59
- package/hub/team/remote-watcher.mjs +61 -33
- package/hub/team/routing.mjs +51 -25
- package/hub/team/runtime-strategy.mjs +3 -1
- package/hub/team/session.mjs +98 -34
- package/hub/team/staleState.mjs +72 -30
- package/hub/team/swarm-locks.mjs +15 -13
- package/hub/team/swarm-planner.mjs +32 -21
- package/hub/team/swarm-reconciler.mjs +48 -23
- package/hub/team/tui-lite.mjs +266 -68
- package/hub/team/tui-remote-adapter.mjs +14 -10
- package/hub/team/tui-viewer.mjs +99 -43
- package/hub/team/tui.mjs +708 -271
- package/hub/team/worktree-lifecycle.mjs +152 -58
- package/hub/team/wt-manager.mjs +24 -14
- package/hub/token-mode.mjs +71 -71
- package/hub/tray.mjs +66 -23
- package/hub/workers/claude-worker.mjs +162 -118
- package/hub/workers/codex-mcp.mjs +192 -141
- package/hub/workers/delegator-mcp.mjs +507 -333
- package/hub/workers/factory.mjs +8 -8
- package/hub/workers/gemini-worker.mjs +115 -84
- package/hub/workers/interface.mjs +6 -1
- package/hub/workers/worker-utils.mjs +21 -14
- package/hud/colors.mjs +27 -9
- package/hud/constants.mjs +162 -26
- package/hud/context-monitor.mjs +82 -41
- package/hud/hud-qos-status.mjs +129 -49
- package/hud/mission-board.mjs +6 -3
- package/hud/providers/claude.mjs +226 -115
- package/hud/providers/codex.mjs +62 -22
- package/hud/providers/gemini.mjs +168 -56
- package/hud/renderers.mjs +384 -119
- package/hud/terminal.mjs +101 -31
- package/hud/utils.mjs +78 -38
- package/mesh/index.mjs +11 -5
- package/mesh/mesh-budget.mjs +18 -9
- package/mesh/mesh-heartbeat.mjs +1 -1
- package/mesh/mesh-queue.mjs +3 -5
- package/mesh/mesh-router.mjs +5 -4
- package/package.json +2 -1
- package/scripts/__tests__/gen-skill-docs.test.mjs +36 -7
- package/scripts/__tests__/keyword-detector.test.mjs +77 -28
- package/scripts/__tests__/mcp-guard-engine.test.mjs +58 -20
- package/scripts/__tests__/remote-spawn-transfer.test.mjs +30 -19
- package/scripts/__tests__/remote-spawn.test.mjs +10 -4
- package/scripts/__tests__/session-start-fast.test.mjs +36 -0
- package/scripts/__tests__/skill-template.test.mjs +98 -50
- package/scripts/__tests__/smoke.test.mjs +1 -1
- package/scripts/__tests__/spawn-trace.test.mjs +102 -0
- package/scripts/__tests__/tfx-doctor-diagnose.test.mjs +48 -0
- package/scripts/cache-doctor.mjs +11 -4
- package/scripts/cache-warmup.mjs +96 -37
- package/scripts/claudemd-sync.mjs +27 -17
- package/scripts/codex-gateway-preflight.mjs +52 -37
- package/scripts/codex-mcp-gateway-sync.mjs +59 -39
- package/scripts/completions/tfx.bash +47 -47
- package/scripts/completions/tfx.fish +44 -44
- package/scripts/completions/tfx.zsh +83 -83
- package/scripts/config-audit.mjs +232 -0
- package/scripts/convert-to-tmpl.mjs +54 -0
- package/scripts/cross-review-gate.mjs +35 -12
- package/scripts/cross-review-tracker.mjs +21 -8
- package/scripts/demo.mjs +35 -17
- package/scripts/doctor-diagnose.mjs +284 -0
- package/scripts/gen-skill-docs.mjs +7 -2
- package/scripts/gen-skill-manifest.mjs +2 -1
- package/scripts/headless-guard.mjs +86 -48
- package/scripts/hub-ensure.mjs +45 -26
- package/scripts/keyword-detector.mjs +41 -20
- package/scripts/keyword-rules-expander.mjs +47 -30
- package/scripts/lib/claudemd-scanner.mjs +6 -1
- package/scripts/lib/context.mjs +3 -3
- package/scripts/lib/cross-review-utils.mjs +6 -3
- package/scripts/lib/env-probe.mjs +47 -28
- package/scripts/lib/gemini-profiles.mjs +44 -10
- package/scripts/lib/handoff.mjs +33 -17
- package/scripts/lib/hook-utils.mjs +8 -6
- package/scripts/lib/keyword-rules.mjs +43 -19
- package/scripts/lib/logger.mjs +24 -24
- package/scripts/lib/mcp-filter.mjs +377 -239
- package/scripts/lib/mcp-guard-engine.mjs +194 -79
- package/scripts/lib/mcp-manifest.mjs +23 -13
- package/scripts/lib/mcp-server-catalog.mjs +300 -63
- package/scripts/lib/psmux-info.mjs +11 -6
- package/scripts/lib/remote-spawn-transfer.mjs +44 -14
- package/scripts/lib/skill-template.mjs +30 -7
- package/scripts/mcp-check.mjs +58 -39
- package/scripts/mcp-gateway-config.mjs +83 -39
- package/scripts/mcp-gateway-ensure.mjs +43 -35
- package/scripts/mcp-gateway-integration-test.mjs +70 -58
- package/scripts/mcp-gateway-start.mjs +126 -60
- package/scripts/mcp-gateway-verify.mjs +24 -22
- package/scripts/mcp-safety-guard.mjs +44 -11
- package/scripts/notion-read.mjs +199 -84
- package/scripts/pack.mjs +94 -89
- package/scripts/preflight-cache.mjs +27 -10
- package/scripts/preinstall.mjs +42 -13
- package/scripts/remote-spawn.mjs +309 -94
- package/scripts/run.cjs +8 -5
- package/scripts/session-spawn-helper.mjs +130 -39
- package/scripts/session-stale-cleanup.mjs +123 -0
- package/scripts/setup.mjs +941 -492
- package/scripts/test-lock.mjs +20 -7
- package/scripts/test-tfx-route-no-claude-native.mjs +16 -12
- package/scripts/tfx-batch-stats.mjs +32 -11
- package/scripts/tfx-gate-activate.mjs +11 -4
- package/scripts/tfx-route-post.mjs +87 -20
- package/scripts/tfx-route-worker.mjs +57 -51
- package/scripts/tfx-route.sh +41 -124
- package/scripts/tmp-cleanup.mjs +21 -7
- package/scripts/token-snapshot.mjs +204 -85
- package/skills/.omc/state/agent-replay-8f0e10a9-9693-4410-96f5-a6b07e8ed995.jsonl +1 -0
- package/skills/.omc/state/idle-notif-cooldown.json +3 -0
- package/skills/.omc/state/last-tool-error.json +7 -0
- package/skills/.omc/state/subagent-tracking.json +7 -0
- package/skills/_templates/base.md +1 -6
- package/skills/merge-worktree/SKILL.md.tmpl +144 -0
- package/skills/shared/telemetry-segment.md +6 -0
- package/skills/star-prompt/SKILL.md.tmpl +222 -0
- package/skills/tfx-analysis/SKILL.md.tmpl +107 -0
- package/skills/tfx-analysis/skill.json +1 -6
- package/skills/tfx-auto/SKILL.md +1 -0
- package/skills/tfx-auto-codex/SKILL.md.tmpl +106 -0
- package/skills/tfx-auto-codex/skill.json +1 -3
- package/skills/tfx-autopilot/SKILL.md.tmpl +116 -0
- package/skills/tfx-autopilot/skill.json +1 -5
- package/skills/tfx-autoresearch/SKILL.md.tmpl +136 -0
- package/skills/tfx-autoroute/SKILL.md.tmpl +189 -0
- package/skills/tfx-autoroute/skill.json +1 -7
- package/skills/tfx-codex/SKILL.md +1 -0
- package/skills/tfx-codex/skill.json +1 -3
- package/skills/tfx-codex-swarm/SKILL.md.tmpl +16 -0
- package/skills/tfx-codex-swarm/evals/evals.json +1 -1
- package/skills/tfx-codex-swarm/skill.json +1 -4
- package/skills/tfx-codex-swarm-workspace/iteration-1/benchmark.json +54 -12
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/grading.json +35 -7
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/grading.json +35 -7
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/grading.json +25 -5
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/grading.json +25 -5
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/grading.json +20 -4
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/grading.json +16 -4
- package/skills/tfx-consensus/SKILL.md.tmpl +146 -0
- package/skills/tfx-debate/SKILL.md.tmpl +192 -0
- package/skills/tfx-debate/skill.json +1 -7
- package/skills/tfx-deep-analysis/SKILL.md.tmpl +228 -0
- package/skills/tfx-deep-analysis/skill.json +1 -5
- package/skills/tfx-deep-interview/SKILL.md.tmpl +203 -0
- package/skills/tfx-deep-plan/SKILL.md.tmpl +282 -0
- package/skills/tfx-deep-qa/SKILL.md.tmpl +165 -0
- package/skills/tfx-deep-qa/skill.json +1 -6
- package/skills/tfx-deep-research/SKILL.md.tmpl +217 -0
- package/skills/tfx-deep-review/SKILL.md.tmpl +179 -0
- package/skills/tfx-doctor/SKILL.md +21 -0
- package/skills/tfx-doctor/SKILL.md.tmpl +172 -0
- package/skills/tfx-doctor/skill.json +1 -3
- package/skills/tfx-find/SKILL.md +1 -0
- package/skills/tfx-forge/SKILL.md.tmpl +187 -0
- package/skills/tfx-fullcycle/SKILL.md.tmpl +286 -0
- package/skills/tfx-fullcycle/skill.json +1 -6
- package/skills/tfx-gemini/SKILL.md.tmpl +91 -0
- package/skills/tfx-gemini/skill.json +1 -3
- package/skills/tfx-hooks/SKILL.md.tmpl +216 -0
- package/skills/tfx-hooks/skill.json +1 -3
- package/skills/tfx-hub/SKILL.md.tmpl +212 -0
- package/skills/tfx-hub/skill.json +1 -3
- package/skills/tfx-index/SKILL.md +1 -0
- package/skills/tfx-index/skill.json +1 -6
- package/skills/tfx-interview/SKILL.md.tmpl +285 -0
- package/skills/tfx-multi/SKILL.md.tmpl +183 -0
- package/skills/tfx-multi/skill.json +1 -3
- package/skills/tfx-panel/SKILL.md.tmpl +189 -0
- package/skills/tfx-panel/skill.json +1 -7
- package/skills/tfx-persist/SKILL.md.tmpl +270 -0
- package/skills/tfx-persist/skill.json +1 -7
- package/skills/tfx-plan/SKILL.md +1 -0
- package/skills/tfx-plan/skill.json +1 -6
- package/skills/tfx-profile/SKILL.md.tmpl +239 -0
- package/skills/tfx-profile/skill.json +1 -3
- package/skills/tfx-prune/SKILL.md.tmpl +200 -0
- package/skills/tfx-prune/skill.json +1 -7
- package/skills/tfx-psmux-rules/SKILL.md.tmpl +326 -0
- package/skills/tfx-psmux-rules/skill.json +1 -4
- package/skills/tfx-qa/SKILL.md +1 -0
- package/skills/tfx-qa/skill.json +1 -6
- package/skills/tfx-ralph/SKILL.md.tmpl +28 -0
- package/skills/tfx-ralph/skill.json +1 -4
- package/skills/tfx-remote-setup/SKILL.md.tmpl +576 -0
- package/skills/tfx-remote-setup/skill.json +1 -3
- package/skills/tfx-remote-spawn/SKILL.md.tmpl +263 -0
- package/skills/tfx-remote-spawn/references/hosts.json +16 -0
- package/skills/tfx-remote-spawn/skill.json +1 -4
- package/skills/tfx-research/SKILL.md +1 -0
- package/skills/tfx-review/SKILL.md +1 -0
- package/skills/tfx-review/skill.json +1 -6
- package/skills/tfx-setup/SKILL.md.tmpl +504 -0
- package/skills/tfx-setup/skill.json +1 -3
- package/skills/tfx-swarm/SKILL.md +22 -0
- package/skills/tfx-swarm/SKILL.md.tmpl +218 -0
- package/tui/codex-profile.mjs +88 -33
- package/tui/core.mjs +45 -15
- package/tui/doctor.mjs +75 -28
- package/tui/gemini-profile.mjs +74 -29
- package/tui/monitor-data.mjs +8 -4
- package/tui/monitor.mjs +71 -27
- package/tui/setup.mjs +133 -42
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
// L1.5: INPUT_WAIT 패턴 감지 (detectInputWait 재사용)
|
|
8
8
|
// L3: 완료 토큰 감지 (__TRIFLUX_DONE__ 또는 프롬프트 idle)
|
|
9
9
|
|
|
10
|
-
import { execFileSync } from
|
|
11
|
-
import { detectInputWait, PROBE_DEFAULTS } from
|
|
10
|
+
import { execFileSync } from "node:child_process";
|
|
11
|
+
import { detectInputWait, PROBE_DEFAULTS } from "./health-probe.mjs";
|
|
12
12
|
|
|
13
13
|
/** 완료 토큰 패턴 */
|
|
14
14
|
const COMPLETION_TOKEN_RE = /__TRIFLUX_DONE__/;
|
|
@@ -27,19 +27,25 @@ const _PROMPT_IDLE_RE = /(\u276f|\u2795|>\s*$)/;
|
|
|
27
27
|
export function sshCapturePane(host, paneTarget, lines = 20, deps = {}) {
|
|
28
28
|
const execFn = deps.execFileSync || execFileSync;
|
|
29
29
|
try {
|
|
30
|
-
const output = execFn(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
30
|
+
const output = execFn(
|
|
31
|
+
"ssh",
|
|
32
|
+
[
|
|
33
|
+
"-o",
|
|
34
|
+
"ConnectTimeout=5",
|
|
35
|
+
"-o",
|
|
36
|
+
"BatchMode=yes",
|
|
37
|
+
host,
|
|
38
|
+
`psmux capture-pane -t ${paneTarget} -p -S -`,
|
|
39
|
+
],
|
|
40
|
+
{
|
|
41
|
+
encoding: "utf8",
|
|
42
|
+
timeout: 10_000,
|
|
43
|
+
windowsHide: true,
|
|
44
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
45
|
+
},
|
|
46
|
+
);
|
|
47
|
+
const nonEmpty = output.split("\n").filter((line) => line.trim() !== "");
|
|
48
|
+
return nonEmpty.slice(-lines).join("\n");
|
|
43
49
|
} catch {
|
|
44
50
|
return null;
|
|
45
51
|
}
|
|
@@ -55,16 +61,22 @@ export function sshCapturePane(host, paneTarget, lines = 20, deps = {}) {
|
|
|
55
61
|
export function sshSessionExists(host, sessionName, deps = {}) {
|
|
56
62
|
const execFn = deps.execFileSync || execFileSync;
|
|
57
63
|
try {
|
|
58
|
-
execFn(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
execFn(
|
|
65
|
+
"ssh",
|
|
66
|
+
[
|
|
67
|
+
"-o",
|
|
68
|
+
"ConnectTimeout=5",
|
|
69
|
+
"-o",
|
|
70
|
+
"BatchMode=yes",
|
|
71
|
+
host,
|
|
72
|
+
`psmux has-session -t ${sessionName}`,
|
|
73
|
+
],
|
|
74
|
+
{
|
|
75
|
+
timeout: 10_000,
|
|
76
|
+
windowsHide: true,
|
|
77
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
78
|
+
},
|
|
79
|
+
);
|
|
68
80
|
return true;
|
|
69
81
|
} catch {
|
|
70
82
|
return false;
|
|
@@ -92,7 +104,7 @@ export function createRemoteProbe(session, opts = {}) {
|
|
|
92
104
|
let started = false;
|
|
93
105
|
|
|
94
106
|
// L1 tracking — 출력 변화 감지
|
|
95
|
-
let lastCaptureHash =
|
|
107
|
+
let lastCaptureHash = "";
|
|
96
108
|
let lastOutputChangeAt = Date.now();
|
|
97
109
|
|
|
98
110
|
// L3 tracking — 완료 토큰 / prompt idle
|
|
@@ -102,7 +114,7 @@ export function createRemoteProbe(session, opts = {}) {
|
|
|
102
114
|
const status = {
|
|
103
115
|
l0: null,
|
|
104
116
|
l1: null,
|
|
105
|
-
l2:
|
|
117
|
+
l2: "skip", // 원격은 MCP L2 미지원
|
|
106
118
|
l3: null,
|
|
107
119
|
lastProbeAt: null,
|
|
108
120
|
inputWaitPattern: null,
|
|
@@ -113,7 +125,7 @@ export function createRemoteProbe(session, opts = {}) {
|
|
|
113
125
|
*/
|
|
114
126
|
function probeL0() {
|
|
115
127
|
const exists = sshSessionExists(session.host, session.sessionName, deps);
|
|
116
|
-
status.l0 = exists ?
|
|
128
|
+
status.l0 = exists ? "ok" : "fail";
|
|
117
129
|
return status.l0;
|
|
118
130
|
}
|
|
119
131
|
|
|
@@ -135,9 +147,9 @@ export function createRemoteProbe(session, opts = {}) {
|
|
|
135
147
|
if (hash !== lastCaptureHash) {
|
|
136
148
|
lastCaptureHash = hash;
|
|
137
149
|
lastOutputChangeAt = now;
|
|
138
|
-
status.l1 =
|
|
150
|
+
status.l1 = "ok";
|
|
139
151
|
status.inputWaitPattern = null;
|
|
140
|
-
return
|
|
152
|
+
return "ok";
|
|
141
153
|
}
|
|
142
154
|
|
|
143
155
|
const silenceMs = now - lastOutputChangeAt;
|
|
@@ -147,18 +159,18 @@ export function createRemoteProbe(session, opts = {}) {
|
|
|
147
159
|
const inputWait = detectInputWait(captured);
|
|
148
160
|
|
|
149
161
|
if (inputWait.detected) {
|
|
150
|
-
status.l1 =
|
|
162
|
+
status.l1 = "input_wait";
|
|
151
163
|
status.inputWaitPattern = inputWait.pattern;
|
|
152
|
-
return
|
|
164
|
+
return "input_wait";
|
|
153
165
|
}
|
|
154
166
|
|
|
155
|
-
status.l1 =
|
|
167
|
+
status.l1 = "stall";
|
|
156
168
|
status.inputWaitPattern = null;
|
|
157
|
-
return
|
|
169
|
+
return "stall";
|
|
158
170
|
}
|
|
159
171
|
|
|
160
|
-
status.l1 =
|
|
161
|
-
return
|
|
172
|
+
status.l1 = "ok";
|
|
173
|
+
return "ok";
|
|
162
174
|
}
|
|
163
175
|
|
|
164
176
|
/**
|
|
@@ -167,28 +179,28 @@ export function createRemoteProbe(session, opts = {}) {
|
|
|
167
179
|
*/
|
|
168
180
|
function probeL3(captured) {
|
|
169
181
|
if (promptAcked) {
|
|
170
|
-
status.l3 =
|
|
171
|
-
return
|
|
182
|
+
status.l3 = "ok";
|
|
183
|
+
return "ok";
|
|
172
184
|
}
|
|
173
185
|
|
|
174
186
|
if (captured != null && captured.length > 0) {
|
|
175
187
|
// 완료 토큰 감지 → 즉시 ok
|
|
176
188
|
if (COMPLETION_TOKEN_RE.test(captured)) {
|
|
177
189
|
promptAcked = true;
|
|
178
|
-
status.l3 =
|
|
179
|
-
return
|
|
190
|
+
status.l3 = "completed";
|
|
191
|
+
return "completed";
|
|
180
192
|
}
|
|
181
193
|
|
|
182
194
|
// 출력이 있으면 prompt acknowledged
|
|
183
195
|
promptAcked = true;
|
|
184
|
-
status.l3 =
|
|
185
|
-
return
|
|
196
|
+
status.l3 = "ok";
|
|
197
|
+
return "ok";
|
|
186
198
|
}
|
|
187
199
|
|
|
188
200
|
const elapsed = Date.now() - spawnedAt;
|
|
189
201
|
if (elapsed >= config.l3ThresholdMs) {
|
|
190
|
-
status.l3 =
|
|
191
|
-
return
|
|
202
|
+
status.l3 = "timeout";
|
|
203
|
+
return "timeout";
|
|
192
204
|
}
|
|
193
205
|
|
|
194
206
|
status.l3 = null;
|
|
@@ -204,7 +216,7 @@ export function createRemoteProbe(session, opts = {}) {
|
|
|
204
216
|
|
|
205
217
|
// L0 실패 시 나머지 probe 스킵
|
|
206
218
|
let captured = null;
|
|
207
|
-
if (l0 ===
|
|
219
|
+
if (l0 === "ok") {
|
|
208
220
|
captured = sshCapturePane(session.host, session.paneTarget, 20, deps);
|
|
209
221
|
}
|
|
210
222
|
|
|
@@ -214,14 +226,14 @@ export function createRemoteProbe(session, opts = {}) {
|
|
|
214
226
|
const result = {
|
|
215
227
|
l0,
|
|
216
228
|
l1,
|
|
217
|
-
l2:
|
|
229
|
+
l2: "skip",
|
|
218
230
|
l3,
|
|
219
231
|
inputWaitPattern: status.inputWaitPattern,
|
|
220
232
|
ts: Date.now(),
|
|
221
233
|
};
|
|
222
234
|
status.lastProbeAt = result.ts;
|
|
223
235
|
|
|
224
|
-
if (typeof config.onProbe ===
|
|
236
|
+
if (typeof config.onProbe === "function") {
|
|
225
237
|
config.onProbe(result);
|
|
226
238
|
}
|
|
227
239
|
|
|
@@ -233,10 +245,12 @@ export function createRemoteProbe(session, opts = {}) {
|
|
|
233
245
|
started = true;
|
|
234
246
|
spawnedAt = Date.now();
|
|
235
247
|
lastOutputChangeAt = Date.now();
|
|
236
|
-
lastCaptureHash =
|
|
248
|
+
lastCaptureHash = "";
|
|
237
249
|
promptAcked = false;
|
|
238
250
|
|
|
239
|
-
timer = setInterval(() => {
|
|
251
|
+
timer = setInterval(() => {
|
|
252
|
+
void probe();
|
|
253
|
+
}, config.intervalMs);
|
|
240
254
|
timer.unref?.();
|
|
241
255
|
|
|
242
256
|
// 즉시 첫 probe 실행
|
|
@@ -254,13 +268,13 @@ export function createRemoteProbe(session, opts = {}) {
|
|
|
254
268
|
|
|
255
269
|
/** tracking 리셋 (restart 후 호출) */
|
|
256
270
|
function resetTracking() {
|
|
257
|
-
lastCaptureHash =
|
|
271
|
+
lastCaptureHash = "";
|
|
258
272
|
lastOutputChangeAt = Date.now();
|
|
259
273
|
promptAcked = false;
|
|
260
274
|
spawnedAt = Date.now();
|
|
261
275
|
status.l0 = null;
|
|
262
276
|
status.l1 = null;
|
|
263
|
-
status.l2 =
|
|
277
|
+
status.l2 = "skip";
|
|
264
278
|
status.l3 = null;
|
|
265
279
|
status.inputWaitPattern = null;
|
|
266
280
|
}
|
|
@@ -271,6 +285,8 @@ export function createRemoteProbe(session, opts = {}) {
|
|
|
271
285
|
probe,
|
|
272
286
|
resetTracking,
|
|
273
287
|
getStatus: () => ({ ...status }),
|
|
274
|
-
get started() {
|
|
288
|
+
get started() {
|
|
289
|
+
return started;
|
|
290
|
+
},
|
|
275
291
|
});
|
|
276
292
|
}
|
|
@@ -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 "../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,27 @@ 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,
|
|
73
81
|
home: parsed.home,
|
|
74
82
|
os,
|
|
75
|
-
shell: parsed.shell ===
|
|
83
|
+
shell: parsed.shell === "zsh" ? "zsh" : "bash",
|
|
76
84
|
});
|
|
77
85
|
}
|
|
78
86
|
|
|
@@ -81,14 +89,20 @@ function probeRemoteEnvViaPwsh(host) {
|
|
|
81
89
|
"Write-Output 'shell=pwsh'",
|
|
82
90
|
'Write-Output "home=$env:USERPROFILE"',
|
|
83
91
|
'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(
|
|
92
|
+
"Write-Output \"os=$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows) ? 'win32' : 'other')\"",
|
|
93
|
+
].join("; ");
|
|
86
94
|
|
|
87
95
|
try {
|
|
88
|
-
const output = execSshWithRetry(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
96
|
+
const output = execSshWithRetry(
|
|
97
|
+
[host, "pwsh", "-NoProfile", "-Command", command],
|
|
98
|
+
{
|
|
99
|
+
encoding: "utf8",
|
|
100
|
+
timeout: 15000,
|
|
101
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
102
|
+
maxRetries: 2,
|
|
103
|
+
baseDelayMs: 1000,
|
|
104
|
+
},
|
|
105
|
+
);
|
|
92
106
|
return normalizePwshProbeEnv(parseProbeLines(output));
|
|
93
107
|
} catch {
|
|
94
108
|
return null;
|
|
@@ -97,16 +111,19 @@ function probeRemoteEnvViaPwsh(host) {
|
|
|
97
111
|
|
|
98
112
|
function probeRemoteEnvViaPosix(host) {
|
|
99
113
|
const script = [
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
].join(
|
|
114
|
+
"echo shell=$(basename $SHELL)",
|
|
115
|
+
"echo home=$HOME",
|
|
116
|
+
"command -v claude >/dev/null 2>&1 && echo claude=$(command -v claude) || echo claude=notfound",
|
|
117
|
+
"echo os=$(uname -s | tr A-Z a-z)",
|
|
118
|
+
].join("\n");
|
|
105
119
|
|
|
106
120
|
try {
|
|
107
|
-
const output = execSshWithRetry([host,
|
|
108
|
-
encoding:
|
|
109
|
-
|
|
121
|
+
const output = execSshWithRetry([host, "sh"], {
|
|
122
|
+
encoding: "utf8",
|
|
123
|
+
timeout: 15000,
|
|
124
|
+
input: script,
|
|
125
|
+
maxRetries: 2,
|
|
126
|
+
baseDelayMs: 1000,
|
|
110
127
|
});
|
|
111
128
|
return normalizePosixProbeEnv(parseProbeLines(output));
|
|
112
129
|
} catch {
|
|
@@ -124,8 +141,8 @@ function readEnvCache(host, cacheDir) {
|
|
|
124
141
|
const cachePath = getEnvCachePath(host, cacheDir);
|
|
125
142
|
if (!existsSync(cachePath)) return null;
|
|
126
143
|
try {
|
|
127
|
-
const parsed = JSON.parse(readFileSync(cachePath,
|
|
128
|
-
return parsed && typeof parsed ===
|
|
144
|
+
const parsed = JSON.parse(readFileSync(cachePath, "utf8"));
|
|
145
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
129
146
|
} catch {
|
|
130
147
|
return null;
|
|
131
148
|
}
|
|
@@ -133,16 +150,20 @@ function readEnvCache(host, cacheDir) {
|
|
|
133
150
|
|
|
134
151
|
function isEnvCacheFresh(entry) {
|
|
135
152
|
return Boolean(
|
|
136
|
-
entry
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
153
|
+
entry &&
|
|
154
|
+
typeof entry.cachedAt === "number" &&
|
|
155
|
+
entry.env &&
|
|
156
|
+
Date.now() - entry.cachedAt < REMOTE_ENV_TTL_MS,
|
|
140
157
|
);
|
|
141
158
|
}
|
|
142
159
|
|
|
143
160
|
function writeEnvCache(host, env, cacheDir) {
|
|
144
161
|
mkdirSync(cacheDir, { recursive: true });
|
|
145
|
-
writeFileSync(
|
|
162
|
+
writeFileSync(
|
|
163
|
+
getEnvCachePath(host, cacheDir),
|
|
164
|
+
JSON.stringify({ cachedAt: Date.now(), env }, null, 2),
|
|
165
|
+
"utf8",
|
|
166
|
+
);
|
|
146
167
|
}
|
|
147
168
|
|
|
148
169
|
/**
|
|
@@ -158,7 +179,7 @@ function writeEnvCache(host, env, cacheDir) {
|
|
|
158
179
|
export function probeRemoteEnv(host, opts = {}) {
|
|
159
180
|
validateHost(host);
|
|
160
181
|
const force = opts.force === true;
|
|
161
|
-
const cacheDir = opts.cacheDir || join(
|
|
182
|
+
const cacheDir = opts.cacheDir || join(".omc", "state", "remote-env");
|
|
162
183
|
|
|
163
184
|
if (!force) {
|
|
164
185
|
const cached = readEnvCache(host, cacheDir);
|
|
@@ -166,10 +187,16 @@ export function probeRemoteEnv(host, opts = {}) {
|
|
|
166
187
|
}
|
|
167
188
|
|
|
168
189
|
const pwshEnv = probeRemoteEnvViaPwsh(host);
|
|
169
|
-
if (pwshEnv) {
|
|
190
|
+
if (pwshEnv) {
|
|
191
|
+
writeEnvCache(host, pwshEnv, cacheDir);
|
|
192
|
+
return pwshEnv;
|
|
193
|
+
}
|
|
170
194
|
|
|
171
195
|
const posixEnv = probeRemoteEnvViaPosix(host);
|
|
172
|
-
if (posixEnv) {
|
|
196
|
+
if (posixEnv) {
|
|
197
|
+
writeEnvCache(host, posixEnv, cacheDir);
|
|
198
|
+
return posixEnv;
|
|
199
|
+
}
|
|
173
200
|
|
|
174
201
|
throw new Error(`remote probe failed for ${host}`);
|
|
175
202
|
}
|
|
@@ -177,7 +204,7 @@ export function probeRemoteEnv(host, opts = {}) {
|
|
|
177
204
|
// ── Remote directory resolution ─────────────────────────────────
|
|
178
205
|
|
|
179
206
|
function isWindowsAbsolutePath(value) {
|
|
180
|
-
return /^[a-zA-Z]:[\\/]/u.test(value) || value.startsWith(
|
|
207
|
+
return /^[a-zA-Z]:[\\/]/u.test(value) || value.startsWith("\\\\");
|
|
181
208
|
}
|
|
182
209
|
|
|
183
210
|
/**
|
|
@@ -191,17 +218,19 @@ function isWindowsAbsolutePath(value) {
|
|
|
191
218
|
export function resolveRemoteDir(dir, env) {
|
|
192
219
|
const requestedDir = dir || env.home;
|
|
193
220
|
|
|
194
|
-
if (env.os ===
|
|
195
|
-
const winDir = requestedDir.replace(/\//g,
|
|
196
|
-
if (winDir ===
|
|
197
|
-
if (/^~[\\/]/u.test(winDir))
|
|
221
|
+
if (env.os === "win32") {
|
|
222
|
+
const winDir = requestedDir.replace(/\//g, "\\");
|
|
223
|
+
if (winDir === "~") return env.home;
|
|
224
|
+
if (/^~[\\/]/u.test(winDir))
|
|
225
|
+
return win32Path.join(env.home, winDir.slice(2));
|
|
198
226
|
if (isWindowsAbsolutePath(winDir)) return winDir;
|
|
199
227
|
return win32Path.join(env.home, winDir);
|
|
200
228
|
}
|
|
201
229
|
|
|
202
|
-
if (requestedDir ===
|
|
203
|
-
if (requestedDir.startsWith(
|
|
204
|
-
|
|
230
|
+
if (requestedDir === "~") return env.home;
|
|
231
|
+
if (requestedDir.startsWith("~/"))
|
|
232
|
+
return posixPath.join(env.home, requestedDir.slice(2));
|
|
233
|
+
if (requestedDir.startsWith("/")) return requestedDir;
|
|
205
234
|
return posixPath.join(env.home, requestedDir);
|
|
206
235
|
}
|
|
207
236
|
|
|
@@ -224,12 +253,26 @@ export function resolveRemoteStageDir(env, stageId) {
|
|
|
224
253
|
* @param {string} remoteStageDir
|
|
225
254
|
*/
|
|
226
255
|
export function ensureRemoteStageDir(host, env, remoteStageDir) {
|
|
227
|
-
if (env.os ===
|
|
256
|
+
if (env.os === "win32") {
|
|
228
257
|
const safePath = escapePwshSingleQuoted(remoteStageDir);
|
|
229
|
-
execFileSync(
|
|
258
|
+
execFileSync(
|
|
259
|
+
"ssh",
|
|
260
|
+
[
|
|
261
|
+
host,
|
|
262
|
+
"pwsh",
|
|
263
|
+
"-NoProfile",
|
|
264
|
+
"-Command",
|
|
265
|
+
`New-Item -ItemType Directory -Path '${safePath}' -Force | Out-Null`,
|
|
266
|
+
],
|
|
267
|
+
{ timeout: 10000, stdio: "pipe" },
|
|
268
|
+
);
|
|
230
269
|
return;
|
|
231
270
|
}
|
|
232
|
-
execFileSync(
|
|
271
|
+
execFileSync(
|
|
272
|
+
"ssh",
|
|
273
|
+
[host, "sh", "-lc", `mkdir -p ${shellQuote(remoteStageDir)}`],
|
|
274
|
+
{ timeout: 10000, stdio: "pipe" },
|
|
275
|
+
);
|
|
233
276
|
}
|
|
234
277
|
|
|
235
278
|
/**
|
|
@@ -239,7 +282,10 @@ export function ensureRemoteStageDir(host, env, remoteStageDir) {
|
|
|
239
282
|
* @param {string} remotePath
|
|
240
283
|
*/
|
|
241
284
|
export function uploadFileToRemote(host, localPath, remotePath) {
|
|
242
|
-
execFileSync(
|
|
285
|
+
execFileSync("scp", [localPath, `${host}:${remotePath}`], {
|
|
286
|
+
timeout: 15000,
|
|
287
|
+
stdio: "pipe",
|
|
288
|
+
});
|
|
243
289
|
}
|
|
244
290
|
|
|
245
291
|
/**
|
|
@@ -283,17 +329,29 @@ export function stageRemotePromptFiles(host, env, transferCandidates, stageId) {
|
|
|
283
329
|
* @returns {string} stdout
|
|
284
330
|
*/
|
|
285
331
|
export function remoteGit(host, env, gitArgs, cwd) {
|
|
286
|
-
const gitCmd = [
|
|
332
|
+
const gitCmd = ["git", ...gitArgs].map((a) => shellQuote(a)).join(" ");
|
|
287
333
|
|
|
288
|
-
if (env.os ===
|
|
334
|
+
if (env.os === "win32") {
|
|
289
335
|
const cdPath = escapePwshSingleQuoted(cwd);
|
|
290
336
|
const command = `Set-Location '${cdPath}'; ${gitCmd}`;
|
|
291
|
-
return execFileSync(
|
|
292
|
-
|
|
293
|
-
|
|
337
|
+
return execFileSync(
|
|
338
|
+
"ssh",
|
|
339
|
+
[host, "pwsh", "-NoProfile", "-Command", command],
|
|
340
|
+
{
|
|
341
|
+
encoding: "utf8",
|
|
342
|
+
timeout: 30_000,
|
|
343
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
344
|
+
},
|
|
345
|
+
).trim();
|
|
294
346
|
}
|
|
295
347
|
|
|
296
|
-
return execFileSync(
|
|
297
|
-
|
|
298
|
-
|
|
348
|
+
return execFileSync(
|
|
349
|
+
"ssh",
|
|
350
|
+
[host, "sh", "-lc", `cd ${shellQuote(cwd)} && ${gitCmd}`],
|
|
351
|
+
{
|
|
352
|
+
encoding: "utf8",
|
|
353
|
+
timeout: 30_000,
|
|
354
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
355
|
+
},
|
|
356
|
+
).trim();
|
|
299
357
|
}
|