triflux 10.3.4 → 10.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/bin/tfx-doctor-tui.mjs +1 -1
- package/bin/tfx-doctor.mjs +6 -1
- package/bin/tfx-profile.mjs +1 -1
- package/bin/tfx-setup-tui.mjs +1 -1
- package/bin/tfx-setup.mjs +6 -1
- package/bin/triflux.mjs +2396 -1140
- package/hooks/agent-route-guard.mjs +12 -8
- package/hooks/cross-review-tracker.mjs +21 -8
- package/hooks/error-context.mjs +19 -7
- package/hooks/hook-adaptive-collector.mjs +18 -16
- package/hooks/hook-manager.mjs +93 -32
- package/hooks/hook-orchestrator.mjs +108 -24
- package/hooks/hook-registry.json +11 -0
- package/hooks/keyword-rules.json +6 -10
- package/hooks/lib/resolve-root.mjs +1 -1
- package/hooks/mcp-config-watcher.mjs +6 -2
- package/hooks/pipeline-stop.mjs +3 -6
- package/hooks/safety-guard.mjs +99 -28
- package/hooks/session-start-fast.mjs +143 -0
- package/hooks/subagent-verifier.mjs +5 -4
- package/hub/account-broker.mjs +256 -60
- package/hub/adaptive-diagnostic.mjs +75 -48
- package/hub/adaptive-inject.mjs +95 -57
- package/hub/adaptive-memory.mjs +156 -42
- package/hub/adaptive.mjs +60 -31
- package/hub/assign-callbacks.mjs +67 -30
- package/hub/bridge.mjs +0 -1
- package/hub/cli-adapter-base.mjs +200 -48
- package/hub/codex-adapter.mjs +76 -96
- package/hub/codex-compat.mjs +3 -3
- package/hub/codex-preflight.mjs +63 -37
- package/hub/delegator/contracts.mjs +19 -23
- package/hub/delegator/index.mjs +3 -3
- package/hub/delegator/service.mjs +88 -64
- package/hub/delegator/tool-definitions.mjs +5 -5
- package/hub/fullcycle.mjs +33 -17
- package/hub/gemini-adapter.mjs +69 -94
- package/hub/hitl.mjs +89 -30
- package/hub/intent.mjs +161 -38
- package/hub/lib/cache-guard.mjs +43 -17
- package/hub/lib/mcp-response-cache.mjs +66 -32
- package/hub/lib/memory-store.mjs +285 -111
- package/hub/lib/path-utils.mjs +35 -37
- package/hub/lib/process-utils.mjs +106 -37
- package/hub/lib/spawn-trace.mjs +527 -0
- package/hub/lib/ssh-command.mjs +34 -4
- package/hub/lib/ssh-retry.mjs +5 -1
- package/hub/lib/uuidv7.mjs +4 -3
- package/hub/memory-doctor.mjs +266 -106
- package/hub/middleware/request-logger.mjs +61 -34
- package/hub/paths.mjs +9 -9
- package/hub/pipeline/gates/confidence.mjs +34 -15
- package/hub/pipeline/gates/consensus.mjs +27 -15
- package/hub/pipeline/gates/index.mjs +7 -3
- package/hub/pipeline/gates/selfcheck.mjs +57 -19
- package/hub/pipeline/index.mjs +77 -42
- package/hub/pipeline/state.mjs +10 -10
- package/hub/pipeline/transitions.mjs +40 -23
- package/hub/platform.mjs +57 -48
- package/hub/promote-penalties.mjs +25 -7
- package/hub/quality/deslop.mjs +70 -49
- package/hub/research.mjs +32 -25
- package/hub/router.mjs +240 -107
- package/hub/routing/complexity.mjs +132 -29
- package/hub/routing/index.mjs +17 -12
- package/hub/routing/q-learning.mjs +76 -28
- package/hub/server.mjs +4 -4
- package/hub/session-fingerprint.mjs +126 -60
- package/hub/state.mjs +84 -43
- package/hub/store-adapter.mjs +59 -26
- package/hub/store.mjs +356 -153
- package/hub/team/agent-map.json +22 -7
- package/hub/team/ansi.mjs +186 -122
- package/hub/team/backend.mjs +28 -10
- package/hub/team/cli/commands/attach.mjs +29 -9
- package/hub/team/cli/commands/control.mjs +29 -8
- package/hub/team/cli/commands/debug.mjs +32 -11
- package/hub/team/cli/commands/focus.mjs +38 -11
- package/hub/team/cli/commands/interrupt.mjs +18 -6
- package/hub/team/cli/commands/kill.mjs +16 -5
- package/hub/team/cli/commands/list.mjs +11 -4
- package/hub/team/cli/commands/send.mjs +19 -6
- package/hub/team/cli/commands/start/index.mjs +154 -31
- package/hub/team/cli/commands/start/parse-args.mjs +38 -11
- package/hub/team/cli/commands/start/start-headless.mjs +112 -36
- package/hub/team/cli/commands/start/start-in-process.mjs +12 -2
- package/hub/team/cli/commands/start/start-mux.mjs +70 -21
- package/hub/team/cli/commands/start/start-wt.mjs +29 -12
- package/hub/team/cli/commands/status.mjs +43 -14
- package/hub/team/cli/commands/stop.mjs +11 -4
- package/hub/team/cli/commands/task.mjs +8 -3
- package/hub/team/cli/commands/tasks.mjs +1 -1
- package/hub/team/cli/index.mjs +2 -2
- package/hub/team/cli/manifest.mjs +38 -8
- package/hub/team/cli/render.mjs +30 -8
- package/hub/team/cli/services/attach-fallback.mjs +31 -11
- package/hub/team/cli/services/hub-client.mjs +42 -14
- package/hub/team/cli/services/member-selector.mjs +11 -4
- package/hub/team/cli/services/native-control.mjs +48 -21
- package/hub/team/cli/services/runtime-mode.mjs +2 -1
- package/hub/team/cli/services/state-store.mjs +25 -8
- package/hub/team/cli/services/task-model.mjs +16 -6
- package/hub/team/conductor-mesh-bridge.mjs +24 -23
- package/hub/team/conductor.mjs +8 -4
- package/hub/team/dashboard-anchor.mjs +4 -5
- package/hub/team/dashboard-layout.mjs +3 -1
- package/hub/team/dashboard-open.mjs +41 -21
- package/hub/team/dashboard.mjs +76 -28
- package/hub/team/event-log.mjs +18 -10
- package/hub/team/handoff.mjs +31 -15
- package/hub/team/headless.mjs +2 -1
- package/hub/team/health-probe.mjs +69 -54
- package/hub/team/launcher-template.mjs +16 -13
- package/hub/team/native-supervisor.mjs +65 -21
- package/hub/team/native.mjs +74 -35
- package/hub/team/nativeProxy.mjs +184 -113
- package/hub/team/notify.mjs +119 -76
- package/hub/team/orchestrator.mjs +9 -4
- package/hub/team/pane.mjs +12 -7
- package/hub/team/process-cleanup.mjs +25 -16
- package/hub/team/psmux.mjs +491 -201
- package/hub/team/remote-probe.mjs +68 -52
- package/hub/team/remote-session.mjs +117 -59
- package/hub/team/remote-watcher.mjs +61 -33
- package/hub/team/routing.mjs +51 -25
- package/hub/team/runtime-strategy.mjs +3 -1
- package/hub/team/session.mjs +98 -34
- package/hub/team/staleState.mjs +72 -30
- package/hub/team/swarm-locks.mjs +15 -13
- package/hub/team/swarm-planner.mjs +32 -21
- package/hub/team/swarm-reconciler.mjs +48 -23
- package/hub/team/tui-lite.mjs +266 -68
- package/hub/team/tui-remote-adapter.mjs +14 -10
- package/hub/team/tui-viewer.mjs +99 -43
- package/hub/team/tui.mjs +708 -271
- package/hub/team/worktree-lifecycle.mjs +152 -58
- package/hub/team/wt-manager.mjs +24 -14
- package/hub/token-mode.mjs +71 -71
- package/hub/tray.mjs +66 -23
- package/hub/workers/claude-worker.mjs +162 -118
- package/hub/workers/codex-mcp.mjs +192 -141
- package/hub/workers/delegator-mcp.mjs +507 -333
- package/hub/workers/factory.mjs +8 -8
- package/hub/workers/gemini-worker.mjs +115 -84
- package/hub/workers/interface.mjs +6 -1
- package/hub/workers/worker-utils.mjs +21 -14
- package/hud/colors.mjs +27 -9
- package/hud/constants.mjs +162 -26
- package/hud/context-monitor.mjs +82 -41
- package/hud/hud-qos-status.mjs +129 -49
- package/hud/mission-board.mjs +6 -3
- package/hud/providers/claude.mjs +226 -115
- package/hud/providers/codex.mjs +62 -22
- package/hud/providers/gemini.mjs +168 -56
- package/hud/renderers.mjs +384 -119
- package/hud/terminal.mjs +101 -31
- package/hud/utils.mjs +78 -38
- package/mesh/index.mjs +11 -5
- package/mesh/mesh-budget.mjs +18 -9
- package/mesh/mesh-heartbeat.mjs +1 -1
- package/mesh/mesh-queue.mjs +3 -5
- package/mesh/mesh-router.mjs +5 -4
- package/package.json +2 -1
- package/scripts/__tests__/gen-skill-docs.test.mjs +36 -7
- package/scripts/__tests__/keyword-detector.test.mjs +77 -28
- package/scripts/__tests__/mcp-guard-engine.test.mjs +58 -20
- package/scripts/__tests__/remote-spawn-transfer.test.mjs +30 -19
- package/scripts/__tests__/remote-spawn.test.mjs +10 -4
- package/scripts/__tests__/session-start-fast.test.mjs +36 -0
- package/scripts/__tests__/skill-template.test.mjs +98 -50
- package/scripts/__tests__/smoke.test.mjs +1 -1
- package/scripts/__tests__/spawn-trace.test.mjs +102 -0
- package/scripts/__tests__/tfx-doctor-diagnose.test.mjs +48 -0
- package/scripts/cache-doctor.mjs +11 -4
- package/scripts/cache-warmup.mjs +96 -37
- package/scripts/claudemd-sync.mjs +27 -17
- package/scripts/codex-gateway-preflight.mjs +52 -37
- package/scripts/codex-mcp-gateway-sync.mjs +59 -39
- package/scripts/completions/tfx.bash +47 -47
- package/scripts/completions/tfx.fish +44 -44
- package/scripts/completions/tfx.zsh +83 -83
- package/scripts/config-audit.mjs +232 -0
- package/scripts/convert-to-tmpl.mjs +54 -0
- package/scripts/cross-review-gate.mjs +35 -12
- package/scripts/cross-review-tracker.mjs +21 -8
- package/scripts/demo.mjs +35 -17
- package/scripts/doctor-diagnose.mjs +284 -0
- package/scripts/gen-skill-docs.mjs +7 -2
- package/scripts/gen-skill-manifest.mjs +2 -1
- package/scripts/headless-guard.mjs +86 -48
- package/scripts/hub-ensure.mjs +45 -26
- package/scripts/keyword-detector.mjs +41 -20
- package/scripts/keyword-rules-expander.mjs +47 -30
- package/scripts/lib/claudemd-scanner.mjs +6 -1
- package/scripts/lib/context.mjs +3 -3
- package/scripts/lib/cross-review-utils.mjs +6 -3
- package/scripts/lib/env-probe.mjs +47 -28
- package/scripts/lib/gemini-profiles.mjs +44 -10
- package/scripts/lib/handoff.mjs +33 -17
- package/scripts/lib/hook-utils.mjs +8 -6
- package/scripts/lib/keyword-rules.mjs +43 -19
- package/scripts/lib/logger.mjs +24 -24
- package/scripts/lib/mcp-filter.mjs +377 -239
- package/scripts/lib/mcp-guard-engine.mjs +194 -79
- package/scripts/lib/mcp-manifest.mjs +23 -13
- package/scripts/lib/mcp-server-catalog.mjs +300 -63
- package/scripts/lib/psmux-info.mjs +11 -6
- package/scripts/lib/remote-spawn-transfer.mjs +44 -14
- package/scripts/lib/skill-template.mjs +30 -7
- package/scripts/mcp-check.mjs +58 -39
- package/scripts/mcp-gateway-config.mjs +83 -39
- package/scripts/mcp-gateway-ensure.mjs +43 -35
- package/scripts/mcp-gateway-integration-test.mjs +70 -58
- package/scripts/mcp-gateway-start.mjs +126 -60
- package/scripts/mcp-gateway-verify.mjs +24 -22
- package/scripts/mcp-safety-guard.mjs +44 -11
- package/scripts/notion-read.mjs +199 -84
- package/scripts/pack.mjs +94 -89
- package/scripts/preflight-cache.mjs +27 -10
- package/scripts/preinstall.mjs +42 -13
- package/scripts/remote-spawn.mjs +309 -94
- package/scripts/run.cjs +8 -5
- package/scripts/session-spawn-helper.mjs +130 -39
- package/scripts/session-stale-cleanup.mjs +123 -0
- package/scripts/setup.mjs +941 -492
- package/scripts/test-lock.mjs +20 -7
- package/scripts/test-tfx-route-no-claude-native.mjs +16 -12
- package/scripts/tfx-batch-stats.mjs +32 -11
- package/scripts/tfx-gate-activate.mjs +11 -4
- package/scripts/tfx-route-post.mjs +87 -20
- package/scripts/tfx-route-worker.mjs +57 -51
- package/scripts/tfx-route.sh +41 -124
- package/scripts/tmp-cleanup.mjs +21 -7
- package/scripts/token-snapshot.mjs +204 -85
- package/skills/.omc/state/agent-replay-8f0e10a9-9693-4410-96f5-a6b07e8ed995.jsonl +1 -0
- package/skills/.omc/state/idle-notif-cooldown.json +3 -0
- package/skills/.omc/state/last-tool-error.json +7 -0
- package/skills/.omc/state/subagent-tracking.json +7 -0
- package/skills/_templates/base.md +1 -6
- package/skills/merge-worktree/SKILL.md.tmpl +144 -0
- package/skills/shared/telemetry-segment.md +6 -0
- package/skills/star-prompt/SKILL.md.tmpl +222 -0
- package/skills/tfx-analysis/SKILL.md.tmpl +107 -0
- package/skills/tfx-analysis/skill.json +1 -6
- package/skills/tfx-auto/SKILL.md +1 -0
- package/skills/tfx-auto-codex/SKILL.md.tmpl +106 -0
- package/skills/tfx-auto-codex/skill.json +1 -3
- package/skills/tfx-autopilot/SKILL.md.tmpl +116 -0
- package/skills/tfx-autopilot/skill.json +1 -5
- package/skills/tfx-autoresearch/SKILL.md.tmpl +136 -0
- package/skills/tfx-autoroute/SKILL.md.tmpl +189 -0
- package/skills/tfx-autoroute/skill.json +1 -7
- package/skills/tfx-codex/SKILL.md +1 -0
- package/skills/tfx-codex/skill.json +1 -3
- package/skills/tfx-codex-swarm/SKILL.md.tmpl +16 -0
- package/skills/tfx-codex-swarm/evals/evals.json +1 -1
- package/skills/tfx-codex-swarm/skill.json +1 -4
- package/skills/tfx-codex-swarm-workspace/iteration-1/benchmark.json +54 -12
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/grading.json +35 -7
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/grading.json +35 -7
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/grading.json +25 -5
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/grading.json +25 -5
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/grading.json +20 -4
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/grading.json +16 -4
- package/skills/tfx-consensus/SKILL.md.tmpl +146 -0
- package/skills/tfx-debate/SKILL.md.tmpl +192 -0
- package/skills/tfx-debate/skill.json +1 -7
- package/skills/tfx-deep-analysis/SKILL.md.tmpl +228 -0
- package/skills/tfx-deep-analysis/skill.json +1 -5
- package/skills/tfx-deep-interview/SKILL.md.tmpl +203 -0
- package/skills/tfx-deep-plan/SKILL.md.tmpl +282 -0
- package/skills/tfx-deep-qa/SKILL.md.tmpl +165 -0
- package/skills/tfx-deep-qa/skill.json +1 -6
- package/skills/tfx-deep-research/SKILL.md.tmpl +217 -0
- package/skills/tfx-deep-review/SKILL.md.tmpl +179 -0
- package/skills/tfx-doctor/SKILL.md +21 -0
- package/skills/tfx-doctor/SKILL.md.tmpl +172 -0
- package/skills/tfx-doctor/skill.json +1 -3
- package/skills/tfx-find/SKILL.md +1 -0
- package/skills/tfx-forge/SKILL.md.tmpl +187 -0
- package/skills/tfx-fullcycle/SKILL.md.tmpl +286 -0
- package/skills/tfx-fullcycle/skill.json +1 -6
- package/skills/tfx-gemini/SKILL.md.tmpl +91 -0
- package/skills/tfx-gemini/skill.json +1 -3
- package/skills/tfx-hooks/SKILL.md.tmpl +216 -0
- package/skills/tfx-hooks/skill.json +1 -3
- package/skills/tfx-hub/SKILL.md.tmpl +212 -0
- package/skills/tfx-hub/skill.json +1 -3
- package/skills/tfx-index/SKILL.md +1 -0
- package/skills/tfx-index/skill.json +1 -6
- package/skills/tfx-interview/SKILL.md.tmpl +285 -0
- package/skills/tfx-multi/SKILL.md.tmpl +183 -0
- package/skills/tfx-multi/skill.json +1 -3
- package/skills/tfx-panel/SKILL.md.tmpl +189 -0
- package/skills/tfx-panel/skill.json +1 -7
- package/skills/tfx-persist/SKILL.md.tmpl +270 -0
- package/skills/tfx-persist/skill.json +1 -7
- package/skills/tfx-plan/SKILL.md +1 -0
- package/skills/tfx-plan/skill.json +1 -6
- package/skills/tfx-profile/SKILL.md.tmpl +239 -0
- package/skills/tfx-profile/skill.json +1 -3
- package/skills/tfx-prune/SKILL.md.tmpl +200 -0
- package/skills/tfx-prune/skill.json +1 -7
- package/skills/tfx-psmux-rules/SKILL.md.tmpl +326 -0
- package/skills/tfx-psmux-rules/skill.json +1 -4
- package/skills/tfx-qa/SKILL.md +1 -0
- package/skills/tfx-qa/skill.json +1 -6
- package/skills/tfx-ralph/SKILL.md.tmpl +28 -0
- package/skills/tfx-ralph/skill.json +1 -4
- package/skills/tfx-remote-setup/SKILL.md.tmpl +576 -0
- package/skills/tfx-remote-setup/skill.json +1 -3
- package/skills/tfx-remote-spawn/SKILL.md.tmpl +263 -0
- package/skills/tfx-remote-spawn/references/hosts.json +16 -0
- package/skills/tfx-remote-spawn/skill.json +1 -4
- package/skills/tfx-research/SKILL.md +1 -0
- package/skills/tfx-review/SKILL.md +1 -0
- package/skills/tfx-review/skill.json +1 -6
- package/skills/tfx-setup/SKILL.md.tmpl +504 -0
- package/skills/tfx-setup/skill.json +1 -3
- package/skills/tfx-swarm/SKILL.md +22 -0
- package/skills/tfx-swarm/SKILL.md.tmpl +218 -0
- package/tui/codex-profile.mjs +88 -33
- package/tui/core.mjs +45 -15
- package/tui/doctor.mjs +75 -28
- package/tui/gemini-profile.mjs +74 -29
- package/tui/monitor-data.mjs +8 -4
- package/tui/monitor.mjs +71 -27
- package/tui/setup.mjs +133 -42
package/bin/triflux.mjs
CHANGED
|
@@ -1,19 +1,49 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
3
|
+
import { execFileSync, execSync, spawn } from "child_process";
|
|
2
4
|
// triflux CLI — setup, doctor, version
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
+
import {
|
|
6
|
+
chmodSync,
|
|
7
|
+
closeSync,
|
|
8
|
+
copyFileSync,
|
|
9
|
+
existsSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
openSync,
|
|
12
|
+
readdirSync,
|
|
13
|
+
readFileSync,
|
|
14
|
+
readSync,
|
|
15
|
+
statSync,
|
|
16
|
+
unlinkSync,
|
|
17
|
+
writeFileSync,
|
|
18
|
+
} from "fs";
|
|
5
19
|
import { homedir, tmpdir } from "os";
|
|
6
|
-
import {
|
|
20
|
+
import { basename, dirname, join, resolve } from "path";
|
|
7
21
|
import { fileURLToPath } from "url";
|
|
8
|
-
import { setTimeout as delay } from "node:timers/promises";
|
|
9
22
|
import { loadDelegatorSchemaBundle } from "../hub/delegator/tool-definitions.mjs";
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
23
|
+
import {
|
|
24
|
+
checkNetworkAvailability,
|
|
25
|
+
validateRuntimeCachePaths,
|
|
26
|
+
} from "../hub/lib/cache-guard.mjs";
|
|
13
27
|
import { getPipelineStateDbPath } from "../hub/pipeline/state.mjs";
|
|
14
|
-
import {
|
|
28
|
+
import { forceCleanupTeam } from "../hub/team/nativeProxy.mjs";
|
|
29
|
+
import {
|
|
30
|
+
detectMultiplexer,
|
|
31
|
+
getSessionAttachedCount,
|
|
32
|
+
killSession,
|
|
33
|
+
listSessions,
|
|
34
|
+
tmuxExec,
|
|
35
|
+
} from "../hub/team/session.mjs";
|
|
36
|
+
import {
|
|
37
|
+
cleanupStaleOmcTeams,
|
|
38
|
+
inspectStaleOmcTeams,
|
|
39
|
+
} from "../hub/team/staleState.mjs";
|
|
40
|
+
import {
|
|
41
|
+
ensureGlobalClaudeRoutingSection,
|
|
42
|
+
ensureTfxSection,
|
|
43
|
+
getLatestRoutingTable,
|
|
44
|
+
} from "../scripts/claudemd-sync.mjs";
|
|
15
45
|
import { ensureGeminiProfiles } from "../scripts/lib/gemini-profiles.mjs";
|
|
16
|
-
import {
|
|
46
|
+
import { serializeHandoff } from "../scripts/lib/handoff.mjs";
|
|
17
47
|
import {
|
|
18
48
|
addRegistryServer,
|
|
19
49
|
createDefaultRegistry,
|
|
@@ -25,19 +55,27 @@ import {
|
|
|
25
55
|
syncRegistryTargets,
|
|
26
56
|
} from "../scripts/lib/mcp-guard-engine.mjs";
|
|
27
57
|
import {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
58
|
+
formatPsmuxInstallGuidance,
|
|
59
|
+
formatPsmuxUpdateGuidance,
|
|
60
|
+
probePsmuxSupport,
|
|
61
|
+
} from "../scripts/lib/psmux-info.mjs";
|
|
62
|
+
import {
|
|
63
|
+
cleanupStaleSkills,
|
|
32
64
|
ensureCodexHubServerConfig,
|
|
65
|
+
ensureCodexProfiles,
|
|
66
|
+
ensureHooksInSettings,
|
|
67
|
+
extractManagedHookFilename,
|
|
68
|
+
getManagedRegistryHooks,
|
|
69
|
+
getVersion,
|
|
70
|
+
hasProfileSection,
|
|
71
|
+
LEGACY_CODEX_MODELS,
|
|
72
|
+
REQUIRED_CODEX_PROFILES,
|
|
73
|
+
replaceProfileSection,
|
|
74
|
+
SKILL_ALIASES,
|
|
75
|
+
SYNC_MAP,
|
|
76
|
+
syncAliasedSkillDir,
|
|
33
77
|
} from "../scripts/setup.mjs";
|
|
34
|
-
import {
|
|
35
|
-
ensureGlobalClaudeRoutingSection,
|
|
36
|
-
ensureTfxSection,
|
|
37
|
-
getLatestRoutingTable,
|
|
38
|
-
} from "../scripts/claudemd-sync.mjs";
|
|
39
78
|
import { cleanupTmpFiles } from "../scripts/tmp-cleanup.mjs";
|
|
40
|
-
import { checkNetworkAvailability, validateRuntimeCachePaths } from "../hub/lib/cache-guard.mjs";
|
|
41
79
|
|
|
42
80
|
const PKG_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
43
81
|
const CLAUDE_DIR = join(homedir(), ".claude");
|
|
@@ -48,7 +86,6 @@ const PKG = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8"));
|
|
|
48
86
|
// 이 배열에 포함된 버전에서만 star prompt를 표시한다 (빈 배열 = 모든 버전에서 표시)
|
|
49
87
|
const STAR_PROMPT_VERSIONS = [];
|
|
50
88
|
|
|
51
|
-
|
|
52
89
|
// ── 색상 체계 (triflux brand: amber/orange accent) ──
|
|
53
90
|
const CYAN = "\x1b[36m";
|
|
54
91
|
const GREEN = "\x1b[32m";
|
|
@@ -88,48 +125,107 @@ const CLI_COMMAND_SCHEMAS = Object.freeze({
|
|
|
88
125
|
usage: "tfx setup [--dry-run]",
|
|
89
126
|
description: "파일 동기화 + HUD/MCP 설정",
|
|
90
127
|
options: [
|
|
91
|
-
{
|
|
128
|
+
{
|
|
129
|
+
name: "--dry-run",
|
|
130
|
+
type: "boolean",
|
|
131
|
+
description: "실제 변경 없이 예정 작업을 JSON으로 출력",
|
|
132
|
+
},
|
|
92
133
|
],
|
|
93
134
|
},
|
|
94
135
|
doctor: {
|
|
95
|
-
usage: "tfx doctor [--fix] [--reset] [--json]",
|
|
136
|
+
usage: "tfx doctor [--fix] [--reset] [--audit] [--diagnose] [--json]",
|
|
96
137
|
description: "설치 상태 진단 및 자동 복구",
|
|
97
138
|
options: [
|
|
98
|
-
{
|
|
99
|
-
|
|
100
|
-
|
|
139
|
+
{
|
|
140
|
+
name: "--fix",
|
|
141
|
+
type: "boolean",
|
|
142
|
+
description: "파일/캐시 자동 복구 후 재진단",
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: "--reset",
|
|
146
|
+
type: "boolean",
|
|
147
|
+
description: "캐시 초기화 후 재생성",
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: "--audit",
|
|
151
|
+
type: "boolean",
|
|
152
|
+
description: "설정 보안/성능 정적 감사",
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: "--diagnose",
|
|
156
|
+
type: "boolean",
|
|
157
|
+
description: "진단 번들(zip) 생성: spawn-trace + hook timing + system info",
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: "--json",
|
|
161
|
+
type: "boolean",
|
|
162
|
+
description: "구조화된 진단 결과 JSON 출력",
|
|
163
|
+
},
|
|
101
164
|
],
|
|
102
165
|
},
|
|
103
166
|
version: {
|
|
104
167
|
usage: "tfx version [--json]",
|
|
105
168
|
description: "triflux 및 동기화된 스크립트 버전 표시",
|
|
106
169
|
options: [
|
|
107
|
-
{
|
|
170
|
+
{
|
|
171
|
+
name: "--json",
|
|
172
|
+
type: "boolean",
|
|
173
|
+
description: "버전 정보를 JSON으로 출력",
|
|
174
|
+
},
|
|
108
175
|
],
|
|
109
176
|
},
|
|
110
177
|
handoff: {
|
|
111
|
-
usage:
|
|
178
|
+
usage:
|
|
179
|
+
"tfx handoff [--target local|remote] [--decision <text>] [--decision-file <path>] [--output <path>] [--json]",
|
|
112
180
|
description: "현재 작업 컨텍스트를 세션 핸드오프 프롬프트로 직렬화",
|
|
113
181
|
options: [
|
|
114
|
-
{
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
182
|
+
{
|
|
183
|
+
name: "--target",
|
|
184
|
+
type: "string",
|
|
185
|
+
description: "주입 대상 (local|remote, 기본값 remote)",
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
name: "--decision",
|
|
189
|
+
type: "string",
|
|
190
|
+
description: "핸드오프 결정사항 (반복 지정 가능)",
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
name: "--decision-file",
|
|
194
|
+
type: "string",
|
|
195
|
+
description: "결정사항 파일 (라인/불릿 단위)",
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
name: "--output",
|
|
199
|
+
type: "string",
|
|
200
|
+
description: "생성한 핸드오프 프롬프트 저장 경로",
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: "--json",
|
|
204
|
+
type: "boolean",
|
|
205
|
+
description: "핸드오프 결과를 JSON으로 출력",
|
|
206
|
+
},
|
|
119
207
|
],
|
|
120
208
|
},
|
|
121
209
|
list: {
|
|
122
210
|
usage: "tfx list [--json]",
|
|
123
211
|
description: "패키지 스킬과 사용자 스킬 목록 표시",
|
|
124
212
|
options: [
|
|
125
|
-
{
|
|
213
|
+
{
|
|
214
|
+
name: "--json",
|
|
215
|
+
type: "boolean",
|
|
216
|
+
description: "스킬 목록을 JSON으로 출력",
|
|
217
|
+
},
|
|
126
218
|
],
|
|
127
219
|
},
|
|
128
220
|
schema: {
|
|
129
221
|
usage: "tfx schema [command-or-tool]",
|
|
130
222
|
description: "CLI 커맨드 파라미터와 Hub delegator schema 번들 출력",
|
|
131
223
|
options: [
|
|
132
|
-
{
|
|
224
|
+
{
|
|
225
|
+
name: "command-or-tool",
|
|
226
|
+
type: "string",
|
|
227
|
+
description: "예: doctor, setup, delegate, delegate-reply, status",
|
|
228
|
+
},
|
|
133
229
|
],
|
|
134
230
|
},
|
|
135
231
|
hooks: {
|
|
@@ -141,7 +237,8 @@ const CLI_COMMAND_SCHEMAS = Object.freeze({
|
|
|
141
237
|
apply: "오케스트레이터 적용 (settings.json 통합)",
|
|
142
238
|
restore: "원래 settings.json 훅 복원",
|
|
143
239
|
status: "오케스트레이터 적용 상태 확인",
|
|
144
|
-
"set-priority":
|
|
240
|
+
"set-priority":
|
|
241
|
+
"특정 훅 우선순위 변경: hooks set-priority <hookId> <priority>",
|
|
145
242
|
toggle: "특정 훅 활성/비활성 토글: hooks toggle <hookId>",
|
|
146
243
|
},
|
|
147
244
|
},
|
|
@@ -151,22 +248,44 @@ const CLI_COMMAND_SCHEMAS = Object.freeze({
|
|
|
151
248
|
subcommands: {
|
|
152
249
|
list: {
|
|
153
250
|
usage: "tfx mcp list [--json]",
|
|
154
|
-
options: [
|
|
251
|
+
options: [
|
|
252
|
+
{
|
|
253
|
+
name: "--json",
|
|
254
|
+
type: "boolean",
|
|
255
|
+
description: "registry + 실제 설정 상태를 JSON으로 출력",
|
|
256
|
+
},
|
|
257
|
+
],
|
|
155
258
|
},
|
|
156
259
|
sync: {
|
|
157
260
|
usage: "tfx mcp sync [--json]",
|
|
158
|
-
options: [
|
|
261
|
+
options: [
|
|
262
|
+
{
|
|
263
|
+
name: "--json",
|
|
264
|
+
type: "boolean",
|
|
265
|
+
description: "동기화 결과를 JSON으로 출력",
|
|
266
|
+
},
|
|
267
|
+
],
|
|
159
268
|
},
|
|
160
269
|
add: {
|
|
161
270
|
usage: "tfx mcp add <name> --url <url> [--json]",
|
|
162
271
|
options: [
|
|
163
272
|
{ name: "--url", type: "string", description: "등록할 MCP URL" },
|
|
164
|
-
{
|
|
273
|
+
{
|
|
274
|
+
name: "--json",
|
|
275
|
+
type: "boolean",
|
|
276
|
+
description: "등록 결과를 JSON으로 출력",
|
|
277
|
+
},
|
|
165
278
|
],
|
|
166
279
|
},
|
|
167
280
|
remove: {
|
|
168
281
|
usage: "tfx mcp remove <name> [--json]",
|
|
169
|
-
options: [
|
|
282
|
+
options: [
|
|
283
|
+
{
|
|
284
|
+
name: "--json",
|
|
285
|
+
type: "boolean",
|
|
286
|
+
description: "제거 결과를 JSON으로 출력",
|
|
287
|
+
},
|
|
288
|
+
],
|
|
170
289
|
},
|
|
171
290
|
},
|
|
172
291
|
},
|
|
@@ -176,25 +295,54 @@ const CLI_COMMAND_SCHEMAS = Object.freeze({
|
|
|
176
295
|
subcommands: {
|
|
177
296
|
start: { usage: "tfx hub start [--port N]" },
|
|
178
297
|
stop: { usage: "tfx hub stop" },
|
|
179
|
-
ensure: {
|
|
298
|
+
ensure: {
|
|
299
|
+
usage: "tfx hub ensure [--port N] [--json]",
|
|
300
|
+
description: "헬스체크 + 자동 시작 (idempotent)",
|
|
301
|
+
},
|
|
180
302
|
status: {
|
|
181
303
|
usage: "tfx hub status [--json]",
|
|
182
|
-
options: [
|
|
304
|
+
options: [
|
|
305
|
+
{
|
|
306
|
+
name: "--json",
|
|
307
|
+
type: "boolean",
|
|
308
|
+
description: "허브 상태를 JSON으로 출력",
|
|
309
|
+
},
|
|
310
|
+
],
|
|
183
311
|
},
|
|
184
312
|
},
|
|
185
313
|
},
|
|
186
314
|
multi: {
|
|
187
|
-
usage:
|
|
315
|
+
usage:
|
|
316
|
+
"tfx multi [--dashboard-layout lite|single|split-2col|split-3col|auto] <subcommand|task>",
|
|
188
317
|
description: "멀티-CLI 팀 모드",
|
|
189
318
|
options: [
|
|
190
|
-
{
|
|
191
|
-
|
|
192
|
-
|
|
319
|
+
{
|
|
320
|
+
name: "--dashboard",
|
|
321
|
+
type: "boolean",
|
|
322
|
+
description: "headless dashboard viewer 표시 (기본값: 켜짐)",
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
name: "--no-dashboard",
|
|
326
|
+
type: "boolean",
|
|
327
|
+
description: "headless dashboard viewer 비활성화",
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
name: "--dashboard-layout",
|
|
331
|
+
type: "string",
|
|
332
|
+
description:
|
|
333
|
+
"dashboard viewer 레이아웃 선택: lite|single|split-2col|split-3col|auto",
|
|
334
|
+
},
|
|
193
335
|
],
|
|
194
336
|
subcommands: {
|
|
195
337
|
status: {
|
|
196
338
|
usage: "tfx multi status [--json]",
|
|
197
|
-
options: [
|
|
339
|
+
options: [
|
|
340
|
+
{
|
|
341
|
+
name: "--json",
|
|
342
|
+
type: "boolean",
|
|
343
|
+
description: "팀 상태를 JSON으로 출력",
|
|
344
|
+
},
|
|
345
|
+
],
|
|
198
346
|
},
|
|
199
347
|
},
|
|
200
348
|
},
|
|
@@ -203,13 +351,27 @@ const CLI_COMMAND_SCHEMAS = Object.freeze({
|
|
|
203
351
|
// ── 유틸리티 ──
|
|
204
352
|
// ok/warn/fail/info/section 의 console.log는 디버그 로그가 아닌 의도된 CLI 출력입니다.
|
|
205
353
|
|
|
206
|
-
function ok(msg) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
function
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
function
|
|
354
|
+
function ok(msg) {
|
|
355
|
+
console.log(` ${GREEN_BRIGHT}✓${RESET} ${msg}`);
|
|
356
|
+
}
|
|
357
|
+
function warn(msg) {
|
|
358
|
+
console.log(` ${YELLOW}⚠${RESET} ${msg}`);
|
|
359
|
+
}
|
|
360
|
+
function fail(msg) {
|
|
361
|
+
console.log(` ${RED_BRIGHT}✗${RESET} ${msg}`);
|
|
362
|
+
}
|
|
363
|
+
function info(msg) {
|
|
364
|
+
console.log(` ${GRAY}${msg}${RESET}`);
|
|
365
|
+
}
|
|
366
|
+
function section(title) {
|
|
367
|
+
console.log(`\n ${AMBER}▸${RESET} ${BOLD}${title}${RESET}`);
|
|
368
|
+
}
|
|
369
|
+
function stripAnsi(value) {
|
|
370
|
+
return String(value ?? "").replace(ANSI_PATTERN, "");
|
|
371
|
+
}
|
|
372
|
+
function printJson(payload) {
|
|
373
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
374
|
+
}
|
|
213
375
|
|
|
214
376
|
function withConsoleSilenced(enabled, fn) {
|
|
215
377
|
if (!enabled) return fn();
|
|
@@ -225,12 +387,10 @@ function withConsoleSilenced(enabled, fn) {
|
|
|
225
387
|
}
|
|
226
388
|
}
|
|
227
389
|
|
|
228
|
-
function createCliError(
|
|
229
|
-
|
|
230
|
-
reason = "error",
|
|
231
|
-
|
|
232
|
-
cause = null,
|
|
233
|
-
} = {}) {
|
|
390
|
+
function createCliError(
|
|
391
|
+
message,
|
|
392
|
+
{ exitCode = EXIT_ERROR, reason = "error", fix = null, cause = null } = {},
|
|
393
|
+
) {
|
|
234
394
|
const error = new Error(message);
|
|
235
395
|
error.exitCode = exitCode;
|
|
236
396
|
error.reason = reason;
|
|
@@ -257,9 +417,12 @@ function inferReason(error, exitCode) {
|
|
|
257
417
|
function inferFix(error, exitCode) {
|
|
258
418
|
if (typeof error?.fix === "string" && error.fix) return error.fix;
|
|
259
419
|
if (exitCode === EXIT_ARG_ERROR) return "tfx --help";
|
|
260
|
-
if (exitCode === EXIT_CLI_MISSING)
|
|
261
|
-
|
|
262
|
-
if (exitCode ===
|
|
420
|
+
if (exitCode === EXIT_CLI_MISSING)
|
|
421
|
+
return "필수 CLI를 설치한 뒤 `tfx doctor`로 상태를 다시 확인하세요.";
|
|
422
|
+
if (exitCode === EXIT_HUB_ERROR)
|
|
423
|
+
return "`tfx hub start`로 허브를 다시 시작하거나 설치 상태를 확인하세요.";
|
|
424
|
+
if (exitCode === EXIT_CONFIG_ERROR)
|
|
425
|
+
return "설정 파일 JSON/TOML 문법을 수정한 뒤 다시 실행하세요.";
|
|
263
426
|
return null;
|
|
264
427
|
}
|
|
265
428
|
|
|
@@ -288,7 +451,11 @@ function handleFatalError(error, { json = false } = {}) {
|
|
|
288
451
|
function renderErrorMessage(message, fallback = "unknown error") {
|
|
289
452
|
if (typeof message === "string") {
|
|
290
453
|
const normalized = message.trim().toLowerCase();
|
|
291
|
-
if (
|
|
454
|
+
if (
|
|
455
|
+
normalized.length > 0 &&
|
|
456
|
+
normalized !== "undefined" &&
|
|
457
|
+
normalized !== "null"
|
|
458
|
+
) {
|
|
292
459
|
return message.trim();
|
|
293
460
|
}
|
|
294
461
|
}
|
|
@@ -297,18 +464,40 @@ function renderErrorMessage(message, fallback = "unknown error") {
|
|
|
297
464
|
|
|
298
465
|
function which(cmd) {
|
|
299
466
|
try {
|
|
300
|
-
const result =
|
|
301
|
-
|
|
302
|
-
|
|
467
|
+
const result =
|
|
468
|
+
process.platform === "win32"
|
|
469
|
+
? execFileSync("where", [cmd], {
|
|
470
|
+
encoding: "utf8",
|
|
471
|
+
timeout: 5000,
|
|
472
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
473
|
+
windowsHide: true,
|
|
474
|
+
})
|
|
475
|
+
: execFileSync("which", [cmd], {
|
|
476
|
+
encoding: "utf8",
|
|
477
|
+
timeout: 5000,
|
|
478
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
479
|
+
});
|
|
303
480
|
return result.trim().split(/\r?\n/)[0] || null;
|
|
304
|
-
} catch {
|
|
481
|
+
} catch {
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
305
484
|
}
|
|
306
485
|
|
|
307
486
|
function whichInShell(cmd, shell) {
|
|
308
487
|
const shellArgs = {
|
|
309
|
-
bash: [
|
|
488
|
+
bash: [
|
|
489
|
+
"bash",
|
|
490
|
+
["-c", `source ~/.bashrc 2>/dev/null && command -v "${cmd}" 2>/dev/null`],
|
|
491
|
+
],
|
|
310
492
|
cmd: ["cmd", ["/c", "where", cmd]],
|
|
311
|
-
pwsh: [
|
|
493
|
+
pwsh: [
|
|
494
|
+
"pwsh",
|
|
495
|
+
[
|
|
496
|
+
"-NoProfile",
|
|
497
|
+
"-c",
|
|
498
|
+
`(Get-Command '${cmd.replace(/'/g, "''")}' -EA SilentlyContinue).Source`,
|
|
499
|
+
],
|
|
500
|
+
],
|
|
312
501
|
};
|
|
313
502
|
const entry = shellArgs[shell];
|
|
314
503
|
if (!entry) return null;
|
|
@@ -320,22 +509,36 @@ function whichInShell(cmd, shell) {
|
|
|
320
509
|
windowsHide: true,
|
|
321
510
|
}).trim();
|
|
322
511
|
return result.split(/\r?\n/)[0] || null;
|
|
323
|
-
} catch {
|
|
512
|
+
} catch {
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
324
515
|
}
|
|
325
516
|
|
|
326
517
|
function isDevUpdateRequested(argv = process.argv) {
|
|
327
|
-
return
|
|
518
|
+
return (
|
|
519
|
+
argv.includes("--dev") || argv.includes("@dev") || argv.includes("dev")
|
|
520
|
+
);
|
|
328
521
|
}
|
|
329
522
|
|
|
330
523
|
function checkShellAvailable(shell) {
|
|
331
|
-
const cmds = {
|
|
524
|
+
const cmds = {
|
|
525
|
+
bash: "bash --version",
|
|
526
|
+
cmd: "cmd /c echo ok",
|
|
527
|
+
pwsh: "pwsh -NoProfile -c echo ok",
|
|
528
|
+
};
|
|
332
529
|
try {
|
|
333
|
-
execSync(cmds[shell], {
|
|
530
|
+
execSync(cmds[shell], {
|
|
531
|
+
encoding: "utf8",
|
|
532
|
+
timeout: 5000,
|
|
533
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
534
|
+
windowsHide: true,
|
|
535
|
+
});
|
|
334
536
|
return true;
|
|
335
|
-
} catch {
|
|
537
|
+
} catch {
|
|
538
|
+
return false;
|
|
539
|
+
}
|
|
336
540
|
}
|
|
337
541
|
|
|
338
|
-
|
|
339
542
|
function parseSessionCreated(rawValue) {
|
|
340
543
|
const value = String(rawValue || "").trim();
|
|
341
544
|
if (!value) return null;
|
|
@@ -350,7 +553,10 @@ function parseSessionCreated(rawValue) {
|
|
|
350
553
|
return Math.floor(parsed / 1000);
|
|
351
554
|
}
|
|
352
555
|
|
|
353
|
-
const normalized = value.replace(
|
|
556
|
+
const normalized = value.replace(
|
|
557
|
+
/^(\d{2})-(\d{2})-(\d{2})(\s+)/,
|
|
558
|
+
"20$1-$2-$3$4",
|
|
559
|
+
);
|
|
354
560
|
const reparsed = Date.parse(normalized);
|
|
355
561
|
if (Number.isFinite(reparsed)) {
|
|
356
562
|
return Math.floor(reparsed / 1000);
|
|
@@ -371,7 +577,9 @@ function readTeamSessionCreatedMap() {
|
|
|
371
577
|
const createdMap = new Map();
|
|
372
578
|
|
|
373
579
|
try {
|
|
374
|
-
const output = tmuxExec(
|
|
580
|
+
const output = tmuxExec(
|
|
581
|
+
'list-sessions -F "#{session_name} #{session_created}"',
|
|
582
|
+
);
|
|
375
583
|
for (const line of output.split(/\r?\n/)) {
|
|
376
584
|
const trimmed = line.trim();
|
|
377
585
|
if (!trimmed) continue;
|
|
@@ -408,10 +616,17 @@ function inspectTeamSessions() {
|
|
|
408
616
|
const createdMap = readTeamSessionCreatedMap();
|
|
409
617
|
const nowSec = Math.floor(Date.now() / 1000);
|
|
410
618
|
const sessions = sessionNames.map((sessionName) => {
|
|
411
|
-
const createdInfo = createdMap.get(sessionName) || {
|
|
619
|
+
const createdInfo = createdMap.get(sessionName) || {
|
|
620
|
+
createdAt: null,
|
|
621
|
+
createdRaw: "",
|
|
622
|
+
};
|
|
412
623
|
const attachedCount = getSessionAttachedCount(sessionName);
|
|
413
|
-
const ageSec =
|
|
414
|
-
|
|
624
|
+
const ageSec =
|
|
625
|
+
createdInfo.createdAt == null
|
|
626
|
+
? null
|
|
627
|
+
: Math.max(0, nowSec - createdInfo.createdAt);
|
|
628
|
+
const stale =
|
|
629
|
+
ageSec != null && ageSec >= STALE_TEAM_MAX_AGE_SEC && attachedCount === 0;
|
|
415
630
|
|
|
416
631
|
return {
|
|
417
632
|
sessionName,
|
|
@@ -459,7 +674,6 @@ async function cleanupStaleTeamSessions(staleSessions) {
|
|
|
459
674
|
return { cleaned, failed };
|
|
460
675
|
}
|
|
461
676
|
|
|
462
|
-
|
|
463
677
|
function previewCodexProfiles() {
|
|
464
678
|
const original = existsSync(CODEX_CONFIG_PATH)
|
|
465
679
|
? readFileSync(CODEX_CONFIG_PATH, "utf8")
|
|
@@ -481,13 +695,19 @@ function previewCodexProfiles() {
|
|
|
481
695
|
}
|
|
482
696
|
}
|
|
483
697
|
|
|
484
|
-
const windowsSandbox =
|
|
698
|
+
const windowsSandbox =
|
|
699
|
+
process.platform === "win32" && !updated.includes("[windows]");
|
|
485
700
|
|
|
486
701
|
return {
|
|
487
702
|
path: CODEX_CONFIG_PATH,
|
|
488
703
|
profiles,
|
|
489
704
|
windowsSandbox,
|
|
490
|
-
change:
|
|
705
|
+
change:
|
|
706
|
+
profiles.length > 0 || windowsSandbox
|
|
707
|
+
? original
|
|
708
|
+
? "update"
|
|
709
|
+
: "create"
|
|
710
|
+
: "noop",
|
|
491
711
|
};
|
|
492
712
|
}
|
|
493
713
|
|
|
@@ -505,7 +725,9 @@ function syncFile(src, dst, label) {
|
|
|
505
725
|
|
|
506
726
|
if (!existsSync(dst)) {
|
|
507
727
|
copyFileSync(src, dst);
|
|
508
|
-
try {
|
|
728
|
+
try {
|
|
729
|
+
chmodSync(dst, 0o755);
|
|
730
|
+
} catch {}
|
|
509
731
|
ok(`${label}: 설치됨 ${srcVer ? `(v${srcVer})` : ""}`);
|
|
510
732
|
return true;
|
|
511
733
|
}
|
|
@@ -514,10 +736,15 @@ function syncFile(src, dst, label) {
|
|
|
514
736
|
const dstContent = readFileSync(dst, "utf8");
|
|
515
737
|
if (srcContent !== dstContent) {
|
|
516
738
|
copyFileSync(src, dst);
|
|
517
|
-
try {
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
739
|
+
try {
|
|
740
|
+
chmodSync(dst, 0o755);
|
|
741
|
+
} catch {}
|
|
742
|
+
const verInfo =
|
|
743
|
+
srcVer && dstVer && srcVer !== dstVer
|
|
744
|
+
? `(v${dstVer} → v${srcVer})`
|
|
745
|
+
: srcVer
|
|
746
|
+
? `(v${srcVer}, 내용 변경)`
|
|
747
|
+
: "(내용 변경)";
|
|
521
748
|
ok(`${label}: 업데이트됨 ${verInfo}`);
|
|
522
749
|
return true;
|
|
523
750
|
}
|
|
@@ -561,22 +788,36 @@ function syncClaudeRoutingSectionsForCli() {
|
|
|
561
788
|
ensureGlobalClaudeRoutingSection(CLAUDE_DIR),
|
|
562
789
|
];
|
|
563
790
|
} catch (error) {
|
|
564
|
-
const reason =
|
|
565
|
-
|
|
791
|
+
const reason =
|
|
792
|
+
error instanceof Error ? error.message : "routing_sync_failed";
|
|
793
|
+
return [
|
|
794
|
+
{
|
|
795
|
+
action: "unchanged",
|
|
796
|
+
path: join(PKG_ROOT, "CLAUDE.md"),
|
|
797
|
+
skipped: true,
|
|
798
|
+
reason,
|
|
799
|
+
},
|
|
800
|
+
];
|
|
566
801
|
}
|
|
567
802
|
}
|
|
568
803
|
|
|
569
804
|
function getClaudeRoutingSyncSummary(results) {
|
|
570
|
-
return results.reduce(
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
805
|
+
return results.reduce(
|
|
806
|
+
(summary, result) => ({
|
|
807
|
+
changed:
|
|
808
|
+
summary.changed +
|
|
809
|
+
(result.action === "created" || result.action === "updated" ? 1 : 0),
|
|
810
|
+
skipped: summary.skipped + (result.skipped ? 1 : 0),
|
|
811
|
+
}),
|
|
812
|
+
{ changed: 0, skipped: 0 },
|
|
813
|
+
);
|
|
574
814
|
}
|
|
575
815
|
|
|
576
816
|
// ── 크로스 셸 진단 ──
|
|
577
817
|
|
|
578
818
|
function checkCliCrossShell(cmd, installHint) {
|
|
579
|
-
const shells =
|
|
819
|
+
const shells =
|
|
820
|
+
process.platform === "win32" ? ["bash", "cmd", "pwsh"] : ["bash"];
|
|
580
821
|
let anyFound = false;
|
|
581
822
|
let bashMissing = false;
|
|
582
823
|
const shellResults = [];
|
|
@@ -595,7 +836,12 @@ function checkCliCrossShell(cmd, installHint) {
|
|
|
595
836
|
} else {
|
|
596
837
|
fail(`${shell}: 미발견`);
|
|
597
838
|
if (shell === "bash") bashMissing = true;
|
|
598
|
-
shellResults.push({
|
|
839
|
+
shellResults.push({
|
|
840
|
+
shell,
|
|
841
|
+
status: "missing",
|
|
842
|
+
path: null,
|
|
843
|
+
fix: installHint,
|
|
844
|
+
});
|
|
599
845
|
}
|
|
600
846
|
}
|
|
601
847
|
|
|
@@ -620,7 +866,7 @@ function checkCliCrossShell(cmd, installHint) {
|
|
|
620
866
|
bashMissing,
|
|
621
867
|
shells: shellResults,
|
|
622
868
|
status: "degraded",
|
|
623
|
-
fix:
|
|
869
|
+
fix: "bash PATH를 정리한 뒤 `tfx doctor`를 다시 실행하세요.",
|
|
624
870
|
};
|
|
625
871
|
}
|
|
626
872
|
return {
|
|
@@ -677,7 +923,11 @@ function previewStatusLineAction() {
|
|
|
677
923
|
return {
|
|
678
924
|
type: "statusLine",
|
|
679
925
|
path: settingsPath,
|
|
680
|
-
change: currentCmd.includes("hud-qos-status.mjs")
|
|
926
|
+
change: currentCmd.includes("hud-qos-status.mjs")
|
|
927
|
+
? "noop"
|
|
928
|
+
: currentCmd
|
|
929
|
+
? "update"
|
|
930
|
+
: "create",
|
|
681
931
|
current: currentCmd || null,
|
|
682
932
|
target: hudPath,
|
|
683
933
|
};
|
|
@@ -744,7 +994,9 @@ function previewClaudeRoutingAction() {
|
|
|
744
994
|
}
|
|
745
995
|
|
|
746
996
|
const globalContent = readFileSync(globalClaudePath, "utf8");
|
|
747
|
-
const hasRouting =
|
|
997
|
+
const hasRouting =
|
|
998
|
+
globalContent.includes("<routing>") ||
|
|
999
|
+
globalContent.includes("## triflux CLI 라우팅");
|
|
748
1000
|
|
|
749
1001
|
return {
|
|
750
1002
|
type: "claude-guidance",
|
|
@@ -756,7 +1008,9 @@ function previewClaudeRoutingAction() {
|
|
|
756
1008
|
|
|
757
1009
|
function buildSetupDryRunPlan() {
|
|
758
1010
|
const actions = [
|
|
759
|
-
...SYNC_MAP.map(({ src, dst, label }) =>
|
|
1011
|
+
...SYNC_MAP.map(({ src, dst, label }) =>
|
|
1012
|
+
describeSyncAction(src, dst, label),
|
|
1013
|
+
),
|
|
760
1014
|
...listSkillSyncActions(),
|
|
761
1015
|
];
|
|
762
1016
|
actions.push(previewClaudeRoutingAction());
|
|
@@ -793,8 +1047,13 @@ function cmdSetup(options = {}) {
|
|
|
793
1047
|
}
|
|
794
1048
|
{
|
|
795
1049
|
const claudeGuide = ensureGlobalClaudeRoutingSection(CLAUDE_DIR);
|
|
796
|
-
if (claudeGuide.skipped)
|
|
797
|
-
|
|
1050
|
+
if (claudeGuide.skipped)
|
|
1051
|
+
warn(`CLAUDE.md 라우팅 섹션 확인 실패: ${claudeGuide.reason}`);
|
|
1052
|
+
else if (
|
|
1053
|
+
claudeGuide.action === "created" ||
|
|
1054
|
+
claudeGuide.action === "updated"
|
|
1055
|
+
)
|
|
1056
|
+
ok("CLAUDE.md: 전역 triflux 라우팅 요약 갱신");
|
|
798
1057
|
else ok("CLAUDE.md: 전역 triflux 라우팅 요약 유지");
|
|
799
1058
|
}
|
|
800
1059
|
|
|
@@ -833,7 +1092,10 @@ function cmdSetup(options = {}) {
|
|
|
833
1092
|
const rSrc = join(refSrc, refFile);
|
|
834
1093
|
const rDst = join(refDst, refFile);
|
|
835
1094
|
if (statSync(rSrc).isFile()) {
|
|
836
|
-
if (
|
|
1095
|
+
if (
|
|
1096
|
+
!existsSync(rDst) ||
|
|
1097
|
+
readFileSync(rSrc, "utf8") !== readFileSync(rDst, "utf8")
|
|
1098
|
+
) {
|
|
837
1099
|
copyFileSync(rSrc, rDst);
|
|
838
1100
|
}
|
|
839
1101
|
}
|
|
@@ -845,7 +1107,10 @@ function cmdSetup(options = {}) {
|
|
|
845
1107
|
const src = join(srcDir, "SKILL.md");
|
|
846
1108
|
if (!existsSync(src)) continue;
|
|
847
1109
|
skillTotal++;
|
|
848
|
-
skillCount += syncAliasedSkillDir(srcDir, join(skillsDst, alias), {
|
|
1110
|
+
skillCount += syncAliasedSkillDir(srcDir, join(skillsDst, alias), {
|
|
1111
|
+
alias,
|
|
1112
|
+
source,
|
|
1113
|
+
});
|
|
849
1114
|
}
|
|
850
1115
|
if (skillCount > 0) {
|
|
851
1116
|
ok(`스킬: ${skillCount}/${skillTotal}개 업데이트됨`);
|
|
@@ -855,22 +1120,36 @@ function cmdSetup(options = {}) {
|
|
|
855
1120
|
// Stale 스킬 정리 (패키지에서 제거된 tfx-* 스킬 삭제)
|
|
856
1121
|
const staleCleanup = cleanupStaleSkills(skillsDst, skillsSrc);
|
|
857
1122
|
if (staleCleanup.count > 0) {
|
|
858
|
-
ok(
|
|
1123
|
+
ok(
|
|
1124
|
+
`구형 스킬 ${staleCleanup.count}개 제거: ${staleCleanup.removed.join(", ")}`,
|
|
1125
|
+
);
|
|
859
1126
|
}
|
|
860
1127
|
}
|
|
861
1128
|
|
|
862
1129
|
// ── psmux 기본 셸 자동 수정 (cmd.exe → PowerShell) ──
|
|
863
1130
|
if (process.platform === "win32" && which("psmux")) {
|
|
864
1131
|
try {
|
|
865
|
-
const shellOut = execSync("psmux show-options -g default-shell 2>NUL", {
|
|
1132
|
+
const shellOut = execSync("psmux show-options -g default-shell 2>NUL", {
|
|
1133
|
+
encoding: "utf8",
|
|
1134
|
+
timeout: 3000,
|
|
1135
|
+
}).trim();
|
|
866
1136
|
if (!/powershell|pwsh/i.test(shellOut)) {
|
|
867
|
-
const pwsh = which("pwsh")
|
|
1137
|
+
const pwsh = which("pwsh")
|
|
1138
|
+
? "pwsh"
|
|
1139
|
+
: which("powershell.exe")
|
|
1140
|
+
? "powershell.exe"
|
|
1141
|
+
: "";
|
|
868
1142
|
if (pwsh) {
|
|
869
|
-
execSync(`psmux set-option -g default-shell "${pwsh}"`, {
|
|
1143
|
+
execSync(`psmux set-option -g default-shell "${pwsh}"`, {
|
|
1144
|
+
timeout: 3000,
|
|
1145
|
+
stdio: "pipe",
|
|
1146
|
+
});
|
|
870
1147
|
ok(`psmux 기본 셸 → ${pwsh}`);
|
|
871
1148
|
}
|
|
872
1149
|
}
|
|
873
|
-
} catch {
|
|
1150
|
+
} catch {
|
|
1151
|
+
/* psmux 서버 미실행 — 무시 */
|
|
1152
|
+
}
|
|
874
1153
|
}
|
|
875
1154
|
|
|
876
1155
|
// ── 결과 추적 ──
|
|
@@ -878,16 +1157,29 @@ function cmdSetup(options = {}) {
|
|
|
878
1157
|
|
|
879
1158
|
if (!skipClaudeMdSync) {
|
|
880
1159
|
const claudeRoutingResults = syncClaudeRoutingSectionsForCli();
|
|
881
|
-
const claudeRoutingSummary =
|
|
1160
|
+
const claudeRoutingSummary =
|
|
1161
|
+
getClaudeRoutingSyncSummary(claudeRoutingResults);
|
|
882
1162
|
if (claudeRoutingSummary.changed > 0) {
|
|
883
1163
|
ok(`CLAUDE.md 라우팅: ${claudeRoutingSummary.changed}개 파일 반영`);
|
|
884
|
-
summary.push({
|
|
1164
|
+
summary.push({
|
|
1165
|
+
item: "CLAUDE.md 라우팅",
|
|
1166
|
+
status: "✅",
|
|
1167
|
+
detail: `${claudeRoutingSummary.changed}개 파일 반영`,
|
|
1168
|
+
});
|
|
885
1169
|
} else if (claudeRoutingSummary.skipped > 0) {
|
|
886
1170
|
ok("CLAUDE.md 라우팅: 대상 파일 없음 (건너뜀)");
|
|
887
|
-
summary.push({
|
|
1171
|
+
summary.push({
|
|
1172
|
+
item: "CLAUDE.md 라우팅",
|
|
1173
|
+
status: "⏭️",
|
|
1174
|
+
detail: "대상 파일 없음",
|
|
1175
|
+
});
|
|
888
1176
|
} else {
|
|
889
1177
|
ok("CLAUDE.md 라우팅: 최신 상태");
|
|
890
|
-
summary.push({
|
|
1178
|
+
summary.push({
|
|
1179
|
+
item: "CLAUDE.md 라우팅",
|
|
1180
|
+
status: "✅",
|
|
1181
|
+
detail: "최신 상태",
|
|
1182
|
+
});
|
|
891
1183
|
}
|
|
892
1184
|
}
|
|
893
1185
|
|
|
@@ -897,11 +1189,21 @@ function cmdSetup(options = {}) {
|
|
|
897
1189
|
warn(`Codex profiles 설정 실패: ${reason}`);
|
|
898
1190
|
summary.push({ item: "Codex profiles", status: "⚠️", detail: reason });
|
|
899
1191
|
} else if (codexProfileResult.changed > 0) {
|
|
900
|
-
ok(
|
|
901
|
-
|
|
1192
|
+
ok(
|
|
1193
|
+
`Codex profiles: ${codexProfileResult.changed}개 반영됨 (~/.codex/config.toml)`,
|
|
1194
|
+
);
|
|
1195
|
+
summary.push({
|
|
1196
|
+
item: "Codex profiles",
|
|
1197
|
+
status: "✅",
|
|
1198
|
+
detail: `${codexProfileResult.changed}개 반영됨`,
|
|
1199
|
+
});
|
|
902
1200
|
} else {
|
|
903
1201
|
ok("Codex profiles: 이미 준비됨");
|
|
904
|
-
summary.push({
|
|
1202
|
+
summary.push({
|
|
1203
|
+
item: "Codex profiles",
|
|
1204
|
+
status: "✅",
|
|
1205
|
+
detail: "이미 준비됨",
|
|
1206
|
+
});
|
|
905
1207
|
}
|
|
906
1208
|
|
|
907
1209
|
// Gemini 프로필
|
|
@@ -911,14 +1213,28 @@ function cmdSetup(options = {}) {
|
|
|
911
1213
|
warn(`Gemini profiles 설정 실패: ${reason}`);
|
|
912
1214
|
summary.push({ item: "Gemini profiles", status: "⚠️", detail: reason });
|
|
913
1215
|
} else if (geminiResult.created) {
|
|
914
|
-
ok(
|
|
915
|
-
|
|
1216
|
+
ok(
|
|
1217
|
+
`Gemini profiles: ${geminiResult.count}개 생성됨 (~/.gemini/triflux-profiles.json)`,
|
|
1218
|
+
);
|
|
1219
|
+
summary.push({
|
|
1220
|
+
item: "Gemini profiles",
|
|
1221
|
+
status: "✅",
|
|
1222
|
+
detail: `${geminiResult.count}개 생성됨`,
|
|
1223
|
+
});
|
|
916
1224
|
} else if (geminiResult.added > 0) {
|
|
917
1225
|
ok(`Gemini profiles: ${geminiResult.added}개 추가됨`);
|
|
918
|
-
summary.push({
|
|
1226
|
+
summary.push({
|
|
1227
|
+
item: "Gemini profiles",
|
|
1228
|
+
status: "✅",
|
|
1229
|
+
detail: `${geminiResult.added}개 추가됨 (총 ${geminiResult.count}개)`,
|
|
1230
|
+
});
|
|
919
1231
|
} else {
|
|
920
1232
|
ok(`Gemini profiles: ${geminiResult.count}개 준비됨`);
|
|
921
|
-
summary.push({
|
|
1233
|
+
summary.push({
|
|
1234
|
+
item: "Gemini profiles",
|
|
1235
|
+
status: "✅",
|
|
1236
|
+
detail: `${geminiResult.count}개 준비됨`,
|
|
1237
|
+
});
|
|
922
1238
|
}
|
|
923
1239
|
|
|
924
1240
|
// hub MCP 사전 등록 (서버 미실행이어도 설정만 등록 — hub start 시 즉시 사용 가능)
|
|
@@ -944,12 +1260,18 @@ function cmdSetup(options = {}) {
|
|
|
944
1260
|
const currentCmd = settings.statusLine?.command || "";
|
|
945
1261
|
if (currentCmd.includes("hud-qos-status.mjs")) {
|
|
946
1262
|
ok("statusLine 이미 설정됨");
|
|
947
|
-
summary.push({
|
|
1263
|
+
summary.push({
|
|
1264
|
+
item: "HUD statusLine",
|
|
1265
|
+
status: "✅",
|
|
1266
|
+
detail: "이미 설정됨",
|
|
1267
|
+
});
|
|
948
1268
|
} else {
|
|
949
1269
|
const nodePath = process.execPath.replace(/\\/g, "/");
|
|
950
1270
|
const hudForward = hudPath.replace(/\\/g, "/");
|
|
951
1271
|
const nodeRef = nodePath.includes(" ") ? `"${nodePath}"` : nodePath;
|
|
952
|
-
const hudRef = hudForward.includes(" ")
|
|
1272
|
+
const hudRef = hudForward.includes(" ")
|
|
1273
|
+
? `"${hudForward}"`
|
|
1274
|
+
: hudForward;
|
|
953
1275
|
|
|
954
1276
|
if (currentCmd) {
|
|
955
1277
|
warn(`기존 statusLine 덮어쓰기: ${currentCmd}`);
|
|
@@ -960,9 +1282,17 @@ function cmdSetup(options = {}) {
|
|
|
960
1282
|
command: `${nodeRef} ${hudRef}`,
|
|
961
1283
|
};
|
|
962
1284
|
|
|
963
|
-
writeFileSync(
|
|
1285
|
+
writeFileSync(
|
|
1286
|
+
settingsPath,
|
|
1287
|
+
JSON.stringify(settings, null, 2) + "\n",
|
|
1288
|
+
"utf8",
|
|
1289
|
+
);
|
|
964
1290
|
ok("statusLine 설정 완료 — 세션 재시작 후 HUD 표시");
|
|
965
|
-
summary.push({
|
|
1291
|
+
summary.push({
|
|
1292
|
+
item: "HUD statusLine",
|
|
1293
|
+
status: "✅",
|
|
1294
|
+
detail: "설정 완료",
|
|
1295
|
+
});
|
|
966
1296
|
}
|
|
967
1297
|
} catch (e) {
|
|
968
1298
|
throw createCliError(`settings.json 처리 실패: ${e.message}`, {
|
|
@@ -974,7 +1304,11 @@ function cmdSetup(options = {}) {
|
|
|
974
1304
|
}
|
|
975
1305
|
} else {
|
|
976
1306
|
warn("HUD 파일 없음 — 먼저 파일 동기화 필요");
|
|
977
|
-
summary.push({
|
|
1307
|
+
summary.push({
|
|
1308
|
+
item: "HUD statusLine",
|
|
1309
|
+
status: "⚠️",
|
|
1310
|
+
detail: "HUD 파일 없음",
|
|
1311
|
+
});
|
|
978
1312
|
}
|
|
979
1313
|
|
|
980
1314
|
// CLI 존재 확인
|
|
@@ -986,27 +1320,41 @@ function cmdSetup(options = {}) {
|
|
|
986
1320
|
if (which(name)) {
|
|
987
1321
|
summary.push({ item: `${name} CLI`, status: "✅", detail: "설치됨" });
|
|
988
1322
|
} else {
|
|
989
|
-
summary.push({
|
|
1323
|
+
summary.push({
|
|
1324
|
+
item: `${name} CLI`,
|
|
1325
|
+
status: "⏭️",
|
|
1326
|
+
detail: `미설치 (${install})`,
|
|
1327
|
+
});
|
|
990
1328
|
}
|
|
991
1329
|
}
|
|
992
1330
|
|
|
993
1331
|
// Star request (버전 게이팅 + 인터랙티브 [y/n])
|
|
994
|
-
const showStar =
|
|
1332
|
+
const showStar =
|
|
1333
|
+
STAR_PROMPT_VERSIONS.length === 0 ||
|
|
1334
|
+
STAR_PROMPT_VERSIONS.includes(PKG.version);
|
|
995
1335
|
if (showStar) {
|
|
996
1336
|
let ghOk = false;
|
|
997
1337
|
try {
|
|
998
|
-
execFileSync("gh", ["auth", "status"], {
|
|
1338
|
+
execFileSync("gh", ["auth", "status"], {
|
|
1339
|
+
timeout: 5000,
|
|
1340
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1341
|
+
});
|
|
999
1342
|
ghOk = true;
|
|
1000
1343
|
} catch {}
|
|
1001
1344
|
|
|
1002
1345
|
if (!ghOk) {
|
|
1003
1346
|
// gh 미설치/미인증 — URL만 표시
|
|
1004
1347
|
console.log();
|
|
1005
|
-
info(
|
|
1348
|
+
info(
|
|
1349
|
+
`${AMBER}⭐${RESET} 하나가 큰 차이를 만듭니다. ${CYAN}https://github.com/tellang/triflux${RESET}`,
|
|
1350
|
+
);
|
|
1006
1351
|
} else {
|
|
1007
1352
|
let alreadyStarred = false;
|
|
1008
1353
|
try {
|
|
1009
|
-
execFileSync("gh", ["api", "user/starred/tellang/triflux"], {
|
|
1354
|
+
execFileSync("gh", ["api", "user/starred/tellang/triflux"], {
|
|
1355
|
+
timeout: 5000,
|
|
1356
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1357
|
+
});
|
|
1010
1358
|
alreadyStarred = true;
|
|
1011
1359
|
} catch {}
|
|
1012
1360
|
|
|
@@ -1016,7 +1364,9 @@ function cmdSetup(options = {}) {
|
|
|
1016
1364
|
} else {
|
|
1017
1365
|
// 인터랙티브 confirm
|
|
1018
1366
|
console.log();
|
|
1019
|
-
process.stdout.write(
|
|
1367
|
+
process.stdout.write(
|
|
1368
|
+
` ${AMBER}⭐${RESET} 하나가 큰 차이를 만듭니다. Star? ${DIM}[y/N]${RESET} `,
|
|
1369
|
+
);
|
|
1020
1370
|
let answer = "";
|
|
1021
1371
|
try {
|
|
1022
1372
|
const buf = Buffer.alloc(128);
|
|
@@ -1027,9 +1377,14 @@ function cmdSetup(options = {}) {
|
|
|
1027
1377
|
}
|
|
1028
1378
|
if (answer.startsWith("y")) {
|
|
1029
1379
|
try {
|
|
1030
|
-
execFileSync(
|
|
1031
|
-
|
|
1032
|
-
|
|
1380
|
+
execFileSync(
|
|
1381
|
+
"gh",
|
|
1382
|
+
["api", "-X", "PUT", "/user/starred/tellang/triflux"],
|
|
1383
|
+
{
|
|
1384
|
+
timeout: 5000,
|
|
1385
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1386
|
+
},
|
|
1387
|
+
);
|
|
1033
1388
|
ok(`함께해 주셔서 감사합니다. ${AMBER}⭐${RESET}`);
|
|
1034
1389
|
} catch {
|
|
1035
1390
|
info(`${CYAN}https://github.com/tellang/triflux${RESET}`);
|
|
@@ -1072,30 +1427,42 @@ function computeHookCoverage(settings, managedHooks) {
|
|
|
1072
1427
|
duplicates: [],
|
|
1073
1428
|
};
|
|
1074
1429
|
|
|
1075
|
-
const hooksByEvent =
|
|
1430
|
+
const hooksByEvent =
|
|
1431
|
+
settings?.hooks && typeof settings.hooks === "object" ? settings.hooks : {};
|
|
1076
1432
|
|
|
1077
1433
|
// 이벤트별 orchestrator 존재 여부를 캐시
|
|
1078
1434
|
const orchestratorByEvent = {};
|
|
1079
1435
|
for (const [event, entries] of Object.entries(hooksByEvent)) {
|
|
1080
|
-
orchestratorByEvent[event] =
|
|
1081
|
-
Array.isArray(
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1436
|
+
orchestratorByEvent[event] =
|
|
1437
|
+
Array.isArray(entries) &&
|
|
1438
|
+
entries.some(
|
|
1439
|
+
(entry) =>
|
|
1440
|
+
Array.isArray(entry?.hooks) &&
|
|
1441
|
+
entry.hooks.some(
|
|
1442
|
+
(hook) =>
|
|
1443
|
+
typeof hook?.command === "string" &&
|
|
1444
|
+
hook.command.includes("hook-orchestrator"),
|
|
1445
|
+
),
|
|
1446
|
+
);
|
|
1086
1447
|
}
|
|
1087
1448
|
|
|
1088
1449
|
for (const spec of managedHooks) {
|
|
1089
|
-
const eventEntries = Array.isArray(hooksByEvent[spec.event])
|
|
1450
|
+
const eventEntries = Array.isArray(hooksByEvent[spec.event])
|
|
1451
|
+
? hooksByEvent[spec.event]
|
|
1452
|
+
: [];
|
|
1090
1453
|
|
|
1091
1454
|
// orchestrator가 있으면 registry 훅을 체이닝하므로 "registered"로 간주
|
|
1092
1455
|
if (orchestratorByEvent[spec.event]) {
|
|
1093
1456
|
coverage.registered++;
|
|
1094
1457
|
|
|
1095
1458
|
// 동시에 개별 훅도 직접 등록되어 있으면 → 이중 실행 (duplicate)
|
|
1096
|
-
const directlyRegistered = eventEntries.some(
|
|
1097
|
-
|
|
1098
|
-
|
|
1459
|
+
const directlyRegistered = eventEntries.some(
|
|
1460
|
+
(entry) =>
|
|
1461
|
+
Array.isArray(entry?.hooks) &&
|
|
1462
|
+
entry.hooks.some(
|
|
1463
|
+
(hook) =>
|
|
1464
|
+
extractManagedHookFilename(hook?.command) === spec.fileName,
|
|
1465
|
+
),
|
|
1099
1466
|
);
|
|
1100
1467
|
if (directlyRegistered) {
|
|
1101
1468
|
coverage.duplicates.push(toHookCoverageName(spec.fileName, spec.id));
|
|
@@ -1104,9 +1471,12 @@ function computeHookCoverage(settings, managedHooks) {
|
|
|
1104
1471
|
}
|
|
1105
1472
|
|
|
1106
1473
|
// orchestrator 없으면 기존 방식: 개별 훅 직접 등록 확인
|
|
1107
|
-
const found = eventEntries.some(
|
|
1108
|
-
|
|
1109
|
-
|
|
1474
|
+
const found = eventEntries.some(
|
|
1475
|
+
(entry) =>
|
|
1476
|
+
Array.isArray(entry?.hooks) &&
|
|
1477
|
+
entry.hooks.some(
|
|
1478
|
+
(hook) => extractManagedHookFilename(hook?.command) === spec.fileName,
|
|
1479
|
+
),
|
|
1110
1480
|
);
|
|
1111
1481
|
if (found) {
|
|
1112
1482
|
coverage.registered++;
|
|
@@ -1121,13 +1491,17 @@ function computeHookCoverage(settings, managedHooks) {
|
|
|
1121
1491
|
function formatPathForDisplay(filePath) {
|
|
1122
1492
|
const value = String(filePath || "").replace(/\\/g, "/");
|
|
1123
1493
|
const homePath = homedir().replace(/\\/g, "/");
|
|
1124
|
-
return value.startsWith(homePath)
|
|
1494
|
+
return value.startsWith(homePath)
|
|
1495
|
+
? `~${value.slice(homePath.length)}`
|
|
1496
|
+
: value;
|
|
1125
1497
|
}
|
|
1126
1498
|
|
|
1127
1499
|
function renderTable(headers, rows) {
|
|
1128
1500
|
if (!rows.length) return;
|
|
1129
1501
|
const widths = headers.map((header, index) => {
|
|
1130
|
-
const cellWidths = rows.map(
|
|
1502
|
+
const cellWidths = rows.map(
|
|
1503
|
+
(row) => stripAnsi(String(row[index] ?? "")).length,
|
|
1504
|
+
);
|
|
1131
1505
|
return Math.max(stripAnsi(header).length, ...cellWidths);
|
|
1132
1506
|
});
|
|
1133
1507
|
|
|
@@ -1135,7 +1509,8 @@ function renderTable(headers, rows) {
|
|
|
1135
1509
|
const text = String(cell ?? "");
|
|
1136
1510
|
return text + " ".repeat(Math.max(0, width - stripAnsi(text).length));
|
|
1137
1511
|
};
|
|
1138
|
-
const formatRow = (row) =>
|
|
1512
|
+
const formatRow = (row) =>
|
|
1513
|
+
row.map((cell, index) => padCell(cell, widths[index])).join(" ");
|
|
1139
1514
|
console.log(` ${formatRow(headers)}`);
|
|
1140
1515
|
console.log(` ${widths.map((width) => "─".repeat(width)).join(" ")}`);
|
|
1141
1516
|
for (const row of rows) {
|
|
@@ -1175,12 +1550,15 @@ function inspectSerenaMcpConfig(configContent) {
|
|
|
1175
1550
|
};
|
|
1176
1551
|
}
|
|
1177
1552
|
|
|
1178
|
-
const hasProjectBinding =
|
|
1179
|
-
|
|
1180
|
-
|
|
1553
|
+
const hasProjectBinding =
|
|
1554
|
+
section.includes("--project-from-cwd") ||
|
|
1555
|
+
/--project(?:\s|=|")/.test(section);
|
|
1556
|
+
const hasContextCodex =
|
|
1557
|
+
/--context(?:\s|",\s*")?codex/i.test(section) || /"codex"/i.test(section);
|
|
1181
1558
|
const timeoutMatch = section.match(/startup_timeout_sec\s*=\s*([0-9.]+)/i);
|
|
1182
1559
|
const startupTimeoutSec = timeoutMatch ? Number(timeoutMatch[1]) : null;
|
|
1183
|
-
const timeoutRecommended =
|
|
1560
|
+
const timeoutRecommended =
|
|
1561
|
+
startupTimeoutSec !== null && startupTimeoutSec >= 30;
|
|
1184
1562
|
|
|
1185
1563
|
return {
|
|
1186
1564
|
present: true,
|
|
@@ -1220,10 +1598,17 @@ function buildMcpStatusRows(statusInfo) {
|
|
|
1220
1598
|
if (row.status === "present") detail = row.actualUrl || row.expectedUrl;
|
|
1221
1599
|
else if (row.status === "missing") detail = "registry only";
|
|
1222
1600
|
else if (row.status === "missing-file") detail = "config missing";
|
|
1223
|
-
else if (row.status === "mismatch")
|
|
1601
|
+
else if (row.status === "mismatch")
|
|
1602
|
+
detail = `expected ${row.expectedUrl}`;
|
|
1224
1603
|
else if (row.status === "invalid-config") detail = "parse error";
|
|
1225
1604
|
else if (row.status === "stdio") detail = "configured as stdio";
|
|
1226
|
-
return [
|
|
1605
|
+
return [
|
|
1606
|
+
row.name,
|
|
1607
|
+
row.label,
|
|
1608
|
+
statusBadge(row.status),
|
|
1609
|
+
formatPathForDisplay(row.filePath),
|
|
1610
|
+
detail,
|
|
1611
|
+
];
|
|
1227
1612
|
});
|
|
1228
1613
|
|
|
1229
1614
|
const stdioRows = statusInfo.rows
|
|
@@ -1246,11 +1631,14 @@ function ensureValidRegistryState() {
|
|
|
1246
1631
|
registryState = inspectRegistry();
|
|
1247
1632
|
}
|
|
1248
1633
|
if (!registryState.valid) {
|
|
1249
|
-
throw createCliError(
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1634
|
+
throw createCliError(
|
|
1635
|
+
`MCP registry invalid: ${registryState.errors.join("; ")}`,
|
|
1636
|
+
{
|
|
1637
|
+
exitCode: EXIT_CONFIG_ERROR,
|
|
1638
|
+
reason: "configError",
|
|
1639
|
+
fix: `${registryState.path}의 JSON 구조를 수정하세요.`,
|
|
1640
|
+
},
|
|
1641
|
+
);
|
|
1254
1642
|
}
|
|
1255
1643
|
return registryState;
|
|
1256
1644
|
}
|
|
@@ -1267,8 +1655,14 @@ async function cmdDoctor(options = {}) {
|
|
|
1267
1655
|
};
|
|
1268
1656
|
|
|
1269
1657
|
return await withConsoleSilenced(json, async () => {
|
|
1270
|
-
const modeLabel = reset
|
|
1271
|
-
|
|
1658
|
+
const modeLabel = reset
|
|
1659
|
+
? ` ${RED}--reset${RESET}`
|
|
1660
|
+
: fix
|
|
1661
|
+
? ` ${YELLOW}--fix${RESET}`
|
|
1662
|
+
: "";
|
|
1663
|
+
console.log(
|
|
1664
|
+
`\n ${AMBER}${BOLD}⬡ triflux doctor${RESET} ${VER}${modeLabel}\n`,
|
|
1665
|
+
);
|
|
1272
1666
|
console.log(` ${LINE}`);
|
|
1273
1667
|
|
|
1274
1668
|
// ── reset 모드: 캐시 전체 초기화 ──
|
|
@@ -1298,7 +1692,12 @@ async function cmdDoctor(options = {}) {
|
|
|
1298
1692
|
report.actions.push({ type: "delete", path: fp, status: "ok" });
|
|
1299
1693
|
ok(`삭제됨: ${name}`);
|
|
1300
1694
|
} catch (e) {
|
|
1301
|
-
report.actions.push({
|
|
1695
|
+
report.actions.push({
|
|
1696
|
+
type: "delete",
|
|
1697
|
+
path: fp,
|
|
1698
|
+
status: "failed",
|
|
1699
|
+
message: e.message,
|
|
1700
|
+
});
|
|
1302
1701
|
fail(`삭제 실패: ${name} — ${e.message}`);
|
|
1303
1702
|
}
|
|
1304
1703
|
}
|
|
@@ -1314,38 +1713,86 @@ async function cmdDoctor(options = {}) {
|
|
|
1314
1713
|
const mcpCheck = join(PKG_ROOT, "scripts", "mcp-check.mjs");
|
|
1315
1714
|
if (existsSync(mcpCheck)) {
|
|
1316
1715
|
try {
|
|
1317
|
-
execFileSync(process.execPath, [mcpCheck], {
|
|
1318
|
-
|
|
1716
|
+
execFileSync(process.execPath, [mcpCheck], {
|
|
1717
|
+
timeout: 15000,
|
|
1718
|
+
stdio: "ignore",
|
|
1719
|
+
windowsHide: true,
|
|
1720
|
+
});
|
|
1721
|
+
report.actions.push({
|
|
1722
|
+
type: "rebuild",
|
|
1723
|
+
name: "mcp-inventory",
|
|
1724
|
+
status: "ok",
|
|
1725
|
+
});
|
|
1319
1726
|
ok("MCP 인벤토리 재생성됨");
|
|
1320
1727
|
} catch {
|
|
1321
|
-
report.actions.push({
|
|
1728
|
+
report.actions.push({
|
|
1729
|
+
type: "rebuild",
|
|
1730
|
+
name: "mcp-inventory",
|
|
1731
|
+
status: "failed",
|
|
1732
|
+
});
|
|
1322
1733
|
warn("MCP 인벤토리 재생성 실패 — 다음 세션에서 자동 재시도");
|
|
1323
1734
|
}
|
|
1324
1735
|
}
|
|
1325
1736
|
const hudScript = join(CLAUDE_DIR, "hud", "hud-qos-status.mjs");
|
|
1326
1737
|
if (existsSync(hudScript)) {
|
|
1327
1738
|
try {
|
|
1328
|
-
execFileSync(
|
|
1329
|
-
|
|
1739
|
+
execFileSync(
|
|
1740
|
+
process.execPath,
|
|
1741
|
+
[hudScript, "--refresh-claude-usage"],
|
|
1742
|
+
{ timeout: 20000, stdio: "ignore", windowsHide: true },
|
|
1743
|
+
);
|
|
1744
|
+
report.actions.push({
|
|
1745
|
+
type: "rebuild",
|
|
1746
|
+
name: "claude-usage-cache",
|
|
1747
|
+
status: "ok",
|
|
1748
|
+
});
|
|
1330
1749
|
ok("Claude 사용량 캐시 재생성됨");
|
|
1331
1750
|
} catch {
|
|
1332
|
-
report.actions.push({
|
|
1751
|
+
report.actions.push({
|
|
1752
|
+
type: "rebuild",
|
|
1753
|
+
name: "claude-usage-cache",
|
|
1754
|
+
status: "failed",
|
|
1755
|
+
});
|
|
1333
1756
|
warn("Claude 사용량 캐시 재생성 실패 — 다음 API 호출 시 자동 생성");
|
|
1334
1757
|
}
|
|
1335
1758
|
try {
|
|
1336
|
-
execFileSync(
|
|
1337
|
-
|
|
1759
|
+
execFileSync(
|
|
1760
|
+
process.execPath,
|
|
1761
|
+
[hudScript, "--refresh-codex-rate-limits"],
|
|
1762
|
+
{ timeout: 15000, stdio: "ignore", windowsHide: true },
|
|
1763
|
+
);
|
|
1764
|
+
report.actions.push({
|
|
1765
|
+
type: "rebuild",
|
|
1766
|
+
name: "codex-rate-limits-cache",
|
|
1767
|
+
status: "ok",
|
|
1768
|
+
});
|
|
1338
1769
|
ok("Codex 레이트 리밋 캐시 재생성됨");
|
|
1339
1770
|
} catch {
|
|
1340
|
-
report.actions.push({
|
|
1771
|
+
report.actions.push({
|
|
1772
|
+
type: "rebuild",
|
|
1773
|
+
name: "codex-rate-limits-cache",
|
|
1774
|
+
status: "failed",
|
|
1775
|
+
});
|
|
1341
1776
|
warn("Codex 레이트 리밋 캐시 재생성 실패");
|
|
1342
1777
|
}
|
|
1343
1778
|
try {
|
|
1344
|
-
execFileSync(
|
|
1345
|
-
|
|
1779
|
+
execFileSync(
|
|
1780
|
+
process.execPath,
|
|
1781
|
+
[hudScript, "--refresh-gemini-quota"],
|
|
1782
|
+
{ timeout: 15000, stdio: "ignore", windowsHide: true },
|
|
1783
|
+
);
|
|
1784
|
+
report.actions.push({
|
|
1785
|
+
type: "rebuild",
|
|
1786
|
+
name: "gemini-quota-cache",
|
|
1787
|
+
status: "ok",
|
|
1788
|
+
});
|
|
1346
1789
|
ok("Gemini 쿼터 캐시 재생성됨");
|
|
1347
1790
|
} catch {
|
|
1348
|
-
report.actions.push({
|
|
1791
|
+
report.actions.push({
|
|
1792
|
+
type: "rebuild",
|
|
1793
|
+
name: "gemini-quota-cache",
|
|
1794
|
+
status: "failed",
|
|
1795
|
+
});
|
|
1349
1796
|
warn("Gemini 쿼터 캐시 재생성 실패");
|
|
1350
1797
|
}
|
|
1351
1798
|
}
|
|
@@ -1353,114 +1800,178 @@ async function cmdDoctor(options = {}) {
|
|
|
1353
1800
|
const { buildAll } = await import("../scripts/cache-warmup.mjs");
|
|
1354
1801
|
const warmupSummary = buildAll({ cwd: process.cwd(), force: true });
|
|
1355
1802
|
if (warmupSummary.ok) {
|
|
1356
|
-
report.actions.push({
|
|
1803
|
+
report.actions.push({
|
|
1804
|
+
type: "rebuild",
|
|
1805
|
+
name: "warmup-caches",
|
|
1806
|
+
status: "ok",
|
|
1807
|
+
built: warmupSummary.built,
|
|
1808
|
+
});
|
|
1357
1809
|
ok("Phase 1 웜업 캐시 재생성됨");
|
|
1358
1810
|
} else {
|
|
1359
|
-
report.actions.push({
|
|
1811
|
+
report.actions.push({
|
|
1812
|
+
type: "rebuild",
|
|
1813
|
+
name: "warmup-caches",
|
|
1814
|
+
status: "failed",
|
|
1815
|
+
});
|
|
1360
1816
|
warn("Phase 1 웜업 캐시 재생성 실패");
|
|
1361
1817
|
}
|
|
1362
1818
|
} catch {
|
|
1363
|
-
report.actions.push({
|
|
1819
|
+
report.actions.push({
|
|
1820
|
+
type: "rebuild",
|
|
1821
|
+
name: "warmup-caches",
|
|
1822
|
+
status: "failed",
|
|
1823
|
+
});
|
|
1364
1824
|
warn("Phase 1 웜업 캐시 재생성 실패");
|
|
1365
1825
|
}
|
|
1366
1826
|
console.log(`\n ${LINE}`);
|
|
1367
|
-
console.log(
|
|
1368
|
-
|
|
1369
|
-
|
|
1827
|
+
console.log(
|
|
1828
|
+
` ${GREEN_BRIGHT}${BOLD}✓ 캐시 초기화 + 재생성 완료${RESET}\n`,
|
|
1829
|
+
);
|
|
1830
|
+
report.status = report.actions.some(
|
|
1831
|
+
(action) => action.status === "failed",
|
|
1832
|
+
)
|
|
1833
|
+
? "issues"
|
|
1834
|
+
: "ok";
|
|
1835
|
+
report.issue_count = report.actions.filter(
|
|
1836
|
+
(action) => action.status === "failed",
|
|
1837
|
+
).length;
|
|
1370
1838
|
if (json) printJson(report);
|
|
1371
1839
|
return report;
|
|
1372
1840
|
}
|
|
1373
1841
|
|
|
1374
1842
|
// ── fix 모드: 파일 동기화 + 캐시 정리 후 진단 ──
|
|
1375
1843
|
if (fix) {
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
} catch { try { unlinkSync(fp); cleaned++; ok(`손상된 캐시 정리: ${name}`); } catch {} }
|
|
1422
|
-
}
|
|
1423
|
-
if (cleaned === 0) info("에러 캐시 없음");
|
|
1424
|
-
try {
|
|
1425
|
-
const { fixCaches } = await import("../scripts/cache-doctor.mjs");
|
|
1426
|
-
const cacheRepair = await fixCaches({ cwd: process.cwd() });
|
|
1427
|
-
if (cacheRepair.fixed.length > 0 && cacheRepair.ok) {
|
|
1428
|
-
ok(`웜업 캐시 자동 복구: ${cacheRepair.fixed.join(", ")}`);
|
|
1429
|
-
} else if (cacheRepair.fixed.length > 0) {
|
|
1430
|
-
warn(`웜업 캐시 자동 복구 실패: ${cacheRepair.fixed.join(", ")}`);
|
|
1844
|
+
section("Auto Fix");
|
|
1845
|
+
for (const target of SYNC_MAP) {
|
|
1846
|
+
syncFile(target.src, target.dst, target.label);
|
|
1847
|
+
}
|
|
1848
|
+
{
|
|
1849
|
+
const claudeGuide = ensureGlobalClaudeRoutingSection(CLAUDE_DIR);
|
|
1850
|
+
if (claudeGuide.skipped)
|
|
1851
|
+
warn(`CLAUDE.md 라우팅 섹션 확인 실패: ${claudeGuide.reason}`);
|
|
1852
|
+
else if (
|
|
1853
|
+
claudeGuide.action === "created" ||
|
|
1854
|
+
claudeGuide.action === "updated"
|
|
1855
|
+
)
|
|
1856
|
+
ok("CLAUDE.md: 전역 triflux 라우팅 요약 갱신");
|
|
1857
|
+
}
|
|
1858
|
+
// 스킬 동기화
|
|
1859
|
+
const fSkillsSrc = join(PKG_ROOT, "skills");
|
|
1860
|
+
const fSkillsDst = join(CLAUDE_DIR, "skills");
|
|
1861
|
+
if (existsSync(fSkillsSrc)) {
|
|
1862
|
+
let sc = 0,
|
|
1863
|
+
st = 0;
|
|
1864
|
+
for (const name of readdirSync(fSkillsSrc)) {
|
|
1865
|
+
const src = join(fSkillsSrc, name, "SKILL.md");
|
|
1866
|
+
const dst = join(fSkillsDst, name, "SKILL.md");
|
|
1867
|
+
if (!existsSync(src)) continue;
|
|
1868
|
+
st++;
|
|
1869
|
+
const dstDir = dirname(dst);
|
|
1870
|
+
if (!existsSync(dstDir)) mkdirSync(dstDir, { recursive: true });
|
|
1871
|
+
if (!existsSync(dst)) {
|
|
1872
|
+
copyFileSync(src, dst);
|
|
1873
|
+
sc++;
|
|
1874
|
+
} else if (readFileSync(src, "utf8") !== readFileSync(dst, "utf8")) {
|
|
1875
|
+
copyFileSync(src, dst);
|
|
1876
|
+
sc++;
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
if (sc > 0) ok(`스킬: ${sc}/${st}개 업데이트됨`);
|
|
1880
|
+
else ok(`스킬: ${st}개 최신 상태`);
|
|
1881
|
+
}
|
|
1882
|
+
const profileFix = ensureCodexProfiles();
|
|
1883
|
+
if (!profileFix.ok) {
|
|
1884
|
+
warn(
|
|
1885
|
+
`Codex Profiles 자동 복구 실패: ${renderErrorMessage(profileFix.message)}`,
|
|
1886
|
+
);
|
|
1887
|
+
} else if (profileFix.changed > 0) {
|
|
1888
|
+
ok(`Codex Profiles: ${profileFix.changed}개 반영됨`);
|
|
1431
1889
|
} else {
|
|
1432
|
-
info("
|
|
1890
|
+
info("Codex Profiles: 이미 최신 상태");
|
|
1433
1891
|
}
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1892
|
+
// 에러/스테일 캐시 정리
|
|
1893
|
+
const fCacheDir = join(CLAUDE_DIR, "cache");
|
|
1894
|
+
const staleNames = [
|
|
1895
|
+
"claude-usage-cache.json",
|
|
1896
|
+
".claude-refresh-lock",
|
|
1897
|
+
"codex-rate-limits-cache.json",
|
|
1898
|
+
];
|
|
1899
|
+
let cleaned = 0;
|
|
1900
|
+
for (const name of staleNames) {
|
|
1901
|
+
const fp = join(fCacheDir, name);
|
|
1902
|
+
if (!existsSync(fp)) continue;
|
|
1903
|
+
try {
|
|
1904
|
+
const parsed = JSON.parse(readFileSync(fp, "utf8"));
|
|
1905
|
+
if (parsed.error || name.startsWith(".")) {
|
|
1906
|
+
unlinkSync(fp);
|
|
1907
|
+
cleaned++;
|
|
1908
|
+
ok(`에러 캐시 정리: ${name}`);
|
|
1909
|
+
}
|
|
1910
|
+
} catch {
|
|
1911
|
+
try {
|
|
1912
|
+
unlinkSync(fp);
|
|
1913
|
+
cleaned++;
|
|
1914
|
+
ok(`손상된 캐시 정리: ${name}`);
|
|
1915
|
+
} catch {}
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
if (cleaned === 0) info("에러 캐시 없음");
|
|
1439
1919
|
try {
|
|
1440
|
-
const
|
|
1441
|
-
const
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
if (
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1920
|
+
const { fixCaches } = await import("../scripts/cache-doctor.mjs");
|
|
1921
|
+
const cacheRepair = await fixCaches({ cwd: process.cwd() });
|
|
1922
|
+
if (cacheRepair.fixed.length > 0 && cacheRepair.ok) {
|
|
1923
|
+
ok(`웜업 캐시 자동 복구: ${cacheRepair.fixed.join(", ")}`);
|
|
1924
|
+
} else if (cacheRepair.fixed.length > 0) {
|
|
1925
|
+
warn(`웜업 캐시 자동 복구 실패: ${cacheRepair.fixed.join(", ")}`);
|
|
1926
|
+
} else {
|
|
1927
|
+
info("웜업 캐시: 이미 정상 상태");
|
|
1928
|
+
}
|
|
1929
|
+
} catch {
|
|
1930
|
+
warn("웜업 캐시 자동 복구 실패");
|
|
1931
|
+
}
|
|
1932
|
+
const registryStateForFix = inspectRegistry();
|
|
1933
|
+
if (registryStateForFix.valid) {
|
|
1934
|
+
try {
|
|
1935
|
+
const mcpSync = syncRegistryTargets({
|
|
1936
|
+
registry: registryStateForFix.registry,
|
|
1937
|
+
});
|
|
1938
|
+
const updatedCount = mcpSync.actions.filter(
|
|
1939
|
+
(action) => action.status === "updated",
|
|
1940
|
+
).length;
|
|
1941
|
+
const invalidCount = mcpSync.actions.filter(
|
|
1942
|
+
(action) => action.status === "invalid-config",
|
|
1943
|
+
).length;
|
|
1944
|
+
report.actions.push({
|
|
1945
|
+
type: "mcp-sync",
|
|
1946
|
+
status: invalidCount > 0 ? "issues" : "ok",
|
|
1947
|
+
actions: mcpSync.actions,
|
|
1948
|
+
});
|
|
1949
|
+
if (updatedCount > 0)
|
|
1950
|
+
ok(`MCP registry 동기화: ${updatedCount}개 설정 반영됨`);
|
|
1951
|
+
else info("MCP registry: 이미 최신 상태");
|
|
1952
|
+
if (invalidCount > 0)
|
|
1953
|
+
warn(`MCP registry 동기화 건너뜀: parse error ${invalidCount}개`);
|
|
1954
|
+
} catch (error) {
|
|
1955
|
+
report.actions.push({
|
|
1956
|
+
type: "mcp-sync",
|
|
1957
|
+
status: "failed",
|
|
1958
|
+
message: error.message,
|
|
1959
|
+
});
|
|
1960
|
+
warn(`MCP registry 자동 동기화 실패: ${error.message}`);
|
|
1961
|
+
}
|
|
1962
|
+
} else if (registryStateForFix.exists) {
|
|
1963
|
+
saveRegistry(createDefaultRegistry());
|
|
1964
|
+
report.actions.push({ type: "mcp-registry-reset", status: "ok" });
|
|
1965
|
+
ok("MCP registry 손상 → 기본값으로 재생성됨");
|
|
1966
|
+
} else {
|
|
1967
|
+
saveRegistry(createDefaultRegistry());
|
|
1968
|
+
report.actions.push({ type: "mcp-registry-create", status: "ok" });
|
|
1969
|
+
ok("MCP registry 없음 → 기본값으로 자동 생성됨");
|
|
1970
|
+
}
|
|
1971
|
+
console.log(`\n ${LINE}`);
|
|
1972
|
+
info("수정 완료 — 아래 진단 결과를 확인하세요");
|
|
1973
|
+
console.log("");
|
|
1459
1974
|
}
|
|
1460
|
-
console.log(`\n ${LINE}`);
|
|
1461
|
-
info("수정 완료 — 아래 진단 결과를 확인하세요");
|
|
1462
|
-
console.log("");
|
|
1463
|
-
}
|
|
1464
1975
|
|
|
1465
1976
|
let issues = 0;
|
|
1466
1977
|
|
|
@@ -1469,10 +1980,20 @@ async function cmdDoctor(options = {}) {
|
|
|
1469
1980
|
const routeSh = join(CLAUDE_DIR, "scripts", "tfx-route.sh");
|
|
1470
1981
|
if (existsSync(routeSh)) {
|
|
1471
1982
|
const ver = getVersion(routeSh);
|
|
1472
|
-
addDoctorCheck(report, {
|
|
1983
|
+
addDoctorCheck(report, {
|
|
1984
|
+
name: "tfx-route.sh",
|
|
1985
|
+
status: "ok",
|
|
1986
|
+
path: routeSh,
|
|
1987
|
+
version: ver,
|
|
1988
|
+
});
|
|
1473
1989
|
ok(`설치됨 ${ver ? `${DIM}v${ver}${RESET}` : ""}`);
|
|
1474
1990
|
} else {
|
|
1475
|
-
addDoctorCheck(report, {
|
|
1991
|
+
addDoctorCheck(report, {
|
|
1992
|
+
name: "tfx-route.sh",
|
|
1993
|
+
status: "missing",
|
|
1994
|
+
path: routeSh,
|
|
1995
|
+
fix: "tfx setup",
|
|
1996
|
+
});
|
|
1476
1997
|
fail("미설치 — tfx setup 실행 필요");
|
|
1477
1998
|
issues++;
|
|
1478
1999
|
}
|
|
@@ -1481,16 +2002,29 @@ async function cmdDoctor(options = {}) {
|
|
|
1481
2002
|
section("HUD");
|
|
1482
2003
|
const hud = join(CLAUDE_DIR, "hud", "hud-qos-status.mjs");
|
|
1483
2004
|
if (existsSync(hud)) {
|
|
1484
|
-
addDoctorCheck(report, {
|
|
2005
|
+
addDoctorCheck(report, {
|
|
2006
|
+
name: "hud-qos-status.mjs",
|
|
2007
|
+
status: "ok",
|
|
2008
|
+
path: hud,
|
|
2009
|
+
});
|
|
1485
2010
|
ok("설치됨");
|
|
1486
2011
|
} else {
|
|
1487
|
-
addDoctorCheck(report, {
|
|
2012
|
+
addDoctorCheck(report, {
|
|
2013
|
+
name: "hud-qos-status.mjs",
|
|
2014
|
+
status: "missing",
|
|
2015
|
+
path: hud,
|
|
2016
|
+
optional: true,
|
|
2017
|
+
fix: "tfx setup",
|
|
2018
|
+
});
|
|
1488
2019
|
warn(`미설치 ${GRAY}(선택사항)${RESET}`);
|
|
1489
2020
|
}
|
|
1490
2021
|
|
|
1491
2022
|
// 3. Codex CLI
|
|
1492
2023
|
section(`Codex CLI ${WHITE_BRIGHT}●${RESET}`);
|
|
1493
|
-
const codexCli = checkCliCrossShell(
|
|
2024
|
+
const codexCli = checkCliCrossShell(
|
|
2025
|
+
"codex",
|
|
2026
|
+
"npm install -g @openai/codex",
|
|
2027
|
+
);
|
|
1494
2028
|
issues += codexCli.issues;
|
|
1495
2029
|
addDoctorCheck(report, {
|
|
1496
2030
|
name: "codex",
|
|
@@ -1507,9 +2041,13 @@ async function cmdDoctor(options = {}) {
|
|
|
1507
2041
|
const missingProfiles = [];
|
|
1508
2042
|
for (const profile of REQUIRED_CODEX_PROFILES) {
|
|
1509
2043
|
if (hasProfileSection(codexConfig, profile.name)) {
|
|
1510
|
-
ok(
|
|
2044
|
+
ok(
|
|
2045
|
+
`${profile.name}: 정상${profile.proOnly ? ` ${DIM}(Pro 전용)${RESET}` : ""}`,
|
|
2046
|
+
);
|
|
1511
2047
|
} else if (profile.proOnly) {
|
|
1512
|
-
info(
|
|
2048
|
+
info(
|
|
2049
|
+
`${profile.name}: 미설정 ${DIM}(Pro 전용 — Plus/기본에서는 불필요)${RESET}`,
|
|
2050
|
+
);
|
|
1513
2051
|
} else {
|
|
1514
2052
|
missingProfiles.push(profile.name);
|
|
1515
2053
|
warn(`${profile.name}: 미설정`);
|
|
@@ -1524,7 +2062,12 @@ async function cmdDoctor(options = {}) {
|
|
|
1524
2062
|
...(missingProfiles.length > 0 ? { fix: "tfx setup" } : {}),
|
|
1525
2063
|
});
|
|
1526
2064
|
} else {
|
|
1527
|
-
addDoctorCheck(report, {
|
|
2065
|
+
addDoctorCheck(report, {
|
|
2066
|
+
name: "codex-profiles",
|
|
2067
|
+
status: "missing",
|
|
2068
|
+
path: CODEX_CONFIG_PATH,
|
|
2069
|
+
fix: "tfx setup",
|
|
2070
|
+
});
|
|
1528
2071
|
warn("config.toml 미존재");
|
|
1529
2072
|
issues++;
|
|
1530
2073
|
}
|
|
@@ -1532,11 +2075,18 @@ async function cmdDoctor(options = {}) {
|
|
|
1532
2075
|
// Codex 구형 모델 감지
|
|
1533
2076
|
if (existsSync(CODEX_CONFIG_PATH)) {
|
|
1534
2077
|
const codexContent = readFileSync(CODEX_CONFIG_PATH, "utf8");
|
|
1535
|
-
const legacyFound = LEGACY_CODEX_MODELS.filter(m =>
|
|
2078
|
+
const legacyFound = LEGACY_CODEX_MODELS.filter((m) =>
|
|
2079
|
+
codexContent.includes(`"${m}"`),
|
|
2080
|
+
);
|
|
1536
2081
|
if (legacyFound.length > 0) {
|
|
1537
2082
|
warn(`구형 모델 감지: ${legacyFound.join(", ")}`);
|
|
1538
2083
|
info("최신 프로필로 마이그레이션: tfx setup 또는 tfx profile");
|
|
1539
|
-
addDoctorCheck(report, {
|
|
2084
|
+
addDoctorCheck(report, {
|
|
2085
|
+
name: "codex-legacy-models",
|
|
2086
|
+
status: "issues",
|
|
2087
|
+
models: legacyFound,
|
|
2088
|
+
fix: "tfx setup",
|
|
2089
|
+
});
|
|
1540
2090
|
issues++;
|
|
1541
2091
|
}
|
|
1542
2092
|
}
|
|
@@ -1548,7 +2098,9 @@ async function cmdDoctor(options = {}) {
|
|
|
1548
2098
|
const serenaConfig = inspectSerenaMcpConfig(codexConfig);
|
|
1549
2099
|
if (!serenaConfig.present) {
|
|
1550
2100
|
warn("serena MCP 설정 없음");
|
|
1551
|
-
info(
|
|
2101
|
+
info(
|
|
2102
|
+
"권장: [mcp_servers.serena]에 --project-from-cwd, --context codex, startup_timeout_sec=30+ 설정",
|
|
2103
|
+
);
|
|
1552
2104
|
addDoctorCheck(report, {
|
|
1553
2105
|
name: "serena-mcp",
|
|
1554
2106
|
status: "missing",
|
|
@@ -1557,7 +2109,8 @@ async function cmdDoctor(options = {}) {
|
|
|
1557
2109
|
});
|
|
1558
2110
|
issues++;
|
|
1559
2111
|
} else {
|
|
1560
|
-
const hasSerenaIssues =
|
|
2112
|
+
const hasSerenaIssues =
|
|
2113
|
+
!serenaConfig.hasProjectBinding || !serenaConfig.timeoutRecommended;
|
|
1561
2114
|
|
|
1562
2115
|
if (serenaConfig.hasProjectBinding) ok("project binding: 정상");
|
|
1563
2116
|
else {
|
|
@@ -1589,7 +2142,9 @@ async function cmdDoctor(options = {}) {
|
|
|
1589
2142
|
context_codex: serenaConfig.hasContextCodex,
|
|
1590
2143
|
startup_timeout_sec: serenaConfig.startupTimeoutSec,
|
|
1591
2144
|
...(hasSerenaIssues
|
|
1592
|
-
? {
|
|
2145
|
+
? {
|
|
2146
|
+
fix: "Serena MCP에 --project-from-cwd 와 startup_timeout_sec=30+ 를 설정하세요.",
|
|
2147
|
+
}
|
|
1593
2148
|
: {}),
|
|
1594
2149
|
});
|
|
1595
2150
|
}
|
|
@@ -1606,7 +2161,10 @@ async function cmdDoctor(options = {}) {
|
|
|
1606
2161
|
|
|
1607
2162
|
// 5. Gemini CLI
|
|
1608
2163
|
section(`Gemini CLI ${BLUE}●${RESET}`);
|
|
1609
|
-
const geminiCli = checkCliCrossShell(
|
|
2164
|
+
const geminiCli = checkCliCrossShell(
|
|
2165
|
+
"gemini",
|
|
2166
|
+
"npm install -g @google/gemini-cli",
|
|
2167
|
+
);
|
|
1610
2168
|
issues += geminiCli.issues;
|
|
1611
2169
|
addDoctorCheck(report, {
|
|
1612
2170
|
name: "gemini",
|
|
@@ -1617,16 +2175,32 @@ async function cmdDoctor(options = {}) {
|
|
|
1617
2175
|
// API 키 검사 제거 — bash exec 기반이므로 API 키 불필요
|
|
1618
2176
|
|
|
1619
2177
|
// Gemini 구형 모델 감지
|
|
1620
|
-
const geminiProfilesPath = join(
|
|
1621
|
-
|
|
2178
|
+
const geminiProfilesPath = join(
|
|
2179
|
+
homedir(),
|
|
2180
|
+
".gemini",
|
|
2181
|
+
"triflux-profiles.json",
|
|
2182
|
+
);
|
|
2183
|
+
const LEGACY_GEMINI_MODELS = [
|
|
2184
|
+
"gemini-2.0-flash",
|
|
2185
|
+
"gemini-1.5-pro",
|
|
2186
|
+
"gemini-1.5-flash",
|
|
2187
|
+
"gemini-2.5-pro-preview",
|
|
2188
|
+
];
|
|
1622
2189
|
if (existsSync(geminiProfilesPath)) {
|
|
1623
2190
|
try {
|
|
1624
2191
|
const geminiContent = readFileSync(geminiProfilesPath, "utf8");
|
|
1625
|
-
const geminiLegacy = LEGACY_GEMINI_MODELS.filter(m =>
|
|
2192
|
+
const geminiLegacy = LEGACY_GEMINI_MODELS.filter((m) =>
|
|
2193
|
+
geminiContent.includes(m),
|
|
2194
|
+
);
|
|
1626
2195
|
if (geminiLegacy.length > 0) {
|
|
1627
2196
|
warn(`구형 모델 감지: ${geminiLegacy.join(", ")}`);
|
|
1628
2197
|
info("최신 프로필로 마이그레이션: tfx setup 또는 tfx profile");
|
|
1629
|
-
addDoctorCheck(report, {
|
|
2198
|
+
addDoctorCheck(report, {
|
|
2199
|
+
name: "gemini-legacy-models",
|
|
2200
|
+
status: "issues",
|
|
2201
|
+
models: geminiLegacy,
|
|
2202
|
+
fix: "tfx setup",
|
|
2203
|
+
});
|
|
1630
2204
|
issues++;
|
|
1631
2205
|
}
|
|
1632
2206
|
} catch {}
|
|
@@ -1636,10 +2210,18 @@ async function cmdDoctor(options = {}) {
|
|
|
1636
2210
|
section(`Claude Code ${AMBER}●${RESET}`);
|
|
1637
2211
|
const claudePath = which("claude");
|
|
1638
2212
|
if (claudePath) {
|
|
1639
|
-
addDoctorCheck(report, {
|
|
2213
|
+
addDoctorCheck(report, {
|
|
2214
|
+
name: "claude",
|
|
2215
|
+
status: "ok",
|
|
2216
|
+
path: claudePath,
|
|
2217
|
+
});
|
|
1640
2218
|
ok("설치됨");
|
|
1641
2219
|
} else {
|
|
1642
|
-
addDoctorCheck(report, {
|
|
2220
|
+
addDoctorCheck(report, {
|
|
2221
|
+
name: "claude",
|
|
2222
|
+
status: "missing",
|
|
2223
|
+
fix: "Claude Code를 설치한 뒤 `tfx doctor`를 다시 실행하세요.",
|
|
2224
|
+
});
|
|
1643
2225
|
fail("미설치 (필수)");
|
|
1644
2226
|
issues++;
|
|
1645
2227
|
}
|
|
@@ -1650,7 +2232,9 @@ async function cmdDoctor(options = {}) {
|
|
|
1650
2232
|
const psmuxPath = which("psmux");
|
|
1651
2233
|
if (psmuxPath) {
|
|
1652
2234
|
ok("설치됨");
|
|
1653
|
-
const psmuxSupport = probePsmuxSupport({
|
|
2235
|
+
const psmuxSupport = probePsmuxSupport({
|
|
2236
|
+
execFileSyncFn: execFileSync,
|
|
2237
|
+
});
|
|
1654
2238
|
const supportOk = psmuxSupport.ok;
|
|
1655
2239
|
info(`버전: ${psmuxSupport.version || "unknown"}`);
|
|
1656
2240
|
if (!supportOk) {
|
|
@@ -1666,17 +2250,24 @@ async function cmdDoctor(options = {}) {
|
|
|
1666
2250
|
});
|
|
1667
2251
|
issues++;
|
|
1668
2252
|
} else if (!psmuxSupport.recommended) {
|
|
1669
|
-
warn(
|
|
2253
|
+
warn(
|
|
2254
|
+
`권장 버전 미만: v${psmuxSupport.version || "unknown"} (권장: v${psmuxSupport.recommendedVersion}+)`,
|
|
2255
|
+
);
|
|
1670
2256
|
info(`업데이트 권장:\n${formatPsmuxUpdateGuidance(" ")}`);
|
|
1671
2257
|
}
|
|
1672
2258
|
if (psmuxSupport.missingOptionalCommands?.length > 0) {
|
|
1673
|
-
info(
|
|
2259
|
+
info(
|
|
2260
|
+
`선택 capability 미지원: ${psmuxSupport.missingOptionalCommands.join(", ")} (detach-first hardening 경로에서만 사용)`,
|
|
2261
|
+
);
|
|
1674
2262
|
}
|
|
1675
2263
|
|
|
1676
2264
|
// 기본 셸 확인: psmux 세션의 기본 셸이 PowerShell인지 cmd.exe인지
|
|
1677
2265
|
let shellOk = false;
|
|
1678
2266
|
try {
|
|
1679
|
-
const defaultShell = execSync(
|
|
2267
|
+
const defaultShell = execSync(
|
|
2268
|
+
"psmux show-options -g default-shell 2>NUL",
|
|
2269
|
+
{ encoding: "utf8", timeout: 3000 },
|
|
2270
|
+
).trim();
|
|
1680
2271
|
shellOk = /powershell|pwsh/i.test(defaultShell);
|
|
1681
2272
|
} catch {
|
|
1682
2273
|
// show-options 실패 시 pwsh/powershell 존재 여부로 판단
|
|
@@ -1684,65 +2275,116 @@ async function cmdDoctor(options = {}) {
|
|
|
1684
2275
|
}
|
|
1685
2276
|
if (supportOk && shellOk) {
|
|
1686
2277
|
ok("기본 셸: PowerShell");
|
|
1687
|
-
addDoctorCheck(report, {
|
|
2278
|
+
addDoctorCheck(report, {
|
|
2279
|
+
name: "psmux",
|
|
2280
|
+
status: "ok",
|
|
2281
|
+
path: psmuxPath,
|
|
2282
|
+
shell: "powershell",
|
|
2283
|
+
});
|
|
1688
2284
|
} else {
|
|
1689
2285
|
if (fix) {
|
|
1690
2286
|
// --fix: PowerShell로 자동 변경
|
|
1691
2287
|
const pwshBin = which("pwsh") ? "pwsh" : "powershell.exe";
|
|
1692
2288
|
try {
|
|
1693
|
-
execSync(`psmux set-option -g default-shell "${pwshBin}"`, {
|
|
2289
|
+
execSync(`psmux set-option -g default-shell "${pwshBin}"`, {
|
|
2290
|
+
timeout: 3000,
|
|
2291
|
+
stdio: "pipe",
|
|
2292
|
+
});
|
|
1694
2293
|
ok(`기본 셸 → ${pwshBin} 으로 변경 완료`);
|
|
1695
|
-
addDoctorCheck(report, {
|
|
2294
|
+
addDoctorCheck(report, {
|
|
2295
|
+
name: "psmux",
|
|
2296
|
+
status: "ok",
|
|
2297
|
+
path: psmuxPath,
|
|
2298
|
+
shell: pwshBin,
|
|
2299
|
+
fixed: true,
|
|
2300
|
+
});
|
|
1696
2301
|
report.actions.push("psmux default-shell → " + pwshBin);
|
|
1697
2302
|
} catch (e) {
|
|
1698
2303
|
fail(`기본 셸 변경 실패: ${e.message}`);
|
|
1699
|
-
addDoctorCheck(report, {
|
|
2304
|
+
addDoctorCheck(report, {
|
|
2305
|
+
name: "psmux",
|
|
2306
|
+
status: "issues",
|
|
2307
|
+
path: psmuxPath,
|
|
2308
|
+
shell: "cmd",
|
|
2309
|
+
fix: `psmux set-option -g default-shell "${pwshBin}"`,
|
|
2310
|
+
});
|
|
1700
2311
|
issues++;
|
|
1701
2312
|
}
|
|
1702
2313
|
} else {
|
|
1703
2314
|
warn("기본 셸이 cmd.exe — headless 명령 실패 가능");
|
|
1704
|
-
info(
|
|
1705
|
-
|
|
2315
|
+
info(
|
|
2316
|
+
`수정: tfx doctor --fix 또는 psmux set-option -g default-shell "powershell.exe"`,
|
|
2317
|
+
);
|
|
2318
|
+
addDoctorCheck(report, {
|
|
2319
|
+
name: "psmux",
|
|
2320
|
+
status: "issues",
|
|
2321
|
+
path: psmuxPath,
|
|
2322
|
+
shell: "cmd",
|
|
2323
|
+
fix: "tfx doctor --fix",
|
|
2324
|
+
});
|
|
1706
2325
|
issues++;
|
|
1707
2326
|
}
|
|
1708
2327
|
}
|
|
1709
2328
|
} else {
|
|
1710
2329
|
info(`미설치 ${GRAY}(선택 — 멀티모델 병렬 실행에 필요)${RESET}`);
|
|
1711
2330
|
info(`설치 방법:\n${formatPsmuxInstallGuidance(" ")}`);
|
|
1712
|
-
addDoctorCheck(report, {
|
|
2331
|
+
addDoctorCheck(report, {
|
|
2332
|
+
name: "psmux",
|
|
2333
|
+
status: "skipped",
|
|
2334
|
+
detail: "미설치 (선택)",
|
|
2335
|
+
fix: "winget install marlocarlo.psmux",
|
|
2336
|
+
});
|
|
1713
2337
|
}
|
|
1714
2338
|
}
|
|
1715
2339
|
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
2340
|
+
// 8. 스킬 설치 상태
|
|
2341
|
+
section("Skills");
|
|
2342
|
+
const skillsSrc = join(PKG_ROOT, "skills");
|
|
2343
|
+
const skillsDst = join(CLAUDE_DIR, "skills");
|
|
2344
|
+
if (existsSync(skillsSrc)) {
|
|
2345
|
+
let installed = 0;
|
|
2346
|
+
let total = 0;
|
|
2347
|
+
const missing = [];
|
|
2348
|
+
for (const name of readdirSync(skillsSrc)) {
|
|
2349
|
+
if (!existsSync(join(skillsSrc, name, "SKILL.md"))) continue;
|
|
2350
|
+
total++;
|
|
2351
|
+
if (existsSync(join(skillsDst, name, "SKILL.md"))) {
|
|
2352
|
+
installed++;
|
|
2353
|
+
} else {
|
|
2354
|
+
missing.push(name);
|
|
2355
|
+
}
|
|
1731
2356
|
}
|
|
1732
|
-
}
|
|
1733
2357
|
if (installed === total) {
|
|
1734
|
-
addDoctorCheck(report, {
|
|
2358
|
+
addDoctorCheck(report, {
|
|
2359
|
+
name: "skills",
|
|
2360
|
+
status: "ok",
|
|
2361
|
+
installed,
|
|
2362
|
+
total,
|
|
2363
|
+
});
|
|
1735
2364
|
ok(`${installed}/${total}개 설치됨`);
|
|
1736
2365
|
} else {
|
|
1737
|
-
addDoctorCheck(report, {
|
|
2366
|
+
addDoctorCheck(report, {
|
|
2367
|
+
name: "skills",
|
|
2368
|
+
status: "missing",
|
|
2369
|
+
installed,
|
|
2370
|
+
total,
|
|
2371
|
+
missing,
|
|
2372
|
+
fix: "tfx setup",
|
|
2373
|
+
});
|
|
1738
2374
|
warn(`${installed}/${total}개 설치됨 — 미설치: ${missing.join(", ")}`);
|
|
1739
2375
|
info("triflux setup으로 동기화 가능");
|
|
1740
2376
|
issues++;
|
|
1741
2377
|
}
|
|
1742
2378
|
} else {
|
|
1743
|
-
addDoctorCheck(report, {
|
|
1744
|
-
|
|
1745
|
-
|
|
2379
|
+
addDoctorCheck(report, {
|
|
2380
|
+
name: "skills",
|
|
2381
|
+
status: "missing",
|
|
2382
|
+
installed: 0,
|
|
2383
|
+
total: 0,
|
|
2384
|
+
fix: "패키지 skills 디렉토리를 확인하세요.",
|
|
2385
|
+
});
|
|
2386
|
+
}
|
|
2387
|
+
|
|
1746
2388
|
// Stale 스킬 체크
|
|
1747
2389
|
const staleSkills = [];
|
|
1748
2390
|
const userSkillsDir = join(CLAUDE_DIR, "skills");
|
|
@@ -1762,7 +2404,12 @@ async function cmdDoctor(options = {}) {
|
|
|
1762
2404
|
if (staleSkills.length > 0) {
|
|
1763
2405
|
warn(`구형 스킬 ${staleSkills.length}개 감지: ${staleSkills.join(", ")}`);
|
|
1764
2406
|
info("제거: tfx setup 또는 tfx update");
|
|
1765
|
-
addDoctorCheck(report, {
|
|
2407
|
+
addDoctorCheck(report, {
|
|
2408
|
+
name: "stale-skills",
|
|
2409
|
+
status: "issues",
|
|
2410
|
+
skills: staleSkills,
|
|
2411
|
+
fix: "tfx setup",
|
|
2412
|
+
});
|
|
1766
2413
|
issues++;
|
|
1767
2414
|
} else {
|
|
1768
2415
|
addDoctorCheck(report, { name: "stale-skills", status: "ok" });
|
|
@@ -1774,776 +2421,1041 @@ async function cmdDoctor(options = {}) {
|
|
|
1774
2421
|
if (existsSync(pluginsFile)) {
|
|
1775
2422
|
const content = readFileSync(pluginsFile, "utf8");
|
|
1776
2423
|
if (content.includes("triflux")) {
|
|
1777
|
-
addDoctorCheck(report, {
|
|
2424
|
+
addDoctorCheck(report, {
|
|
2425
|
+
name: "plugin",
|
|
2426
|
+
status: "ok",
|
|
2427
|
+
path: pluginsFile,
|
|
2428
|
+
});
|
|
1778
2429
|
ok("triflux 플러그인 등록됨");
|
|
1779
2430
|
} else {
|
|
1780
|
-
addDoctorCheck(report, {
|
|
2431
|
+
addDoctorCheck(report, {
|
|
2432
|
+
name: "plugin",
|
|
2433
|
+
status: "missing",
|
|
2434
|
+
path: pluginsFile,
|
|
2435
|
+
optional: true,
|
|
2436
|
+
fix: "/plugin marketplace add <repo-url>",
|
|
2437
|
+
});
|
|
1781
2438
|
warn("triflux 플러그인 미등록 — npm 단독 사용 중");
|
|
1782
2439
|
info("플러그인 등록: /plugin marketplace add <repo-url>");
|
|
1783
2440
|
}
|
|
1784
2441
|
} else {
|
|
1785
|
-
addDoctorCheck(report, {
|
|
2442
|
+
addDoctorCheck(report, {
|
|
2443
|
+
name: "plugin",
|
|
2444
|
+
status: "unavailable",
|
|
2445
|
+
optional: true,
|
|
2446
|
+
});
|
|
1786
2447
|
info("플러그인 시스템 감지 안 됨 — npm 단독 사용");
|
|
1787
2448
|
}
|
|
1788
2449
|
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
2450
|
+
// 10. MCP 인벤토리
|
|
2451
|
+
section("MCP Inventory");
|
|
2452
|
+
const mcpCache = join(CLAUDE_DIR, "cache", "mcp-inventory.json");
|
|
2453
|
+
if (existsSync(mcpCache)) {
|
|
2454
|
+
try {
|
|
2455
|
+
const inv = JSON.parse(readFileSync(mcpCache, "utf8"));
|
|
2456
|
+
addDoctorCheck(report, {
|
|
2457
|
+
name: "mcp-inventory",
|
|
2458
|
+
status: "ok",
|
|
2459
|
+
path: mcpCache,
|
|
2460
|
+
codex_servers: inv.codex?.servers?.length || 0,
|
|
2461
|
+
gemini_servers: inv.gemini?.servers?.length || 0,
|
|
2462
|
+
});
|
|
2463
|
+
ok(`캐시 존재 (${inv.timestamp})`);
|
|
2464
|
+
if (inv.codex?.servers?.length) {
|
|
2465
|
+
const names = inv.codex.servers.map((s) => s.name).join(", ");
|
|
2466
|
+
info(`Codex: ${inv.codex.servers.length}개 서버 (${names})`);
|
|
2467
|
+
}
|
|
2468
|
+
if (inv.gemini?.servers?.length) {
|
|
2469
|
+
const names = inv.gemini.servers.map((s) => s.name).join(", ");
|
|
2470
|
+
info(`Gemini: ${inv.gemini.servers.length}개 서버 (${names})`);
|
|
2471
|
+
}
|
|
2472
|
+
} catch {
|
|
2473
|
+
addDoctorCheck(report, {
|
|
2474
|
+
name: "mcp-inventory",
|
|
2475
|
+
status: "invalid",
|
|
2476
|
+
path: mcpCache,
|
|
2477
|
+
fix: `node ${join(PKG_ROOT, "scripts", "mcp-check.mjs")}`,
|
|
2478
|
+
});
|
|
2479
|
+
warn("캐시 파일 파싱 실패");
|
|
2480
|
+
}
|
|
2481
|
+
} else {
|
|
1795
2482
|
addDoctorCheck(report, {
|
|
1796
2483
|
name: "mcp-inventory",
|
|
1797
|
-
status: "
|
|
2484
|
+
status: "missing",
|
|
1798
2485
|
path: mcpCache,
|
|
1799
|
-
|
|
1800
|
-
gemini_servers: inv.gemini?.servers?.length || 0,
|
|
2486
|
+
fix: `node ${join(PKG_ROOT, "scripts", "mcp-check.mjs")}`,
|
|
1801
2487
|
});
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
const names = inv.codex.servers.map(s => s.name).join(", ");
|
|
1805
|
-
info(`Codex: ${inv.codex.servers.length}개 서버 (${names})`);
|
|
1806
|
-
}
|
|
1807
|
-
if (inv.gemini?.servers?.length) {
|
|
1808
|
-
const names = inv.gemini.servers.map(s => s.name).join(", ");
|
|
1809
|
-
info(`Gemini: ${inv.gemini.servers.length}개 서버 (${names})`);
|
|
1810
|
-
}
|
|
1811
|
-
} catch {
|
|
1812
|
-
addDoctorCheck(report, { name: "mcp-inventory", status: "invalid", path: mcpCache, fix: `node ${join(PKG_ROOT, "scripts", "mcp-check.mjs")}` });
|
|
1813
|
-
warn("캐시 파일 파싱 실패");
|
|
2488
|
+
warn("캐시 없음 — 다음 세션 시작 시 자동 생성");
|
|
2489
|
+
info(`수동: node ${join(PKG_ROOT, "scripts", "mcp-check.mjs")}`);
|
|
1814
2490
|
}
|
|
1815
|
-
} else {
|
|
1816
|
-
addDoctorCheck(report, { name: "mcp-inventory", status: "missing", path: mcpCache, fix: `node ${join(PKG_ROOT, "scripts", "mcp-check.mjs")}` });
|
|
1817
|
-
warn("캐시 없음 — 다음 세션 시작 시 자동 생성");
|
|
1818
|
-
info(`수동: node ${join(PKG_ROOT, "scripts", "mcp-check.mjs")}`);
|
|
1819
|
-
}
|
|
1820
2491
|
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
2492
|
+
// 9.5. Phase 1 웜업 캐시
|
|
2493
|
+
section("Warmup Cache");
|
|
2494
|
+
try {
|
|
2495
|
+
const { verifyCaches } = await import("../scripts/cache-doctor.mjs");
|
|
2496
|
+
const cacheVerification = verifyCaches({ cwd: process.cwd() });
|
|
2497
|
+
const brokenCaches = cacheVerification.results.filter(
|
|
2498
|
+
(result) => result.status !== "ok",
|
|
2499
|
+
);
|
|
1827
2500
|
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
2501
|
+
addDoctorCheck(report, {
|
|
2502
|
+
name: "warmup-cache",
|
|
2503
|
+
status: cacheVerification.ok ? "ok" : "issues",
|
|
2504
|
+
files: cacheVerification.results.map((result) => ({
|
|
2505
|
+
target: result.target,
|
|
2506
|
+
status: result.status,
|
|
2507
|
+
path: result.file,
|
|
2508
|
+
})),
|
|
2509
|
+
...(cacheVerification.ok ? {} : { fix: "tfx doctor --fix" }),
|
|
2510
|
+
});
|
|
1838
2511
|
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
2512
|
+
if (brokenCaches.length === 0) {
|
|
2513
|
+
ok("4개 웜업 캐시 정상");
|
|
2514
|
+
} else {
|
|
2515
|
+
warn(`${brokenCaches.length}개 웜업 캐시 이슈 발견`);
|
|
2516
|
+
for (const entry of brokenCaches) {
|
|
2517
|
+
info(`${entry.target}: ${entry.status}`);
|
|
2518
|
+
}
|
|
2519
|
+
if (!fix) issues += brokenCaches.length;
|
|
1845
2520
|
}
|
|
1846
|
-
|
|
2521
|
+
} catch (error) {
|
|
2522
|
+
addDoctorCheck(report, {
|
|
2523
|
+
name: "warmup-cache",
|
|
2524
|
+
status: "invalid",
|
|
2525
|
+
fix: "node scripts/cache-doctor.mjs --fix",
|
|
2526
|
+
});
|
|
2527
|
+
warn(`웜업 캐시 검사 실패: ${error.message}`);
|
|
2528
|
+
issues++;
|
|
1847
2529
|
}
|
|
1848
|
-
} catch (error) {
|
|
1849
|
-
addDoctorCheck(report, {
|
|
1850
|
-
name: "warmup-cache",
|
|
1851
|
-
status: "invalid",
|
|
1852
|
-
fix: "node scripts/cache-doctor.mjs --fix",
|
|
1853
|
-
});
|
|
1854
|
-
warn(`웜업 캐시 검사 실패: ${error.message}`);
|
|
1855
|
-
issues++;
|
|
1856
|
-
}
|
|
1857
2530
|
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
2531
|
+
// 11. CLI 이슈 트래커
|
|
2532
|
+
section("CLI Issues");
|
|
2533
|
+
const issuesFile = join(CLAUDE_DIR, "cache", "cli-issues.jsonl");
|
|
2534
|
+
if (existsSync(issuesFile)) {
|
|
2535
|
+
try {
|
|
2536
|
+
const lines = readFileSync(issuesFile, "utf8")
|
|
2537
|
+
.trim()
|
|
2538
|
+
.split("\n")
|
|
2539
|
+
.filter(Boolean);
|
|
2540
|
+
const entries = lines
|
|
2541
|
+
.map((l) => {
|
|
2542
|
+
try {
|
|
2543
|
+
return JSON.parse(l);
|
|
2544
|
+
} catch {
|
|
2545
|
+
return null;
|
|
2546
|
+
}
|
|
2547
|
+
})
|
|
2548
|
+
.filter(Boolean);
|
|
2549
|
+
const unresolved = entries.filter((e) => !e.resolved);
|
|
1866
2550
|
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
if ((pa[i] || 0) < (pb[i] || 0)) return false;
|
|
2551
|
+
if (unresolved.length === 0) {
|
|
2552
|
+
addDoctorCheck(report, {
|
|
2553
|
+
name: "cli-issues",
|
|
2554
|
+
status: "ok",
|
|
2555
|
+
path: issuesFile,
|
|
2556
|
+
unresolved: 0,
|
|
2557
|
+
});
|
|
2558
|
+
ok("미해결 이슈 없음");
|
|
2559
|
+
} else {
|
|
2560
|
+
// 패턴별 그룹핑
|
|
2561
|
+
const groups = {};
|
|
2562
|
+
for (const e of unresolved) {
|
|
2563
|
+
const key = `${e.cli}:${e.pattern}`;
|
|
2564
|
+
if (!groups[key]) groups[key] = { ...e, count: 0 };
|
|
2565
|
+
groups[key].count++;
|
|
2566
|
+
if (e.ts > groups[key].ts) {
|
|
2567
|
+
groups[key].ts = e.ts;
|
|
2568
|
+
groups[key].snippet = e.snippet;
|
|
2569
|
+
}
|
|
1887
2570
|
}
|
|
1888
|
-
return true;
|
|
1889
|
-
}
|
|
1890
2571
|
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
2572
|
+
// semver 비교 (lexicographic 비교 버그 방지)
|
|
2573
|
+
function semverGte(a, b) {
|
|
2574
|
+
const pa = a.split(".").map(Number);
|
|
2575
|
+
const pb = b.split(".").map(Number);
|
|
2576
|
+
for (let i = 0; i < 3; i++) {
|
|
2577
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return true;
|
|
2578
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return false;
|
|
2579
|
+
}
|
|
2580
|
+
return true;
|
|
2581
|
+
}
|
|
1895
2582
|
|
|
1896
|
-
|
|
1897
|
-
|
|
2583
|
+
// 알려진 해결 버전 (패턴별 수정된 triflux 버전)
|
|
2584
|
+
const KNOWN_FIXES = {
|
|
2585
|
+
"gemini:deprecated_flag": "1.8.9", // -p → --prompt
|
|
2586
|
+
};
|
|
1898
2587
|
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
cleaned += g.count;
|
|
1904
|
-
continue;
|
|
1905
|
-
}
|
|
1906
|
-
const age = Date.now() - g.ts;
|
|
1907
|
-
const ago = age < 3600000 ? `${Math.round(age / 60000)}분 전` :
|
|
1908
|
-
age < 86400000 ? `${Math.round(age / 3600000)}시간 전` :
|
|
1909
|
-
`${Math.round(age / 86400000)}일 전`;
|
|
1910
|
-
const sev = g.severity === "error" ? `${RED}ERROR${RESET}` : `${YELLOW}WARN${RESET}`;
|
|
1911
|
-
warn(`[${sev}] ${g.cli}/${g.pattern} x${g.count} (최근: ${ago})`);
|
|
1912
|
-
if (g.snippet) info(` ${g.snippet.substring(0, 120)}`);
|
|
1913
|
-
if (fixVer) info(` 해결: triflux >= v${fixVer} (npm update -g triflux)`);
|
|
1914
|
-
issues++;
|
|
1915
|
-
}
|
|
2588
|
+
const currentVer = JSON.parse(
|
|
2589
|
+
readFileSync(join(PKG_ROOT, "package.json"), "utf8"),
|
|
2590
|
+
).version;
|
|
2591
|
+
let cleaned = 0;
|
|
1916
2592
|
|
|
1917
|
-
|
|
1918
|
-
if (cleaned > 0) {
|
|
1919
|
-
const remaining = entries.filter(e => {
|
|
1920
|
-
const key = `${e.cli}:${e.pattern}`;
|
|
2593
|
+
for (const [key, g] of Object.entries(groups)) {
|
|
1921
2594
|
const fixVer = KNOWN_FIXES[key];
|
|
1922
|
-
|
|
2595
|
+
if (fixVer && semverGte(currentVer, fixVer)) {
|
|
2596
|
+
// 해결된 이슈 — 자동 정리
|
|
2597
|
+
cleaned += g.count;
|
|
2598
|
+
continue;
|
|
2599
|
+
}
|
|
2600
|
+
const age = Date.now() - g.ts;
|
|
2601
|
+
const ago =
|
|
2602
|
+
age < 3600000
|
|
2603
|
+
? `${Math.round(age / 60000)}분 전`
|
|
2604
|
+
: age < 86400000
|
|
2605
|
+
? `${Math.round(age / 3600000)}시간 전`
|
|
2606
|
+
: `${Math.round(age / 86400000)}일 전`;
|
|
2607
|
+
const sev =
|
|
2608
|
+
g.severity === "error"
|
|
2609
|
+
? `${RED}ERROR${RESET}`
|
|
2610
|
+
: `${YELLOW}WARN${RESET}`;
|
|
2611
|
+
warn(`[${sev}] ${g.cli}/${g.pattern} x${g.count} (최근: ${ago})`);
|
|
2612
|
+
if (g.snippet) info(` ${g.snippet.substring(0, 120)}`);
|
|
2613
|
+
if (fixVer)
|
|
2614
|
+
info(` 해결: triflux >= v${fixVer} (npm update -g triflux)`);
|
|
2615
|
+
issues++;
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
// 해결된 이슈 자동 정리
|
|
2619
|
+
if (cleaned > 0) {
|
|
2620
|
+
const remaining = entries.filter((e) => {
|
|
2621
|
+
const key = `${e.cli}:${e.pattern}`;
|
|
2622
|
+
const fixVer = KNOWN_FIXES[key];
|
|
2623
|
+
return !(fixVer && semverGte(currentVer, fixVer));
|
|
2624
|
+
});
|
|
2625
|
+
writeFileSync(
|
|
2626
|
+
issuesFile,
|
|
2627
|
+
remaining.map((e) => JSON.stringify(e)).join("\n") +
|
|
2628
|
+
(remaining.length ? "\n" : ""),
|
|
2629
|
+
);
|
|
2630
|
+
ok(`${cleaned}개 해결된 이슈 자동 정리됨`);
|
|
2631
|
+
}
|
|
2632
|
+
addDoctorCheck(report, {
|
|
2633
|
+
name: "cli-issues",
|
|
2634
|
+
status: unresolved.length === 0 ? "ok" : "issues",
|
|
2635
|
+
path: issuesFile,
|
|
2636
|
+
unresolved: unresolved.length,
|
|
1923
2637
|
});
|
|
1924
|
-
writeFileSync(issuesFile, remaining.map(e => JSON.stringify(e)).join("\n") + (remaining.length ? "\n" : ""));
|
|
1925
|
-
ok(`${cleaned}개 해결된 이슈 자동 정리됨`);
|
|
1926
2638
|
}
|
|
1927
|
-
|
|
2639
|
+
} catch (e) {
|
|
2640
|
+
addDoctorCheck(report, {
|
|
2641
|
+
name: "cli-issues",
|
|
2642
|
+
status: "invalid",
|
|
2643
|
+
path: issuesFile,
|
|
2644
|
+
fix: "cli-issues.jsonl 형식을 확인하세요.",
|
|
2645
|
+
});
|
|
2646
|
+
warn(`이슈 파일 읽기 실패: ${e.message}`);
|
|
1928
2647
|
}
|
|
1929
|
-
}
|
|
1930
|
-
addDoctorCheck(report, {
|
|
1931
|
-
|
|
2648
|
+
} else {
|
|
2649
|
+
addDoctorCheck(report, {
|
|
2650
|
+
name: "cli-issues",
|
|
2651
|
+
status: "ok",
|
|
2652
|
+
path: issuesFile,
|
|
2653
|
+
unresolved: 0,
|
|
2654
|
+
});
|
|
2655
|
+
ok("이슈 로그 없음 (정상)");
|
|
1932
2656
|
}
|
|
1933
|
-
} else {
|
|
1934
|
-
addDoctorCheck(report, { name: "cli-issues", status: "ok", path: issuesFile, unresolved: 0 });
|
|
1935
|
-
ok("이슈 로그 없음 (정상)");
|
|
1936
|
-
}
|
|
1937
|
-
|
|
1938
|
-
// 12. Team Sessions
|
|
1939
|
-
section("Team Sessions");
|
|
1940
|
-
const teamSessionReport = inspectTeamSessions();
|
|
1941
|
-
if (!teamSessionReport.mux) {
|
|
1942
|
-
addDoctorCheck(report, { name: "team-sessions", status: "skipped", detail: "tmux/psmux unavailable" });
|
|
1943
|
-
info("tmux/psmux 미감지 — 팀 세션 검사 건너뜀");
|
|
1944
|
-
} else if (teamSessionReport.sessions.length === 0) {
|
|
1945
|
-
addDoctorCheck(report, { name: "team-sessions", status: "ok", multiplexer: teamSessionReport.mux, sessions: 0 });
|
|
1946
|
-
ok(`활성 팀 세션 없음 ${DIM}(${teamSessionReport.mux})${RESET}`);
|
|
1947
|
-
} else {
|
|
1948
|
-
addDoctorCheck(report, {
|
|
1949
|
-
name: "team-sessions",
|
|
1950
|
-
status: teamSessionReport.sessions.some((session) => session.stale) ? "issues" : "ok",
|
|
1951
|
-
multiplexer: teamSessionReport.mux,
|
|
1952
|
-
sessions: teamSessionReport.sessions.map((session) => ({
|
|
1953
|
-
name: session.sessionName,
|
|
1954
|
-
attached: session.attachedCount,
|
|
1955
|
-
age_sec: session.ageSec,
|
|
1956
|
-
stale: session.stale,
|
|
1957
|
-
})),
|
|
1958
|
-
});
|
|
1959
|
-
info(`multiplexer: ${teamSessionReport.mux}`);
|
|
1960
2657
|
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
2658
|
+
// 12. Team Sessions
|
|
2659
|
+
section("Team Sessions");
|
|
2660
|
+
const teamSessionReport = inspectTeamSessions();
|
|
2661
|
+
if (!teamSessionReport.mux) {
|
|
2662
|
+
addDoctorCheck(report, {
|
|
2663
|
+
name: "team-sessions",
|
|
2664
|
+
status: "skipped",
|
|
2665
|
+
detail: "tmux/psmux unavailable",
|
|
2666
|
+
});
|
|
2667
|
+
info("tmux/psmux 미감지 — 팀 세션 검사 건너뜀");
|
|
2668
|
+
} else if (teamSessionReport.sessions.length === 0) {
|
|
2669
|
+
addDoctorCheck(report, {
|
|
2670
|
+
name: "team-sessions",
|
|
2671
|
+
status: "ok",
|
|
2672
|
+
multiplexer: teamSessionReport.mux,
|
|
2673
|
+
sessions: 0,
|
|
2674
|
+
});
|
|
2675
|
+
ok(`활성 팀 세션 없음 ${DIM}(${teamSessionReport.mux})${RESET}`);
|
|
2676
|
+
} else {
|
|
2677
|
+
addDoctorCheck(report, {
|
|
2678
|
+
name: "team-sessions",
|
|
2679
|
+
status: teamSessionReport.sessions.some((session) => session.stale)
|
|
2680
|
+
? "issues"
|
|
2681
|
+
: "ok",
|
|
2682
|
+
multiplexer: teamSessionReport.mux,
|
|
2683
|
+
sessions: teamSessionReport.sessions.map((session) => ({
|
|
2684
|
+
name: session.sessionName,
|
|
2685
|
+
attached: session.attachedCount,
|
|
2686
|
+
age_sec: session.ageSec,
|
|
2687
|
+
stale: session.stale,
|
|
2688
|
+
})),
|
|
2689
|
+
});
|
|
2690
|
+
info(`multiplexer: ${teamSessionReport.mux}`);
|
|
1964
2691
|
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
}
|
|
2692
|
+
for (const session of teamSessionReport.sessions) {
|
|
2693
|
+
const attachedLabel =
|
|
2694
|
+
session.attachedCount == null ? "?" : `${session.attachedCount}`;
|
|
2695
|
+
const ageLabel = formatElapsedAge(session.ageSec);
|
|
1970
2696
|
|
|
1971
|
-
|
|
1972
|
-
|
|
2697
|
+
if (session.stale) {
|
|
2698
|
+
warn(
|
|
2699
|
+
`${session.sessionName}: stale 추정 (attach=${attachedLabel}, 경과=${ageLabel})`,
|
|
2700
|
+
);
|
|
2701
|
+
} else {
|
|
2702
|
+
ok(
|
|
2703
|
+
`${session.sessionName}: 정상 (attach=${attachedLabel}, 경과=${ageLabel})`,
|
|
2704
|
+
);
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
if (session.createdAt == null) {
|
|
2708
|
+
info(
|
|
2709
|
+
`${session.sessionName}: session_created 파싱 실패${session.createdRaw ? ` (${session.createdRaw})` : ""}`,
|
|
2710
|
+
);
|
|
2711
|
+
}
|
|
1973
2712
|
}
|
|
1974
|
-
}
|
|
1975
2713
|
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
2714
|
+
const staleSessions = teamSessionReport.sessions.filter(
|
|
2715
|
+
(session) => session.stale,
|
|
2716
|
+
);
|
|
2717
|
+
if (staleSessions.length > 0) {
|
|
2718
|
+
if (fix) {
|
|
2719
|
+
const cleanupResult = await cleanupStaleTeamSessions(staleSessions);
|
|
2720
|
+
issues += cleanupResult.failed;
|
|
2721
|
+
} else {
|
|
2722
|
+
info("정리: tfx doctor --fix");
|
|
2723
|
+
issues += staleSessions.length;
|
|
2724
|
+
}
|
|
1984
2725
|
}
|
|
1985
2726
|
}
|
|
1986
|
-
}
|
|
1987
|
-
|
|
1988
|
-
// 13. OMC stale team 상태
|
|
1989
|
-
section("OMC Stale Teams");
|
|
1990
|
-
const omcTeamReport = inspectStaleOmcTeams({
|
|
1991
|
-
startDir: process.cwd(),
|
|
1992
|
-
maxAgeMs: STALE_TEAM_MAX_AGE_SEC * 1000,
|
|
1993
|
-
liveSessionNames: teamSessionReport.sessions.map((session) => session.sessionName),
|
|
1994
|
-
});
|
|
1995
|
-
if (!omcTeamReport.stateRoot && !omcTeamReport.teamsRoot) {
|
|
1996
|
-
addDoctorCheck(report, { name: "omc-stale-teams", status: "skipped" });
|
|
1997
|
-
info(".omc/state 및 ~/.claude/teams 없음 — 검사 건너뜀");
|
|
1998
|
-
} else if (omcTeamReport.entries.length === 0) {
|
|
1999
|
-
addDoctorCheck(report, { name: "omc-stale-teams", status: "ok", entries: 0 });
|
|
2000
|
-
const roots = [omcTeamReport.stateRoot, omcTeamReport.teamsRoot].filter(Boolean).join(", ");
|
|
2001
|
-
ok(`stale team 없음 ${DIM}(${roots})${RESET}`);
|
|
2002
|
-
} else {
|
|
2003
|
-
addDoctorCheck(report, { name: "omc-stale-teams", status: "issues", entries: omcTeamReport.entries.length, fix: "tfx doctor --fix" });
|
|
2004
|
-
warn(`${omcTeamReport.entries.length}개 stale team 발견`);
|
|
2005
2727
|
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2728
|
+
// 13. OMC stale team 상태
|
|
2729
|
+
section("OMC Stale Teams");
|
|
2730
|
+
const omcTeamReport = inspectStaleOmcTeams({
|
|
2731
|
+
startDir: process.cwd(),
|
|
2732
|
+
maxAgeMs: STALE_TEAM_MAX_AGE_SEC * 1000,
|
|
2733
|
+
liveSessionNames: teamSessionReport.sessions.map(
|
|
2734
|
+
(session) => session.sessionName,
|
|
2735
|
+
),
|
|
2736
|
+
});
|
|
2737
|
+
if (!omcTeamReport.stateRoot && !omcTeamReport.teamsRoot) {
|
|
2738
|
+
addDoctorCheck(report, { name: "omc-stale-teams", status: "skipped" });
|
|
2739
|
+
info(".omc/state 및 ~/.claude/teams 없음 — 검사 건너뜀");
|
|
2740
|
+
} else if (omcTeamReport.entries.length === 0) {
|
|
2741
|
+
addDoctorCheck(report, {
|
|
2742
|
+
name: "omc-stale-teams",
|
|
2743
|
+
status: "ok",
|
|
2744
|
+
entries: 0,
|
|
2745
|
+
});
|
|
2746
|
+
const roots = [omcTeamReport.stateRoot, omcTeamReport.teamsRoot]
|
|
2747
|
+
.filter(Boolean)
|
|
2748
|
+
.join(", ");
|
|
2749
|
+
ok(`stale team 없음 ${DIM}(${roots})${RESET}`);
|
|
2750
|
+
} else {
|
|
2751
|
+
addDoctorCheck(report, {
|
|
2752
|
+
name: "omc-stale-teams",
|
|
2753
|
+
status: "issues",
|
|
2754
|
+
entries: omcTeamReport.entries.length,
|
|
2755
|
+
fix: "tfx doctor --fix",
|
|
2756
|
+
});
|
|
2757
|
+
warn(`${omcTeamReport.entries.length}개 stale team 발견`);
|
|
2017
2758
|
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
const label = result.entry.scope === "root"
|
|
2759
|
+
for (const entry of omcTeamReport.entries) {
|
|
2760
|
+
const ageLabel = formatElapsedAge(entry.ageSec);
|
|
2761
|
+
const scopeLabel =
|
|
2762
|
+
entry.scope === "root"
|
|
2023
2763
|
? "root-state"
|
|
2024
|
-
:
|
|
2025
|
-
?
|
|
2026
|
-
:
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
? "root-state"
|
|
2031
|
-
: result.entry.scope === "claude_team"
|
|
2032
|
-
? (result.entry.teamName || result.entry.sessionId)
|
|
2033
|
-
: result.entry.sessionId;
|
|
2034
|
-
fail(`stale team 정리 실패: ${label} — ${result.error.message}`);
|
|
2035
|
-
}
|
|
2764
|
+
: entry.scope === "claude_team"
|
|
2765
|
+
? `claude-team:${entry.teamName || entry.sessionId}`
|
|
2766
|
+
: entry.sessionId;
|
|
2767
|
+
warn(`${scopeLabel}: stale team (경과=${ageLabel}, 프로세스 없음)`);
|
|
2768
|
+
if (entry.teamName) info(`팀: ${entry.teamName}`);
|
|
2769
|
+
info(`파일: ${entry.stateFile || entry.cleanupPath}`);
|
|
2036
2770
|
}
|
|
2037
|
-
issues += cleanupResult.failed;
|
|
2038
|
-
} else {
|
|
2039
|
-
info("정리: tfx doctor --fix");
|
|
2040
|
-
issues += omcTeamReport.entries.length;
|
|
2041
|
-
}
|
|
2042
|
-
}
|
|
2043
2771
|
|
|
2044
|
-
// 12.5. 고아 node.exe 프로세스 정리 (Windows)
|
|
2045
|
-
section("Orphan Processes");
|
|
2046
|
-
if (process.platform === "win32") {
|
|
2047
|
-
try {
|
|
2048
|
-
const { cleanupOrphanNodeProcesses } = await import("../hub/lib/process-utils.mjs");
|
|
2049
2772
|
if (fix) {
|
|
2050
|
-
const
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2773
|
+
const cleanupResult = await cleanupStaleOmcTeams(omcTeamReport.entries);
|
|
2774
|
+
for (const result of cleanupResult.results) {
|
|
2775
|
+
if (result.ok) {
|
|
2776
|
+
const label =
|
|
2777
|
+
result.entry.scope === "root"
|
|
2778
|
+
? "root-state"
|
|
2779
|
+
: result.entry.scope === "claude_team"
|
|
2780
|
+
? result.entry.teamName || result.entry.sessionId
|
|
2781
|
+
: result.entry.sessionId;
|
|
2782
|
+
ok(`stale team 정리: ${label}`);
|
|
2783
|
+
} else {
|
|
2784
|
+
const label =
|
|
2785
|
+
result.entry.scope === "root"
|
|
2786
|
+
? "root-state"
|
|
2787
|
+
: result.entry.scope === "claude_team"
|
|
2788
|
+
? result.entry.teamName || result.entry.sessionId
|
|
2789
|
+
: result.entry.sessionId;
|
|
2790
|
+
fail(`stale team 정리 실패: ${label} — ${result.error.message}`);
|
|
2791
|
+
}
|
|
2055
2792
|
}
|
|
2793
|
+
issues += cleanupResult.failed;
|
|
2056
2794
|
} else {
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2795
|
+
info("정리: tfx doctor --fix");
|
|
2796
|
+
issues += omcTeamReport.entries.length;
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
// 12.5. 고아 node.exe 프로세스 정리 (Windows)
|
|
2801
|
+
section("Orphan Processes");
|
|
2802
|
+
if (process.platform === "win32") {
|
|
2803
|
+
try {
|
|
2804
|
+
const { cleanupOrphanNodeProcesses } = await import(
|
|
2805
|
+
"../hub/lib/process-utils.mjs"
|
|
2806
|
+
);
|
|
2807
|
+
if (fix) {
|
|
2808
|
+
const { killed, remaining } = cleanupOrphanNodeProcesses();
|
|
2809
|
+
if (killed > 0) {
|
|
2810
|
+
warn(
|
|
2811
|
+
`고아 node.exe ${killed}개 정리 완료 (남은 프로세스: ${remaining})`,
|
|
2812
|
+
);
|
|
2813
|
+
} else {
|
|
2814
|
+
ok(`고아 node.exe 없음 (활성: ${remaining})`);
|
|
2815
|
+
}
|
|
2067
2816
|
} else {
|
|
2068
|
-
|
|
2817
|
+
// --fix 없이는 개수만 보고
|
|
2818
|
+
const { execSync: execSyncDoctor } = await import(
|
|
2819
|
+
"node:child_process"
|
|
2820
|
+
);
|
|
2821
|
+
const countStr = execSyncDoctor(
|
|
2822
|
+
`powershell -NoProfile -WindowStyle Hidden -Command "(Get-Process node -ErrorAction SilentlyContinue).Count"`,
|
|
2823
|
+
{ encoding: "utf8", timeout: 5000 },
|
|
2824
|
+
).trim();
|
|
2825
|
+
const count = Number.parseInt(countStr, 10) || 0;
|
|
2826
|
+
if (count > 20) {
|
|
2827
|
+
warn(
|
|
2828
|
+
`node.exe ${count}개 실행 중 (고아 포함 가능). 정리: tfx doctor --fix`,
|
|
2829
|
+
);
|
|
2830
|
+
issues++;
|
|
2831
|
+
} else {
|
|
2832
|
+
ok(`node.exe ${count}개 (정상 범위)`);
|
|
2833
|
+
}
|
|
2069
2834
|
}
|
|
2835
|
+
} catch (e) {
|
|
2836
|
+
info(`고아 프로세스 검사 실패: ${e.message}`);
|
|
2070
2837
|
}
|
|
2071
|
-
}
|
|
2072
|
-
|
|
2838
|
+
} else {
|
|
2839
|
+
ok("Windows 전용 검사 — 건너뜀");
|
|
2073
2840
|
}
|
|
2074
|
-
} else {
|
|
2075
|
-
ok("Windows 전용 검사 — 건너뜀");
|
|
2076
|
-
}
|
|
2077
2841
|
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
try { return statSync(join(teamsDir, d)).isDirectory(); } catch { return false; }
|
|
2086
|
-
});
|
|
2087
|
-
if (teamDirs.length === 0) {
|
|
2088
|
-
addDoctorCheck(report, { name: "stale-teams", status: "ok", entries: 0 });
|
|
2089
|
-
ok("잔존 팀 없음");
|
|
2090
|
-
} else {
|
|
2091
|
-
const nowMs = Date.now();
|
|
2092
|
-
const staleMaxAgeMs = STALE_TEAM_MAX_AGE_SEC * 1000;
|
|
2093
|
-
const staleTeams = [];
|
|
2094
|
-
const activeTeams = [];
|
|
2095
|
-
|
|
2096
|
-
for (const d of teamDirs) {
|
|
2097
|
-
const teamPath = join(teamsDir, d);
|
|
2098
|
-
const configPath = join(teamPath, "config.json");
|
|
2099
|
-
let teamConfig = null;
|
|
2100
|
-
let configMtimeMs = null;
|
|
2101
|
-
let missingConfig = false;
|
|
2102
|
-
|
|
2103
|
-
// config.json 읽기 — createdAt 또는 mtime으로 나이 판정
|
|
2842
|
+
// 14. Stale Teams (Claude teams/ + tasks/ 자동 감지)
|
|
2843
|
+
section("Stale Teams");
|
|
2844
|
+
const teamsDir = join(CLAUDE_DIR, "teams");
|
|
2845
|
+
const _tasksDir = join(CLAUDE_DIR, "tasks");
|
|
2846
|
+
if (existsSync(teamsDir)) {
|
|
2847
|
+
try {
|
|
2848
|
+
const teamDirs = readdirSync(teamsDir).filter((d) => {
|
|
2104
2849
|
try {
|
|
2105
|
-
|
|
2106
|
-
configMtimeMs = configStat.mtimeMs;
|
|
2107
|
-
teamConfig = JSON.parse(readFileSync(configPath, "utf8"));
|
|
2850
|
+
return statSync(join(teamsDir, d)).isDirectory();
|
|
2108
2851
|
} catch {
|
|
2109
|
-
|
|
2110
|
-
// config.json 없으면 표시용 경과 시간만 디렉토리 기준으로 계산
|
|
2111
|
-
try { configMtimeMs = statSync(teamPath).mtimeMs; } catch {}
|
|
2852
|
+
return false;
|
|
2112
2853
|
}
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2854
|
+
});
|
|
2855
|
+
if (teamDirs.length === 0) {
|
|
2856
|
+
addDoctorCheck(report, {
|
|
2857
|
+
name: "stale-teams",
|
|
2858
|
+
status: "ok",
|
|
2859
|
+
entries: 0,
|
|
2860
|
+
});
|
|
2861
|
+
ok("잔존 팀 없음");
|
|
2862
|
+
} else {
|
|
2863
|
+
const nowMs = Date.now();
|
|
2864
|
+
const staleMaxAgeMs = STALE_TEAM_MAX_AGE_SEC * 1000;
|
|
2865
|
+
const staleTeams = [];
|
|
2866
|
+
const activeTeams = [];
|
|
2867
|
+
|
|
2868
|
+
for (const d of teamDirs) {
|
|
2869
|
+
const teamPath = join(teamsDir, d);
|
|
2870
|
+
const configPath = join(teamPath, "config.json");
|
|
2871
|
+
let teamConfig = null;
|
|
2872
|
+
let configMtimeMs = null;
|
|
2873
|
+
let missingConfig = false;
|
|
2874
|
+
|
|
2875
|
+
// config.json 읽기 — createdAt 또는 mtime으로 나이 판정
|
|
2876
|
+
try {
|
|
2877
|
+
const configStat = statSync(configPath);
|
|
2878
|
+
configMtimeMs = configStat.mtimeMs;
|
|
2879
|
+
teamConfig = JSON.parse(readFileSync(configPath, "utf8"));
|
|
2880
|
+
} catch {
|
|
2881
|
+
missingConfig = true;
|
|
2882
|
+
// config.json 없으면 표시용 경과 시간만 디렉토리 기준으로 계산
|
|
2883
|
+
try {
|
|
2884
|
+
configMtimeMs = statSync(teamPath).mtimeMs;
|
|
2885
|
+
} catch {}
|
|
2127
2886
|
}
|
|
2128
2887
|
|
|
2129
|
-
|
|
2130
|
-
const
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2888
|
+
const createdAtMs = teamConfig?.createdAt ?? configMtimeMs;
|
|
2889
|
+
const ageMs =
|
|
2890
|
+
createdAtMs != null ? Math.max(0, nowMs - createdAtMs) : null;
|
|
2891
|
+
const ageSec = ageMs != null ? Math.floor(ageMs / 1000) : null;
|
|
2892
|
+
const aged = ageMs != null && ageMs >= staleMaxAgeMs;
|
|
2893
|
+
|
|
2894
|
+
// 활성 멤버 확인 — leadSessionId 또는 멤버 agentId로 프로세스 검색
|
|
2895
|
+
let hasActiveMember = false;
|
|
2896
|
+
if (teamConfig?.members?.length > 0) {
|
|
2897
|
+
const searchTokens = [];
|
|
2898
|
+
if (teamConfig.leadSessionId)
|
|
2899
|
+
searchTokens.push(teamConfig.leadSessionId.toLowerCase());
|
|
2900
|
+
if (teamConfig.name)
|
|
2901
|
+
searchTokens.push(teamConfig.name.toLowerCase());
|
|
2902
|
+
for (const member of teamConfig.members) {
|
|
2903
|
+
if (member.agentId)
|
|
2904
|
+
searchTokens.push(member.agentId.split("@")[0].toLowerCase());
|
|
2905
|
+
}
|
|
2134
2906
|
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2907
|
+
// tmux 세션 이름과 매칭
|
|
2908
|
+
const liveSessionNames = teamSessionReport.sessions.map((s) =>
|
|
2909
|
+
s.sessionName.toLowerCase(),
|
|
2910
|
+
);
|
|
2911
|
+
hasActiveMember = searchTokens.some((token) =>
|
|
2912
|
+
liveSessionNames.some((name) => name.includes(token)),
|
|
2913
|
+
);
|
|
2914
|
+
|
|
2915
|
+
// 프로세스 명령줄에서 세션 ID 매칭 (tmux 없는 in-process 팀 지원)
|
|
2916
|
+
if (!hasActiveMember && teamConfig.leadSessionId) {
|
|
2917
|
+
try {
|
|
2918
|
+
const _sessionToken = teamConfig.leadSessionId.toLowerCase();
|
|
2919
|
+
const safeToken = teamConfig.leadSessionId
|
|
2920
|
+
.slice(0, 8)
|
|
2921
|
+
.replace(/[^a-zA-Z0-9-]/g, "");
|
|
2922
|
+
// Claude Code 프로세스에서 세션 ID 검색
|
|
2923
|
+
if (process.platform === "win32") {
|
|
2924
|
+
const psOut = execSync(
|
|
2925
|
+
`powershell -NoProfile -WindowStyle Hidden -Command "$ErrorActionPreference='SilentlyContinue'; Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -match '${safeToken}' } | Select-Object ProcessId | ConvertTo-Json -Compress"`,
|
|
2926
|
+
{
|
|
2927
|
+
encoding: "utf8",
|
|
2928
|
+
timeout: 8000,
|
|
2929
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
2930
|
+
windowsHide: true,
|
|
2931
|
+
},
|
|
2932
|
+
).trim();
|
|
2933
|
+
if (psOut && psOut !== "null") {
|
|
2934
|
+
const parsed = JSON.parse(psOut);
|
|
2935
|
+
const procs = Array.isArray(parsed) ? parsed : [parsed];
|
|
2936
|
+
hasActiveMember = procs.some((p) => p.ProcessId > 0);
|
|
2937
|
+
}
|
|
2938
|
+
} else {
|
|
2939
|
+
const psOut = execSync(
|
|
2940
|
+
`ps -ax -o pid=,command= | grep -i '${safeToken}' | grep -v grep`,
|
|
2941
|
+
{
|
|
2942
|
+
encoding: "utf8",
|
|
2943
|
+
timeout: 5000,
|
|
2944
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
2945
|
+
windowsHide: true,
|
|
2946
|
+
},
|
|
2947
|
+
).trim();
|
|
2948
|
+
hasActiveMember = psOut.length > 0;
|
|
2150
2949
|
}
|
|
2151
|
-
}
|
|
2152
|
-
|
|
2153
|
-
`ps -ax -o pid=,command= | grep -i '${safeToken}' | grep -v grep`,
|
|
2154
|
-
{ encoding: "utf8", timeout: 5000, stdio: ["ignore", "pipe", "ignore"], windowsHide: true },
|
|
2155
|
-
).trim();
|
|
2156
|
-
hasActiveMember = psOut.length > 0;
|
|
2950
|
+
} catch {
|
|
2951
|
+
// 프로세스 검색 실패 — stale로 간주하지 않음 (보수적)
|
|
2157
2952
|
}
|
|
2158
|
-
} catch {
|
|
2159
|
-
// 프로세스 검색 실패 — stale로 간주하지 않음 (보수적)
|
|
2160
2953
|
}
|
|
2161
2954
|
}
|
|
2162
|
-
}
|
|
2163
|
-
|
|
2164
|
-
const stale = missingConfig || (aged && !hasActiveMember);
|
|
2165
|
-
const teamEntry = {
|
|
2166
|
-
name: d,
|
|
2167
|
-
teamName: teamConfig?.name || d,
|
|
2168
|
-
description: teamConfig?.description || null,
|
|
2169
|
-
memberCount: teamConfig?.members?.length || 0,
|
|
2170
|
-
ageSec,
|
|
2171
|
-
stale,
|
|
2172
|
-
hasActiveMember,
|
|
2173
|
-
missingConfig,
|
|
2174
|
-
};
|
|
2175
2955
|
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2956
|
+
const stale = missingConfig || (aged && !hasActiveMember);
|
|
2957
|
+
const teamEntry = {
|
|
2958
|
+
name: d,
|
|
2959
|
+
teamName: teamConfig?.name || d,
|
|
2960
|
+
description: teamConfig?.description || null,
|
|
2961
|
+
memberCount: teamConfig?.members?.length || 0,
|
|
2962
|
+
ageSec,
|
|
2963
|
+
stale,
|
|
2964
|
+
hasActiveMember,
|
|
2965
|
+
missingConfig,
|
|
2966
|
+
};
|
|
2967
|
+
|
|
2968
|
+
if (stale) {
|
|
2969
|
+
staleTeams.push(teamEntry);
|
|
2970
|
+
} else {
|
|
2971
|
+
activeTeams.push(teamEntry);
|
|
2972
|
+
}
|
|
2180
2973
|
}
|
|
2181
|
-
}
|
|
2182
2974
|
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
const ageLabel = formatElapsedAge(t.ageSec);
|
|
2186
|
-
const memberLabel = `${t.memberCount}명`;
|
|
2187
|
-
ok(`${t.name}: 활성 (경과=${ageLabel}, 멤버=${memberLabel})`);
|
|
2188
|
-
}
|
|
2189
|
-
|
|
2190
|
-
// stale 팀 표시 및 정리
|
|
2191
|
-
if (staleTeams.length === 0 && activeTeams.length > 0) {
|
|
2192
|
-
addDoctorCheck(report, { name: "stale-teams", status: "ok", active: activeTeams.length, stale: 0 });
|
|
2193
|
-
ok("stale 팀 없음");
|
|
2194
|
-
} else if (staleTeams.length > 0) {
|
|
2195
|
-
addDoctorCheck(report, { name: "stale-teams", status: "issues", active: activeTeams.length, stale: staleTeams.length, fix: "tfx doctor --fix" });
|
|
2196
|
-
warn(`${staleTeams.length}개 stale 팀 발견`);
|
|
2197
|
-
for (const t of staleTeams) {
|
|
2975
|
+
// 활성 팀 표시
|
|
2976
|
+
for (const t of activeTeams) {
|
|
2198
2977
|
const ageLabel = formatElapsedAge(t.ageSec);
|
|
2199
|
-
const
|
|
2200
|
-
|
|
2201
|
-
if (t.description) info(`설명: ${t.description}`);
|
|
2978
|
+
const memberLabel = `${t.memberCount}명`;
|
|
2979
|
+
ok(`${t.name}: 활성 (경과=${ageLabel}, 멤버=${memberLabel})`);
|
|
2202
2980
|
}
|
|
2203
2981
|
|
|
2204
|
-
|
|
2205
|
-
|
|
2982
|
+
// stale 팀 표시 및 정리
|
|
2983
|
+
if (staleTeams.length === 0 && activeTeams.length > 0) {
|
|
2984
|
+
addDoctorCheck(report, {
|
|
2985
|
+
name: "stale-teams",
|
|
2986
|
+
status: "ok",
|
|
2987
|
+
active: activeTeams.length,
|
|
2988
|
+
stale: 0,
|
|
2989
|
+
});
|
|
2990
|
+
ok("stale 팀 없음");
|
|
2991
|
+
} else if (staleTeams.length > 0) {
|
|
2992
|
+
addDoctorCheck(report, {
|
|
2993
|
+
name: "stale-teams",
|
|
2994
|
+
status: "issues",
|
|
2995
|
+
active: activeTeams.length,
|
|
2996
|
+
stale: staleTeams.length,
|
|
2997
|
+
fix: "tfx doctor --fix",
|
|
2998
|
+
});
|
|
2999
|
+
warn(`${staleTeams.length}개 stale 팀 발견`);
|
|
2206
3000
|
for (const t of staleTeams) {
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
3001
|
+
const ageLabel = formatElapsedAge(t.ageSec);
|
|
3002
|
+
const reasonLabel = t.missingConfig
|
|
3003
|
+
? "config.json 없음"
|
|
3004
|
+
: "활성 프로세스 없음";
|
|
3005
|
+
warn(
|
|
3006
|
+
`${t.name}: stale (경과=${ageLabel}, 멤버=${t.memberCount}명, ${reasonLabel})`,
|
|
3007
|
+
);
|
|
3008
|
+
if (t.description) info(`설명: ${t.description}`);
|
|
3009
|
+
}
|
|
3010
|
+
|
|
3011
|
+
if (fix) {
|
|
3012
|
+
let cleaned = 0;
|
|
3013
|
+
for (const t of staleTeams) {
|
|
3014
|
+
try {
|
|
3015
|
+
await forceCleanupTeam(t.name);
|
|
3016
|
+
cleaned++;
|
|
3017
|
+
ok(`stale 팀 정리: ${t.name}`);
|
|
3018
|
+
} catch (e) {
|
|
3019
|
+
fail(`팀 정리 실패: ${t.name} — ${e.message}`);
|
|
3020
|
+
}
|
|
2213
3021
|
}
|
|
3022
|
+
info(`${cleaned}/${staleTeams.length}개 stale 팀 정리 완료`);
|
|
3023
|
+
} else {
|
|
3024
|
+
info("정리: tfx doctor --fix");
|
|
3025
|
+
issues += staleTeams.length;
|
|
2214
3026
|
}
|
|
2215
|
-
info(`${cleaned}/${staleTeams.length}개 stale 팀 정리 완료`);
|
|
2216
|
-
} else {
|
|
2217
|
-
info("정리: tfx doctor --fix");
|
|
2218
|
-
issues += staleTeams.length;
|
|
2219
3027
|
}
|
|
2220
3028
|
}
|
|
3029
|
+
} catch (e) {
|
|
3030
|
+
addDoctorCheck(report, {
|
|
3031
|
+
name: "stale-teams",
|
|
3032
|
+
status: "invalid",
|
|
3033
|
+
fix: "teams 디렉토리 구조를 확인하세요.",
|
|
3034
|
+
});
|
|
3035
|
+
warn(`teams 디렉토리 읽기 실패: ${e.message}`);
|
|
2221
3036
|
}
|
|
2222
|
-
}
|
|
2223
|
-
addDoctorCheck(report, { name: "stale-teams", status: "
|
|
2224
|
-
|
|
3037
|
+
} else {
|
|
3038
|
+
addDoctorCheck(report, { name: "stale-teams", status: "ok", entries: 0 });
|
|
3039
|
+
ok("잔존 팀 없음");
|
|
2225
3040
|
}
|
|
2226
|
-
} else {
|
|
2227
|
-
addDoctorCheck(report, { name: "stale-teams", status: "ok", entries: 0 });
|
|
2228
|
-
ok("잔존 팀 없음");
|
|
2229
|
-
}
|
|
2230
3041
|
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
3042
|
+
// ── Docs 동기화 상태 ──
|
|
3043
|
+
section("Docs Sync");
|
|
3044
|
+
{
|
|
3045
|
+
const docsDirs = ["docs/design", "docs/research"];
|
|
3046
|
+
const missingDocs = [];
|
|
3047
|
+
for (const dir of docsDirs) {
|
|
3048
|
+
const src = join(PKG_ROOT, dir);
|
|
3049
|
+
const dest = join(CLAUDE_DIR, dir);
|
|
3050
|
+
if (existsSync(src)) {
|
|
3051
|
+
const srcFiles = readdirSync(src).filter((f) => f.endsWith(".md"));
|
|
3052
|
+
if (!existsSync(dest)) {
|
|
3053
|
+
missingDocs.push({
|
|
3054
|
+
dir,
|
|
3055
|
+
missing: srcFiles.length,
|
|
3056
|
+
detail: "디렉토리 없음",
|
|
3057
|
+
});
|
|
3058
|
+
} else {
|
|
3059
|
+
const destFiles = readdirSync(dest).filter((f) =>
|
|
3060
|
+
f.endsWith(".md"),
|
|
3061
|
+
);
|
|
3062
|
+
const missing = srcFiles.filter((f) => !destFiles.includes(f));
|
|
3063
|
+
if (missing.length > 0)
|
|
3064
|
+
missingDocs.push({
|
|
3065
|
+
dir,
|
|
3066
|
+
missing: missing.length,
|
|
3067
|
+
detail: missing.join(", "),
|
|
3068
|
+
});
|
|
3069
|
+
}
|
|
2247
3070
|
}
|
|
2248
3071
|
}
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
3072
|
+
if (missingDocs.length === 0) {
|
|
3073
|
+
addDoctorCheck(report, { name: "docs-sync", status: "ok" });
|
|
3074
|
+
ok("레퍼런스 문서 동기화 정상");
|
|
3075
|
+
} else {
|
|
3076
|
+
addDoctorCheck(report, {
|
|
3077
|
+
name: "docs-sync",
|
|
3078
|
+
status: "issues",
|
|
3079
|
+
missingDocs,
|
|
3080
|
+
fix: "tfx setup",
|
|
3081
|
+
});
|
|
3082
|
+
warn(
|
|
3083
|
+
`${missingDocs.reduce((s, d) => s + d.missing, 0)}개 레퍼런스 미동기화`,
|
|
3084
|
+
);
|
|
3085
|
+
for (const d of missingDocs) info(`${d.dir}: ${d.detail}`);
|
|
3086
|
+
if (fix) {
|
|
3087
|
+
for (const dir of docsDirs) {
|
|
3088
|
+
const src = join(PKG_ROOT, dir);
|
|
3089
|
+
const dest = join(CLAUDE_DIR, dir);
|
|
3090
|
+
if (existsSync(src)) {
|
|
3091
|
+
mkdirSync(dest, { recursive: true });
|
|
3092
|
+
for (const f of readdirSync(src).filter((f) =>
|
|
3093
|
+
f.endsWith(".md"),
|
|
3094
|
+
)) {
|
|
3095
|
+
copyFileSync(join(src, f), join(dest, f));
|
|
3096
|
+
}
|
|
2265
3097
|
}
|
|
2266
3098
|
}
|
|
3099
|
+
ok("레퍼런스 동기화 완료");
|
|
3100
|
+
} else {
|
|
3101
|
+
issues += missingDocs.length;
|
|
2267
3102
|
}
|
|
2268
|
-
ok("레퍼런스 동기화 완료");
|
|
2269
|
-
} else {
|
|
2270
|
-
issues += missingDocs.length;
|
|
2271
3103
|
}
|
|
2272
3104
|
}
|
|
2273
|
-
}
|
|
2274
3105
|
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
error: config.parseError?.message || "parse error",
|
|
2322
|
-
})),
|
|
2323
|
-
...(stdioRows.length > 0 ? { fix: "tfx doctor --fix 또는 tfx mcp sync" } : {}),
|
|
2324
|
-
});
|
|
3106
|
+
// ── MCP 중앙 레지스트리 ──
|
|
3107
|
+
section("MCP Registry");
|
|
3108
|
+
{
|
|
3109
|
+
let registryState = inspectRegistry();
|
|
3110
|
+
if (!registryState.exists) {
|
|
3111
|
+
saveRegistry(createDefaultRegistry());
|
|
3112
|
+
registryState = inspectRegistry();
|
|
3113
|
+
addDoctorCheck(report, {
|
|
3114
|
+
name: "mcp-registry",
|
|
3115
|
+
status: "fixed",
|
|
3116
|
+
path: registryState.path,
|
|
3117
|
+
action: "기본값으로 자동 생성됨",
|
|
3118
|
+
});
|
|
3119
|
+
ok("mcp-registry.json 없음 → 기본값으로 자동 생성됨");
|
|
3120
|
+
} else if (!registryState.valid) {
|
|
3121
|
+
saveRegistry(createDefaultRegistry());
|
|
3122
|
+
registryState = inspectRegistry();
|
|
3123
|
+
addDoctorCheck(report, {
|
|
3124
|
+
name: "mcp-registry",
|
|
3125
|
+
status: "fixed",
|
|
3126
|
+
path: registryState.path,
|
|
3127
|
+
action: "손상 감지 → 기본값으로 재생성됨",
|
|
3128
|
+
});
|
|
3129
|
+
warn("mcp-registry.json 손상 → 기본값으로 재생성됨");
|
|
3130
|
+
} else {
|
|
3131
|
+
const statusInfo = inspectRegistryStatus(registryState.registry);
|
|
3132
|
+
const invalidConfigs = statusInfo.configs.filter(
|
|
3133
|
+
(config) => config.parseError,
|
|
3134
|
+
);
|
|
3135
|
+
const mismatchRows = statusInfo.rows.filter(
|
|
3136
|
+
(row) => row.type === "registry" && row.status === "mismatch",
|
|
3137
|
+
);
|
|
3138
|
+
const missingRows = statusInfo.rows.filter(
|
|
3139
|
+
(row) => row.type === "registry" && row.status === "missing",
|
|
3140
|
+
);
|
|
3141
|
+
const missingFileRows = statusInfo.rows.filter(
|
|
3142
|
+
(row) => row.type === "registry" && row.status === "missing-file",
|
|
3143
|
+
);
|
|
3144
|
+
const stdioRows = statusInfo.rows.filter((row) => row.type === "stdio");
|
|
3145
|
+
const hasHardIssues =
|
|
3146
|
+
invalidConfigs.length > 0 || mismatchRows.length > 0;
|
|
3147
|
+
const status = hasHardIssues
|
|
3148
|
+
? "issues"
|
|
3149
|
+
: stdioRows.length > 0
|
|
3150
|
+
? "warning"
|
|
3151
|
+
: "ok";
|
|
2325
3152
|
|
|
2326
|
-
|
|
3153
|
+
addDoctorCheck(report, {
|
|
3154
|
+
name: "mcp-registry",
|
|
3155
|
+
status,
|
|
3156
|
+
path: registryState.path,
|
|
3157
|
+
server_count: Object.keys(registryState.registry.servers || {})
|
|
3158
|
+
.length,
|
|
3159
|
+
rows: statusInfo.rows,
|
|
3160
|
+
invalid_configs: invalidConfigs.map((config) => ({
|
|
3161
|
+
file: config.filePath,
|
|
3162
|
+
error: config.parseError?.message || "parse error",
|
|
3163
|
+
})),
|
|
3164
|
+
...(stdioRows.length > 0
|
|
3165
|
+
? { fix: "tfx doctor --fix 또는 tfx mcp sync" }
|
|
3166
|
+
: {}),
|
|
3167
|
+
});
|
|
2327
3168
|
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
["server", "target", "status", "config", "detail"],
|
|
2331
|
-
buildMcpStatusRows(statusInfo),
|
|
3169
|
+
ok(
|
|
3170
|
+
`registry 정상 (${Object.keys(registryState.registry.servers || {}).length}개 server)`,
|
|
2332
3171
|
);
|
|
2333
|
-
} else {
|
|
2334
|
-
info("등록된 MCP server 없음");
|
|
2335
|
-
}
|
|
2336
3172
|
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
if (row.actualUrl) info(`actual ${row.actualUrl}`);
|
|
2346
|
-
}
|
|
3173
|
+
if (statusInfo.rows.length > 0) {
|
|
3174
|
+
renderTable(
|
|
3175
|
+
["server", "target", "status", "config", "detail"],
|
|
3176
|
+
buildMcpStatusRows(statusInfo),
|
|
3177
|
+
);
|
|
3178
|
+
} else {
|
|
3179
|
+
info("등록된 MCP server 없음");
|
|
3180
|
+
}
|
|
2347
3181
|
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
3182
|
+
for (const config of invalidConfigs) {
|
|
3183
|
+
fail(`${config.label}: 설정 파싱 실패`);
|
|
3184
|
+
info(
|
|
3185
|
+
`${formatPathForDisplay(config.filePath)} — ${config.parseError.message}`,
|
|
3186
|
+
);
|
|
3187
|
+
}
|
|
2351
3188
|
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
3189
|
+
for (const row of mismatchRows) {
|
|
3190
|
+
warn(`${row.label}: ${row.name} URL 불일치`);
|
|
3191
|
+
info(`expected ${row.expectedUrl}`);
|
|
3192
|
+
if (row.actualUrl) info(`actual ${row.actualUrl}`);
|
|
3193
|
+
}
|
|
2355
3194
|
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
for (const row of stdioRows) {
|
|
2361
|
-
info(`${row.label}: ${row.name}${row.command ? ` (${row.command})` : ""}`);
|
|
3195
|
+
for (const row of missingFileRows) {
|
|
3196
|
+
info(
|
|
3197
|
+
`${row.label}: ${row.name} 미배치 (${formatPathForDisplay(row.filePath)})`,
|
|
3198
|
+
);
|
|
2362
3199
|
}
|
|
2363
|
-
}
|
|
2364
3200
|
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
}
|
|
2369
|
-
}
|
|
3201
|
+
for (const row of missingRows) {
|
|
3202
|
+
info(`${row.label}: ${row.name} 누락`);
|
|
3203
|
+
}
|
|
2370
3204
|
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
{
|
|
2374
|
-
const srcRoute = join(PKG_ROOT, "scripts", "tfx-route.sh");
|
|
2375
|
-
const destRoute = join(CLAUDE_DIR, "scripts", "tfx-route.sh");
|
|
2376
|
-
if (existsSync(srcRoute) && existsSync(destRoute)) {
|
|
2377
|
-
const srcHash = readFileSync(srcRoute, "utf8").length;
|
|
2378
|
-
const destHash = readFileSync(destRoute, "utf8").length;
|
|
2379
|
-
const srcContent = readFileSync(srcRoute, "utf8");
|
|
2380
|
-
const destContent = readFileSync(destRoute, "utf8");
|
|
2381
|
-
if (srcContent === destContent) {
|
|
2382
|
-
addDoctorCheck(report, { name: "route-sync", status: "ok" });
|
|
2383
|
-
ok("프로젝트 소스와 설치본 일치");
|
|
2384
|
-
} else {
|
|
2385
|
-
addDoctorCheck(report, { name: "route-sync", status: "issues", fix: "tfx setup" });
|
|
2386
|
-
warn("tfx-route.sh 프로젝트 소스와 설치본 불일치");
|
|
2387
|
-
info(`소스: ${srcRoute} (${srcHash}B) / 설치: ${destRoute} (${destHash}B)`);
|
|
2388
|
-
if (fix) {
|
|
2389
|
-
copyFileSync(srcRoute, destRoute);
|
|
2390
|
-
ok("tfx-route.sh 동기화 완료");
|
|
3205
|
+
if (stdioRows.length === 0) {
|
|
3206
|
+
ok("미등록 stdio MCP 없음");
|
|
2391
3207
|
} else {
|
|
2392
|
-
|
|
3208
|
+
warn(`${stdioRows.length}개 미등록 stdio MCP 감지`);
|
|
3209
|
+
for (const row of stdioRows) {
|
|
3210
|
+
info(
|
|
3211
|
+
`${row.label}: ${row.name}${row.command ? ` (${row.command})` : ""}`,
|
|
3212
|
+
);
|
|
3213
|
+
}
|
|
2393
3214
|
}
|
|
3215
|
+
|
|
3216
|
+
issues += invalidConfigs.length;
|
|
3217
|
+
issues += mismatchRows.length;
|
|
3218
|
+
issues += stdioRows.length;
|
|
2394
3219
|
}
|
|
2395
|
-
} else if (existsSync(srcRoute) && !existsSync(destRoute)) {
|
|
2396
|
-
addDoctorCheck(report, { name: "route-sync", status: "missing", fix: "tfx setup" });
|
|
2397
|
-
fail("설치본 없음");
|
|
2398
|
-
issues++;
|
|
2399
|
-
} else {
|
|
2400
|
-
addDoctorCheck(report, { name: "route-sync", status: "ok" });
|
|
2401
|
-
ok("소스 없음 (npm 패키지 모드)");
|
|
2402
3220
|
}
|
|
2403
|
-
}
|
|
2404
|
-
|
|
2405
|
-
// ── Hook Coverage (hook-registry vs settings.json) ──
|
|
2406
|
-
section("Hook Coverage");
|
|
2407
|
-
{
|
|
2408
|
-
const registryPath = join(PKG_ROOT, "hooks", "hook-registry.json");
|
|
2409
|
-
const settingsPath = join(CLAUDE_DIR, "settings.json");
|
|
2410
|
-
const managedHooks = getManagedRegistryHooks(registryPath);
|
|
2411
3221
|
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
try {
|
|
2427
|
-
settings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
2428
|
-
} catch (error) {
|
|
2429
|
-
const unreadableCoverage = {
|
|
2430
|
-
total: managedHooks.length,
|
|
2431
|
-
registered: 0,
|
|
2432
|
-
missing: managedHooks.map((spec) => toHookCoverageName(spec.fileName, spec.id)),
|
|
2433
|
-
};
|
|
2434
|
-
report.hook_coverage = unreadableCoverage;
|
|
3222
|
+
// ── Route Script 정합성 ──
|
|
3223
|
+
section("Route Script Sync");
|
|
3224
|
+
{
|
|
3225
|
+
const srcRoute = join(PKG_ROOT, "scripts", "tfx-route.sh");
|
|
3226
|
+
const destRoute = join(CLAUDE_DIR, "scripts", "tfx-route.sh");
|
|
3227
|
+
if (existsSync(srcRoute) && existsSync(destRoute)) {
|
|
3228
|
+
const srcHash = readFileSync(srcRoute, "utf8").length;
|
|
3229
|
+
const destHash = readFileSync(destRoute, "utf8").length;
|
|
3230
|
+
const srcContent = readFileSync(srcRoute, "utf8");
|
|
3231
|
+
const destContent = readFileSync(destRoute, "utf8");
|
|
3232
|
+
if (srcContent === destContent) {
|
|
3233
|
+
addDoctorCheck(report, { name: "route-sync", status: "ok" });
|
|
3234
|
+
ok("프로젝트 소스와 설치본 일치");
|
|
3235
|
+
} else {
|
|
2435
3236
|
addDoctorCheck(report, {
|
|
2436
|
-
name: "
|
|
2437
|
-
status: "
|
|
2438
|
-
|
|
2439
|
-
registered: unreadableCoverage.registered,
|
|
2440
|
-
missing: unreadableCoverage.missing,
|
|
2441
|
-
fix: "settings.json 문법을 수정하거나 tfx setup을 다시 실행하세요.",
|
|
3237
|
+
name: "route-sync",
|
|
3238
|
+
status: "issues",
|
|
3239
|
+
fix: "tfx setup",
|
|
2442
3240
|
});
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
3241
|
+
warn("tfx-route.sh 프로젝트 소스와 설치본 불일치");
|
|
3242
|
+
info(
|
|
3243
|
+
`소스: ${srcRoute} (${srcHash}B) / 설치: ${destRoute} (${destHash}B)`,
|
|
3244
|
+
);
|
|
3245
|
+
if (fix) {
|
|
3246
|
+
copyFileSync(srcRoute, destRoute);
|
|
3247
|
+
ok("tfx-route.sh 동기화 완료");
|
|
3248
|
+
} else {
|
|
3249
|
+
issues++;
|
|
3250
|
+
}
|
|
2446
3251
|
}
|
|
3252
|
+
} else if (existsSync(srcRoute) && !existsSync(destRoute)) {
|
|
3253
|
+
addDoctorCheck(report, {
|
|
3254
|
+
name: "route-sync",
|
|
3255
|
+
status: "missing",
|
|
3256
|
+
fix: "tfx setup",
|
|
3257
|
+
});
|
|
3258
|
+
fail("설치본 없음");
|
|
3259
|
+
issues++;
|
|
3260
|
+
} else {
|
|
3261
|
+
addDoctorCheck(report, { name: "route-sync", status: "ok" });
|
|
3262
|
+
ok("소스 없음 (npm 패키지 모드)");
|
|
2447
3263
|
}
|
|
3264
|
+
}
|
|
2448
3265
|
|
|
2449
|
-
|
|
2450
|
-
|
|
3266
|
+
// ── Hook Coverage (hook-registry vs settings.json) ──
|
|
3267
|
+
section("Hook Coverage");
|
|
3268
|
+
{
|
|
3269
|
+
const registryPath = join(PKG_ROOT, "hooks", "hook-registry.json");
|
|
3270
|
+
const settingsPath = join(CLAUDE_DIR, "settings.json");
|
|
3271
|
+
const managedHooks = getManagedRegistryHooks(registryPath);
|
|
2451
3272
|
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
3273
|
+
if (managedHooks.length === 0) {
|
|
3274
|
+
addDoctorCheck(report, {
|
|
3275
|
+
name: "hook-coverage",
|
|
3276
|
+
status: "invalid",
|
|
3277
|
+
total: 0,
|
|
3278
|
+
registered: 0,
|
|
3279
|
+
missing: [],
|
|
3280
|
+
fix: "hook-registry.json을 확인하세요.",
|
|
3281
|
+
});
|
|
3282
|
+
warn("hook-registry.json에서 관리 대상 훅을 찾지 못했습니다.");
|
|
3283
|
+
issues++;
|
|
3284
|
+
} else {
|
|
3285
|
+
let settings = {};
|
|
3286
|
+
if (existsSync(settingsPath)) {
|
|
3287
|
+
try {
|
|
3288
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
3289
|
+
} catch (error) {
|
|
3290
|
+
const unreadableCoverage = {
|
|
3291
|
+
total: managedHooks.length,
|
|
3292
|
+
registered: 0,
|
|
3293
|
+
missing: managedHooks.map((spec) =>
|
|
3294
|
+
toHookCoverageName(spec.fileName, spec.id),
|
|
3295
|
+
),
|
|
3296
|
+
};
|
|
3297
|
+
report.hook_coverage = unreadableCoverage;
|
|
3298
|
+
addDoctorCheck(report, {
|
|
3299
|
+
name: "hook-coverage",
|
|
3300
|
+
status: "invalid",
|
|
3301
|
+
total: unreadableCoverage.total,
|
|
3302
|
+
registered: unreadableCoverage.registered,
|
|
3303
|
+
missing: unreadableCoverage.missing,
|
|
3304
|
+
fix: "settings.json 문법을 수정하거나 tfx setup을 다시 실행하세요.",
|
|
3305
|
+
});
|
|
3306
|
+
fail(`settings.json 파싱 실패: ${error.message}`);
|
|
3307
|
+
issues++;
|
|
3308
|
+
settings = null;
|
|
2468
3309
|
}
|
|
2469
3310
|
}
|
|
2470
3311
|
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
3312
|
+
if (settings) {
|
|
3313
|
+
let coverage = computeHookCoverage(settings, managedHooks);
|
|
3314
|
+
|
|
3315
|
+
if (coverage.missing.length > 0 && fix) {
|
|
3316
|
+
const hookFixResult = ensureHooksInSettings({
|
|
3317
|
+
settingsPath,
|
|
3318
|
+
registryPath,
|
|
3319
|
+
});
|
|
3320
|
+
if (hookFixResult.ok) {
|
|
3321
|
+
if (hookFixResult.changed) {
|
|
3322
|
+
ok(`누락 훅 ${hookFixResult.added.length}개 자동 등록됨`);
|
|
3323
|
+
} else {
|
|
3324
|
+
info("누락 훅 자동 등록: 변경 사항 없음");
|
|
3325
|
+
}
|
|
3326
|
+
try {
|
|
3327
|
+
const fixedSettings = JSON.parse(
|
|
3328
|
+
readFileSync(settingsPath, "utf8"),
|
|
2482
3329
|
);
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
Array.isArray(e?.hooks) &&
|
|
2488
|
-
e.hooks.some((h) => typeof h?.command === "string" && h.command.includes("hook-orchestrator")),
|
|
3330
|
+
coverage = computeHookCoverage(fixedSettings, managedHooks);
|
|
3331
|
+
} catch (error) {
|
|
3332
|
+
warn(
|
|
3333
|
+
`자동 등록 후 settings.json 재검증 실패: ${error.message}`,
|
|
2489
3334
|
);
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
3335
|
+
}
|
|
3336
|
+
} else {
|
|
3337
|
+
warn(
|
|
3338
|
+
`누락 훅 자동 등록 실패: ${hookFixResult.reason || "unknown_error"}`,
|
|
3339
|
+
);
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
|
|
3343
|
+
// 중복 훅 감지 + 자동 수정 (orchestrator와 개별 훅이 동시 등록된 경우)
|
|
3344
|
+
if (coverage.duplicates && coverage.duplicates.length > 0) {
|
|
3345
|
+
if (fix) {
|
|
3346
|
+
try {
|
|
3347
|
+
const fixedSettings = JSON.parse(
|
|
3348
|
+
readFileSync(settingsPath, "utf8"),
|
|
3349
|
+
);
|
|
3350
|
+
let removed = 0;
|
|
3351
|
+
for (const [event, entries] of Object.entries(
|
|
3352
|
+
fixedSettings.hooks || {},
|
|
3353
|
+
)) {
|
|
3354
|
+
if (!Array.isArray(entries)) continue;
|
|
3355
|
+
const hasOrch = entries.some(
|
|
3356
|
+
(e) =>
|
|
3357
|
+
Array.isArray(e?.hooks) &&
|
|
3358
|
+
e.hooks.some(
|
|
3359
|
+
(h) =>
|
|
3360
|
+
typeof h?.command === "string" &&
|
|
3361
|
+
h.command.includes("hook-orchestrator"),
|
|
3362
|
+
),
|
|
2497
3363
|
);
|
|
2498
|
-
|
|
3364
|
+
if (!hasOrch) continue;
|
|
3365
|
+
// 패턴 A: orchestrator 없는 별도 엔트리 제거
|
|
3366
|
+
const before = entries.length;
|
|
3367
|
+
fixedSettings.hooks[event] = entries.filter(
|
|
3368
|
+
(e) =>
|
|
3369
|
+
Array.isArray(e?.hooks) &&
|
|
3370
|
+
e.hooks.some(
|
|
3371
|
+
(h) =>
|
|
3372
|
+
typeof h?.command === "string" &&
|
|
3373
|
+
h.command.includes("hook-orchestrator"),
|
|
3374
|
+
),
|
|
3375
|
+
);
|
|
3376
|
+
removed += before - fixedSettings.hooks[event].length;
|
|
3377
|
+
// 패턴 B: orchestrator 엔트리 내부의 개별 훅 제거
|
|
3378
|
+
for (const entry of fixedSettings.hooks[event]) {
|
|
3379
|
+
if (!Array.isArray(entry.hooks) || entry.hooks.length <= 1)
|
|
3380
|
+
continue;
|
|
3381
|
+
const beforeInner = entry.hooks.length;
|
|
3382
|
+
entry.hooks = entry.hooks.filter(
|
|
3383
|
+
(h) =>
|
|
3384
|
+
typeof h?.command === "string" &&
|
|
3385
|
+
h.command.includes("hook-orchestrator"),
|
|
3386
|
+
);
|
|
3387
|
+
removed += beforeInner - entry.hooks.length;
|
|
3388
|
+
}
|
|
2499
3389
|
}
|
|
3390
|
+
if (removed > 0) {
|
|
3391
|
+
writeFileSync(
|
|
3392
|
+
settingsPath,
|
|
3393
|
+
JSON.stringify(fixedSettings, null, 2) + "\n",
|
|
3394
|
+
"utf8",
|
|
3395
|
+
);
|
|
3396
|
+
ok(
|
|
3397
|
+
`중복 훅 ${removed}개 엔트리 제거됨 (orchestrator가 체이닝)`,
|
|
3398
|
+
);
|
|
3399
|
+
const rechecked = JSON.parse(
|
|
3400
|
+
readFileSync(settingsPath, "utf8"),
|
|
3401
|
+
);
|
|
3402
|
+
coverage = computeHookCoverage(rechecked, managedHooks);
|
|
3403
|
+
}
|
|
3404
|
+
} catch (error) {
|
|
3405
|
+
warn(`중복 훅 자동 제거 실패: ${error.message}`);
|
|
2500
3406
|
}
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
} catch (error) {
|
|
2508
|
-
warn(`중복 훅 자동 제거 실패: ${error.message}`);
|
|
3407
|
+
} else {
|
|
3408
|
+
warn(
|
|
3409
|
+
`중복 훅 ${coverage.duplicates.length}개 감지 (이중 실행됨): ${coverage.duplicates.join(", ")}`,
|
|
3410
|
+
);
|
|
3411
|
+
warn("tfx doctor --fix 로 자동 제거하세요.");
|
|
3412
|
+
issues += coverage.duplicates.length;
|
|
2509
3413
|
}
|
|
2510
|
-
} else {
|
|
2511
|
-
warn(`중복 훅 ${coverage.duplicates.length}개 감지 (이중 실행됨): ${coverage.duplicates.join(", ")}`);
|
|
2512
|
-
warn("tfx doctor --fix 로 자동 제거하세요.");
|
|
2513
|
-
issues += coverage.duplicates.length;
|
|
2514
3414
|
}
|
|
2515
|
-
}
|
|
2516
3415
|
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
3416
|
+
report.hook_coverage = coverage;
|
|
3417
|
+
const coverageStatus =
|
|
3418
|
+
coverage.missing.length === 0 &&
|
|
3419
|
+
(!coverage.duplicates || coverage.duplicates.length === 0)
|
|
3420
|
+
? "ok"
|
|
3421
|
+
: "issues";
|
|
3422
|
+
addDoctorCheck(report, {
|
|
3423
|
+
name: "hook-coverage",
|
|
3424
|
+
status: coverageStatus,
|
|
3425
|
+
total: coverage.total,
|
|
3426
|
+
registered: coverage.registered,
|
|
3427
|
+
missing: coverage.missing,
|
|
3428
|
+
duplicates: coverage.duplicates || [],
|
|
3429
|
+
...(coverage.missing.length > 0
|
|
3430
|
+
? { fix: "tfx doctor --fix 또는 tfx setup" }
|
|
3431
|
+
: {}),
|
|
3432
|
+
...(coverage.duplicates?.length > 0
|
|
3433
|
+
? { fix: "tfx doctor --fix 로 중복 훅 제거" }
|
|
3434
|
+
: {}),
|
|
3435
|
+
});
|
|
2529
3436
|
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
3437
|
+
if (
|
|
3438
|
+
coverage.missing.length === 0 &&
|
|
3439
|
+
(!coverage.duplicates || coverage.duplicates.length === 0)
|
|
3440
|
+
) {
|
|
3441
|
+
ok(
|
|
3442
|
+
`Hook Coverage: ${coverage.registered}/${coverage.total} registered`,
|
|
3443
|
+
);
|
|
3444
|
+
} else if (coverage.missing.length > 0) {
|
|
3445
|
+
fail(`Missing hooks: ${coverage.missing.join(", ")}`);
|
|
3446
|
+
issues += coverage.missing.length;
|
|
3447
|
+
}
|
|
2535
3448
|
}
|
|
2536
3449
|
}
|
|
2537
3450
|
}
|
|
2538
|
-
}
|
|
2539
3451
|
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
3452
|
+
// 결과
|
|
3453
|
+
console.log(`\n ${LINE}`);
|
|
3454
|
+
if (issues === 0) {
|
|
3455
|
+
console.log(` ${GREEN_BRIGHT}${BOLD}✓ 모든 검사 통과${RESET}\n`);
|
|
3456
|
+
} else {
|
|
3457
|
+
console.log(` ${YELLOW}${BOLD}⚠ ${issues}개 항목 확인 필요${RESET}\n`);
|
|
3458
|
+
}
|
|
2547
3459
|
report.issue_count = issues;
|
|
2548
3460
|
report.status = issues === 0 ? "ok" : "issues";
|
|
2549
3461
|
if (json) printJson(report);
|
|
@@ -2588,7 +3500,7 @@ function resolveGitUpdateUrl(repoDir) {
|
|
|
2588
3500
|
}
|
|
2589
3501
|
|
|
2590
3502
|
function resolveUpdateTargets({ installMode, pluginPath }) {
|
|
2591
|
-
const repoDir = installMode === "plugin" ?
|
|
3503
|
+
const repoDir = installMode === "plugin" ? pluginPath || PKG_ROOT : PKG_ROOT;
|
|
2592
3504
|
const gitUrl = resolveGitUpdateUrl(repoDir);
|
|
2593
3505
|
|
|
2594
3506
|
if (installMode === "npm-global" || installMode === "npm-local") {
|
|
@@ -2625,7 +3537,10 @@ async function cmdUpdate() {
|
|
|
2625
3537
|
}
|
|
2626
3538
|
|
|
2627
3539
|
// PKG_ROOT가 플러그인 캐시 내에 있으면 플러그인 모드
|
|
2628
|
-
if (
|
|
3540
|
+
if (
|
|
3541
|
+
installMode === "unknown" &&
|
|
3542
|
+
PKG_ROOT.includes(join(".claude", "plugins"))
|
|
3543
|
+
) {
|
|
2629
3544
|
installMode = "plugin";
|
|
2630
3545
|
pluginPath = PKG_ROOT;
|
|
2631
3546
|
}
|
|
@@ -2654,7 +3569,9 @@ async function cmdUpdate() {
|
|
|
2654
3569
|
installMode = "git-local";
|
|
2655
3570
|
}
|
|
2656
3571
|
|
|
2657
|
-
info(
|
|
3572
|
+
info(
|
|
3573
|
+
`검색: ${installMode === "plugin" ? "플러그인" : installMode === "npm-global" ? "npm global" : installMode === "npm-local" ? "npm local" : installMode === "git-local" ? "git 로컬 저장소" : "알 수 없음"} 설치 감지`,
|
|
3574
|
+
);
|
|
2658
3575
|
|
|
2659
3576
|
const networkTargets = resolveUpdateTargets({ installMode, pluginPath });
|
|
2660
3577
|
if (networkTargets.length > 0) {
|
|
@@ -2701,7 +3618,9 @@ async function cmdUpdate() {
|
|
|
2701
3618
|
if (stoppedHubInfo?.pid) {
|
|
2702
3619
|
info(`실행 중 hub 정지 (PID ${stoppedHubInfo.pid})`);
|
|
2703
3620
|
}
|
|
2704
|
-
const npmCmd = isDev
|
|
3621
|
+
const npmCmd = isDev
|
|
3622
|
+
? "npm install -g triflux@dev"
|
|
3623
|
+
: "npm install -g triflux@latest";
|
|
2705
3624
|
let result;
|
|
2706
3625
|
try {
|
|
2707
3626
|
result = execSync(npmCmd, {
|
|
@@ -2709,7 +3628,9 @@ async function cmdUpdate() {
|
|
|
2709
3628
|
timeout: 90000,
|
|
2710
3629
|
stdio: ["pipe", "pipe", "pipe"],
|
|
2711
3630
|
windowsHide: true,
|
|
2712
|
-
})
|
|
3631
|
+
})
|
|
3632
|
+
.trim()
|
|
3633
|
+
.split(/\r?\n/)[0];
|
|
2713
3634
|
} catch {
|
|
2714
3635
|
// Windows: 자기 자신의 파일 잠금으로 첫 시도 실패 가능 → --force 재시도
|
|
2715
3636
|
info("첫 시도 실패, --force 재시도 중...");
|
|
@@ -2718,22 +3639,30 @@ async function cmdUpdate() {
|
|
|
2718
3639
|
timeout: 90000,
|
|
2719
3640
|
stdio: ["pipe", "pipe", "pipe"],
|
|
2720
3641
|
windowsHide: true,
|
|
2721
|
-
})
|
|
3642
|
+
})
|
|
3643
|
+
.trim()
|
|
3644
|
+
.split(/\r?\n/)[0];
|
|
2722
3645
|
}
|
|
2723
3646
|
ok(`${npmCmd} — ${result || "완료"}`);
|
|
2724
3647
|
updated = true;
|
|
2725
3648
|
break;
|
|
2726
3649
|
}
|
|
2727
3650
|
case "npm-local": {
|
|
2728
|
-
const npmLocalCmd = isDev
|
|
3651
|
+
const npmLocalCmd = isDev
|
|
3652
|
+
? "npm install triflux@dev"
|
|
3653
|
+
: "npm update triflux";
|
|
2729
3654
|
const result = execSync(npmLocalCmd, {
|
|
2730
3655
|
encoding: "utf8",
|
|
2731
3656
|
timeout: 60000,
|
|
2732
3657
|
cwd: process.cwd(),
|
|
2733
3658
|
stdio: ["pipe", "pipe", "ignore"],
|
|
2734
3659
|
windowsHide: true,
|
|
2735
|
-
})
|
|
2736
|
-
|
|
3660
|
+
})
|
|
3661
|
+
.trim()
|
|
3662
|
+
.split(/\r?\n/)[0];
|
|
3663
|
+
ok(
|
|
3664
|
+
`${isDev ? "npm install triflux@dev" : "npm update triflux"} — ${result || "완료"}`,
|
|
3665
|
+
);
|
|
2737
3666
|
updated = true;
|
|
2738
3667
|
break;
|
|
2739
3668
|
}
|
|
@@ -2758,7 +3687,9 @@ async function cmdUpdate() {
|
|
|
2758
3687
|
info("업데이트 실패 후 hub 재기동 시도");
|
|
2759
3688
|
}
|
|
2760
3689
|
const stderr = e.stderr?.toString().trim();
|
|
2761
|
-
fail(
|
|
3690
|
+
fail(
|
|
3691
|
+
`업데이트 실패: ${e.message}${stderr ? `\n ${stderr.split(/\r?\n/)[0]}` : ""}`,
|
|
3692
|
+
);
|
|
2762
3693
|
return;
|
|
2763
3694
|
}
|
|
2764
3695
|
|
|
@@ -2768,7 +3699,9 @@ async function cmdUpdate() {
|
|
|
2768
3699
|
// 업데이트 후 새 버전 읽기
|
|
2769
3700
|
let newVer = oldVer;
|
|
2770
3701
|
try {
|
|
2771
|
-
const newPkg = JSON.parse(
|
|
3702
|
+
const newPkg = JSON.parse(
|
|
3703
|
+
readFileSync(join(PKG_ROOT, "package.json"), "utf8"),
|
|
3704
|
+
);
|
|
2772
3705
|
newVer = newPkg.version;
|
|
2773
3706
|
} catch {}
|
|
2774
3707
|
|
|
@@ -2785,22 +3718,37 @@ async function cmdUpdate() {
|
|
|
2785
3718
|
// stale 캐시 삭제
|
|
2786
3719
|
for (const name of ["tfx-preflight.json", "mcp-inventory.json"]) {
|
|
2787
3720
|
const p = join(cacheDir, name);
|
|
2788
|
-
if (existsSync(p)) {
|
|
3721
|
+
if (existsSync(p)) {
|
|
3722
|
+
try {
|
|
3723
|
+
unlinkSync(p);
|
|
3724
|
+
} catch {}
|
|
3725
|
+
}
|
|
2789
3726
|
}
|
|
2790
3727
|
// tmpdir 상태 파일 정리
|
|
2791
3728
|
for (const name of ["tfx-multi-state.json"]) {
|
|
2792
3729
|
const p = join(tmpdir(), name);
|
|
2793
|
-
if (existsSync(p)) {
|
|
3730
|
+
if (existsSync(p)) {
|
|
3731
|
+
try {
|
|
3732
|
+
unlinkSync(p);
|
|
3733
|
+
} catch {}
|
|
3734
|
+
}
|
|
2794
3735
|
}
|
|
2795
3736
|
|
|
2796
3737
|
// preflight 캐시 재생성
|
|
2797
3738
|
const preflightScript = join(PKG_ROOT, "scripts", "preflight-cache.mjs");
|
|
2798
3739
|
if (existsSync(preflightScript)) {
|
|
2799
3740
|
try {
|
|
2800
|
-
execSync(`node "${preflightScript}"`, {
|
|
3741
|
+
execSync(`node "${preflightScript}"`, {
|
|
3742
|
+
encoding: "utf8",
|
|
3743
|
+
timeout: 15000,
|
|
3744
|
+
windowsHide: true,
|
|
3745
|
+
stdio: "pipe",
|
|
3746
|
+
});
|
|
2801
3747
|
ok("preflight 캐시 재생성 완료");
|
|
2802
3748
|
} catch (e) {
|
|
2803
|
-
warn(
|
|
3749
|
+
warn(
|
|
3750
|
+
`preflight 캐시 재생성 실패: ${e.message?.split(/\r?\n/)[0] || "unknown"}`,
|
|
3751
|
+
);
|
|
2804
3752
|
}
|
|
2805
3753
|
}
|
|
2806
3754
|
|
|
@@ -2808,10 +3756,17 @@ async function cmdUpdate() {
|
|
|
2808
3756
|
const mcpCheckScript = join(PKG_ROOT, "scripts", "mcp-check.mjs");
|
|
2809
3757
|
if (existsSync(mcpCheckScript)) {
|
|
2810
3758
|
try {
|
|
2811
|
-
execSync(`node "${mcpCheckScript}"`, {
|
|
3759
|
+
execSync(`node "${mcpCheckScript}"`, {
|
|
3760
|
+
encoding: "utf8",
|
|
3761
|
+
timeout: 10000,
|
|
3762
|
+
windowsHide: true,
|
|
3763
|
+
stdio: "pipe",
|
|
3764
|
+
});
|
|
2812
3765
|
ok("MCP 인벤토리 캐시 재생성 완료");
|
|
2813
3766
|
} catch (e) {
|
|
2814
|
-
warn(
|
|
3767
|
+
warn(
|
|
3768
|
+
`MCP 인벤토리 재생성 실패: ${e.message?.split(/\r?\n/)[0] || "unknown"}`,
|
|
3769
|
+
);
|
|
2815
3770
|
}
|
|
2816
3771
|
}
|
|
2817
3772
|
}
|
|
@@ -2820,10 +3775,22 @@ async function cmdUpdate() {
|
|
|
2820
3775
|
console.log(`\n${CYAN}── 무결성 검증 ──${RESET}`);
|
|
2821
3776
|
{
|
|
2822
3777
|
const criticalFiles = [
|
|
2823
|
-
{
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
3778
|
+
{
|
|
3779
|
+
path: join(PKG_ROOT, "hooks", "hook-orchestrator.mjs"),
|
|
3780
|
+
label: "hook-orchestrator",
|
|
3781
|
+
},
|
|
3782
|
+
{
|
|
3783
|
+
path: join(PKG_ROOT, "hooks", "hook-registry.json"),
|
|
3784
|
+
label: "hook-registry",
|
|
3785
|
+
},
|
|
3786
|
+
{
|
|
3787
|
+
path: join(PKG_ROOT, "hooks", "safety-guard.mjs"),
|
|
3788
|
+
label: "safety-guard",
|
|
3789
|
+
},
|
|
3790
|
+
{
|
|
3791
|
+
path: join(PKG_ROOT, "scripts", "keyword-detector.mjs"),
|
|
3792
|
+
label: "keyword-detector",
|
|
3793
|
+
},
|
|
2827
3794
|
{ path: join(PKG_ROOT, "scripts", "setup.mjs"), label: "setup" },
|
|
2828
3795
|
{ path: join(PKG_ROOT, "bin", "triflux.mjs"), label: "triflux CLI" },
|
|
2829
3796
|
];
|
|
@@ -2835,7 +3802,9 @@ async function cmdUpdate() {
|
|
|
2835
3802
|
}
|
|
2836
3803
|
}
|
|
2837
3804
|
if (missing > 0) {
|
|
2838
|
-
fail(
|
|
3805
|
+
fail(
|
|
3806
|
+
`핵심 파일 ${missing}개 누락 — npm install -g triflux@latest 재설치 필요`,
|
|
3807
|
+
);
|
|
2839
3808
|
} else {
|
|
2840
3809
|
ok(`핵심 파일 ${criticalFiles.length}개 확인 완료`);
|
|
2841
3810
|
}
|
|
@@ -2845,7 +3814,8 @@ async function cmdUpdate() {
|
|
|
2845
3814
|
console.log(`\n${CYAN}── CLAUDE.md 라우팅 동기화 ──${RESET}`);
|
|
2846
3815
|
{
|
|
2847
3816
|
const claudeRoutingResults = syncClaudeRoutingSectionsForCli();
|
|
2848
|
-
const claudeRoutingSummary =
|
|
3817
|
+
const claudeRoutingSummary =
|
|
3818
|
+
getClaudeRoutingSyncSummary(claudeRoutingResults);
|
|
2849
3819
|
if (claudeRoutingSummary.changed > 0) {
|
|
2850
3820
|
ok(`CLAUDE.md 라우팅 ${claudeRoutingSummary.changed}개 파일 반영`);
|
|
2851
3821
|
} else if (claudeRoutingSummary.skipped > 0) {
|
|
@@ -2857,7 +3827,11 @@ async function cmdUpdate() {
|
|
|
2857
3827
|
|
|
2858
3828
|
// ── Post-update: 설정 동기화 ──
|
|
2859
3829
|
console.log(`\n${CYAN}── 설정 동기화 ──${RESET}`);
|
|
2860
|
-
cmdSetup({
|
|
3830
|
+
cmdSetup({
|
|
3831
|
+
fromUpdate: true,
|
|
3832
|
+
overrideVersion: newVer,
|
|
3833
|
+
skipClaudeMdSync: true,
|
|
3834
|
+
});
|
|
2861
3835
|
|
|
2862
3836
|
// ── Post-update: 훅 오케스트레이터 적용 ──
|
|
2863
3837
|
{
|
|
@@ -2871,10 +3845,14 @@ async function cmdUpdate() {
|
|
|
2871
3845
|
}).trim();
|
|
2872
3846
|
const parsed = JSON.parse(result);
|
|
2873
3847
|
if (parsed?.status === "applied") {
|
|
2874
|
-
ok(
|
|
3848
|
+
ok(
|
|
3849
|
+
`훅 오케스트레이터 적용 (${parsed.events?.length || 0}개 이벤트)`,
|
|
3850
|
+
);
|
|
2875
3851
|
}
|
|
2876
3852
|
} catch (e) {
|
|
2877
|
-
warn(
|
|
3853
|
+
warn(
|
|
3854
|
+
`훅 오케스트레이터 적용 실패: ${e.message?.split(/\r?\n/)[0] || "unknown"}`,
|
|
3855
|
+
);
|
|
2878
3856
|
warn("tfx hooks apply 로 수동 적용하세요.");
|
|
2879
3857
|
}
|
|
2880
3858
|
} else {
|
|
@@ -2914,7 +3892,9 @@ function cmdList(options = {}) {
|
|
|
2914
3892
|
skillAliases.push({ alias, source, installed: existsSync(dst) });
|
|
2915
3893
|
}
|
|
2916
3894
|
|
|
2917
|
-
const pkgNames = new Set(
|
|
3895
|
+
const pkgNames = new Set(
|
|
3896
|
+
existsSync(pluginSkills) ? readdirSync(pluginSkills) : [],
|
|
3897
|
+
);
|
|
2918
3898
|
if (existsSync(installedSkills)) {
|
|
2919
3899
|
for (const name of readdirSync(installedSkills).sort()) {
|
|
2920
3900
|
if (pkgNames.has(name) || aliasNames.has(name)) continue;
|
|
@@ -2942,7 +3922,9 @@ function cmdList(options = {}) {
|
|
|
2942
3922
|
if (skill.installed) {
|
|
2943
3923
|
console.log(` ${GREEN_BRIGHT}✓${RESET} ${BOLD}${skill.name}${RESET}`);
|
|
2944
3924
|
} else {
|
|
2945
|
-
console.log(
|
|
3925
|
+
console.log(
|
|
3926
|
+
` ${RED_BRIGHT}✗${RESET} ${DIM}${skill.name}${RESET} ${GRAY}(미설치)${RESET}`,
|
|
3927
|
+
);
|
|
2946
3928
|
}
|
|
2947
3929
|
}
|
|
2948
3930
|
|
|
@@ -2955,9 +3937,13 @@ function cmdList(options = {}) {
|
|
|
2955
3937
|
if (skillAliases.length > 0) {
|
|
2956
3938
|
section("호환 alias");
|
|
2957
3939
|
for (const entry of skillAliases) {
|
|
2958
|
-
const icon = entry.installed
|
|
3940
|
+
const icon = entry.installed
|
|
3941
|
+
? `${GREEN_BRIGHT}↳${RESET}`
|
|
3942
|
+
: `${RED_BRIGHT}↳${RESET}`;
|
|
2959
3943
|
const status = entry.installed ? "" : ` ${GRAY}(미설치)${RESET}`;
|
|
2960
|
-
console.log(
|
|
3944
|
+
console.log(
|
|
3945
|
+
` ${icon} ${BOLD}${entry.alias}${RESET} ${GRAY}→ ${entry.source}${RESET}${status}`,
|
|
3946
|
+
);
|
|
2961
3947
|
}
|
|
2962
3948
|
}
|
|
2963
3949
|
|
|
@@ -2978,7 +3964,9 @@ function cmdVersion(options = {}) {
|
|
|
2978
3964
|
});
|
|
2979
3965
|
return;
|
|
2980
3966
|
}
|
|
2981
|
-
console.log(
|
|
3967
|
+
console.log(
|
|
3968
|
+
`\n ${AMBER}${BOLD}⬡ triflux${RESET} ${WHITE_BRIGHT}v${PKG.version}${RESET}`,
|
|
3969
|
+
);
|
|
2982
3970
|
if (routeVer) console.log(` ${GRAY}tfx-route${RESET} v${routeVer}`);
|
|
2983
3971
|
if (hudVer) console.log(` ${GRAY}hud${RESET} v${hudVer}`);
|
|
2984
3972
|
console.log("");
|
|
@@ -3023,7 +4011,7 @@ function cmdHandoff(args = [], options = {}) {
|
|
|
3023
4011
|
throw createCliError("--decision 값이 필요합니다", {
|
|
3024
4012
|
exitCode: EXIT_ARG_ERROR,
|
|
3025
4013
|
reason: "argError",
|
|
3026
|
-
fix:
|
|
4014
|
+
fix: 'tfx handoff --decision "결정사항"',
|
|
3027
4015
|
});
|
|
3028
4016
|
}
|
|
3029
4017
|
parsed.decisions.push(next);
|
|
@@ -3118,7 +4106,11 @@ function cmdSchema(args = []) {
|
|
|
3118
4106
|
$schema: bundle.$schema,
|
|
3119
4107
|
title: "Triflux CLI Schema Bundle",
|
|
3120
4108
|
global_options: [
|
|
3121
|
-
{
|
|
4109
|
+
{
|
|
4110
|
+
name: "--json",
|
|
4111
|
+
type: "boolean",
|
|
4112
|
+
description: "지원 커맨드의 출력을 JSON으로 전환",
|
|
4113
|
+
},
|
|
3122
4114
|
],
|
|
3123
4115
|
commands: CLI_COMMAND_SCHEMAS,
|
|
3124
4116
|
hub_tools: bundle,
|
|
@@ -3154,7 +4146,9 @@ function cmdSchema(args = []) {
|
|
|
3154
4146
|
|
|
3155
4147
|
function cmdMcp(args = [], options = {}) {
|
|
3156
4148
|
const { json = false } = options;
|
|
3157
|
-
const sub = String(args[0] || "list")
|
|
4149
|
+
const sub = String(args[0] || "list")
|
|
4150
|
+
.trim()
|
|
4151
|
+
.toLowerCase();
|
|
3158
4152
|
|
|
3159
4153
|
if (sub === "help" || sub === "--help" || sub === "-h") {
|
|
3160
4154
|
console.log(`
|
|
@@ -3175,7 +4169,8 @@ function cmdMcp(args = [], options = {}) {
|
|
|
3175
4169
|
if (json) {
|
|
3176
4170
|
printJson({
|
|
3177
4171
|
registry_path: registryState.path,
|
|
3178
|
-
server_count: Object.keys(registryState.registry.servers || {})
|
|
4172
|
+
server_count: Object.keys(registryState.registry.servers || {})
|
|
4173
|
+
.length,
|
|
3179
4174
|
rows: statusInfo.rows,
|
|
3180
4175
|
configs: statusInfo.configs.map((config) => ({
|
|
3181
4176
|
file: config.filePath,
|
|
@@ -3191,7 +4186,9 @@ function cmdMcp(args = [], options = {}) {
|
|
|
3191
4186
|
console.log(` ${LINE}`);
|
|
3192
4187
|
section("Registry");
|
|
3193
4188
|
info(formatPathForDisplay(registryState.path));
|
|
3194
|
-
ok(
|
|
4189
|
+
ok(
|
|
4190
|
+
`${Object.keys(registryState.registry.servers || {}).length}개 server 등록됨`,
|
|
4191
|
+
);
|
|
3195
4192
|
if (statusInfo.rows.length === 0) {
|
|
3196
4193
|
info("표시할 MCP 상태 없음");
|
|
3197
4194
|
} else {
|
|
@@ -3222,7 +4219,8 @@ function cmdMcp(args = [], options = {}) {
|
|
|
3222
4219
|
const label = `${action.label} ${DIM}(${formatPathForDisplay(action.filePath)})${RESET}`;
|
|
3223
4220
|
if (action.status === "updated") ok(`${label} → updated`);
|
|
3224
4221
|
else if (action.status === "warning") warn(`${label} → warning`);
|
|
3225
|
-
else if (action.status === "invalid-config")
|
|
4222
|
+
else if (action.status === "invalid-config")
|
|
4223
|
+
fail(`${label} → invalid-config`);
|
|
3226
4224
|
else info(`${stripAnsi(label)} → ${action.status}`);
|
|
3227
4225
|
}
|
|
3228
4226
|
console.log("");
|
|
@@ -3248,7 +4246,9 @@ function cmdMcp(args = [], options = {}) {
|
|
|
3248
4246
|
}
|
|
3249
4247
|
|
|
3250
4248
|
const normalizedUrl = (() => {
|
|
3251
|
-
try {
|
|
4249
|
+
try {
|
|
4250
|
+
return new URL(url).toString();
|
|
4251
|
+
} catch {
|
|
3252
4252
|
throw createCliError(`Invalid MCP URL: ${url}`, {
|
|
3253
4253
|
exitCode: EXIT_ARG_ERROR,
|
|
3254
4254
|
reason: "argError",
|
|
@@ -3259,7 +4259,9 @@ function cmdMcp(args = [], options = {}) {
|
|
|
3259
4259
|
|
|
3260
4260
|
const server = addRegistryServer(name, normalizedUrl);
|
|
3261
4261
|
const registryState = ensureValidRegistryState();
|
|
3262
|
-
const syncResult = syncRegistryTargets({
|
|
4262
|
+
const syncResult = syncRegistryTargets({
|
|
4263
|
+
registry: registryState.registry,
|
|
4264
|
+
});
|
|
3263
4265
|
if (json) {
|
|
3264
4266
|
printJson({
|
|
3265
4267
|
name,
|
|
@@ -3273,7 +4275,9 @@ function cmdMcp(args = [], options = {}) {
|
|
|
3273
4275
|
console.log(` ${LINE}`);
|
|
3274
4276
|
ok(`${name} 등록됨`);
|
|
3275
4277
|
info(normalizedUrl);
|
|
3276
|
-
const updated = syncResult.actions.filter(
|
|
4278
|
+
const updated = syncResult.actions.filter(
|
|
4279
|
+
(action) => action.status === "updated",
|
|
4280
|
+
).length;
|
|
3277
4281
|
info(`동기화 반영: ${updated}개`);
|
|
3278
4282
|
console.log("");
|
|
3279
4283
|
return;
|
|
@@ -3291,7 +4295,9 @@ function cmdMcp(args = [], options = {}) {
|
|
|
3291
4295
|
|
|
3292
4296
|
ensureValidRegistryState();
|
|
3293
4297
|
const removed = removeRegistryServer(name);
|
|
3294
|
-
const cleanup = removeServerFromTargets(name, {
|
|
4298
|
+
const cleanup = removeServerFromTargets(name, {
|
|
4299
|
+
targets: removed?.targets,
|
|
4300
|
+
});
|
|
3295
4301
|
if (json) {
|
|
3296
4302
|
printJson({
|
|
3297
4303
|
name,
|
|
@@ -3306,7 +4312,9 @@ function cmdMcp(args = [], options = {}) {
|
|
|
3306
4312
|
console.log(` ${LINE}`);
|
|
3307
4313
|
if (removed) ok(`${name} registry에서 제거됨`);
|
|
3308
4314
|
else warn(`${name} registry entry 없음`);
|
|
3309
|
-
const changed = cleanup.actions.filter(
|
|
4315
|
+
const changed = cleanup.actions.filter(
|
|
4316
|
+
(action) => action.status === "removed",
|
|
4317
|
+
).length;
|
|
3310
4318
|
info(`설정 제거 반영: ${changed}개`);
|
|
3311
4319
|
console.log("");
|
|
3312
4320
|
return;
|
|
@@ -3345,7 +4353,10 @@ function checkForUpdate() {
|
|
|
3345
4353
|
}).trim();
|
|
3346
4354
|
|
|
3347
4355
|
if (!existsSync(cacheDir)) mkdirSync(cacheDir, { recursive: true });
|
|
3348
|
-
writeFileSync(
|
|
4356
|
+
writeFileSync(
|
|
4357
|
+
cacheFile,
|
|
4358
|
+
JSON.stringify({ latest: result, timestamp: Date.now() }),
|
|
4359
|
+
);
|
|
3349
4360
|
|
|
3350
4361
|
return result !== PKG.version ? result : null;
|
|
3351
4362
|
} catch {
|
|
@@ -3404,8 +4415,21 @@ ${updateNotice}
|
|
|
3404
4415
|
async function cmdCodexTeam(args = []) {
|
|
3405
4416
|
const sub = String(args[0] || "").toLowerCase();
|
|
3406
4417
|
const passthrough = new Set([
|
|
3407
|
-
"status",
|
|
3408
|
-
"
|
|
4418
|
+
"status",
|
|
4419
|
+
"attach",
|
|
4420
|
+
"stop",
|
|
4421
|
+
"kill",
|
|
4422
|
+
"send",
|
|
4423
|
+
"list",
|
|
4424
|
+
"help",
|
|
4425
|
+
"--help",
|
|
4426
|
+
"-h",
|
|
4427
|
+
"tasks",
|
|
4428
|
+
"task",
|
|
4429
|
+
"focus",
|
|
4430
|
+
"interrupt",
|
|
4431
|
+
"control",
|
|
4432
|
+
"debug",
|
|
3409
4433
|
]);
|
|
3410
4434
|
|
|
3411
4435
|
if (sub === "help" || sub === "--help" || sub === "-h") {
|
|
@@ -3428,7 +4452,8 @@ async function cmdCodexTeam(args = []) {
|
|
|
3428
4452
|
const hasLead = args.includes("--lead");
|
|
3429
4453
|
const hasLayout = args.includes("--layout");
|
|
3430
4454
|
const isControl = passthrough.has(sub);
|
|
3431
|
-
const normalizedArgs =
|
|
4455
|
+
const normalizedArgs =
|
|
4456
|
+
isControl && args.length ? [sub, ...args.slice(1)] : args;
|
|
3432
4457
|
const inject = [];
|
|
3433
4458
|
if (!isControl && !hasLead) inject.push("--lead", "codex");
|
|
3434
4459
|
if (!isControl && !hasAgents) inject.push("--agents", "codex,codex");
|
|
@@ -3439,13 +4464,16 @@ async function cmdCodexTeam(args = []) {
|
|
|
3439
4464
|
const prevProfile = process.env.TFX_TEAM_PROFILE;
|
|
3440
4465
|
process.env.TFX_TEAM_PROFILE = "codex-team";
|
|
3441
4466
|
const { pathToFileURL } = await import("node:url");
|
|
3442
|
-
const { cmdTeam } = await import(
|
|
4467
|
+
const { cmdTeam } = await import(
|
|
4468
|
+
pathToFileURL(join(PKG_ROOT, "hub", "team", "cli", "index.mjs")).href
|
|
4469
|
+
);
|
|
3443
4470
|
process.argv = [prevArgv[0], prevArgv[1], "team", ...forwarded];
|
|
3444
4471
|
try {
|
|
3445
4472
|
await cmdTeam();
|
|
3446
4473
|
} finally {
|
|
3447
4474
|
process.argv = prevArgv;
|
|
3448
|
-
if (typeof prevProfile === "string")
|
|
4475
|
+
if (typeof prevProfile === "string")
|
|
4476
|
+
process.env.TFX_TEAM_PROFILE = prevProfile;
|
|
3449
4477
|
else delete process.env.TFX_TEAM_PROFILE;
|
|
3450
4478
|
}
|
|
3451
4479
|
}
|
|
@@ -3457,7 +4485,8 @@ async function checkHubRunning() {
|
|
|
3457
4485
|
try {
|
|
3458
4486
|
const cacheFile = join(homedir(), ".claude", "cache", "tfx-preflight.json");
|
|
3459
4487
|
const cached = JSON.parse(readFileSync(cacheFile, "utf8"));
|
|
3460
|
-
if (Date.now() - cached.timestamp < 3_600_000 && cached.hub?.ok)
|
|
4488
|
+
if (Date.now() - cached.timestamp < 3_600_000 && cached.hub?.ok)
|
|
4489
|
+
return true;
|
|
3461
4490
|
} catch {}
|
|
3462
4491
|
const port = Number(process.env.TFX_HUB_PORT || "27888");
|
|
3463
4492
|
try {
|
|
@@ -3468,7 +4497,9 @@ async function checkHubRunning() {
|
|
|
3468
4497
|
} catch {}
|
|
3469
4498
|
console.log("");
|
|
3470
4499
|
warn(`${AMBER}tfx-hub${RESET}가 실행되고 있지 않습니다.`);
|
|
3471
|
-
info(
|
|
4500
|
+
info(
|
|
4501
|
+
`Hub 없이 실행하면 Claude 네이티브 에이전트로 폴백되어 토큰이 소비됩니다.`,
|
|
4502
|
+
);
|
|
3472
4503
|
info(`Codex(무료) 위임을 활용하려면 먼저 Hub를 시작하세요:\n`);
|
|
3473
4504
|
console.log(` ${WHITE_BRIGHT}tfx hub start${RESET}\n`);
|
|
3474
4505
|
return false;
|
|
@@ -3490,7 +4521,9 @@ function stopHubForUpdate() {
|
|
|
3490
4521
|
info = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
3491
4522
|
process.kill(info.pid, 0);
|
|
3492
4523
|
} catch {
|
|
3493
|
-
try {
|
|
4524
|
+
try {
|
|
4525
|
+
unlinkSync(HUB_PID_FILE);
|
|
4526
|
+
} catch {}
|
|
3494
4527
|
return null;
|
|
3495
4528
|
}
|
|
3496
4529
|
|
|
@@ -3505,15 +4538,28 @@ function stopHubForUpdate() {
|
|
|
3505
4538
|
process.kill(info.pid, "SIGTERM");
|
|
3506
4539
|
}
|
|
3507
4540
|
} catch {
|
|
3508
|
-
try {
|
|
4541
|
+
try {
|
|
4542
|
+
process.kill(info.pid, "SIGKILL");
|
|
4543
|
+
} catch {}
|
|
3509
4544
|
}
|
|
3510
4545
|
|
|
3511
4546
|
// Windows에서 better-sqlite3.node 파일 핸들 해제 대기
|
|
3512
4547
|
// taskkill 후 프로세스 종료 + 파일 핸들 해제까지 최대 5초
|
|
3513
|
-
const sqliteNode = join(
|
|
4548
|
+
const sqliteNode = join(
|
|
4549
|
+
PKG_ROOT,
|
|
4550
|
+
"node_modules",
|
|
4551
|
+
"better-sqlite3",
|
|
4552
|
+
"build",
|
|
4553
|
+
"Release",
|
|
4554
|
+
"better_sqlite3.node",
|
|
4555
|
+
);
|
|
3514
4556
|
for (let i = 0; i < 10; i++) {
|
|
3515
4557
|
sleepMs(500);
|
|
3516
|
-
try {
|
|
4558
|
+
try {
|
|
4559
|
+
process.kill(info.pid, 0);
|
|
4560
|
+
} catch {
|
|
4561
|
+
break;
|
|
4562
|
+
}
|
|
3517
4563
|
}
|
|
3518
4564
|
// 파일 잠금 해제 확인 (Windows EBUSY 방지)
|
|
3519
4565
|
if (existsSync(sqliteNode)) {
|
|
@@ -3527,7 +4573,9 @@ function stopHubForUpdate() {
|
|
|
3527
4573
|
}
|
|
3528
4574
|
}
|
|
3529
4575
|
}
|
|
3530
|
-
try {
|
|
4576
|
+
try {
|
|
4577
|
+
unlinkSync(HUB_PID_FILE);
|
|
4578
|
+
} catch {}
|
|
3531
4579
|
return info;
|
|
3532
4580
|
}
|
|
3533
4581
|
|
|
@@ -3535,7 +4583,10 @@ function startHubAfterUpdate(info) {
|
|
|
3535
4583
|
if (!info) return false;
|
|
3536
4584
|
const serverPath = join(PKG_ROOT, "hub", "server.mjs");
|
|
3537
4585
|
if (!existsSync(serverPath)) return false;
|
|
3538
|
-
const port =
|
|
4586
|
+
const port =
|
|
4587
|
+
Number(info?.port) > 0
|
|
4588
|
+
? String(info.port)
|
|
4589
|
+
: String(process.env.TFX_HUB_PORT || "27888");
|
|
3539
4590
|
|
|
3540
4591
|
try {
|
|
3541
4592
|
const child = spawn(process.execPath, [serverPath], {
|
|
@@ -3558,14 +4609,24 @@ function autoRegisterMcp(mcpUrl, { codexEnabled = false } = {}) {
|
|
|
3558
4609
|
// Codex — config.json에 기본 disabled 엔트리로 등록
|
|
3559
4610
|
if (which("codex")) {
|
|
3560
4611
|
try {
|
|
3561
|
-
const result = ensureCodexHubServerConfig({
|
|
4612
|
+
const result = ensureCodexHubServerConfig({
|
|
4613
|
+
mcpUrl,
|
|
4614
|
+
createIfMissing: true,
|
|
4615
|
+
enabled: codexEnabled,
|
|
4616
|
+
});
|
|
3562
4617
|
if (!result.ok) throw new Error(result.reason || "unknown");
|
|
3563
4618
|
if (result.changed) {
|
|
3564
|
-
ok(
|
|
4619
|
+
ok(
|
|
4620
|
+
`Codex: config.json에 등록 완료 (${codexEnabled ? "enabled" : "기본 disabled"})`,
|
|
4621
|
+
);
|
|
3565
4622
|
} else {
|
|
3566
|
-
ok(
|
|
4623
|
+
ok(
|
|
4624
|
+
`Codex: 이미 등록됨 (${codexEnabled ? "enabled" : "기본 disabled"})`,
|
|
4625
|
+
);
|
|
3567
4626
|
}
|
|
3568
|
-
} catch (e) {
|
|
4627
|
+
} catch (e) {
|
|
4628
|
+
warn(`Codex 등록 실패: ${e.message}`);
|
|
4629
|
+
}
|
|
3569
4630
|
} else {
|
|
3570
4631
|
info("Codex: 미설치 (건너뜀)");
|
|
3571
4632
|
}
|
|
@@ -3576,7 +4637,8 @@ function autoRegisterMcp(mcpUrl, { codexEnabled = false } = {}) {
|
|
|
3576
4637
|
const geminiDir = join(homedir(), ".gemini");
|
|
3577
4638
|
const settingsFile = join(geminiDir, "settings.json");
|
|
3578
4639
|
let settings = {};
|
|
3579
|
-
if (existsSync(settingsFile))
|
|
4640
|
+
if (existsSync(settingsFile))
|
|
4641
|
+
settings = JSON.parse(readFileSync(settingsFile, "utf8"));
|
|
3580
4642
|
if (!settings.mcpServers) settings.mcpServers = {};
|
|
3581
4643
|
if (!settings.mcpServers["tfx-hub"]) {
|
|
3582
4644
|
settings.mcpServers["tfx-hub"] = { url: mcpUrl };
|
|
@@ -3586,7 +4648,9 @@ function autoRegisterMcp(mcpUrl, { codexEnabled = false } = {}) {
|
|
|
3586
4648
|
} else {
|
|
3587
4649
|
ok("Gemini: 이미 등록됨");
|
|
3588
4650
|
}
|
|
3589
|
-
} catch (e) {
|
|
4651
|
+
} catch (e) {
|
|
4652
|
+
warn(`Gemini 등록 실패: ${e.message}`);
|
|
4653
|
+
}
|
|
3590
4654
|
} else {
|
|
3591
4655
|
info("Gemini: 미설치 (건너뜀)");
|
|
3592
4656
|
}
|
|
@@ -3597,7 +4661,8 @@ function autoRegisterMcp(mcpUrl, { codexEnabled = false } = {}) {
|
|
|
3597
4661
|
if (!existsSync(claudeDir)) mkdirSync(claudeDir, { recursive: true });
|
|
3598
4662
|
const mcpJsonPath = join(claudeDir, "mcp.json");
|
|
3599
4663
|
let mcpJson = {};
|
|
3600
|
-
if (existsSync(mcpJsonPath))
|
|
4664
|
+
if (existsSync(mcpJsonPath))
|
|
4665
|
+
mcpJson = JSON.parse(readFileSync(mcpJsonPath, "utf8"));
|
|
3601
4666
|
if (!mcpJson.mcpServers) mcpJson.mcpServers = {};
|
|
3602
4667
|
if (!mcpJson.mcpServers["tfx-hub"]) {
|
|
3603
4668
|
mcpJson.mcpServers["tfx-hub"] = { type: "url", url: mcpUrl };
|
|
@@ -3606,20 +4671,32 @@ function autoRegisterMcp(mcpUrl, { codexEnabled = false } = {}) {
|
|
|
3606
4671
|
} else {
|
|
3607
4672
|
ok("Claude: 이미 등록됨");
|
|
3608
4673
|
}
|
|
3609
|
-
} catch (e) {
|
|
4674
|
+
} catch (e) {
|
|
4675
|
+
warn(`Claude 등록 실패: ${e.message}`);
|
|
4676
|
+
}
|
|
3610
4677
|
}
|
|
3611
4678
|
|
|
3612
4679
|
async function cmdHub(args = [], options = {}) {
|
|
3613
4680
|
const { json = false } = options;
|
|
3614
4681
|
const sub = args[0] || "status";
|
|
3615
4682
|
const defaultPortRaw = Number(process.env.TFX_HUB_PORT || "27888");
|
|
3616
|
-
const probePort =
|
|
3617
|
-
|
|
3618
|
-
|
|
4683
|
+
const probePort =
|
|
4684
|
+
Number.isFinite(defaultPortRaw) && defaultPortRaw > 0
|
|
4685
|
+
? defaultPortRaw
|
|
4686
|
+
: 27888;
|
|
4687
|
+
const formatHostForUrl = (host) => (host.includes(":") ? `[${host}]` : host);
|
|
4688
|
+
const probeHubStatus = async (
|
|
4689
|
+
host = "127.0.0.1",
|
|
4690
|
+
port = probePort,
|
|
4691
|
+
timeoutMs = 3000,
|
|
4692
|
+
) => {
|
|
3619
4693
|
try {
|
|
3620
|
-
const res = await fetch(
|
|
3621
|
-
|
|
3622
|
-
|
|
4694
|
+
const res = await fetch(
|
|
4695
|
+
`http://${formatHostForUrl(host)}:${port}/status`,
|
|
4696
|
+
{
|
|
4697
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
4698
|
+
},
|
|
4699
|
+
);
|
|
3623
4700
|
if (!res.ok) return null;
|
|
3624
4701
|
const data = await res.json();
|
|
3625
4702
|
return data?.hub ? data : null;
|
|
@@ -3633,13 +4710,16 @@ async function cmdHub(args = [], options = {}) {
|
|
|
3633
4710
|
if (!Number.isFinite(pid) || pid <= 0) return;
|
|
3634
4711
|
try {
|
|
3635
4712
|
mkdirSync(HUB_PID_DIR, { recursive: true });
|
|
3636
|
-
writeFileSync(
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
4713
|
+
writeFileSync(
|
|
4714
|
+
HUB_PID_FILE,
|
|
4715
|
+
JSON.stringify({
|
|
4716
|
+
pid,
|
|
4717
|
+
port,
|
|
4718
|
+
host: defaultHost,
|
|
4719
|
+
url: `http://${formatHostForUrl(defaultHost)}:${port}/mcp`,
|
|
4720
|
+
started: Date.now(),
|
|
4721
|
+
}),
|
|
4722
|
+
);
|
|
3643
4723
|
} catch {}
|
|
3644
4724
|
};
|
|
3645
4725
|
const emitHubStatus = (payload) => {
|
|
@@ -3656,11 +4736,15 @@ async function cmdHub(args = [], options = {}) {
|
|
|
3656
4736
|
const info = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
3657
4737
|
process.kill(info.pid, 0); // 프로세스 존재 확인
|
|
3658
4738
|
autoRegisterMcp(info.url, { codexEnabled: true });
|
|
3659
|
-
console.log(
|
|
4739
|
+
console.log(
|
|
4740
|
+
`\n ${YELLOW}⚠${RESET} hub 이미 실행 중 (PID ${info.pid}, ${info.url})\n`,
|
|
4741
|
+
);
|
|
3660
4742
|
return;
|
|
3661
4743
|
} catch {
|
|
3662
4744
|
// PID 파일 있지만 프로세스 없음 — 정리
|
|
3663
|
-
try {
|
|
4745
|
+
try {
|
|
4746
|
+
unlinkSync(HUB_PID_FILE);
|
|
4747
|
+
} catch {}
|
|
3664
4748
|
}
|
|
3665
4749
|
}
|
|
3666
4750
|
|
|
@@ -3688,7 +4772,10 @@ async function cmdHub(args = [], options = {}) {
|
|
|
3688
4772
|
let started = false;
|
|
3689
4773
|
const deadline = Date.now() + 3000;
|
|
3690
4774
|
while (Date.now() < deadline) {
|
|
3691
|
-
if (existsSync(HUB_PID_FILE)) {
|
|
4775
|
+
if (existsSync(HUB_PID_FILE)) {
|
|
4776
|
+
started = true;
|
|
4777
|
+
break;
|
|
4778
|
+
}
|
|
3692
4779
|
await new Promise((r) => setTimeout(r, 100));
|
|
3693
4780
|
}
|
|
3694
4781
|
|
|
@@ -3697,26 +4784,37 @@ async function cmdHub(args = [], options = {}) {
|
|
|
3697
4784
|
console.log(`\n ${GREEN_BRIGHT}✓${RESET} ${BOLD}tfx-hub 시작${RESET}`);
|
|
3698
4785
|
console.log(` URL: ${AMBER}${hubInfo.url}${RESET}`);
|
|
3699
4786
|
console.log(` PID: ${hubInfo.pid}`);
|
|
3700
|
-
console.log(
|
|
4787
|
+
console.log(
|
|
4788
|
+
` DB: ${DIM}${getPipelineStateDbPath(PKG_ROOT)}${RESET}`,
|
|
4789
|
+
);
|
|
3701
4790
|
console.log("");
|
|
3702
4791
|
autoRegisterMcp(hubInfo.url, { codexEnabled: true });
|
|
3703
4792
|
console.log("");
|
|
3704
4793
|
} else {
|
|
3705
4794
|
// 직접 포그라운드 모드로 안내
|
|
3706
|
-
console.log(
|
|
3707
|
-
|
|
4795
|
+
console.log(
|
|
4796
|
+
`\n ${YELLOW}⚠${RESET} 백그라운드 시작 실패 — 포그라운드로 실행:`,
|
|
4797
|
+
);
|
|
4798
|
+
console.log(
|
|
4799
|
+
` ${DIM}TFX_HUB_PORT=${port} node ${serverPath}${RESET}\n`,
|
|
4800
|
+
);
|
|
3708
4801
|
}
|
|
3709
4802
|
break;
|
|
3710
4803
|
}
|
|
3711
4804
|
|
|
3712
4805
|
case "stop": {
|
|
3713
4806
|
if (!existsSync(HUB_PID_FILE)) {
|
|
3714
|
-
const probed =
|
|
3715
|
-
|
|
4807
|
+
const probed =
|
|
4808
|
+
(await probeHubStatus("127.0.0.1", probePort, 1500)) ||
|
|
4809
|
+
(probePort === 27888
|
|
4810
|
+
? null
|
|
4811
|
+
: await probeHubStatus("127.0.0.1", 27888, 1500));
|
|
3716
4812
|
if (probed && Number.isFinite(Number(probed.pid))) {
|
|
3717
4813
|
try {
|
|
3718
4814
|
process.kill(Number(probed.pid), "SIGTERM");
|
|
3719
|
-
console.log(
|
|
4815
|
+
console.log(
|
|
4816
|
+
`\n ${GREEN_BRIGHT}✓${RESET} hub 종료됨 (PID ${probed.pid})${DIM} (probe)${RESET}\n`,
|
|
4817
|
+
);
|
|
3720
4818
|
return;
|
|
3721
4819
|
} catch {}
|
|
3722
4820
|
}
|
|
@@ -3726,10 +4824,16 @@ async function cmdHub(args = [], options = {}) {
|
|
|
3726
4824
|
try {
|
|
3727
4825
|
const info = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
3728
4826
|
process.kill(info.pid, "SIGTERM");
|
|
3729
|
-
try {
|
|
3730
|
-
|
|
4827
|
+
try {
|
|
4828
|
+
unlinkSync(HUB_PID_FILE);
|
|
4829
|
+
} catch {}
|
|
4830
|
+
console.log(
|
|
4831
|
+
`\n ${GREEN_BRIGHT}✓${RESET} hub 종료됨 (PID ${info.pid})\n`,
|
|
4832
|
+
);
|
|
3731
4833
|
} catch (_e) {
|
|
3732
|
-
try {
|
|
4834
|
+
try {
|
|
4835
|
+
unlinkSync(HUB_PID_FILE);
|
|
4836
|
+
} catch {}
|
|
3733
4837
|
console.log(`\n ${DIM}hub 프로세스 없음 — PID 파일 정리됨${RESET}\n`);
|
|
3734
4838
|
}
|
|
3735
4839
|
break;
|
|
@@ -3739,43 +4843,76 @@ async function cmdHub(args = [], options = {}) {
|
|
|
3739
4843
|
if (!existsSync(HUB_PID_FILE)) {
|
|
3740
4844
|
const probed = await probeHubStatus();
|
|
3741
4845
|
if (!probed) {
|
|
3742
|
-
const fallback =
|
|
4846
|
+
const fallback =
|
|
4847
|
+
probePort === 27888
|
|
4848
|
+
? null
|
|
4849
|
+
: await probeHubStatus("127.0.0.1", 27888, 1500);
|
|
3743
4850
|
if (fallback) {
|
|
3744
4851
|
recoverPidFile(fallback, "127.0.0.1");
|
|
3745
|
-
if (
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
4852
|
+
if (
|
|
4853
|
+
emitHubStatus({
|
|
4854
|
+
status: "online",
|
|
4855
|
+
source: "default-port-probe",
|
|
4856
|
+
url: `http://127.0.0.1:${fallback.port || 27888}/mcp`,
|
|
4857
|
+
pid: fallback.pid,
|
|
4858
|
+
state: fallback.hub?.state || null,
|
|
4859
|
+
sessions: fallback.sessions,
|
|
4860
|
+
})
|
|
4861
|
+
)
|
|
4862
|
+
return;
|
|
4863
|
+
console.log(
|
|
4864
|
+
`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET} ${GREEN_BRIGHT}online${RESET} ${DIM}(default port probe 성공)${RESET}`,
|
|
4865
|
+
);
|
|
4866
|
+
console.log(
|
|
4867
|
+
` URL: http://127.0.0.1:${fallback.port || 27888}/mcp`,
|
|
4868
|
+
);
|
|
4869
|
+
if (fallback.pid !== undefined)
|
|
4870
|
+
console.log(` PID: ${fallback.pid}`);
|
|
4871
|
+
if (fallback.hub?.state)
|
|
4872
|
+
console.log(` State: ${fallback.hub.state}`);
|
|
4873
|
+
if (fallback.sessions !== undefined)
|
|
4874
|
+
console.log(` Sessions: ${fallback.sessions}`);
|
|
3758
4875
|
console.log("");
|
|
3759
4876
|
return;
|
|
3760
4877
|
}
|
|
3761
|
-
if (
|
|
3762
|
-
|
|
4878
|
+
if (
|
|
4879
|
+
emitHubStatus({
|
|
4880
|
+
status: "offline",
|
|
4881
|
+
source: "probe",
|
|
4882
|
+
url: null,
|
|
4883
|
+
pid: null,
|
|
4884
|
+
state: null,
|
|
4885
|
+
sessions: 0,
|
|
4886
|
+
})
|
|
4887
|
+
)
|
|
4888
|
+
return;
|
|
4889
|
+
console.log(
|
|
4890
|
+
`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET} ${RED}offline${RESET}\n`,
|
|
4891
|
+
);
|
|
3763
4892
|
return;
|
|
3764
4893
|
}
|
|
3765
4894
|
recoverPidFile(probed, "127.0.0.1");
|
|
3766
|
-
if (
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
4895
|
+
if (
|
|
4896
|
+
emitHubStatus({
|
|
4897
|
+
status: "online",
|
|
4898
|
+
source: "probe",
|
|
4899
|
+
url: `http://127.0.0.1:${probed.port || probePort}/mcp`,
|
|
4900
|
+
pid: probed.pid,
|
|
4901
|
+
state: probed.hub?.state || null,
|
|
4902
|
+
sessions: probed.sessions,
|
|
4903
|
+
})
|
|
4904
|
+
)
|
|
4905
|
+
return;
|
|
4906
|
+
console.log(
|
|
4907
|
+
`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET} ${GREEN_BRIGHT}online${RESET} ${DIM}(pid file 없음 / probe 성공)${RESET}`,
|
|
4908
|
+
);
|
|
4909
|
+
console.log(
|
|
4910
|
+
` URL: http://127.0.0.1:${probed.port || probePort}/mcp`,
|
|
4911
|
+
);
|
|
3776
4912
|
if (probed.pid !== undefined) console.log(` PID: ${probed.pid}`);
|
|
3777
4913
|
if (probed.hub?.state) console.log(` State: ${probed.hub.state}`);
|
|
3778
|
-
if (probed.sessions !== undefined)
|
|
4914
|
+
if (probed.sessions !== undefined)
|
|
4915
|
+
console.log(` Sessions: ${probed.sessions}`);
|
|
3779
4916
|
console.log("");
|
|
3780
4917
|
return;
|
|
3781
4918
|
}
|
|
@@ -3783,9 +4920,12 @@ async function cmdHub(args = [], options = {}) {
|
|
|
3783
4920
|
const info = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
3784
4921
|
process.kill(info.pid, 0); // 생존 확인
|
|
3785
4922
|
const uptime = Date.now() - info.started;
|
|
3786
|
-
const uptimeStr =
|
|
3787
|
-
|
|
3788
|
-
|
|
4923
|
+
const uptimeStr =
|
|
4924
|
+
uptime < 60000
|
|
4925
|
+
? `${Math.round(uptime / 1000)}초`
|
|
4926
|
+
: uptime < 3600000
|
|
4927
|
+
? `${Math.round(uptime / 60000)}분`
|
|
4928
|
+
: `${Math.round(uptime / 3600000)}시간`;
|
|
3789
4929
|
|
|
3790
4930
|
let data = null;
|
|
3791
4931
|
try {
|
|
@@ -3794,16 +4934,21 @@ async function cmdHub(args = [], options = {}) {
|
|
|
3794
4934
|
data = await probeHubStatus(host, port, 3000);
|
|
3795
4935
|
} catch {}
|
|
3796
4936
|
|
|
3797
|
-
if (
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
4937
|
+
if (
|
|
4938
|
+
emitHubStatus({
|
|
4939
|
+
status: "online",
|
|
4940
|
+
source: "pid-file",
|
|
4941
|
+
url: info.url,
|
|
4942
|
+
pid: info.pid,
|
|
4943
|
+
uptime_ms: uptime,
|
|
4944
|
+
state: data?.hub?.state || null,
|
|
4945
|
+
sessions: data?.sessions,
|
|
4946
|
+
})
|
|
4947
|
+
)
|
|
4948
|
+
return;
|
|
4949
|
+
console.log(
|
|
4950
|
+
`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET} ${GREEN_BRIGHT}online${RESET}`,
|
|
4951
|
+
);
|
|
3807
4952
|
console.log(` URL: ${info.url}`);
|
|
3808
4953
|
console.log(` PID: ${info.pid}`);
|
|
3809
4954
|
console.log(` Uptime: ${uptimeStr}`);
|
|
@@ -3815,27 +4960,49 @@ async function cmdHub(args = [], options = {}) {
|
|
|
3815
4960
|
}
|
|
3816
4961
|
console.log("");
|
|
3817
4962
|
} catch {
|
|
3818
|
-
try {
|
|
4963
|
+
try {
|
|
4964
|
+
unlinkSync(HUB_PID_FILE);
|
|
4965
|
+
} catch {}
|
|
3819
4966
|
const probed = await probeHubStatus();
|
|
3820
4967
|
if (!probed) {
|
|
3821
|
-
if (
|
|
3822
|
-
|
|
4968
|
+
if (
|
|
4969
|
+
emitHubStatus({
|
|
4970
|
+
status: "offline",
|
|
4971
|
+
source: "stale-pid",
|
|
4972
|
+
url: null,
|
|
4973
|
+
pid: null,
|
|
4974
|
+
state: null,
|
|
4975
|
+
sessions: 0,
|
|
4976
|
+
})
|
|
4977
|
+
)
|
|
4978
|
+
break;
|
|
4979
|
+
console.log(
|
|
4980
|
+
`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET} ${RED}offline${RESET} ${DIM}(stale PID 정리됨)${RESET}\n`,
|
|
4981
|
+
);
|
|
3823
4982
|
break;
|
|
3824
4983
|
}
|
|
3825
4984
|
recoverPidFile(probed, "127.0.0.1");
|
|
3826
|
-
if (
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
4985
|
+
if (
|
|
4986
|
+
emitHubStatus({
|
|
4987
|
+
status: "online",
|
|
4988
|
+
source: "stale-pid-probe",
|
|
4989
|
+
url: `http://127.0.0.1:${probed.port || probePort}/mcp`,
|
|
4990
|
+
pid: probed.pid,
|
|
4991
|
+
state: probed.hub?.state || null,
|
|
4992
|
+
sessions: probed.sessions,
|
|
4993
|
+
})
|
|
4994
|
+
)
|
|
4995
|
+
break;
|
|
4996
|
+
console.log(
|
|
4997
|
+
`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET} ${GREEN_BRIGHT}online${RESET} ${DIM}(stale PID 정리 후 probe 성공)${RESET}`,
|
|
4998
|
+
);
|
|
4999
|
+
console.log(
|
|
5000
|
+
` URL: http://127.0.0.1:${probed.port || probePort}/mcp`,
|
|
5001
|
+
);
|
|
3836
5002
|
if (probed.pid !== undefined) console.log(` PID: ${probed.pid}`);
|
|
3837
5003
|
if (probed.hub?.state) console.log(` State: ${probed.hub.state}`);
|
|
3838
|
-
if (probed.sessions !== undefined)
|
|
5004
|
+
if (probed.sessions !== undefined)
|
|
5005
|
+
console.log(` Sessions: ${probed.sessions}`);
|
|
3839
5006
|
console.log("");
|
|
3840
5007
|
}
|
|
3841
5008
|
break;
|
|
@@ -3845,12 +5012,24 @@ async function cmdHub(args = [], options = {}) {
|
|
|
3845
5012
|
// 사일런트 idempotent 보장 — 스킬 환경 프로브용.
|
|
3846
5013
|
// Hub 살아있으면 즉시 종료, 죽어있으면 자동 시작 + ready 대기.
|
|
3847
5014
|
const portArg = args.indexOf("--port");
|
|
3848
|
-
const ensurePort =
|
|
5015
|
+
const ensurePort =
|
|
5016
|
+
portArg !== -1
|
|
5017
|
+
? args[portArg + 1]
|
|
5018
|
+
: process.env.TFX_HUB_PORT || "27888";
|
|
3849
5019
|
|
|
3850
5020
|
// 1. 이미 healthy?
|
|
3851
|
-
const ensureProbed = await probeHubStatus(
|
|
5021
|
+
const ensureProbed = await probeHubStatus(
|
|
5022
|
+
"127.0.0.1",
|
|
5023
|
+
Number(ensurePort),
|
|
5024
|
+
1500,
|
|
5025
|
+
);
|
|
3852
5026
|
if (ensureProbed?.hub?.state === "healthy") {
|
|
3853
|
-
if (json)
|
|
5027
|
+
if (json)
|
|
5028
|
+
printJson({
|
|
5029
|
+
status: "ok",
|
|
5030
|
+
pid: ensureProbed.pid,
|
|
5031
|
+
port: Number(ensurePort),
|
|
5032
|
+
});
|
|
3854
5033
|
else process.stdout.write("hub: ok\n");
|
|
3855
5034
|
return;
|
|
3856
5035
|
}
|
|
@@ -3864,15 +5043,26 @@ async function cmdHub(args = [], options = {}) {
|
|
|
3864
5043
|
const retryDeadline = Date.now() + 3000;
|
|
3865
5044
|
while (Date.now() < retryDeadline) {
|
|
3866
5045
|
await new Promise((r) => setTimeout(r, 250));
|
|
3867
|
-
const retry = await probeHubStatus(
|
|
5046
|
+
const retry = await probeHubStatus(
|
|
5047
|
+
"127.0.0.1",
|
|
5048
|
+
Number(ensurePort),
|
|
5049
|
+
1000,
|
|
5050
|
+
);
|
|
3868
5051
|
if (retry?.hub?.state === "healthy") {
|
|
3869
|
-
if (json)
|
|
5052
|
+
if (json)
|
|
5053
|
+
printJson({
|
|
5054
|
+
status: "ok",
|
|
5055
|
+
pid: retry.pid,
|
|
5056
|
+
port: Number(ensurePort),
|
|
5057
|
+
});
|
|
3870
5058
|
else process.stdout.write("hub: ok\n");
|
|
3871
5059
|
return;
|
|
3872
5060
|
}
|
|
3873
5061
|
}
|
|
3874
5062
|
} catch {
|
|
3875
|
-
try {
|
|
5063
|
+
try {
|
|
5064
|
+
unlinkSync(HUB_PID_FILE);
|
|
5065
|
+
} catch {}
|
|
3876
5066
|
}
|
|
3877
5067
|
}
|
|
3878
5068
|
|
|
@@ -3886,11 +5076,15 @@ async function cmdHub(args = [], options = {}) {
|
|
|
3886
5076
|
}
|
|
3887
5077
|
|
|
3888
5078
|
if (process.platform === "win32") {
|
|
3889
|
-
const child = spawn(
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
5079
|
+
const child = spawn(
|
|
5080
|
+
"cmd.exe",
|
|
5081
|
+
["/c", "start", "/b", "", process.execPath, serverPath],
|
|
5082
|
+
{
|
|
5083
|
+
env: { ...process.env, TFX_HUB_PORT: String(ensurePort) },
|
|
5084
|
+
stdio: "ignore",
|
|
5085
|
+
windowsHide: true,
|
|
5086
|
+
},
|
|
5087
|
+
);
|
|
3894
5088
|
child.unref();
|
|
3895
5089
|
} else {
|
|
3896
5090
|
const child = spawn(process.execPath, [serverPath], {
|
|
@@ -3906,9 +5100,19 @@ async function cmdHub(args = [], options = {}) {
|
|
|
3906
5100
|
while (Date.now() < readyDeadline) {
|
|
3907
5101
|
await new Promise((r) => setTimeout(r, 250));
|
|
3908
5102
|
if (existsSync(HUB_PID_FILE)) {
|
|
3909
|
-
const readyProbe = await probeHubStatus(
|
|
5103
|
+
const readyProbe = await probeHubStatus(
|
|
5104
|
+
"127.0.0.1",
|
|
5105
|
+
Number(ensurePort),
|
|
5106
|
+
1000,
|
|
5107
|
+
);
|
|
3910
5108
|
if (readyProbe?.hub?.state === "healthy") {
|
|
3911
|
-
if (json)
|
|
5109
|
+
if (json)
|
|
5110
|
+
printJson({
|
|
5111
|
+
status: "ok",
|
|
5112
|
+
pid: readyProbe.pid,
|
|
5113
|
+
port: Number(ensurePort),
|
|
5114
|
+
started: true,
|
|
5115
|
+
});
|
|
3912
5116
|
else process.stdout.write("hub: started\n");
|
|
3913
5117
|
return;
|
|
3914
5118
|
}
|
|
@@ -3923,11 +5127,21 @@ async function cmdHub(args = [], options = {}) {
|
|
|
3923
5127
|
|
|
3924
5128
|
default:
|
|
3925
5129
|
console.log(`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET}\n`);
|
|
3926
|
-
console.log(
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
console.log(
|
|
3930
|
-
|
|
5130
|
+
console.log(
|
|
5131
|
+
` ${WHITE_BRIGHT}tfx hub start${RESET} ${GRAY}허브 데몬 시작${RESET}`,
|
|
5132
|
+
);
|
|
5133
|
+
console.log(
|
|
5134
|
+
` ${DIM} --port N${RESET} ${GRAY}포트 지정 (기본 27888)${RESET}`,
|
|
5135
|
+
);
|
|
5136
|
+
console.log(
|
|
5137
|
+
` ${WHITE_BRIGHT}tfx hub stop${RESET} ${GRAY}허브 중지${RESET}`,
|
|
5138
|
+
);
|
|
5139
|
+
console.log(
|
|
5140
|
+
` ${WHITE_BRIGHT}tfx hub status${RESET} ${GRAY}상태 확인${RESET}`,
|
|
5141
|
+
);
|
|
5142
|
+
console.log(
|
|
5143
|
+
` ${WHITE_BRIGHT}tfx hub ensure${RESET} ${GRAY}헬스체크 + 자동 시작 (스킬 프로브용)${RESET}\n`,
|
|
5144
|
+
);
|
|
3931
5145
|
}
|
|
3932
5146
|
}
|
|
3933
5147
|
|
|
@@ -3946,6 +5160,33 @@ async function main() {
|
|
|
3946
5160
|
cmdSetup({ dryRun: cmdArgs.includes("--dry-run") });
|
|
3947
5161
|
return;
|
|
3948
5162
|
case "doctor": {
|
|
5163
|
+
if (cmdArgs.includes("--audit")) {
|
|
5164
|
+
const auditScript = join(PKG_ROOT, "scripts", "config-audit.mjs");
|
|
5165
|
+
const auditArgs = JSON_OUTPUT ? ["--json"] : [];
|
|
5166
|
+
try {
|
|
5167
|
+
const out = execFileSync(process.execPath, [auditScript, ...auditArgs], {
|
|
5168
|
+
timeout: 15000, encoding: "utf8", windowsHide: true,
|
|
5169
|
+
});
|
|
5170
|
+
process.stdout.write(out);
|
|
5171
|
+
} catch (e) {
|
|
5172
|
+
process.stdout.write(e.stdout || "");
|
|
5173
|
+
if (e.stderr) process.stderr.write(e.stderr);
|
|
5174
|
+
}
|
|
5175
|
+
return;
|
|
5176
|
+
}
|
|
5177
|
+
if (cmdArgs.includes("--diagnose")) {
|
|
5178
|
+
const { diagnose } = await import("../scripts/doctor-diagnose.mjs");
|
|
5179
|
+
const result = await diagnose({ json: JSON_OUTPUT });
|
|
5180
|
+
if (!JSON_OUTPUT) {
|
|
5181
|
+
if (result.ok) {
|
|
5182
|
+
console.log(`\n ${GREEN_BRIGHT}✓${RESET} 진단 번들 생성: ${result.zipPath}`);
|
|
5183
|
+
console.log(` spawn 이벤트: ${result.traceCount}건, 훅 타이밍: ${result.hookTimingCount}건\n`);
|
|
5184
|
+
} else {
|
|
5185
|
+
console.log(`\n ${RED}✗${RESET} 진단 실패: ${result.error}\n`);
|
|
5186
|
+
}
|
|
5187
|
+
}
|
|
5188
|
+
return;
|
|
5189
|
+
}
|
|
3949
5190
|
const fix = cmdArgs.includes("--fix");
|
|
3950
5191
|
const reset = cmdArgs.includes("--reset");
|
|
3951
5192
|
await cmdDoctor({ fix, reset, json: JSON_OUTPUT });
|
|
@@ -3968,7 +5209,10 @@ async function main() {
|
|
|
3968
5209
|
cmdHandoff(cmdArgs, { json: JSON_OUTPUT });
|
|
3969
5210
|
return;
|
|
3970
5211
|
case "hub":
|
|
3971
|
-
await cmdHub(cmdArgs, {
|
|
5212
|
+
await cmdHub(cmdArgs, {
|
|
5213
|
+
json:
|
|
5214
|
+
JSON_OUTPUT && ["status", "ensure"].includes(cmdArgs[0] || "status"),
|
|
5215
|
+
});
|
|
3972
5216
|
return;
|
|
3973
5217
|
case "monitor": {
|
|
3974
5218
|
const { createMonitor } = await import("../tui/monitor.mjs");
|
|
@@ -3992,7 +5236,9 @@ async function main() {
|
|
|
3992
5236
|
windowsHide: true,
|
|
3993
5237
|
});
|
|
3994
5238
|
child.unref();
|
|
3995
|
-
console.log(
|
|
5239
|
+
console.log(
|
|
5240
|
+
`\n ${GREEN_BRIGHT}✓${RESET} tray 시작됨 (PID ${child.pid})\n`,
|
|
5241
|
+
);
|
|
3996
5242
|
return;
|
|
3997
5243
|
}
|
|
3998
5244
|
case "multi": {
|
|
@@ -4003,7 +5249,9 @@ async function main() {
|
|
|
4003
5249
|
await checkHubRunning();
|
|
4004
5250
|
}
|
|
4005
5251
|
const { pathToFileURL } = await import("node:url");
|
|
4006
|
-
const { cmdTeam } = await import(
|
|
5252
|
+
const { cmdTeam } = await import(
|
|
5253
|
+
pathToFileURL(join(PKG_ROOT, "hub", "team", "cli", "index.mjs")).href
|
|
5254
|
+
);
|
|
4007
5255
|
const prevArgv = process.argv;
|
|
4008
5256
|
process.argv = [prevArgv[0], prevArgv[1], "team", ...cmdArgs];
|
|
4009
5257
|
try {
|
|
@@ -4028,7 +5276,11 @@ async function main() {
|
|
|
4028
5276
|
case "nr": {
|
|
4029
5277
|
const scriptPath = join(PKG_ROOT, "scripts", "notion-read.mjs");
|
|
4030
5278
|
try {
|
|
4031
|
-
execFileSync(process.execPath, [scriptPath, ...cmdArgs], {
|
|
5279
|
+
execFileSync(process.execPath, [scriptPath, ...cmdArgs], {
|
|
5280
|
+
stdio: "inherit",
|
|
5281
|
+
timeout: 660000,
|
|
5282
|
+
windowsHide: true,
|
|
5283
|
+
});
|
|
4032
5284
|
} catch (e) {
|
|
4033
5285
|
throw createCliError(e.message || "notion-read 실행 실패", {
|
|
4034
5286
|
exitCode: e.status || EXIT_ERROR,
|
|
@@ -4041,11 +5293,15 @@ async function main() {
|
|
|
4041
5293
|
const hookManagerPath = join(PKG_ROOT, "hooks", "hook-manager.mjs");
|
|
4042
5294
|
const sub = cmdArgs[0] || "status";
|
|
4043
5295
|
try {
|
|
4044
|
-
execFileSync(
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
5296
|
+
execFileSync(
|
|
5297
|
+
process.execPath,
|
|
5298
|
+
[hookManagerPath, sub, ...cmdArgs.slice(1)],
|
|
5299
|
+
{
|
|
5300
|
+
stdio: "inherit",
|
|
5301
|
+
timeout: 30000,
|
|
5302
|
+
windowsHide: true,
|
|
5303
|
+
},
|
|
5304
|
+
);
|
|
4049
5305
|
} catch (e) {
|
|
4050
5306
|
if (e.status) process.exitCode = e.status;
|
|
4051
5307
|
}
|