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
package/scripts/remote-spawn.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
|
|
2
3
|
// remote-spawn.mjs — 로컬/원격 Claude 세션 실행 유틸리티
|
|
3
4
|
//
|
|
4
5
|
// Usage:
|
|
@@ -9,11 +10,25 @@
|
|
|
9
10
|
// node remote-spawn.mjs --attach <session>
|
|
10
11
|
// node remote-spawn.mjs --probe <ssh-host>
|
|
11
12
|
|
|
13
|
+
import { execFileSync, execSync } from "child_process";
|
|
14
|
+
import { spawn } from "../hub/lib/spawn-trace.mjs";
|
|
12
15
|
import { randomUUID } from "crypto";
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
import {
|
|
17
|
+
existsSync,
|
|
18
|
+
mkdirSync,
|
|
19
|
+
readFileSync,
|
|
20
|
+
statSync,
|
|
21
|
+
unlinkSync,
|
|
22
|
+
writeFileSync,
|
|
23
|
+
} from "fs";
|
|
24
|
+
import { platform as getPlatform, homedir, tmpdir } from "os";
|
|
25
|
+
import {
|
|
26
|
+
basename,
|
|
27
|
+
join,
|
|
28
|
+
posix as posixPath,
|
|
29
|
+
resolve,
|
|
30
|
+
win32 as win32Path,
|
|
31
|
+
} from "path";
|
|
17
32
|
import { fileURLToPath } from "url";
|
|
18
33
|
import {
|
|
19
34
|
attachPsmuxSession,
|
|
@@ -81,7 +96,9 @@ function sleepMs(ms) {
|
|
|
81
96
|
}
|
|
82
97
|
|
|
83
98
|
function sleepMsAsync(ms) {
|
|
84
|
-
return new Promise((resolveSleep) =>
|
|
99
|
+
return new Promise((resolveSleep) =>
|
|
100
|
+
setTimeout(resolveSleep, Math.max(0, ms)),
|
|
101
|
+
);
|
|
85
102
|
}
|
|
86
103
|
|
|
87
104
|
function parsePositiveInt(value, fallback) {
|
|
@@ -113,15 +130,31 @@ export function buildRemoteClaudeCommand(env, permissionFlags = "") {
|
|
|
113
130
|
return `${shellQuote(env.claudePath)}${permissionFlags ? ` ${permissionFlags}` : ""}; ${buildPosixExitTail()}`;
|
|
114
131
|
}
|
|
115
132
|
|
|
116
|
-
export function resolveCleanupWatcherTimingOptions(
|
|
133
|
+
export function resolveCleanupWatcherTimingOptions(
|
|
134
|
+
source = {},
|
|
135
|
+
env = process.env,
|
|
136
|
+
) {
|
|
117
137
|
return Object.freeze({
|
|
118
|
-
graceMs: parsePositiveInt(
|
|
119
|
-
|
|
120
|
-
|
|
138
|
+
graceMs: parsePositiveInt(
|
|
139
|
+
source.graceMs ?? env.TFX_SPAWN_CLEANUP_GRACE_MS,
|
|
140
|
+
DEFAULT_CLEANUP_WATCH_GRACE_MS,
|
|
141
|
+
),
|
|
142
|
+
maxMs: parsePositiveInt(
|
|
143
|
+
source.maxMs ?? env.TFX_SPAWN_CLEANUP_MAX_MS,
|
|
144
|
+
DEFAULT_CLEANUP_WATCH_MAX_MS,
|
|
145
|
+
),
|
|
146
|
+
pollMs: parsePositiveInt(
|
|
147
|
+
source.pollMs ?? env.TFX_SPAWN_CLEANUP_POLL_MS,
|
|
148
|
+
DEFAULT_CLEANUP_WATCH_POLL_MS,
|
|
149
|
+
),
|
|
121
150
|
});
|
|
122
151
|
}
|
|
123
152
|
|
|
124
|
-
export function buildSpawnCleanupWatcherArgs(
|
|
153
|
+
export function buildSpawnCleanupWatcherArgs(
|
|
154
|
+
sessionName,
|
|
155
|
+
paneId,
|
|
156
|
+
timingOptions = {},
|
|
157
|
+
) {
|
|
125
158
|
const timings = resolveCleanupWatcherTimingOptions(timingOptions);
|
|
126
159
|
return [
|
|
127
160
|
SELF_SCRIPT_PATH,
|
|
@@ -159,11 +192,15 @@ function buildSessionSlug(customName) {
|
|
|
159
192
|
}).trim();
|
|
160
193
|
if (branch) {
|
|
161
194
|
// feature/swarm-hypervisor → swarm-hypervisor
|
|
162
|
-
const stripped = branch.includes("/")
|
|
195
|
+
const stripped = branch.includes("/")
|
|
196
|
+
? branch.split("/").slice(1).join("-")
|
|
197
|
+
: branch;
|
|
163
198
|
const slug = toSlug(stripped);
|
|
164
199
|
if (slug) return slug;
|
|
165
200
|
}
|
|
166
|
-
} catch {
|
|
201
|
+
} catch {
|
|
202
|
+
/* git not available */
|
|
203
|
+
}
|
|
167
204
|
return randomUUID().slice(0, 8);
|
|
168
205
|
}
|
|
169
206
|
|
|
@@ -176,7 +213,9 @@ function deduplicateSessionName(baseName) {
|
|
|
176
213
|
const candidate = `${baseName}-${i}`;
|
|
177
214
|
if (!existing.includes(candidate)) return candidate;
|
|
178
215
|
}
|
|
179
|
-
} catch {
|
|
216
|
+
} catch {
|
|
217
|
+
/* psmux not available */
|
|
218
|
+
}
|
|
180
219
|
return `${baseName}-${randomUUID().slice(0, 4)}`;
|
|
181
220
|
}
|
|
182
221
|
|
|
@@ -323,7 +362,8 @@ function parseArgs(argv) {
|
|
|
323
362
|
promptParts.push(arg);
|
|
324
363
|
}
|
|
325
364
|
|
|
326
|
-
const mergedPrompt =
|
|
365
|
+
const mergedPrompt =
|
|
366
|
+
prompt ?? (promptParts.length > 0 ? promptParts.join(" ") : null);
|
|
327
367
|
return {
|
|
328
368
|
command,
|
|
329
369
|
dir,
|
|
@@ -345,7 +385,11 @@ function parseArgs(argv) {
|
|
|
345
385
|
function parseVersion(versionStr) {
|
|
346
386
|
const match = /(\d+)\.(\d+)\.(\d+)/.exec(versionStr);
|
|
347
387
|
if (!match) return null;
|
|
348
|
-
return [
|
|
388
|
+
return [
|
|
389
|
+
parseInt(match[1], 10),
|
|
390
|
+
parseInt(match[2], 10),
|
|
391
|
+
parseInt(match[3], 10),
|
|
392
|
+
];
|
|
349
393
|
}
|
|
350
394
|
|
|
351
395
|
function compareVersions(a, b) {
|
|
@@ -360,12 +404,16 @@ function probeVersion(binPath) {
|
|
|
360
404
|
if (/\.(cmd|bat)$/iu.test(binPath)) {
|
|
361
405
|
// .cmd/.bat → execSync로 shell 경유 (execFileSync EINVAL 회피)
|
|
362
406
|
const out = execSync(`"${binPath}" --version`, {
|
|
363
|
-
encoding: "utf8",
|
|
407
|
+
encoding: "utf8",
|
|
408
|
+
timeout: 3000,
|
|
409
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
364
410
|
});
|
|
365
411
|
return parseVersion(out);
|
|
366
412
|
}
|
|
367
413
|
const out = execFileSync(binPath, ["--version"], {
|
|
368
|
-
encoding: "utf8",
|
|
414
|
+
encoding: "utf8",
|
|
415
|
+
timeout: 3000,
|
|
416
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
369
417
|
});
|
|
370
418
|
return parseVersion(out);
|
|
371
419
|
} catch {
|
|
@@ -378,7 +426,15 @@ function detectClaudePath() {
|
|
|
378
426
|
|
|
379
427
|
const candidates = [];
|
|
380
428
|
|
|
381
|
-
const wingetPath = join(
|
|
429
|
+
const wingetPath = join(
|
|
430
|
+
homedir(),
|
|
431
|
+
"AppData",
|
|
432
|
+
"Local",
|
|
433
|
+
"Microsoft",
|
|
434
|
+
"WinGet",
|
|
435
|
+
"Links",
|
|
436
|
+
"claude.exe",
|
|
437
|
+
);
|
|
382
438
|
if (existsSync(wingetPath)) candidates.push(wingetPath);
|
|
383
439
|
|
|
384
440
|
const npmPath = join(process.env.APPDATA || "", "npm", "claude.cmd");
|
|
@@ -386,7 +442,10 @@ function detectClaudePath() {
|
|
|
386
442
|
|
|
387
443
|
try {
|
|
388
444
|
const command = IS_WINDOWS_LOCAL ? "where" : "which";
|
|
389
|
-
const result = execFileSync(command, ["claude"], {
|
|
445
|
+
const result = execFileSync(command, ["claude"], {
|
|
446
|
+
encoding: "utf8",
|
|
447
|
+
timeout: 5000,
|
|
448
|
+
}).trim();
|
|
390
449
|
if (result) {
|
|
391
450
|
for (const line of result.split(/\r?\n/u)) {
|
|
392
451
|
const p = line.trim();
|
|
@@ -415,7 +474,9 @@ function detectClaudePath() {
|
|
|
415
474
|
}
|
|
416
475
|
|
|
417
476
|
function getPermissionFlag() {
|
|
418
|
-
return process.env.TFX_CLAUDE_SAFE_MODE === "1"
|
|
477
|
+
return process.env.TFX_CLAUDE_SAFE_MODE === "1"
|
|
478
|
+
? []
|
|
479
|
+
: ["--dangerously-skip-permissions"];
|
|
419
480
|
}
|
|
420
481
|
|
|
421
482
|
function failFast(message) {
|
|
@@ -463,7 +524,10 @@ function buildPromptContext(args, options = {}) {
|
|
|
463
524
|
let content = "";
|
|
464
525
|
|
|
465
526
|
if (args.handoff) {
|
|
466
|
-
const handoffCandidate = validateTransferCandidate(
|
|
527
|
+
const handoffCandidate = validateTransferCandidate(
|
|
528
|
+
args.handoff,
|
|
529
|
+
"handoff file",
|
|
530
|
+
);
|
|
467
531
|
content = readFileSync(handoffCandidate.localPath, "utf8").trim();
|
|
468
532
|
candidates.push(handoffCandidate);
|
|
469
533
|
}
|
|
@@ -558,7 +622,11 @@ function spawnLocalFallback(args, claudePath, prompt) {
|
|
|
558
622
|
}
|
|
559
623
|
|
|
560
624
|
try {
|
|
561
|
-
spawn("wt.exe", wtArgs, {
|
|
625
|
+
spawn("wt.exe", wtArgs, {
|
|
626
|
+
detached: true,
|
|
627
|
+
stdio: "ignore",
|
|
628
|
+
windowsHide: false,
|
|
629
|
+
}).unref();
|
|
562
630
|
console.log(`spawned local Claude in WT tab → ${dir}`);
|
|
563
631
|
} catch (error) {
|
|
564
632
|
console.error("wt.exe spawn failed:", error.message);
|
|
@@ -579,13 +647,21 @@ function spawnRemoteFallback(args, promptContext) {
|
|
|
579
647
|
|
|
580
648
|
let remoteHome;
|
|
581
649
|
try {
|
|
582
|
-
remoteHome = execFileSync("ssh", [host, "echo", "$env:USERPROFILE"], {
|
|
650
|
+
remoteHome = execFileSync("ssh", [host, "echo", "$env:USERPROFILE"], {
|
|
651
|
+
encoding: "utf8",
|
|
652
|
+
timeout: 5000,
|
|
653
|
+
}).trim();
|
|
583
654
|
} catch {
|
|
584
655
|
try {
|
|
585
|
-
remoteHome = execFileSync("ssh", [host, "echo", "$HOME"], {
|
|
656
|
+
remoteHome = execFileSync("ssh", [host, "echo", "$HOME"], {
|
|
657
|
+
encoding: "utf8",
|
|
658
|
+
timeout: 5000,
|
|
659
|
+
}).trim();
|
|
586
660
|
} catch {
|
|
587
661
|
// 원격 홈 감지 실패 시 transfer 기능을 사용할 수 없음
|
|
588
|
-
console.warn(
|
|
662
|
+
console.warn(
|
|
663
|
+
`[tfx] 원격 홈 디렉토리 감지 실패 (${host}) — file transfer 비활성화`,
|
|
664
|
+
);
|
|
589
665
|
remoteHome = null;
|
|
590
666
|
}
|
|
591
667
|
}
|
|
@@ -594,24 +670,33 @@ function spawnRemoteFallback(args, promptContext) {
|
|
|
594
670
|
try {
|
|
595
671
|
const fallbackEnv = { home: remoteHome, os: "win32", shell: "pwsh" };
|
|
596
672
|
const stageId = `spawn-${randomUUID().slice(0, 8)}`;
|
|
597
|
-
const { stagedFiles } = stageRemotePromptFiles(
|
|
673
|
+
const { stagedFiles } = stageRemotePromptFiles(
|
|
674
|
+
host,
|
|
675
|
+
fallbackEnv,
|
|
676
|
+
promptContext.transferCandidates,
|
|
677
|
+
stageId,
|
|
678
|
+
);
|
|
598
679
|
prompt = rewritePromptPaths(prompt, stagedFiles);
|
|
599
680
|
} catch (error) {
|
|
600
|
-
failFast(
|
|
681
|
+
failFast(
|
|
682
|
+
`failed to stage remote files: ${error?.message || String(error)}`,
|
|
683
|
+
);
|
|
601
684
|
}
|
|
602
685
|
} else if (promptContext.transferCandidates.length > 0) {
|
|
603
686
|
console.warn("[tfx] 원격 홈 미감지 — --transfer 파일이 무시됩니다");
|
|
604
687
|
}
|
|
605
688
|
|
|
606
|
-
const scriptLines = [
|
|
607
|
-
`cd '${dir.replace(/'/g, "''")}'`,
|
|
608
|
-
];
|
|
689
|
+
const scriptLines = [`cd '${dir.replace(/'/g, "''")}'`];
|
|
609
690
|
|
|
610
691
|
if (prompt) {
|
|
611
692
|
const safePrompt = prompt.replace(/'/g, "''");
|
|
612
|
-
scriptLines.push(
|
|
693
|
+
scriptLines.push(
|
|
694
|
+
`& "$env:USERPROFILE\\.local\\bin\\claude.exe" ${permFlags.join(" ")} '${safePrompt}'`,
|
|
695
|
+
);
|
|
613
696
|
} else {
|
|
614
|
-
scriptLines.push(
|
|
697
|
+
scriptLines.push(
|
|
698
|
+
`& "$env:USERPROFILE\\.local\\bin\\claude.exe" ${permFlags.join(" ")}`,
|
|
699
|
+
);
|
|
615
700
|
}
|
|
616
701
|
|
|
617
702
|
const scriptContent = scriptLines.join("\n");
|
|
@@ -619,7 +704,10 @@ function spawnRemoteFallback(args, promptContext) {
|
|
|
619
704
|
writeFileSync(localScript, scriptContent, "utf8");
|
|
620
705
|
|
|
621
706
|
try {
|
|
622
|
-
execFileSync("scp", [localScript, `${host}:tfx-remote-spawn.ps1`], {
|
|
707
|
+
execFileSync("scp", [localScript, `${host}:tfx-remote-spawn.ps1`], {
|
|
708
|
+
timeout: 10000,
|
|
709
|
+
stdio: "pipe",
|
|
710
|
+
});
|
|
623
711
|
} catch (error) {
|
|
624
712
|
console.error("failed to copy script to remote:", error.message);
|
|
625
713
|
process.exit(1);
|
|
@@ -641,14 +729,20 @@ function spawnRemoteFallback(args, promptContext) {
|
|
|
641
729
|
remoteCmd,
|
|
642
730
|
];
|
|
643
731
|
try {
|
|
644
|
-
spawn("wt.exe", wtArgs, {
|
|
732
|
+
spawn("wt.exe", wtArgs, {
|
|
733
|
+
detached: true,
|
|
734
|
+
stdio: "ignore",
|
|
735
|
+
windowsHide: false,
|
|
736
|
+
}).unref();
|
|
645
737
|
console.log(`spawned remote Claude → ${host}:${dir}`);
|
|
646
738
|
} catch (error) {
|
|
647
739
|
console.error("wt.exe spawn failed:", error.message);
|
|
648
740
|
process.exit(1);
|
|
649
741
|
}
|
|
650
742
|
} else {
|
|
651
|
-
const child = spawn("ssh", ["-t", "--", host, remoteCmd], {
|
|
743
|
+
const child = spawn("ssh", ["-t", "--", host, remoteCmd], {
|
|
744
|
+
stdio: "inherit",
|
|
745
|
+
});
|
|
652
746
|
child.on("exit", (code) => process.exit(code || 0));
|
|
653
747
|
}
|
|
654
748
|
}
|
|
@@ -689,7 +783,8 @@ function normalizePwshProbeEnv(host, parsed) {
|
|
|
689
783
|
}
|
|
690
784
|
|
|
691
785
|
return Object.freeze({
|
|
692
|
-
claudePath:
|
|
786
|
+
claudePath:
|
|
787
|
+
!parsed.claude || parsed.claude === "notfound" ? null : parsed.claude,
|
|
693
788
|
home: parsed.home,
|
|
694
789
|
os: "win32",
|
|
695
790
|
shell: "pwsh",
|
|
@@ -697,13 +792,15 @@ function normalizePwshProbeEnv(host, parsed) {
|
|
|
697
792
|
}
|
|
698
793
|
|
|
699
794
|
function normalizePosixProbeEnv(host, parsed) {
|
|
700
|
-
const os =
|
|
795
|
+
const os =
|
|
796
|
+
parsed.os === "darwin" ? "darwin" : parsed.os === "linux" ? "linux" : null;
|
|
701
797
|
if (!os || !parsed.home) {
|
|
702
798
|
return null;
|
|
703
799
|
}
|
|
704
800
|
|
|
705
801
|
return Object.freeze({
|
|
706
|
-
claudePath:
|
|
802
|
+
claudePath:
|
|
803
|
+
!parsed.claude || parsed.claude === "notfound" ? null : parsed.claude,
|
|
707
804
|
home: parsed.home,
|
|
708
805
|
os,
|
|
709
806
|
shell: parsed.shell === "zsh" ? "zsh" : "bash",
|
|
@@ -730,10 +827,10 @@ function readRemoteEnvCache(host) {
|
|
|
730
827
|
|
|
731
828
|
function isRemoteEnvCacheFresh(cacheEntry) {
|
|
732
829
|
return Boolean(
|
|
733
|
-
cacheEntry
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
830
|
+
cacheEntry &&
|
|
831
|
+
typeof cacheEntry.cachedAt === "number" &&
|
|
832
|
+
cacheEntry.env &&
|
|
833
|
+
Date.now() - cacheEntry.cachedAt < REMOTE_ENV_TTL_MS,
|
|
737
834
|
);
|
|
738
835
|
}
|
|
739
836
|
|
|
@@ -751,7 +848,7 @@ function probeRemoteEnvViaPwsh(host) {
|
|
|
751
848
|
"Write-Output 'shell=pwsh'",
|
|
752
849
|
'Write-Output "home=$env:USERPROFILE"',
|
|
753
850
|
'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\' }',
|
|
754
|
-
|
|
851
|
+
"Write-Output \"os=$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows) ? 'win32' : 'other')\"",
|
|
755
852
|
].join("; ");
|
|
756
853
|
|
|
757
854
|
let output;
|
|
@@ -825,13 +922,15 @@ function resolveRemoteDir(dir, env) {
|
|
|
825
922
|
if (env.os === "win32") {
|
|
826
923
|
const winDir = requestedDir.replace(/\//g, "\\");
|
|
827
924
|
if (winDir === "~") return env.home;
|
|
828
|
-
if (/^~[\\/]/u.test(winDir))
|
|
925
|
+
if (/^~[\\/]/u.test(winDir))
|
|
926
|
+
return win32Path.join(env.home, winDir.slice(2));
|
|
829
927
|
if (isWindowsAbsolutePath(winDir)) return winDir;
|
|
830
928
|
return win32Path.join(env.home, winDir);
|
|
831
929
|
}
|
|
832
930
|
|
|
833
931
|
if (requestedDir === "~") return env.home;
|
|
834
|
-
if (requestedDir.startsWith("~/"))
|
|
932
|
+
if (requestedDir.startsWith("~/"))
|
|
933
|
+
return posixPath.join(env.home, requestedDir.slice(2));
|
|
835
934
|
if (requestedDir.startsWith("/")) return requestedDir;
|
|
836
935
|
return posixPath.join(env.home, requestedDir);
|
|
837
936
|
}
|
|
@@ -845,20 +944,30 @@ function ensureRemoteStageDir(host, env, remoteStageDir) {
|
|
|
845
944
|
if (env.os === "win32") {
|
|
846
945
|
const safePath = escapePwshSingleQuoted(remoteStageDir);
|
|
847
946
|
const command = `New-Item -ItemType Directory -Path '${safePath}' -Force | Out-Null`;
|
|
848
|
-
execFileSync("ssh", [host, "pwsh", "-NoProfile", "-Command", command], {
|
|
947
|
+
execFileSync("ssh", [host, "pwsh", "-NoProfile", "-Command", command], {
|
|
948
|
+
timeout: 10000,
|
|
949
|
+
stdio: "pipe",
|
|
950
|
+
});
|
|
849
951
|
return;
|
|
850
952
|
}
|
|
851
953
|
|
|
852
|
-
execFileSync(
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
954
|
+
execFileSync(
|
|
955
|
+
"ssh",
|
|
956
|
+
[host, "sh", "-lc", `mkdir -p ${shellQuote(remoteStageDir)}`],
|
|
957
|
+
{
|
|
958
|
+
timeout: 10000,
|
|
959
|
+
stdio: "pipe",
|
|
960
|
+
},
|
|
961
|
+
);
|
|
856
962
|
}
|
|
857
963
|
|
|
858
964
|
function uploadFileToRemote(host, localPath, remotePath) {
|
|
859
965
|
// scp는 remote path를 셸 확장 없이 직접 전달 — shellQuote 불필요
|
|
860
966
|
// Windows 원격에서 쿼트가 리터럴 문자로 해석되어 경로 오류 발생
|
|
861
|
-
execFileSync("scp", [localPath, `${host}:${remotePath}`], {
|
|
967
|
+
execFileSync("scp", [localPath, `${host}:${remotePath}`], {
|
|
968
|
+
timeout: 15000,
|
|
969
|
+
stdio: "pipe",
|
|
970
|
+
});
|
|
862
971
|
}
|
|
863
972
|
|
|
864
973
|
function stageRemotePromptFiles(host, env, transferCandidates, stageId) {
|
|
@@ -896,14 +1005,17 @@ function listSessionNamesFromRawOutput(output) {
|
|
|
896
1005
|
}
|
|
897
1006
|
|
|
898
1007
|
function listSpawnSessions() {
|
|
899
|
-
const helperSessions = listPsmuxSessions().filter((name) =>
|
|
1008
|
+
const helperSessions = listPsmuxSessions().filter((name) =>
|
|
1009
|
+
name.startsWith("tfx-spawn-"),
|
|
1010
|
+
);
|
|
900
1011
|
if (helperSessions.length > 0) {
|
|
901
1012
|
return helperSessions;
|
|
902
1013
|
}
|
|
903
1014
|
|
|
904
1015
|
try {
|
|
905
|
-
return listSessionNamesFromRawOutput(psmuxExec(["list-sessions"]))
|
|
906
|
-
|
|
1016
|
+
return listSessionNamesFromRawOutput(psmuxExec(["list-sessions"])).filter(
|
|
1017
|
+
(name) => name.startsWith("tfx-spawn-"),
|
|
1018
|
+
);
|
|
907
1019
|
} catch {
|
|
908
1020
|
return [];
|
|
909
1021
|
}
|
|
@@ -912,9 +1024,23 @@ function listSpawnSessions() {
|
|
|
912
1024
|
function openAttachTab(sessionName, title = null) {
|
|
913
1025
|
if (IS_WINDOWS_LOCAL) {
|
|
914
1026
|
const wtArgs = title
|
|
915
|
-
? [
|
|
1027
|
+
? [
|
|
1028
|
+
"new-tab",
|
|
1029
|
+
"--title",
|
|
1030
|
+
title,
|
|
1031
|
+
"--suppressApplicationTitle",
|
|
1032
|
+
"--",
|
|
1033
|
+
"psmux",
|
|
1034
|
+
"attach",
|
|
1035
|
+
"-t",
|
|
1036
|
+
sessionName,
|
|
1037
|
+
]
|
|
916
1038
|
: ["new-tab", "--", "psmux", "attach", "-t", sessionName];
|
|
917
|
-
spawn("wt.exe", wtArgs, {
|
|
1039
|
+
spawn("wt.exe", wtArgs, {
|
|
1040
|
+
detached: true,
|
|
1041
|
+
stdio: "ignore",
|
|
1042
|
+
windowsHide: false,
|
|
1043
|
+
}).unref();
|
|
918
1044
|
return;
|
|
919
1045
|
}
|
|
920
1046
|
|
|
@@ -951,14 +1077,25 @@ async function waitForRemotePrompt(sessionName, paneId) {
|
|
|
951
1077
|
}
|
|
952
1078
|
}
|
|
953
1079
|
|
|
954
|
-
throw new Error(
|
|
1080
|
+
throw new Error(
|
|
1081
|
+
`ssh prompt wait timed out for ${sessionName}: ${capturePsmuxPane(paneId, 20)}`,
|
|
1082
|
+
);
|
|
955
1083
|
}
|
|
956
1084
|
|
|
957
1085
|
/** @returns {boolean|null} true=dead, false=alive, null=probe 실패 */
|
|
958
1086
|
function isPrimaryPaneDead(paneId) {
|
|
959
1087
|
try {
|
|
960
|
-
const output = psmuxExec([
|
|
961
|
-
|
|
1088
|
+
const output = psmuxExec([
|
|
1089
|
+
"list-panes",
|
|
1090
|
+
"-t",
|
|
1091
|
+
paneId,
|
|
1092
|
+
"-F",
|
|
1093
|
+
"#{pane_dead}",
|
|
1094
|
+
]);
|
|
1095
|
+
const lines = output
|
|
1096
|
+
.split(/\r?\n/u)
|
|
1097
|
+
.map((line) => line.trim())
|
|
1098
|
+
.filter(Boolean);
|
|
962
1099
|
if (lines.some((line) => line === "1")) return true;
|
|
963
1100
|
return false;
|
|
964
1101
|
} catch {
|
|
@@ -968,20 +1105,36 @@ function isPrimaryPaneDead(paneId) {
|
|
|
968
1105
|
|
|
969
1106
|
async function watchSpawnSessionExit(sessionName, options = {}) {
|
|
970
1107
|
const paneId = options.paneId || `${sessionName}:0.0`;
|
|
971
|
-
const pollMs = parsePositiveInt(
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1108
|
+
const pollMs = parsePositiveInt(
|
|
1109
|
+
options.pollMs,
|
|
1110
|
+
DEFAULT_CLEANUP_WATCH_POLL_MS,
|
|
1111
|
+
);
|
|
1112
|
+
const graceMs = parsePositiveInt(
|
|
1113
|
+
options.graceMs,
|
|
1114
|
+
DEFAULT_CLEANUP_WATCH_GRACE_MS,
|
|
1115
|
+
);
|
|
1116
|
+
const maxWaitMs = parsePositiveInt(
|
|
1117
|
+
options.maxWaitMs,
|
|
1118
|
+
DEFAULT_CLEANUP_WATCH_MAX_MS,
|
|
1119
|
+
);
|
|
1120
|
+
const sessionExists =
|
|
1121
|
+
typeof options.sessionExists === "function"
|
|
1122
|
+
? options.sessionExists
|
|
1123
|
+
: psmuxSessionExists;
|
|
1124
|
+
const getPaneStatus =
|
|
1125
|
+
typeof options.getPaneStatus === "function"
|
|
1126
|
+
? options.getPaneStatus
|
|
1127
|
+
: (targetPaneId) => ({
|
|
1128
|
+
isDead: isPrimaryPaneDead(targetPaneId),
|
|
1129
|
+
exitCode: null,
|
|
1130
|
+
});
|
|
1131
|
+
const killSession =
|
|
1132
|
+
typeof options.killSession === "function"
|
|
1133
|
+
? options.killSession
|
|
1134
|
+
: killPsmuxSession;
|
|
983
1135
|
const now = typeof options.now === "function" ? options.now : Date.now;
|
|
984
|
-
const sleep =
|
|
1136
|
+
const sleep =
|
|
1137
|
+
typeof options.sleep === "function" ? options.sleep : sleepMsAsync;
|
|
985
1138
|
const startedAt = now();
|
|
986
1139
|
let consecutiveErrors = 0;
|
|
987
1140
|
|
|
@@ -1045,7 +1198,8 @@ function startSpawnExitWatcher(sessionName, options = {}) {
|
|
|
1045
1198
|
pollMs: options.pollMs,
|
|
1046
1199
|
});
|
|
1047
1200
|
args[0] = options.scriptPath || args[0];
|
|
1048
|
-
const spawnFn =
|
|
1201
|
+
const spawnFn =
|
|
1202
|
+
typeof options.spawnFn === "function" ? options.spawnFn : spawn;
|
|
1049
1203
|
const child = spawnFn(options.execPath || process.execPath, args, {
|
|
1050
1204
|
detached: true,
|
|
1051
1205
|
stdio: "ignore",
|
|
@@ -1057,9 +1211,17 @@ function startSpawnExitWatcher(sessionName, options = {}) {
|
|
|
1057
1211
|
return true;
|
|
1058
1212
|
}
|
|
1059
1213
|
|
|
1060
|
-
function startSpawnSessionCleanupWatcher(
|
|
1214
|
+
function startSpawnSessionCleanupWatcher(
|
|
1215
|
+
sessionName,
|
|
1216
|
+
paneId,
|
|
1217
|
+
timingOptions = {},
|
|
1218
|
+
) {
|
|
1061
1219
|
try {
|
|
1062
|
-
startSpawnExitWatcher(sessionName, {
|
|
1220
|
+
startSpawnExitWatcher(sessionName, {
|
|
1221
|
+
...timingOptions,
|
|
1222
|
+
force: true,
|
|
1223
|
+
paneId,
|
|
1224
|
+
});
|
|
1063
1225
|
} catch {
|
|
1064
1226
|
// watcher 시작 실패는 spawn 자체 실패로 보지 않는다.
|
|
1065
1227
|
}
|
|
@@ -1096,7 +1258,9 @@ function spawnLocal(args, claudePath, prompt) {
|
|
|
1096
1258
|
// 1단계: 프롬프트를 Get-Content -Raw → claude -p (one-shot), 세션 ID 추출
|
|
1097
1259
|
// 2단계: --resume으로 인터랙티브 세션 이어붙이기
|
|
1098
1260
|
const tmpFileNorm = normalizeCommandPath(tmpFile);
|
|
1099
|
-
const flags = getPermissionFlag()
|
|
1261
|
+
const flags = getPermissionFlag()
|
|
1262
|
+
.map((f) => `'${escapePwshSingleQuoted(f)}'`)
|
|
1263
|
+
.join(", ");
|
|
1100
1264
|
const scriptContent = [
|
|
1101
1265
|
`$ErrorActionPreference = 'SilentlyContinue'`,
|
|
1102
1266
|
`$t = '${escapePwshSingleQuoted(tmpFileNorm)}'`,
|
|
@@ -1109,9 +1273,15 @@ function spawnLocal(args, claudePath, prompt) {
|
|
|
1109
1273
|
`$trifluxExit = if ($null -ne $LASTEXITCODE) { [int]$LASTEXITCODE } else { 0 }`,
|
|
1110
1274
|
`exit $trifluxExit`,
|
|
1111
1275
|
].join("\n");
|
|
1112
|
-
const scriptFile = join(
|
|
1276
|
+
const scriptFile = join(
|
|
1277
|
+
tmpdir(),
|
|
1278
|
+
`tfx-spawn-${randomUUID().slice(0, 8)}.ps1`,
|
|
1279
|
+
);
|
|
1113
1280
|
writeFileSync(scriptFile, scriptContent, { encoding: "utf8" });
|
|
1114
|
-
sendKeysToPane(
|
|
1281
|
+
sendKeysToPane(
|
|
1282
|
+
paneId,
|
|
1283
|
+
`pwsh -NoProfile -File '${escapePwshSingleQuoted(normalizeCommandPath(scriptFile))}'; ${buildPwshExitTail()}`,
|
|
1284
|
+
);
|
|
1115
1285
|
} else {
|
|
1116
1286
|
const command = buildLocalClaudeCommand(claudePathNorm, permissionFlags);
|
|
1117
1287
|
sendKeysToPane(paneId, command);
|
|
@@ -1121,7 +1291,9 @@ function spawnLocal(args, claudePath, prompt) {
|
|
|
1121
1291
|
openAttachTab(sessionName, `local:${slug}`);
|
|
1122
1292
|
console.log(sessionName);
|
|
1123
1293
|
} catch (err) {
|
|
1124
|
-
try {
|
|
1294
|
+
try {
|
|
1295
|
+
killPsmuxSession(sessionName);
|
|
1296
|
+
} catch {}
|
|
1125
1297
|
throw err;
|
|
1126
1298
|
}
|
|
1127
1299
|
}
|
|
@@ -1140,7 +1312,9 @@ async function spawnRemote(args, promptContext) {
|
|
|
1140
1312
|
|
|
1141
1313
|
const env = probeRemoteEnv(host);
|
|
1142
1314
|
if (!env.claudePath) {
|
|
1143
|
-
console.error(
|
|
1315
|
+
console.error(
|
|
1316
|
+
`claude not found on ${host}. Install Claude Code on the remote host first.`,
|
|
1317
|
+
);
|
|
1144
1318
|
process.exit(1);
|
|
1145
1319
|
}
|
|
1146
1320
|
const resolvedDir = resolveRemoteDir(args.dir, env);
|
|
@@ -1151,10 +1325,17 @@ async function spawnRemote(args, promptContext) {
|
|
|
1151
1325
|
let prompt = promptContext.prompt;
|
|
1152
1326
|
|
|
1153
1327
|
try {
|
|
1154
|
-
const { stagedFiles } = stageRemotePromptFiles(
|
|
1328
|
+
const { stagedFiles } = stageRemotePromptFiles(
|
|
1329
|
+
host,
|
|
1330
|
+
env,
|
|
1331
|
+
promptContext.transferCandidates,
|
|
1332
|
+
sessionName,
|
|
1333
|
+
);
|
|
1155
1334
|
prompt = rewritePromptPaths(prompt, stagedFiles);
|
|
1156
1335
|
} catch (error) {
|
|
1157
|
-
failFast(
|
|
1336
|
+
failFast(
|
|
1337
|
+
`failed to stage remote files: ${error?.message || String(error)}`,
|
|
1338
|
+
);
|
|
1158
1339
|
}
|
|
1159
1340
|
|
|
1160
1341
|
createPsmuxSession(sessionName, { layout: "1xN", paneCount: 1 });
|
|
@@ -1174,11 +1355,18 @@ async function spawnRemote(args, promptContext) {
|
|
|
1174
1355
|
const stageDir = resolveRemoteStageDir(env, sessionName);
|
|
1175
1356
|
ensureRemoteStageDir(host, env, stageDir);
|
|
1176
1357
|
|
|
1177
|
-
const localPromptFile = join(
|
|
1358
|
+
const localPromptFile = join(
|
|
1359
|
+
tmpdir(),
|
|
1360
|
+
`tfx-prompt-${randomUUID().slice(0, 8)}.md`,
|
|
1361
|
+
);
|
|
1178
1362
|
writeFileSync(localPromptFile, prompt, { encoding: "utf8" });
|
|
1179
1363
|
const remotePromptFile = `${stageDir}/prompt.md`;
|
|
1180
1364
|
uploadFileToRemote(host, localPromptFile, remotePromptFile);
|
|
1181
|
-
try {
|
|
1365
|
+
try {
|
|
1366
|
+
unlinkSync(localPromptFile);
|
|
1367
|
+
} catch {
|
|
1368
|
+
/* cleanup best-effort */
|
|
1369
|
+
}
|
|
1182
1370
|
|
|
1183
1371
|
if (env.shell === "pwsh") {
|
|
1184
1372
|
const remotePromptWin = remotePromptFile.replace(/\//g, "\\");
|
|
@@ -1193,16 +1381,29 @@ async function spawnRemote(args, promptContext) {
|
|
|
1193
1381
|
`$trifluxExit = if ($null -ne $LASTEXITCODE) { [int]$LASTEXITCODE } else { 0 }`,
|
|
1194
1382
|
`exit $trifluxExit`,
|
|
1195
1383
|
].join("\n");
|
|
1196
|
-
const localScript = join(
|
|
1384
|
+
const localScript = join(
|
|
1385
|
+
tmpdir(),
|
|
1386
|
+
`tfx-spawn-${randomUUID().slice(0, 8)}.ps1`,
|
|
1387
|
+
);
|
|
1197
1388
|
writeFileSync(localScript, scriptContent, { encoding: "utf8" });
|
|
1198
1389
|
const remoteScript = `${stageDir}/launch.ps1`;
|
|
1199
1390
|
uploadFileToRemote(host, localScript, remoteScript);
|
|
1200
|
-
try {
|
|
1391
|
+
try {
|
|
1392
|
+
unlinkSync(localScript);
|
|
1393
|
+
} catch {
|
|
1394
|
+
/* cleanup best-effort */
|
|
1395
|
+
}
|
|
1201
1396
|
|
|
1202
1397
|
const remoteScriptWin = remoteScript.replace(/\//g, "\\");
|
|
1203
|
-
sendKeysToPane(
|
|
1398
|
+
sendKeysToPane(
|
|
1399
|
+
paneId,
|
|
1400
|
+
`pwsh -NoProfile -File '${escapePwshSingleQuoted(remoteScriptWin)}'`,
|
|
1401
|
+
);
|
|
1204
1402
|
} else {
|
|
1205
|
-
sendKeysToPane(
|
|
1403
|
+
sendKeysToPane(
|
|
1404
|
+
paneId,
|
|
1405
|
+
`${shellQuote(env.claudePath)} ${permissionFlags} < ${shellQuote(remotePromptFile)} && rm -f ${shellQuote(remotePromptFile)}; ${buildPosixExitTail()}`,
|
|
1406
|
+
);
|
|
1206
1407
|
}
|
|
1207
1408
|
} else {
|
|
1208
1409
|
const claudeCommand = buildRemoteClaudeCommand(env, permissionFlags);
|
|
@@ -1213,7 +1414,9 @@ async function spawnRemote(args, promptContext) {
|
|
|
1213
1414
|
openAttachTab(sessionName, `${host}:${slug}`);
|
|
1214
1415
|
console.log(sessionName);
|
|
1215
1416
|
} catch (err) {
|
|
1216
|
-
try {
|
|
1417
|
+
try {
|
|
1418
|
+
killPsmuxSession(sessionName);
|
|
1419
|
+
} catch {}
|
|
1217
1420
|
throw err;
|
|
1218
1421
|
}
|
|
1219
1422
|
}
|
|
@@ -1253,13 +1456,19 @@ async function waitForClaudeReady(sessionName, timeoutSec = 60) {
|
|
|
1253
1456
|
|
|
1254
1457
|
while (Date.now() <= deadline) {
|
|
1255
1458
|
const snapshot = capturePsmuxPane(paneId, 5);
|
|
1256
|
-
const lastLine =
|
|
1459
|
+
const lastLine =
|
|
1460
|
+
snapshot
|
|
1461
|
+
.split(/\r?\n/)
|
|
1462
|
+
.filter((l) => l.trim())
|
|
1463
|
+
.at(-1) || "";
|
|
1257
1464
|
if (readyPattern.test(lastLine)) {
|
|
1258
1465
|
return true;
|
|
1259
1466
|
}
|
|
1260
1467
|
sleepMs(1000);
|
|
1261
1468
|
}
|
|
1262
|
-
throw new Error(
|
|
1469
|
+
throw new Error(
|
|
1470
|
+
`claude ready wait timed out after ${timeoutSec}s for ${sessionName}`,
|
|
1471
|
+
);
|
|
1263
1472
|
}
|
|
1264
1473
|
|
|
1265
1474
|
async function main() {
|
|
@@ -1299,7 +1508,9 @@ async function main() {
|
|
|
1299
1508
|
console.error("--probe requires a host");
|
|
1300
1509
|
process.exit(1);
|
|
1301
1510
|
}
|
|
1302
|
-
console.log(
|
|
1511
|
+
console.log(
|
|
1512
|
+
JSON.stringify(probeRemoteEnv(args.probeHost, { force: true }), null, 2),
|
|
1513
|
+
);
|
|
1303
1514
|
return;
|
|
1304
1515
|
}
|
|
1305
1516
|
|
|
@@ -1328,7 +1539,9 @@ async function main() {
|
|
|
1328
1539
|
const prompt = promptContext.prompt;
|
|
1329
1540
|
|
|
1330
1541
|
if (args.local && args.transferFiles.length > 0) {
|
|
1331
|
-
console.warn(
|
|
1542
|
+
console.warn(
|
|
1543
|
+
"[tfx] --transfer는 원격 모드에서만 사용 가능합니다 (--local에서는 무시됨)",
|
|
1544
|
+
);
|
|
1332
1545
|
}
|
|
1333
1546
|
|
|
1334
1547
|
if (args.command === "send") {
|
|
@@ -1357,7 +1570,9 @@ async function main() {
|
|
|
1357
1570
|
await spawnRemote(args, promptContext);
|
|
1358
1571
|
}
|
|
1359
1572
|
|
|
1360
|
-
const selfRun =
|
|
1573
|
+
const selfRun =
|
|
1574
|
+
process.argv[1] &&
|
|
1575
|
+
fileURLToPath(import.meta.url) === resolve(process.argv[1]);
|
|
1361
1576
|
if (selfRun) {
|
|
1362
1577
|
main().catch((error) => {
|
|
1363
1578
|
console.error(error?.message || String(error));
|