triflux 10.3.4 → 10.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/bin/tfx-doctor-tui.mjs +1 -1
- package/bin/tfx-doctor.mjs +6 -1
- package/bin/tfx-profile.mjs +1 -1
- package/bin/tfx-setup-tui.mjs +1 -1
- package/bin/tfx-setup.mjs +6 -1
- package/bin/triflux.mjs +2396 -1140
- package/hooks/agent-route-guard.mjs +12 -8
- package/hooks/cross-review-tracker.mjs +21 -8
- package/hooks/error-context.mjs +19 -7
- package/hooks/hook-adaptive-collector.mjs +18 -16
- package/hooks/hook-manager.mjs +93 -32
- package/hooks/hook-orchestrator.mjs +108 -24
- package/hooks/hook-registry.json +11 -0
- package/hooks/keyword-rules.json +6 -10
- package/hooks/lib/resolve-root.mjs +1 -1
- package/hooks/mcp-config-watcher.mjs +6 -2
- package/hooks/pipeline-stop.mjs +3 -6
- package/hooks/safety-guard.mjs +99 -28
- package/hooks/session-start-fast.mjs +143 -0
- package/hooks/subagent-verifier.mjs +5 -4
- package/hub/account-broker.mjs +256 -60
- package/hub/adaptive-diagnostic.mjs +75 -48
- package/hub/adaptive-inject.mjs +95 -57
- package/hub/adaptive-memory.mjs +156 -42
- package/hub/adaptive.mjs +60 -31
- package/hub/assign-callbacks.mjs +67 -30
- package/hub/bridge.mjs +0 -1
- package/hub/cli-adapter-base.mjs +200 -48
- package/hub/codex-adapter.mjs +76 -96
- package/hub/codex-compat.mjs +3 -3
- package/hub/codex-preflight.mjs +63 -37
- package/hub/delegator/contracts.mjs +19 -23
- package/hub/delegator/index.mjs +3 -3
- package/hub/delegator/service.mjs +88 -64
- package/hub/delegator/tool-definitions.mjs +5 -5
- package/hub/fullcycle.mjs +33 -17
- package/hub/gemini-adapter.mjs +69 -94
- package/hub/hitl.mjs +89 -30
- package/hub/intent.mjs +161 -38
- package/hub/lib/cache-guard.mjs +43 -17
- package/hub/lib/mcp-response-cache.mjs +66 -32
- package/hub/lib/memory-store.mjs +285 -111
- package/hub/lib/path-utils.mjs +35 -37
- package/hub/lib/process-utils.mjs +106 -37
- package/hub/lib/spawn-trace.mjs +527 -0
- package/hub/lib/ssh-command.mjs +34 -4
- package/hub/lib/ssh-retry.mjs +5 -1
- package/hub/lib/uuidv7.mjs +4 -3
- package/hub/memory-doctor.mjs +266 -106
- package/hub/middleware/request-logger.mjs +61 -34
- package/hub/paths.mjs +9 -9
- package/hub/pipeline/gates/confidence.mjs +34 -15
- package/hub/pipeline/gates/consensus.mjs +27 -15
- package/hub/pipeline/gates/index.mjs +7 -3
- package/hub/pipeline/gates/selfcheck.mjs +57 -19
- package/hub/pipeline/index.mjs +77 -42
- package/hub/pipeline/state.mjs +10 -10
- package/hub/pipeline/transitions.mjs +40 -23
- package/hub/platform.mjs +57 -48
- package/hub/promote-penalties.mjs +25 -7
- package/hub/quality/deslop.mjs +70 -49
- package/hub/research.mjs +32 -25
- package/hub/router.mjs +240 -107
- package/hub/routing/complexity.mjs +132 -29
- package/hub/routing/index.mjs +17 -12
- package/hub/routing/q-learning.mjs +76 -28
- package/hub/server.mjs +4 -4
- package/hub/session-fingerprint.mjs +126 -60
- package/hub/state.mjs +84 -43
- package/hub/store-adapter.mjs +59 -26
- package/hub/store.mjs +356 -153
- package/hub/team/agent-map.json +22 -7
- package/hub/team/ansi.mjs +186 -122
- package/hub/team/backend.mjs +28 -10
- package/hub/team/cli/commands/attach.mjs +29 -9
- package/hub/team/cli/commands/control.mjs +29 -8
- package/hub/team/cli/commands/debug.mjs +32 -11
- package/hub/team/cli/commands/focus.mjs +38 -11
- package/hub/team/cli/commands/interrupt.mjs +18 -6
- package/hub/team/cli/commands/kill.mjs +16 -5
- package/hub/team/cli/commands/list.mjs +11 -4
- package/hub/team/cli/commands/send.mjs +19 -6
- package/hub/team/cli/commands/start/index.mjs +154 -31
- package/hub/team/cli/commands/start/parse-args.mjs +38 -11
- package/hub/team/cli/commands/start/start-headless.mjs +112 -36
- package/hub/team/cli/commands/start/start-in-process.mjs +12 -2
- package/hub/team/cli/commands/start/start-mux.mjs +70 -21
- package/hub/team/cli/commands/start/start-wt.mjs +29 -12
- package/hub/team/cli/commands/status.mjs +43 -14
- package/hub/team/cli/commands/stop.mjs +11 -4
- package/hub/team/cli/commands/task.mjs +8 -3
- package/hub/team/cli/commands/tasks.mjs +1 -1
- package/hub/team/cli/index.mjs +2 -2
- package/hub/team/cli/manifest.mjs +38 -8
- package/hub/team/cli/render.mjs +30 -8
- package/hub/team/cli/services/attach-fallback.mjs +31 -11
- package/hub/team/cli/services/hub-client.mjs +42 -14
- package/hub/team/cli/services/member-selector.mjs +11 -4
- package/hub/team/cli/services/native-control.mjs +48 -21
- package/hub/team/cli/services/runtime-mode.mjs +2 -1
- package/hub/team/cli/services/state-store.mjs +25 -8
- package/hub/team/cli/services/task-model.mjs +16 -6
- package/hub/team/conductor-mesh-bridge.mjs +24 -23
- package/hub/team/conductor.mjs +8 -4
- package/hub/team/dashboard-anchor.mjs +4 -5
- package/hub/team/dashboard-layout.mjs +3 -1
- package/hub/team/dashboard-open.mjs +41 -21
- package/hub/team/dashboard.mjs +76 -28
- package/hub/team/event-log.mjs +18 -10
- package/hub/team/handoff.mjs +31 -15
- package/hub/team/headless.mjs +2 -1
- package/hub/team/health-probe.mjs +69 -54
- package/hub/team/launcher-template.mjs +16 -13
- package/hub/team/native-supervisor.mjs +65 -21
- package/hub/team/native.mjs +74 -35
- package/hub/team/nativeProxy.mjs +184 -113
- package/hub/team/notify.mjs +119 -76
- package/hub/team/orchestrator.mjs +9 -4
- package/hub/team/pane.mjs +12 -7
- package/hub/team/process-cleanup.mjs +25 -16
- package/hub/team/psmux.mjs +491 -201
- package/hub/team/remote-probe.mjs +68 -52
- package/hub/team/remote-session.mjs +117 -59
- package/hub/team/remote-watcher.mjs +61 -33
- package/hub/team/routing.mjs +51 -25
- package/hub/team/runtime-strategy.mjs +3 -1
- package/hub/team/session.mjs +98 -34
- package/hub/team/staleState.mjs +72 -30
- package/hub/team/swarm-locks.mjs +15 -13
- package/hub/team/swarm-planner.mjs +32 -21
- package/hub/team/swarm-reconciler.mjs +48 -23
- package/hub/team/tui-lite.mjs +266 -68
- package/hub/team/tui-remote-adapter.mjs +14 -10
- package/hub/team/tui-viewer.mjs +99 -43
- package/hub/team/tui.mjs +708 -271
- package/hub/team/worktree-lifecycle.mjs +152 -58
- package/hub/team/wt-manager.mjs +24 -14
- package/hub/token-mode.mjs +71 -71
- package/hub/tray.mjs +66 -23
- package/hub/workers/claude-worker.mjs +162 -118
- package/hub/workers/codex-mcp.mjs +192 -141
- package/hub/workers/delegator-mcp.mjs +507 -333
- package/hub/workers/factory.mjs +8 -8
- package/hub/workers/gemini-worker.mjs +115 -84
- package/hub/workers/interface.mjs +6 -1
- package/hub/workers/worker-utils.mjs +21 -14
- package/hud/colors.mjs +27 -9
- package/hud/constants.mjs +162 -26
- package/hud/context-monitor.mjs +82 -41
- package/hud/hud-qos-status.mjs +129 -49
- package/hud/mission-board.mjs +6 -3
- package/hud/providers/claude.mjs +226 -115
- package/hud/providers/codex.mjs +62 -22
- package/hud/providers/gemini.mjs +168 -56
- package/hud/renderers.mjs +384 -119
- package/hud/terminal.mjs +101 -31
- package/hud/utils.mjs +78 -38
- package/mesh/index.mjs +11 -5
- package/mesh/mesh-budget.mjs +18 -9
- package/mesh/mesh-heartbeat.mjs +1 -1
- package/mesh/mesh-queue.mjs +3 -5
- package/mesh/mesh-router.mjs +5 -4
- package/package.json +2 -1
- package/scripts/__tests__/gen-skill-docs.test.mjs +36 -7
- package/scripts/__tests__/keyword-detector.test.mjs +77 -28
- package/scripts/__tests__/mcp-guard-engine.test.mjs +58 -20
- package/scripts/__tests__/remote-spawn-transfer.test.mjs +30 -19
- package/scripts/__tests__/remote-spawn.test.mjs +10 -4
- package/scripts/__tests__/session-start-fast.test.mjs +36 -0
- package/scripts/__tests__/skill-template.test.mjs +98 -50
- package/scripts/__tests__/smoke.test.mjs +1 -1
- package/scripts/__tests__/spawn-trace.test.mjs +102 -0
- package/scripts/__tests__/tfx-doctor-diagnose.test.mjs +48 -0
- package/scripts/cache-doctor.mjs +11 -4
- package/scripts/cache-warmup.mjs +96 -37
- package/scripts/claudemd-sync.mjs +27 -17
- package/scripts/codex-gateway-preflight.mjs +52 -37
- package/scripts/codex-mcp-gateway-sync.mjs +59 -39
- package/scripts/completions/tfx.bash +47 -47
- package/scripts/completions/tfx.fish +44 -44
- package/scripts/completions/tfx.zsh +83 -83
- package/scripts/config-audit.mjs +232 -0
- package/scripts/convert-to-tmpl.mjs +54 -0
- package/scripts/cross-review-gate.mjs +35 -12
- package/scripts/cross-review-tracker.mjs +21 -8
- package/scripts/demo.mjs +35 -17
- package/scripts/doctor-diagnose.mjs +284 -0
- package/scripts/gen-skill-docs.mjs +7 -2
- package/scripts/gen-skill-manifest.mjs +2 -1
- package/scripts/headless-guard.mjs +86 -48
- package/scripts/hub-ensure.mjs +45 -26
- package/scripts/keyword-detector.mjs +41 -20
- package/scripts/keyword-rules-expander.mjs +47 -30
- package/scripts/lib/claudemd-scanner.mjs +6 -1
- package/scripts/lib/context.mjs +3 -3
- package/scripts/lib/cross-review-utils.mjs +6 -3
- package/scripts/lib/env-probe.mjs +47 -28
- package/scripts/lib/gemini-profiles.mjs +44 -10
- package/scripts/lib/handoff.mjs +33 -17
- package/scripts/lib/hook-utils.mjs +8 -6
- package/scripts/lib/keyword-rules.mjs +43 -19
- package/scripts/lib/logger.mjs +24 -24
- package/scripts/lib/mcp-filter.mjs +377 -239
- package/scripts/lib/mcp-guard-engine.mjs +194 -79
- package/scripts/lib/mcp-manifest.mjs +23 -13
- package/scripts/lib/mcp-server-catalog.mjs +300 -63
- package/scripts/lib/psmux-info.mjs +11 -6
- package/scripts/lib/remote-spawn-transfer.mjs +44 -14
- package/scripts/lib/skill-template.mjs +30 -7
- package/scripts/mcp-check.mjs +58 -39
- package/scripts/mcp-gateway-config.mjs +83 -39
- package/scripts/mcp-gateway-ensure.mjs +43 -35
- package/scripts/mcp-gateway-integration-test.mjs +70 -58
- package/scripts/mcp-gateway-start.mjs +126 -60
- package/scripts/mcp-gateway-verify.mjs +24 -22
- package/scripts/mcp-safety-guard.mjs +44 -11
- package/scripts/notion-read.mjs +199 -84
- package/scripts/pack.mjs +94 -89
- package/scripts/preflight-cache.mjs +27 -10
- package/scripts/preinstall.mjs +42 -13
- package/scripts/remote-spawn.mjs +309 -94
- package/scripts/run.cjs +8 -5
- package/scripts/session-spawn-helper.mjs +130 -39
- package/scripts/session-stale-cleanup.mjs +123 -0
- package/scripts/setup.mjs +941 -492
- package/scripts/test-lock.mjs +20 -7
- package/scripts/test-tfx-route-no-claude-native.mjs +16 -12
- package/scripts/tfx-batch-stats.mjs +32 -11
- package/scripts/tfx-gate-activate.mjs +11 -4
- package/scripts/tfx-route-post.mjs +87 -20
- package/scripts/tfx-route-worker.mjs +57 -51
- package/scripts/tfx-route.sh +41 -124
- package/scripts/tmp-cleanup.mjs +21 -7
- package/scripts/token-snapshot.mjs +204 -85
- package/skills/.omc/state/agent-replay-8f0e10a9-9693-4410-96f5-a6b07e8ed995.jsonl +1 -0
- package/skills/.omc/state/idle-notif-cooldown.json +3 -0
- package/skills/.omc/state/last-tool-error.json +7 -0
- package/skills/.omc/state/subagent-tracking.json +7 -0
- package/skills/_templates/base.md +1 -6
- package/skills/merge-worktree/SKILL.md.tmpl +144 -0
- package/skills/shared/telemetry-segment.md +6 -0
- package/skills/star-prompt/SKILL.md.tmpl +222 -0
- package/skills/tfx-analysis/SKILL.md.tmpl +107 -0
- package/skills/tfx-analysis/skill.json +1 -6
- package/skills/tfx-auto/SKILL.md +1 -0
- package/skills/tfx-auto-codex/SKILL.md.tmpl +106 -0
- package/skills/tfx-auto-codex/skill.json +1 -3
- package/skills/tfx-autopilot/SKILL.md.tmpl +116 -0
- package/skills/tfx-autopilot/skill.json +1 -5
- package/skills/tfx-autoresearch/SKILL.md.tmpl +136 -0
- package/skills/tfx-autoroute/SKILL.md.tmpl +189 -0
- package/skills/tfx-autoroute/skill.json +1 -7
- package/skills/tfx-codex/SKILL.md +1 -0
- package/skills/tfx-codex/skill.json +1 -3
- package/skills/tfx-codex-swarm/SKILL.md.tmpl +16 -0
- package/skills/tfx-codex-swarm/evals/evals.json +1 -1
- package/skills/tfx-codex-swarm/skill.json +1 -4
- package/skills/tfx-codex-swarm-workspace/iteration-1/benchmark.json +54 -12
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/grading.json +35 -7
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/grading.json +35 -7
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/grading.json +25 -5
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/grading.json +25 -5
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/grading.json +20 -4
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/grading.json +16 -4
- package/skills/tfx-consensus/SKILL.md.tmpl +146 -0
- package/skills/tfx-debate/SKILL.md.tmpl +192 -0
- package/skills/tfx-debate/skill.json +1 -7
- package/skills/tfx-deep-analysis/SKILL.md.tmpl +228 -0
- package/skills/tfx-deep-analysis/skill.json +1 -5
- package/skills/tfx-deep-interview/SKILL.md.tmpl +203 -0
- package/skills/tfx-deep-plan/SKILL.md.tmpl +282 -0
- package/skills/tfx-deep-qa/SKILL.md.tmpl +165 -0
- package/skills/tfx-deep-qa/skill.json +1 -6
- package/skills/tfx-deep-research/SKILL.md.tmpl +217 -0
- package/skills/tfx-deep-review/SKILL.md.tmpl +179 -0
- package/skills/tfx-doctor/SKILL.md +21 -0
- package/skills/tfx-doctor/SKILL.md.tmpl +172 -0
- package/skills/tfx-doctor/skill.json +1 -3
- package/skills/tfx-find/SKILL.md +1 -0
- package/skills/tfx-forge/SKILL.md.tmpl +187 -0
- package/skills/tfx-fullcycle/SKILL.md.tmpl +286 -0
- package/skills/tfx-fullcycle/skill.json +1 -6
- package/skills/tfx-gemini/SKILL.md.tmpl +91 -0
- package/skills/tfx-gemini/skill.json +1 -3
- package/skills/tfx-hooks/SKILL.md.tmpl +216 -0
- package/skills/tfx-hooks/skill.json +1 -3
- package/skills/tfx-hub/SKILL.md.tmpl +212 -0
- package/skills/tfx-hub/skill.json +1 -3
- package/skills/tfx-index/SKILL.md +1 -0
- package/skills/tfx-index/skill.json +1 -6
- package/skills/tfx-interview/SKILL.md.tmpl +285 -0
- package/skills/tfx-multi/SKILL.md.tmpl +183 -0
- package/skills/tfx-multi/skill.json +1 -3
- package/skills/tfx-panel/SKILL.md.tmpl +189 -0
- package/skills/tfx-panel/skill.json +1 -7
- package/skills/tfx-persist/SKILL.md.tmpl +270 -0
- package/skills/tfx-persist/skill.json +1 -7
- package/skills/tfx-plan/SKILL.md +1 -0
- package/skills/tfx-plan/skill.json +1 -6
- package/skills/tfx-profile/SKILL.md.tmpl +239 -0
- package/skills/tfx-profile/skill.json +1 -3
- package/skills/tfx-prune/SKILL.md.tmpl +200 -0
- package/skills/tfx-prune/skill.json +1 -7
- package/skills/tfx-psmux-rules/SKILL.md.tmpl +326 -0
- package/skills/tfx-psmux-rules/skill.json +1 -4
- package/skills/tfx-qa/SKILL.md +1 -0
- package/skills/tfx-qa/skill.json +1 -6
- package/skills/tfx-ralph/SKILL.md.tmpl +28 -0
- package/skills/tfx-ralph/skill.json +1 -4
- package/skills/tfx-remote-setup/SKILL.md.tmpl +576 -0
- package/skills/tfx-remote-setup/skill.json +1 -3
- package/skills/tfx-remote-spawn/SKILL.md.tmpl +263 -0
- package/skills/tfx-remote-spawn/references/hosts.json +16 -0
- package/skills/tfx-remote-spawn/skill.json +1 -4
- package/skills/tfx-research/SKILL.md +1 -0
- package/skills/tfx-review/SKILL.md +1 -0
- package/skills/tfx-review/skill.json +1 -6
- package/skills/tfx-setup/SKILL.md.tmpl +504 -0
- package/skills/tfx-setup/skill.json +1 -3
- package/skills/tfx-swarm/SKILL.md +22 -0
- package/skills/tfx-swarm/SKILL.md.tmpl +218 -0
- package/tui/codex-profile.mjs +88 -33
- package/tui/core.mjs +45 -15
- package/tui/doctor.mjs +75 -28
- package/tui/gemini-profile.mjs +74 -29
- package/tui/monitor-data.mjs +8 -4
- package/tui/monitor.mjs +71 -27
- package/tui/setup.mjs +133 -42
package/scripts/setup.mjs
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
|
|
2
3
|
// triflux 세션 시작 시 자동 설정 스크립트
|
|
3
4
|
// - tfx-route.sh를 ~/.claude/scripts/에 동기화
|
|
4
5
|
// - hud-qos-status.mjs를 ~/.claude/hud/에 동기화
|
|
5
6
|
// - skills/를 ~/.claude/skills/에 동기화
|
|
6
7
|
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
8
|
+
import { execFileSync, spawn } from "child_process";
|
|
9
|
+
import {
|
|
10
|
+
chmodSync,
|
|
11
|
+
copyFileSync,
|
|
12
|
+
existsSync,
|
|
13
|
+
mkdirSync,
|
|
14
|
+
readdirSync,
|
|
15
|
+
readFileSync,
|
|
16
|
+
unlinkSync,
|
|
17
|
+
writeFileSync,
|
|
18
|
+
} from "fs";
|
|
9
19
|
import { homedir } from "os";
|
|
10
|
-
import {
|
|
20
|
+
import { dirname, join, relative } from "path";
|
|
11
21
|
import { fileURLToPath } from "url";
|
|
12
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
ensureGlobalClaudeRoutingSection,
|
|
24
|
+
ensureTfxSection,
|
|
25
|
+
getLatestRoutingTable,
|
|
26
|
+
} from "./claudemd-sync.mjs";
|
|
13
27
|
import { cleanupTmpFiles } from "./tmp-cleanup.mjs";
|
|
14
28
|
|
|
15
29
|
const PLUGIN_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
@@ -30,28 +44,21 @@ function detectDevMode(root = PLUGIN_ROOT) {
|
|
|
30
44
|
}
|
|
31
45
|
|
|
32
46
|
const BREADCRUMB_PATH = join(CLAUDE_DIR, "scripts", ".tfx-pkg-root");
|
|
47
|
+
const SETTINGS_PATH = join(CLAUDE_DIR, "settings.json");
|
|
48
|
+
const HUD_PATH = join(CLAUDE_DIR, "hud", "hud-qos-status.mjs");
|
|
33
49
|
|
|
34
50
|
const REQUIRED_CODEX_PROFILES = [
|
|
35
51
|
{
|
|
36
52
|
name: "codex53_high",
|
|
37
|
-
lines: [
|
|
38
|
-
'model = "gpt-5.3-codex"',
|
|
39
|
-
'model_reasoning_effort = "high"',
|
|
40
|
-
],
|
|
53
|
+
lines: ['model = "gpt-5.3-codex"', 'model_reasoning_effort = "high"'],
|
|
41
54
|
},
|
|
42
55
|
{
|
|
43
56
|
name: "codex53_xhigh",
|
|
44
|
-
lines: [
|
|
45
|
-
'model = "gpt-5.3-codex"',
|
|
46
|
-
'model_reasoning_effort = "xhigh"',
|
|
47
|
-
],
|
|
57
|
+
lines: ['model = "gpt-5.3-codex"', 'model_reasoning_effort = "xhigh"'],
|
|
48
58
|
},
|
|
49
59
|
{
|
|
50
60
|
name: "spark53_low",
|
|
51
|
-
lines: [
|
|
52
|
-
'model = "gpt-5.3-codex-spark"',
|
|
53
|
-
'model_reasoning_effort = "low"',
|
|
54
|
-
],
|
|
61
|
+
lines: ['model = "gpt-5.3-codex-spark"', 'model_reasoning_effort = "low"'],
|
|
55
62
|
},
|
|
56
63
|
];
|
|
57
64
|
|
|
@@ -62,8 +69,9 @@ function scanHudFiles(pluginRoot, claudeDir) {
|
|
|
62
69
|
if (!existsSync(hudRoot)) return [];
|
|
63
70
|
|
|
64
71
|
const walk = (currentDir) => {
|
|
65
|
-
const entries = readdirSync(currentDir, { withFileTypes: true })
|
|
66
|
-
|
|
72
|
+
const entries = readdirSync(currentDir, { withFileTypes: true }).sort(
|
|
73
|
+
(left, right) => left.name.localeCompare(right.name),
|
|
74
|
+
);
|
|
67
75
|
|
|
68
76
|
return entries.flatMap((entry) => {
|
|
69
77
|
const absolutePath = join(currentDir, entry.name);
|
|
@@ -71,20 +79,27 @@ function scanHudFiles(pluginRoot, claudeDir) {
|
|
|
71
79
|
return walk(absolutePath);
|
|
72
80
|
}
|
|
73
81
|
|
|
74
|
-
if (
|
|
82
|
+
if (
|
|
83
|
+
!entry.isFile() ||
|
|
84
|
+
HUD_SYNC_EXCLUDES.has(entry.name) ||
|
|
85
|
+
!entry.name.endsWith(".mjs")
|
|
86
|
+
) {
|
|
75
87
|
return [];
|
|
76
88
|
}
|
|
77
89
|
|
|
78
90
|
const hudRelativePath = relative(hudRoot, absolutePath);
|
|
79
91
|
const normalizedRelativePath = hudRelativePath.replace(/\\/g, "/");
|
|
80
92
|
|
|
81
|
-
return [
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
93
|
+
return [
|
|
94
|
+
{
|
|
95
|
+
src: absolutePath,
|
|
96
|
+
dst: join(claudeDir, "hud", hudRelativePath),
|
|
97
|
+
label:
|
|
98
|
+
normalizedRelativePath === "hud-qos-status.mjs"
|
|
99
|
+
? "hud-qos-status.mjs"
|
|
100
|
+
: `hud/${normalizedRelativePath}`,
|
|
101
|
+
},
|
|
102
|
+
];
|
|
88
103
|
});
|
|
89
104
|
};
|
|
90
105
|
|
|
@@ -218,7 +233,8 @@ function shouldSyncTextFile(src, dst) {
|
|
|
218
233
|
|
|
219
234
|
function getPackageVersion() {
|
|
220
235
|
try {
|
|
221
|
-
return JSON.parse(readFileSync(join(PLUGIN_ROOT, "package.json"), "utf8"))
|
|
236
|
+
return JSON.parse(readFileSync(join(PLUGIN_ROOT, "package.json"), "utf8"))
|
|
237
|
+
.version;
|
|
222
238
|
} catch {
|
|
223
239
|
return null;
|
|
224
240
|
}
|
|
@@ -237,7 +253,11 @@ function readMarker() {
|
|
|
237
253
|
function writeMarker(marker) {
|
|
238
254
|
const markerDir = dirname(SETUP_MARKER_PATH);
|
|
239
255
|
if (!existsSync(markerDir)) mkdirSync(markerDir, { recursive: true });
|
|
240
|
-
writeFileSync(
|
|
256
|
+
writeFileSync(
|
|
257
|
+
SETUP_MARKER_PATH,
|
|
258
|
+
JSON.stringify(marker, null, 2) + "\n",
|
|
259
|
+
"utf8",
|
|
260
|
+
);
|
|
241
261
|
}
|
|
242
262
|
|
|
243
263
|
function escapeRegExp(value) {
|
|
@@ -288,18 +308,11 @@ const SKILL_ALIASES = [
|
|
|
288
308
|
|
|
289
309
|
// ── 폐기 예정 스킬 목록 ──
|
|
290
310
|
|
|
291
|
-
const DEPRECATED_SKILLS = [
|
|
292
|
-
"tfx-codex-route",
|
|
293
|
-
"tfx-gemini-route",
|
|
294
|
-
];
|
|
311
|
+
const DEPRECATED_SKILLS = ["tfx-codex-route", "tfx-gemini-route"];
|
|
295
312
|
|
|
296
313
|
// ── 구형 Codex 모델 (마이그레이션 안내 대상) ──
|
|
297
314
|
|
|
298
|
-
const LEGACY_CODEX_MODELS = [
|
|
299
|
-
"o4-mini",
|
|
300
|
-
"o3",
|
|
301
|
-
"codex-mini-latest",
|
|
302
|
-
];
|
|
315
|
+
const LEGACY_CODEX_MODELS = ["o4-mini", "o3", "codex-mini-latest"];
|
|
303
316
|
|
|
304
317
|
/**
|
|
305
318
|
* 별칭 스킬 디렉토리를 동기화한다.
|
|
@@ -362,8 +375,12 @@ function cleanupStaleSkills(installedDir, pkgDir) {
|
|
|
362
375
|
const entries = readdirSync(skillPath);
|
|
363
376
|
for (const f of entries) unlinkSync(join(skillPath, f));
|
|
364
377
|
// rmdir only works on empty dirs; ignore errors for nested
|
|
365
|
-
try {
|
|
366
|
-
|
|
378
|
+
try {
|
|
379
|
+
readdirSync(skillPath).length === 0 && unlinkSync(skillPath);
|
|
380
|
+
} catch {}
|
|
381
|
+
} catch {
|
|
382
|
+
/* best effort */
|
|
383
|
+
}
|
|
367
384
|
removed.push(name);
|
|
368
385
|
}
|
|
369
386
|
return { count: removed.length, removed };
|
|
@@ -436,9 +453,12 @@ function ensureHooksInSettings({ settingsPath, registryPath }) {
|
|
|
436
453
|
settings.hooks[spec.event] = [];
|
|
437
454
|
}
|
|
438
455
|
const entries = settings.hooks[spec.event];
|
|
439
|
-
const alreadyRegistered = entries.some(
|
|
440
|
-
|
|
441
|
-
|
|
456
|
+
const alreadyRegistered = entries.some(
|
|
457
|
+
(entry) =>
|
|
458
|
+
Array.isArray(entry?.hooks) &&
|
|
459
|
+
entry.hooks.some(
|
|
460
|
+
(h) => extractManagedHookFilename(h?.command) === spec.fileName,
|
|
461
|
+
),
|
|
442
462
|
);
|
|
443
463
|
if (alreadyRegistered) continue;
|
|
444
464
|
|
|
@@ -450,7 +470,11 @@ function ensureHooksInSettings({ settingsPath, registryPath }) {
|
|
|
450
470
|
}
|
|
451
471
|
|
|
452
472
|
if (added.length > 0) {
|
|
453
|
-
writeFileSync(
|
|
473
|
+
writeFileSync(
|
|
474
|
+
settingsPath,
|
|
475
|
+
JSON.stringify(settings, null, 2) + "\n",
|
|
476
|
+
"utf8",
|
|
477
|
+
);
|
|
454
478
|
}
|
|
455
479
|
return { ok: true, changed: added.length > 0, added };
|
|
456
480
|
} catch {
|
|
@@ -463,13 +487,19 @@ function ensureHooksInSettings({ settingsPath, registryPath }) {
|
|
|
463
487
|
* @param {{ mcpUrl: string, createIfMissing?: boolean, enabled?: boolean }} opts
|
|
464
488
|
* @returns {{ ok: boolean, changed: boolean, reason?: string }}
|
|
465
489
|
*/
|
|
466
|
-
function ensureCodexHubServerConfig({
|
|
490
|
+
function ensureCodexHubServerConfig({
|
|
491
|
+
configFile,
|
|
492
|
+
mcpUrl,
|
|
493
|
+
createIfMissing = false,
|
|
494
|
+
enabled = false,
|
|
495
|
+
}) {
|
|
467
496
|
try {
|
|
468
497
|
const codexConfigDir = join(homedir(), ".codex");
|
|
469
498
|
const configPath = configFile || join(codexConfigDir, "config.json");
|
|
470
499
|
|
|
471
500
|
if (!existsSync(configPath)) {
|
|
472
|
-
if (!createIfMissing)
|
|
501
|
+
if (!createIfMissing)
|
|
502
|
+
return { ok: true, changed: false, reason: "no-config" };
|
|
473
503
|
const dir = dirname(configPath);
|
|
474
504
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
475
505
|
const config = { mcpServers: { "tfx-hub": { url: mcpUrl, enabled } } };
|
|
@@ -483,7 +513,11 @@ function ensureCodexHubServerConfig({ configFile, mcpUrl, createIfMissing = fals
|
|
|
483
513
|
const existing = config.mcpServers["tfx-hub"];
|
|
484
514
|
const desired = { ...(existing || {}), url: mcpUrl, enabled };
|
|
485
515
|
|
|
486
|
-
if (
|
|
516
|
+
if (
|
|
517
|
+
existing &&
|
|
518
|
+
existing.url === desired.url &&
|
|
519
|
+
existing.enabled === desired.enabled
|
|
520
|
+
) {
|
|
487
521
|
return { ok: true, changed: false };
|
|
488
522
|
}
|
|
489
523
|
|
|
@@ -528,9 +562,9 @@ function ensureCodexProfiles() {
|
|
|
528
562
|
|
|
529
563
|
// headless 모드에서 승인 없이 실행하려면 sandbox 설정 필수
|
|
530
564
|
// Codex 0.117.0+: config.toml 설정과 CLI 플래그 중복 시 에러
|
|
531
|
-
if (process.platform === "win32" && !updated.includes(
|
|
565
|
+
if (process.platform === "win32" && !updated.includes("[windows]")) {
|
|
532
566
|
if (updated.length > 0 && !updated.endsWith("\n")) updated += "\n";
|
|
533
|
-
updated +=
|
|
567
|
+
updated += '\n[windows]\nsandbox = "elevated"\n';
|
|
534
568
|
changed++;
|
|
535
569
|
}
|
|
536
570
|
|
|
@@ -540,7 +574,10 @@ function ensureCodexProfiles() {
|
|
|
540
574
|
|
|
541
575
|
return { ok: true, changed };
|
|
542
576
|
} catch (error) {
|
|
543
|
-
const message =
|
|
577
|
+
const message =
|
|
578
|
+
error instanceof Error && error.message
|
|
579
|
+
? error.message.trim()
|
|
580
|
+
: "unknown error";
|
|
544
581
|
return { ok: false, changed: 0, message };
|
|
545
582
|
}
|
|
546
583
|
}
|
|
@@ -553,301 +590,105 @@ function syncClaudeRoutingSections() {
|
|
|
553
590
|
ensureGlobalClaudeRoutingSection(CLAUDE_DIR),
|
|
554
591
|
];
|
|
555
592
|
} catch (error) {
|
|
556
|
-
const reason =
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
SYNC_MAP,
|
|
567
|
-
BREADCRUMB_PATH,
|
|
568
|
-
PLUGIN_ROOT,
|
|
569
|
-
CLAUDE_DIR,
|
|
570
|
-
SETUP_MARKER_PATH,
|
|
571
|
-
readMarker,
|
|
572
|
-
writeMarker,
|
|
573
|
-
REQUIRED_CODEX_PROFILES,
|
|
574
|
-
getVersion,
|
|
575
|
-
ensureCodexProfiles,
|
|
576
|
-
SKILL_ALIASES,
|
|
577
|
-
LEGACY_CODEX_MODELS,
|
|
578
|
-
DEPRECATED_SKILLS,
|
|
579
|
-
syncAliasedSkillDir,
|
|
580
|
-
cleanupStaleSkills,
|
|
581
|
-
extractManagedHookFilename,
|
|
582
|
-
getManagedRegistryHooks,
|
|
583
|
-
ensureHooksInSettings,
|
|
584
|
-
ensureCodexHubServerConfig,
|
|
585
|
-
};
|
|
586
|
-
|
|
587
|
-
async function main() {
|
|
588
|
-
const isSync = process.argv.includes("--sync");
|
|
589
|
-
const isForce = process.argv.includes("--force");
|
|
590
|
-
const isDev = detectDevMode();
|
|
591
|
-
|
|
592
|
-
if (isDev) {
|
|
593
|
-
console.log(" [dev] \uB85C\uCEEC \uAC1C\uBC1C \uBAA8\uB4DC \uAC10\uC9C0");
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
if (isSync) {
|
|
597
|
-
console.log(" [sync] \uBA85\uC2DC\uC801 \uC7AC\uB3D9\uAE30\uD654 \uC2E4\uD589");
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
const pkgVersion = getPackageVersion();
|
|
601
|
-
const marker = readMarker();
|
|
602
|
-
const claudeRoutingResults = syncClaudeRoutingSections();
|
|
603
|
-
const claudeRoutingChangedCount = claudeRoutingResults.filter((result) => result.action === "created" || result.action === "updated").length;
|
|
604
|
-
if (pkgVersion && marker?.version === pkgVersion && !isForce) {
|
|
605
|
-
if (claudeRoutingChangedCount > 0) {
|
|
606
|
-
console.log(`setup: skip core sync (v${pkgVersion} already synced, CLAUDE.md ${claudeRoutingChangedCount}건 반영)`);
|
|
607
|
-
} else {
|
|
608
|
-
console.log(`setup: skip (v${pkgVersion} already synced)`);
|
|
609
|
-
}
|
|
610
|
-
process.exit(0);
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
let synced = claudeRoutingChangedCount;
|
|
614
|
-
|
|
615
|
-
// ── Memory Doctor (P0 자동 수정) ──
|
|
616
|
-
const isCIEnv = process.env.CI === "true" || process.env.DOCKER === "true";
|
|
617
|
-
if (!isCIEnv) {
|
|
618
|
-
try {
|
|
619
|
-
const { createMemoryDoctor } = await import("../hub/memory-doctor.mjs");
|
|
620
|
-
const projectSlug = process.cwd().replace(/^([A-Z]):/u, "$1-").replace(/[\\/]/gu, "-");
|
|
621
|
-
const memDir = join(CLAUDE_DIR, "projects", projectSlug, "memory");
|
|
622
|
-
if (existsSync(memDir)) {
|
|
623
|
-
const doctor = createMemoryDoctor({
|
|
624
|
-
memoryDir: memDir,
|
|
625
|
-
rulesDir: join(process.cwd(), ".claude", "rules"),
|
|
626
|
-
projectDir: process.cwd(),
|
|
627
|
-
claudeDir: CLAUDE_DIR,
|
|
628
|
-
});
|
|
629
|
-
const { checks, healthScore } = doctor.scan();
|
|
630
|
-
const p0Auto = checks.filter((c) => c.severity === "P0" && c.autofix && !c.passed);
|
|
631
|
-
if (p0Auto.length > 0) {
|
|
632
|
-
doctor.fixAll({ severity: "P0" });
|
|
633
|
-
console.log(` memory-doctor: ${p0Auto.length}건 P0 자동 수정 (health: ${healthScore})`);
|
|
634
|
-
synced += p0Auto.length;
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
} catch (err) {
|
|
638
|
-
console.log(` memory-doctor: skip (${err.message})`);
|
|
593
|
+
const reason =
|
|
594
|
+
error instanceof Error ? error.message : "routing_sync_failed";
|
|
595
|
+
return [
|
|
596
|
+
{
|
|
597
|
+
action: "unchanged",
|
|
598
|
+
path: join(PLUGIN_ROOT, "CLAUDE.md"),
|
|
599
|
+
skipped: true,
|
|
600
|
+
reason,
|
|
601
|
+
},
|
|
602
|
+
];
|
|
639
603
|
}
|
|
640
604
|
}
|
|
641
605
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
}
|
|
606
|
+
function createCommandIo() {
|
|
607
|
+
const stdout = [];
|
|
608
|
+
const stderr = [];
|
|
609
|
+
|
|
610
|
+
return {
|
|
611
|
+
log(message = "") {
|
|
612
|
+
stdout.push(`${message}\n`);
|
|
613
|
+
},
|
|
614
|
+
writeStdout(message = "") {
|
|
615
|
+
stdout.push(message);
|
|
616
|
+
},
|
|
617
|
+
writeStderr(message = "") {
|
|
618
|
+
stderr.push(message);
|
|
619
|
+
},
|
|
620
|
+
result(code = 0) {
|
|
621
|
+
return { code, stdout: stdout.join(""), stderr: stderr.join("") };
|
|
622
|
+
},
|
|
623
|
+
};
|
|
661
624
|
}
|
|
662
625
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
if (claudeGuide.changed) synced++;
|
|
666
|
-
} catch (e) {
|
|
667
|
-
console.log(` \x1b[33m⚠\x1b[0m CLAUDE.md 라우팅: ${e.message}`);
|
|
626
|
+
function getSetupArgv(stdinData) {
|
|
627
|
+
return Array.isArray(stdinData?.argv) ? stdinData.argv : [];
|
|
668
628
|
}
|
|
669
629
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
const workerNodeModules = join(CLAUDE_DIR, "scripts", "node_modules");
|
|
673
|
-
const mcpSdkPath = join(workerNodeModules, "@modelcontextprotocol", "sdk");
|
|
674
|
-
const srcNodeModules = join(PLUGIN_ROOT, "node_modules");
|
|
675
|
-
|
|
676
|
-
// native 모듈은 제외 (플랫폼 의존적, worker에서 불필요)
|
|
677
|
-
const SKIP_PACKAGES = new Set(["better-sqlite3", "prebuild-install", "node-abi", "node-addon-api"]);
|
|
630
|
+
function loadSettings() {
|
|
631
|
+
if (!existsSync(SETTINGS_PATH)) return {};
|
|
678
632
|
|
|
679
|
-
if (!existsSync(mcpSdkPath) && existsSync(srcNodeModules)) {
|
|
680
633
|
try {
|
|
681
|
-
|
|
682
|
-
for (const entry of readdirSync(srcNodeModules)) {
|
|
683
|
-
if (SKIP_PACKAGES.has(entry)) continue;
|
|
684
|
-
|
|
685
|
-
const src = join(srcNodeModules, entry);
|
|
686
|
-
const dst = join(workerNodeModules, entry);
|
|
687
|
-
if (existsSync(dst)) continue;
|
|
688
|
-
|
|
689
|
-
mkdirSync(dirname(dst), { recursive: true });
|
|
690
|
-
cpSync(src, dst, { recursive: true });
|
|
691
|
-
}
|
|
692
|
-
synced++;
|
|
634
|
+
return JSON.parse(readFileSync(SETTINGS_PATH, "utf8"));
|
|
693
635
|
} catch {
|
|
694
|
-
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
// ── 패키지 루트 breadcrumb 기록 ──
|
|
699
|
-
// tfx-route.sh가 hub/server.mjs, hub/bridge.mjs를 찾을 수 있도록
|
|
700
|
-
// 패키지 루트 경로를 ~/.claude/scripts/.tfx-pkg-root에 기록한다.
|
|
701
|
-
// dev mode에서는 항상 최신 경로를 기록 (--sync 시 강제 갱신).
|
|
702
|
-
{
|
|
703
|
-
const pkgRootForward = PLUGIN_ROOT.replace(/\\/g, "/");
|
|
704
|
-
const currentBreadcrumb = existsSync(BREADCRUMB_PATH)
|
|
705
|
-
? readFileSync(BREADCRUMB_PATH, "utf8").trim()
|
|
706
|
-
: "";
|
|
707
|
-
if (currentBreadcrumb !== pkgRootForward || isSync) {
|
|
708
|
-
const breadcrumbDir = dirname(BREADCRUMB_PATH);
|
|
709
|
-
if (!existsSync(breadcrumbDir)) mkdirSync(breadcrumbDir, { recursive: true });
|
|
710
|
-
writeFileSync(BREADCRUMB_PATH, pkgRootForward + "\n", "utf8");
|
|
711
|
-
synced++;
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
// ── 에이전트 동기화 (.claude/agents/ → ~/.claude/agents/) ──
|
|
716
|
-
// slim-wrapper 등 커스텀 에이전트를 글로벌에 배포하여
|
|
717
|
-
// 다른 프로젝트에서도 subagent_type으로 참조 가능하게 한다.
|
|
718
|
-
|
|
719
|
-
const agentsSrc = join(PLUGIN_ROOT, ".claude", "agents");
|
|
720
|
-
const agentsDst = join(CLAUDE_DIR, "agents");
|
|
721
|
-
|
|
722
|
-
if (existsSync(agentsSrc)) {
|
|
723
|
-
if (!existsSync(agentsDst)) mkdirSync(agentsDst, { recursive: true });
|
|
724
|
-
|
|
725
|
-
for (const name of readdirSync(agentsSrc)) {
|
|
726
|
-
if (!name.endsWith(".md")) continue;
|
|
727
|
-
|
|
728
|
-
const src = join(agentsSrc, name);
|
|
729
|
-
const dst = join(agentsDst, name);
|
|
730
|
-
|
|
731
|
-
if (!existsSync(dst)) {
|
|
732
|
-
copyFileSync(src, dst);
|
|
733
|
-
synced++;
|
|
734
|
-
} else if (shouldSyncTextFile(src, dst)) {
|
|
735
|
-
copyFileSync(src, dst);
|
|
736
|
-
synced++;
|
|
737
|
-
}
|
|
636
|
+
return {};
|
|
738
637
|
}
|
|
739
638
|
}
|
|
740
639
|
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
const skillsSrc = join(PLUGIN_ROOT, "skills");
|
|
745
|
-
const skillsDst = join(CLAUDE_DIR, "skills");
|
|
746
|
-
|
|
747
|
-
function syncSkillDir(srcDir, dstDir) {
|
|
748
|
-
if (!existsSync(dstDir)) mkdirSync(dstDir, { recursive: true });
|
|
749
|
-
|
|
750
|
-
let count = 0;
|
|
751
|
-
for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
|
|
752
|
-
const srcPath = join(srcDir, entry.name);
|
|
753
|
-
const dstPath = join(dstDir, entry.name);
|
|
754
|
-
|
|
755
|
-
if (entry.isDirectory()) {
|
|
756
|
-
count += syncSkillDir(srcPath, dstPath);
|
|
757
|
-
} else if (entry.name.endsWith(".md")) {
|
|
758
|
-
if (shouldSyncTextFile(srcPath, dstPath)) {
|
|
759
|
-
copyFileSync(srcPath, dstPath);
|
|
760
|
-
count++;
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
return count;
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
if (existsSync(skillsSrc)) {
|
|
768
|
-
for (const name of readdirSync(skillsSrc)) {
|
|
769
|
-
const skillDir = join(skillsSrc, name);
|
|
770
|
-
const skillMd = join(skillDir, "SKILL.md");
|
|
771
|
-
if (!existsSync(skillMd)) continue;
|
|
772
|
-
|
|
773
|
-
synced += syncSkillDir(skillDir, join(skillsDst, name));
|
|
774
|
-
}
|
|
640
|
+
function persistSettings(settings) {
|
|
641
|
+
writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
775
642
|
}
|
|
776
643
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
const settingsPath = join(CLAUDE_DIR, "settings.json");
|
|
781
|
-
const hudPath = join(CLAUDE_DIR, "hud", "hud-qos-status.mjs");
|
|
782
|
-
|
|
783
|
-
/**
|
|
784
|
-
* statusLine 섹션 적용.
|
|
785
|
-
* @param {object} s - settings 객체 (직접 변경)
|
|
786
|
-
* @returns {boolean} 변경 여부
|
|
787
|
-
*/
|
|
788
|
-
function applyStatusLine(s) {
|
|
789
|
-
if (!existsSync(hudPath)) return false;
|
|
790
|
-
const currentCmd = s.statusLine?.command || "";
|
|
644
|
+
function applyStatusLine(settings) {
|
|
645
|
+
if (!existsSync(HUD_PATH)) return false;
|
|
646
|
+
const currentCmd = settings.statusLine?.command || "";
|
|
791
647
|
if (currentCmd.includes("hud-qos-status.mjs")) return false;
|
|
792
648
|
|
|
793
649
|
const nodePath = process.execPath.replace(/\\/g, "/");
|
|
794
|
-
const hudForward =
|
|
650
|
+
const hudForward = HUD_PATH.replace(/\\/g, "/");
|
|
795
651
|
const nodeRef = nodePath.includes(" ") ? `"${nodePath}"` : nodePath;
|
|
796
652
|
const hudRef = hudForward.includes(" ") ? `"${hudForward}"` : hudForward;
|
|
797
653
|
|
|
798
|
-
|
|
654
|
+
settings.statusLine = { type: "command", command: `${nodeRef} ${hudRef}` };
|
|
799
655
|
return true;
|
|
800
656
|
}
|
|
801
657
|
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
* @param {object} s - settings 객체 (직접 변경)
|
|
805
|
-
* @returns {boolean} 변경 여부
|
|
806
|
-
*/
|
|
807
|
-
function applyAgentTeams(s) {
|
|
808
|
-
if (!s.env) s.env = {};
|
|
658
|
+
function applyAgentTeams(settings) {
|
|
659
|
+
if (!settings.env) settings.env = {};
|
|
809
660
|
let changed = false;
|
|
810
661
|
|
|
811
|
-
if (
|
|
812
|
-
|
|
662
|
+
if (settings.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS !== "1") {
|
|
663
|
+
settings.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = "1";
|
|
813
664
|
changed = true;
|
|
814
665
|
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
s.teammateMode = "auto";
|
|
666
|
+
if (!settings.teammateMode) {
|
|
667
|
+
settings.teammateMode = "auto";
|
|
818
668
|
changed = true;
|
|
819
669
|
}
|
|
820
670
|
return changed;
|
|
821
671
|
}
|
|
822
672
|
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
* 모든 세션에서 remote control URL을 자동 발급하도록 설정.
|
|
826
|
-
* @param {object} s - settings 객체 (직접 변경)
|
|
827
|
-
* @returns {boolean} 변경 여부
|
|
828
|
-
*/
|
|
829
|
-
function applyRemoteControl(s) {
|
|
830
|
-
if (s.remoteControlAtStartup === true) return false;
|
|
673
|
+
function applyRemoteControl(settings) {
|
|
674
|
+
if (settings.remoteControlAtStartup === true) return false;
|
|
831
675
|
if (process.env.TFX_REMOTE_CONTROL !== "1" && !detectDevMode()) return false;
|
|
832
|
-
|
|
676
|
+
settings.remoteControlAtStartup = true;
|
|
833
677
|
return true;
|
|
834
678
|
}
|
|
835
679
|
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
* @param {object} s - settings 객체 (직접 변경)
|
|
839
|
-
* @returns {boolean} 변경 여부
|
|
840
|
-
*/
|
|
841
|
-
function applyHooks(s) {
|
|
842
|
-
if (!s.hooks) s.hooks = {};
|
|
680
|
+
function applyHooks(settings) {
|
|
681
|
+
if (!settings.hooks) settings.hooks = {};
|
|
843
682
|
let changed = false;
|
|
844
683
|
|
|
845
|
-
|
|
846
|
-
if (!Array.isArray(s.hooks.SessionStart)) s.hooks.SessionStart = [];
|
|
684
|
+
if (!Array.isArray(settings.hooks.SessionStart)) settings.hooks.SessionStart = [];
|
|
847
685
|
|
|
848
|
-
const hasTrifluxHooks =
|
|
849
|
-
|
|
850
|
-
|
|
686
|
+
const hasTrifluxHooks = settings.hooks.SessionStart.some(
|
|
687
|
+
(entry) =>
|
|
688
|
+
Array.isArray(entry.hooks) &&
|
|
689
|
+
entry.hooks.some(
|
|
690
|
+
(hook) => typeof hook.command === "string" && hook.command.includes("triflux"),
|
|
691
|
+
),
|
|
851
692
|
);
|
|
852
693
|
|
|
853
694
|
if (!hasTrifluxHooks) {
|
|
@@ -855,7 +696,7 @@ function applyHooks(s) {
|
|
|
855
696
|
const nodeRef = nodePath.includes(" ") ? `"${nodePath}"` : nodePath;
|
|
856
697
|
const pluginRoot = PLUGIN_ROOT.replace(/\\/g, "/");
|
|
857
698
|
|
|
858
|
-
|
|
699
|
+
settings.hooks.SessionStart.push({
|
|
859
700
|
matcher: "*",
|
|
860
701
|
hooks: [
|
|
861
702
|
{
|
|
@@ -878,17 +719,25 @@ function applyHooks(s) {
|
|
|
878
719
|
changed = true;
|
|
879
720
|
}
|
|
880
721
|
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
722
|
+
if (!Array.isArray(settings.hooks.PreToolUse)) settings.hooks.PreToolUse = [];
|
|
723
|
+
|
|
724
|
+
const guardScriptPath = join(
|
|
725
|
+
CLAUDE_DIR,
|
|
726
|
+
"scripts",
|
|
727
|
+
"headless-guard-fast.sh",
|
|
728
|
+
).replace(/\\/g, "/");
|
|
729
|
+
const hasGuardHook = settings.hooks.PreToolUse.some(
|
|
730
|
+
(entry) =>
|
|
731
|
+
Array.isArray(entry.hooks) &&
|
|
732
|
+
entry.hooks.some(
|
|
733
|
+
(hook) =>
|
|
734
|
+
typeof hook.command === "string" &&
|
|
735
|
+
hook.command.includes("headless-guard"),
|
|
736
|
+
),
|
|
888
737
|
);
|
|
889
738
|
|
|
890
739
|
if (!hasGuardHook && existsSync(guardScriptPath.replace(/\//g, "\\"))) {
|
|
891
|
-
|
|
740
|
+
settings.hooks.PreToolUse.push({
|
|
892
741
|
matcher: "Bash|Agent",
|
|
893
742
|
hooks: [
|
|
894
743
|
{
|
|
@@ -900,27 +749,38 @@ function applyHooks(s) {
|
|
|
900
749
|
});
|
|
901
750
|
changed = true;
|
|
902
751
|
} else if (hasGuardHook) {
|
|
903
|
-
|
|
904
|
-
for (const entry of s.hooks.PreToolUse) {
|
|
752
|
+
for (const entry of settings.hooks.PreToolUse) {
|
|
905
753
|
if (!Array.isArray(entry.hooks)) continue;
|
|
906
|
-
for (const
|
|
907
|
-
if (
|
|
908
|
-
|
|
754
|
+
for (const hook of entry.hooks) {
|
|
755
|
+
if (
|
|
756
|
+
typeof hook.command === "string" &&
|
|
757
|
+
hook.command.includes("headless-guard") &&
|
|
758
|
+
!hook.command.includes(guardScriptPath)
|
|
759
|
+
) {
|
|
760
|
+
hook.command = `bash "${guardScriptPath}"`;
|
|
909
761
|
changed = true;
|
|
910
762
|
}
|
|
911
763
|
}
|
|
912
764
|
}
|
|
913
765
|
}
|
|
914
766
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
767
|
+
const gateScriptPath = join(
|
|
768
|
+
CLAUDE_DIR,
|
|
769
|
+
"scripts",
|
|
770
|
+
"tfx-gate-activate.mjs",
|
|
771
|
+
).replace(/\\/g, "/");
|
|
772
|
+
const hasGateHook = settings.hooks.PreToolUse.some(
|
|
773
|
+
(entry) =>
|
|
774
|
+
Array.isArray(entry.hooks) &&
|
|
775
|
+
entry.hooks.some(
|
|
776
|
+
(hook) =>
|
|
777
|
+
typeof hook.command === "string" &&
|
|
778
|
+
hook.command.includes("tfx-gate-activate"),
|
|
779
|
+
),
|
|
920
780
|
);
|
|
921
781
|
|
|
922
782
|
if (!hasGateHook && existsSync(gateScriptPath.replace(/\//g, "\\"))) {
|
|
923
|
-
|
|
783
|
+
settings.hooks.PreToolUse.push({
|
|
924
784
|
matcher: "Skill",
|
|
925
785
|
hooks: [
|
|
926
786
|
{
|
|
@@ -932,11 +792,15 @@ function applyHooks(s) {
|
|
|
932
792
|
});
|
|
933
793
|
changed = true;
|
|
934
794
|
} else if (hasGateHook) {
|
|
935
|
-
for (const entry of
|
|
795
|
+
for (const entry of settings.hooks.PreToolUse) {
|
|
936
796
|
if (!Array.isArray(entry.hooks)) continue;
|
|
937
|
-
for (const
|
|
938
|
-
if (
|
|
939
|
-
|
|
797
|
+
for (const hook of entry.hooks) {
|
|
798
|
+
if (
|
|
799
|
+
typeof hook.command === "string" &&
|
|
800
|
+
hook.command.includes("tfx-gate-activate") &&
|
|
801
|
+
!hook.command.includes(gateScriptPath)
|
|
802
|
+
) {
|
|
803
|
+
hook.command = `node "${gateScriptPath}"`;
|
|
940
804
|
changed = true;
|
|
941
805
|
}
|
|
942
806
|
}
|
|
@@ -946,220 +810,796 @@ function applyHooks(s) {
|
|
|
946
810
|
return changed;
|
|
947
811
|
}
|
|
948
812
|
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
813
|
+
function ensureCriticalSetup() {
|
|
814
|
+
const settings = loadSettings();
|
|
815
|
+
let settingsChanged = false;
|
|
816
|
+
|
|
817
|
+
try {
|
|
818
|
+
if (applyStatusLine(settings)) settingsChanged = true;
|
|
819
|
+
} catch {}
|
|
820
|
+
try {
|
|
821
|
+
if (applyAgentTeams(settings)) settingsChanged = true;
|
|
822
|
+
} catch {}
|
|
823
|
+
try {
|
|
824
|
+
if (applyRemoteControl(settings)) settingsChanged = true;
|
|
825
|
+
} catch {}
|
|
826
|
+
try {
|
|
827
|
+
if (applyHooks(settings)) settingsChanged = true;
|
|
828
|
+
} catch {}
|
|
954
829
|
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
try { if (applyHooks(settings)) { settingsChanged = true; synced++; } } catch {}
|
|
830
|
+
if (settingsChanged) {
|
|
831
|
+
try {
|
|
832
|
+
persistSettings(settings);
|
|
833
|
+
} catch {}
|
|
834
|
+
}
|
|
961
835
|
|
|
962
|
-
// 1회 쓰기
|
|
963
|
-
if (settingsChanged) {
|
|
964
836
|
try {
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
837
|
+
const pkgRootForward = PLUGIN_ROOT.replace(/\\/g, "/");
|
|
838
|
+
const currentBreadcrumb = existsSync(BREADCRUMB_PATH)
|
|
839
|
+
? readFileSync(BREADCRUMB_PATH, "utf8").trim()
|
|
840
|
+
: "";
|
|
841
|
+
if (currentBreadcrumb !== pkgRootForward) {
|
|
842
|
+
const breadcrumbDir = dirname(BREADCRUMB_PATH);
|
|
843
|
+
if (!existsSync(breadcrumbDir)) {
|
|
844
|
+
mkdirSync(breadcrumbDir, { recursive: true });
|
|
845
|
+
}
|
|
846
|
+
writeFileSync(BREADCRUMB_PATH, pkgRootForward + "\n", "utf8");
|
|
847
|
+
}
|
|
848
|
+
} catch {}
|
|
849
|
+
|
|
850
|
+
try {
|
|
851
|
+
ensureCodexProfiles();
|
|
852
|
+
} catch {}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
export {
|
|
856
|
+
BREADCRUMB_PATH,
|
|
857
|
+
CLAUDE_DIR,
|
|
858
|
+
cleanupStaleSkills,
|
|
859
|
+
DEPRECATED_SKILLS,
|
|
860
|
+
detectDevMode,
|
|
861
|
+
ensureCodexHubServerConfig,
|
|
862
|
+
ensureCodexProfiles,
|
|
863
|
+
ensureHooksInSettings,
|
|
864
|
+
extractManagedHookFilename,
|
|
865
|
+
getManagedRegistryHooks,
|
|
866
|
+
getVersion,
|
|
867
|
+
hasProfileSection,
|
|
868
|
+
LEGACY_CODEX_MODELS,
|
|
869
|
+
PLUGIN_ROOT,
|
|
870
|
+
REQUIRED_CODEX_PROFILES,
|
|
871
|
+
readMarker,
|
|
872
|
+
replaceProfileSection,
|
|
873
|
+
SETUP_MARKER_PATH,
|
|
874
|
+
SKILL_ALIASES,
|
|
875
|
+
SYNC_MAP,
|
|
876
|
+
scanHudFiles,
|
|
877
|
+
syncAliasedSkillDir,
|
|
878
|
+
writeMarker,
|
|
879
|
+
};
|
|
880
|
+
|
|
881
|
+
export async function runCritical(stdinData) {
|
|
882
|
+
const io = createCommandIo();
|
|
883
|
+
const argv = getSetupArgv(stdinData);
|
|
884
|
+
const isSync = argv.includes("--sync");
|
|
885
|
+
const isDev = detectDevMode();
|
|
886
|
+
|
|
887
|
+
// version check remains part of the critical path for in-process callers.
|
|
888
|
+
getPackageVersion();
|
|
889
|
+
readMarker();
|
|
890
|
+
|
|
891
|
+
if (isDev) {
|
|
892
|
+
io.log(" [dev] 로컬 개발 모드 감지");
|
|
968
893
|
}
|
|
894
|
+
|
|
895
|
+
if (isSync) {
|
|
896
|
+
io.log(" [sync] 명시적 재동기화 실행");
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
ensureCriticalSetup();
|
|
900
|
+
return io.result(0);
|
|
969
901
|
}
|
|
970
902
|
|
|
971
|
-
|
|
903
|
+
export async function runDeferred(stdinData) {
|
|
904
|
+
const io = createCommandIo();
|
|
905
|
+
const argv = getSetupArgv(stdinData);
|
|
906
|
+
const isSync = argv.includes("--sync");
|
|
907
|
+
const isForce = argv.includes("--force");
|
|
908
|
+
const isDev = detectDevMode();
|
|
972
909
|
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
910
|
+
if (isDev) {
|
|
911
|
+
io.log(" [dev] \uB85C\uCEEC \uAC1C\uBC1C \uBAA8\uB4DC \uAC10\uC9C0");
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
if (isSync) {
|
|
915
|
+
io.log(
|
|
916
|
+
" [sync] \uBA85\uC2DC\uC801 \uC7AC\uB3D9\uAE30\uD654 \uC2E4\uD589",
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
const pkgVersion = getPackageVersion();
|
|
921
|
+
const marker = readMarker();
|
|
922
|
+
const claudeRoutingResults = syncClaudeRoutingSections();
|
|
923
|
+
const claudeRoutingChangedCount = claudeRoutingResults.filter(
|
|
924
|
+
(result) => result.action === "created" || result.action === "updated",
|
|
925
|
+
).length;
|
|
926
|
+
if (pkgVersion && marker?.version === pkgVersion && !isForce) {
|
|
927
|
+
if (claudeRoutingChangedCount > 0) {
|
|
928
|
+
io.log(
|
|
929
|
+
`setup: skip core sync (v${pkgVersion} already synced, CLAUDE.md ${claudeRoutingChangedCount}건 반영)`,
|
|
930
|
+
);
|
|
931
|
+
} else {
|
|
932
|
+
io.log(`setup: skip (v${pkgVersion} already synced)`);
|
|
933
|
+
}
|
|
934
|
+
return io.result(0);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
let synced = claudeRoutingChangedCount;
|
|
938
|
+
|
|
939
|
+
// ── Memory Doctor (P0 자동 수정) ──
|
|
940
|
+
const isCIEnv = process.env.CI === "true" || process.env.DOCKER === "true";
|
|
941
|
+
if (!isCIEnv) {
|
|
982
942
|
try {
|
|
983
|
-
const
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
943
|
+
const { createMemoryDoctor } = await import("../hub/memory-doctor.mjs");
|
|
944
|
+
const projectSlug = process
|
|
945
|
+
.cwd()
|
|
946
|
+
.replace(/^([A-Z]):/u, "$1-")
|
|
947
|
+
.replace(/[\\/]/gu, "-");
|
|
948
|
+
const memDir = join(CLAUDE_DIR, "projects", projectSlug, "memory");
|
|
949
|
+
if (existsSync(memDir)) {
|
|
950
|
+
const doctor = createMemoryDoctor({
|
|
951
|
+
memoryDir: memDir,
|
|
952
|
+
rulesDir: join(process.cwd(), ".claude", "rules"),
|
|
953
|
+
projectDir: process.cwd(),
|
|
954
|
+
claudeDir: CLAUDE_DIR,
|
|
955
|
+
});
|
|
956
|
+
const { checks, healthScore } = doctor.scan();
|
|
957
|
+
const p0Auto = checks.filter(
|
|
958
|
+
(c) => c.severity === "P0" && c.autofix && !c.passed,
|
|
959
|
+
);
|
|
960
|
+
if (p0Auto.length > 0) {
|
|
961
|
+
doctor.fixAll({ severity: "P0" });
|
|
962
|
+
io.log(
|
|
963
|
+
` memory-doctor: ${p0Auto.length}건 P0 자동 수정 (health: ${healthScore})`,
|
|
964
|
+
);
|
|
965
|
+
synced += p0Auto.length;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
} catch (err) {
|
|
969
|
+
io.log(` memory-doctor: skip (${err.message})`);
|
|
970
|
+
}
|
|
990
971
|
}
|
|
991
|
-
console.log(" \x1b[32m✓\x1b[0m HUD cache pre-warm (background)");
|
|
992
|
-
}
|
|
993
972
|
|
|
994
|
-
|
|
973
|
+
for (const { src, dst } of SYNC_MAP) {
|
|
974
|
+
if (!existsSync(src)) continue;
|
|
975
|
+
|
|
976
|
+
const dstDir = dirname(dst);
|
|
977
|
+
if (!existsSync(dstDir)) {
|
|
978
|
+
mkdirSync(dstDir, { recursive: true });
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
if (!existsSync(dst)) {
|
|
982
|
+
copyFileSync(src, dst);
|
|
983
|
+
try {
|
|
984
|
+
chmodSync(dst, 0o755);
|
|
985
|
+
} catch {}
|
|
986
|
+
synced++;
|
|
987
|
+
} else {
|
|
988
|
+
if (shouldSyncTextFile(src, dst)) {
|
|
989
|
+
copyFileSync(src, dst);
|
|
990
|
+
try {
|
|
991
|
+
chmodSync(dst, 0o755);
|
|
992
|
+
} catch {}
|
|
993
|
+
synced++;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
995
997
|
|
|
996
|
-
const HUB_PID_FILE = join(CLAUDE_DIR, "cache", "tfx-hub", "hub.pid");
|
|
997
|
-
if (existsSync(HUB_PID_FILE)) {
|
|
998
998
|
try {
|
|
999
|
-
const
|
|
1000
|
-
|
|
1001
|
-
} catch {
|
|
1002
|
-
|
|
1003
|
-
synced++;
|
|
999
|
+
const claudeGuide = ensureGlobalClaudeRoutingSection(CLAUDE_DIR);
|
|
1000
|
+
if (claudeGuide.changed) synced++;
|
|
1001
|
+
} catch (e) {
|
|
1002
|
+
io.log(` \x1b[33m⚠\x1b[0m CLAUDE.md 라우팅: ${e.message}`);
|
|
1004
1003
|
}
|
|
1005
|
-
}
|
|
1006
1004
|
|
|
1007
|
-
// ──
|
|
1005
|
+
// ── Worker 의존성 동기화 (MCP SDK + transitive deps) ──
|
|
1008
1006
|
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1007
|
+
const workerNodeModules = join(CLAUDE_DIR, "scripts", "node_modules");
|
|
1008
|
+
const mcpSdkPath = join(workerNodeModules, "@modelcontextprotocol", "sdk");
|
|
1009
|
+
const srcNodeModules = join(PLUGIN_ROOT, "node_modules");
|
|
1010
|
+
|
|
1011
|
+
// native 모듈은 제외 (플랫폼 의존적, worker에서 불필요)
|
|
1012
|
+
const SKIP_PACKAGES = new Set([
|
|
1013
|
+
"better-sqlite3",
|
|
1014
|
+
"prebuild-install",
|
|
1015
|
+
"node-abi",
|
|
1016
|
+
"node-addon-api",
|
|
1017
|
+
]);
|
|
1018
|
+
|
|
1019
|
+
if (!existsSync(mcpSdkPath) && existsSync(srcNodeModules)) {
|
|
1015
1020
|
try {
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
+
const { cpSync } = await import("fs");
|
|
1022
|
+
for (const entry of readdirSync(srcNodeModules)) {
|
|
1023
|
+
if (SKIP_PACKAGES.has(entry)) continue;
|
|
1024
|
+
|
|
1025
|
+
const src = join(srcNodeModules, entry);
|
|
1026
|
+
const dst = join(workerNodeModules, entry);
|
|
1027
|
+
if (existsSync(dst)) continue;
|
|
1028
|
+
|
|
1029
|
+
mkdirSync(dirname(dst), { recursive: true });
|
|
1030
|
+
cpSync(src, dst, { recursive: true });
|
|
1031
|
+
}
|
|
1021
1032
|
synced++;
|
|
1022
1033
|
} catch {
|
|
1023
|
-
|
|
1034
|
+
// best effort: 의존성 복사 실패 시 exec fallback으로 동작
|
|
1024
1035
|
}
|
|
1025
1036
|
}
|
|
1026
|
-
}
|
|
1027
1037
|
|
|
1028
|
-
// ──
|
|
1038
|
+
// ── 패키지 루트 breadcrumb 기록 ──
|
|
1039
|
+
// tfx-route.sh가 hub/server.mjs, hub/bridge.mjs를 찾을 수 있도록
|
|
1040
|
+
// 패키지 루트 경로를 ~/.claude/scripts/.tfx-pkg-root에 기록한다.
|
|
1041
|
+
// dev mode에서는 항상 최신 경로를 기록 (--sync 시 강제 갱신).
|
|
1042
|
+
{
|
|
1043
|
+
const pkgRootForward = PLUGIN_ROOT.replace(/\\/g, "/");
|
|
1044
|
+
const currentBreadcrumb = existsSync(BREADCRUMB_PATH)
|
|
1045
|
+
? readFileSync(BREADCRUMB_PATH, "utf8").trim()
|
|
1046
|
+
: "";
|
|
1047
|
+
if (currentBreadcrumb !== pkgRootForward || isSync) {
|
|
1048
|
+
const breadcrumbDir = dirname(BREADCRUMB_PATH);
|
|
1049
|
+
if (!existsSync(breadcrumbDir))
|
|
1050
|
+
mkdirSync(breadcrumbDir, { recursive: true });
|
|
1051
|
+
writeFileSync(BREADCRUMB_PATH, pkgRootForward + "\n", "utf8");
|
|
1052
|
+
synced++;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1029
1055
|
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
"
|
|
1035
|
-
|
|
1056
|
+
// ── 에이전트 동기화 (.claude/agents/ → ~/.claude/agents/) ──
|
|
1057
|
+
// slim-wrapper 등 커스텀 에이전트를 글로벌에 배포하여
|
|
1058
|
+
// 다른 프로젝트에서도 subagent_type으로 참조 가능하게 한다.
|
|
1059
|
+
|
|
1060
|
+
const agentsSrc = join(PLUGIN_ROOT, ".claude", "agents");
|
|
1061
|
+
const agentsDst = join(CLAUDE_DIR, "agents");
|
|
1062
|
+
|
|
1063
|
+
if (existsSync(agentsSrc)) {
|
|
1064
|
+
if (!existsSync(agentsDst)) mkdirSync(agentsDst, { recursive: true });
|
|
1065
|
+
|
|
1066
|
+
for (const name of readdirSync(agentsSrc)) {
|
|
1067
|
+
if (!name.endsWith(".md")) continue;
|
|
1068
|
+
|
|
1069
|
+
const src = join(agentsSrc, name);
|
|
1070
|
+
const dst = join(agentsDst, name);
|
|
1071
|
+
|
|
1072
|
+
if (!existsSync(dst)) {
|
|
1073
|
+
copyFileSync(src, dst);
|
|
1074
|
+
synced++;
|
|
1075
|
+
} else if (shouldSyncTextFile(src, dst)) {
|
|
1076
|
+
copyFileSync(src, dst);
|
|
1077
|
+
synced++;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// ── 스킬 동기화 ──
|
|
1083
|
+
// SKILL.md + 하위 디렉토리(references/ 등)를 재귀적으로 동기화
|
|
1084
|
+
|
|
1085
|
+
const skillsSrc = join(PLUGIN_ROOT, "skills");
|
|
1086
|
+
const skillsDst = join(CLAUDE_DIR, "skills");
|
|
1036
1087
|
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1088
|
+
function syncSkillDir(srcDir, dstDir) {
|
|
1089
|
+
if (!existsSync(dstDir)) mkdirSync(dstDir, { recursive: true });
|
|
1090
|
+
|
|
1091
|
+
let count = 0;
|
|
1092
|
+
for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
|
|
1093
|
+
const srcPath = join(srcDir, entry.name);
|
|
1094
|
+
const dstPath = join(dstDir, entry.name);
|
|
1095
|
+
|
|
1096
|
+
if (entry.isDirectory()) {
|
|
1097
|
+
count += syncSkillDir(srcPath, dstPath);
|
|
1098
|
+
} else if (entry.name.endsWith(".md")) {
|
|
1099
|
+
if (shouldSyncTextFile(srcPath, dstPath)) {
|
|
1100
|
+
copyFileSync(srcPath, dstPath);
|
|
1101
|
+
count++;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
return count;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
if (existsSync(skillsSrc)) {
|
|
1109
|
+
for (const name of readdirSync(skillsSrc)) {
|
|
1110
|
+
const skillDir = join(skillsSrc, name);
|
|
1111
|
+
const skillMd = join(skillDir, "SKILL.md");
|
|
1112
|
+
if (!existsSync(skillMd)) continue;
|
|
1113
|
+
|
|
1114
|
+
synced += syncSkillDir(skillDir, join(skillsDst, name));
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// ── settings.json 통합 R/W ──
|
|
1119
|
+
// 3개 섹션(statusLine, agentTeams, hooks)을 1회 read → 일괄 수정 → 1회 write
|
|
1120
|
+
|
|
1121
|
+
const settingsPath = join(CLAUDE_DIR, "settings.json");
|
|
1122
|
+
const hudPath = join(CLAUDE_DIR, "hud", "hud-qos-status.mjs");
|
|
1123
|
+
|
|
1124
|
+
/**
|
|
1125
|
+
* statusLine 섹션 적용.
|
|
1126
|
+
* @param {object} s - settings 객체 (직접 변경)
|
|
1127
|
+
* @returns {boolean} 변경 여부
|
|
1128
|
+
*/
|
|
1129
|
+
function applyStatusLine(s) {
|
|
1130
|
+
if (!existsSync(hudPath)) return false;
|
|
1131
|
+
const currentCmd = s.statusLine?.command || "";
|
|
1132
|
+
if (currentCmd.includes("hud-qos-status.mjs")) return false;
|
|
1133
|
+
|
|
1134
|
+
const nodePath = process.execPath.replace(/\\/g, "/");
|
|
1135
|
+
const hudForward = hudPath.replace(/\\/g, "/");
|
|
1136
|
+
const nodeRef = nodePath.includes(" ") ? `"${nodePath}"` : nodePath;
|
|
1137
|
+
const hudRef = hudForward.includes(" ") ? `"${hudForward}"` : hudForward;
|
|
1138
|
+
|
|
1139
|
+
s.statusLine = { type: "command", command: `${nodeRef} ${hudRef}` };
|
|
1140
|
+
return true;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
/**
|
|
1144
|
+
* Agent Teams 환경변수 섹션 적용.
|
|
1145
|
+
* @param {object} s - settings 객체 (직접 변경)
|
|
1146
|
+
* @returns {boolean} 변경 여부
|
|
1147
|
+
*/
|
|
1148
|
+
function applyAgentTeams(s) {
|
|
1149
|
+
if (!s.env) s.env = {};
|
|
1150
|
+
let changed = false;
|
|
1151
|
+
|
|
1152
|
+
if (s.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS !== "1") {
|
|
1153
|
+
s.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = "1";
|
|
1154
|
+
changed = true;
|
|
1155
|
+
}
|
|
1156
|
+
// teammateMode: auto (tmux 밖이면 in-process, 안이면 split-pane)
|
|
1157
|
+
if (!s.teammateMode) {
|
|
1158
|
+
s.teammateMode = "auto";
|
|
1159
|
+
changed = true;
|
|
1160
|
+
}
|
|
1161
|
+
return changed;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
/**
|
|
1165
|
+
* Remote Control 자동 활성화.
|
|
1166
|
+
* 모든 세션에서 remote control URL을 자동 발급하도록 설정.
|
|
1167
|
+
* @param {object} s - settings 객체 (직접 변경)
|
|
1168
|
+
* @returns {boolean} 변경 여부
|
|
1169
|
+
*/
|
|
1170
|
+
function applyRemoteControl(s) {
|
|
1171
|
+
if (s.remoteControlAtStartup === true) return false;
|
|
1172
|
+
if (process.env.TFX_REMOTE_CONTROL !== "1" && !detectDevMode())
|
|
1173
|
+
return false;
|
|
1174
|
+
s.remoteControlAtStartup = true;
|
|
1175
|
+
return true;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
/**
|
|
1179
|
+
* SessionStart + PreToolUse 훅 섹션 적용.
|
|
1180
|
+
* @param {object} s - settings 객체 (직접 변경)
|
|
1181
|
+
* @returns {boolean} 변경 여부
|
|
1182
|
+
*/
|
|
1183
|
+
function applyHooks(s) {
|
|
1184
|
+
if (!s.hooks) s.hooks = {};
|
|
1185
|
+
let changed = false;
|
|
1186
|
+
|
|
1187
|
+
// ── SessionStart 훅 ──
|
|
1188
|
+
if (!Array.isArray(s.hooks.SessionStart)) s.hooks.SessionStart = [];
|
|
1189
|
+
|
|
1190
|
+
const hasTrifluxHooks = s.hooks.SessionStart.some(
|
|
1191
|
+
(entry) =>
|
|
1192
|
+
Array.isArray(entry.hooks) &&
|
|
1193
|
+
entry.hooks.some(
|
|
1194
|
+
(h) => typeof h.command === "string" && h.command.includes("triflux"),
|
|
1195
|
+
),
|
|
1196
|
+
);
|
|
1197
|
+
|
|
1198
|
+
if (!hasTrifluxHooks) {
|
|
1199
|
+
const nodePath = process.execPath.replace(/\\/g, "/");
|
|
1200
|
+
const nodeRef = nodePath.includes(" ") ? `"${nodePath}"` : nodePath;
|
|
1201
|
+
const pluginRoot = PLUGIN_ROOT.replace(/\\/g, "/");
|
|
1202
|
+
|
|
1203
|
+
s.hooks.SessionStart.push({
|
|
1204
|
+
matcher: "*",
|
|
1205
|
+
hooks: [
|
|
1206
|
+
{
|
|
1207
|
+
type: "command",
|
|
1208
|
+
command: `${nodeRef} "${pluginRoot}/scripts/setup.mjs"`,
|
|
1209
|
+
timeout: 10,
|
|
1210
|
+
},
|
|
1211
|
+
{
|
|
1212
|
+
type: "command",
|
|
1213
|
+
command: `${nodeRef} "${pluginRoot}/scripts/hub-ensure.mjs"`,
|
|
1214
|
+
timeout: 8,
|
|
1215
|
+
},
|
|
1216
|
+
{
|
|
1217
|
+
type: "command",
|
|
1218
|
+
command: `${nodeRef} "${pluginRoot}/scripts/preflight-cache.mjs"`,
|
|
1219
|
+
timeout: 5,
|
|
1220
|
+
},
|
|
1221
|
+
],
|
|
1222
|
+
});
|
|
1223
|
+
changed = true;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
// ── PreToolUse 훅: headless-guard (auto-route) ──
|
|
1227
|
+
if (!Array.isArray(s.hooks.PreToolUse)) s.hooks.PreToolUse = [];
|
|
1228
|
+
|
|
1229
|
+
const guardScriptPath = join(
|
|
1230
|
+
CLAUDE_DIR,
|
|
1231
|
+
"scripts",
|
|
1232
|
+
"headless-guard-fast.sh",
|
|
1233
|
+
).replace(/\\/g, "/");
|
|
1234
|
+
const hasGuardHook = s.hooks.PreToolUse.some(
|
|
1235
|
+
(entry) =>
|
|
1236
|
+
Array.isArray(entry.hooks) &&
|
|
1237
|
+
entry.hooks.some(
|
|
1238
|
+
(h) =>
|
|
1239
|
+
typeof h.command === "string" &&
|
|
1240
|
+
h.command.includes("headless-guard"),
|
|
1241
|
+
),
|
|
1242
|
+
);
|
|
1243
|
+
|
|
1244
|
+
if (!hasGuardHook && existsSync(guardScriptPath.replace(/\//g, "\\"))) {
|
|
1245
|
+
s.hooks.PreToolUse.push({
|
|
1246
|
+
matcher: "Bash|Agent",
|
|
1247
|
+
hooks: [
|
|
1248
|
+
{
|
|
1249
|
+
type: "command",
|
|
1250
|
+
command: `bash "${guardScriptPath}"`,
|
|
1251
|
+
timeout: 3,
|
|
1252
|
+
},
|
|
1253
|
+
],
|
|
1254
|
+
});
|
|
1255
|
+
changed = true;
|
|
1256
|
+
} else if (hasGuardHook) {
|
|
1257
|
+
// 기존 훅 경로를 동기화된 경로로 업데이트
|
|
1258
|
+
for (const entry of s.hooks.PreToolUse) {
|
|
1259
|
+
if (!Array.isArray(entry.hooks)) continue;
|
|
1260
|
+
for (const h of entry.hooks) {
|
|
1261
|
+
if (
|
|
1262
|
+
typeof h.command === "string" &&
|
|
1263
|
+
h.command.includes("headless-guard") &&
|
|
1264
|
+
!h.command.includes(guardScriptPath)
|
|
1265
|
+
) {
|
|
1266
|
+
h.command = `bash "${guardScriptPath}"`;
|
|
1267
|
+
changed = true;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// ── PreToolUse 훅: tfx-gate-activate (Skill 감지 → A+B gate) ──
|
|
1274
|
+
const gateScriptPath = join(
|
|
1275
|
+
CLAUDE_DIR,
|
|
1276
|
+
"scripts",
|
|
1277
|
+
"tfx-gate-activate.mjs",
|
|
1278
|
+
).replace(/\\/g, "/");
|
|
1279
|
+
const hasGateHook = s.hooks.PreToolUse.some(
|
|
1280
|
+
(entry) =>
|
|
1281
|
+
Array.isArray(entry.hooks) &&
|
|
1282
|
+
entry.hooks.some(
|
|
1283
|
+
(h) =>
|
|
1284
|
+
typeof h.command === "string" &&
|
|
1285
|
+
h.command.includes("tfx-gate-activate"),
|
|
1286
|
+
),
|
|
1287
|
+
);
|
|
1288
|
+
|
|
1289
|
+
if (!hasGateHook && existsSync(gateScriptPath.replace(/\//g, "\\"))) {
|
|
1290
|
+
s.hooks.PreToolUse.push({
|
|
1291
|
+
matcher: "Skill",
|
|
1292
|
+
hooks: [
|
|
1293
|
+
{
|
|
1294
|
+
type: "command",
|
|
1295
|
+
command: `node "${gateScriptPath}"`,
|
|
1296
|
+
timeout: 2,
|
|
1297
|
+
},
|
|
1298
|
+
],
|
|
1299
|
+
});
|
|
1300
|
+
changed = true;
|
|
1301
|
+
} else if (hasGateHook) {
|
|
1302
|
+
for (const entry of s.hooks.PreToolUse) {
|
|
1303
|
+
if (!Array.isArray(entry.hooks)) continue;
|
|
1304
|
+
for (const h of entry.hooks) {
|
|
1305
|
+
if (
|
|
1306
|
+
typeof h.command === "string" &&
|
|
1307
|
+
h.command.includes("tfx-gate-activate") &&
|
|
1308
|
+
!h.command.includes(gateScriptPath)
|
|
1309
|
+
) {
|
|
1310
|
+
h.command = `node "${gateScriptPath}"`;
|
|
1311
|
+
changed = true;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
return changed;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// 1회 읽기
|
|
1321
|
+
let settings = {};
|
|
1322
|
+
if (existsSync(settingsPath)) {
|
|
1323
|
+
try {
|
|
1324
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
1325
|
+
} catch {
|
|
1326
|
+
/* 기존 설정 보존 */
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// 3개 섹션 일괄 수정 (각각 try-catch로 독립 실행)
|
|
1331
|
+
let settingsChanged = false;
|
|
1040
1332
|
try {
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
// 에러 상태이거나 락 파일이면 삭제 → 새 세션에서 fresh start
|
|
1044
|
-
if (parsed.error || name.startsWith(".")) {
|
|
1045
|
-
unlinkSync(fp);
|
|
1333
|
+
if (applyStatusLine(settings)) {
|
|
1334
|
+
settingsChanged = true;
|
|
1046
1335
|
synced++;
|
|
1047
1336
|
}
|
|
1048
|
-
} catch {
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1337
|
+
} catch {}
|
|
1338
|
+
try {
|
|
1339
|
+
if (applyAgentTeams(settings)) {
|
|
1340
|
+
settingsChanged = true;
|
|
1341
|
+
synced++;
|
|
1342
|
+
}
|
|
1343
|
+
} catch {}
|
|
1344
|
+
try {
|
|
1345
|
+
if (applyRemoteControl(settings)) {
|
|
1346
|
+
settingsChanged = true;
|
|
1347
|
+
synced++;
|
|
1348
|
+
}
|
|
1349
|
+
} catch {}
|
|
1350
|
+
try {
|
|
1351
|
+
if (applyHooks(settings)) {
|
|
1352
|
+
settingsChanged = true;
|
|
1353
|
+
synced++;
|
|
1354
|
+
}
|
|
1355
|
+
} catch {}
|
|
1053
1356
|
|
|
1054
|
-
//
|
|
1055
|
-
|
|
1357
|
+
// 1회 쓰기
|
|
1358
|
+
if (settingsChanged) {
|
|
1359
|
+
try {
|
|
1360
|
+
writeFileSync(
|
|
1361
|
+
settingsPath,
|
|
1362
|
+
JSON.stringify(settings, null, 2) + "\n",
|
|
1363
|
+
"utf8",
|
|
1364
|
+
);
|
|
1365
|
+
} catch {
|
|
1366
|
+
// settings.json 쓰기 실패 시 무시
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1056
1369
|
|
|
1057
|
-
|
|
1058
|
-
const npmBin = join(process.env.APPDATA || "", "npm");
|
|
1059
|
-
if (existsSync(npmBin)) {
|
|
1060
|
-
const bashrcPath = join(homedir(), ".bashrc");
|
|
1061
|
-
const pathExport = 'export PATH="$PATH:$APPDATA/npm"';
|
|
1062
|
-
let needsUpdate = true;
|
|
1370
|
+
// ── HUD 캐시 pre-warm (백그라운드) ──
|
|
1063
1371
|
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1372
|
+
const preWarmHudPath = join(CLAUDE_DIR, "hud", "hud-qos-status.mjs");
|
|
1373
|
+
if (existsSync(preWarmHudPath)) {
|
|
1374
|
+
const refreshFlags = [
|
|
1375
|
+
["--refresh-claude-usage"],
|
|
1376
|
+
["--refresh-codex-rate-limits"],
|
|
1377
|
+
["--refresh-gemini-quota", "--account", "gemini-main"],
|
|
1378
|
+
["--refresh-gemini-session"],
|
|
1379
|
+
];
|
|
1380
|
+
for (const args of refreshFlags) {
|
|
1381
|
+
try {
|
|
1382
|
+
const child = spawn(process.execPath, [preWarmHudPath, ...args], {
|
|
1383
|
+
detached: true,
|
|
1384
|
+
stdio: "ignore",
|
|
1385
|
+
windowsHide: true,
|
|
1386
|
+
});
|
|
1387
|
+
child.unref();
|
|
1388
|
+
} catch {
|
|
1389
|
+
/* pre-warm 실패 무시 */
|
|
1068
1390
|
}
|
|
1069
1391
|
}
|
|
1392
|
+
io.log(" \x1b[32m✓\x1b[0m HUD cache pre-warm (background)");
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
// ── Stale PID 파일 정리 (hub 좀비 방지) ──
|
|
1396
|
+
|
|
1397
|
+
const HUB_PID_FILE = join(CLAUDE_DIR, "cache", "tfx-hub", "hub.pid");
|
|
1398
|
+
if (existsSync(HUB_PID_FILE)) {
|
|
1399
|
+
try {
|
|
1400
|
+
const pidInfo = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
1401
|
+
process.kill(pidInfo.pid, 0); // 프로세스 존재 확인 (신호 미전송)
|
|
1402
|
+
} catch {
|
|
1403
|
+
try {
|
|
1404
|
+
unlinkSync(HUB_PID_FILE);
|
|
1405
|
+
} catch {} // 죽은 프로세스면 PID 파일 삭제
|
|
1406
|
+
synced++;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
// ── psmux 자동 설치 (Windows, headless 모드용) ──
|
|
1070
1411
|
|
|
1071
|
-
|
|
1072
|
-
|
|
1412
|
+
if (process.platform === "win32") {
|
|
1413
|
+
try {
|
|
1414
|
+
execFileSync("where", ["psmux"], { stdio: "ignore" });
|
|
1415
|
+
} catch {
|
|
1416
|
+
// psmux 미설치 — winget으로 자동 설치 시도
|
|
1417
|
+
io.log(" psmux 미설치 — winget으로 설치 중...");
|
|
1073
1418
|
try {
|
|
1074
|
-
|
|
1419
|
+
execFileSync(
|
|
1420
|
+
"winget",
|
|
1421
|
+
[
|
|
1422
|
+
"install",
|
|
1423
|
+
"--id",
|
|
1424
|
+
"marlocarlo.psmux",
|
|
1425
|
+
"--accept-package-agreements",
|
|
1426
|
+
"--accept-source-agreements",
|
|
1427
|
+
],
|
|
1428
|
+
{
|
|
1429
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1430
|
+
timeout: 60000,
|
|
1431
|
+
},
|
|
1432
|
+
);
|
|
1433
|
+
io.log(" \x1b[32m✓\x1b[0m psmux 설치 완료");
|
|
1075
1434
|
synced++;
|
|
1076
|
-
} catch {
|
|
1435
|
+
} catch {
|
|
1436
|
+
io.log(
|
|
1437
|
+
" \x1b[33m⚠\x1b[0m psmux 자동 설치 실패 — 수동 설치: winget install psmux",
|
|
1438
|
+
);
|
|
1439
|
+
}
|
|
1077
1440
|
}
|
|
1078
1441
|
}
|
|
1079
|
-
}
|
|
1080
1442
|
|
|
1081
|
-
// ──
|
|
1443
|
+
// ── HUD 에러 캐시 자동 클리어 (업데이트/재설치 시) ──
|
|
1082
1444
|
|
|
1083
|
-
const
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1445
|
+
const cacheDir = join(CLAUDE_DIR, "cache");
|
|
1446
|
+
const staleFiles = [
|
|
1447
|
+
"claude-usage-cache.json",
|
|
1448
|
+
".claude-refresh-lock",
|
|
1449
|
+
"codex-rate-limits-cache.json",
|
|
1450
|
+
];
|
|
1451
|
+
|
|
1452
|
+
for (const name of staleFiles) {
|
|
1453
|
+
const fp = join(cacheDir, name);
|
|
1454
|
+
if (!existsSync(fp)) continue;
|
|
1455
|
+
try {
|
|
1456
|
+
const content = readFileSync(fp, "utf8");
|
|
1457
|
+
const parsed = JSON.parse(content);
|
|
1458
|
+
// 에러 상태이거나 락 파일이면 삭제 → 새 세션에서 fresh start
|
|
1459
|
+
if (parsed.error || name.startsWith(".")) {
|
|
1460
|
+
unlinkSync(fp);
|
|
1461
|
+
synced++;
|
|
1462
|
+
}
|
|
1463
|
+
} catch {
|
|
1464
|
+
// 파싱 실패 파일도 삭제
|
|
1465
|
+
try {
|
|
1466
|
+
unlinkSync(fp);
|
|
1467
|
+
} catch {}
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1087
1470
|
|
|
1088
|
-
// ──
|
|
1471
|
+
// ── Windows bash PATH 자동 설정 ──
|
|
1472
|
+
// Codex/Gemini가 cmd에는 있지만 bash에서 못 찾는 문제 해결
|
|
1473
|
+
|
|
1474
|
+
if (process.platform === "win32") {
|
|
1475
|
+
const npmBin = join(process.env.APPDATA || "", "npm");
|
|
1476
|
+
if (existsSync(npmBin)) {
|
|
1477
|
+
const bashrcPath = join(homedir(), ".bashrc");
|
|
1478
|
+
const pathExport = 'export PATH="$PATH:$APPDATA/npm"';
|
|
1479
|
+
let needsUpdate = true;
|
|
1480
|
+
|
|
1481
|
+
if (existsSync(bashrcPath)) {
|
|
1482
|
+
const content = readFileSync(bashrcPath, "utf8");
|
|
1483
|
+
if (
|
|
1484
|
+
content.includes("APPDATA/npm") ||
|
|
1485
|
+
content.includes("APPDATA\\npm")
|
|
1486
|
+
) {
|
|
1487
|
+
needsUpdate = false;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1089
1490
|
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1491
|
+
if (needsUpdate) {
|
|
1492
|
+
const line = `\n# triflux: Codex/Gemini CLI를 bash에서 사용하기 위한 PATH 설정\n${pathExport}\n`;
|
|
1493
|
+
try {
|
|
1494
|
+
writeFileSync(
|
|
1495
|
+
bashrcPath,
|
|
1496
|
+
(existsSync(bashrcPath) ? readFileSync(bashrcPath, "utf8") : "") +
|
|
1497
|
+
line,
|
|
1498
|
+
"utf8",
|
|
1499
|
+
);
|
|
1500
|
+
synced++;
|
|
1501
|
+
} catch {}
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1096
1504
|
}
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1505
|
+
|
|
1506
|
+
// ── Codex 프로필 자동 보정 ──
|
|
1507
|
+
|
|
1508
|
+
const codexProfilesResult = ensureCodexProfiles();
|
|
1509
|
+
if (codexProfilesResult.ok && codexProfilesResult.changed > 0) {
|
|
1100
1510
|
synced++;
|
|
1101
1511
|
}
|
|
1102
|
-
} catch (error) {
|
|
1103
|
-
console.log(` \x1b[33m⚠\x1b[0m CLAUDE.md 동기화 실패: ${error.message}`);
|
|
1104
|
-
}
|
|
1105
|
-
// ── MCP 인벤토리 백그라운드 갱신 ──
|
|
1106
|
-
|
|
1107
|
-
const mcpCheck = join(PLUGIN_ROOT, "scripts", "mcp-check.mjs");
|
|
1108
|
-
if (existsSync(mcpCheck)) {
|
|
1109
|
-
const child = spawn(process.execPath, [mcpCheck], {
|
|
1110
|
-
detached: true,
|
|
1111
|
-
stdio: "ignore",
|
|
1112
|
-
windowsHide: true,
|
|
1113
|
-
});
|
|
1114
|
-
child.unref(); // 부모 프로세스와 분리 — 비동기 실행
|
|
1115
|
-
}
|
|
1116
1512
|
|
|
1117
|
-
// ──
|
|
1118
|
-
cleanupTmpFiles().catch(() => {});
|
|
1513
|
+
// ── CLAUDE.md 라우팅 섹션 자동 동기화 ──
|
|
1119
1514
|
|
|
1120
|
-
// ── npm 글로벌 패키지 동기화 ──
|
|
1121
|
-
// dev mode가 아닌 경우(npm install로 설치), 글로벌 triflux 패키지 버전을 확인하고
|
|
1122
|
-
// 로컬 버전과 다르면 업데이트를 안내한다. dev mode에서는 git 기반이므로 skip.
|
|
1123
|
-
if (pkgVersion && !isDev) {
|
|
1124
1515
|
try {
|
|
1125
|
-
const
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1516
|
+
const routingTable = getLatestRoutingTable();
|
|
1517
|
+
const projectResult = ensureTfxSection(
|
|
1518
|
+
join(PLUGIN_ROOT, "CLAUDE.md"),
|
|
1519
|
+
routingTable,
|
|
1520
|
+
);
|
|
1521
|
+
if (projectResult.action !== "unchanged") {
|
|
1522
|
+
io.log(
|
|
1523
|
+
` \x1b[32m✓\x1b[0m CLAUDE.md (project): ${projectResult.action}`,
|
|
1524
|
+
);
|
|
1525
|
+
synced++;
|
|
1135
1526
|
}
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1527
|
+
const globalResult = ensureGlobalClaudeRoutingSection(CLAUDE_DIR);
|
|
1528
|
+
if (globalResult.action !== "unchanged") {
|
|
1529
|
+
io.log(
|
|
1530
|
+
` \x1b[32m✓\x1b[0m CLAUDE.md (global): ${globalResult.action}`,
|
|
1531
|
+
);
|
|
1532
|
+
synced++;
|
|
1140
1533
|
}
|
|
1534
|
+
} catch (error) {
|
|
1535
|
+
io.log(` \x1b[33m⚠\x1b[0m CLAUDE.md 동기화 실패: ${error.message}`);
|
|
1536
|
+
}
|
|
1537
|
+
// ── MCP 인벤토리 백그라운드 갱신 ──
|
|
1538
|
+
|
|
1539
|
+
const mcpCheck = join(PLUGIN_ROOT, "scripts", "mcp-check.mjs");
|
|
1540
|
+
if (existsSync(mcpCheck)) {
|
|
1541
|
+
const child = spawn(process.execPath, [mcpCheck], {
|
|
1542
|
+
detached: true,
|
|
1543
|
+
stdio: "ignore",
|
|
1544
|
+
windowsHide: true,
|
|
1545
|
+
});
|
|
1546
|
+
child.unref(); // 부모 프로세스와 분리 — 비동기 실행
|
|
1141
1547
|
}
|
|
1142
|
-
}
|
|
1143
1548
|
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1549
|
+
// ── /tmp 임시 파일 자동 정리 (setup 지연 방지: fire-and-forget) ──
|
|
1550
|
+
cleanupTmpFiles().catch(() => {});
|
|
1551
|
+
|
|
1552
|
+
// ── npm 글로벌 패키지 동기화 ──
|
|
1553
|
+
// dev mode가 아닌 경우(npm install로 설치), 글로벌 triflux 패키지 버전을 확인하고
|
|
1554
|
+
// 로컬 버전과 다르면 업데이트를 안내한다. dev mode에서는 git 기반이므로 skip.
|
|
1555
|
+
if (pkgVersion && !isDev) {
|
|
1556
|
+
try {
|
|
1557
|
+
const globalVer = execFileSync(
|
|
1558
|
+
"npm",
|
|
1559
|
+
["list", "-g", "triflux", "--json", "--depth=0"],
|
|
1560
|
+
{
|
|
1561
|
+
encoding: "utf8",
|
|
1562
|
+
timeout: 10000,
|
|
1563
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1564
|
+
},
|
|
1565
|
+
);
|
|
1566
|
+
const parsed = JSON.parse(globalVer);
|
|
1567
|
+
const installedVer = parsed?.dependencies?.triflux?.version;
|
|
1568
|
+
if (installedVer && installedVer !== pkgVersion) {
|
|
1569
|
+
const tag = pkgVersion.includes("alpha") ? "alpha" : "latest";
|
|
1570
|
+
io.log(
|
|
1571
|
+
` npm: triflux global ${installedVer} → ${pkgVersion} (npm i -g triflux@${tag})`,
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
} catch {
|
|
1575
|
+
// npm list 실패 = 글로벌 미설치. 안내만 출력.
|
|
1576
|
+
if (pkgVersion.includes("alpha")) {
|
|
1577
|
+
io.log(
|
|
1578
|
+
" npm: triflux global 미설치 (npm i -g triflux@alpha 로 설치 가능)",
|
|
1579
|
+
);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1147
1583
|
|
|
1148
|
-
|
|
1584
|
+
if (pkgVersion) {
|
|
1585
|
+
writeMarker({ version: pkgVersion, timestamp: Date.now() });
|
|
1586
|
+
}
|
|
1149
1587
|
|
|
1150
|
-
|
|
1151
|
-
const G = "\x1b[32m";
|
|
1152
|
-
const C = "\x1b[36m";
|
|
1153
|
-
const Y = "\x1b[33m";
|
|
1154
|
-
const D = "\x1b[2m";
|
|
1155
|
-
const B = "\x1b[1m";
|
|
1156
|
-
const R = "\x1b[0m";
|
|
1588
|
+
// ── postinstall 배너 (npm install 시에만 출력) ──
|
|
1157
1589
|
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1590
|
+
if (process.env.npm_lifecycle_event === "postinstall") {
|
|
1591
|
+
const G = "\x1b[32m";
|
|
1592
|
+
const C = "\x1b[36m";
|
|
1593
|
+
const Y = "\x1b[33m";
|
|
1594
|
+
const D = "\x1b[2m";
|
|
1595
|
+
const B = "\x1b[1m";
|
|
1596
|
+
const R = "\x1b[0m";
|
|
1161
1597
|
|
|
1162
|
-
|
|
1598
|
+
const ver = (() => {
|
|
1599
|
+
return pkgVersion || "?";
|
|
1600
|
+
})();
|
|
1601
|
+
|
|
1602
|
+
io.log(`
|
|
1163
1603
|
${B}╔═══════════════════════════════════════════════╗${R}
|
|
1164
1604
|
${B}║${R} ${C}triflux${R} ${D}v${ver}${R} ${B}— Setup Complete${R} ${B}║${R}
|
|
1165
1605
|
${B}╚═══════════════════════════════════════════════╝${R}
|
|
@@ -1191,11 +1631,20 @@ ${B}Skills (Claude Code):${R}
|
|
|
1191
1631
|
${Y}!${R} 세션 재시작 후 스킬이 활성화됩니다
|
|
1192
1632
|
${D}https://github.com/tellang/triflux${R}
|
|
1193
1633
|
`);
|
|
1194
|
-
}
|
|
1634
|
+
}
|
|
1195
1635
|
|
|
1196
|
-
|
|
1636
|
+
return io.result(0);
|
|
1197
1637
|
}
|
|
1198
1638
|
|
|
1199
|
-
|
|
1200
|
-
|
|
1639
|
+
const isMain =
|
|
1640
|
+
process.argv[1] &&
|
|
1641
|
+
import.meta.url.endsWith(
|
|
1642
|
+
process.argv[1].replace(/\\/g, "/").split("/").pop(),
|
|
1643
|
+
);
|
|
1644
|
+
|
|
1645
|
+
if (isMain) {
|
|
1646
|
+
const result = await runDeferred({ argv: process.argv.slice(2) });
|
|
1647
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
1648
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
1649
|
+
process.exit(result.code);
|
|
1201
1650
|
}
|