triflux 10.50.2 → 10.52.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/CLAUDE.md +57 -41
- package/README.ko.md +41 -45
- package/README.md +41 -45
- package/adapters/codex/skills/tfx-live/SKILL.md +25 -11
- package/bin/tfx-live.mjs +341 -104
- package/bin/triflux.mjs +147 -2624
- package/config/mcp-registry.json +3 -12
- package/cto/brief.mjs +0 -20
- package/cto/collect.mjs +1 -200
- package/cto/current.schema.json +2 -23
- package/cto/hygiene.mjs +2 -61
- package/cto/lake-root.mjs +0 -56
- package/cto/status.mjs +5 -187
- package/hooks/agy-session-hook.mjs +11 -56
- package/hooks/codex-session-hook.mjs +11 -101
- package/hub/bridge.mjs +65 -1250
- package/hub/cli-adapter-base.mjs +12 -90
- package/hub/codex-adapter.mjs +0 -1
- package/hub/codex-preflight.mjs +5 -28
- package/hub/lib/process-utils.mjs +1 -156
- package/hub/lib/spawn-trace.mjs +3 -2
- package/hub/lib/timeout-defaults.mjs +0 -1
- package/hub/team/agent-map.json +0 -1
- package/hub/team/agents-row-attach.py +120 -0
- package/hub/team/agents-row.mjs +200 -0
- package/hub/team/ansi.mjs +1 -35
- package/hub/team/backend.mjs +9 -145
- package/hub/team/claude-daemon-control.mjs +1 -2
- package/hub/team/claude-session-projection.mjs +5 -73
- package/hub/team/cli/commands/attach.mjs +1 -17
- package/hub/team/cli/commands/control.mjs +12 -38
- package/hub/team/cli/commands/debug.mjs +2 -39
- package/hub/team/cli/commands/focus.mjs +5 -31
- package/hub/team/cli/commands/interrupt.mjs +6 -25
- package/hub/team/cli/commands/kill.mjs +1 -30
- package/hub/team/cli/commands/list.mjs +0 -16
- package/hub/team/cli/commands/send.mjs +6 -26
- package/hub/team/cli/commands/start/index.mjs +42 -133
- package/hub/team/cli/commands/start/parse-args.mjs +15 -40
- package/hub/team/cli/commands/start/start-headless.mjs +138 -105
- package/hub/team/cli/commands/start/start-mux.mjs +2 -7
- package/hub/team/cli/commands/status.mjs +2 -57
- package/hub/team/cli/commands/stop.mjs +14 -19
- package/hub/team/cli/help.mjs +4 -5
- package/hub/team/cli/manifest.mjs +5 -1
- package/hub/team/cli/render.mjs +0 -12
- package/hub/team/cli/services/runtime-mode.mjs +27 -98
- package/hub/team/cli/services/state-store.mjs +5 -20
- package/hub/team/codex-queue.mjs +385 -0
- package/hub/team/handoff.mjs +3 -34
- package/hub/team/headless.mjs +108 -1296
- package/hub/team/orchestrator.mjs +4 -30
- package/hub/team/pane.mjs +6 -28
- package/hub/team/psmux.mjs +3 -34
- package/hub/team/remote-session.mjs +1 -2
- package/hub/team/session-context.mjs +1 -1
- package/hub/team/session.mjs +7 -91
- package/hub/team/tui-core.mjs +1 -1
- package/hub/team/tui-viewer.mjs +11 -58
- package/hub/team/tui.mjs +8 -306
- package/hub/team/uds-orchestrator.mjs +2 -580
- package/hub/team/wt-manager.mjs +2 -2
- package/hub/workers/factory.mjs +6 -84
- package/hub/workers/interface.mjs +1 -7
- package/hub/workers/lib/jsonrpc-core.mjs +11 -0
- package/hub/workers/lib/jsonrpc-ws-uds.mjs +5 -8
- package/hud/cli-policy.mjs +0 -22
- package/hud/constants.mjs +13 -26
- package/hud/hud-qos-status.mjs +42 -54
- package/hud/providers/gemini.mjs +125 -20
- package/hud/renderers.mjs +101 -122
- package/hud/utils.mjs +10 -6
- package/package.json +7 -12
- package/scripts/__tests__/mcp-guard-engine-http-headers.test.mjs +6 -6
- package/scripts/__tests__/mcp-guard-engine-policy-sync.test.mjs +1 -4
- package/scripts/__tests__/mcp-guard-engine-proactive-sync.test.mjs +16 -21
- package/scripts/__tests__/mcp-guard-engine-stdio-sync.test.mjs +1 -1
- package/scripts/__tests__/mcp-guard-engine-sync-http-headers.test.mjs +2 -2
- package/scripts/__tests__/mcp-guard-engine.test.mjs +56 -63
- package/scripts/__tests__/spawn-trace.test.mjs +16 -0
- package/scripts/check-codex-config-stable.mjs +4 -37
- package/scripts/claude-login-detect.mjs +0 -1
- package/scripts/claudemd-sync.mjs +1 -28
- package/scripts/lib/agent-route-policy.mjs +143 -62
- package/scripts/lib/cli-claude.mjs +0 -14
- package/scripts/lib/env-probe.mjs +2 -131
- package/scripts/lib/gemini-profiles.mjs +2 -69
- package/scripts/lib/legacy-mcp-cleanup.mjs +283 -0
- package/scripts/lib/machine-profile.mjs +3 -17
- package/scripts/lib/mcp-filter.mjs +109 -53
- package/scripts/lib/mcp-guard-engine.mjs +40 -98
- package/scripts/lib/mcp-health.mjs +2 -2
- package/scripts/lib/mcp-server-catalog.mjs +2 -2
- package/scripts/lib/psmux-info.mjs +11 -59
- package/scripts/lib/session-presence.mjs +2 -100
- package/scripts/lib/skill-template.mjs +0 -245
- package/scripts/pack.mjs +8 -83
- package/scripts/preflight-cache.mjs +5 -18
- package/scripts/release/check-packages-mirror.mjs +2 -8
- package/scripts/release/version-manifest.json +9 -1
- package/scripts/setup.mjs +100 -417
- package/scripts/test-lock.mjs +0 -1
- package/scripts/tfx-route-post.mjs +5 -69
- package/scripts/tfx-route-worker.mjs +4 -79
- package/scripts/tfx-route.sh +169 -1495
- package/scripts/wt-cli.mjs +1 -2
- package/skills/tfx-auto/SKILL.md +123 -741
- package/skills/tfx-auto/references/consensus-schema.md +161 -0
- package/skills/tfx-auto/references/tmux-observation.md +97 -0
- package/skills/tfx-doctor/SKILL.md +5 -6
- package/skills/tfx-lead/SKILL.md +195 -0
- package/skills/tfx-live/SKILL.md +25 -11
- package/skills/tfx-remote/SKILL.md +24 -87
- package/skills/tfx-research/SKILL.md +13 -20
- package/skills/tfx-review/SKILL.md +33 -35
- package/skills/tfx-setup/SKILL.md +1 -1
- package/skills/tfx-ship/SKILL.md +6 -27
- package/skills/tfx-wt/SKILL.md +34 -68
- package/bin/tfx-doctor-tui.mjs +0 -7
- package/bin/tfx-setup-tui.mjs +0 -7
- package/config/routing-policy.json +0 -124
- package/docs/assets/consensus-flow.svg +0 -125
- package/docs/assets/deep-vs-light.svg +0 -193
- package/docs/assets/demo-dark.gif +0 -0
- package/docs/assets/demo-light.gif +0 -0
- package/docs/assets/demo-psmux.gif +0 -0
- package/hub/adaptive-diagnostic.mjs +0 -350
- package/hub/adaptive-inject.mjs +0 -224
- package/hub/adaptive-memory.mjs +0 -277
- package/hub/adaptive.mjs +0 -172
- package/hub/assign-callbacks.mjs +0 -170
- package/hub/delegator/contracts.mjs +0 -38
- package/hub/delegator/index.mjs +0 -14
- package/hub/delegator/schema/delegator-tools.schema.json +0 -250
- package/hub/delegator/service.mjs +0 -336
- package/hub/delegator/tool-definitions.mjs +0 -35
- package/hub/dynamic-routing-engine.mjs +0 -511
- package/hub/hitl.mjs +0 -202
- package/hub/hub-lifecycle.mjs +0 -248
- package/hub/lib/known-errors.json +0 -72
- package/hub/lib/memory-store.mjs +0 -1001
- package/hub/lib/phase-manager.mjs +0 -493
- package/hub/lib/risk-tier.mjs +0 -53
- package/hub/lib/ssh-command.mjs +0 -231
- package/hub/lib/staged-file-detect.mjs +0 -43
- package/hub/lib/state-snapshot.mjs +0 -293
- package/hub/lib/tfx-route-args.mjs +0 -353
- package/hub/lib/trace-recorder.mjs +0 -153
- package/hub/lib/uuidv7.mjs +0 -45
- package/hub/memory-doctor.mjs +0 -640
- package/hub/middleware/quota-middleware.mjs +0 -70
- package/hub/middleware/request-logger.mjs +0 -191
- package/hub/paths.mjs +0 -24
- package/hub/pipe.mjs +0 -825
- package/hub/pipeline/index.mjs +0 -163
- package/hub/pipeline/state.mjs +0 -191
- package/hub/pipeline/transitions.mjs +0 -131
- package/hub/promote-penalties.mjs +0 -138
- package/hub/public/dashboard.html +0 -416
- package/hub/reflexion.mjs +0 -290
- package/hub/role-contract.mjs +0 -314
- package/hub/router.mjs +0 -2026
- package/hub/routing-snapshot.mjs +0 -261
- package/hub/schema.sql +0 -209
- package/hub/server.mjs +0 -3048
- package/hub/session-fingerprint.mjs +0 -418
- package/hub/state.mjs +0 -300
- package/hub/store-adapter.mjs +0 -182
- package/hub/store.mjs +0 -1736
- package/hub/team/build-worker-prompt.mjs +0 -92
- package/hub/team/check-mcp-hub.mjs +0 -49
- package/hub/team/claude-native-bridge.mjs +0 -987
- package/hub/team/cli/commands/start/start-in-process.mjs +0 -50
- package/hub/team/cli/commands/start/start-wt.mjs +0 -86
- package/hub/team/cli/services/hub-client.mjs +0 -264
- package/hub/team/cli/services/native-control.mjs +0 -145
- package/hub/team/codex-review.mjs +0 -520
- package/hub/team/conductor-mesh-bridge.mjs +0 -122
- package/hub/team/conductor-registry.mjs +0 -121
- package/hub/team/conductor.mjs +0 -1529
- package/hub/team/consensus-meta.mjs +0 -176
- package/hub/team/cto-auto-collect.mjs +0 -115
- package/hub/team/cto-pull.mjs +0 -31
- package/hub/team/daemon-pty-tmux-bridge.mjs +0 -146
- package/hub/team/dashboard.mjs +0 -322
- package/hub/team/event-log.mjs +0 -84
- package/hub/team/execution-mode.mjs +0 -324
- package/hub/team/extract-completion-payload.mjs +0 -110
- package/hub/team/headless-bridge-session.mjs +0 -17
- package/hub/team/health-probe.mjs +0 -435
- package/hub/team/interactive-native-launcher.mjs +0 -119
- package/hub/team/interactive-native-worker.mjs +0 -80
- package/hub/team/interactive-tui-transport.mjs +0 -479
- package/hub/team/launcher-template.mjs +0 -144
- package/hub/team/lead-control.mjs +0 -191
- package/hub/team/mcp-selector.mjs +0 -145
- package/hub/team/native-supervisor.mjs +0 -460
- package/hub/team/native.mjs +0 -727
- package/hub/team/recovery-store.mjs +0 -149
- package/hub/team/remote-probe.mjs +0 -292
- package/hub/team/remote-watcher.mjs +0 -506
- package/hub/team/runtime-strategy.mjs +0 -200
- package/hub/team/schemas/worker-completion.json +0 -39
- package/hub/team/sentinel-capture.mjs +0 -127
- package/hub/team/session-sync.mjs +0 -316
- package/hub/team/swarm-cli.mjs +0 -390
- package/hub/team/swarm-hypervisor.mjs +0 -2206
- package/hub/team/swarm-intent.mjs +0 -216
- package/hub/team/swarm-locks.mjs +0 -335
- package/hub/team/swarm-planner.mjs +0 -371
- package/hub/team/swarm-preflight.mjs +0 -366
- package/hub/team/swarm-reconciler.mjs +0 -242
- package/hub/team/synapse-cli.mjs +0 -458
- package/hub/team/synapse-http.mjs +0 -311
- package/hub/team/synapse-registry.mjs +0 -618
- package/hub/team/tui-remote-adapter.mjs +0 -377
- package/hub/team/tui-synapse.mjs +0 -223
- package/hub/team/worker-completion-validator.mjs +0 -79
- package/hub/team/worker-sandbox.mjs +0 -125
- package/hub/team/worktree-lifecycle.mjs +0 -639
- package/hub/team/wt-templates.json +0 -43
- package/hub/team-bridge.mjs +0 -27
- package/hub/tools.mjs +0 -900
- package/hub/workers/antigravity-route-worker.mjs +0 -179
- package/hub/workers/codex-app-server-worker.mjs +0 -1067
- package/hub/workers/delegator-mcp.mjs +0 -1098
- package/hub/workers/lib/jsonrpc-stdio.mjs +0 -204
- package/mesh/index.mjs +0 -69
- package/mesh/mesh-budget.mjs +0 -137
- package/mesh/mesh-heartbeat.mjs +0 -100
- package/mesh/mesh-protocol.mjs +0 -96
- package/mesh/mesh-queue.mjs +0 -163
- package/mesh/mesh-registry.mjs +0 -78
- package/mesh/mesh-router.mjs +0 -77
- package/scripts/__tests__/skill-surface.test.mjs +0 -93
- package/scripts/cache-doctor.mjs +0 -156
- package/scripts/cache-warmup.mjs +0 -630
- package/scripts/completions/tfx.bash +0 -74
- package/scripts/completions/tfx.fish +0 -57
- package/scripts/completions/tfx.zsh +0 -98
- package/scripts/demo.mjs +0 -197
- package/scripts/doctor-dynamic-routing.mjs +0 -87
- package/scripts/hub-ensure.mjs +0 -842
- package/scripts/hub-watchdog.mjs +0 -90
- package/scripts/lib/claudemd-scanner.mjs +0 -223
- package/scripts/lib/context.mjs +0 -67
- package/scripts/lib/dynamic-route-cli.mjs +0 -107
- package/scripts/lib/handoff.mjs +0 -187
- package/scripts/lib/logger.mjs +0 -122
- package/scripts/lib/stealth-fetch.mjs +0 -176
- package/scripts/notion-read.mjs +0 -686
- package/scripts/snapshot-codex-state.mjs +0 -37
- package/scripts/snapshot-gemini-state.mjs +0 -37
- package/scripts/sync-codex-auth.mjs +0 -71
- package/scripts/sync-hub-mcp-settings.mjs +0 -697
- package/skills/_templates/base.md +0 -4
- package/skills/_templates/deep.md +0 -6
- package/skills/shared/telemetry-segment.md +0 -6
- package/tui/core.mjs +0 -266
- package/tui/doctor.mjs +0 -375
- package/tui/monitor-data.mjs +0 -152
- package/tui/monitor.mjs +0 -318
- package/tui/setup.mjs +0 -506
package/bin/triflux.mjs
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { setTimeout as delay } from "node:timers/promises";
|
|
3
|
-
import { execFileSync, execSync
|
|
3
|
+
import { execFileSync, execSync } from "child_process";
|
|
4
4
|
// triflux CLI — setup, doctor, version
|
|
5
5
|
import {
|
|
6
|
-
appendFileSync,
|
|
7
6
|
chmodSync,
|
|
8
|
-
closeSync,
|
|
9
7
|
copyFileSync,
|
|
10
8
|
existsSync,
|
|
11
9
|
mkdirSync,
|
|
12
|
-
openSync,
|
|
13
10
|
readdirSync,
|
|
14
11
|
readFileSync,
|
|
15
12
|
readSync,
|
|
@@ -22,18 +19,11 @@ import {
|
|
|
22
19
|
import { homedir } from "os";
|
|
23
20
|
import { dirname, join, resolve } from "path";
|
|
24
21
|
import { fileURLToPath } from "url";
|
|
25
|
-
import { loadDelegatorSchemaBundle } from "../hub/delegator/tool-definitions.mjs";
|
|
26
22
|
import { inspectClaudeRuntimeFlags } from "../hub/diagnostics/claude-runtime-flags.mjs";
|
|
27
23
|
import {
|
|
28
24
|
checkNetworkAvailability,
|
|
29
25
|
validateRuntimeCachePaths,
|
|
30
26
|
} from "../hub/lib/cache-guard.mjs";
|
|
31
|
-
import {
|
|
32
|
-
decideDispatchMode,
|
|
33
|
-
parseArgs as parseRouteArgs,
|
|
34
|
-
} from "../hub/lib/tfx-route-args.mjs";
|
|
35
|
-
import { getPipelineStateDbPath } from "../hub/pipeline/state.mjs";
|
|
36
|
-
import { getVersionHash } from "../hub/state.mjs";
|
|
37
27
|
import {
|
|
38
28
|
detectMultiplexer,
|
|
39
29
|
getSessionAttachedCount,
|
|
@@ -46,9 +36,12 @@ import {
|
|
|
46
36
|
inspectMacTimeoutDependency,
|
|
47
37
|
} from "../scripts/lib/doctor-env-checks.mjs";
|
|
48
38
|
import { ensureGeminiProfiles } from "../scripts/lib/gemini-profiles.mjs";
|
|
49
|
-
import { serializeHandoff } from "../scripts/lib/handoff.mjs";
|
|
50
39
|
import { cleanupLegacyHooks } from "../scripts/lib/legacy-hook-cleanup.mjs";
|
|
51
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
cleanupLegacyMcp,
|
|
42
|
+
cleanupTfxHub,
|
|
43
|
+
findProjectHubEntries,
|
|
44
|
+
} from "../scripts/lib/legacy-mcp-cleanup.mjs";
|
|
52
45
|
import {
|
|
53
46
|
addRegistryServer,
|
|
54
47
|
createDefaultRegistry,
|
|
@@ -65,14 +58,11 @@ import {
|
|
|
65
58
|
formatPsmuxUpdateGuidance,
|
|
66
59
|
probePsmuxSupport,
|
|
67
60
|
} from "../scripts/lib/psmux-info.mjs";
|
|
68
|
-
import { main as stealthFetchMain } from "../scripts/lib/stealth-fetch.mjs";
|
|
69
61
|
import {
|
|
70
62
|
applyStatusLine,
|
|
71
|
-
buildWindowsHubAutostartCommand,
|
|
72
|
-
ensureCodexHubServerConfig,
|
|
73
63
|
ensureCodexProfiles,
|
|
64
|
+
ensureTrifluxMods,
|
|
74
65
|
getVersion,
|
|
75
|
-
getWindowsHubAutostartStatus,
|
|
76
66
|
isLocalDevSkillDir,
|
|
77
67
|
isSkillSupportedOnPlatform,
|
|
78
68
|
LEGACY_CODEX_MODELS,
|
|
@@ -108,22 +98,16 @@ const GREEN_BRIGHT = "\x1b[38;5;82m";
|
|
|
108
98
|
const RED_BRIGHT = "\x1b[38;5;196m";
|
|
109
99
|
|
|
110
100
|
// ── 브랜드 요소 ──
|
|
111
|
-
const _BRAND = `${AMBER}${BOLD}triflux${RESET}`;
|
|
112
101
|
const VER = `${DIM}v${PKG.version}${RESET}`;
|
|
113
102
|
const LINE = `${GRAY}${"─".repeat(48)}${RESET}`;
|
|
114
|
-
const _DOT = `${GRAY}·${RESET}`;
|
|
115
103
|
const STALE_TEAM_MAX_AGE_SEC = 3600;
|
|
116
104
|
const DEFAULT_TMUX_CLEANUP_PREFIX = "tfx-*";
|
|
117
105
|
const DEFAULT_TMUX_CLEANUP_AGE_MIN = 60;
|
|
118
106
|
const ANSI_PATTERN = /\x1B\[[0-?]*[ -/]*[@-~]/g;
|
|
119
|
-
const HUB_DEFAULT_PORT = 27888;
|
|
120
|
-
const DOCTOR_HUB_PID_FILE = join(CLAUDE_DIR, "cache", "tfx-hub", "hub.pid");
|
|
121
107
|
|
|
122
|
-
const _EXIT_SUCCESS = 0;
|
|
123
108
|
const EXIT_ERROR = 1;
|
|
124
109
|
const EXIT_ARG_ERROR = 2;
|
|
125
110
|
const EXIT_CLI_MISSING = 3;
|
|
126
|
-
const EXIT_HUB_ERROR = 4;
|
|
127
111
|
const EXIT_CONFIG_ERROR = 5;
|
|
128
112
|
|
|
129
113
|
const RAW_ARGS = process.argv.slice(2);
|
|
@@ -131,64 +115,25 @@ const JSON_OUTPUT = RAW_ARGS.includes("--json");
|
|
|
131
115
|
const NORMALIZED_ARGS = RAW_ARGS.filter((arg) => arg !== "--json");
|
|
132
116
|
|
|
133
117
|
const CLI_COMMAND_SCHEMAS = Object.freeze({
|
|
134
|
-
auto: {
|
|
135
|
-
usage:
|
|
136
|
-
"tfx auto [--cli auto|codex|antigravity|claude] [--mode quick|deep|consensus|live] [--rounds N] [--parallel 1|N|swarm] [--json]",
|
|
137
|
-
description:
|
|
138
|
-
"tfx-auto 라우팅 결정을 CLI에서 미리보기/직렬화 (실행 skill front door와 같은 flag surface)",
|
|
139
|
-
options: [
|
|
140
|
-
{
|
|
141
|
-
name: "--cli <name>",
|
|
142
|
-
type: "string",
|
|
143
|
-
description: "실행 lane 강제: auto|codex|antigravity|claude",
|
|
144
|
-
},
|
|
145
|
-
{
|
|
146
|
-
name: "--mode <name>",
|
|
147
|
-
type: "string",
|
|
148
|
-
description: "라우팅 모드: quick|deep|consensus|live",
|
|
149
|
-
},
|
|
150
|
-
{
|
|
151
|
-
name: "--rounds <N>",
|
|
152
|
-
type: "number",
|
|
153
|
-
description: "live peer 왕복 횟수 (기본 4, --mode live 전용)",
|
|
154
|
-
},
|
|
155
|
-
{
|
|
156
|
-
name: "--parallel <1|N|swarm>",
|
|
157
|
-
type: "string",
|
|
158
|
-
description: "단일/로컬 병렬/PRD swarm 라우팅 힌트",
|
|
159
|
-
},
|
|
160
|
-
{
|
|
161
|
-
name: "--json",
|
|
162
|
-
type: "boolean",
|
|
163
|
-
description: "parse 결과와 dispatch 결정을 JSON으로 출력",
|
|
164
|
-
},
|
|
165
|
-
],
|
|
166
|
-
},
|
|
167
118
|
setup: {
|
|
168
|
-
usage: "tfx setup [--dry-run] [--
|
|
119
|
+
usage: "tfx setup [--dry-run] [--mods]",
|
|
169
120
|
description: "파일 동기화 + HUD/MCP 설정",
|
|
170
121
|
options: [
|
|
171
122
|
{
|
|
172
|
-
name: "--
|
|
123
|
+
name: "--mods",
|
|
173
124
|
type: "boolean",
|
|
174
|
-
description: "
|
|
125
|
+
description: "Claude Code mods 설치 (2.1.287 이상)",
|
|
175
126
|
},
|
|
176
127
|
{
|
|
177
|
-
name: "--
|
|
128
|
+
name: "--dry-run",
|
|
178
129
|
type: "boolean",
|
|
179
|
-
description:
|
|
180
|
-
"Windows 로그인 시 tfx-hub를 보장하는 Task Scheduler 항목 등록",
|
|
130
|
+
description: "실제 변경 없이 예정 작업을 JSON으로 출력",
|
|
181
131
|
},
|
|
182
132
|
],
|
|
183
133
|
},
|
|
184
|
-
"stealth-fetch": {
|
|
185
|
-
usage: "tfx stealth-fetch <url>",
|
|
186
|
-
description:
|
|
187
|
-
"cloakbrowser 기반 단일 URL fetch (http/https only, JSON stdout)",
|
|
188
|
-
},
|
|
189
134
|
doctor: {
|
|
190
135
|
usage:
|
|
191
|
-
"tfx doctor [--fix] [--reset] [--audit] [--diagnose] [--purge-logs] [--
|
|
136
|
+
"tfx doctor [--fix] [--reset] [--audit] [--diagnose] [--purge-logs] [--cleanup-stale-tmux --prefix tfx-* --age-min N --dry-run|--apply] [--json]",
|
|
192
137
|
description: "설치 상태 진단 및 자동 복구",
|
|
193
138
|
options: [
|
|
194
139
|
{
|
|
@@ -218,18 +163,6 @@ const CLI_COMMAND_SCHEMAS = Object.freeze({
|
|
|
218
163
|
description:
|
|
219
164
|
"--fix 와 함께 사용. cli-issues.jsonl 에서 7일 초과 항목 물리 삭제 (#144)",
|
|
220
165
|
},
|
|
221
|
-
{
|
|
222
|
-
name: "--dynamic-routing",
|
|
223
|
-
type: "boolean",
|
|
224
|
-
description:
|
|
225
|
-
"Phase 1 dynamic routing 상태 진단 (env / policy / snapshot cache / preview decision)",
|
|
226
|
-
},
|
|
227
|
-
{
|
|
228
|
-
name: "--cleanup-stale-hubs",
|
|
229
|
-
type: "boolean",
|
|
230
|
-
description:
|
|
231
|
-
"PPID=1 hub/server.mjs 후보를 보고하고 opt-in 정리 모드를 활성화",
|
|
232
|
-
},
|
|
233
166
|
{
|
|
234
167
|
name: "--cleanup-stale-tmux",
|
|
235
168
|
type: "boolean",
|
|
@@ -249,14 +182,12 @@ const CLI_COMMAND_SCHEMAS = Object.freeze({
|
|
|
249
182
|
{
|
|
250
183
|
name: "--dry-run",
|
|
251
184
|
type: "boolean",
|
|
252
|
-
description:
|
|
253
|
-
"--cleanup-stale-hubs/--cleanup-stale-tmux 와 함께 사용. 정리 후보만 표시",
|
|
185
|
+
description: "--cleanup-stale-tmux 와 함께 사용. 정리 후보만 표시",
|
|
254
186
|
},
|
|
255
187
|
{
|
|
256
188
|
name: "--apply",
|
|
257
189
|
type: "boolean",
|
|
258
|
-
description:
|
|
259
|
-
"--cleanup-stale-hubs/--cleanup-stale-tmux 와 함께 사용. stale 대상 종료",
|
|
190
|
+
description: "--cleanup-stale-tmux 와 함께 사용. stale 대상 종료",
|
|
260
191
|
},
|
|
261
192
|
{
|
|
262
193
|
name: "--json",
|
|
@@ -276,38 +207,6 @@ const CLI_COMMAND_SCHEMAS = Object.freeze({
|
|
|
276
207
|
},
|
|
277
208
|
],
|
|
278
209
|
},
|
|
279
|
-
handoff: {
|
|
280
|
-
usage:
|
|
281
|
-
"tfx handoff [--target local|remote] [--decision <text>] [--decision-file <path>] [--output <path>] [--json]",
|
|
282
|
-
description: "현재 작업 컨텍스트를 세션 핸드오프 프롬프트로 직렬화",
|
|
283
|
-
options: [
|
|
284
|
-
{
|
|
285
|
-
name: "--target",
|
|
286
|
-
type: "string",
|
|
287
|
-
description: "주입 대상 (local|remote, 기본값 remote)",
|
|
288
|
-
},
|
|
289
|
-
{
|
|
290
|
-
name: "--decision",
|
|
291
|
-
type: "string",
|
|
292
|
-
description: "핸드오프 결정사항 (반복 지정 가능)",
|
|
293
|
-
},
|
|
294
|
-
{
|
|
295
|
-
name: "--decision-file",
|
|
296
|
-
type: "string",
|
|
297
|
-
description: "결정사항 파일 (라인/불릿 단위)",
|
|
298
|
-
},
|
|
299
|
-
{
|
|
300
|
-
name: "--output",
|
|
301
|
-
type: "string",
|
|
302
|
-
description: "생성한 핸드오프 프롬프트 저장 경로",
|
|
303
|
-
},
|
|
304
|
-
{
|
|
305
|
-
name: "--json",
|
|
306
|
-
type: "boolean",
|
|
307
|
-
description: "핸드오프 결과를 JSON으로 출력",
|
|
308
|
-
},
|
|
309
|
-
],
|
|
310
|
-
},
|
|
311
210
|
list: {
|
|
312
211
|
usage: "tfx list [--json]",
|
|
313
212
|
description: "패키지 스킬과 사용자 스킬 목록 표시",
|
|
@@ -319,17 +218,6 @@ const CLI_COMMAND_SCHEMAS = Object.freeze({
|
|
|
319
218
|
},
|
|
320
219
|
],
|
|
321
220
|
},
|
|
322
|
-
schema: {
|
|
323
|
-
usage: "tfx schema [command-or-tool]",
|
|
324
|
-
description: "CLI 커맨드 파라미터와 Hub delegator schema 번들 출력",
|
|
325
|
-
options: [
|
|
326
|
-
{
|
|
327
|
-
name: "command-or-tool",
|
|
328
|
-
type: "string",
|
|
329
|
-
description: "예: doctor, setup, delegate, delegate-reply, status",
|
|
330
|
-
},
|
|
331
|
-
],
|
|
332
|
-
},
|
|
333
221
|
mcp: {
|
|
334
222
|
usage: "tfx mcp <list|sync|add|remove> [--json]",
|
|
335
223
|
description: "MCP registry 상태 확인 및 중앙 동기화",
|
|
@@ -378,103 +266,6 @@ const CLI_COMMAND_SCHEMAS = Object.freeze({
|
|
|
378
266
|
},
|
|
379
267
|
},
|
|
380
268
|
},
|
|
381
|
-
synapse: {
|
|
382
|
-
usage: "tfx synapse status [--json] [--registry <path>]",
|
|
383
|
-
description: "Synapse v1 세션 레지스트리 조회 (활성 스웜 세션 목록)",
|
|
384
|
-
subcommands: {
|
|
385
|
-
status: "활성 세션 테이블 표시 (host/branch/dirty/state/task)",
|
|
386
|
-
},
|
|
387
|
-
options: [
|
|
388
|
-
{
|
|
389
|
-
name: "--json",
|
|
390
|
-
type: "boolean",
|
|
391
|
-
description: "구조화된 JSON 출력",
|
|
392
|
-
},
|
|
393
|
-
{
|
|
394
|
-
name: "--registry",
|
|
395
|
-
type: "string",
|
|
396
|
-
description: "registry 파일 경로 오버라이드",
|
|
397
|
-
},
|
|
398
|
-
],
|
|
399
|
-
},
|
|
400
|
-
swarm: {
|
|
401
|
-
usage:
|
|
402
|
-
"tfx swarm [run|preflight] <prd-path> [--dry-run|--json|--filter <shard>]",
|
|
403
|
-
description: "PRD 기반 멀티모델 x 멀티기기 스웜 실행 (#93)",
|
|
404
|
-
subcommands: {
|
|
405
|
-
run: "tfx swarm run <prd-path> — verb 명시 실행 (기본 경로와 동일)",
|
|
406
|
-
preflight:
|
|
407
|
-
"tfx swarm preflight <prd-path> [--json] — 실행 전 go/no-go 리포트 출력",
|
|
408
|
-
plan: "tfx swarm plan <prd-path> [--json] — 실행 없이 계획만 출력",
|
|
409
|
-
list: "tfx swarm list [--json] — 활성 스웜 세션 조회 (synapse status)",
|
|
410
|
-
status: "tfx swarm status [--json] — list alias",
|
|
411
|
-
},
|
|
412
|
-
options: [
|
|
413
|
-
{
|
|
414
|
-
name: "--dry-run",
|
|
415
|
-
type: "boolean",
|
|
416
|
-
description: "PRD 분석만 수행, shard를 실행하지 않음",
|
|
417
|
-
},
|
|
418
|
-
{
|
|
419
|
-
name: "--json",
|
|
420
|
-
type: "boolean",
|
|
421
|
-
description: "구조화된 JSON 출력",
|
|
422
|
-
},
|
|
423
|
-
{
|
|
424
|
-
name: "--filter",
|
|
425
|
-
type: "string",
|
|
426
|
-
description: "특정 shard만 실행 (이름 매칭)",
|
|
427
|
-
},
|
|
428
|
-
{
|
|
429
|
-
name: "--max-restarts",
|
|
430
|
-
type: "number",
|
|
431
|
-
description: "shard별 최대 재시작 횟수 (기본 2)",
|
|
432
|
-
},
|
|
433
|
-
{
|
|
434
|
-
name: "--logs-dir",
|
|
435
|
-
type: "string",
|
|
436
|
-
description: "이벤트 로그 출력 디렉토리 오버라이드",
|
|
437
|
-
},
|
|
438
|
-
],
|
|
439
|
-
},
|
|
440
|
-
why: {
|
|
441
|
-
usage: "tfx why <path> [--json]",
|
|
442
|
-
description: "해당 경로의 마지막 커밋에서 X-Intent 트레일러 추출",
|
|
443
|
-
options: [
|
|
444
|
-
{
|
|
445
|
-
name: "path",
|
|
446
|
-
type: "string",
|
|
447
|
-
description: "intent를 조회할 파일 경로",
|
|
448
|
-
},
|
|
449
|
-
{
|
|
450
|
-
name: "--json",
|
|
451
|
-
type: "boolean",
|
|
452
|
-
description: "구조화된 JSON 출력",
|
|
453
|
-
},
|
|
454
|
-
],
|
|
455
|
-
},
|
|
456
|
-
hub: {
|
|
457
|
-
usage: "tfx hub <start|stop|status|ensure> [--port N] [--json]",
|
|
458
|
-
description: "tfx-hub 프로세스 제어",
|
|
459
|
-
subcommands: {
|
|
460
|
-
start: { usage: "tfx hub start [--port N]" },
|
|
461
|
-
stop: { usage: "tfx hub stop" },
|
|
462
|
-
ensure: {
|
|
463
|
-
usage: "tfx hub ensure [--port N] [--json]",
|
|
464
|
-
description: "헬스체크 + 자동 시작 (idempotent)",
|
|
465
|
-
},
|
|
466
|
-
status: {
|
|
467
|
-
usage: "tfx hub status [--json]",
|
|
468
|
-
options: [
|
|
469
|
-
{
|
|
470
|
-
name: "--json",
|
|
471
|
-
type: "boolean",
|
|
472
|
-
description: "허브 상태를 JSON으로 출력",
|
|
473
|
-
},
|
|
474
|
-
],
|
|
475
|
-
},
|
|
476
|
-
},
|
|
477
|
-
},
|
|
478
269
|
cto: {
|
|
479
270
|
usage: "tfx cto <collect|status|hygiene> [options]",
|
|
480
271
|
description: "repo-local authority layer console",
|
|
@@ -491,9 +282,9 @@ const CLI_COMMAND_SCHEMAS = Object.freeze({
|
|
|
491
282
|
description: "멀티-CLI 팀 모드",
|
|
492
283
|
options: [
|
|
493
284
|
{
|
|
494
|
-
name: "--
|
|
495
|
-
type: "
|
|
496
|
-
description: "
|
|
285
|
+
name: "--teammate-mode",
|
|
286
|
+
type: "string",
|
|
287
|
+
description: "실행 모드: auto|headless|tmux|psmux (Windows)",
|
|
497
288
|
},
|
|
498
289
|
{
|
|
499
290
|
name: "--no-dashboard",
|
|
@@ -537,74 +328,6 @@ const CLI_COMMAND_SCHEMAS = Object.freeze({
|
|
|
537
328
|
},
|
|
538
329
|
],
|
|
539
330
|
},
|
|
540
|
-
"codex-team": {
|
|
541
|
-
usage:
|
|
542
|
-
"tfx codex-team [status|debug|send|attach|stop|<task>] [--layout 1xN|Nx1] [--json]",
|
|
543
|
-
description:
|
|
544
|
-
"Codex lead + Codex workers 기본값으로 tfx multi 팀 모드를 시작/제어",
|
|
545
|
-
subcommands: {
|
|
546
|
-
status: "현재 Codex team 상태 확인",
|
|
547
|
-
debug: "최근 로그/상태 진단 출력",
|
|
548
|
-
send: "워커에게 메시지 전송: tfx codex-team send <N> <msg>",
|
|
549
|
-
attach: "팀 pane/session attach",
|
|
550
|
-
stop: "팀 세션 정리",
|
|
551
|
-
},
|
|
552
|
-
options: [
|
|
553
|
-
{
|
|
554
|
-
name: "--layout <shape>",
|
|
555
|
-
type: "string",
|
|
556
|
-
description: "기본 1xN. Nx1 등 team layout 전달",
|
|
557
|
-
},
|
|
558
|
-
{
|
|
559
|
-
name: "--json",
|
|
560
|
-
type: "boolean",
|
|
561
|
-
description: "지원 subcommand의 출력을 JSON으로 전환",
|
|
562
|
-
},
|
|
563
|
-
],
|
|
564
|
-
},
|
|
565
|
-
"notion-read": {
|
|
566
|
-
usage: "tfx notion-read <notion-url-or-page-id> [options]",
|
|
567
|
-
description: "Notion page/database를 markdown/JSON으로 읽기 (nr alias)",
|
|
568
|
-
aliases: ["nr"],
|
|
569
|
-
options: [
|
|
570
|
-
{
|
|
571
|
-
name: "--json",
|
|
572
|
-
type: "boolean",
|
|
573
|
-
description: "가능한 경우 구조화된 JSON 출력",
|
|
574
|
-
},
|
|
575
|
-
],
|
|
576
|
-
},
|
|
577
|
-
review: {
|
|
578
|
-
usage:
|
|
579
|
-
"tfx review [ref] [--base <ref>] [--timeout <seconds>] [--shard off|per-file] [--json]",
|
|
580
|
-
description: "Codex 기반 git diff review 실행",
|
|
581
|
-
options: [
|
|
582
|
-
{
|
|
583
|
-
name: "--base <ref>",
|
|
584
|
-
type: "string",
|
|
585
|
-
description: "비교 기준 ref",
|
|
586
|
-
},
|
|
587
|
-
{
|
|
588
|
-
name: "--timeout <seconds>",
|
|
589
|
-
type: "number",
|
|
590
|
-
description: "review 실행 timeout (기본 180)",
|
|
591
|
-
},
|
|
592
|
-
{
|
|
593
|
-
name: "--shard <mode>",
|
|
594
|
-
type: "string",
|
|
595
|
-
description: "off 또는 per-file",
|
|
596
|
-
},
|
|
597
|
-
{
|
|
598
|
-
name: "--json",
|
|
599
|
-
type: "boolean",
|
|
600
|
-
description: "review 결과 JSON 출력",
|
|
601
|
-
},
|
|
602
|
-
],
|
|
603
|
-
},
|
|
604
|
-
monitor: {
|
|
605
|
-
usage: "tfx monitor",
|
|
606
|
-
description: "터미널 TUI 모니터 실행",
|
|
607
|
-
},
|
|
608
331
|
});
|
|
609
332
|
|
|
610
333
|
// ── 유틸리티 ──
|
|
@@ -705,512 +428,84 @@ function createCliError(
|
|
|
705
428
|
return error;
|
|
706
429
|
}
|
|
707
430
|
|
|
708
|
-
function
|
|
709
|
-
|
|
710
|
-
|
|
431
|
+
function inferExitCode(error) {
|
|
432
|
+
if (Number.isInteger(error?.exitCode)) return error.exitCode;
|
|
433
|
+
if (error?.code === "ENOENT") return EXIT_CLI_MISSING;
|
|
434
|
+
return EXIT_ERROR;
|
|
711
435
|
}
|
|
712
436
|
|
|
713
|
-
function
|
|
714
|
-
|
|
715
|
-
if (
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
} catch (error) {
|
|
720
|
-
return error?.code === "EPERM";
|
|
721
|
-
}
|
|
437
|
+
function inferReason(error, exitCode) {
|
|
438
|
+
if (typeof error?.reason === "string" && error.reason) return error.reason;
|
|
439
|
+
if (exitCode === EXIT_ARG_ERROR) return "argError";
|
|
440
|
+
if (exitCode === EXIT_CLI_MISSING) return "cliMissing";
|
|
441
|
+
if (exitCode === EXIT_CONFIG_ERROR) return "configError";
|
|
442
|
+
return "error";
|
|
722
443
|
}
|
|
723
444
|
|
|
724
|
-
function
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
)
|
|
445
|
+
function inferFix(error, exitCode) {
|
|
446
|
+
if (typeof error?.fix === "string" && error.fix) return error.fix;
|
|
447
|
+
if (exitCode === EXIT_ARG_ERROR) return "tfx --help";
|
|
448
|
+
if (exitCode === EXIT_CLI_MISSING)
|
|
449
|
+
return "필수 CLI를 설치한 뒤 `tfx doctor`로 상태를 다시 확인하세요.";
|
|
450
|
+
if (exitCode === EXIT_CONFIG_ERROR)
|
|
451
|
+
return "설정 파일 JSON/TOML 문법을 수정한 뒤 다시 실행하세요.";
|
|
452
|
+
return null;
|
|
728
453
|
}
|
|
729
454
|
|
|
730
|
-
function
|
|
731
|
-
const
|
|
732
|
-
|
|
733
|
-
|
|
455
|
+
function handleFatalError(error, { json = false } = {}) {
|
|
456
|
+
const exitCode = inferExitCode(error);
|
|
457
|
+
const message = stripAnsi(error?.message || "알 수 없는 오류");
|
|
458
|
+
const reason = inferReason(error, exitCode);
|
|
459
|
+
const fix = inferFix(error, exitCode);
|
|
734
460
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
const rssKb = Number.parseInt(rssText, 10);
|
|
744
|
-
if (!Number.isFinite(pid) || !Number.isFinite(ppid)) continue;
|
|
745
|
-
if (ppid !== 1) continue;
|
|
746
|
-
if (!isHubServerCommand(command)) continue;
|
|
747
|
-
rows.push({
|
|
748
|
-
pid,
|
|
749
|
-
ppid,
|
|
750
|
-
rssKb: Number.isFinite(rssKb) ? rssKb : null,
|
|
751
|
-
uptime,
|
|
752
|
-
command: command.trim(),
|
|
461
|
+
if (json) {
|
|
462
|
+
printJson({
|
|
463
|
+
error: {
|
|
464
|
+
code: exitCode,
|
|
465
|
+
message,
|
|
466
|
+
reason,
|
|
467
|
+
...(fix ? { fix } : {}),
|
|
468
|
+
},
|
|
753
469
|
});
|
|
470
|
+
} else {
|
|
471
|
+
console.error(message);
|
|
472
|
+
if (fix) console.error(`fix: ${fix}`);
|
|
754
473
|
}
|
|
755
|
-
|
|
474
|
+
process.exit(exitCode);
|
|
756
475
|
}
|
|
757
476
|
|
|
758
|
-
function
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
windowsHide: true,
|
|
769
|
-
});
|
|
770
|
-
return parseDetachedHubProcessRows(output);
|
|
771
|
-
} catch {
|
|
772
|
-
return [];
|
|
477
|
+
function renderErrorMessage(message, fallback = "unknown error") {
|
|
478
|
+
if (typeof message === "string") {
|
|
479
|
+
const normalized = message.trim().toLowerCase();
|
|
480
|
+
if (
|
|
481
|
+
normalized.length > 0 &&
|
|
482
|
+
normalized !== "undefined" &&
|
|
483
|
+
normalized !== "null"
|
|
484
|
+
) {
|
|
485
|
+
return message.trim();
|
|
486
|
+
}
|
|
773
487
|
}
|
|
488
|
+
return fallback;
|
|
774
489
|
}
|
|
775
490
|
|
|
776
|
-
function
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
)
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
try {
|
|
795
|
-
const output = execFile(
|
|
796
|
-
"lsof",
|
|
797
|
-
["-nP", "-Pan", "-p", String(resolvedPid), "-iTCP", "-sTCP:LISTEN"],
|
|
798
|
-
{
|
|
799
|
-
encoding: "utf8",
|
|
800
|
-
timeout: 5000,
|
|
801
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
802
|
-
windowsHide: true,
|
|
803
|
-
},
|
|
804
|
-
);
|
|
805
|
-
return parseLsofListeningPorts(output);
|
|
806
|
-
} catch {
|
|
807
|
-
return [];
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
function queryEstablishedCountForPid(
|
|
812
|
-
pid,
|
|
813
|
-
{ platform = process.platform, execFile = execFileSync } = {},
|
|
814
|
-
) {
|
|
815
|
-
const resolvedPid = Number(pid);
|
|
816
|
-
if (!Number.isFinite(resolvedPid) || resolvedPid <= 0) return 0;
|
|
817
|
-
if (platform === "win32") return 0;
|
|
818
|
-
try {
|
|
819
|
-
const output = execFile(
|
|
820
|
-
"lsof",
|
|
821
|
-
["-nP", "-Pan", "-p", String(resolvedPid), "-iTCP", "-sTCP:ESTABLISHED"],
|
|
822
|
-
{
|
|
823
|
-
encoding: "utf8",
|
|
824
|
-
timeout: 5000,
|
|
825
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
826
|
-
windowsHide: true,
|
|
827
|
-
},
|
|
828
|
-
);
|
|
829
|
-
return Math.max(0, output.trim().split(/\r?\n/).filter(Boolean).length - 1);
|
|
830
|
-
} catch {
|
|
831
|
-
return 0;
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
function queryPidCommand(
|
|
836
|
-
pid,
|
|
837
|
-
{ platform = process.platform, execFile = execFileSync } = {},
|
|
838
|
-
) {
|
|
839
|
-
const resolvedPid = Number(pid);
|
|
840
|
-
if (!Number.isFinite(resolvedPid) || resolvedPid <= 0) return "";
|
|
841
|
-
try {
|
|
842
|
-
if (platform === "win32") return "";
|
|
843
|
-
return execFile("ps", ["-p", String(resolvedPid), "-o", "command="], {
|
|
844
|
-
encoding: "utf8",
|
|
845
|
-
timeout: 5000,
|
|
846
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
847
|
-
windowsHide: true,
|
|
848
|
-
}).trim();
|
|
849
|
-
} catch {
|
|
850
|
-
return "";
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
function queryListeningPidByPort(
|
|
855
|
-
port,
|
|
856
|
-
{ platform = process.platform, execFile = execFileSync } = {},
|
|
857
|
-
) {
|
|
858
|
-
const targetPort = parseHubPort(port);
|
|
859
|
-
if (!targetPort || platform === "win32") return null;
|
|
860
|
-
try {
|
|
861
|
-
const output = execFile(
|
|
862
|
-
"lsof",
|
|
863
|
-
["-nP", "-iTCP:" + targetPort, "-sTCP:LISTEN", "-t"],
|
|
864
|
-
{
|
|
865
|
-
encoding: "utf8",
|
|
866
|
-
timeout: 5000,
|
|
867
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
868
|
-
windowsHide: true,
|
|
869
|
-
},
|
|
870
|
-
);
|
|
871
|
-
const pid = Number.parseInt(output.trim().split(/\r?\n/)[0] ?? "", 10);
|
|
872
|
-
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
873
|
-
} catch {
|
|
874
|
-
return null;
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
async function fetchHubHealthForDoctor(
|
|
879
|
-
host,
|
|
880
|
-
port,
|
|
881
|
-
{ fetchImpl = fetch, timeoutMs = 1000 } = {},
|
|
882
|
-
) {
|
|
883
|
-
try {
|
|
884
|
-
const urlHost = String(host || "127.0.0.1").includes(":")
|
|
885
|
-
? `[${host}]`
|
|
886
|
-
: host || "127.0.0.1";
|
|
887
|
-
const response = await fetchImpl(`http://${urlHost}:${port}/health`, {
|
|
888
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
889
|
-
});
|
|
890
|
-
if (!response.ok) return { ok: false, version: null };
|
|
891
|
-
const body = await response.json().catch(() => null);
|
|
892
|
-
return {
|
|
893
|
-
ok: body?.ok === true,
|
|
894
|
-
version: typeof body?.version === "string" ? body.version : null,
|
|
895
|
-
raw: body,
|
|
896
|
-
};
|
|
897
|
-
} catch (error) {
|
|
898
|
-
return { ok: false, version: null, error };
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
function readHubPidInfo({
|
|
903
|
-
pidFilePath = DOCTOR_HUB_PID_FILE,
|
|
904
|
-
exists = existsSync,
|
|
905
|
-
readFile = readFileSync,
|
|
906
|
-
} = {}) {
|
|
907
|
-
if (!exists(pidFilePath)) return null;
|
|
908
|
-
try {
|
|
909
|
-
return JSON.parse(readFile(pidFilePath, "utf8"));
|
|
910
|
-
} catch {
|
|
911
|
-
return null;
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
async function resolveActiveHealthyHub({
|
|
916
|
-
expectedVersion = getVersionHash(),
|
|
917
|
-
pidFilePath = DOCTOR_HUB_PID_FILE,
|
|
918
|
-
exists = existsSync,
|
|
919
|
-
readFile = readFileSync,
|
|
920
|
-
killFn = process.kill,
|
|
921
|
-
platform = process.platform,
|
|
922
|
-
execFile = execFileSync,
|
|
923
|
-
fetchImpl = fetch,
|
|
924
|
-
} = {}) {
|
|
925
|
-
const info = readHubPidInfo({ pidFilePath, exists, readFile });
|
|
926
|
-
const pid = Number(info?.pid);
|
|
927
|
-
const port = parseHubPort(info?.port);
|
|
928
|
-
const host =
|
|
929
|
-
typeof info?.host === "string" && info.host.trim()
|
|
930
|
-
? info.host.trim()
|
|
931
|
-
: "127.0.0.1";
|
|
932
|
-
if (pid && port && isPidAliveForHub(pid, killFn)) {
|
|
933
|
-
const command = queryPidCommand(pid, { platform, execFile });
|
|
934
|
-
const health = await fetchHubHealthForDoctor(host, port, { fetchImpl });
|
|
935
|
-
if (
|
|
936
|
-
isHubServerCommand(command) &&
|
|
937
|
-
health.ok &&
|
|
938
|
-
health.version === expectedVersion
|
|
939
|
-
) {
|
|
940
|
-
return { pid, port, host, version: health.version, source: "pid-file" };
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
const defaultPid = queryListeningPidByPort(HUB_DEFAULT_PORT, {
|
|
945
|
-
platform,
|
|
946
|
-
execFile,
|
|
947
|
-
});
|
|
948
|
-
if (!defaultPid || !isPidAliveForHub(defaultPid, killFn)) return null;
|
|
949
|
-
const command = queryPidCommand(defaultPid, { platform, execFile });
|
|
950
|
-
if (!isHubServerCommand(command)) return null;
|
|
951
|
-
const health = await fetchHubHealthForDoctor("127.0.0.1", HUB_DEFAULT_PORT, {
|
|
952
|
-
fetchImpl,
|
|
953
|
-
});
|
|
954
|
-
if (!health.ok || health.version !== expectedVersion) return null;
|
|
955
|
-
return {
|
|
956
|
-
pid: defaultPid,
|
|
957
|
-
port: HUB_DEFAULT_PORT,
|
|
958
|
-
host: "127.0.0.1",
|
|
959
|
-
version: health.version,
|
|
960
|
-
source: "default-port",
|
|
961
|
-
};
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
export async function inspectDetachedHubProcesses({
|
|
965
|
-
expectedVersion = getVersionHash(),
|
|
966
|
-
pidFilePath = DOCTOR_HUB_PID_FILE,
|
|
967
|
-
exists = existsSync,
|
|
968
|
-
readFile = readFileSync,
|
|
969
|
-
killFn = process.kill,
|
|
970
|
-
platform = process.platform,
|
|
971
|
-
execFile = execFileSync,
|
|
972
|
-
fetchImpl = fetch,
|
|
973
|
-
} = {}) {
|
|
974
|
-
const rows = queryDetachedHubProcessRows({ platform, execFile });
|
|
975
|
-
const activeHealthy = await resolveActiveHealthyHub({
|
|
976
|
-
expectedVersion,
|
|
977
|
-
pidFilePath,
|
|
978
|
-
exists,
|
|
979
|
-
readFile,
|
|
980
|
-
killFn,
|
|
981
|
-
platform,
|
|
982
|
-
execFile,
|
|
983
|
-
fetchImpl,
|
|
984
|
-
});
|
|
985
|
-
|
|
986
|
-
const hubs = [];
|
|
987
|
-
for (const row of rows) {
|
|
988
|
-
const ports = queryListeningPortsForPid(row.pid, { platform, execFile });
|
|
989
|
-
const established = queryEstablishedCountForPid(row.pid, {
|
|
990
|
-
platform,
|
|
991
|
-
execFile,
|
|
992
|
-
});
|
|
993
|
-
let version = null;
|
|
994
|
-
let healthy = false;
|
|
995
|
-
for (const port of ports) {
|
|
996
|
-
const health = await fetchHubHealthForDoctor("127.0.0.1", port, {
|
|
997
|
-
fetchImpl,
|
|
998
|
-
});
|
|
999
|
-
if (!health.ok) continue;
|
|
1000
|
-
version = health.version;
|
|
1001
|
-
healthy = true;
|
|
1002
|
-
break;
|
|
1003
|
-
}
|
|
1004
|
-
const activeByPidFile = activeHealthy?.pid === row.pid;
|
|
1005
|
-
const activeByConnection = established >= 1;
|
|
1006
|
-
const isActiveHealthy = activeByPidFile || activeByConnection;
|
|
1007
|
-
hubs.push({
|
|
1008
|
-
...row,
|
|
1009
|
-
ports,
|
|
1010
|
-
established,
|
|
1011
|
-
version,
|
|
1012
|
-
healthy: healthy || activeByConnection,
|
|
1013
|
-
activeHealthy: isActiveHealthy,
|
|
1014
|
-
activeReason: activeByPidFile
|
|
1015
|
-
? "pid-file-health"
|
|
1016
|
-
: activeByConnection
|
|
1017
|
-
? "established-connection"
|
|
1018
|
-
: null,
|
|
1019
|
-
healthStatus: isActiveHealthy ? "healthy" : "stale",
|
|
1020
|
-
staleCandidate: !isActiveHealthy,
|
|
1021
|
-
});
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
return {
|
|
1025
|
-
expectedVersion,
|
|
1026
|
-
activeHealthy,
|
|
1027
|
-
hubs,
|
|
1028
|
-
staleCandidates: hubs.filter((hub) => hub.staleCandidate),
|
|
1029
|
-
};
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
async function waitForHubProcessExit(
|
|
1033
|
-
pid,
|
|
1034
|
-
{ killFn = process.kill, graceMs = 5000, pollMs = 100 } = {},
|
|
1035
|
-
) {
|
|
1036
|
-
const deadline = Date.now() + Math.max(0, graceMs);
|
|
1037
|
-
while (Date.now() <= deadline) {
|
|
1038
|
-
if (!isPidAliveForHub(pid, killFn)) return true;
|
|
1039
|
-
await delay(pollMs);
|
|
1040
|
-
}
|
|
1041
|
-
return !isPidAliveForHub(pid, killFn);
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
async function retireDetachedHubPid(
|
|
1045
|
-
pid,
|
|
1046
|
-
{ killFn = process.kill, graceMs = 5000, pollMs = 100 } = {},
|
|
1047
|
-
) {
|
|
1048
|
-
if (!isPidAliveForHub(pid, killFn)) return { ok: true, reason: "dead" };
|
|
1049
|
-
try {
|
|
1050
|
-
killFn(pid, "SIGTERM");
|
|
1051
|
-
} catch (error) {
|
|
1052
|
-
return { ok: false, reason: "sigterm_failed", error };
|
|
1053
|
-
}
|
|
1054
|
-
if (await waitForHubProcessExit(pid, { killFn, graceMs, pollMs })) {
|
|
1055
|
-
return { ok: true, reason: "sigterm" };
|
|
1056
|
-
}
|
|
1057
|
-
try {
|
|
1058
|
-
killFn(pid, "SIGKILL");
|
|
1059
|
-
} catch (error) {
|
|
1060
|
-
return { ok: false, reason: "sigkill_failed", error };
|
|
1061
|
-
}
|
|
1062
|
-
const exited = await waitForHubProcessExit(pid, {
|
|
1063
|
-
killFn,
|
|
1064
|
-
graceMs: 1000,
|
|
1065
|
-
pollMs,
|
|
1066
|
-
});
|
|
1067
|
-
return { ok: exited, reason: exited ? "sigkill" : "still_alive" };
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
export async function cleanupDetachedHubProcesses({
|
|
1071
|
-
hubs,
|
|
1072
|
-
activeHealthy,
|
|
1073
|
-
dryRun = true,
|
|
1074
|
-
apply = false,
|
|
1075
|
-
killFn = process.kill,
|
|
1076
|
-
graceMs = 5000,
|
|
1077
|
-
pollMs = 100,
|
|
1078
|
-
} = {}) {
|
|
1079
|
-
const results = [];
|
|
1080
|
-
for (const hub of hubs || []) {
|
|
1081
|
-
const classification =
|
|
1082
|
-
activeHealthy?.pid === hub.pid ||
|
|
1083
|
-
hub.activeHealthy ||
|
|
1084
|
-
Number(hub.established) >= 1
|
|
1085
|
-
? "healthy"
|
|
1086
|
-
: "stale";
|
|
1087
|
-
if (classification === "healthy") {
|
|
1088
|
-
results.push({
|
|
1089
|
-
pid: hub.pid,
|
|
1090
|
-
classification,
|
|
1091
|
-
action: "excluded-active",
|
|
1092
|
-
ok: true,
|
|
1093
|
-
hub,
|
|
1094
|
-
});
|
|
1095
|
-
continue;
|
|
1096
|
-
}
|
|
1097
|
-
if (!apply || dryRun) {
|
|
1098
|
-
results.push({
|
|
1099
|
-
pid: hub.pid,
|
|
1100
|
-
classification,
|
|
1101
|
-
action: "dry-run-skip",
|
|
1102
|
-
ok: true,
|
|
1103
|
-
hub,
|
|
1104
|
-
});
|
|
1105
|
-
continue;
|
|
1106
|
-
}
|
|
1107
|
-
const retired = await retireDetachedHubPid(hub.pid, {
|
|
1108
|
-
killFn,
|
|
1109
|
-
graceMs,
|
|
1110
|
-
pollMs,
|
|
1111
|
-
});
|
|
1112
|
-
results.push({
|
|
1113
|
-
pid: hub.pid,
|
|
1114
|
-
classification,
|
|
1115
|
-
action: retired.ok ? "retired" : "failed",
|
|
1116
|
-
ok: retired.ok,
|
|
1117
|
-
reason: retired.reason,
|
|
1118
|
-
hub,
|
|
1119
|
-
});
|
|
1120
|
-
}
|
|
1121
|
-
return {
|
|
1122
|
-
dryRun: !apply || dryRun,
|
|
1123
|
-
results,
|
|
1124
|
-
failed: results.filter((result) => result.ok === false).length,
|
|
1125
|
-
retired: results.filter((result) => result.action === "retired").length,
|
|
1126
|
-
skipped: results.filter((result) => result.action === "dry-run-skip")
|
|
1127
|
-
.length,
|
|
1128
|
-
excluded: results.filter((result) => result.action === "excluded-active")
|
|
1129
|
-
.length,
|
|
1130
|
-
};
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
function inferExitCode(error) {
|
|
1134
|
-
if (Number.isInteger(error?.exitCode)) return error.exitCode;
|
|
1135
|
-
if (error?.code === "ENOENT") return EXIT_CLI_MISSING;
|
|
1136
|
-
return EXIT_ERROR;
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
function inferReason(error, exitCode) {
|
|
1140
|
-
if (typeof error?.reason === "string" && error.reason) return error.reason;
|
|
1141
|
-
if (exitCode === EXIT_ARG_ERROR) return "argError";
|
|
1142
|
-
if (exitCode === EXIT_CLI_MISSING) return "cliMissing";
|
|
1143
|
-
if (exitCode === EXIT_HUB_ERROR) return "hubError";
|
|
1144
|
-
if (exitCode === EXIT_CONFIG_ERROR) return "configError";
|
|
1145
|
-
return "error";
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
function inferFix(error, exitCode) {
|
|
1149
|
-
if (typeof error?.fix === "string" && error.fix) return error.fix;
|
|
1150
|
-
if (exitCode === EXIT_ARG_ERROR) return "tfx --help";
|
|
1151
|
-
if (exitCode === EXIT_CLI_MISSING)
|
|
1152
|
-
return "필수 CLI를 설치한 뒤 `tfx doctor`로 상태를 다시 확인하세요.";
|
|
1153
|
-
if (exitCode === EXIT_HUB_ERROR)
|
|
1154
|
-
return "`tfx hub start`로 허브를 다시 시작하거나 설치 상태를 확인하세요.";
|
|
1155
|
-
if (exitCode === EXIT_CONFIG_ERROR)
|
|
1156
|
-
return "설정 파일 JSON/TOML 문법을 수정한 뒤 다시 실행하세요.";
|
|
1157
|
-
return null;
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
function handleFatalError(error, { json = false } = {}) {
|
|
1161
|
-
const exitCode = inferExitCode(error);
|
|
1162
|
-
const message = stripAnsi(error?.message || "알 수 없는 오류");
|
|
1163
|
-
const reason = inferReason(error, exitCode);
|
|
1164
|
-
const fix = inferFix(error, exitCode);
|
|
1165
|
-
|
|
1166
|
-
if (json) {
|
|
1167
|
-
printJson({
|
|
1168
|
-
error: {
|
|
1169
|
-
code: exitCode,
|
|
1170
|
-
message,
|
|
1171
|
-
reason,
|
|
1172
|
-
...(fix ? { fix } : {}),
|
|
1173
|
-
},
|
|
1174
|
-
});
|
|
1175
|
-
} else {
|
|
1176
|
-
console.error(message);
|
|
1177
|
-
if (fix) console.error(`fix: ${fix}`);
|
|
1178
|
-
}
|
|
1179
|
-
process.exit(exitCode);
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
function renderErrorMessage(message, fallback = "unknown error") {
|
|
1183
|
-
if (typeof message === "string") {
|
|
1184
|
-
const normalized = message.trim().toLowerCase();
|
|
1185
|
-
if (
|
|
1186
|
-
normalized.length > 0 &&
|
|
1187
|
-
normalized !== "undefined" &&
|
|
1188
|
-
normalized !== "null"
|
|
1189
|
-
) {
|
|
1190
|
-
return message.trim();
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
return fallback;
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
function which(cmd) {
|
|
1197
|
-
try {
|
|
1198
|
-
const result =
|
|
1199
|
-
process.platform === "win32"
|
|
1200
|
-
? execFileSync("where", [cmd], {
|
|
1201
|
-
encoding: "utf8",
|
|
1202
|
-
timeout: 5000,
|
|
1203
|
-
stdio: ["pipe", "pipe", "ignore"],
|
|
1204
|
-
windowsHide: true,
|
|
1205
|
-
})
|
|
1206
|
-
: execFileSync("which", [cmd], {
|
|
1207
|
-
encoding: "utf8",
|
|
1208
|
-
timeout: 5000,
|
|
1209
|
-
stdio: ["pipe", "pipe", "ignore"],
|
|
1210
|
-
});
|
|
1211
|
-
return result.trim().split(/\r?\n/)[0] || null;
|
|
1212
|
-
} catch {
|
|
1213
|
-
return null;
|
|
491
|
+
function which(cmd) {
|
|
492
|
+
try {
|
|
493
|
+
const result =
|
|
494
|
+
process.platform === "win32"
|
|
495
|
+
? execFileSync("where", [cmd], {
|
|
496
|
+
encoding: "utf8",
|
|
497
|
+
timeout: 5000,
|
|
498
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
499
|
+
windowsHide: true,
|
|
500
|
+
})
|
|
501
|
+
: execFileSync("which", [cmd], {
|
|
502
|
+
encoding: "utf8",
|
|
503
|
+
timeout: 5000,
|
|
504
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
505
|
+
});
|
|
506
|
+
return result.trim().split(/\r?\n/)[0] || null;
|
|
507
|
+
} catch {
|
|
508
|
+
return null;
|
|
1214
509
|
}
|
|
1215
510
|
}
|
|
1216
511
|
|
|
@@ -1876,40 +1171,6 @@ function previewStatusLineAction() {
|
|
|
1876
1171
|
};
|
|
1877
1172
|
}
|
|
1878
1173
|
|
|
1879
|
-
function previewMcpRegistrationActions(mcpUrl) {
|
|
1880
|
-
const actions = [];
|
|
1881
|
-
|
|
1882
|
-
if (which("codex")) {
|
|
1883
|
-
actions.push({
|
|
1884
|
-
type: "mcp-register",
|
|
1885
|
-
cli: "codex",
|
|
1886
|
-
target: "tfx-hub",
|
|
1887
|
-
url: mcpUrl,
|
|
1888
|
-
change: "check",
|
|
1889
|
-
});
|
|
1890
|
-
}
|
|
1891
|
-
if (which("gemini")) {
|
|
1892
|
-
actions.push({
|
|
1893
|
-
type: "mcp-register",
|
|
1894
|
-
cli: "gemini",
|
|
1895
|
-
target: "tfx-hub",
|
|
1896
|
-
url: mcpUrl,
|
|
1897
|
-
change: "check",
|
|
1898
|
-
});
|
|
1899
|
-
}
|
|
1900
|
-
|
|
1901
|
-
actions.push({
|
|
1902
|
-
type: "mcp-register",
|
|
1903
|
-
cli: "claude",
|
|
1904
|
-
target: "tfx-hub",
|
|
1905
|
-
path: join(process.cwd(), ".claude", "mcp.json"),
|
|
1906
|
-
url: mcpUrl,
|
|
1907
|
-
change: "check",
|
|
1908
|
-
});
|
|
1909
|
-
|
|
1910
|
-
return actions;
|
|
1911
|
-
}
|
|
1912
|
-
|
|
1913
1174
|
function buildSetupDryRunPlan() {
|
|
1914
1175
|
const actions = [
|
|
1915
1176
|
...SYNC_MAP.map(({ src, dst, label }) =>
|
|
@@ -1926,19 +1187,7 @@ function buildSetupDryRunPlan() {
|
|
|
1926
1187
|
windowsSandbox: codexProfiles.windowsSandbox,
|
|
1927
1188
|
});
|
|
1928
1189
|
|
|
1929
|
-
const defaultHubUrl = `http://127.0.0.1:${process.env.TFX_HUB_PORT || "27888"}/mcp`;
|
|
1930
|
-
actions.push(...previewMcpRegistrationActions(defaultHubUrl));
|
|
1931
1190
|
actions.push(previewStatusLineAction());
|
|
1932
|
-
const autostart = getWindowsHubAutostartStatus();
|
|
1933
|
-
actions.push({
|
|
1934
|
-
type: "hub-autostart",
|
|
1935
|
-
platform: process.platform,
|
|
1936
|
-
taskName: autostart.taskName,
|
|
1937
|
-
change: autostart.supported && !autostart.registered ? "available" : "noop",
|
|
1938
|
-
registered: autostart.registered,
|
|
1939
|
-
command: autostart.supported ? buildWindowsHubAutostartCommand() : null,
|
|
1940
|
-
enableWith: "tfx setup --enable-hub-autostart",
|
|
1941
|
-
});
|
|
1942
1191
|
|
|
1943
1192
|
return {
|
|
1944
1193
|
dry_run: true,
|
|
@@ -1999,7 +1248,7 @@ function cmdSetup(options = {}) {
|
|
|
1999
1248
|
dryRun = false,
|
|
2000
1249
|
fromUpdate = false,
|
|
2001
1250
|
overrideVersion,
|
|
2002
|
-
|
|
1251
|
+
mods = false,
|
|
2003
1252
|
} = options;
|
|
2004
1253
|
if (dryRun) {
|
|
2005
1254
|
printJson(buildSetupDryRunPlan());
|
|
@@ -2018,6 +1267,11 @@ function cmdSetup(options = {}) {
|
|
|
2018
1267
|
}
|
|
2019
1268
|
// 이주가 막혀도 setup 은 계속한다. 남은 항목은 경고로 알린다.
|
|
2020
1269
|
for (const warning of cleanupLegacyMcp().warnings) warn(warning);
|
|
1270
|
+
const hubCleanup = cleanupTfxHub({
|
|
1271
|
+
pluginRoot: existsSync(join(PKG_ROOT, ".git")) ? undefined : PKG_ROOT,
|
|
1272
|
+
});
|
|
1273
|
+
for (const warning of hubCleanup.warnings) warn(warning);
|
|
1274
|
+
if (hubCleanup.changed) ok("제거된 허브의 설정과 실행 흔적 정리");
|
|
2021
1275
|
if (fromUpdate) refreshSetupCaches();
|
|
2022
1276
|
|
|
2023
1277
|
console.log(`\n${BOLD}triflux setup${RESET}\n`);
|
|
@@ -2026,6 +1280,7 @@ function cmdSetup(options = {}) {
|
|
|
2026
1280
|
syncFile(target.src, target.dst, target.label);
|
|
2027
1281
|
}
|
|
2028
1282
|
reportSkillSync();
|
|
1283
|
+
ensureTrifluxMods({ install: mods, log: console.log, warn });
|
|
2029
1284
|
|
|
2030
1285
|
// ── psmux 기본 셸 자동 수정 (cmd.exe → PowerShell) ──
|
|
2031
1286
|
if (process.platform === "win32" && which("psmux")) {
|
|
@@ -2114,79 +1369,10 @@ function cmdSetup(options = {}) {
|
|
|
2114
1369
|
});
|
|
2115
1370
|
}
|
|
2116
1371
|
|
|
2117
|
-
//
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
summary.push({ item: "Hub MCP", status: "✅", detail: "등록됨" });
|
|
2122
|
-
console.log("");
|
|
2123
|
-
}
|
|
2124
|
-
|
|
2125
|
-
if (process.platform === "win32") {
|
|
2126
|
-
const status = getWindowsHubAutostartStatus();
|
|
2127
|
-
if (enableHubAutostart) {
|
|
2128
|
-
try {
|
|
2129
|
-
const script = join(PKG_ROOT, "scripts", "setup.mjs");
|
|
2130
|
-
execFileSync(
|
|
2131
|
-
process.execPath,
|
|
2132
|
-
[script, "--enable-hub-autostart", "--sync"],
|
|
2133
|
-
{
|
|
2134
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
2135
|
-
timeout: 10000,
|
|
2136
|
-
windowsHide: true,
|
|
2137
|
-
},
|
|
2138
|
-
);
|
|
2139
|
-
// subprocess silent-catch 회귀 가드: schtasks /Query 로 실제 등록 재검증.
|
|
2140
|
-
const verified = getWindowsHubAutostartStatus();
|
|
2141
|
-
if (verified.registered) {
|
|
2142
|
-
ok(`Hub autostart: ${verified.taskName} 등록됨`);
|
|
2143
|
-
summary.push({
|
|
2144
|
-
item: "Hub autostart",
|
|
2145
|
-
status: "✅",
|
|
2146
|
-
detail: `${verified.taskName} 등록됨`,
|
|
2147
|
-
});
|
|
2148
|
-
} else {
|
|
2149
|
-
warn(
|
|
2150
|
-
"Hub autostart 등록 실패: subprocess 성공했으나 /Query 에서 미발견",
|
|
2151
|
-
);
|
|
2152
|
-
summary.push({
|
|
2153
|
-
item: "Hub autostart",
|
|
2154
|
-
status: "⚠️",
|
|
2155
|
-
detail: "등록 실패 (subprocess silent catch 의심)",
|
|
2156
|
-
});
|
|
2157
|
-
}
|
|
2158
|
-
} catch (error) {
|
|
2159
|
-
warn(`Hub autostart 등록 실패: ${renderErrorMessage(error.message)}`);
|
|
2160
|
-
summary.push({
|
|
2161
|
-
item: "Hub autostart",
|
|
2162
|
-
status: "⚠️",
|
|
2163
|
-
detail: "등록 실패",
|
|
2164
|
-
});
|
|
2165
|
-
}
|
|
2166
|
-
} else if (status.registered) {
|
|
2167
|
-
ok(`Hub autostart: ${status.taskName} 이미 등록됨`);
|
|
2168
|
-
summary.push({
|
|
2169
|
-
item: "Hub autostart",
|
|
2170
|
-
status: "✅",
|
|
2171
|
-
detail: "이미 등록됨",
|
|
2172
|
-
});
|
|
2173
|
-
} else {
|
|
2174
|
-
warn(
|
|
2175
|
-
"Hub autostart 미등록 — Codex 단독 시작 전 hub가 죽어 있으면 MCP가 실패할 수 있음",
|
|
2176
|
-
);
|
|
2177
|
-
info("등록: tfx setup --enable-hub-autostart");
|
|
2178
|
-
summary.push({
|
|
2179
|
-
item: "Hub autostart",
|
|
2180
|
-
status: "⏭️",
|
|
2181
|
-
detail: "미등록",
|
|
2182
|
-
});
|
|
2183
|
-
}
|
|
2184
|
-
}
|
|
2185
|
-
|
|
2186
|
-
// HUD statusLine 설정
|
|
2187
|
-
console.log(`${CYAN}[HUD 설정]${RESET}`);
|
|
2188
|
-
const settingsPath = join(CLAUDE_DIR, "settings.json");
|
|
2189
|
-
const hudPath = join(CLAUDE_DIR, "hud", "hud-qos-status.mjs");
|
|
1372
|
+
// HUD statusLine 설정
|
|
1373
|
+
console.log(`${CYAN}[HUD 설정]${RESET}`);
|
|
1374
|
+
const settingsPath = join(CLAUDE_DIR, "settings.json");
|
|
1375
|
+
const hudPath = join(CLAUDE_DIR, "hud", "hud-qos-status.mjs");
|
|
2190
1376
|
|
|
2191
1377
|
if (existsSync(hudPath)) {
|
|
2192
1378
|
try {
|
|
@@ -2385,9 +1571,6 @@ async function cmdDoctor(options = {}) {
|
|
|
2385
1571
|
fix = false,
|
|
2386
1572
|
reset = false,
|
|
2387
1573
|
purgeLogs = false,
|
|
2388
|
-
cleanupStaleHubs = false,
|
|
2389
|
-
cleanupStaleHubsDryRun = true,
|
|
2390
|
-
cleanupStaleHubsApply = false,
|
|
2391
1574
|
cleanupStaleTmux = false,
|
|
2392
1575
|
cleanupStaleTmuxDryRun = true,
|
|
2393
1576
|
cleanupStaleTmuxApply = false,
|
|
@@ -2402,7 +1585,6 @@ async function cmdDoctor(options = {}) {
|
|
|
2402
1585
|
actions: [],
|
|
2403
1586
|
legacy_hooks: { remaining: 0, removed: 0 },
|
|
2404
1587
|
fsmonitorDaemons: { stale: 0, killed: 0 },
|
|
2405
|
-
hubServers: { detached: 0, stale: 0, activeHealthy: null },
|
|
2406
1588
|
tmuxSessions: {
|
|
2407
1589
|
detached: 0,
|
|
2408
1590
|
stale: 0,
|
|
@@ -2535,33 +1717,6 @@ async function cmdDoctor(options = {}) {
|
|
|
2535
1717
|
warn("Codex 레이트 리밋 캐시 재생성 실패");
|
|
2536
1718
|
}
|
|
2537
1719
|
}
|
|
2538
|
-
try {
|
|
2539
|
-
const { buildAll } = await import("../scripts/cache-warmup.mjs");
|
|
2540
|
-
const warmupSummary = buildAll({ cwd: process.cwd(), force: true });
|
|
2541
|
-
if (warmupSummary.ok) {
|
|
2542
|
-
report.actions.push({
|
|
2543
|
-
type: "rebuild",
|
|
2544
|
-
name: "warmup-caches",
|
|
2545
|
-
status: "ok",
|
|
2546
|
-
built: warmupSummary.built,
|
|
2547
|
-
});
|
|
2548
|
-
ok("Phase 1 웜업 캐시 재생성됨");
|
|
2549
|
-
} else {
|
|
2550
|
-
report.actions.push({
|
|
2551
|
-
type: "rebuild",
|
|
2552
|
-
name: "warmup-caches",
|
|
2553
|
-
status: "failed",
|
|
2554
|
-
});
|
|
2555
|
-
warn("Phase 1 웜업 캐시 재생성 실패");
|
|
2556
|
-
}
|
|
2557
|
-
} catch {
|
|
2558
|
-
report.actions.push({
|
|
2559
|
-
type: "rebuild",
|
|
2560
|
-
name: "warmup-caches",
|
|
2561
|
-
status: "failed",
|
|
2562
|
-
});
|
|
2563
|
-
warn("Phase 1 웜업 캐시 재생성 실패");
|
|
2564
|
-
}
|
|
2565
1720
|
console.log(`\n ${LINE}`);
|
|
2566
1721
|
console.log(
|
|
2567
1722
|
` ${GREEN_BRIGHT}${BOLD}✓ 캐시 초기화 + 재생성 완료${RESET}\n`,
|
|
@@ -2683,19 +1838,6 @@ async function cmdDoctor(options = {}) {
|
|
|
2683
1838
|
}
|
|
2684
1839
|
}
|
|
2685
1840
|
if (cleaned === 0) info("에러 캐시 없음");
|
|
2686
|
-
try {
|
|
2687
|
-
const { fixCaches } = await import("../scripts/cache-doctor.mjs");
|
|
2688
|
-
const cacheRepair = await fixCaches({ cwd: process.cwd() });
|
|
2689
|
-
if (cacheRepair.fixed.length > 0 && cacheRepair.ok) {
|
|
2690
|
-
ok(`웜업 캐시 자동 복구: ${cacheRepair.fixed.join(", ")}`);
|
|
2691
|
-
} else if (cacheRepair.fixed.length > 0) {
|
|
2692
|
-
warn(`웜업 캐시 자동 복구 실패: ${cacheRepair.fixed.join(", ")}`);
|
|
2693
|
-
} else {
|
|
2694
|
-
info("웜업 캐시: 이미 정상 상태");
|
|
2695
|
-
}
|
|
2696
|
-
} catch {
|
|
2697
|
-
warn("웜업 캐시 자동 복구 실패");
|
|
2698
|
-
}
|
|
2699
1841
|
const registryStateForFix = inspectRegistry();
|
|
2700
1842
|
if (registryStateForFix.valid) {
|
|
2701
1843
|
try {
|
|
@@ -3278,45 +2420,6 @@ async function cmdDoctor(options = {}) {
|
|
|
3278
2420
|
info(`수동: node ${join(PKG_ROOT, "scripts", "mcp-check.mjs")}`);
|
|
3279
2421
|
}
|
|
3280
2422
|
|
|
3281
|
-
// Phase 1 웜업 캐시
|
|
3282
|
-
section("Warmup Cache");
|
|
3283
|
-
try {
|
|
3284
|
-
const { verifyCaches } = await import("../scripts/cache-doctor.mjs");
|
|
3285
|
-
const cacheVerification = verifyCaches({ cwd: process.cwd() });
|
|
3286
|
-
const brokenCaches = cacheVerification.results.filter(
|
|
3287
|
-
(result) => result.status !== "ok",
|
|
3288
|
-
);
|
|
3289
|
-
|
|
3290
|
-
addDoctorCheck(report, {
|
|
3291
|
-
name: "warmup-cache",
|
|
3292
|
-
status: cacheVerification.ok ? "ok" : "issues",
|
|
3293
|
-
files: cacheVerification.results.map((result) => ({
|
|
3294
|
-
target: result.target,
|
|
3295
|
-
status: result.status,
|
|
3296
|
-
path: result.file,
|
|
3297
|
-
})),
|
|
3298
|
-
...(cacheVerification.ok ? {} : { fix: "tfx doctor --fix" }),
|
|
3299
|
-
});
|
|
3300
|
-
|
|
3301
|
-
if (brokenCaches.length === 0) {
|
|
3302
|
-
ok(`${cacheVerification.results.length}개 웜업 캐시 정상`);
|
|
3303
|
-
} else {
|
|
3304
|
-
warn(`${brokenCaches.length}개 웜업 캐시 이슈 발견`);
|
|
3305
|
-
for (const entry of brokenCaches) {
|
|
3306
|
-
info(`${entry.target}: ${entry.status}`);
|
|
3307
|
-
}
|
|
3308
|
-
if (!fix) issues += brokenCaches.length;
|
|
3309
|
-
}
|
|
3310
|
-
} catch (error) {
|
|
3311
|
-
addDoctorCheck(report, {
|
|
3312
|
-
name: "warmup-cache",
|
|
3313
|
-
status: "invalid",
|
|
3314
|
-
fix: "node scripts/cache-doctor.mjs --fix",
|
|
3315
|
-
});
|
|
3316
|
-
warn(`웜업 캐시 검사 실패: ${error.message}`);
|
|
3317
|
-
issues++;
|
|
3318
|
-
}
|
|
3319
|
-
|
|
3320
2423
|
// CLI 이슈 트래커
|
|
3321
2424
|
section("CLI Issues");
|
|
3322
2425
|
const issuesFile = join(CLAUDE_DIR, "cache", "cli-issues.jsonl");
|
|
@@ -3610,98 +2713,6 @@ async function cmdDoctor(options = {}) {
|
|
|
3610
2713
|
issues += detachedTmuxReport.staleCandidates.length;
|
|
3611
2714
|
}
|
|
3612
2715
|
|
|
3613
|
-
// detached tfx-hub/server.mjs 누적 감지 및 opt-in 정리
|
|
3614
|
-
section("Hub Servers");
|
|
3615
|
-
try {
|
|
3616
|
-
const hubReport = await inspectDetachedHubProcesses();
|
|
3617
|
-
report.hubServers = {
|
|
3618
|
-
detached: hubReport.hubs.length,
|
|
3619
|
-
stale: hubReport.staleCandidates.length,
|
|
3620
|
-
activeHealthy: hubReport.activeHealthy,
|
|
3621
|
-
expectedVersion: hubReport.expectedVersion,
|
|
3622
|
-
hubs: hubReport.hubs.map((hub) => ({
|
|
3623
|
-
pid: hub.pid,
|
|
3624
|
-
ppid: hub.ppid,
|
|
3625
|
-
version: hub.version,
|
|
3626
|
-
uptime: hub.uptime,
|
|
3627
|
-
ports: hub.ports,
|
|
3628
|
-
established: hub.established,
|
|
3629
|
-
rssKb: hub.rssKb,
|
|
3630
|
-
activeHealthy: hub.activeHealthy,
|
|
3631
|
-
activeReason: hub.activeReason,
|
|
3632
|
-
healthStatus: hub.healthStatus,
|
|
3633
|
-
staleCandidate: hub.staleCandidate,
|
|
3634
|
-
})),
|
|
3635
|
-
};
|
|
3636
|
-
addDoctorCheck(report, {
|
|
3637
|
-
name: "hub-server-processes",
|
|
3638
|
-
status: hubReport.staleCandidates.length > 0 ? "warning" : "ok",
|
|
3639
|
-
detached: hubReport.hubs.length,
|
|
3640
|
-
stale: hubReport.staleCandidates.length,
|
|
3641
|
-
activeHealthy: hubReport.activeHealthy,
|
|
3642
|
-
});
|
|
3643
|
-
|
|
3644
|
-
if (hubReport.hubs.length === 0) {
|
|
3645
|
-
ok("PPID=1 hub/server.mjs 없음");
|
|
3646
|
-
} else {
|
|
3647
|
-
for (const hub of hubReport.hubs) {
|
|
3648
|
-
const portLabel = hub.ports.length > 0 ? hub.ports.join(",") : "?";
|
|
3649
|
-
const versionLabel = hub.version || "unknown";
|
|
3650
|
-
const rssLabel = hub.rssKb == null ? "?" : `${hub.rssKb}KB`;
|
|
3651
|
-
const activeLabel = hub.activeHealthy
|
|
3652
|
-
? ` healthy excluded${hub.activeReason ? ` (${hub.activeReason})` : ""}`
|
|
3653
|
-
: " stale candidate";
|
|
3654
|
-
const line = `PID=${hub.pid} PPID=${hub.ppid} status=${hub.healthStatus} version=${versionLabel} uptime=${hub.uptime} port=${portLabel} ESTABLISHED=${hub.established} RSS=${rssLabel}${activeLabel}`;
|
|
3655
|
-
if (hub.activeHealthy) ok(line);
|
|
3656
|
-
else warn(line);
|
|
3657
|
-
}
|
|
3658
|
-
}
|
|
3659
|
-
|
|
3660
|
-
if (cleanupStaleHubs) {
|
|
3661
|
-
const cleanupResult = await cleanupDetachedHubProcesses({
|
|
3662
|
-
hubs: hubReport.hubs,
|
|
3663
|
-
activeHealthy: hubReport.activeHealthy,
|
|
3664
|
-
dryRun: cleanupStaleHubsDryRun,
|
|
3665
|
-
apply: cleanupStaleHubsApply,
|
|
3666
|
-
});
|
|
3667
|
-
report.actions.push({
|
|
3668
|
-
type: "cleanup-stale-hubs",
|
|
3669
|
-
status: cleanupResult.failed > 0 ? "failed" : "ok",
|
|
3670
|
-
dryRun: cleanupResult.dryRun,
|
|
3671
|
-
retired: cleanupResult.retired,
|
|
3672
|
-
skipped: cleanupResult.skipped,
|
|
3673
|
-
excluded: cleanupResult.excluded,
|
|
3674
|
-
failed: cleanupResult.failed,
|
|
3675
|
-
});
|
|
3676
|
-
for (const result of cleanupResult.results) {
|
|
3677
|
-
if (result.action === "excluded-active") {
|
|
3678
|
-
ok(`dry-run healthy hub excluded: PID=${result.pid}`);
|
|
3679
|
-
} else if (result.action === "dry-run-skip") {
|
|
3680
|
-
info(`dry-run stale hub target: PID=${result.pid}`);
|
|
3681
|
-
} else if (result.action === "retired") {
|
|
3682
|
-
ok(`stale hub retired: PID=${result.pid} (${result.reason})`);
|
|
3683
|
-
} else {
|
|
3684
|
-
fail(`stale hub cleanup failed: PID=${result.pid}`);
|
|
3685
|
-
}
|
|
3686
|
-
}
|
|
3687
|
-
issues += cleanupResult.failed;
|
|
3688
|
-
if (cleanupResult.dryRun && hubReport.staleCandidates.length > 0) {
|
|
3689
|
-
issues += hubReport.staleCandidates.length;
|
|
3690
|
-
}
|
|
3691
|
-
} else if (hubReport.staleCandidates.length > 0) {
|
|
3692
|
-
info("정리: tfx doctor --cleanup-stale-hubs --dry-run|--apply");
|
|
3693
|
-
issues += hubReport.staleCandidates.length;
|
|
3694
|
-
}
|
|
3695
|
-
} catch (error) {
|
|
3696
|
-
addDoctorCheck(report, {
|
|
3697
|
-
name: "hub-server-processes",
|
|
3698
|
-
status: "warning",
|
|
3699
|
-
error: error.message,
|
|
3700
|
-
});
|
|
3701
|
-
warn(`hub/server.mjs 검사 실패: ${error.message}`);
|
|
3702
|
-
issues++;
|
|
3703
|
-
}
|
|
3704
|
-
|
|
3705
2716
|
// 고아 node.exe 프로세스 정리 (Windows)
|
|
3706
2717
|
section("Orphan Processes");
|
|
3707
2718
|
if (process.platform === "win32") {
|
|
@@ -3874,56 +2885,6 @@ async function cmdDoctor(options = {}) {
|
|
|
3874
2885
|
if (row.actualUrl) info(`actual ${row.actualUrl}`);
|
|
3875
2886
|
}
|
|
3876
2887
|
|
|
3877
|
-
// #144: --fix 모드에서 tfx-hub URL 불일치를 hub status 기준으로 자동 갱신.
|
|
3878
|
-
// Project MCP (.mcp.json) 와 Codex/Claude/Gemini settings 모두 대상.
|
|
3879
|
-
// Codex review P2: fix 성공 시 issues 집계에서 차감해야 doctor 결과가 ok 로 반영됨.
|
|
3880
|
-
let autoFixedMismatches = 0;
|
|
3881
|
-
if (fix && mismatchRows.some((r) => r.name === "tfx-hub")) {
|
|
3882
|
-
try {
|
|
3883
|
-
const hubUrl = mismatchRows.find(
|
|
3884
|
-
(r) => r.name === "tfx-hub",
|
|
3885
|
-
)?.expectedUrl;
|
|
3886
|
-
if (hubUrl) {
|
|
3887
|
-
const { syncHubMcpSettings, syncProjectMcpJson } = await import(
|
|
3888
|
-
"../scripts/sync-hub-mcp-settings.mjs"
|
|
3889
|
-
);
|
|
3890
|
-
const settingsResult = await syncHubMcpSettings({
|
|
3891
|
-
hubUrl,
|
|
3892
|
-
logger: { log() {}, warn() {}, error() {} },
|
|
3893
|
-
});
|
|
3894
|
-
const projectResult = await syncProjectMcpJson({
|
|
3895
|
-
hubUrl,
|
|
3896
|
-
projectRoot: process.cwd(),
|
|
3897
|
-
logger: { log() {}, warn() {}, error() {} },
|
|
3898
|
-
});
|
|
3899
|
-
const totalUpdated =
|
|
3900
|
-
(settingsResult?.updated?.length || 0) +
|
|
3901
|
-
(projectResult?.updated?.length || 0);
|
|
3902
|
-
if (totalUpdated > 0) {
|
|
3903
|
-
ok(`tfx-hub URL ${totalUpdated}개 파일 자동 갱신 (${hubUrl})`);
|
|
3904
|
-
report.actions.push({
|
|
3905
|
-
name: "sync-hub-url",
|
|
3906
|
-
status: "applied",
|
|
3907
|
-
files: [
|
|
3908
|
-
...(settingsResult?.updated || []),
|
|
3909
|
-
...(projectResult?.updated || []),
|
|
3910
|
-
],
|
|
3911
|
-
});
|
|
3912
|
-
// fix 성공 — mismatchRows 중 tfx-hub 엔트리는 해결된 것으로 집계
|
|
3913
|
-
autoFixedMismatches = mismatchRows.filter(
|
|
3914
|
-
(r) => r.name === "tfx-hub",
|
|
3915
|
-
).length;
|
|
3916
|
-
} else {
|
|
3917
|
-
info("tfx-hub URL 자동 갱신: 대상 파일 없음");
|
|
3918
|
-
}
|
|
3919
|
-
}
|
|
3920
|
-
} catch (e) {
|
|
3921
|
-
warn(
|
|
3922
|
-
`tfx-hub URL 자동 갱신 실패: ${e?.message?.split(/\r?\n/)[0] || e}`,
|
|
3923
|
-
);
|
|
3924
|
-
}
|
|
3925
|
-
}
|
|
3926
|
-
|
|
3927
2888
|
for (const row of missingFileRows) {
|
|
3928
2889
|
info(
|
|
3929
2890
|
`${row.label}: ${row.name} 미배치 (${formatPathForDisplay(row.filePath)})`,
|
|
@@ -3946,15 +2907,33 @@ async function cmdDoctor(options = {}) {
|
|
|
3946
2907
|
}
|
|
3947
2908
|
|
|
3948
2909
|
issues += invalidConfigs.length;
|
|
3949
|
-
issues +=
|
|
2910
|
+
issues += mismatchRows.length;
|
|
3950
2911
|
issues += stdioRows.length;
|
|
3951
2912
|
}
|
|
3952
2913
|
}
|
|
3953
2914
|
|
|
2915
|
+
// 제거된 허브가 cwd 에 만든 항목은 사용자 파일과 구분할 수 없어 자동으로 지우지 않는다.
|
|
2916
|
+
section("제거된 허브 흔적");
|
|
2917
|
+
{
|
|
2918
|
+
const leftovers = findProjectHubEntries();
|
|
2919
|
+
addDoctorCheck(report, {
|
|
2920
|
+
name: "legacy-hub-mcp-entries",
|
|
2921
|
+
status: leftovers.length > 0 ? "warning" : "ok",
|
|
2922
|
+
paths: leftovers,
|
|
2923
|
+
});
|
|
2924
|
+
if (leftovers.length === 0) {
|
|
2925
|
+
ok("현재 디렉터리에 tfx-hub MCP 항목 없음");
|
|
2926
|
+
} else {
|
|
2927
|
+
for (const file of leftovers) {
|
|
2928
|
+
warn(`tfx-hub MCP 항목: ${formatPathForDisplay(file)}`);
|
|
2929
|
+
}
|
|
2930
|
+
info("제거된 허브가 만든 항목입니다. 직접 지우세요.");
|
|
2931
|
+
issues += leftovers.length;
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
|
|
3954
2935
|
// ── Codex Config Health (BUG-H #132) ──
|
|
3955
|
-
//
|
|
3956
|
-
// ~/.codex/config.toml.pre-exec 가 남아 [mcp_servers.*] 섹션이 영구 손실된다.
|
|
3957
|
-
// orphan backup 을 감지하고 --fix 로 자동 복원한다.
|
|
2936
|
+
// 이전 버전의 swap이 남긴 백업을 감지하고 --fix로 복원한다.
|
|
3958
2937
|
section("Codex Config Health");
|
|
3959
2938
|
{
|
|
3960
2939
|
const codexConfig = join(CODEX_DIR, "config.toml");
|
|
@@ -4269,7 +3248,6 @@ async function cmdUpdate(args = []) {
|
|
|
4269
3248
|
// 2. 설치 방식에 따라 업데이트
|
|
4270
3249
|
const oldVer = PKG.version;
|
|
4271
3250
|
let updated = false;
|
|
4272
|
-
let stoppedHubInfo = null;
|
|
4273
3251
|
|
|
4274
3252
|
try {
|
|
4275
3253
|
switch (installMode) {
|
|
@@ -4286,10 +3264,6 @@ async function cmdUpdate(args = []) {
|
|
|
4286
3264
|
break;
|
|
4287
3265
|
}
|
|
4288
3266
|
case "npm-global": {
|
|
4289
|
-
stoppedHubInfo = stopHubForUpdate();
|
|
4290
|
-
if (stoppedHubInfo?.pid) {
|
|
4291
|
-
info(`실행 중 hub 정지 (PID ${stoppedHubInfo.pid})`);
|
|
4292
|
-
}
|
|
4293
3267
|
const npmCmd = isDev
|
|
4294
3268
|
? "npm install -g triflux@dev"
|
|
4295
3269
|
: "npm install -g triflux@latest";
|
|
@@ -4355,9 +3329,6 @@ async function cmdUpdate(args = []) {
|
|
|
4355
3329
|
return;
|
|
4356
3330
|
}
|
|
4357
3331
|
} catch (e) {
|
|
4358
|
-
if (stoppedHubInfo && startHubAfterUpdate(stoppedHubInfo)) {
|
|
4359
|
-
info("업데이트 실패 후 hub 재기동 시도");
|
|
4360
|
-
}
|
|
4361
3332
|
const stderr = e.stderr?.toString().trim();
|
|
4362
3333
|
fail(
|
|
4363
3334
|
`업데이트 실패: ${e.message}${stderr ? `\n ${stderr.split(/\r?\n/)[0]}` : ""}`,
|
|
@@ -4388,88 +3359,31 @@ async function cmdUpdate(args = []) {
|
|
|
4388
3359
|
try {
|
|
4389
3360
|
runUpdatedSetup({ packageRoot: updatedRoot });
|
|
4390
3361
|
} catch (error) {
|
|
4391
|
-
if (stoppedHubInfo) startHubAfterUpdate(stoppedHubInfo);
|
|
4392
3362
|
throw createCliError(`업데이트 후 설정 동기화 실패: ${error.message}`, {
|
|
4393
3363
|
exitCode: error.status || EXIT_ERROR,
|
|
4394
3364
|
reason: "error",
|
|
4395
3365
|
});
|
|
4396
3366
|
}
|
|
4397
|
-
|
|
4398
|
-
if (stoppedHubInfo) {
|
|
4399
|
-
if (startHubAfterUpdate(stoppedHubInfo)) ok("hub 재기동 완료");
|
|
4400
|
-
else warn("hub 재기동 실패 — `tfx hub start`로 수동 시작 필요");
|
|
4401
|
-
}
|
|
4402
3367
|
}
|
|
4403
3368
|
|
|
4404
3369
|
console.log(`${GREEN}${BOLD}✓ 업데이트 완료${RESET}\n`);
|
|
4405
3370
|
}
|
|
4406
3371
|
|
|
4407
|
-
function readPackagedSkillMetadata(skillName) {
|
|
4408
|
-
const skillPath = join(PKG_ROOT, "skills", skillName, "SKILL.md");
|
|
4409
|
-
if (!existsSync(skillPath)) return null;
|
|
4410
|
-
const metadata = { name: skillName, deprecated: false, supersededBy: null };
|
|
4411
|
-
let inFrontmatter = false;
|
|
4412
|
-
for (const line of readFileSync(skillPath, "utf8").split(/\r?\n/)) {
|
|
4413
|
-
if (line.trim() === "---") {
|
|
4414
|
-
if (!inFrontmatter) {
|
|
4415
|
-
inFrontmatter = true;
|
|
4416
|
-
continue;
|
|
4417
|
-
}
|
|
4418
|
-
break;
|
|
4419
|
-
}
|
|
4420
|
-
if (!inFrontmatter) continue;
|
|
4421
|
-
const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
4422
|
-
if (!match) continue;
|
|
4423
|
-
const [, key, rawValue] = match;
|
|
4424
|
-
const value = rawValue.trim().replace(/^["']|["']$/g, "");
|
|
4425
|
-
if (key === "deprecated") metadata.deprecated = value === "true";
|
|
4426
|
-
if (key === "superseded-by") metadata.supersededBy = value || null;
|
|
4427
|
-
}
|
|
4428
|
-
return metadata;
|
|
4429
|
-
}
|
|
4430
|
-
|
|
4431
|
-
function listDeprecatedPackagedSkillAliases(pluginSkills, installedSkills) {
|
|
4432
|
-
if (!existsSync(pluginSkills)) return [];
|
|
4433
|
-
return readdirSync(pluginSkills)
|
|
4434
|
-
.sort()
|
|
4435
|
-
.map((name) => readPackagedSkillMetadata(name))
|
|
4436
|
-
.filter((metadata) => metadata?.deprecated)
|
|
4437
|
-
.map((metadata) => ({
|
|
4438
|
-
alias: metadata.name,
|
|
4439
|
-
source: metadata.supersededBy || "deprecated",
|
|
4440
|
-
installed: existsSync(join(installedSkills, metadata.name, "SKILL.md")),
|
|
4441
|
-
deprecated: true,
|
|
4442
|
-
}));
|
|
4443
|
-
}
|
|
4444
|
-
|
|
4445
3372
|
function cmdList(options = {}) {
|
|
4446
3373
|
const { json = false } = options;
|
|
4447
3374
|
const pluginSkills = join(PKG_ROOT, "skills");
|
|
4448
3375
|
const installedSkills = join(CLAUDE_DIR, "skills");
|
|
4449
3376
|
const packageSkills = [];
|
|
4450
3377
|
const userSkills = [];
|
|
4451
|
-
const skillAliases = listDeprecatedPackagedSkillAliases(
|
|
4452
|
-
pluginSkills,
|
|
4453
|
-
installedSkills,
|
|
4454
|
-
);
|
|
4455
|
-
const aliasNames = new Set([
|
|
4456
|
-
...SKILL_ALIASES.map(({ alias }) => alias),
|
|
4457
|
-
...skillAliases.map(({ alias }) => alias),
|
|
4458
|
-
]);
|
|
4459
3378
|
|
|
4460
3379
|
if (existsSync(pluginSkills)) {
|
|
4461
3380
|
for (const name of readdirSync(pluginSkills).sort()) {
|
|
4462
3381
|
const src = join(pluginSkills, name, "SKILL.md");
|
|
4463
3382
|
if (!existsSync(src)) continue;
|
|
4464
|
-
const metadata = readPackagedSkillMetadata(name);
|
|
4465
3383
|
const dst = join(installedSkills, name, "SKILL.md");
|
|
4466
3384
|
packageSkills.push({
|
|
4467
3385
|
name,
|
|
4468
3386
|
installed: existsSync(dst),
|
|
4469
|
-
...(metadata?.deprecated ? { deprecated: true } : {}),
|
|
4470
|
-
...(metadata?.supersededBy
|
|
4471
|
-
? { superseded_by: metadata.supersededBy }
|
|
4472
|
-
: {}),
|
|
4473
3387
|
});
|
|
4474
3388
|
}
|
|
4475
3389
|
}
|
|
@@ -4479,7 +3393,7 @@ function cmdList(options = {}) {
|
|
|
4479
3393
|
);
|
|
4480
3394
|
if (existsSync(installedSkills)) {
|
|
4481
3395
|
for (const name of readdirSync(installedSkills).sort()) {
|
|
4482
|
-
if (pkgNames.has(name)
|
|
3396
|
+
if (pkgNames.has(name)) continue;
|
|
4483
3397
|
const skill = join(installedSkills, name, "SKILL.md");
|
|
4484
3398
|
if (!existsSync(skill)) continue;
|
|
4485
3399
|
userSkills.push(name);
|
|
@@ -4489,7 +3403,6 @@ function cmdList(options = {}) {
|
|
|
4489
3403
|
if (json) {
|
|
4490
3404
|
printJson({
|
|
4491
3405
|
package_skills: packageSkills,
|
|
4492
|
-
skill_aliases: skillAliases,
|
|
4493
3406
|
user_skills: userSkills,
|
|
4494
3407
|
install_path: installedSkills,
|
|
4495
3408
|
});
|
|
@@ -4516,19 +3429,6 @@ function cmdList(options = {}) {
|
|
|
4516
3429
|
}
|
|
4517
3430
|
if (userSkills.length === 0) console.log(` ${GRAY}없음${RESET}`);
|
|
4518
3431
|
|
|
4519
|
-
if (skillAliases.length > 0) {
|
|
4520
|
-
section("호환 alias");
|
|
4521
|
-
for (const entry of skillAliases) {
|
|
4522
|
-
const icon = entry.installed
|
|
4523
|
-
? `${GREEN_BRIGHT}↳${RESET}`
|
|
4524
|
-
: `${RED_BRIGHT}↳${RESET}`;
|
|
4525
|
-
const status = entry.installed ? "" : ` ${GRAY}(미설치)${RESET}`;
|
|
4526
|
-
console.log(
|
|
4527
|
-
` ${icon} ${BOLD}${entry.alias}${RESET} ${GRAY}→ ${entry.source}${RESET}${entry.deprecated ? ` ${YELLOW}(deprecated)${RESET}` : ""}${status}`,
|
|
4528
|
-
);
|
|
4529
|
-
}
|
|
4530
|
-
}
|
|
4531
|
-
|
|
4532
3432
|
console.log(`\n ${LINE}`);
|
|
4533
3433
|
console.log(` ${GRAY}${installedSkills}${RESET}\n`);
|
|
4534
3434
|
}
|
|
@@ -4554,188 +3454,6 @@ function cmdVersion(options = {}) {
|
|
|
4554
3454
|
console.log("");
|
|
4555
3455
|
}
|
|
4556
3456
|
|
|
4557
|
-
function cmdHandoff(args = [], options = {}) {
|
|
4558
|
-
if (args.some(isHelpArg)) {
|
|
4559
|
-
printCommandHelp("handoff");
|
|
4560
|
-
return;
|
|
4561
|
-
}
|
|
4562
|
-
|
|
4563
|
-
const { json = false } = options;
|
|
4564
|
-
const parsed = {
|
|
4565
|
-
target: "remote",
|
|
4566
|
-
decisions: [],
|
|
4567
|
-
decisionFile: null,
|
|
4568
|
-
output: null,
|
|
4569
|
-
cwd: process.cwd(),
|
|
4570
|
-
};
|
|
4571
|
-
|
|
4572
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
4573
|
-
const arg = args[index];
|
|
4574
|
-
const next = args[index + 1];
|
|
4575
|
-
|
|
4576
|
-
if (arg === "--target") {
|
|
4577
|
-
if (!next || next.startsWith("-")) {
|
|
4578
|
-
throw createCliError("--target 값이 필요합니다 (local|remote)", {
|
|
4579
|
-
exitCode: EXIT_ARG_ERROR,
|
|
4580
|
-
reason: "argError",
|
|
4581
|
-
fix: "tfx handoff --target remote",
|
|
4582
|
-
});
|
|
4583
|
-
}
|
|
4584
|
-
if (!["local", "remote"].includes(next)) {
|
|
4585
|
-
throw createCliError(`지원하지 않는 --target 값: ${next}`, {
|
|
4586
|
-
exitCode: EXIT_ARG_ERROR,
|
|
4587
|
-
reason: "argError",
|
|
4588
|
-
fix: "tfx handoff --target local|remote",
|
|
4589
|
-
});
|
|
4590
|
-
}
|
|
4591
|
-
parsed.target = next;
|
|
4592
|
-
index += 1;
|
|
4593
|
-
continue;
|
|
4594
|
-
}
|
|
4595
|
-
|
|
4596
|
-
if (arg === "--decision") {
|
|
4597
|
-
if (!next || next.startsWith("-")) {
|
|
4598
|
-
throw createCliError("--decision 값이 필요합니다", {
|
|
4599
|
-
exitCode: EXIT_ARG_ERROR,
|
|
4600
|
-
reason: "argError",
|
|
4601
|
-
fix: 'tfx handoff --decision "결정사항"',
|
|
4602
|
-
});
|
|
4603
|
-
}
|
|
4604
|
-
parsed.decisions.push(next);
|
|
4605
|
-
index += 1;
|
|
4606
|
-
continue;
|
|
4607
|
-
}
|
|
4608
|
-
|
|
4609
|
-
if (arg === "--decision-file") {
|
|
4610
|
-
if (!next || next.startsWith("-")) {
|
|
4611
|
-
throw createCliError("--decision-file 경로가 필요합니다", {
|
|
4612
|
-
exitCode: EXIT_ARG_ERROR,
|
|
4613
|
-
reason: "argError",
|
|
4614
|
-
fix: "tfx handoff --decision-file .omx/notepad.md",
|
|
4615
|
-
});
|
|
4616
|
-
}
|
|
4617
|
-
parsed.decisionFile = resolve(next);
|
|
4618
|
-
index += 1;
|
|
4619
|
-
continue;
|
|
4620
|
-
}
|
|
4621
|
-
|
|
4622
|
-
if (arg === "--output" || arg === "--out") {
|
|
4623
|
-
if (!next || next.startsWith("-")) {
|
|
4624
|
-
throw createCliError(`${arg} 경로가 필요합니다`, {
|
|
4625
|
-
exitCode: EXIT_ARG_ERROR,
|
|
4626
|
-
reason: "argError",
|
|
4627
|
-
fix: "tfx handoff --output .omx/handoff.md",
|
|
4628
|
-
});
|
|
4629
|
-
}
|
|
4630
|
-
parsed.output = resolve(next);
|
|
4631
|
-
index += 1;
|
|
4632
|
-
continue;
|
|
4633
|
-
}
|
|
4634
|
-
|
|
4635
|
-
if (arg === "--cwd") {
|
|
4636
|
-
if (!next || next.startsWith("-")) {
|
|
4637
|
-
throw createCliError("--cwd 경로가 필요합니다", {
|
|
4638
|
-
exitCode: EXIT_ARG_ERROR,
|
|
4639
|
-
reason: "argError",
|
|
4640
|
-
fix: "tfx handoff --cwd <project-path>",
|
|
4641
|
-
});
|
|
4642
|
-
}
|
|
4643
|
-
parsed.cwd = resolve(next);
|
|
4644
|
-
index += 1;
|
|
4645
|
-
continue;
|
|
4646
|
-
}
|
|
4647
|
-
|
|
4648
|
-
throw createCliError(`알 수 없는 handoff 옵션: ${arg}`, {
|
|
4649
|
-
exitCode: EXIT_ARG_ERROR,
|
|
4650
|
-
reason: "argError",
|
|
4651
|
-
fix: "tfx handoff --target remote --output .omx/handoff.md",
|
|
4652
|
-
});
|
|
4653
|
-
}
|
|
4654
|
-
|
|
4655
|
-
const result = serializeHandoff({
|
|
4656
|
-
target: parsed.target,
|
|
4657
|
-
decisions: parsed.decisions,
|
|
4658
|
-
decisionFile: parsed.decisionFile,
|
|
4659
|
-
cwd: parsed.cwd,
|
|
4660
|
-
});
|
|
4661
|
-
|
|
4662
|
-
if (parsed.output) {
|
|
4663
|
-
const outputDir = dirname(parsed.output);
|
|
4664
|
-
if (!existsSync(outputDir)) mkdirSync(outputDir, { recursive: true });
|
|
4665
|
-
writeFileSync(parsed.output, `${result.prompt}\n`, "utf8");
|
|
4666
|
-
}
|
|
4667
|
-
|
|
4668
|
-
if (json) {
|
|
4669
|
-
printJson({
|
|
4670
|
-
handoff: {
|
|
4671
|
-
...result,
|
|
4672
|
-
...(parsed.output ? { output: parsed.output } : {}),
|
|
4673
|
-
},
|
|
4674
|
-
});
|
|
4675
|
-
return;
|
|
4676
|
-
}
|
|
4677
|
-
|
|
4678
|
-
console.log(result.prompt);
|
|
4679
|
-
if (parsed.output) {
|
|
4680
|
-
console.log(`\n${DIM}saved:${RESET} ${parsed.output}`);
|
|
4681
|
-
}
|
|
4682
|
-
}
|
|
4683
|
-
|
|
4684
|
-
function cmdSchema(args = []) {
|
|
4685
|
-
const bundle = loadDelegatorSchemaBundle();
|
|
4686
|
-
const selector = String(args[0] || "").trim();
|
|
4687
|
-
const toolEntry = Array.isArray(bundle["x-triflux-mcp-tools"])
|
|
4688
|
-
? bundle["x-triflux-mcp-tools"].find((tool) => tool.name === selector)
|
|
4689
|
-
: null;
|
|
4690
|
-
|
|
4691
|
-
if (isHelpArg(selector)) {
|
|
4692
|
-
printCommandHelp("schema");
|
|
4693
|
-
return;
|
|
4694
|
-
}
|
|
4695
|
-
|
|
4696
|
-
if (!selector) {
|
|
4697
|
-
printJson({
|
|
4698
|
-
$schema: bundle.$schema,
|
|
4699
|
-
title: "Triflux CLI Schema Bundle",
|
|
4700
|
-
global_options: [
|
|
4701
|
-
{
|
|
4702
|
-
name: "--json",
|
|
4703
|
-
type: "boolean",
|
|
4704
|
-
description: "지원 커맨드의 출력을 JSON으로 전환",
|
|
4705
|
-
},
|
|
4706
|
-
],
|
|
4707
|
-
commands: CLI_COMMAND_SCHEMAS,
|
|
4708
|
-
hub_tools: bundle,
|
|
4709
|
-
});
|
|
4710
|
-
return;
|
|
4711
|
-
}
|
|
4712
|
-
|
|
4713
|
-
if (CLI_COMMAND_SCHEMAS[selector]) {
|
|
4714
|
-
printJson({
|
|
4715
|
-
command: selector,
|
|
4716
|
-
...CLI_COMMAND_SCHEMAS[selector],
|
|
4717
|
-
});
|
|
4718
|
-
return;
|
|
4719
|
-
}
|
|
4720
|
-
|
|
4721
|
-
if (toolEntry) {
|
|
4722
|
-
printJson({
|
|
4723
|
-
tool: toolEntry.name,
|
|
4724
|
-
description: toolEntry.description,
|
|
4725
|
-
pipeAction: toolEntry.pipeAction,
|
|
4726
|
-
inputSchema: bundle.$defs?.[toolEntry.inputSchemaDef] || null,
|
|
4727
|
-
outputSchema: bundle.$defs?.[toolEntry.outputSchemaDef] || null,
|
|
4728
|
-
});
|
|
4729
|
-
return;
|
|
4730
|
-
}
|
|
4731
|
-
|
|
4732
|
-
throw createCliError(`알 수 없는 schema 대상: ${selector}`, {
|
|
4733
|
-
exitCode: EXIT_ARG_ERROR,
|
|
4734
|
-
reason: "argError",
|
|
4735
|
-
fix: "tfx schema 또는 tfx schema <command>를 실행해 사용 가능한 대상을 확인하세요.",
|
|
4736
|
-
});
|
|
4737
|
-
}
|
|
4738
|
-
|
|
4739
3457
|
function cmdMcp(args = [], options = {}) {
|
|
4740
3458
|
const { json = false } = options;
|
|
4741
3459
|
const sub = String(args[0] || "list")
|
|
@@ -4970,82 +3688,32 @@ function cmdMcp(args = [], options = {}) {
|
|
|
4970
3688
|
}
|
|
4971
3689
|
}
|
|
4972
3690
|
|
|
4973
|
-
function checkForUpdate() {
|
|
4974
|
-
const cacheFile = join(CLAUDE_DIR, "cache", "triflux-update-check.json");
|
|
4975
|
-
const cacheDir = dirname(cacheFile);
|
|
4976
|
-
|
|
4977
|
-
// 캐시 확인 (1시간 이내면 캐시 사용)
|
|
4978
|
-
try {
|
|
4979
|
-
if (existsSync(cacheFile)) {
|
|
4980
|
-
const cache = JSON.parse(readFileSync(cacheFile, "utf8"));
|
|
4981
|
-
if (Date.now() - cache.timestamp < 3600000) {
|
|
4982
|
-
return cache.latest !== PKG.version ? cache.latest : null;
|
|
4983
|
-
}
|
|
4984
|
-
}
|
|
4985
|
-
} catch {}
|
|
4986
|
-
|
|
4987
|
-
// npm registry 조회
|
|
4988
|
-
try {
|
|
4989
|
-
const result = execSync("npm view triflux version", {
|
|
4990
|
-
encoding: "utf8",
|
|
4991
|
-
timeout: 5000,
|
|
4992
|
-
stdio: ["pipe", "pipe", "ignore"],
|
|
4993
|
-
windowsHide: true,
|
|
4994
|
-
}).trim();
|
|
4995
|
-
|
|
4996
|
-
if (!existsSync(cacheDir)) mkdirSync(cacheDir, { recursive: true });
|
|
4997
|
-
writeFileSync(
|
|
4998
|
-
cacheFile,
|
|
4999
|
-
JSON.stringify({ latest: result, timestamp: Date.now() }),
|
|
5000
|
-
);
|
|
5001
|
-
|
|
5002
|
-
return result !== PKG.version ? result : null;
|
|
5003
|
-
} catch {
|
|
5004
|
-
return null;
|
|
5005
|
-
}
|
|
5006
|
-
}
|
|
5007
|
-
|
|
5008
3691
|
function cmdHelp() {
|
|
5009
|
-
const latestVer = checkForUpdate();
|
|
5010
|
-
const updateNotice = latestVer
|
|
5011
|
-
? `\n ${YELLOW}${BOLD}↑ v${latestVer} 사용 가능${RESET} ${GRAY}npm update -g triflux${RESET}\n`
|
|
5012
|
-
: "";
|
|
5013
|
-
|
|
5014
3692
|
console.log(`
|
|
5015
3693
|
${AMBER}${BOLD}⬡ triflux${RESET} ${DIM}v${PKG.version}${RESET}
|
|
5016
3694
|
${GRAY}CLI-first multi-model orchestrator for Claude Code${RESET}
|
|
5017
|
-
|
|
3695
|
+
|
|
5018
3696
|
${LINE}
|
|
5019
3697
|
|
|
5020
3698
|
${BOLD}Commands${RESET}
|
|
5021
3699
|
|
|
5022
3700
|
${WHITE_BRIGHT}tfx setup${RESET} ${GRAY}파일 동기화 + HUD 설정${RESET}
|
|
5023
3701
|
${DIM} --dry-run${RESET} ${GRAY}변경 예정 작업을 JSON으로 미리보기${RESET}
|
|
3702
|
+
${DIM} --mods${RESET} ${GRAY}Claude Code mods 설치 (2.1.287 이상)${RESET}
|
|
5024
3703
|
${WHITE_BRIGHT}tfx doctor${RESET} ${GRAY}CLI 진단 + 이슈 확인${RESET}
|
|
5025
3704
|
${DIM} --fix${RESET} ${GRAY}진단 + 자동 수정${RESET}
|
|
5026
3705
|
${DIM} --reset${RESET} ${GRAY}캐시 전체 초기화${RESET}
|
|
5027
3706
|
${DIM} --json${RESET} ${GRAY}구조화된 진단 결과 JSON 출력${RESET}
|
|
5028
|
-
${WHITE_BRIGHT}tfx auto${RESET} ${GRAY}tfx-auto 라우팅 결정 미리보기 (--cli codex|antigravity|claude)${RESET}
|
|
5029
|
-
${WHITE_BRIGHT}tfx stealth-fetch${RESET} ${GRAY}cloakbrowser 기반 URL fetch (JSON stdout)${RESET}
|
|
5030
3707
|
${WHITE_BRIGHT}tfx mcp${RESET} ${GRAY}MCP registry 관리 (list/sync/add/remove)${RESET}
|
|
5031
3708
|
${WHITE_BRIGHT}tfx update${RESET} ${GRAY}최신 안정 버전으로 업데이트${RESET}
|
|
5032
3709
|
${DIM} --dev / dev${RESET} ${GRAY}dev 태그로 업데이트${RESET}
|
|
5033
3710
|
${WHITE_BRIGHT}tfx list${RESET} ${GRAY}설치된 스킬 목록${RESET}
|
|
5034
|
-
${WHITE_BRIGHT}tfx
|
|
5035
|
-
${WHITE_BRIGHT}tfx schema${RESET} ${GRAY}CLI/Hub schema JSON 출력${RESET}
|
|
5036
|
-
${WHITE_BRIGHT}tfx hub${RESET} ${GRAY}MCP 메시지 버스 관리 (start/stop/status)${RESET}
|
|
5037
|
-
${WHITE_BRIGHT}tfx multi${RESET} ${GRAY}멀티-CLI 팀 모드 (tmux + Hub)${RESET}
|
|
5038
|
-
${WHITE_BRIGHT}tfx swarm${RESET} ${GRAY}PRD 기반 worktree 격리 병렬 실행 (run/plan/list)${RESET}
|
|
5039
|
-
${WHITE_BRIGHT}tfx synapse${RESET} ${GRAY}스웜 세션 registry 조회 / lease 관리${RESET}
|
|
5040
|
-
${WHITE_BRIGHT}tfx review${RESET} ${GRAY}Codex 기반 git diff review${RESET}
|
|
5041
|
-
${WHITE_BRIGHT}tfx why${RESET} ${GRAY}경로의 마지막 커밋 X-Intent 트레일러 추출${RESET}
|
|
5042
|
-
${WHITE_BRIGHT}tfx codex-team${RESET} ${GRAY}Codex 전용 팀 모드 (기본 lead/agents: codex)${RESET}
|
|
5043
|
-
${WHITE_BRIGHT}tfx notion-read${RESET} ${GRAY}Notion 페이지 → 마크다운 (Codex/Gemini MCP)${RESET}
|
|
3711
|
+
${WHITE_BRIGHT}tfx multi${RESET} ${GRAY}멀티-CLI 팀 모드 (tmux)${RESET}
|
|
5044
3712
|
${WHITE_BRIGHT}tfx version${RESET} ${GRAY}버전 표시${RESET}
|
|
5045
3713
|
|
|
5046
3714
|
${BOLD}Skills${RESET} ${GRAY}(Claude Code 슬래시 커맨드)${RESET}
|
|
5047
3715
|
|
|
5048
|
-
${AMBER}/tfx-auto${RESET} ${GRAY}자동 분류 + 병렬 실행 (--cli codex|antigravity|claude, --parallel N
|
|
3716
|
+
${AMBER}/tfx-auto${RESET} ${GRAY}자동 분류 + 병렬 실행 (--cli codex|antigravity|claude, --parallel N)${RESET}
|
|
5049
3717
|
${AMBER}/tfx-setup${RESET} ${GRAY}HUD 설정 + 진단${RESET}
|
|
5050
3718
|
${YELLOW}/tfx-doctor${RESET} ${GRAY}진단 + 수리 + 캐시 초기화${RESET}
|
|
5051
3719
|
|
|
@@ -5054,896 +3722,29 @@ ${updateNotice}
|
|
|
5054
3722
|
`);
|
|
5055
3723
|
}
|
|
5056
3724
|
|
|
5057
|
-
|
|
5058
|
-
const sub = String(args[0] || "").toLowerCase();
|
|
5059
|
-
const passthrough = new Set([
|
|
5060
|
-
"status",
|
|
5061
|
-
"attach",
|
|
5062
|
-
"stop",
|
|
5063
|
-
"kill",
|
|
5064
|
-
"send",
|
|
5065
|
-
"list",
|
|
5066
|
-
"help",
|
|
5067
|
-
"--help",
|
|
5068
|
-
"-h",
|
|
5069
|
-
"tasks",
|
|
5070
|
-
"task",
|
|
5071
|
-
"focus",
|
|
5072
|
-
"interrupt",
|
|
5073
|
-
"control",
|
|
5074
|
-
"debug",
|
|
5075
|
-
]);
|
|
5076
|
-
|
|
5077
|
-
if (sub === "help" || sub === "--help" || sub === "-h") {
|
|
5078
|
-
console.log(`
|
|
5079
|
-
${AMBER}${BOLD}⬡ tfx codex-team${RESET}
|
|
3725
|
+
// ── 메인 ──
|
|
5080
3726
|
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
3727
|
+
function readCliOptionValue(args, name) {
|
|
3728
|
+
const index = args.indexOf(name);
|
|
3729
|
+
if (index === -1) return null;
|
|
3730
|
+
const value = args[index + 1];
|
|
3731
|
+
if (!value || value.startsWith("--")) return null;
|
|
3732
|
+
return value;
|
|
3733
|
+
}
|
|
5087
3734
|
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
3735
|
+
function parsePositiveIntegerOption(args, name, fallback) {
|
|
3736
|
+
const value = readCliOptionValue(args, name);
|
|
3737
|
+
if (value == null) return fallback;
|
|
3738
|
+
const parsed = Number.parseInt(value, 10);
|
|
3739
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
3740
|
+
throw createCliError(`${name} 값은 0 이상의 정수여야 합니다`, {
|
|
3741
|
+
exitCode: EXIT_ARG_ERROR,
|
|
3742
|
+
reason: "argError",
|
|
3743
|
+
fix: `${name} ${fallback}`,
|
|
3744
|
+
});
|
|
5091
3745
|
}
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
const hasLead = args.includes("--lead");
|
|
5095
|
-
const hasLayout = args.includes("--layout");
|
|
5096
|
-
const isControl = passthrough.has(sub);
|
|
5097
|
-
const normalizedArgs =
|
|
5098
|
-
isControl && args.length ? [sub, ...args.slice(1)] : args;
|
|
5099
|
-
const inject = [];
|
|
5100
|
-
if (!isControl && !hasLead) inject.push("--lead", "codex");
|
|
5101
|
-
if (!isControl && !hasAgents) inject.push("--agents", "codex,codex");
|
|
5102
|
-
if (!isControl && !hasLayout) inject.push("--layout", "1xN");
|
|
5103
|
-
const forwarded = isControl ? normalizedArgs : [...inject, ...args];
|
|
5104
|
-
|
|
5105
|
-
const prevArgv = process.argv;
|
|
5106
|
-
const prevProfile = process.env.TFX_TEAM_PROFILE;
|
|
5107
|
-
process.env.TFX_TEAM_PROFILE = "codex-team";
|
|
5108
|
-
const { pathToFileURL } = await import("node:url");
|
|
5109
|
-
const { cmdTeam } = await import(
|
|
5110
|
-
pathToFileURL(join(PKG_ROOT, "hub", "team", "cli", "index.mjs")).href
|
|
5111
|
-
);
|
|
5112
|
-
process.argv = [prevArgv[0], prevArgv[1], "team", ...forwarded];
|
|
5113
|
-
try {
|
|
5114
|
-
await cmdTeam();
|
|
5115
|
-
} finally {
|
|
5116
|
-
process.argv = prevArgv;
|
|
5117
|
-
if (typeof prevProfile === "string")
|
|
5118
|
-
process.env.TFX_TEAM_PROFILE = prevProfile;
|
|
5119
|
-
else delete process.env.TFX_TEAM_PROFILE;
|
|
5120
|
-
}
|
|
5121
|
-
}
|
|
5122
|
-
|
|
5123
|
-
// ── Hub preflight 체크 (multi/auto 실행 전) ──
|
|
5124
|
-
|
|
5125
|
-
async function checkHubRunning() {
|
|
5126
|
-
// preflight 캐시 먼저 확인 — 히트 시 fetch 스킵
|
|
5127
|
-
try {
|
|
5128
|
-
const cacheFile = join(homedir(), ".claude", "cache", "tfx-preflight.json");
|
|
5129
|
-
const cached = JSON.parse(readFileSync(cacheFile, "utf8"));
|
|
5130
|
-
if (Date.now() - cached.timestamp < 3_600_000 && cached.hub?.ok)
|
|
5131
|
-
return true;
|
|
5132
|
-
} catch {}
|
|
5133
|
-
const port = Number(process.env.TFX_HUB_PORT || "27888");
|
|
5134
|
-
try {
|
|
5135
|
-
const res = await fetch(`http://127.0.0.1:${port}/status`, {
|
|
5136
|
-
signal: AbortSignal.timeout(2000),
|
|
5137
|
-
});
|
|
5138
|
-
if (res.ok) return true;
|
|
5139
|
-
} catch {}
|
|
5140
|
-
console.log("");
|
|
5141
|
-
warn(`${AMBER}tfx-hub${RESET}가 실행되고 있지 않습니다.`);
|
|
5142
|
-
info(
|
|
5143
|
-
`Hub 없이 실행하면 Claude 네이티브 에이전트로 폴백되어 토큰이 소비됩니다.`,
|
|
5144
|
-
);
|
|
5145
|
-
info(`Codex(무료) 위임을 활용하려면 먼저 Hub를 시작하세요:\n`);
|
|
5146
|
-
console.log(` ${WHITE_BRIGHT}tfx hub start${RESET}\n`);
|
|
5147
|
-
return false;
|
|
5148
|
-
}
|
|
5149
|
-
|
|
5150
|
-
// ── hub 서브커맨드 ──
|
|
5151
|
-
|
|
5152
|
-
const HUB_PID_DIR = join(homedir(), ".claude", "cache", "tfx-hub");
|
|
5153
|
-
const HUB_PID_FILE = join(HUB_PID_DIR, "hub.pid");
|
|
5154
|
-
|
|
5155
|
-
function sleepMs(ms) {
|
|
5156
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
5157
|
-
}
|
|
5158
|
-
|
|
5159
|
-
function stopHubForUpdate() {
|
|
5160
|
-
if (!existsSync(HUB_PID_FILE)) return null;
|
|
5161
|
-
let info = null;
|
|
5162
|
-
try {
|
|
5163
|
-
info = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
5164
|
-
process.kill(info.pid, 0);
|
|
5165
|
-
} catch {
|
|
5166
|
-
try {
|
|
5167
|
-
unlinkSync(HUB_PID_FILE);
|
|
5168
|
-
} catch {}
|
|
5169
|
-
return null;
|
|
5170
|
-
}
|
|
5171
|
-
|
|
5172
|
-
try {
|
|
5173
|
-
if (process.platform === "win32") {
|
|
5174
|
-
execFileSync("taskkill", ["/PID", String(info.pid), "/T", "/F"], {
|
|
5175
|
-
stdio: ["pipe", "pipe", "ignore"],
|
|
5176
|
-
timeout: 10000,
|
|
5177
|
-
windowsHide: true,
|
|
5178
|
-
});
|
|
5179
|
-
} else {
|
|
5180
|
-
process.kill(info.pid, "SIGTERM");
|
|
5181
|
-
}
|
|
5182
|
-
} catch {
|
|
5183
|
-
try {
|
|
5184
|
-
process.kill(info.pid, "SIGKILL");
|
|
5185
|
-
} catch {}
|
|
5186
|
-
}
|
|
5187
|
-
|
|
5188
|
-
// Windows에서 better-sqlite3.node 파일 핸들 해제 대기
|
|
5189
|
-
// taskkill 후 프로세스 종료 + 파일 핸들 해제까지 최대 5초
|
|
5190
|
-
const sqliteNode = join(
|
|
5191
|
-
PKG_ROOT,
|
|
5192
|
-
"node_modules",
|
|
5193
|
-
"better-sqlite3",
|
|
5194
|
-
"build",
|
|
5195
|
-
"Release",
|
|
5196
|
-
"better_sqlite3.node",
|
|
5197
|
-
);
|
|
5198
|
-
for (let i = 0; i < 10; i++) {
|
|
5199
|
-
sleepMs(500);
|
|
5200
|
-
try {
|
|
5201
|
-
process.kill(info.pid, 0);
|
|
5202
|
-
} catch {
|
|
5203
|
-
break;
|
|
5204
|
-
}
|
|
5205
|
-
}
|
|
5206
|
-
// 파일 잠금 해제 확인 (Windows EBUSY 방지)
|
|
5207
|
-
if (existsSync(sqliteNode)) {
|
|
5208
|
-
for (let i = 0; i < 6; i++) {
|
|
5209
|
-
try {
|
|
5210
|
-
const fd = openSync(sqliteNode, "r");
|
|
5211
|
-
closeSync(fd);
|
|
5212
|
-
break;
|
|
5213
|
-
} catch {
|
|
5214
|
-
sleepMs(500);
|
|
5215
|
-
}
|
|
5216
|
-
}
|
|
5217
|
-
}
|
|
5218
|
-
try {
|
|
5219
|
-
unlinkSync(HUB_PID_FILE);
|
|
5220
|
-
} catch {}
|
|
5221
|
-
return info;
|
|
5222
|
-
}
|
|
5223
|
-
|
|
5224
|
-
function openHubLogFd() {
|
|
5225
|
-
try {
|
|
5226
|
-
const logDir = join(homedir(), ".claude", "cache", "tfx-hub");
|
|
5227
|
-
mkdirSync(logDir, { recursive: true });
|
|
5228
|
-
return openSync(join(logDir, "hub.log"), "a");
|
|
5229
|
-
} catch {
|
|
5230
|
-
return undefined;
|
|
5231
|
-
}
|
|
5232
|
-
}
|
|
5233
|
-
|
|
5234
|
-
function startHubAfterUpdate(info) {
|
|
5235
|
-
if (!info) return false;
|
|
5236
|
-
const serverPath = join(PKG_ROOT, "hub", "server.mjs");
|
|
5237
|
-
if (!existsSync(serverPath)) return false;
|
|
5238
|
-
const port =
|
|
5239
|
-
Number(info?.port) > 0
|
|
5240
|
-
? String(info.port)
|
|
5241
|
-
: String(process.env.TFX_HUB_PORT || "27888");
|
|
5242
|
-
|
|
5243
|
-
try {
|
|
5244
|
-
const logFd = openHubLogFd();
|
|
5245
|
-
const child = spawn(process.execPath, [serverPath], {
|
|
5246
|
-
env: { ...process.env, TFX_HUB_PORT: port },
|
|
5247
|
-
stdio: ["ignore", logFd ?? "ignore", logFd ?? "ignore"],
|
|
5248
|
-
detached: true,
|
|
5249
|
-
windowsHide: true,
|
|
5250
|
-
});
|
|
5251
|
-
child.unref();
|
|
5252
|
-
if (logFd !== undefined) {
|
|
5253
|
-
try {
|
|
5254
|
-
closeSync(logFd);
|
|
5255
|
-
} catch {}
|
|
5256
|
-
}
|
|
5257
|
-
return true;
|
|
5258
|
-
} catch {
|
|
5259
|
-
return false;
|
|
5260
|
-
}
|
|
5261
|
-
}
|
|
5262
|
-
|
|
5263
|
-
// 설치된 CLI에 tfx-hub MCP 서버 자동 등록 (1회 설정, 이후 재실행 불필요)
|
|
5264
|
-
function autoRegisterMcp(mcpUrl, { codexEnabled = false } = {}) {
|
|
5265
|
-
section("MCP 자동 등록");
|
|
5266
|
-
|
|
5267
|
-
// Codex — config.json에 기본 disabled 엔트리로 등록.
|
|
5268
|
-
// Hub startup must keep the MCP config fresh even on CI/dev machines where
|
|
5269
|
-
// the Codex CLI binary itself is not installed.
|
|
5270
|
-
try {
|
|
5271
|
-
const result = ensureCodexHubServerConfig({
|
|
5272
|
-
mcpUrl,
|
|
5273
|
-
createIfMissing: true,
|
|
5274
|
-
enabled: codexEnabled,
|
|
5275
|
-
});
|
|
5276
|
-
if (!result.ok) throw new Error(result.reason || "unknown");
|
|
5277
|
-
const suffix = which("codex") ? "" : " (CLI 미설치)";
|
|
5278
|
-
if (result.changed) {
|
|
5279
|
-
ok(
|
|
5280
|
-
`Codex: config.json에 등록 완료 (${codexEnabled ? "enabled" : "기본 disabled"})${suffix}`,
|
|
5281
|
-
);
|
|
5282
|
-
} else {
|
|
5283
|
-
ok(
|
|
5284
|
-
`Codex: 이미 등록됨 (${codexEnabled ? "enabled" : "기본 disabled"})${suffix}`,
|
|
5285
|
-
);
|
|
5286
|
-
}
|
|
5287
|
-
} catch (e) {
|
|
5288
|
-
warn(`Codex 등록 실패: ${e.message}`);
|
|
5289
|
-
}
|
|
5290
|
-
|
|
5291
|
-
// Gemini — settings.json 직접 수정
|
|
5292
|
-
if (which("gemini")) {
|
|
5293
|
-
try {
|
|
5294
|
-
const geminiDir = join(homedir(), ".gemini");
|
|
5295
|
-
const settingsFile = join(geminiDir, "settings.json");
|
|
5296
|
-
let settings = {};
|
|
5297
|
-
if (existsSync(settingsFile))
|
|
5298
|
-
settings = JSON.parse(readFileSync(settingsFile, "utf8"));
|
|
5299
|
-
if (!settings.mcpServers) settings.mcpServers = {};
|
|
5300
|
-
const current = settings.mcpServers["tfx-hub"];
|
|
5301
|
-
if (!current || current.url !== mcpUrl) {
|
|
5302
|
-
settings.mcpServers["tfx-hub"] = {
|
|
5303
|
-
...(current && typeof current === "object" ? current : {}),
|
|
5304
|
-
url: mcpUrl,
|
|
5305
|
-
};
|
|
5306
|
-
if (!existsSync(geminiDir)) mkdirSync(geminiDir, { recursive: true });
|
|
5307
|
-
writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + "\n");
|
|
5308
|
-
ok(
|
|
5309
|
-
current
|
|
5310
|
-
? "Gemini: settings.json URL 갱신 완료"
|
|
5311
|
-
: "Gemini: settings.json에 등록 완료",
|
|
5312
|
-
);
|
|
5313
|
-
} else {
|
|
5314
|
-
ok("Gemini: 이미 등록됨");
|
|
5315
|
-
}
|
|
5316
|
-
} catch (e) {
|
|
5317
|
-
warn(`Gemini 등록 실패: ${e.message}`);
|
|
5318
|
-
}
|
|
5319
|
-
} else {
|
|
5320
|
-
info("Gemini: 미설치 (건너뜀)");
|
|
5321
|
-
}
|
|
5322
|
-
|
|
5323
|
-
// Claude — .claude/mcp.json에 등록 (Claude Code 공식 경로)
|
|
5324
|
-
try {
|
|
5325
|
-
const claudeDir = join(process.cwd(), ".claude");
|
|
5326
|
-
if (!existsSync(claudeDir)) mkdirSync(claudeDir, { recursive: true });
|
|
5327
|
-
const mcpJsonPath = join(claudeDir, "mcp.json");
|
|
5328
|
-
let mcpJson = {};
|
|
5329
|
-
if (existsSync(mcpJsonPath))
|
|
5330
|
-
mcpJson = JSON.parse(readFileSync(mcpJsonPath, "utf8"));
|
|
5331
|
-
if (!mcpJson.mcpServers) mcpJson.mcpServers = {};
|
|
5332
|
-
const current = mcpJson.mcpServers["tfx-hub"];
|
|
5333
|
-
if (!current || current.type !== "http" || current.url !== mcpUrl) {
|
|
5334
|
-
mcpJson.mcpServers["tfx-hub"] = {
|
|
5335
|
-
...(current && typeof current === "object" ? current : {}),
|
|
5336
|
-
type: "http",
|
|
5337
|
-
url: mcpUrl,
|
|
5338
|
-
};
|
|
5339
|
-
writeFileSync(mcpJsonPath, JSON.stringify(mcpJson, null, 2) + "\n");
|
|
5340
|
-
ok(
|
|
5341
|
-
current
|
|
5342
|
-
? "Claude: .claude/mcp.json URL/type 갱신 완료"
|
|
5343
|
-
: "Claude: .claude/mcp.json에 등록 완료",
|
|
5344
|
-
);
|
|
5345
|
-
} else {
|
|
5346
|
-
ok("Claude: 이미 등록됨");
|
|
5347
|
-
}
|
|
5348
|
-
} catch (e) {
|
|
5349
|
-
warn(`Claude 등록 실패: ${e.message}`);
|
|
5350
|
-
}
|
|
5351
|
-
}
|
|
5352
|
-
|
|
5353
|
-
async function cmdHub(args = [], options = {}) {
|
|
5354
|
-
const { json = false } = options;
|
|
5355
|
-
const sub = args[0] || "status";
|
|
5356
|
-
const defaultPortRaw = Number(process.env.TFX_HUB_PORT || "27888");
|
|
5357
|
-
const probePort =
|
|
5358
|
-
Number.isFinite(defaultPortRaw) && defaultPortRaw > 0
|
|
5359
|
-
? defaultPortRaw
|
|
5360
|
-
: 27888;
|
|
5361
|
-
const formatHostForUrl = (host) => (host.includes(":") ? `[${host}]` : host);
|
|
5362
|
-
const probeHubStatus = async (
|
|
5363
|
-
host = "127.0.0.1",
|
|
5364
|
-
port = probePort,
|
|
5365
|
-
timeoutMs = 3000,
|
|
5366
|
-
) => {
|
|
5367
|
-
try {
|
|
5368
|
-
const res = await fetch(
|
|
5369
|
-
`http://${formatHostForUrl(host)}:${port}/status`,
|
|
5370
|
-
{
|
|
5371
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
5372
|
-
},
|
|
5373
|
-
);
|
|
5374
|
-
if (!res.ok) return null;
|
|
5375
|
-
const data = await res.json();
|
|
5376
|
-
return data?.hub ? data : null;
|
|
5377
|
-
} catch {
|
|
5378
|
-
return null;
|
|
5379
|
-
}
|
|
5380
|
-
};
|
|
5381
|
-
const recoverPidFile = (statusData, defaultHost = "127.0.0.1") => {
|
|
5382
|
-
const pid = Number(statusData?.pid);
|
|
5383
|
-
const port = Number(statusData?.port) || probePort;
|
|
5384
|
-
if (!Number.isFinite(pid) || pid <= 0) return;
|
|
5385
|
-
try {
|
|
5386
|
-
mkdirSync(HUB_PID_DIR, { recursive: true });
|
|
5387
|
-
writeFileSync(
|
|
5388
|
-
HUB_PID_FILE,
|
|
5389
|
-
JSON.stringify({
|
|
5390
|
-
pid,
|
|
5391
|
-
port,
|
|
5392
|
-
host: defaultHost,
|
|
5393
|
-
url: `http://${formatHostForUrl(defaultHost)}:${port}/mcp`,
|
|
5394
|
-
started: Date.now(),
|
|
5395
|
-
}),
|
|
5396
|
-
);
|
|
5397
|
-
} catch {}
|
|
5398
|
-
};
|
|
5399
|
-
const emitHubStatus = (payload) => {
|
|
5400
|
-
if (!json) return false;
|
|
5401
|
-
printJson(payload);
|
|
5402
|
-
return true;
|
|
5403
|
-
};
|
|
5404
|
-
|
|
5405
|
-
switch (sub) {
|
|
5406
|
-
case "start": {
|
|
5407
|
-
// 이미 실행 중인지 확인
|
|
5408
|
-
if (existsSync(HUB_PID_FILE)) {
|
|
5409
|
-
try {
|
|
5410
|
-
const info = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
5411
|
-
process.kill(info.pid, 0); // 프로세스 존재 확인
|
|
5412
|
-
const host =
|
|
5413
|
-
typeof info.host === "string" && info.host.trim()
|
|
5414
|
-
? info.host.trim()
|
|
5415
|
-
: "127.0.0.1";
|
|
5416
|
-
const port = Number(info.port) || probePort;
|
|
5417
|
-
const probed = await probeHubStatus(host, port, 1500);
|
|
5418
|
-
if (probed?.hub) {
|
|
5419
|
-
const url = `http://${formatHostForUrl(host)}:${probed.port || port}/mcp`;
|
|
5420
|
-
recoverPidFile(probed, host);
|
|
5421
|
-
autoRegisterMcp(url, { codexEnabled: true });
|
|
5422
|
-
console.log(
|
|
5423
|
-
`\n ${YELLOW}⚠${RESET} hub 이미 실행 중 (PID ${probed.pid || info.pid}, ${url})\n`,
|
|
5424
|
-
);
|
|
5425
|
-
return;
|
|
5426
|
-
}
|
|
5427
|
-
warn(
|
|
5428
|
-
`stale hub PID 파일 감지: PID ${info.pid}는 살아있지만 hub status 응답이 없음. PID 파일을 정리합니다.`,
|
|
5429
|
-
);
|
|
5430
|
-
unlinkSync(HUB_PID_FILE);
|
|
5431
|
-
} catch {
|
|
5432
|
-
// PID 파일 있지만 프로세스 없음 — 정리
|
|
5433
|
-
try {
|
|
5434
|
-
unlinkSync(HUB_PID_FILE);
|
|
5435
|
-
} catch {}
|
|
5436
|
-
}
|
|
5437
|
-
}
|
|
5438
|
-
|
|
5439
|
-
const portArg = args.indexOf("--port");
|
|
5440
|
-
const port = portArg !== -1 ? args[portArg + 1] : String(probePort);
|
|
5441
|
-
const serverPath = join(PKG_ROOT, "hub", "server.mjs");
|
|
5442
|
-
|
|
5443
|
-
if (!existsSync(serverPath)) {
|
|
5444
|
-
throw createCliError("hub/server.mjs 없음 — hub 모듈이 설치되지 않음", {
|
|
5445
|
-
exitCode: EXIT_HUB_ERROR,
|
|
5446
|
-
reason: "hubError",
|
|
5447
|
-
fix: "hub 모듈이 포함된 triflux 설치본인지 확인한 뒤 다시 실행하세요.",
|
|
5448
|
-
});
|
|
5449
|
-
}
|
|
5450
|
-
|
|
5451
|
-
// Issue #102 + hub-detach fix: spawn stdout/stderr 를 두 채널로 redirect.
|
|
5452
|
-
// - startupErrPath (tmp): 3초 안의 startup 실패 진단 (성공 시 cleanup)
|
|
5453
|
-
// - hub.log (cache): runtime stdout/stderr 영구 보존 (crash 추적)
|
|
5454
|
-
// detached spawn 은 pipe 유지가 까다로우니 fd 리다이렉트로 접근.
|
|
5455
|
-
const { openSync: _openSync, closeSync: _closeSync } = await import(
|
|
5456
|
-
"node:fs"
|
|
5457
|
-
);
|
|
5458
|
-
const { tmpdir: _tmpdir } = await import("node:os");
|
|
5459
|
-
const startupErrPath = join(
|
|
5460
|
-
_tmpdir(),
|
|
5461
|
-
`tfx-hub-start-${Date.now()}-${process.pid}.err`,
|
|
5462
|
-
);
|
|
5463
|
-
let errFd;
|
|
5464
|
-
try {
|
|
5465
|
-
errFd = _openSync(startupErrPath, "w");
|
|
5466
|
-
} catch {
|
|
5467
|
-
errFd = undefined;
|
|
5468
|
-
}
|
|
5469
|
-
const logFd = openHubLogFd();
|
|
5470
|
-
|
|
5471
|
-
const child = spawn(process.execPath, [serverPath], {
|
|
5472
|
-
env: { ...process.env, TFX_HUB_PORT: port },
|
|
5473
|
-
stdio: ["ignore", logFd ?? "ignore", errFd ?? logFd ?? "ignore"],
|
|
5474
|
-
detached: true,
|
|
5475
|
-
windowsHide: true,
|
|
5476
|
-
});
|
|
5477
|
-
child.unref();
|
|
5478
|
-
if (errFd !== undefined) {
|
|
5479
|
-
try {
|
|
5480
|
-
_closeSync(errFd);
|
|
5481
|
-
} catch {}
|
|
5482
|
-
}
|
|
5483
|
-
if (logFd !== undefined) {
|
|
5484
|
-
try {
|
|
5485
|
-
_closeSync(logFd);
|
|
5486
|
-
} catch {}
|
|
5487
|
-
}
|
|
5488
|
-
|
|
5489
|
-
// PID 파일 확인 (최대 3초 대기, 100ms 폴링)
|
|
5490
|
-
let started = false;
|
|
5491
|
-
const deadline = Date.now() + 3000;
|
|
5492
|
-
while (Date.now() < deadline) {
|
|
5493
|
-
if (existsSync(HUB_PID_FILE)) {
|
|
5494
|
-
started = true;
|
|
5495
|
-
break;
|
|
5496
|
-
}
|
|
5497
|
-
await new Promise((r) => setTimeout(r, 100));
|
|
5498
|
-
}
|
|
5499
|
-
|
|
5500
|
-
if (started) {
|
|
5501
|
-
const hubInfo = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
5502
|
-
console.log(`\n ${GREEN_BRIGHT}✓${RESET} ${BOLD}tfx-hub 시작${RESET}`);
|
|
5503
|
-
console.log(` URL: ${AMBER}${hubInfo.url}${RESET}`);
|
|
5504
|
-
console.log(` PID: ${hubInfo.pid}`);
|
|
5505
|
-
console.log(
|
|
5506
|
-
` DB: ${DIM}${getPipelineStateDbPath(PKG_ROOT)}${RESET}`,
|
|
5507
|
-
);
|
|
5508
|
-
console.log("");
|
|
5509
|
-
autoRegisterMcp(hubInfo.url, { codexEnabled: true });
|
|
5510
|
-
console.log("");
|
|
5511
|
-
// 성공했으면 임시 stderr 파일 정리
|
|
5512
|
-
try {
|
|
5513
|
-
unlinkSync(startupErrPath);
|
|
5514
|
-
} catch {}
|
|
5515
|
-
} else {
|
|
5516
|
-
// Issue #102: 캡처된 stderr 에서 root cause 추출
|
|
5517
|
-
let rootCause = "";
|
|
5518
|
-
try {
|
|
5519
|
-
rootCause = readFileSync(startupErrPath, "utf8").trim();
|
|
5520
|
-
} catch {}
|
|
5521
|
-
|
|
5522
|
-
console.log(`\n ${YELLOW}⚠${RESET} 백그라운드 시작 실패`);
|
|
5523
|
-
|
|
5524
|
-
if (rootCause) {
|
|
5525
|
-
// 가장 유용한 에러 라인 강조 (ERR_*, Error:, throw)
|
|
5526
|
-
const highlight = rootCause
|
|
5527
|
-
.split(/\r?\n/)
|
|
5528
|
-
.find((line) => /ERR_[A-Z_]+|^Error:|cannot find/i.test(line));
|
|
5529
|
-
if (highlight) {
|
|
5530
|
-
console.log(` ${RED}▸ ${highlight.trim()}${RESET}`);
|
|
5531
|
-
}
|
|
5532
|
-
console.log(`\n ${DIM}전체 로그: ${startupErrPath}${RESET}`);
|
|
5533
|
-
// 원인별 실전 힌트
|
|
5534
|
-
if (/Cannot find package/i.test(rootCause)) {
|
|
5535
|
-
console.log(
|
|
5536
|
-
` ${DIM}힌트: \`cd ${PKG_ROOT} && npm install\` 로 의존성 복구 (특히 \`npm link\` 환경).${RESET}`,
|
|
5537
|
-
);
|
|
5538
|
-
} else if (/EADDRINUSE/i.test(rootCause)) {
|
|
5539
|
-
console.log(
|
|
5540
|
-
` ${DIM}힌트: 포트 ${port} 이 이미 사용 중. \`tfx hub stop\` 후 재시도.${RESET}`,
|
|
5541
|
-
);
|
|
5542
|
-
}
|
|
5543
|
-
} else {
|
|
5544
|
-
console.log(
|
|
5545
|
-
` ${DIM}stderr 캡처 실패 — 아래 명령으로 포그라운드 실행해 원인 확인:${RESET}`,
|
|
5546
|
-
);
|
|
5547
|
-
}
|
|
5548
|
-
|
|
5549
|
-
console.log(
|
|
5550
|
-
`\n ${DIM}포그라운드 실행: TFX_HUB_PORT=${port} node ${serverPath}${RESET}\n`,
|
|
5551
|
-
);
|
|
5552
|
-
}
|
|
5553
|
-
break;
|
|
5554
|
-
}
|
|
5555
|
-
|
|
5556
|
-
case "stop": {
|
|
5557
|
-
if (!existsSync(HUB_PID_FILE)) {
|
|
5558
|
-
const probed =
|
|
5559
|
-
(await probeHubStatus("127.0.0.1", probePort, 1500)) ||
|
|
5560
|
-
(probePort === 27888
|
|
5561
|
-
? null
|
|
5562
|
-
: await probeHubStatus("127.0.0.1", 27888, 1500));
|
|
5563
|
-
if (probed && Number.isFinite(Number(probed.pid))) {
|
|
5564
|
-
try {
|
|
5565
|
-
process.kill(Number(probed.pid), "SIGTERM");
|
|
5566
|
-
console.log(
|
|
5567
|
-
`\n ${GREEN_BRIGHT}✓${RESET} hub 종료됨 (PID ${probed.pid})${DIM} (probe)${RESET}\n`,
|
|
5568
|
-
);
|
|
5569
|
-
return;
|
|
5570
|
-
} catch {}
|
|
5571
|
-
}
|
|
5572
|
-
console.log(`\n ${DIM}hub 미실행${RESET}\n`);
|
|
5573
|
-
return;
|
|
5574
|
-
}
|
|
5575
|
-
try {
|
|
5576
|
-
const info = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
5577
|
-
process.kill(info.pid, "SIGTERM");
|
|
5578
|
-
try {
|
|
5579
|
-
unlinkSync(HUB_PID_FILE);
|
|
5580
|
-
} catch {}
|
|
5581
|
-
console.log(
|
|
5582
|
-
`\n ${GREEN_BRIGHT}✓${RESET} hub 종료됨 (PID ${info.pid})\n`,
|
|
5583
|
-
);
|
|
5584
|
-
} catch (_e) {
|
|
5585
|
-
try {
|
|
5586
|
-
unlinkSync(HUB_PID_FILE);
|
|
5587
|
-
} catch {}
|
|
5588
|
-
console.log(`\n ${DIM}hub 프로세스 없음 — PID 파일 정리됨${RESET}\n`);
|
|
5589
|
-
}
|
|
5590
|
-
break;
|
|
5591
|
-
}
|
|
5592
|
-
|
|
5593
|
-
case "status": {
|
|
5594
|
-
if (!existsSync(HUB_PID_FILE)) {
|
|
5595
|
-
const probed = await probeHubStatus();
|
|
5596
|
-
if (!probed) {
|
|
5597
|
-
const fallback =
|
|
5598
|
-
probePort === 27888
|
|
5599
|
-
? null
|
|
5600
|
-
: await probeHubStatus("127.0.0.1", 27888, 1500);
|
|
5601
|
-
if (fallback) {
|
|
5602
|
-
recoverPidFile(fallback, "127.0.0.1");
|
|
5603
|
-
if (
|
|
5604
|
-
emitHubStatus({
|
|
5605
|
-
status: "online",
|
|
5606
|
-
source: "default-port-probe",
|
|
5607
|
-
url: `http://127.0.0.1:${fallback.port || 27888}/mcp`,
|
|
5608
|
-
pid: fallback.pid,
|
|
5609
|
-
state: fallback.hub?.state || null,
|
|
5610
|
-
sessions: fallback.sessions,
|
|
5611
|
-
})
|
|
5612
|
-
)
|
|
5613
|
-
return;
|
|
5614
|
-
console.log(
|
|
5615
|
-
`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET} ${GREEN_BRIGHT}online${RESET} ${DIM}(default port probe 성공)${RESET}`,
|
|
5616
|
-
);
|
|
5617
|
-
console.log(
|
|
5618
|
-
` URL: http://127.0.0.1:${fallback.port || 27888}/mcp`,
|
|
5619
|
-
);
|
|
5620
|
-
if (fallback.pid !== undefined)
|
|
5621
|
-
console.log(` PID: ${fallback.pid}`);
|
|
5622
|
-
if (fallback.hub?.state)
|
|
5623
|
-
console.log(` State: ${fallback.hub.state}`);
|
|
5624
|
-
if (fallback.sessions !== undefined)
|
|
5625
|
-
console.log(` Sessions: ${fallback.sessions}`);
|
|
5626
|
-
console.log("");
|
|
5627
|
-
return;
|
|
5628
|
-
}
|
|
5629
|
-
if (
|
|
5630
|
-
emitHubStatus({
|
|
5631
|
-
status: "offline",
|
|
5632
|
-
source: "probe",
|
|
5633
|
-
url: null,
|
|
5634
|
-
pid: null,
|
|
5635
|
-
state: null,
|
|
5636
|
-
sessions: 0,
|
|
5637
|
-
})
|
|
5638
|
-
)
|
|
5639
|
-
return;
|
|
5640
|
-
console.log(
|
|
5641
|
-
`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET} ${RED}offline${RESET}\n`,
|
|
5642
|
-
);
|
|
5643
|
-
return;
|
|
5644
|
-
}
|
|
5645
|
-
recoverPidFile(probed, "127.0.0.1");
|
|
5646
|
-
if (
|
|
5647
|
-
emitHubStatus({
|
|
5648
|
-
status: "online",
|
|
5649
|
-
source: "probe",
|
|
5650
|
-
url: `http://127.0.0.1:${probed.port || probePort}/mcp`,
|
|
5651
|
-
pid: probed.pid,
|
|
5652
|
-
state: probed.hub?.state || null,
|
|
5653
|
-
sessions: probed.sessions,
|
|
5654
|
-
})
|
|
5655
|
-
)
|
|
5656
|
-
return;
|
|
5657
|
-
console.log(
|
|
5658
|
-
`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET} ${GREEN_BRIGHT}online${RESET} ${DIM}(pid file 없음 / probe 성공)${RESET}`,
|
|
5659
|
-
);
|
|
5660
|
-
console.log(
|
|
5661
|
-
` URL: http://127.0.0.1:${probed.port || probePort}/mcp`,
|
|
5662
|
-
);
|
|
5663
|
-
if (probed.pid !== undefined) console.log(` PID: ${probed.pid}`);
|
|
5664
|
-
if (probed.hub?.state) console.log(` State: ${probed.hub.state}`);
|
|
5665
|
-
if (probed.sessions !== undefined)
|
|
5666
|
-
console.log(` Sessions: ${probed.sessions}`);
|
|
5667
|
-
console.log("");
|
|
5668
|
-
return;
|
|
5669
|
-
}
|
|
5670
|
-
try {
|
|
5671
|
-
const info = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
5672
|
-
process.kill(info.pid, 0); // 생존 확인
|
|
5673
|
-
const uptime = Date.now() - info.started;
|
|
5674
|
-
const uptimeStr =
|
|
5675
|
-
uptime < 60000
|
|
5676
|
-
? `${Math.round(uptime / 1000)}초`
|
|
5677
|
-
: uptime < 3600000
|
|
5678
|
-
? `${Math.round(uptime / 60000)}분`
|
|
5679
|
-
: `${Math.round(uptime / 3600000)}시간`;
|
|
5680
|
-
|
|
5681
|
-
let data = null;
|
|
5682
|
-
try {
|
|
5683
|
-
const host = typeof info.host === "string" ? info.host : "127.0.0.1";
|
|
5684
|
-
const port = Number(info.port) || probePort;
|
|
5685
|
-
data = await probeHubStatus(host, port, 3000);
|
|
5686
|
-
} catch {}
|
|
5687
|
-
|
|
5688
|
-
if (
|
|
5689
|
-
emitHubStatus({
|
|
5690
|
-
status: "online",
|
|
5691
|
-
source: "pid-file",
|
|
5692
|
-
url: info.url,
|
|
5693
|
-
pid: info.pid,
|
|
5694
|
-
uptime_ms: uptime,
|
|
5695
|
-
state: data?.hub?.state || null,
|
|
5696
|
-
sessions: data?.sessions,
|
|
5697
|
-
})
|
|
5698
|
-
)
|
|
5699
|
-
return;
|
|
5700
|
-
console.log(
|
|
5701
|
-
`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET} ${GREEN_BRIGHT}online${RESET}`,
|
|
5702
|
-
);
|
|
5703
|
-
console.log(` URL: ${info.url}`);
|
|
5704
|
-
console.log(` PID: ${info.pid}`);
|
|
5705
|
-
console.log(` Uptime: ${uptimeStr}`);
|
|
5706
|
-
if (data?.hub) {
|
|
5707
|
-
console.log(` State: ${data.hub.state}`);
|
|
5708
|
-
}
|
|
5709
|
-
if (data?.sessions !== undefined) {
|
|
5710
|
-
console.log(` Sessions: ${data.sessions}`);
|
|
5711
|
-
}
|
|
5712
|
-
console.log("");
|
|
5713
|
-
} catch {
|
|
5714
|
-
try {
|
|
5715
|
-
unlinkSync(HUB_PID_FILE);
|
|
5716
|
-
} catch {}
|
|
5717
|
-
const probed = await probeHubStatus();
|
|
5718
|
-
if (!probed) {
|
|
5719
|
-
if (
|
|
5720
|
-
emitHubStatus({
|
|
5721
|
-
status: "offline",
|
|
5722
|
-
source: "stale-pid",
|
|
5723
|
-
url: null,
|
|
5724
|
-
pid: null,
|
|
5725
|
-
state: null,
|
|
5726
|
-
sessions: 0,
|
|
5727
|
-
})
|
|
5728
|
-
)
|
|
5729
|
-
break;
|
|
5730
|
-
console.log(
|
|
5731
|
-
`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET} ${RED}offline${RESET} ${DIM}(stale PID 정리됨)${RESET}\n`,
|
|
5732
|
-
);
|
|
5733
|
-
break;
|
|
5734
|
-
}
|
|
5735
|
-
recoverPidFile(probed, "127.0.0.1");
|
|
5736
|
-
if (
|
|
5737
|
-
emitHubStatus({
|
|
5738
|
-
status: "online",
|
|
5739
|
-
source: "stale-pid-probe",
|
|
5740
|
-
url: `http://127.0.0.1:${probed.port || probePort}/mcp`,
|
|
5741
|
-
pid: probed.pid,
|
|
5742
|
-
state: probed.hub?.state || null,
|
|
5743
|
-
sessions: probed.sessions,
|
|
5744
|
-
})
|
|
5745
|
-
)
|
|
5746
|
-
break;
|
|
5747
|
-
console.log(
|
|
5748
|
-
`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET} ${GREEN_BRIGHT}online${RESET} ${DIM}(stale PID 정리 후 probe 성공)${RESET}`,
|
|
5749
|
-
);
|
|
5750
|
-
console.log(
|
|
5751
|
-
` URL: http://127.0.0.1:${probed.port || probePort}/mcp`,
|
|
5752
|
-
);
|
|
5753
|
-
if (probed.pid !== undefined) console.log(` PID: ${probed.pid}`);
|
|
5754
|
-
if (probed.hub?.state) console.log(` State: ${probed.hub.state}`);
|
|
5755
|
-
if (probed.sessions !== undefined)
|
|
5756
|
-
console.log(` Sessions: ${probed.sessions}`);
|
|
5757
|
-
console.log("");
|
|
5758
|
-
}
|
|
5759
|
-
break;
|
|
5760
|
-
}
|
|
5761
|
-
|
|
5762
|
-
case "ensure": {
|
|
5763
|
-
// 사일런트 idempotent 보장 — 스킬 환경 프로브용.
|
|
5764
|
-
// Hub 살아있으면 즉시 종료, 죽어있으면 자동 시작 + ready 대기.
|
|
5765
|
-
const portArg = args.indexOf("--port");
|
|
5766
|
-
const ensurePort =
|
|
5767
|
-
portArg !== -1
|
|
5768
|
-
? args[portArg + 1]
|
|
5769
|
-
: process.env.TFX_HUB_PORT || "27888";
|
|
5770
|
-
|
|
5771
|
-
// 1. 이미 healthy?
|
|
5772
|
-
const ensureProbed = await probeHubStatus(
|
|
5773
|
-
"127.0.0.1",
|
|
5774
|
-
Number(ensurePort),
|
|
5775
|
-
1500,
|
|
5776
|
-
);
|
|
5777
|
-
if (ensureProbed?.hub?.state === "healthy") {
|
|
5778
|
-
if (json)
|
|
5779
|
-
printJson({
|
|
5780
|
-
status: "ok",
|
|
5781
|
-
pid: ensureProbed.pid,
|
|
5782
|
-
port: Number(ensurePort),
|
|
5783
|
-
});
|
|
5784
|
-
else process.stdout.write("hub: ok\n");
|
|
5785
|
-
return;
|
|
5786
|
-
}
|
|
5787
|
-
|
|
5788
|
-
// 2. PID 파일 있는데 프로세스 죽었으면 정리
|
|
5789
|
-
if (existsSync(HUB_PID_FILE)) {
|
|
5790
|
-
try {
|
|
5791
|
-
const staleInfo = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
5792
|
-
process.kill(staleInfo.pid, 0);
|
|
5793
|
-
// 프로세스 살아있지만 healthy가 아님 — 잠시 더 대기
|
|
5794
|
-
const retryDeadline = Date.now() + 3000;
|
|
5795
|
-
while (Date.now() < retryDeadline) {
|
|
5796
|
-
await new Promise((r) => setTimeout(r, 250));
|
|
5797
|
-
const retry = await probeHubStatus(
|
|
5798
|
-
"127.0.0.1",
|
|
5799
|
-
Number(ensurePort),
|
|
5800
|
-
1000,
|
|
5801
|
-
);
|
|
5802
|
-
if (retry?.hub?.state === "healthy") {
|
|
5803
|
-
if (json)
|
|
5804
|
-
printJson({
|
|
5805
|
-
status: "ok",
|
|
5806
|
-
pid: retry.pid,
|
|
5807
|
-
port: Number(ensurePort),
|
|
5808
|
-
});
|
|
5809
|
-
else process.stdout.write("hub: ok\n");
|
|
5810
|
-
return;
|
|
5811
|
-
}
|
|
5812
|
-
}
|
|
5813
|
-
} catch {
|
|
5814
|
-
try {
|
|
5815
|
-
unlinkSync(HUB_PID_FILE);
|
|
5816
|
-
} catch {}
|
|
5817
|
-
}
|
|
5818
|
-
}
|
|
5819
|
-
|
|
5820
|
-
// 3. 시작
|
|
5821
|
-
const serverPath = join(PKG_ROOT, "hub", "server.mjs");
|
|
5822
|
-
if (!existsSync(serverPath)) {
|
|
5823
|
-
if (json) printJson({ status: "error", reason: "server_missing" });
|
|
5824
|
-
else process.stderr.write("hub: server.mjs not found\n");
|
|
5825
|
-
process.exitCode = 1;
|
|
5826
|
-
return;
|
|
5827
|
-
}
|
|
5828
|
-
|
|
5829
|
-
if (process.platform === "win32") {
|
|
5830
|
-
const child = spawn(
|
|
5831
|
-
"cmd.exe",
|
|
5832
|
-
["/c", "start", "/b", "", process.execPath, serverPath],
|
|
5833
|
-
{
|
|
5834
|
-
env: { ...process.env, TFX_HUB_PORT: String(ensurePort) },
|
|
5835
|
-
stdio: "ignore",
|
|
5836
|
-
windowsHide: true,
|
|
5837
|
-
},
|
|
5838
|
-
);
|
|
5839
|
-
child.unref();
|
|
5840
|
-
} else {
|
|
5841
|
-
const child = spawn(process.execPath, [serverPath], {
|
|
5842
|
-
env: { ...process.env, TFX_HUB_PORT: String(ensurePort) },
|
|
5843
|
-
detached: true,
|
|
5844
|
-
stdio: "ignore",
|
|
5845
|
-
});
|
|
5846
|
-
child.unref();
|
|
5847
|
-
}
|
|
5848
|
-
|
|
5849
|
-
// 4. ready 대기 (최대 5초)
|
|
5850
|
-
const readyDeadline = Date.now() + 5000;
|
|
5851
|
-
while (Date.now() < readyDeadline) {
|
|
5852
|
-
await new Promise((r) => setTimeout(r, 250));
|
|
5853
|
-
if (existsSync(HUB_PID_FILE)) {
|
|
5854
|
-
const readyProbe = await probeHubStatus(
|
|
5855
|
-
"127.0.0.1",
|
|
5856
|
-
Number(ensurePort),
|
|
5857
|
-
1000,
|
|
5858
|
-
);
|
|
5859
|
-
if (readyProbe?.hub?.state === "healthy") {
|
|
5860
|
-
if (json)
|
|
5861
|
-
printJson({
|
|
5862
|
-
status: "ok",
|
|
5863
|
-
pid: readyProbe.pid,
|
|
5864
|
-
port: Number(ensurePort),
|
|
5865
|
-
started: true,
|
|
5866
|
-
});
|
|
5867
|
-
else process.stdout.write("hub: started\n");
|
|
5868
|
-
return;
|
|
5869
|
-
}
|
|
5870
|
-
}
|
|
5871
|
-
}
|
|
5872
|
-
|
|
5873
|
-
// 5. 타임아웃이지만 프로세스는 기동 중일 수 있음
|
|
5874
|
-
if (json) printJson({ status: "starting", port: Number(ensurePort) });
|
|
5875
|
-
else process.stdout.write("hub: starting\n");
|
|
5876
|
-
break;
|
|
5877
|
-
}
|
|
5878
|
-
|
|
5879
|
-
default:
|
|
5880
|
-
console.log(`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET}\n`);
|
|
5881
|
-
console.log(
|
|
5882
|
-
` ${WHITE_BRIGHT}tfx hub start${RESET} ${GRAY}허브 데몬 시작${RESET}`,
|
|
5883
|
-
);
|
|
5884
|
-
console.log(
|
|
5885
|
-
` ${DIM} --port N${RESET} ${GRAY}포트 지정 (기본 27888)${RESET}`,
|
|
5886
|
-
);
|
|
5887
|
-
console.log(
|
|
5888
|
-
` ${WHITE_BRIGHT}tfx hub stop${RESET} ${GRAY}허브 중지${RESET}`,
|
|
5889
|
-
);
|
|
5890
|
-
console.log(
|
|
5891
|
-
` ${WHITE_BRIGHT}tfx hub status${RESET} ${GRAY}상태 확인${RESET}`,
|
|
5892
|
-
);
|
|
5893
|
-
console.log(
|
|
5894
|
-
` ${WHITE_BRIGHT}tfx hub ensure${RESET} ${GRAY}헬스체크 + 자동 시작 (스킬 프로브용)${RESET}\n`,
|
|
5895
|
-
);
|
|
5896
|
-
}
|
|
5897
|
-
}
|
|
5898
|
-
|
|
5899
|
-
// ── 메인 ──
|
|
5900
|
-
|
|
5901
|
-
function readCliOptionValue(args, name) {
|
|
5902
|
-
const index = args.indexOf(name);
|
|
5903
|
-
if (index === -1) return null;
|
|
5904
|
-
const value = args[index + 1];
|
|
5905
|
-
if (!value || value.startsWith("--")) return null;
|
|
5906
|
-
return value;
|
|
5907
|
-
}
|
|
5908
|
-
|
|
5909
|
-
function parsePositiveIntegerOption(args, name, fallback) {
|
|
5910
|
-
const value = readCliOptionValue(args, name);
|
|
5911
|
-
if (value == null) return fallback;
|
|
5912
|
-
const parsed = Number.parseInt(value, 10);
|
|
5913
|
-
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
5914
|
-
throw createCliError(`${name} 값은 0 이상의 정수여야 합니다`, {
|
|
5915
|
-
exitCode: EXIT_ARG_ERROR,
|
|
5916
|
-
reason: "argError",
|
|
5917
|
-
fix: `${name} ${fallback}`,
|
|
5918
|
-
});
|
|
5919
|
-
}
|
|
5920
|
-
return parsed;
|
|
5921
|
-
}
|
|
5922
|
-
|
|
5923
|
-
function applyAutoDispatchDecision(parsedArgs, decision = null) {
|
|
5924
|
-
const resolved = decision || decideDispatchMode(parsedArgs);
|
|
5925
|
-
if (resolved.warning) {
|
|
5926
|
-
process.stderr.write(`${resolved.warning}\n`);
|
|
5927
|
-
}
|
|
5928
|
-
if (resolved.escalated) {
|
|
5929
|
-
const logLine = `${JSON.stringify({
|
|
5930
|
-
at: new Date().toISOString(),
|
|
5931
|
-
issue: 281,
|
|
5932
|
-
from: "multi",
|
|
5933
|
-
to: "swarm",
|
|
5934
|
-
tasksCount: Array.isArray(parsedArgs.tasks)
|
|
5935
|
-
? parsedArgs.tasks.length
|
|
5936
|
-
: (parsedArgs.tasks ?? null),
|
|
5937
|
-
reason: resolved.reason,
|
|
5938
|
-
})}\n`;
|
|
5939
|
-
try {
|
|
5940
|
-
appendFileSync(".omc/state/auto-escalation.log", logLine, "utf8");
|
|
5941
|
-
} catch {
|
|
5942
|
-
// Fresh checkouts may not have .omc/state yet; dispatch should continue.
|
|
5943
|
-
}
|
|
5944
|
-
}
|
|
5945
|
-
return resolved;
|
|
5946
|
-
}
|
|
3746
|
+
return parsed;
|
|
3747
|
+
}
|
|
5947
3748
|
|
|
5948
3749
|
async function main() {
|
|
5949
3750
|
const cmd = NORMALIZED_ARGS[0] || "help";
|
|
@@ -5954,20 +3755,6 @@ async function main() {
|
|
|
5954
3755
|
}).catch(() => {});
|
|
5955
3756
|
|
|
5956
3757
|
switch (cmd) {
|
|
5957
|
-
case "auto": {
|
|
5958
|
-
if (cmdArgs.some(isHelpArg)) {
|
|
5959
|
-
printCommandHelp("auto");
|
|
5960
|
-
return;
|
|
5961
|
-
}
|
|
5962
|
-
const parsedArgs = parseRouteArgs(cmdArgs);
|
|
5963
|
-
const decision = applyAutoDispatchDecision(parsedArgs);
|
|
5964
|
-
if (JSON_OUTPUT) {
|
|
5965
|
-
printJson({ args: parsedArgs, dispatch: decision });
|
|
5966
|
-
} else {
|
|
5967
|
-
process.stdout.write(`${decision.mode}\n`);
|
|
5968
|
-
}
|
|
5969
|
-
return;
|
|
5970
|
-
}
|
|
5971
3758
|
case "setup":
|
|
5972
3759
|
if (cmdArgs.some(isHelpArg)) {
|
|
5973
3760
|
printCommandHelp("setup");
|
|
@@ -5976,16 +3763,9 @@ async function main() {
|
|
|
5976
3763
|
cmdSetup({
|
|
5977
3764
|
dryRun: cmdArgs.includes("--dry-run"),
|
|
5978
3765
|
fromUpdate: cmdArgs.includes("--from-update"),
|
|
5979
|
-
|
|
3766
|
+
mods: cmdArgs.includes("--mods"),
|
|
5980
3767
|
});
|
|
5981
3768
|
return;
|
|
5982
|
-
case "stealth-fetch":
|
|
5983
|
-
if (cmdArgs.some(isHelpArg)) {
|
|
5984
|
-
printCommandHelp("stealth-fetch");
|
|
5985
|
-
return;
|
|
5986
|
-
}
|
|
5987
|
-
await stealthFetchMain([process.argv[0], "stealth-fetch", ...cmdArgs]);
|
|
5988
|
-
return;
|
|
5989
3769
|
case "doctor": {
|
|
5990
3770
|
if (cmdArgs.some(isHelpArg)) {
|
|
5991
3771
|
printCommandHelp("doctor");
|
|
@@ -6028,57 +3808,13 @@ async function main() {
|
|
|
6028
3808
|
}
|
|
6029
3809
|
return;
|
|
6030
3810
|
}
|
|
6031
|
-
if (cmdArgs.includes("--dynamic-routing")) {
|
|
6032
|
-
const { diagnoseDynamicRouting } = await import(
|
|
6033
|
-
"../scripts/doctor-dynamic-routing.mjs"
|
|
6034
|
-
);
|
|
6035
|
-
const report = await diagnoseDynamicRouting();
|
|
6036
|
-
if (JSON_OUTPUT) {
|
|
6037
|
-
console.log(JSON.stringify(report, null, 2));
|
|
6038
|
-
} else {
|
|
6039
|
-
const mark = (b) =>
|
|
6040
|
-
b ? `${GREEN_BRIGHT}✓${RESET}` : `${RED}✗${RESET}`;
|
|
6041
|
-
console.log(
|
|
6042
|
-
`\n ${AMBER}${BOLD}⬡ triflux doctor — dynamic routing${RESET}\n`,
|
|
6043
|
-
);
|
|
6044
|
-
console.log(
|
|
6045
|
-
` ${mark(report.enabled)} enabled: ${report.enabled} (TRIFLUX_DYNAMIC_ROUTING=${report.envFlag ?? "미설정"})`,
|
|
6046
|
-
);
|
|
6047
|
-
console.log(
|
|
6048
|
-
` ${mark(report.policyLoaded)} policy loaded: ${report.policyLoaded} (scenarios=${report.policyScenarios.length})`,
|
|
6049
|
-
);
|
|
6050
|
-
console.log(
|
|
6051
|
-
` ${mark(report.snapshotCached)} snapshot cache: ${
|
|
6052
|
-
report.snapshotCached
|
|
6053
|
-
? `hit (age=${report.snapshotAgeMs}ms)`
|
|
6054
|
-
: "miss"
|
|
6055
|
-
}`,
|
|
6056
|
-
);
|
|
6057
|
-
if (report.previewDecision) {
|
|
6058
|
-
const d = report.previewDecision;
|
|
6059
|
-
console.log(
|
|
6060
|
-
` ${GREEN_BRIGHT}→${RESET} preview decision: scenario=${d.scenario}, mode=${d.mode}, lane=${d.lane}, shards[0].cli=${d.shards?.[0]?.cli ?? "?"}`,
|
|
6061
|
-
);
|
|
6062
|
-
}
|
|
6063
|
-
if (report.error) {
|
|
6064
|
-
console.log(` ${RED}✗${RESET} error: ${report.error}`);
|
|
6065
|
-
}
|
|
6066
|
-
console.log("");
|
|
6067
|
-
}
|
|
6068
|
-
return;
|
|
6069
|
-
}
|
|
6070
3811
|
const fix = cmdArgs.includes("--fix");
|
|
6071
3812
|
const reset = cmdArgs.includes("--reset");
|
|
6072
3813
|
const purgeLogs = cmdArgs.includes("--purge-logs");
|
|
6073
|
-
const cleanupStaleHubs = cmdArgs.includes("--cleanup-stale-hubs");
|
|
6074
3814
|
const cleanupStaleTmux = cmdArgs.includes("--cleanup-stale-tmux");
|
|
6075
3815
|
const cleanupApply = cmdArgs.includes("--apply");
|
|
6076
3816
|
const cleanupDryRun = cmdArgs.includes("--dry-run") || !cleanupApply;
|
|
6077
|
-
if (
|
|
6078
|
-
(cleanupStaleHubs || cleanupStaleTmux) &&
|
|
6079
|
-
cleanupApply &&
|
|
6080
|
-
cmdArgs.includes("--dry-run")
|
|
6081
|
-
) {
|
|
3817
|
+
if (cleanupStaleTmux && cleanupApply && cmdArgs.includes("--dry-run")) {
|
|
6082
3818
|
throw createCliError(
|
|
6083
3819
|
"cleanup 옵션에서는 --dry-run 과 --apply 중 하나만 지정하세요",
|
|
6084
3820
|
{
|
|
@@ -6099,9 +3835,6 @@ async function main() {
|
|
|
6099
3835
|
fix,
|
|
6100
3836
|
reset,
|
|
6101
3837
|
purgeLogs,
|
|
6102
|
-
cleanupStaleHubs,
|
|
6103
|
-
cleanupStaleHubsDryRun: cleanupDryRun,
|
|
6104
|
-
cleanupStaleHubsApply: cleanupApply,
|
|
6105
3838
|
cleanupStaleTmux,
|
|
6106
3839
|
cleanupStaleTmuxDryRun: cleanupDryRun,
|
|
6107
3840
|
cleanupStaleTmuxApply: cleanupApply,
|
|
@@ -6114,9 +3847,6 @@ async function main() {
|
|
|
6114
3847
|
case "mcp":
|
|
6115
3848
|
cmdMcp(cmdArgs, { json: JSON_OUTPUT });
|
|
6116
3849
|
return;
|
|
6117
|
-
case "schema":
|
|
6118
|
-
cmdSchema(cmdArgs);
|
|
6119
|
-
return;
|
|
6120
3850
|
case "update":
|
|
6121
3851
|
await cmdUpdate(cmdArgs);
|
|
6122
3852
|
return;
|
|
@@ -6128,29 +3858,6 @@ async function main() {
|
|
|
6128
3858
|
}
|
|
6129
3859
|
cmdList({ json: JSON_OUTPUT });
|
|
6130
3860
|
return;
|
|
6131
|
-
case "handoff":
|
|
6132
|
-
cmdHandoff(cmdArgs, { json: JSON_OUTPUT });
|
|
6133
|
-
return;
|
|
6134
|
-
case "hub":
|
|
6135
|
-
if (cmdArgs.some(isHelpArg)) {
|
|
6136
|
-
printCommandHelp("hub");
|
|
6137
|
-
return;
|
|
6138
|
-
}
|
|
6139
|
-
await cmdHub(cmdArgs, {
|
|
6140
|
-
json:
|
|
6141
|
-
JSON_OUTPUT && ["status", "ensure"].includes(cmdArgs[0] || "status"),
|
|
6142
|
-
});
|
|
6143
|
-
return;
|
|
6144
|
-
case "monitor": {
|
|
6145
|
-
if (cmdArgs.some(isHelpArg)) {
|
|
6146
|
-
printCommandHelp("monitor");
|
|
6147
|
-
return;
|
|
6148
|
-
}
|
|
6149
|
-
const { createMonitor } = await import("../tui/monitor.mjs");
|
|
6150
|
-
const mon = createMonitor({ targetPane: process.env.TMUX_PANE });
|
|
6151
|
-
await mon.start();
|
|
6152
|
-
break;
|
|
6153
|
-
}
|
|
6154
3861
|
case "cto": {
|
|
6155
3862
|
if (cmdArgs.some(isHelpArg)) {
|
|
6156
3863
|
printCommandHelp("cto");
|
|
@@ -6164,16 +3871,16 @@ async function main() {
|
|
|
6164
3871
|
return;
|
|
6165
3872
|
}
|
|
6166
3873
|
case "multi": {
|
|
6167
|
-
const subcommand = cmdArgs[0] || "";
|
|
6168
3874
|
if (cmdArgs.some(isHelpArg)) {
|
|
6169
|
-
|
|
3875
|
+
const { pathToFileURL } = await import("node:url");
|
|
3876
|
+
const { renderTeamHelp } = await import(
|
|
3877
|
+
pathToFileURL(join(PKG_ROOT, "hub", "team", "cli", "help.mjs")).href
|
|
3878
|
+
);
|
|
3879
|
+
renderTeamHelp();
|
|
6170
3880
|
return;
|
|
6171
3881
|
}
|
|
6172
3882
|
if (JSON_OUTPUT) process.env.TFX_OUTPUT_JSON = "1";
|
|
6173
3883
|
else delete process.env.TFX_OUTPUT_JSON;
|
|
6174
|
-
if (subcommand !== "status") {
|
|
6175
|
-
await checkHubRunning();
|
|
6176
|
-
}
|
|
6177
3884
|
const { pathToFileURL } = await import("node:url");
|
|
6178
3885
|
const { cmdTeam } = await import(
|
|
6179
3886
|
pathToFileURL(join(PKG_ROOT, "hub", "team", "cli", "index.mjs")).href
|
|
@@ -6188,190 +3895,6 @@ async function main() {
|
|
|
6188
3895
|
}
|
|
6189
3896
|
return;
|
|
6190
3897
|
}
|
|
6191
|
-
case "codex-team":
|
|
6192
|
-
if (cmdArgs.some(isHelpArg)) {
|
|
6193
|
-
await cmdCodexTeam(["--help"]);
|
|
6194
|
-
return;
|
|
6195
|
-
}
|
|
6196
|
-
if (JSON_OUTPUT) process.env.TFX_OUTPUT_JSON = "1";
|
|
6197
|
-
else delete process.env.TFX_OUTPUT_JSON;
|
|
6198
|
-
await checkHubRunning();
|
|
6199
|
-
try {
|
|
6200
|
-
await cmdCodexTeam(cmdArgs);
|
|
6201
|
-
} finally {
|
|
6202
|
-
delete process.env.TFX_OUTPUT_JSON;
|
|
6203
|
-
}
|
|
6204
|
-
return;
|
|
6205
|
-
case "notion-read":
|
|
6206
|
-
case "nr": {
|
|
6207
|
-
if (cmdArgs.some(isHelpArg)) {
|
|
6208
|
-
printCommandHelp("notion-read");
|
|
6209
|
-
return;
|
|
6210
|
-
}
|
|
6211
|
-
const scriptPath = join(PKG_ROOT, "scripts", "notion-read.mjs");
|
|
6212
|
-
try {
|
|
6213
|
-
execFileSync(process.execPath, [scriptPath, ...cmdArgs], {
|
|
6214
|
-
stdio: "inherit",
|
|
6215
|
-
timeout: 660000,
|
|
6216
|
-
windowsHide: true,
|
|
6217
|
-
});
|
|
6218
|
-
} catch (e) {
|
|
6219
|
-
throw createCliError(e.message || "notion-read 실행 실패", {
|
|
6220
|
-
exitCode: e.status || EXIT_ERROR,
|
|
6221
|
-
reason: "error",
|
|
6222
|
-
});
|
|
6223
|
-
}
|
|
6224
|
-
return;
|
|
6225
|
-
}
|
|
6226
|
-
case "synapse": {
|
|
6227
|
-
const { cmdSynapseStatus } = await import("../hub/team/synapse-cli.mjs");
|
|
6228
|
-
const sub = cmdArgs[0] || "status";
|
|
6229
|
-
if (isHelpArg(sub)) {
|
|
6230
|
-
printCommandHelp("synapse");
|
|
6231
|
-
return;
|
|
6232
|
-
}
|
|
6233
|
-
if (sub !== "status") {
|
|
6234
|
-
throw createCliError(`synapse 서브커맨드 미지원: ${sub}`, {
|
|
6235
|
-
exitCode: EXIT_ARG_ERROR,
|
|
6236
|
-
reason: "argError",
|
|
6237
|
-
fix: "tfx synapse status [--json] [--registry <path>]",
|
|
6238
|
-
});
|
|
6239
|
-
}
|
|
6240
|
-
await cmdSynapseStatus(cmdArgs.slice(1), { json: JSON_OUTPUT });
|
|
6241
|
-
return;
|
|
6242
|
-
}
|
|
6243
|
-
case "review": {
|
|
6244
|
-
if (cmdArgs.some(isHelpArg)) {
|
|
6245
|
-
printCommandHelp("review");
|
|
6246
|
-
return;
|
|
6247
|
-
}
|
|
6248
|
-
const ref =
|
|
6249
|
-
cmdArgs[0] && !cmdArgs[0].startsWith("--") ? cmdArgs[0] : "HEAD";
|
|
6250
|
-
const baseIdx = cmdArgs.indexOf("--base");
|
|
6251
|
-
const base = baseIdx >= 0 ? cmdArgs[baseIdx + 1] : undefined;
|
|
6252
|
-
const timeoutIdx = cmdArgs.indexOf("--timeout");
|
|
6253
|
-
const timeoutMs =
|
|
6254
|
-
timeoutIdx >= 0 ? Number(cmdArgs[timeoutIdx + 1]) * 1000 : 180_000;
|
|
6255
|
-
const shardIdx = cmdArgs.indexOf("--shard");
|
|
6256
|
-
const shard = shardIdx >= 0 ? cmdArgs[shardIdx + 1] : "off";
|
|
6257
|
-
|
|
6258
|
-
if (shard === "per-file") {
|
|
6259
|
-
const { runCodexReviewSharded } = await import(
|
|
6260
|
-
"../hub/team/codex-review.mjs"
|
|
6261
|
-
);
|
|
6262
|
-
const result = await runCodexReviewSharded({
|
|
6263
|
-
ref,
|
|
6264
|
-
base,
|
|
6265
|
-
timeoutMs,
|
|
6266
|
-
onFileStart: ({ file, index, total }) => {
|
|
6267
|
-
if (!JSON_OUTPUT) {
|
|
6268
|
-
process.stderr.write(
|
|
6269
|
-
`[${index + 1}/${total}] reviewing ${file}\n`,
|
|
6270
|
-
);
|
|
6271
|
-
}
|
|
6272
|
-
},
|
|
6273
|
-
});
|
|
6274
|
-
if (JSON_OUTPUT) {
|
|
6275
|
-
console.log(JSON.stringify(result, null, 2));
|
|
6276
|
-
} else {
|
|
6277
|
-
console.log(
|
|
6278
|
-
`=== per-file review (${result.files.length} files, range=${result.range}) ===`,
|
|
6279
|
-
);
|
|
6280
|
-
for (const r of result.perFile) {
|
|
6281
|
-
console.log("");
|
|
6282
|
-
console.log(
|
|
6283
|
-
`--- ${r.file} (${r.diffBytes}b, verdict=${r.verdict || "n/a"}${r.skipped ? ", skipped" : ""}) ---`,
|
|
6284
|
-
);
|
|
6285
|
-
if (r.error) console.log(` error: ${r.error}`);
|
|
6286
|
-
if (r.stdout) process.stdout.write(r.stdout);
|
|
6287
|
-
}
|
|
6288
|
-
console.log("");
|
|
6289
|
-
console.log(
|
|
6290
|
-
`Aggregate: files=${result.files.length} verdict=${result.verdict}`,
|
|
6291
|
-
);
|
|
6292
|
-
if (result.error) console.log(`error: ${result.error}`);
|
|
6293
|
-
}
|
|
6294
|
-
process.exit(result.ok ? 0 : 1);
|
|
6295
|
-
}
|
|
6296
|
-
|
|
6297
|
-
const { runCodexReview } = await import("../hub/team/codex-review.mjs");
|
|
6298
|
-
const result = await runCodexReview({ ref, base, timeoutMs });
|
|
6299
|
-
if (JSON_OUTPUT) {
|
|
6300
|
-
console.log(JSON.stringify(result, null, 2));
|
|
6301
|
-
} else {
|
|
6302
|
-
if (result.stdout) process.stdout.write(result.stdout);
|
|
6303
|
-
if (result.stderr) process.stderr.write(result.stderr);
|
|
6304
|
-
console.log("");
|
|
6305
|
-
console.log(
|
|
6306
|
-
`range=${result.range || ref} diffBytes=${result.diffBytes} verdict=${result.verdict || "n/a"}`,
|
|
6307
|
-
);
|
|
6308
|
-
if (result.error) console.log(`error: ${result.error}`);
|
|
6309
|
-
}
|
|
6310
|
-
process.exit(result.ok ? 0 : 1);
|
|
6311
|
-
return;
|
|
6312
|
-
}
|
|
6313
|
-
case "swarm": {
|
|
6314
|
-
const sub = cmdArgs[0] || "";
|
|
6315
|
-
if (sub === "help" || sub === "--help" || sub === "-h") {
|
|
6316
|
-
const s = CLI_COMMAND_SCHEMAS.swarm;
|
|
6317
|
-
console.log(`
|
|
6318
|
-
${AMBER}${BOLD}⬡ tfx swarm${RESET}
|
|
6319
|
-
|
|
6320
|
-
${GRAY}${s.description}${RESET}
|
|
6321
|
-
|
|
6322
|
-
${BOLD}Usage${RESET}
|
|
6323
|
-
${WHITE_BRIGHT}${s.usage}${RESET}
|
|
6324
|
-
|
|
6325
|
-
${BOLD}Subcommands${RESET}
|
|
6326
|
-
${WHITE_BRIGHT}tfx swarm run <prd>${RESET} ${GRAY}${s.subcommands.run}${RESET}
|
|
6327
|
-
${WHITE_BRIGHT}tfx swarm preflight <prd>${RESET} ${GRAY}${s.subcommands.preflight}${RESET}
|
|
6328
|
-
${WHITE_BRIGHT}tfx swarm plan <prd>${RESET} ${GRAY}${s.subcommands.plan}${RESET}
|
|
6329
|
-
${WHITE_BRIGHT}tfx swarm list${RESET} ${GRAY}${s.subcommands.list}${RESET}
|
|
6330
|
-
${WHITE_BRIGHT}tfx swarm status${RESET} ${GRAY}${s.subcommands.status}${RESET}
|
|
6331
|
-
|
|
6332
|
-
${BOLD}Options${RESET}
|
|
6333
|
-
${s.options.map((o) => ` ${DIM}${o.name.padEnd(16)}${RESET} ${GRAY}${o.description}${RESET}`).join("\n")}
|
|
6334
|
-
`);
|
|
6335
|
-
return;
|
|
6336
|
-
}
|
|
6337
|
-
await checkHubRunning();
|
|
6338
|
-
const { cmdSwarmRun, cmdSwarmPlan, cmdSwarmList, cmdSwarmPreflight } =
|
|
6339
|
-
await import("../hub/team/swarm-cli.mjs");
|
|
6340
|
-
if (sub === "list" || sub === "status") {
|
|
6341
|
-
await cmdSwarmList(cmdArgs.slice(1), { json: JSON_OUTPUT });
|
|
6342
|
-
return;
|
|
6343
|
-
}
|
|
6344
|
-
if (sub === "plan") {
|
|
6345
|
-
await cmdSwarmPlan(cmdArgs.slice(1), { json: JSON_OUTPUT });
|
|
6346
|
-
return;
|
|
6347
|
-
}
|
|
6348
|
-
if (sub === "preflight") {
|
|
6349
|
-
await cmdSwarmPreflight(cmdArgs.slice(1), { json: JSON_OUTPUT });
|
|
6350
|
-
return;
|
|
6351
|
-
}
|
|
6352
|
-
if (sub === "run") {
|
|
6353
|
-
await cmdSwarmRun(cmdArgs.slice(1), { json: JSON_OUTPUT });
|
|
6354
|
-
return;
|
|
6355
|
-
}
|
|
6356
|
-
if (!sub || sub.startsWith("--")) {
|
|
6357
|
-
throw createCliError("PRD 경로가 필요합니다", {
|
|
6358
|
-
exitCode: EXIT_ARG_ERROR,
|
|
6359
|
-
reason: "argError",
|
|
6360
|
-
fix: "tfx swarm [run] <prd-path> [--dry-run|--json|--filter <shard>]",
|
|
6361
|
-
});
|
|
6362
|
-
}
|
|
6363
|
-
await cmdSwarmRun(cmdArgs, { json: JSON_OUTPUT });
|
|
6364
|
-
return;
|
|
6365
|
-
}
|
|
6366
|
-
case "why": {
|
|
6367
|
-
if (cmdArgs.some(isHelpArg)) {
|
|
6368
|
-
printCommandHelp("why");
|
|
6369
|
-
return;
|
|
6370
|
-
}
|
|
6371
|
-
const { cmdSynapseWhy } = await import("../hub/team/synapse-cli.mjs");
|
|
6372
|
-
await cmdSynapseWhy(cmdArgs, { json: JSON_OUTPUT });
|
|
6373
|
-
return;
|
|
6374
|
-
}
|
|
6375
3898
|
case "version":
|
|
6376
3899
|
case "--version":
|
|
6377
3900
|
case "-v":
|