triflux 10.0.0 → 10.0.2
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/CLAUDE.md +171 -0
- package/README.md +32 -15
- package/bin/triflux.mjs +62 -5
- package/hooks/agent-route-guard.mjs +109 -0
- package/hooks/cross-review-tracker.mjs +122 -0
- package/hooks/error-context.mjs +148 -0
- package/hooks/hook-adaptive-collector.mjs +86 -0
- package/hooks/hook-manager.mjs +365 -0
- package/hooks/hook-orchestrator.mjs +312 -0
- package/hooks/hook-registry.json +246 -0
- package/hooks/hooks.json +89 -0
- package/hooks/keyword-rules.json +574 -0
- package/hooks/lib/resolve-root.mjs +59 -0
- package/hooks/mcp-config-watcher.mjs +80 -0
- package/hooks/pipeline-stop.mjs +76 -0
- package/hooks/safety-guard.mjs +169 -0
- package/hooks/subagent-verifier.mjs +80 -0
- package/hub/account-broker.mjs +251 -0
- package/hub/adaptive-diagnostic.mjs +323 -0
- package/hub/adaptive-inject.mjs +186 -0
- package/hub/adaptive-memory.mjs +163 -0
- package/hub/adaptive.mjs +143 -0
- package/hub/assign-callbacks.mjs +133 -0
- package/hub/bridge.mjs +799 -0
- package/hub/cli-adapter-base.mjs +280 -0
- package/hub/codex-adapter.mjs +199 -0
- package/hub/codex-compat.mjs +11 -0
- package/hub/codex-preflight.mjs +166 -0
- package/hub/delegator/contracts.mjs +37 -0
- package/hub/delegator/index.mjs +14 -0
- package/hub/delegator/schema/delegator-tools.schema.json +250 -0
- package/hub/delegator/service.mjs +307 -0
- package/hub/delegator/tool-definitions.mjs +35 -0
- package/hub/fullcycle.mjs +96 -0
- package/hub/gemini-adapter.mjs +180 -0
- package/hub/hitl.mjs +143 -0
- package/hub/intent.mjs +193 -0
- package/hub/lib/cache-guard.mjs +114 -0
- package/hub/lib/known-errors.json +72 -0
- package/hub/lib/memory-store.mjs +748 -0
- package/hub/lib/process-utils.mjs +361 -0
- package/hub/lib/ssh-command.mjs +211 -0
- package/hub/lib/ssh-retry.mjs +59 -0
- package/hub/lib/uuidv7.mjs +44 -0
- package/hub/memory-doctor.mjs +480 -0
- package/hub/middleware/request-logger.mjs +161 -0
- package/hub/paths.mjs +30 -0
- package/hub/pipe.mjs +664 -0
- package/hub/pipeline/gates/confidence.mjs +56 -0
- package/hub/pipeline/gates/consensus.mjs +94 -0
- package/hub/pipeline/gates/index.mjs +5 -0
- package/hub/pipeline/gates/selfcheck.mjs +82 -0
- package/hub/pipeline/index.mjs +318 -0
- package/hub/pipeline/state.mjs +191 -0
- package/hub/pipeline/transitions.mjs +124 -0
- package/hub/platform.mjs +225 -0
- package/hub/public/dashboard.html +355 -0
- package/hub/public/tray-icon.ico +0 -0
- package/hub/public/tray-icon.png +0 -0
- package/hub/quality/deslop.mjs +253 -0
- package/hub/reflexion.mjs +372 -0
- package/hub/research.mjs +146 -0
- package/hub/router.mjs +791 -0
- package/hub/routing/complexity.mjs +166 -0
- package/hub/routing/index.mjs +117 -0
- package/hub/routing/q-learning.mjs +336 -0
- package/hub/schema.sql +148 -0
- package/hub/server.mjs +1264 -0
- package/hub/session-fingerprint.mjs +352 -0
- package/hub/state.mjs +258 -0
- package/hub/store-adapter.mjs +118 -0
- package/hub/store.mjs +857 -0
- package/hub/team/agent-map.json +11 -0
- package/hub/team/ansi.mjs +379 -0
- package/hub/team/backend.mjs +90 -0
- package/hub/team/cli/commands/attach.mjs +37 -0
- package/hub/team/cli/commands/control.mjs +43 -0
- package/hub/team/cli/commands/debug.mjs +74 -0
- package/hub/team/cli/commands/focus.mjs +53 -0
- package/hub/team/cli/commands/interrupt.mjs +36 -0
- package/hub/team/cli/commands/kill.mjs +37 -0
- package/hub/team/cli/commands/list.mjs +24 -0
- package/hub/team/cli/commands/send.mjs +37 -0
- package/hub/team/cli/commands/start/index.mjs +106 -0
- package/hub/team/cli/commands/start/parse-args.mjs +130 -0
- package/hub/team/cli/commands/start/start-headless.mjs +109 -0
- package/hub/team/cli/commands/start/start-in-process.mjs +40 -0
- package/hub/team/cli/commands/start/start-mux.mjs +73 -0
- package/hub/team/cli/commands/start/start-wt.mjs +69 -0
- package/hub/team/cli/commands/status.mjs +87 -0
- package/hub/team/cli/commands/stop.mjs +31 -0
- package/hub/team/cli/commands/task.mjs +30 -0
- package/hub/team/cli/commands/tasks.mjs +13 -0
- package/hub/team/cli/help.mjs +42 -0
- package/hub/team/cli/index.mjs +41 -0
- package/hub/team/cli/manifest.mjs +29 -0
- package/hub/team/cli/render.mjs +30 -0
- package/hub/team/cli/services/attach-fallback.mjs +54 -0
- package/hub/team/cli/services/hub-client.mjs +227 -0
- package/hub/team/cli/services/member-selector.mjs +30 -0
- package/hub/team/cli/services/native-control.mjs +117 -0
- package/hub/team/cli/services/runtime-mode.mjs +62 -0
- package/hub/team/cli/services/state-store.mjs +48 -0
- package/hub/team/cli/services/task-model.mjs +30 -0
- package/hub/team/conductor-mesh-bridge.mjs +121 -0
- package/hub/team/conductor.mjs +671 -0
- package/hub/team/dashboard-anchor.mjs +14 -0
- package/hub/team/dashboard-layout.mjs +33 -0
- package/hub/team/dashboard-open.mjs +153 -0
- package/hub/team/dashboard.mjs +274 -0
- package/hub/team/event-log.mjs +76 -0
- package/hub/team/handoff.mjs +303 -0
- package/hub/team/headless.mjs +1156 -0
- package/hub/team/health-probe.mjs +272 -0
- package/hub/team/launcher-template.mjs +95 -0
- package/hub/team/lead-control.mjs +104 -0
- package/hub/team/native-supervisor.mjs +392 -0
- package/hub/team/native.mjs +649 -0
- package/hub/team/nativeProxy.mjs +688 -0
- package/hub/team/notify.mjs +293 -0
- package/hub/team/orchestrator.mjs +161 -0
- package/hub/team/pane.mjs +153 -0
- package/hub/team/process-cleanup.mjs +342 -0
- package/hub/team/psmux.mjs +1354 -0
- package/hub/team/remote-probe.mjs +276 -0
- package/hub/team/remote-session.mjs +299 -0
- package/hub/team/remote-watcher.mjs +478 -0
- package/hub/team/routing.mjs +223 -0
- package/hub/team/session-sync.mjs +169 -0
- package/hub/team/session.mjs +611 -0
- package/hub/team/shared.mjs +13 -0
- package/hub/team/staleState.mjs +361 -0
- package/hub/team/swarm-hypervisor.mjs +589 -0
- package/hub/team/swarm-locks.mjs +204 -0
- package/hub/team/swarm-planner.mjs +260 -0
- package/hub/team/swarm-reconciler.mjs +137 -0
- package/hub/team/tui-lite.mjs +380 -0
- package/hub/team/tui-remote-adapter.mjs +393 -0
- package/hub/team/tui-viewer.mjs +463 -0
- package/hub/team/tui.mjs +1449 -0
- package/hub/team/worktree-lifecycle.mjs +193 -0
- package/hub/team/wt-manager.mjs +407 -0
- package/hub/team/wt-templates.json +43 -0
- package/hub/team-bridge.mjs +27 -0
- package/hub/token-mode.mjs +224 -0
- package/hub/tools.mjs +636 -0
- package/hub/tray.mjs +376 -0
- package/hub/workers/claude-worker.mjs +475 -0
- package/hub/workers/codex-mcp.mjs +507 -0
- package/hub/workers/delegator-mcp.mjs +1076 -0
- package/hub/workers/factory.mjs +21 -0
- package/hub/workers/gemini-worker.mjs +374 -0
- package/hub/workers/interface.mjs +52 -0
- package/hub/workers/worker-utils.mjs +104 -0
- package/hud/colors.mjs +88 -0
- package/hud/constants.mjs +88 -0
- package/hud/context-monitor.mjs +403 -0
- package/hud/hud-qos-status.mjs +210 -0
- package/hud/providers/claude.mjs +314 -0
- package/hud/providers/codex.mjs +151 -0
- package/hud/providers/gemini.mjs +320 -0
- package/hud/renderers.mjs +442 -0
- package/hud/terminal.mjs +140 -0
- package/hud/utils.mjs +313 -0
- package/mesh/index.mjs +63 -0
- package/mesh/mesh-budget.mjs +128 -0
- package/mesh/mesh-heartbeat.mjs +100 -0
- package/mesh/mesh-protocol.mjs +96 -0
- package/mesh/mesh-queue.mjs +165 -0
- package/mesh/mesh-registry.mjs +78 -0
- package/mesh/mesh-router.mjs +76 -0
- package/package.json +8 -1
- package/references/hosts.json +33 -0
- package/scripts/__tests__/gen-skill-docs.test.mjs +87 -0
- package/scripts/__tests__/keyword-detector.test.mjs +234 -0
- package/scripts/__tests__/mcp-guard-engine.test.mjs +118 -0
- package/scripts/__tests__/remote-spawn-transfer.test.mjs +117 -0
- package/scripts/__tests__/remote-spawn.test.mjs +92 -0
- package/scripts/__tests__/skill-template.test.mjs +193 -0
- package/scripts/__tests__/smoke.test.mjs +34 -0
- package/scripts/cache-buildup.mjs +30 -0
- package/scripts/cache-doctor.mjs +149 -0
- package/scripts/cache-warmup.mjs +557 -0
- package/scripts/claudemd-sync.mjs +148 -0
- package/scripts/cli-route.sh +3 -0
- package/scripts/completions/tfx.bash +47 -0
- package/scripts/completions/tfx.fish +44 -0
- package/scripts/completions/tfx.zsh +83 -0
- package/scripts/cross-review-gate.mjs +126 -0
- package/scripts/cross-review-tracker.mjs +238 -0
- package/scripts/gen-skill-docs.mjs +111 -0
- package/scripts/headless-guard-fast.sh +21 -0
- package/scripts/headless-guard.mjs +360 -0
- package/scripts/hub-ensure.mjs +120 -0
- package/scripts/keyword-detector.mjs +272 -0
- package/scripts/keyword-rules-expander.mjs +521 -0
- package/scripts/lib/claudemd-scanner.mjs +218 -0
- package/scripts/lib/context.mjs +67 -0
- package/scripts/lib/cross-review-utils.mjs +51 -0
- package/scripts/lib/env-probe.mjs +241 -0
- package/scripts/lib/gemini-profiles.mjs +85 -0
- package/scripts/lib/handoff.mjs +171 -0
- package/scripts/lib/hook-utils.mjs +14 -0
- package/scripts/lib/keyword-rules.mjs +166 -0
- package/scripts/lib/logger.mjs +105 -0
- package/scripts/lib/mcp-filter.mjs +739 -0
- package/scripts/lib/mcp-guard-engine.mjs +954 -0
- package/scripts/lib/mcp-manifest.mjs +79 -0
- package/scripts/lib/mcp-server-catalog.mjs +118 -0
- package/scripts/lib/psmux-info.mjs +119 -0
- package/scripts/lib/remote-spawn-transfer.mjs +196 -0
- package/scripts/lib/skill-template.mjs +326 -0
- package/scripts/mcp-check.mjs +237 -0
- package/scripts/mcp-cleanup.ps1 +17 -0
- package/scripts/mcp-gateway-config.mjs +207 -0
- package/scripts/mcp-gateway-ensure.mjs +85 -0
- package/scripts/mcp-gateway-integration-test.mjs +228 -0
- package/scripts/mcp-gateway-start.mjs +226 -0
- package/scripts/mcp-gateway-start.ps1 +141 -0
- package/scripts/mcp-gateway-verify.mjs +77 -0
- package/scripts/mcp-safety-guard.mjs +44 -0
- package/scripts/notion-read.mjs +556 -0
- package/scripts/pack.mjs +295 -0
- package/scripts/preflight-cache.mjs +69 -0
- package/scripts/preinstall.mjs +96 -0
- package/scripts/remote-spawn.mjs +1376 -0
- package/scripts/run.cjs +79 -0
- package/scripts/session-spawn-helper.mjs +185 -0
- package/scripts/setup.mjs +1178 -0
- package/scripts/test-lock.mjs +71 -0
- package/scripts/test-tfx-route-no-claude-native.mjs +57 -0
- package/scripts/tfx-batch-stats.mjs +96 -0
- package/scripts/tfx-gate-activate.mjs +89 -0
- package/scripts/tfx-route-post.mjs +505 -0
- package/scripts/tfx-route-worker.mjs +223 -0
- package/scripts/tfx-route.sh +2014 -0
- package/scripts/tmp-cleanup.mjs +103 -0
- package/scripts/token-snapshot.mjs +575 -0
- package/skills/tfx-auto/SKILL.md.tmpl +2 -3
- package/skills/tfx-autoresearch/SKILL.md +6 -5
- package/skills/tfx-codex/SKILL.md.tmpl +2 -3
- package/skills/tfx-codex-swarm-workspace/iteration-1/benchmark.json +33 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/eval_metadata.json +42 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/grading.json +11 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/outputs/analysis.md +87 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/outputs/classification.md +35 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/outputs/commands.sh +275 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/outputs/routing.md +56 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/timing.json +5 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/grading.json +11 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/outputs/analysis.md +92 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/outputs/classification.md +71 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/outputs/commands.sh +264 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/outputs/routing.md +113 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/timing.json +5 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/eval_metadata.json +32 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/grading.json +9 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/outputs/analysis.md +96 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/outputs/classification.md +38 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/outputs/commands.sh +151 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/outputs/routing.md +51 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/timing.json +5 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/grading.json +9 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/outputs/analysis.md +127 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/outputs/classification.md +57 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/outputs/commands.sh +129 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/outputs/routing.md +84 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/timing.json +5 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/eval_metadata.json +27 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/grading.json +8 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/outputs/analysis.md +98 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/outputs/classification.md +65 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/outputs/commands.sh +123 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/outputs/routing.md +66 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/timing.json +5 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/grading.json +8 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/outputs/analysis.md +88 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/outputs/classification.md +40 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/outputs/commands.sh +130 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/outputs/routing.md +61 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/timing.json +5 -0
- package/skills/tfx-deep-interview/SKILL.md +1 -2
- package/skills/tfx-plan/SKILL.md.tmpl +2 -3
- package/skills/tfx-psmux-rules/SKILL.md +11 -2
- package/skills/tfx-qa/SKILL.md.tmpl +2 -3
- package/skills/tfx-remote-spawn/SKILL.md +8 -11
- package/skills/tfx-research/SKILL.md.tmpl +2 -3
- package/skills/tfx-review/SKILL.md.tmpl +2 -3
- package/skills/tfx-workspace/async-tests/run-tests.sh +203 -0
- package/skills/tfx-workspace/evals/evals.json +79 -0
- package/skills/tfx-workspace/iteration-1/benchmark.json +162 -0
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/eval_metadata.json +11 -0
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/grading.json +9 -0
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/outputs/analysis.md +154 -0
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/grading.json +9 -0
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/outputs/analysis.md +126 -0
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/eval_metadata.json +11 -0
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/grading.json +9 -0
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/outputs/analysis.md +119 -0
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/grading.json +9 -0
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/outputs/analysis.md +115 -0
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/eval_metadata.json +10 -0
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/grading.json +8 -0
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/outputs/analysis.md +86 -0
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/grading.json +8 -0
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/outputs/analysis.md +81 -0
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/multi-team-creation/eval_metadata.json +12 -0
- package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/grading.json +10 -0
- package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/outputs/analysis.md +316 -0
- package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/grading.json +10 -0
- package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/outputs/analysis.md +352 -0
- package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/review.html +1325 -0
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/eval_metadata.json +12 -0
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/grading.json +10 -0
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/outputs/analysis.md +97 -0
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/grading.json +10 -0
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/outputs/analysis.md +94 -0
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/eval_metadata.json +12 -0
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/grading.json +10 -0
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/outputs/analysis.md +209 -0
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/grading.json +10 -0
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/outputs/analysis.md +193 -0
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-2/benchmark.json +62 -0
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/eval_metadata.json +13 -0
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/grading.json +11 -0
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/outputs/analysis.md +382 -0
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/grading.json +11 -0
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/outputs/analysis.md +333 -0
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-2/review.html +1325 -0
- package/skills/tfx-workspace/skill-snapshot/tfx-auto/SKILL.md +217 -0
- package/skills/{tfx-auto-codex/SKILL.md.tmpl → tfx-workspace/skill-snapshot/tfx-auto-codex/SKILL.md} +3 -31
- package/skills/tfx-workspace/skill-snapshot/tfx-codex/SKILL.md +65 -0
- package/skills/tfx-workspace/skill-snapshot/tfx-doctor/SKILL.md +94 -0
- package/skills/{tfx-gemini/SKILL.md.tmpl → tfx-workspace/skill-snapshot/tfx-gemini/SKILL.md} +6 -14
- package/skills/tfx-workspace/skill-snapshot/tfx-hub/SKILL.md +133 -0
- package/skills/tfx-workspace/skill-snapshot/tfx-multi/SKILL.md +426 -0
- package/skills/tfx-workspace/skill-snapshot/tfx-setup/SKILL.md +101 -0
- package/skills/merge-worktree/SKILL.md.tmpl +0 -144
- package/skills/shared/arguments-processing.md +0 -2
- package/skills/shared/mandatory-rules.md +0 -6
- package/skills/shared/telemetry-segment.md +0 -6
- package/skills/star-prompt/SKILL.md.tmpl +0 -122
- package/skills/tfx-analysis/SKILL.md.tmpl +0 -106
- package/skills/tfx-analysis/skill.json +0 -11
- package/skills/tfx-auto/skill.json +0 -26
- package/skills/tfx-auto-codex/skill.json +0 -8
- package/skills/tfx-autopilot/SKILL.md.tmpl +0 -115
- package/skills/tfx-autopilot/skill.json +0 -10
- package/skills/tfx-autoresearch/SKILL.md.tmpl +0 -135
- package/skills/tfx-autoresearch/skill.json +0 -14
- package/skills/tfx-autoroute/SKILL.md.tmpl +0 -188
- package/skills/tfx-autoroute/skill.json +0 -12
- package/skills/tfx-codex/skill.json +0 -8
- package/skills/tfx-codex-swarm/SKILL.md.tmpl +0 -16
- package/skills/tfx-codex-swarm/skill.json +0 -5
- package/skills/tfx-consensus/SKILL.md.tmpl +0 -145
- package/skills/tfx-consensus/skill.json +0 -8
- package/skills/tfx-debate/SKILL.md.tmpl +0 -191
- package/skills/tfx-debate/skill.json +0 -12
- package/skills/tfx-deep-analysis/SKILL.md.tmpl +0 -227
- package/skills/tfx-deep-analysis/skill.json +0 -10
- package/skills/tfx-deep-interview/SKILL.md.tmpl +0 -203
- package/skills/tfx-deep-interview/skill.json +0 -12
- package/skills/tfx-deep-plan/SKILL.md.tmpl +0 -281
- package/skills/tfx-deep-plan/skill.json +0 -13
- package/skills/tfx-deep-qa/SKILL.md.tmpl +0 -164
- package/skills/tfx-deep-qa/skill.json +0 -11
- package/skills/tfx-deep-research/SKILL.md.tmpl +0 -216
- package/skills/tfx-deep-research/skill.json +0 -14
- package/skills/tfx-deep-review/SKILL.md.tmpl +0 -178
- package/skills/tfx-deep-review/skill.json +0 -12
- package/skills/tfx-doctor/SKILL.md.tmpl +0 -172
- package/skills/tfx-doctor/skill.json +0 -8
- package/skills/tfx-find/skill.json +0 -12
- package/skills/tfx-forge/SKILL.md.tmpl +0 -187
- package/skills/tfx-forge/skill.json +0 -12
- package/skills/tfx-fullcycle/SKILL.md.tmpl +0 -285
- package/skills/tfx-fullcycle/skill.json +0 -11
- package/skills/tfx-gemini/skill.json +0 -8
- package/skills/tfx-hooks/SKILL.md.tmpl +0 -216
- package/skills/tfx-hooks/skill.json +0 -8
- package/skills/tfx-hub/SKILL.md.tmpl +0 -212
- package/skills/tfx-hub/skill.json +0 -8
- package/skills/tfx-index/skill.json +0 -11
- package/skills/tfx-interview/SKILL.md.tmpl +0 -284
- package/skills/tfx-interview/skill.json +0 -12
- package/skills/tfx-multi/SKILL.md.tmpl +0 -183
- package/skills/tfx-multi/skill.json +0 -8
- package/skills/tfx-panel/SKILL.md.tmpl +0 -188
- package/skills/tfx-panel/skill.json +0 -12
- package/skills/tfx-persist/SKILL.md.tmpl +0 -269
- package/skills/tfx-persist/skill.json +0 -12
- package/skills/tfx-plan/skill.json +0 -11
- package/skills/tfx-profile/SKILL.md.tmpl +0 -239
- package/skills/tfx-profile/skill.json +0 -8
- package/skills/tfx-prune/SKILL.md.tmpl +0 -199
- package/skills/tfx-prune/skill.json +0 -12
- package/skills/tfx-psmux-rules/SKILL.md.tmpl +0 -317
- package/skills/tfx-psmux-rules/skill.json +0 -8
- package/skills/tfx-qa/skill.json +0 -11
- package/skills/tfx-ralph/SKILL.md.tmpl +0 -27
- package/skills/tfx-ralph/skill.json +0 -8
- package/skills/tfx-remote-setup/SKILL.md.tmpl +0 -576
- package/skills/tfx-remote-setup/skill.json +0 -8
- package/skills/tfx-remote-spawn/SKILL.md.tmpl +0 -263
- package/skills/tfx-remote-spawn/skill.json +0 -9
- package/skills/tfx-research/skill.json +0 -13
- package/skills/tfx-review/skill.json +0 -11
- package/skills/tfx-setup/SKILL.md.tmpl +0 -380
- package/skills/tfx-setup/skill.json +0 -8
- package/skills/tfx-swarm/SKILL.md.tmpl +0 -154
- package/skills/tfx-swarm/skill.json +0 -5
|
@@ -0,0 +1,1376 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// remote-spawn.mjs — 로컬/원격 Claude 세션 실행 유틸리티
|
|
3
|
+
//
|
|
4
|
+
// Usage:
|
|
5
|
+
// node remote-spawn.mjs --local [--dir <path>] [--prompt "..."] [--handoff <file>] [--transfer <file>]
|
|
6
|
+
// node remote-spawn.mjs --host <ssh-host> [--dir <path>] [--prompt "..."] [--handoff <file>] [--transfer <file>]
|
|
7
|
+
// node remote-spawn.mjs --send <session> "prompt"
|
|
8
|
+
// node remote-spawn.mjs --list
|
|
9
|
+
// node remote-spawn.mjs --attach <session>
|
|
10
|
+
// node remote-spawn.mjs --probe <ssh-host>
|
|
11
|
+
|
|
12
|
+
import { randomUUID } from "crypto";
|
|
13
|
+
import { execFileSync, execSync, spawn } from "child_process";
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "fs";
|
|
15
|
+
import { homedir, platform as getPlatform, tmpdir } from "os";
|
|
16
|
+
import { basename, join, posix as posixPath, resolve, win32 as win32Path } from "path";
|
|
17
|
+
import { fileURLToPath } from "url";
|
|
18
|
+
import {
|
|
19
|
+
attachPsmuxSession,
|
|
20
|
+
capturePsmuxPane,
|
|
21
|
+
createPsmuxSession,
|
|
22
|
+
hasPsmux,
|
|
23
|
+
killPsmuxSession,
|
|
24
|
+
listPsmuxSessions,
|
|
25
|
+
psmuxExec,
|
|
26
|
+
psmuxSessionExists,
|
|
27
|
+
sendKeysToPane,
|
|
28
|
+
startCapture,
|
|
29
|
+
waitForPattern,
|
|
30
|
+
} from "../hub/team/psmux.mjs";
|
|
31
|
+
|
|
32
|
+
const MAX_HANDOFF_BYTES = 1 * 1024 * 1024; // 1 MB
|
|
33
|
+
const REMOTE_ENV_TTL_MS = 86_400_000;
|
|
34
|
+
const REMOTE_ENV_CACHE_DIR = resolve(".omc", "state", "remote-env");
|
|
35
|
+
const REMOTE_STAGE_ROOT = "tfx-remote";
|
|
36
|
+
const SSH_PROMPT_PATTERN = /(\$|%|#|PS |>)\s*$/;
|
|
37
|
+
const IS_WINDOWS_LOCAL = getPlatform() === "win32";
|
|
38
|
+
const SELF_SCRIPT_PATH = fileURLToPath(import.meta.url);
|
|
39
|
+
|
|
40
|
+
const DEFAULT_CLEANUP_WATCH_POLL_MS = 1000;
|
|
41
|
+
const DEFAULT_CLEANUP_WATCH_GRACE_MS = 1500;
|
|
42
|
+
const DEFAULT_CLEANUP_WATCH_MAX_MS = 60 * 60 * 1000;
|
|
43
|
+
|
|
44
|
+
const SAFE_HOST_RE = /^[a-zA-Z0-9._-]+$/;
|
|
45
|
+
const SAFE_DIR_RE = /^[a-zA-Z0-9_.~\/:\\-]+$/;
|
|
46
|
+
|
|
47
|
+
function validateHost(host) {
|
|
48
|
+
if (!SAFE_HOST_RE.test(host)) {
|
|
49
|
+
console.error(`invalid host name: ${host}`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
return host;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function validateDir(dir) {
|
|
56
|
+
if (!SAFE_DIR_RE.test(dir)) {
|
|
57
|
+
console.error(`invalid directory path: ${dir}`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
return dir;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function shellQuote(value) {
|
|
64
|
+
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function escapePwshSingleQuoted(value) {
|
|
68
|
+
return String(value).replace(/'/g, "''");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function escapePwshDoubleQuoted(value) {
|
|
72
|
+
return String(value).replace(/`/g, "``").replace(/"/g, '`"');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function normalizeCommandPath(value) {
|
|
76
|
+
return String(value).replace(/\\/g, "/");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function sleepMs(ms) {
|
|
80
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.max(0, ms));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function sleepMsAsync(ms) {
|
|
84
|
+
return new Promise((resolveSleep) => setTimeout(resolveSleep, Math.max(0, ms)));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parsePositiveInt(value, fallback) {
|
|
88
|
+
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
89
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function buildPwshExitTail() {
|
|
93
|
+
return "$trifluxExit = if ($null -ne $LASTEXITCODE) { [int]$LASTEXITCODE } else { 0 }; exit $trifluxExit";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function buildPosixExitTail() {
|
|
97
|
+
return "exit $?";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function buildRemoteBootstrapCommand(host) {
|
|
101
|
+
return `ssh -t ${host}; ${buildPwshExitTail()}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function buildLocalClaudeCommand(claudePathNorm, permissionFlags = "") {
|
|
105
|
+
// try/finally: Ctrl+C로 Claude 종료해도 shell이 자동 exit
|
|
106
|
+
return `try { & '${escapePwshSingleQuoted(claudePathNorm)}'${permissionFlags ? ` ${permissionFlags}` : ""} } finally { exit }`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function buildRemoteClaudeCommand(env, permissionFlags = "") {
|
|
110
|
+
if (env.shell === "pwsh") {
|
|
111
|
+
return `try { & "${escapePwshDoubleQuoted(env.claudePath)}"${permissionFlags ? ` ${permissionFlags}` : ""} } finally { exit }`;
|
|
112
|
+
}
|
|
113
|
+
return `${shellQuote(env.claudePath)}${permissionFlags ? ` ${permissionFlags}` : ""}; ${buildPosixExitTail()}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function resolveCleanupWatcherTimingOptions(source = {}, env = process.env) {
|
|
117
|
+
return Object.freeze({
|
|
118
|
+
graceMs: parsePositiveInt(source.graceMs ?? env.TFX_SPAWN_CLEANUP_GRACE_MS, DEFAULT_CLEANUP_WATCH_GRACE_MS),
|
|
119
|
+
maxMs: parsePositiveInt(source.maxMs ?? env.TFX_SPAWN_CLEANUP_MAX_MS, DEFAULT_CLEANUP_WATCH_MAX_MS),
|
|
120
|
+
pollMs: parsePositiveInt(source.pollMs ?? env.TFX_SPAWN_CLEANUP_POLL_MS, DEFAULT_CLEANUP_WATCH_POLL_MS),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function buildSpawnCleanupWatcherArgs(sessionName, paneId, timingOptions = {}) {
|
|
125
|
+
const timings = resolveCleanupWatcherTimingOptions(timingOptions);
|
|
126
|
+
return [
|
|
127
|
+
SELF_SCRIPT_PATH,
|
|
128
|
+
"--watch-cleanup",
|
|
129
|
+
sessionName,
|
|
130
|
+
"--pane",
|
|
131
|
+
paneId,
|
|
132
|
+
"--poll-ms",
|
|
133
|
+
String(timings.pollMs),
|
|
134
|
+
"--grace-ms",
|
|
135
|
+
String(timings.graceMs),
|
|
136
|
+
"--max-ms",
|
|
137
|
+
String(timings.maxMs),
|
|
138
|
+
];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** 문자열을 psmux 세션명에 안전한 slug로 변환 */
|
|
142
|
+
function toSlug(raw) {
|
|
143
|
+
return raw
|
|
144
|
+
.replace(/[^a-zA-Z0-9-]/g, "-")
|
|
145
|
+
.replace(/-+/g, "-")
|
|
146
|
+
.replace(/^-|-$/g, "")
|
|
147
|
+
.toLowerCase()
|
|
148
|
+
.slice(0, 24);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** 세션 이름용 slug 생성: --name > git branch > random UUID */
|
|
152
|
+
function buildSessionSlug(customName) {
|
|
153
|
+
if (customName) return toSlug(customName);
|
|
154
|
+
try {
|
|
155
|
+
const branch = execFileSync("git", ["branch", "--show-current"], {
|
|
156
|
+
encoding: "utf8",
|
|
157
|
+
timeout: 3000,
|
|
158
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
159
|
+
}).trim();
|
|
160
|
+
if (branch) {
|
|
161
|
+
// feature/swarm-hypervisor → swarm-hypervisor
|
|
162
|
+
const stripped = branch.includes("/") ? branch.split("/").slice(1).join("-") : branch;
|
|
163
|
+
const slug = toSlug(stripped);
|
|
164
|
+
if (slug) return slug;
|
|
165
|
+
}
|
|
166
|
+
} catch { /* git not available */ }
|
|
167
|
+
return randomUUID().slice(0, 8);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** 같은 slug의 세션이 이미 존재하면 -2, -3 ... 카운터 추가 */
|
|
171
|
+
function deduplicateSessionName(baseName) {
|
|
172
|
+
try {
|
|
173
|
+
const existing = listPsmuxSessions();
|
|
174
|
+
if (!existing.includes(baseName)) return baseName;
|
|
175
|
+
for (let i = 2; i <= 99; i++) {
|
|
176
|
+
const candidate = `${baseName}-${i}`;
|
|
177
|
+
if (!existing.includes(candidate)) return candidate;
|
|
178
|
+
}
|
|
179
|
+
} catch { /* psmux not available */ }
|
|
180
|
+
return `${baseName}-${randomUUID().slice(0, 4)}`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function usageText() {
|
|
184
|
+
return `Usage:
|
|
185
|
+
remote-spawn --local [--dir <path>] [--prompt "task"] [--name <slug>] [--handoff <file>] [--transfer <file>]
|
|
186
|
+
remote-spawn --host <ssh-host> [--dir <path>] [--prompt "task"] [--name <slug>] [--handoff <file>] [--transfer <file>]
|
|
187
|
+
remote-spawn --send <session> "prompt"
|
|
188
|
+
remote-spawn --list
|
|
189
|
+
remote-spawn --attach <session>
|
|
190
|
+
remote-spawn --probe <ssh-host>
|
|
191
|
+
|
|
192
|
+
Options:
|
|
193
|
+
--local 로컬 WT 탭에서 Claude 실행
|
|
194
|
+
--host <name> SSH 호스트로 원격 Claude 실행
|
|
195
|
+
--dir <path> 작업 디렉토리 (기본: 현재 디렉토리 / 원격 홈)
|
|
196
|
+
--prompt "..." Claude에 전달할 첫 메시지
|
|
197
|
+
--name <slug> 세션/탭 이름 (기본: git branch명)
|
|
198
|
+
--handoff <file> 핸드오프 파일 경로 (prompt와 결합 가능)
|
|
199
|
+
--transfer <file> 원격 스테이징할 추가 파일 (반복 지정 가능)
|
|
200
|
+
--send <session> 실행 중인 세션에 프롬프트 전송
|
|
201
|
+
--list tfx-spawn-* psmux 세션 목록
|
|
202
|
+
--attach <name> WT 새 탭에서 세션 attach
|
|
203
|
+
--probe <host> SSH 원격 환경 강제 프로브 + 캐시 갱신
|
|
204
|
+
--capture <name> 세션 pane 내용 캡처 출력
|
|
205
|
+
--wait <name> 세션의 Claude 준비 완료 대기 (기본 60초)`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function parseArgs(argv) {
|
|
209
|
+
let command = "spawn";
|
|
210
|
+
let host = null;
|
|
211
|
+
let dir = null;
|
|
212
|
+
let prompt = null;
|
|
213
|
+
let handoff = null;
|
|
214
|
+
const transferFiles = [];
|
|
215
|
+
let local = false;
|
|
216
|
+
let spawnName = null;
|
|
217
|
+
let sessionName = null;
|
|
218
|
+
let probeHost = null;
|
|
219
|
+
let watchPane = null;
|
|
220
|
+
let watchGraceMs = null;
|
|
221
|
+
let watchPollMs = null;
|
|
222
|
+
let watchMaxMs = null;
|
|
223
|
+
const promptParts = [];
|
|
224
|
+
|
|
225
|
+
for (let index = 2; index < argv.length; index += 1) {
|
|
226
|
+
const arg = argv[index];
|
|
227
|
+
|
|
228
|
+
if (arg === "--local") {
|
|
229
|
+
local = true;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (arg === "--host" && argv[index + 1]) {
|
|
233
|
+
host = validateHost(argv[index + 1]);
|
|
234
|
+
index += 1;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (arg === "--dir" && argv[index + 1]) {
|
|
238
|
+
dir = validateDir(argv[index + 1]);
|
|
239
|
+
index += 1;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (arg === "--prompt" && argv[index + 1]) {
|
|
243
|
+
prompt = argv[index + 1];
|
|
244
|
+
index += 1;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (arg === "--handoff" && argv[index + 1]) {
|
|
248
|
+
handoff = argv[index + 1];
|
|
249
|
+
index += 1;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (arg === "--transfer" && argv[index + 1]) {
|
|
253
|
+
transferFiles.push(argv[index + 1]);
|
|
254
|
+
index += 1;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (arg === "--name" && argv[index + 1]) {
|
|
258
|
+
spawnName = argv[index + 1];
|
|
259
|
+
index += 1;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (arg === "--send" && argv[index + 1]) {
|
|
263
|
+
command = "send";
|
|
264
|
+
sessionName = argv[index + 1];
|
|
265
|
+
index += 1;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (arg === "--list") {
|
|
269
|
+
command = "list";
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (arg === "--attach" && argv[index + 1]) {
|
|
273
|
+
command = "attach";
|
|
274
|
+
sessionName = argv[index + 1];
|
|
275
|
+
index += 1;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (arg === "--probe" && argv[index + 1]) {
|
|
279
|
+
command = "probe";
|
|
280
|
+
probeHost = validateHost(argv[index + 1]);
|
|
281
|
+
index += 1;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (arg === "--capture" && argv[index + 1]) {
|
|
285
|
+
command = "capture";
|
|
286
|
+
sessionName = argv[index + 1];
|
|
287
|
+
index += 1;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (arg === "--wait" && argv[index + 1]) {
|
|
291
|
+
command = "wait";
|
|
292
|
+
sessionName = argv[index + 1];
|
|
293
|
+
index += 1;
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (arg === "--watch-cleanup" && argv[index + 1]) {
|
|
297
|
+
command = "watch-cleanup";
|
|
298
|
+
sessionName = argv[index + 1];
|
|
299
|
+
index += 1;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (arg === "--pane" && argv[index + 1]) {
|
|
303
|
+
watchPane = argv[index + 1];
|
|
304
|
+
index += 1;
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (arg === "--grace-ms" && argv[index + 1]) {
|
|
308
|
+
watchGraceMs = parsePositiveInt(argv[index + 1], null);
|
|
309
|
+
index += 1;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (arg === "--poll-ms" && argv[index + 1]) {
|
|
313
|
+
watchPollMs = parsePositiveInt(argv[index + 1], null);
|
|
314
|
+
index += 1;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (arg === "--max-ms" && argv[index + 1]) {
|
|
318
|
+
watchMaxMs = parsePositiveInt(argv[index + 1], null);
|
|
319
|
+
index += 1;
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
promptParts.push(arg);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const mergedPrompt = prompt ?? (promptParts.length > 0 ? promptParts.join(" ") : null);
|
|
327
|
+
return {
|
|
328
|
+
command,
|
|
329
|
+
dir,
|
|
330
|
+
handoff,
|
|
331
|
+
host,
|
|
332
|
+
local,
|
|
333
|
+
probeHost,
|
|
334
|
+
prompt: mergedPrompt,
|
|
335
|
+
sessionName,
|
|
336
|
+
spawnName,
|
|
337
|
+
transferFiles,
|
|
338
|
+
watchGraceMs,
|
|
339
|
+
watchMaxMs,
|
|
340
|
+
watchPane,
|
|
341
|
+
watchPollMs,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function parseVersion(versionStr) {
|
|
346
|
+
const match = /(\d+)\.(\d+)\.(\d+)/.exec(versionStr);
|
|
347
|
+
if (!match) return null;
|
|
348
|
+
return [parseInt(match[1], 10), parseInt(match[2], 10), parseInt(match[3], 10)];
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function compareVersions(a, b) {
|
|
352
|
+
for (let i = 0; i < 3; i++) {
|
|
353
|
+
if (a[i] !== b[i]) return a[i] - b[i];
|
|
354
|
+
}
|
|
355
|
+
return 0;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function probeVersion(binPath) {
|
|
359
|
+
try {
|
|
360
|
+
if (/\.(cmd|bat)$/iu.test(binPath)) {
|
|
361
|
+
// .cmd/.bat → execSync로 shell 경유 (execFileSync EINVAL 회피)
|
|
362
|
+
const out = execSync(`"${binPath}" --version`, {
|
|
363
|
+
encoding: "utf8", timeout: 3000, stdio: ["ignore", "pipe", "ignore"],
|
|
364
|
+
});
|
|
365
|
+
return parseVersion(out);
|
|
366
|
+
}
|
|
367
|
+
const out = execFileSync(binPath, ["--version"], {
|
|
368
|
+
encoding: "utf8", timeout: 3000, stdio: ["ignore", "pipe", "ignore"],
|
|
369
|
+
});
|
|
370
|
+
return parseVersion(out);
|
|
371
|
+
} catch {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function detectClaudePath() {
|
|
377
|
+
if (process.env.CLAUDE_BIN_PATH) return process.env.CLAUDE_BIN_PATH;
|
|
378
|
+
|
|
379
|
+
const candidates = [];
|
|
380
|
+
|
|
381
|
+
const wingetPath = join(homedir(), "AppData", "Local", "Microsoft", "WinGet", "Links", "claude.exe");
|
|
382
|
+
if (existsSync(wingetPath)) candidates.push(wingetPath);
|
|
383
|
+
|
|
384
|
+
const npmPath = join(process.env.APPDATA || "", "npm", "claude.cmd");
|
|
385
|
+
if (existsSync(npmPath)) candidates.push(npmPath);
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
const command = IS_WINDOWS_LOCAL ? "where" : "which";
|
|
389
|
+
const result = execFileSync(command, ["claude"], { encoding: "utf8", timeout: 5000 }).trim();
|
|
390
|
+
if (result) {
|
|
391
|
+
for (const line of result.split(/\r?\n/u)) {
|
|
392
|
+
const p = line.trim();
|
|
393
|
+
if (p && !candidates.includes(p)) candidates.push(p);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
} catch {
|
|
397
|
+
// not found
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (candidates.length === 0) return "claude";
|
|
401
|
+
|
|
402
|
+
let bestPath = candidates[0];
|
|
403
|
+
let bestVersion = probeVersion(candidates[0]);
|
|
404
|
+
|
|
405
|
+
for (const candidate of candidates.slice(1)) {
|
|
406
|
+
const ver = probeVersion(candidate);
|
|
407
|
+
if (ver === null) continue;
|
|
408
|
+
if (bestVersion === null || compareVersions(ver, bestVersion) > 0) {
|
|
409
|
+
bestVersion = ver;
|
|
410
|
+
bestPath = candidate;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return bestPath;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function getPermissionFlag() {
|
|
418
|
+
return process.env.TFX_CLAUDE_SAFE_MODE === "1" ? [] : ["--dangerously-skip-permissions"];
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function failFast(message) {
|
|
422
|
+
console.error(message);
|
|
423
|
+
process.exit(1);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function validateTransferCandidate(pathLike, label) {
|
|
427
|
+
const localPath = resolve(pathLike);
|
|
428
|
+
if (!existsSync(localPath)) {
|
|
429
|
+
failFast(`${label} not found: ${localPath}`);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// 프로젝트 디렉토리 외부 파일 전송 방지
|
|
433
|
+
const projectRoot = process.cwd();
|
|
434
|
+
if (!localPath.startsWith(projectRoot)) {
|
|
435
|
+
failFast(`${label} must be within project directory: ${localPath}`);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const size = statSync(localPath).size;
|
|
439
|
+
if (size > MAX_HANDOFF_BYTES) {
|
|
440
|
+
failFast(`${label} too large: ${size} bytes (max ${MAX_HANDOFF_BYTES})`);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return {
|
|
444
|
+
inputPath: pathLike,
|
|
445
|
+
localPath,
|
|
446
|
+
size,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function dedupeTransferCandidates(candidates) {
|
|
451
|
+
const byPath = new Map();
|
|
452
|
+
for (const candidate of candidates) {
|
|
453
|
+
if (!byPath.has(candidate.localPath)) {
|
|
454
|
+
byPath.set(candidate.localPath, candidate);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return [...byPath.values()];
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function buildPromptContext(args, options = {}) {
|
|
461
|
+
const includeTransferFiles = options.includeTransferFiles === true;
|
|
462
|
+
const candidates = [];
|
|
463
|
+
let content = "";
|
|
464
|
+
|
|
465
|
+
if (args.handoff) {
|
|
466
|
+
const handoffCandidate = validateTransferCandidate(args.handoff, "handoff file");
|
|
467
|
+
content = readFileSync(handoffCandidate.localPath, "utf8").trim();
|
|
468
|
+
candidates.push(handoffCandidate);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (includeTransferFiles) {
|
|
472
|
+
for (const filePath of args.transferFiles || []) {
|
|
473
|
+
candidates.push(validateTransferCandidate(filePath, "transfer file"));
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (args.prompt) {
|
|
478
|
+
content = content ? `${content}\n\n---\n\n${args.prompt}` : args.prompt;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return {
|
|
482
|
+
prompt: content,
|
|
483
|
+
transferCandidates: dedupeTransferCandidates(candidates),
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function buildLocalPathVariants(pathLike) {
|
|
488
|
+
const resolvedPath = resolve(pathLike);
|
|
489
|
+
const variants = new Set([
|
|
490
|
+
pathLike,
|
|
491
|
+
resolvedPath,
|
|
492
|
+
normalizeCommandPath(pathLike),
|
|
493
|
+
normalizeCommandPath(resolvedPath),
|
|
494
|
+
pathLike.replace(/\//g, "\\"),
|
|
495
|
+
resolvedPath.replace(/\//g, "\\"),
|
|
496
|
+
]);
|
|
497
|
+
|
|
498
|
+
return [...variants]
|
|
499
|
+
.map((value) => String(value || "").trim())
|
|
500
|
+
.filter((value) => value.length > 2);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function rewritePromptPaths(prompt, stagedFiles) {
|
|
504
|
+
if (!prompt || stagedFiles.length === 0) {
|
|
505
|
+
return prompt;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const replacements = [];
|
|
509
|
+
for (const file of stagedFiles) {
|
|
510
|
+
for (const variant of buildLocalPathVariants(file.inputPath)) {
|
|
511
|
+
replacements.push({ from: variant, to: file.remotePath });
|
|
512
|
+
}
|
|
513
|
+
for (const variant of buildLocalPathVariants(file.localPath)) {
|
|
514
|
+
replacements.push({ from: variant, to: file.remotePath });
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
replacements.sort((a, b) => b.from.length - a.from.length);
|
|
519
|
+
|
|
520
|
+
let rewritten = prompt;
|
|
521
|
+
for (const replacement of replacements) {
|
|
522
|
+
if (rewritten.includes(replacement.from)) {
|
|
523
|
+
rewritten = rewritten.split(replacement.from).join(replacement.to);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return rewritten;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function spawnLocalFallback(args, claudePath, prompt) {
|
|
531
|
+
const dir = args.dir ? resolve(args.dir) : process.cwd();
|
|
532
|
+
|
|
533
|
+
if (!IS_WINDOWS_LOCAL) {
|
|
534
|
+
const cliArgs = [...getPermissionFlag()];
|
|
535
|
+
if (prompt) cliArgs.push(prompt);
|
|
536
|
+
|
|
537
|
+
const child = spawn(claudePath, cliArgs, {
|
|
538
|
+
cwd: dir,
|
|
539
|
+
stdio: "inherit",
|
|
540
|
+
});
|
|
541
|
+
child.on("exit", (code) => process.exit(code || 0));
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const wtArgs = ["new-tab", "-d", dir, "--"];
|
|
546
|
+
const claudeForward = claudePath.replace(/\\/g, "/");
|
|
547
|
+
|
|
548
|
+
if (prompt) {
|
|
549
|
+
const psQuoted = `'${prompt.replace(/'/g, "''")}'`;
|
|
550
|
+
wtArgs.push(
|
|
551
|
+
"pwsh",
|
|
552
|
+
"-NoProfile",
|
|
553
|
+
"-Command",
|
|
554
|
+
`& '${claudeForward}' ${getPermissionFlag().join(" ")} ${psQuoted}`,
|
|
555
|
+
);
|
|
556
|
+
} else {
|
|
557
|
+
wtArgs.push(claudeForward, ...getPermissionFlag());
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
try {
|
|
561
|
+
spawn("wt.exe", wtArgs, { detached: true, stdio: "ignore", windowsHide: false }).unref();
|
|
562
|
+
console.log(`spawned local Claude in WT tab → ${dir}`);
|
|
563
|
+
} catch (error) {
|
|
564
|
+
console.error("wt.exe spawn failed:", error.message);
|
|
565
|
+
process.exit(1);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function spawnRemoteFallback(args, promptContext) {
|
|
570
|
+
const { host } = args;
|
|
571
|
+
if (!host) {
|
|
572
|
+
console.error("--host required for remote spawn");
|
|
573
|
+
process.exit(1);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const dir = args.dir || "~";
|
|
577
|
+
const permFlags = getPermissionFlag();
|
|
578
|
+
let prompt = promptContext.prompt;
|
|
579
|
+
|
|
580
|
+
let remoteHome;
|
|
581
|
+
try {
|
|
582
|
+
remoteHome = execFileSync("ssh", [host, "echo", "$env:USERPROFILE"], { encoding: "utf8", timeout: 5000 }).trim();
|
|
583
|
+
} catch {
|
|
584
|
+
try {
|
|
585
|
+
remoteHome = execFileSync("ssh", [host, "echo", "$HOME"], { encoding: "utf8", timeout: 5000 }).trim();
|
|
586
|
+
} catch {
|
|
587
|
+
// 원격 홈 감지 실패 시 transfer 기능을 사용할 수 없음
|
|
588
|
+
console.warn(`[tfx] 원격 홈 디렉토리 감지 실패 (${host}) — file transfer 비활성화`);
|
|
589
|
+
remoteHome = null;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (remoteHome) {
|
|
594
|
+
try {
|
|
595
|
+
const fallbackEnv = { home: remoteHome, os: "win32", shell: "pwsh" };
|
|
596
|
+
const stageId = `spawn-${randomUUID().slice(0, 8)}`;
|
|
597
|
+
const { stagedFiles } = stageRemotePromptFiles(host, fallbackEnv, promptContext.transferCandidates, stageId);
|
|
598
|
+
prompt = rewritePromptPaths(prompt, stagedFiles);
|
|
599
|
+
} catch (error) {
|
|
600
|
+
failFast(`failed to stage remote files: ${error?.message || String(error)}`);
|
|
601
|
+
}
|
|
602
|
+
} else if (promptContext.transferCandidates.length > 0) {
|
|
603
|
+
console.warn("[tfx] 원격 홈 미감지 — --transfer 파일이 무시됩니다");
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const scriptLines = [
|
|
607
|
+
`cd '${dir.replace(/'/g, "''")}'`,
|
|
608
|
+
];
|
|
609
|
+
|
|
610
|
+
if (prompt) {
|
|
611
|
+
const safePrompt = prompt.replace(/'/g, "''");
|
|
612
|
+
scriptLines.push(`& "$env:USERPROFILE\\.local\\bin\\claude.exe" ${permFlags.join(" ")} '${safePrompt}'`);
|
|
613
|
+
} else {
|
|
614
|
+
scriptLines.push(`& "$env:USERPROFILE\\.local\\bin\\claude.exe" ${permFlags.join(" ")}`);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const scriptContent = scriptLines.join("\n");
|
|
618
|
+
const localScript = join(tmpdir(), "tfx-remote-spawn.ps1");
|
|
619
|
+
writeFileSync(localScript, scriptContent, "utf8");
|
|
620
|
+
|
|
621
|
+
try {
|
|
622
|
+
execFileSync("scp", [localScript, `${host}:tfx-remote-spawn.ps1`], { timeout: 10000, stdio: "pipe" });
|
|
623
|
+
} catch (error) {
|
|
624
|
+
console.error("failed to copy script to remote:", error.message);
|
|
625
|
+
process.exit(1);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const remoteScript = `${remoteHome.replace(/\\/g, "/")}/tfx-remote-spawn.ps1`;
|
|
629
|
+
const remoteCmd = `pwsh -NoExit -File ${remoteScript}`;
|
|
630
|
+
|
|
631
|
+
if (IS_WINDOWS_LOCAL) {
|
|
632
|
+
const wtArgs = [
|
|
633
|
+
"new-tab",
|
|
634
|
+
"--title",
|
|
635
|
+
`Claude@${host}`,
|
|
636
|
+
"--",
|
|
637
|
+
"ssh",
|
|
638
|
+
"-t",
|
|
639
|
+
"--",
|
|
640
|
+
host,
|
|
641
|
+
remoteCmd,
|
|
642
|
+
];
|
|
643
|
+
try {
|
|
644
|
+
spawn("wt.exe", wtArgs, { detached: true, stdio: "ignore", windowsHide: false }).unref();
|
|
645
|
+
console.log(`spawned remote Claude → ${host}:${dir}`);
|
|
646
|
+
} catch (error) {
|
|
647
|
+
console.error("wt.exe spawn failed:", error.message);
|
|
648
|
+
process.exit(1);
|
|
649
|
+
}
|
|
650
|
+
} else {
|
|
651
|
+
const child = spawn("ssh", ["-t", "--", host, remoteCmd], { stdio: "inherit" });
|
|
652
|
+
child.on("exit", (code) => process.exit(code || 0));
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function shouldUsePsmux() {
|
|
657
|
+
return IS_WINDOWS_LOCAL && hasPsmux();
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function requirePsmux() {
|
|
661
|
+
if (!hasPsmux()) {
|
|
662
|
+
throw new Error("psmux is required for this command");
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function parseProbeLines(text) {
|
|
667
|
+
return Object.fromEntries(
|
|
668
|
+
text
|
|
669
|
+
.split(/\r?\n/u)
|
|
670
|
+
.map((line) => line.trim())
|
|
671
|
+
.filter(Boolean)
|
|
672
|
+
.map((line) => {
|
|
673
|
+
const separatorIndex = line.indexOf("=");
|
|
674
|
+
return separatorIndex === -1
|
|
675
|
+
? null
|
|
676
|
+
: [line.slice(0, separatorIndex), line.slice(separatorIndex + 1)];
|
|
677
|
+
})
|
|
678
|
+
.filter(Boolean),
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function normalizePwshProbeEnv(host, parsed) {
|
|
683
|
+
if (parsed.shell !== "pwsh" || parsed.os !== "win32") {
|
|
684
|
+
return null;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (!parsed.home) {
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
return Object.freeze({
|
|
692
|
+
claudePath: (!parsed.claude || parsed.claude === "notfound") ? null : parsed.claude,
|
|
693
|
+
home: parsed.home,
|
|
694
|
+
os: "win32",
|
|
695
|
+
shell: "pwsh",
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function normalizePosixProbeEnv(host, parsed) {
|
|
700
|
+
const os = parsed.os === "darwin" ? "darwin" : parsed.os === "linux" ? "linux" : null;
|
|
701
|
+
if (!os || !parsed.home) {
|
|
702
|
+
return null;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
return Object.freeze({
|
|
706
|
+
claudePath: (!parsed.claude || parsed.claude === "notfound") ? null : parsed.claude,
|
|
707
|
+
home: parsed.home,
|
|
708
|
+
os,
|
|
709
|
+
shell: parsed.shell === "zsh" ? "zsh" : "bash",
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function getRemoteEnvCachePath(host) {
|
|
714
|
+
return join(REMOTE_ENV_CACHE_DIR, `${host}.json`);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function readRemoteEnvCache(host) {
|
|
718
|
+
const cachePath = getRemoteEnvCachePath(host);
|
|
719
|
+
if (!existsSync(cachePath)) {
|
|
720
|
+
return null;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
try {
|
|
724
|
+
const parsed = JSON.parse(readFileSync(cachePath, "utf8"));
|
|
725
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
726
|
+
} catch {
|
|
727
|
+
return null;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function isRemoteEnvCacheFresh(cacheEntry) {
|
|
732
|
+
return Boolean(
|
|
733
|
+
cacheEntry
|
|
734
|
+
&& typeof cacheEntry.cachedAt === "number"
|
|
735
|
+
&& cacheEntry.env
|
|
736
|
+
&& (Date.now() - cacheEntry.cachedAt) < REMOTE_ENV_TTL_MS,
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function writeRemoteEnvCache(host, env) {
|
|
741
|
+
mkdirSync(REMOTE_ENV_CACHE_DIR, { recursive: true });
|
|
742
|
+
writeFileSync(
|
|
743
|
+
getRemoteEnvCachePath(host),
|
|
744
|
+
JSON.stringify({ cachedAt: Date.now(), env }, null, 2),
|
|
745
|
+
"utf8",
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function probeRemoteEnvViaPwsh(host) {
|
|
750
|
+
const command = [
|
|
751
|
+
"Write-Output 'shell=pwsh'",
|
|
752
|
+
'Write-Output "home=$env:USERPROFILE"',
|
|
753
|
+
'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
|
+
'Write-Output "os=$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows) ? \'win32\' : \'other\')"',
|
|
755
|
+
].join("; ");
|
|
756
|
+
|
|
757
|
+
let output;
|
|
758
|
+
try {
|
|
759
|
+
output = execFileSync(
|
|
760
|
+
"ssh",
|
|
761
|
+
[host, "pwsh", "-NoProfile", "-Command", command],
|
|
762
|
+
{ encoding: "utf8", timeout: 15000, stdio: ["pipe", "pipe", "pipe"] },
|
|
763
|
+
);
|
|
764
|
+
} catch {
|
|
765
|
+
return null;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
return normalizePwshProbeEnv(host, parseProbeLines(output));
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function probeRemoteEnvViaPosix(host) {
|
|
772
|
+
const script = [
|
|
773
|
+
"echo shell=$(basename $SHELL)",
|
|
774
|
+
"echo home=$HOME",
|
|
775
|
+
"command -v claude >/dev/null 2>&1 && echo claude=$(command -v claude) || echo claude=notfound",
|
|
776
|
+
"echo os=$(uname -s | tr A-Z a-z)",
|
|
777
|
+
].join("\n");
|
|
778
|
+
|
|
779
|
+
let output;
|
|
780
|
+
try {
|
|
781
|
+
output = execFileSync("ssh", [host, "sh"], {
|
|
782
|
+
encoding: "utf8",
|
|
783
|
+
timeout: 15000,
|
|
784
|
+
input: script,
|
|
785
|
+
});
|
|
786
|
+
} catch {
|
|
787
|
+
return null;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
return normalizePosixProbeEnv(host, parseProbeLines(output));
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function probeRemoteEnv(host, opts = {}) {
|
|
794
|
+
const force = opts.force === true;
|
|
795
|
+
|
|
796
|
+
if (!force) {
|
|
797
|
+
const cached = readRemoteEnvCache(host);
|
|
798
|
+
if (isRemoteEnvCacheFresh(cached)) {
|
|
799
|
+
return cached.env;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const pwshEnv = probeRemoteEnvViaPwsh(host);
|
|
804
|
+
if (pwshEnv) {
|
|
805
|
+
writeRemoteEnvCache(host, pwshEnv);
|
|
806
|
+
return pwshEnv;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const posixEnv = probeRemoteEnvViaPosix(host);
|
|
810
|
+
if (posixEnv) {
|
|
811
|
+
writeRemoteEnvCache(host, posixEnv);
|
|
812
|
+
return posixEnv;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
throw new Error(`remote probe failed for ${host}`);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function isWindowsAbsolutePath(value) {
|
|
819
|
+
return /^[a-zA-Z]:[\\/]/u.test(value) || value.startsWith("\\\\");
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function resolveRemoteDir(dir, env) {
|
|
823
|
+
const requestedDir = dir || env.home;
|
|
824
|
+
|
|
825
|
+
if (env.os === "win32") {
|
|
826
|
+
const winDir = requestedDir.replace(/\//g, "\\");
|
|
827
|
+
if (winDir === "~") return env.home;
|
|
828
|
+
if (/^~[\\/]/u.test(winDir)) return win32Path.join(env.home, winDir.slice(2));
|
|
829
|
+
if (isWindowsAbsolutePath(winDir)) return winDir;
|
|
830
|
+
return win32Path.join(env.home, winDir);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
if (requestedDir === "~") return env.home;
|
|
834
|
+
if (requestedDir.startsWith("~/")) return posixPath.join(env.home, requestedDir.slice(2));
|
|
835
|
+
if (requestedDir.startsWith("/")) return requestedDir;
|
|
836
|
+
return posixPath.join(env.home, requestedDir);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function resolveRemoteStageDir(env, stageId) {
|
|
840
|
+
const normalizedHome = normalizeCommandPath(env.home);
|
|
841
|
+
return `${normalizedHome}/${REMOTE_STAGE_ROOT}/${stageId}`;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function ensureRemoteStageDir(host, env, remoteStageDir) {
|
|
845
|
+
if (env.os === "win32") {
|
|
846
|
+
const safePath = escapePwshSingleQuoted(remoteStageDir);
|
|
847
|
+
const command = `New-Item -ItemType Directory -Path '${safePath}' -Force | Out-Null`;
|
|
848
|
+
execFileSync("ssh", [host, "pwsh", "-NoProfile", "-Command", command], { timeout: 10000, stdio: "pipe" });
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
execFileSync("ssh", [host, "sh", "-lc", `mkdir -p ${shellQuote(remoteStageDir)}`], {
|
|
853
|
+
timeout: 10000,
|
|
854
|
+
stdio: "pipe",
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function uploadFileToRemote(host, localPath, remotePath) {
|
|
859
|
+
// scp는 remote path를 셸 확장 없이 직접 전달 — shellQuote 불필요
|
|
860
|
+
// Windows 원격에서 쿼트가 리터럴 문자로 해석되어 경로 오류 발생
|
|
861
|
+
execFileSync("scp", [localPath, `${host}:${remotePath}`], { timeout: 15000, stdio: "pipe" });
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function stageRemotePromptFiles(host, env, transferCandidates, stageId) {
|
|
865
|
+
if (!transferCandidates || transferCandidates.length === 0) {
|
|
866
|
+
return { remoteStageDir: null, stagedFiles: [] };
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
const remoteStageDir = resolveRemoteStageDir(env, stageId);
|
|
870
|
+
ensureRemoteStageDir(host, env, remoteStageDir);
|
|
871
|
+
|
|
872
|
+
const basenameCounts = new Map();
|
|
873
|
+
const stagedFiles = transferCandidates.map((candidate) => {
|
|
874
|
+
const fileName = basename(candidate.localPath);
|
|
875
|
+
const count = (basenameCounts.get(fileName) || 0) + 1;
|
|
876
|
+
basenameCounts.set(fileName, count);
|
|
877
|
+
const stagedName = count === 1 ? fileName : `${count}-${fileName}`;
|
|
878
|
+
const remotePath = `${remoteStageDir}/${stagedName}`;
|
|
879
|
+
uploadFileToRemote(host, candidate.localPath, remotePath);
|
|
880
|
+
return {
|
|
881
|
+
...candidate,
|
|
882
|
+
remotePath,
|
|
883
|
+
};
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
return { remoteStageDir, stagedFiles };
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function listSessionNamesFromRawOutput(output) {
|
|
890
|
+
return output
|
|
891
|
+
.split(/\r?\n/u)
|
|
892
|
+
.map((line) => line.trim())
|
|
893
|
+
.filter(Boolean)
|
|
894
|
+
.map((line) => line.split(":")[0]?.trim())
|
|
895
|
+
.filter(Boolean);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function listSpawnSessions() {
|
|
899
|
+
const helperSessions = listPsmuxSessions().filter((name) => name.startsWith("tfx-spawn-"));
|
|
900
|
+
if (helperSessions.length > 0) {
|
|
901
|
+
return helperSessions;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
try {
|
|
905
|
+
return listSessionNamesFromRawOutput(psmuxExec(["list-sessions"]))
|
|
906
|
+
.filter((name) => name.startsWith("tfx-spawn-"));
|
|
907
|
+
} catch {
|
|
908
|
+
return [];
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function openAttachTab(sessionName, title = null) {
|
|
913
|
+
if (IS_WINDOWS_LOCAL) {
|
|
914
|
+
const wtArgs = title
|
|
915
|
+
? ["new-tab", "--title", title, "--suppressApplicationTitle", "--", "psmux", "attach", "-t", sessionName]
|
|
916
|
+
: ["new-tab", "--", "psmux", "attach", "-t", sessionName];
|
|
917
|
+
spawn("wt.exe", wtArgs, { detached: true, stdio: "ignore", windowsHide: false }).unref();
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
attachPsmuxSession(sessionName);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function getLastNonEmptyLine(text) {
|
|
925
|
+
const lines = String(text)
|
|
926
|
+
.split(/\r?\n/u)
|
|
927
|
+
.map((line) => line.trimEnd())
|
|
928
|
+
.filter((line) => line.trim().length > 0);
|
|
929
|
+
return lines.at(-1) || "";
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
async function waitForRemotePrompt(sessionName, paneId) {
|
|
933
|
+
const baseline = capturePsmuxPane(paneId, 20);
|
|
934
|
+
const capture = startCapture(sessionName, paneId);
|
|
935
|
+
const deadline = Date.now() + 15_000;
|
|
936
|
+
|
|
937
|
+
while (Date.now() <= deadline) {
|
|
938
|
+
const remainingMs = Math.max(250, deadline - Date.now());
|
|
939
|
+
await waitForPattern(
|
|
940
|
+
sessionName,
|
|
941
|
+
paneId,
|
|
942
|
+
SSH_PROMPT_PATTERN,
|
|
943
|
+
Math.min(1, remainingMs / 1000),
|
|
944
|
+
{ logPath: capture.logPath },
|
|
945
|
+
);
|
|
946
|
+
|
|
947
|
+
const tail = capturePsmuxPane(paneId, 20);
|
|
948
|
+
const lastLine = getLastNonEmptyLine(tail);
|
|
949
|
+
if (tail !== baseline && SSH_PROMPT_PATTERN.test(lastLine)) {
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
throw new Error(`ssh prompt wait timed out for ${sessionName}: ${capturePsmuxPane(paneId, 20)}`);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/** @returns {boolean|null} true=dead, false=alive, null=probe 실패 */
|
|
958
|
+
function isPrimaryPaneDead(paneId) {
|
|
959
|
+
try {
|
|
960
|
+
const output = psmuxExec(["list-panes", "-t", paneId, "-F", "#{pane_dead}"]);
|
|
961
|
+
const lines = output.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean);
|
|
962
|
+
if (lines.some((line) => line === "1")) return true;
|
|
963
|
+
return false;
|
|
964
|
+
} catch {
|
|
965
|
+
return null;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
async function watchSpawnSessionExit(sessionName, options = {}) {
|
|
970
|
+
const paneId = options.paneId || `${sessionName}:0.0`;
|
|
971
|
+
const pollMs = parsePositiveInt(options.pollMs, DEFAULT_CLEANUP_WATCH_POLL_MS);
|
|
972
|
+
const graceMs = parsePositiveInt(options.graceMs, DEFAULT_CLEANUP_WATCH_GRACE_MS);
|
|
973
|
+
const maxWaitMs = parsePositiveInt(options.maxWaitMs, DEFAULT_CLEANUP_WATCH_MAX_MS);
|
|
974
|
+
const sessionExists = typeof options.sessionExists === "function"
|
|
975
|
+
? options.sessionExists
|
|
976
|
+
: psmuxSessionExists;
|
|
977
|
+
const getPaneStatus = typeof options.getPaneStatus === "function"
|
|
978
|
+
? options.getPaneStatus
|
|
979
|
+
: (targetPaneId) => ({ isDead: isPrimaryPaneDead(targetPaneId), exitCode: null });
|
|
980
|
+
const killSession = typeof options.killSession === "function"
|
|
981
|
+
? options.killSession
|
|
982
|
+
: killPsmuxSession;
|
|
983
|
+
const now = typeof options.now === "function" ? options.now : Date.now;
|
|
984
|
+
const sleep = typeof options.sleep === "function" ? options.sleep : sleepMsAsync;
|
|
985
|
+
const startedAt = now();
|
|
986
|
+
let consecutiveErrors = 0;
|
|
987
|
+
|
|
988
|
+
while (now() - startedAt <= maxWaitMs) {
|
|
989
|
+
if (!sessionExists(sessionName)) {
|
|
990
|
+
return { cleaned: false, reason: "session-missing" };
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
const paneStatus = getPaneStatus(paneId) || {};
|
|
994
|
+
if (paneStatus.isDead == null) {
|
|
995
|
+
consecutiveErrors += 1;
|
|
996
|
+
if (consecutiveErrors >= 10) {
|
|
997
|
+
return { cleaned: false, reason: "probe-failed" };
|
|
998
|
+
}
|
|
999
|
+
} else {
|
|
1000
|
+
consecutiveErrors = 0;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
if (paneStatus.isDead === true) {
|
|
1004
|
+
await sleep(graceMs);
|
|
1005
|
+
|
|
1006
|
+
if (!sessionExists(sessionName)) {
|
|
1007
|
+
return { cleaned: false, reason: "session-missing" };
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
const afterGrace = getPaneStatus(paneId) || {};
|
|
1011
|
+
if (afterGrace.isDead === true) {
|
|
1012
|
+
killSession(sessionName);
|
|
1013
|
+
return {
|
|
1014
|
+
cleaned: true,
|
|
1015
|
+
reason: "pane-dead",
|
|
1016
|
+
exitCode: afterGrace.exitCode ?? paneStatus.exitCode ?? null,
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
await sleep(pollMs);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
return { cleaned: false, reason: "timeout" };
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
async function runSpawnCleanupWatcher(sessionName, paneId, timingOptions = {}) {
|
|
1028
|
+
await watchSpawnSessionExit(sessionName, {
|
|
1029
|
+
paneId,
|
|
1030
|
+
pollMs: timingOptions.pollMs,
|
|
1031
|
+
graceMs: timingOptions.graceMs,
|
|
1032
|
+
maxWaitMs: timingOptions.maxMs,
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function startSpawnExitWatcher(sessionName, options = {}) {
|
|
1037
|
+
if (!options.force && !shouldUsePsmux()) {
|
|
1038
|
+
return false;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
const paneId = options.paneId || `${sessionName}:0.0`;
|
|
1042
|
+
const args = buildSpawnCleanupWatcherArgs(sessionName, paneId, {
|
|
1043
|
+
graceMs: options.graceMs,
|
|
1044
|
+
maxMs: options.maxMs,
|
|
1045
|
+
pollMs: options.pollMs,
|
|
1046
|
+
});
|
|
1047
|
+
args[0] = options.scriptPath || args[0];
|
|
1048
|
+
const spawnFn = typeof options.spawnFn === "function" ? options.spawnFn : spawn;
|
|
1049
|
+
const child = spawnFn(options.execPath || process.execPath, args, {
|
|
1050
|
+
detached: true,
|
|
1051
|
+
stdio: "ignore",
|
|
1052
|
+
windowsHide: true,
|
|
1053
|
+
});
|
|
1054
|
+
if (child && typeof child.unref === "function") {
|
|
1055
|
+
child.unref();
|
|
1056
|
+
}
|
|
1057
|
+
return true;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
function startSpawnSessionCleanupWatcher(sessionName, paneId, timingOptions = {}) {
|
|
1061
|
+
try {
|
|
1062
|
+
startSpawnExitWatcher(sessionName, { ...timingOptions, force: true, paneId });
|
|
1063
|
+
} catch {
|
|
1064
|
+
// watcher 시작 실패는 spawn 자체 실패로 보지 않는다.
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
function spawnLocal(args, claudePath, prompt) {
|
|
1069
|
+
if (!shouldUsePsmux()) {
|
|
1070
|
+
spawnLocalFallback(args, claudePath, prompt);
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
const dir = args.dir ? resolve(args.dir) : process.cwd();
|
|
1075
|
+
const slug = buildSessionSlug(args.spawnName);
|
|
1076
|
+
const sessionName = deduplicateSessionName(`tfx-spawn-${slug}`);
|
|
1077
|
+
const paneId = `${sessionName}:0.0`;
|
|
1078
|
+
const permissionFlags = getPermissionFlag().join(" ");
|
|
1079
|
+
const claudePathNorm = normalizeCommandPath(claudePath);
|
|
1080
|
+
|
|
1081
|
+
// 임시파일 생성 (프롬프트가 있을 때만)
|
|
1082
|
+
// 정리는 pwsh 스크립트 내부에서 수행 (Node exit 시 삭제하면 pane 실행 전 사라짐)
|
|
1083
|
+
let tmpFile = null;
|
|
1084
|
+
if (prompt) {
|
|
1085
|
+
tmpFile = join(tmpdir(), `tfx-prompt-${randomUUID().slice(0, 8)}.md`);
|
|
1086
|
+
writeFileSync(tmpFile, prompt, { encoding: "utf8" });
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
createPsmuxSession(sessionName, { layout: "1xN", paneCount: 1 });
|
|
1090
|
+
try {
|
|
1091
|
+
sendKeysToPane(paneId, `cd '${escapePwshSingleQuoted(dir)}'`);
|
|
1092
|
+
sleepMs(300);
|
|
1093
|
+
|
|
1094
|
+
if (prompt && tmpFile) {
|
|
1095
|
+
// pwsh -File 패턴: 인라인 쿼팅 문제 회피 (피드백: -Command 금지)
|
|
1096
|
+
// 1단계: 프롬프트를 Get-Content -Raw → claude -p (one-shot), 세션 ID 추출
|
|
1097
|
+
// 2단계: --resume으로 인터랙티브 세션 이어붙이기
|
|
1098
|
+
const tmpFileNorm = normalizeCommandPath(tmpFile);
|
|
1099
|
+
const flags = getPermissionFlag().map((f) => `'${escapePwshSingleQuoted(f)}'`).join(", ");
|
|
1100
|
+
const scriptContent = [
|
|
1101
|
+
`$ErrorActionPreference = 'SilentlyContinue'`,
|
|
1102
|
+
`$t = '${escapePwshSingleQuoted(tmpFileNorm)}'`,
|
|
1103
|
+
`$c = '${escapePwshSingleQuoted(claudePathNorm)}'`,
|
|
1104
|
+
`$f = @(${flags})`,
|
|
1105
|
+
`$raw = Get-Content -Raw $t`,
|
|
1106
|
+
`Remove-Item -ErrorAction SilentlyContinue $t`,
|
|
1107
|
+
`Remove-Item -ErrorAction SilentlyContinue $MyInvocation.MyCommand.Definition`,
|
|
1108
|
+
`& $c @f $raw`,
|
|
1109
|
+
`$trifluxExit = if ($null -ne $LASTEXITCODE) { [int]$LASTEXITCODE } else { 0 }`,
|
|
1110
|
+
`exit $trifluxExit`,
|
|
1111
|
+
].join("\n");
|
|
1112
|
+
const scriptFile = join(tmpdir(), `tfx-spawn-${randomUUID().slice(0, 8)}.ps1`);
|
|
1113
|
+
writeFileSync(scriptFile, scriptContent, { encoding: "utf8" });
|
|
1114
|
+
sendKeysToPane(paneId, `pwsh -NoProfile -File '${escapePwshSingleQuoted(normalizeCommandPath(scriptFile))}'; ${buildPwshExitTail()}`);
|
|
1115
|
+
} else {
|
|
1116
|
+
const command = buildLocalClaudeCommand(claudePathNorm, permissionFlags);
|
|
1117
|
+
sendKeysToPane(paneId, command);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
startSpawnSessionCleanupWatcher(sessionName, paneId);
|
|
1121
|
+
openAttachTab(sessionName, `local:${slug}`);
|
|
1122
|
+
console.log(sessionName);
|
|
1123
|
+
} catch (err) {
|
|
1124
|
+
try { killPsmuxSession(sessionName); } catch {}
|
|
1125
|
+
throw err;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
async function spawnRemote(args, promptContext) {
|
|
1130
|
+
const { host } = args;
|
|
1131
|
+
if (!host) {
|
|
1132
|
+
console.error("--host required for remote spawn");
|
|
1133
|
+
process.exit(1);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
if (!shouldUsePsmux()) {
|
|
1137
|
+
spawnRemoteFallback(args, promptContext);
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
const env = probeRemoteEnv(host);
|
|
1142
|
+
if (!env.claudePath) {
|
|
1143
|
+
console.error(`claude not found on ${host}. Install Claude Code on the remote host first.`);
|
|
1144
|
+
process.exit(1);
|
|
1145
|
+
}
|
|
1146
|
+
const resolvedDir = resolveRemoteDir(args.dir, env);
|
|
1147
|
+
const slug = buildSessionSlug(args.spawnName);
|
|
1148
|
+
const sessionName = deduplicateSessionName(`tfx-spawn-${host}-${slug}`);
|
|
1149
|
+
const paneId = `${sessionName}:0.0`;
|
|
1150
|
+
const permissionFlags = getPermissionFlag().join(" ");
|
|
1151
|
+
let prompt = promptContext.prompt;
|
|
1152
|
+
|
|
1153
|
+
try {
|
|
1154
|
+
const { stagedFiles } = stageRemotePromptFiles(host, env, promptContext.transferCandidates, sessionName);
|
|
1155
|
+
prompt = rewritePromptPaths(prompt, stagedFiles);
|
|
1156
|
+
} catch (error) {
|
|
1157
|
+
failFast(`failed to stage remote files: ${error?.message || String(error)}`);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
createPsmuxSession(sessionName, { layout: "1xN", paneCount: 1 });
|
|
1161
|
+
try {
|
|
1162
|
+
sendKeysToPane(paneId, buildRemoteBootstrapCommand(host));
|
|
1163
|
+
await waitForRemotePrompt(sessionName, paneId);
|
|
1164
|
+
|
|
1165
|
+
if (env.shell === "pwsh") {
|
|
1166
|
+
sendKeysToPane(paneId, `cd '${escapePwshSingleQuoted(resolvedDir)}'`);
|
|
1167
|
+
} else {
|
|
1168
|
+
sendKeysToPane(paneId, `cd ${shellQuote(resolvedDir)}`);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
if (prompt) {
|
|
1172
|
+
// file-based prompt delivery: SCP prompt → remote launcher script
|
|
1173
|
+
// send-keys로 긴 프롬프트를 보내면 깨지므로 파일 전송 후 스크립트 실행
|
|
1174
|
+
const stageDir = resolveRemoteStageDir(env, sessionName);
|
|
1175
|
+
ensureRemoteStageDir(host, env, stageDir);
|
|
1176
|
+
|
|
1177
|
+
const localPromptFile = join(tmpdir(), `tfx-prompt-${randomUUID().slice(0, 8)}.md`);
|
|
1178
|
+
writeFileSync(localPromptFile, prompt, { encoding: "utf8" });
|
|
1179
|
+
const remotePromptFile = `${stageDir}/prompt.md`;
|
|
1180
|
+
uploadFileToRemote(host, localPromptFile, remotePromptFile);
|
|
1181
|
+
try { unlinkSync(localPromptFile); } catch { /* cleanup best-effort */ }
|
|
1182
|
+
|
|
1183
|
+
if (env.shell === "pwsh") {
|
|
1184
|
+
const remotePromptWin = remotePromptFile.replace(/\//g, "\\");
|
|
1185
|
+
const scriptContent = [
|
|
1186
|
+
`$ErrorActionPreference = 'SilentlyContinue'`,
|
|
1187
|
+
`$t = '${escapePwshSingleQuoted(remotePromptWin)}'`,
|
|
1188
|
+
`$c = '${escapePwshSingleQuoted(env.claudePath)}'`,
|
|
1189
|
+
`$raw = Get-Content -Raw $t`,
|
|
1190
|
+
`Remove-Item -ErrorAction SilentlyContinue $t`,
|
|
1191
|
+
`Remove-Item -ErrorAction SilentlyContinue $MyInvocation.MyCommand.Definition`,
|
|
1192
|
+
`& $c ${permissionFlags} $raw`,
|
|
1193
|
+
`$trifluxExit = if ($null -ne $LASTEXITCODE) { [int]$LASTEXITCODE } else { 0 }`,
|
|
1194
|
+
`exit $trifluxExit`,
|
|
1195
|
+
].join("\n");
|
|
1196
|
+
const localScript = join(tmpdir(), `tfx-spawn-${randomUUID().slice(0, 8)}.ps1`);
|
|
1197
|
+
writeFileSync(localScript, scriptContent, { encoding: "utf8" });
|
|
1198
|
+
const remoteScript = `${stageDir}/launch.ps1`;
|
|
1199
|
+
uploadFileToRemote(host, localScript, remoteScript);
|
|
1200
|
+
try { unlinkSync(localScript); } catch { /* cleanup best-effort */ }
|
|
1201
|
+
|
|
1202
|
+
const remoteScriptWin = remoteScript.replace(/\//g, "\\");
|
|
1203
|
+
sendKeysToPane(paneId, `pwsh -NoProfile -File '${escapePwshSingleQuoted(remoteScriptWin)}'`);
|
|
1204
|
+
} else {
|
|
1205
|
+
sendKeysToPane(paneId, `${shellQuote(env.claudePath)} ${permissionFlags} < ${shellQuote(remotePromptFile)} && rm -f ${shellQuote(remotePromptFile)}; ${buildPosixExitTail()}`);
|
|
1206
|
+
}
|
|
1207
|
+
} else {
|
|
1208
|
+
const claudeCommand = buildRemoteClaudeCommand(env, permissionFlags);
|
|
1209
|
+
sendKeysToPane(paneId, claudeCommand);
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
startSpawnSessionCleanupWatcher(sessionName, paneId);
|
|
1213
|
+
openAttachTab(sessionName, `${host}:${slug}`);
|
|
1214
|
+
console.log(sessionName);
|
|
1215
|
+
} catch (err) {
|
|
1216
|
+
try { killPsmuxSession(sessionName); } catch {}
|
|
1217
|
+
throw err;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
function sendPromptToSession(sessionName, prompt) {
|
|
1222
|
+
requirePsmux();
|
|
1223
|
+
if (!psmuxSessionExists(sessionName)) {
|
|
1224
|
+
throw new Error(`psmux session not found: ${sessionName}`);
|
|
1225
|
+
}
|
|
1226
|
+
sendKeysToPane(`${sessionName}:0.0`, prompt);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
function attachSession(sessionName) {
|
|
1230
|
+
requirePsmux();
|
|
1231
|
+
if (!psmuxSessionExists(sessionName)) {
|
|
1232
|
+
throw new Error(`psmux session not found: ${sessionName}`);
|
|
1233
|
+
}
|
|
1234
|
+
openAttachTab(sessionName);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
function captureSession(sessionName, lines = 30) {
|
|
1238
|
+
requirePsmux();
|
|
1239
|
+
if (!psmuxSessionExists(sessionName)) {
|
|
1240
|
+
throw new Error(`psmux session not found: ${sessionName}`);
|
|
1241
|
+
}
|
|
1242
|
+
return capturePsmuxPane(`${sessionName}:0.0`, lines);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
async function waitForClaudeReady(sessionName, timeoutSec = 60) {
|
|
1246
|
+
requirePsmux();
|
|
1247
|
+
if (!psmuxSessionExists(sessionName)) {
|
|
1248
|
+
throw new Error(`psmux session not found: ${sessionName}`);
|
|
1249
|
+
}
|
|
1250
|
+
const paneId = `${sessionName}:0.0`;
|
|
1251
|
+
const readyPattern = /(\u276f|\u2795|>\s*$|bypass permissions)/;
|
|
1252
|
+
const deadline = Date.now() + timeoutSec * 1000;
|
|
1253
|
+
|
|
1254
|
+
while (Date.now() <= deadline) {
|
|
1255
|
+
const snapshot = capturePsmuxPane(paneId, 5);
|
|
1256
|
+
const lastLine = snapshot.split(/\r?\n/).filter((l) => l.trim()).at(-1) || "";
|
|
1257
|
+
if (readyPattern.test(lastLine)) {
|
|
1258
|
+
return true;
|
|
1259
|
+
}
|
|
1260
|
+
sleepMs(1000);
|
|
1261
|
+
}
|
|
1262
|
+
throw new Error(`claude ready wait timed out after ${timeoutSec}s for ${sessionName}`);
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
async function main() {
|
|
1266
|
+
const args = parseArgs(process.argv);
|
|
1267
|
+
|
|
1268
|
+
if (args.command === "watch-cleanup") {
|
|
1269
|
+
if (!args.sessionName) {
|
|
1270
|
+
console.error("--watch-cleanup requires a session name");
|
|
1271
|
+
process.exit(1);
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
const paneId = args.watchPane || `${args.sessionName}:0.0`;
|
|
1275
|
+
await runSpawnCleanupWatcher(args.sessionName, paneId, {
|
|
1276
|
+
graceMs: args.watchGraceMs,
|
|
1277
|
+
maxMs: args.watchMaxMs,
|
|
1278
|
+
pollMs: args.watchPollMs,
|
|
1279
|
+
});
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
if (args.command === "list") {
|
|
1284
|
+
console.log(listSpawnSessions().join("\n"));
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
if (args.command === "attach") {
|
|
1289
|
+
if (!args.sessionName) {
|
|
1290
|
+
console.error("--attach requires a session name");
|
|
1291
|
+
process.exit(1);
|
|
1292
|
+
}
|
|
1293
|
+
attachSession(args.sessionName);
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
if (args.command === "probe") {
|
|
1298
|
+
if (!args.probeHost) {
|
|
1299
|
+
console.error("--probe requires a host");
|
|
1300
|
+
process.exit(1);
|
|
1301
|
+
}
|
|
1302
|
+
console.log(JSON.stringify(probeRemoteEnv(args.probeHost, { force: true }), null, 2));
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
if (args.command === "capture") {
|
|
1307
|
+
if (!args.sessionName) {
|
|
1308
|
+
console.error("--capture requires a session name");
|
|
1309
|
+
process.exit(1);
|
|
1310
|
+
}
|
|
1311
|
+
console.log(captureSession(args.sessionName));
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
if (args.command === "wait") {
|
|
1316
|
+
if (!args.sessionName) {
|
|
1317
|
+
console.error("--wait requires a session name");
|
|
1318
|
+
process.exit(1);
|
|
1319
|
+
}
|
|
1320
|
+
await waitForClaudeReady(args.sessionName);
|
|
1321
|
+
console.log("ready");
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
const promptContext = buildPromptContext(args, {
|
|
1326
|
+
includeTransferFiles: Boolean(args.host),
|
|
1327
|
+
});
|
|
1328
|
+
const prompt = promptContext.prompt;
|
|
1329
|
+
|
|
1330
|
+
if (args.local && args.transferFiles.length > 0) {
|
|
1331
|
+
console.warn("[tfx] --transfer는 원격 모드에서만 사용 가능합니다 (--local에서는 무시됨)");
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
if (args.command === "send") {
|
|
1335
|
+
if (!args.sessionName) {
|
|
1336
|
+
console.error("--send requires a session name");
|
|
1337
|
+
process.exit(1);
|
|
1338
|
+
}
|
|
1339
|
+
if (!prompt) {
|
|
1340
|
+
console.error("--send requires a prompt or --handoff");
|
|
1341
|
+
process.exit(1);
|
|
1342
|
+
}
|
|
1343
|
+
sendPromptToSession(args.sessionName, prompt);
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
if (!args.local && !args.host) {
|
|
1348
|
+
console.log(usageText());
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
if (args.local) {
|
|
1353
|
+
spawnLocal(args, detectClaudePath(), prompt);
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
await spawnRemote(args, promptContext);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
const selfRun = process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1]);
|
|
1361
|
+
if (selfRun) {
|
|
1362
|
+
main().catch((error) => {
|
|
1363
|
+
console.error(error?.message || String(error));
|
|
1364
|
+
process.exit(1);
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
export const __remoteSpawnTest = {
|
|
1369
|
+
buildPromptContext,
|
|
1370
|
+
parseArgs,
|
|
1371
|
+
rewritePromptPaths,
|
|
1372
|
+
startSpawnExitWatcher,
|
|
1373
|
+
stageRemotePromptFiles,
|
|
1374
|
+
validateTransferCandidate,
|
|
1375
|
+
watchSpawnSessionExit,
|
|
1376
|
+
};
|