@triflux/core 10.49.0 → 10.50.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.
Files changed (46) hide show
  1. package/hooks/agy-session-hook.mjs +1 -1
  2. package/hooks/claude-cwd-projection-refresh.mjs +7 -79
  3. package/hooks/codex-session-hook.mjs +3 -8
  4. package/hooks/hook-orchestrator.mjs +8 -648
  5. package/hooks/lib/resolve-root.mjs +1 -1
  6. package/hub/bridge.mjs +290 -51
  7. package/hub/team/claude-daemon-control.mjs +54 -6
  8. package/hub/team/claude-transcript.mjs +240 -0
  9. package/hub/team/session-context.mjs +203 -0
  10. package/hud/constants.mjs +10 -0
  11. package/hud/hud-qos-status.mjs +23 -4
  12. package/hud/providers/gemini.mjs +4 -1
  13. package/hud/renderers.mjs +10 -6
  14. package/package.json +1 -1
  15. package/scripts/lib/doctor-env-checks.mjs +0 -85
  16. package/scripts/lib/legacy-hook-cleanup.mjs +230 -0
  17. package/scripts/lib/legacy-mcp-cleanup.mjs +772 -0
  18. package/scripts/lib/mcp-guard-engine.mjs +44 -271
  19. package/scripts/lib/mcp-manifest.mjs +2 -2
  20. package/scripts/lib/session-presence.mjs +249 -0
  21. package/hooks/agent-route-guard.mjs +0 -113
  22. package/hooks/cross-review-tracker.mjs +0 -135
  23. package/hooks/cto-north-star-brief.mjs +0 -133
  24. package/hooks/error-context.mjs +0 -185
  25. package/hooks/hook-adaptive-collector.mjs +0 -88
  26. package/hooks/hook-manager.mjs +0 -426
  27. package/hooks/hook-registry.json +0 -367
  28. package/hooks/hooks.json +0 -149
  29. package/hooks/keyword-rules.json +0 -676
  30. package/hooks/mcp-config-watcher.mjs +0 -84
  31. package/hooks/permission-safe-allow.mjs +0 -267
  32. package/hooks/pipeline-stop.mjs +0 -435
  33. package/hooks/post-tool-tips.mjs +0 -306
  34. package/hooks/pre-compact-snapshot.mjs +0 -181
  35. package/hooks/safety-guard.mjs +0 -466
  36. package/hooks/session-end-cleanup.mjs +0 -360
  37. package/hooks/session-start-fast.mjs +0 -579
  38. package/hooks/session-start-lake.mjs +0 -81
  39. package/hooks/subagent-tracker.mjs +0 -281
  40. package/hooks/subagent-verifier.mjs +0 -85
  41. package/scripts/lib/cross-review-utils.mjs +0 -59
  42. package/scripts/lib/hook-utils.mjs +0 -16
  43. package/scripts/lib/keyword-rules.mjs +0 -296
  44. package/scripts/lib/mcp-gateway-health-check.mjs +0 -224
  45. package/scripts/lib/mcp-gateway-servers.mjs +0 -70
  46. package/scripts/lib/mcp-gateway-wrapper-check.mjs +0 -35
@@ -1,185 +0,0 @@
1
- #!/usr/bin/env node
2
- // hooks/error-context.mjs — PostToolUseFailure 훅
3
- //
4
- // 도구 실패 시 에러 패턴을 분석하여 해결 힌트를 additionalContext로 주입한다.
5
- // Claude가 동일 에러를 반복하지 않도록 구체적 가이드를 제공.
6
-
7
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
8
- import { join } from "node:path";
9
-
10
- // ── 에러 패턴 → 해결 힌트 매핑 ─────────────────────────────
11
- const ERROR_HINTS = [
12
- // Node.js / npm
13
- {
14
- pattern: /ENOENT.*no such file or directory/i,
15
- hint: "파일/디렉토리가 존재하지 않습니다. 경로를 확인하거나 mkdir -p로 디렉토리를 먼저 생성하세요.",
16
- },
17
- {
18
- pattern: /EACCES.*permission denied/i,
19
- hint: "권한 부족. macOS/Linux에서는 chmod/sudo, Windows에서는 관리자 권한을 확인하세요.",
20
- },
21
- {
22
- pattern: /EADDRINUSE/i,
23
- hint: "포트가 이미 사용 중입니다. lsof -i :{port} 또는 netstat -ano | findstr :{port}로 확인 후 프로세스를 종료하세요.",
24
- },
25
- {
26
- pattern: /ERR_MODULE_NOT_FOUND|Cannot find module/i,
27
- hint: "모듈을 찾을 수 없습니다. npm install을 실행하거나, import 경로에 .mjs/.js 확장자를 명시하세요.",
28
- },
29
- {
30
- pattern: /ETARGET|ERR_INVALID_PACKAGE_TARGET/i,
31
- hint: "패키지 버전 해석 실패. package.json의 exports 필드 또는 의존성 버전을 확인하세요.",
32
- },
33
- {
34
- pattern: /npm ERR! code E40[134]/i,
35
- hint: "npm 인증 오류. npm login 또는 .npmrc 토큰을 확인하세요.",
36
- },
37
-
38
- // Git
39
- {
40
- pattern: /fatal: not a git repository/i,
41
- hint: "git 저장소가 아닙니다. git init 또는 올바른 디렉토리로 이동하세요.",
42
- },
43
- {
44
- pattern: /merge conflict|CONFLICT.*Merge/i,
45
- hint: "병합 충돌 발생. 충돌 파일을 수동 해결 후 git add + git commit 하세요.",
46
- },
47
- {
48
- pattern: /rejected.*non-fast-forward/i,
49
- hint: "원격에 새 커밋이 있습니다. git pull --rebase 후 다시 push하세요.",
50
- },
51
- {
52
- pattern: /fatal: refusing to merge unrelated histories/i,
53
- hint: "--allow-unrelated-histories 플래그가 필요할 수 있습니다.",
54
- },
55
-
56
- // Python
57
- {
58
- pattern: /ModuleNotFoundError/i,
59
- hint: "Python 모듈 미설치. pip install 또는 가상환경 활성화를 확인하세요.",
60
- },
61
- {
62
- pattern: /SyntaxError.*invalid syntax/i,
63
- hint: "Python 문법 오류. Python 버전(2 vs 3) 호환성도 확인하세요.",
64
- },
65
-
66
- // Windows 특이
67
- {
68
- pattern: /is not recognized as an internal or external command/i,
69
- hint: "명령어를 찾을 수 없습니다. PATH 환경변수를 확인하거나 절대 경로를 사용하세요.",
70
- },
71
- {
72
- pattern: /execution policy/i,
73
- hint: "PowerShell 실행 정책 제한. -ExecutionPolicy Bypass 플래그를 추가하세요.",
74
- },
75
- {
76
- pattern: /The process cannot access the file because it is being used/i,
77
- hint: "파일이 다른 프로세스에 의해 잠겨 있습니다. 해당 프로세스를 종료하거나 잠시 후 재시도하세요.",
78
- },
79
-
80
- // 일반
81
- {
82
- pattern: /timeout|timed out|ETIMEDOUT/i,
83
- hint: "타임아웃 발생. 네트워크 상태를 확인하거나 timeout 값을 늘리세요.",
84
- },
85
- {
86
- pattern: /out of memory|heap|ENOMEM/i,
87
- hint: "메모리 부족. --max-old-space-size를 늘리거나 데이터 크기를 줄이세요.",
88
- },
89
- {
90
- pattern: /ECONNREFUSED/i,
91
- hint: "연결 거부. 대상 서버/서비스가 실행 중인지 확인하세요.",
92
- },
93
- ];
94
-
95
- function readStdin() {
96
- try {
97
- return readFileSync(0, "utf8");
98
- } catch {
99
- return "";
100
- }
101
- }
102
-
103
- function findHints(errorText) {
104
- const hints = [];
105
- for (const rule of ERROR_HINTS) {
106
- if (rule.pattern.test(errorText)) {
107
- hints.push(rule.hint);
108
- }
109
- }
110
- return hints;
111
- }
112
-
113
- function main() {
114
- const raw = readStdin();
115
- if (!raw.trim()) process.exit(0);
116
-
117
- let input;
118
- try {
119
- input = JSON.parse(raw);
120
- } catch {
121
- process.exit(0);
122
- }
123
-
124
- // tool_output 또는 error 필드에서 에러 텍스트 추출
125
- const errorText = [
126
- input.tool_output || "",
127
- input.error || "",
128
- input.tool_input?.command || "",
129
- JSON.stringify(input.tool_result || ""),
130
- ].join("\n");
131
-
132
- // ── reflexion 적응형 학습: safety-guard 차단을 패널티로 기록 ──
133
- const isSafetyBlock = /\[safety-guard\].*(?:BLOCKED|차단)/i.test(errorText);
134
- if (isSafetyBlock) {
135
- try {
136
- const home = process.env.HOME || process.env.USERPROFILE || "";
137
- const penaltyDir = join(home, ".triflux", "reflexion");
138
- mkdirSync(penaltyDir, { recursive: true });
139
- const penaltyFile = join(penaltyDir, "pending-penalties.jsonl");
140
- const command = input.tool_input?.command || "";
141
- const entry = {
142
- ts: new Date().toISOString(),
143
- type: "guard_block",
144
- tool: input.tool_name || "Bash",
145
- error_pattern:
146
- errorText.match(/\[.*?\]\s*(.{0,120})/)?.[1] ||
147
- errorText.slice(0, 120),
148
- command_preview: command.slice(0, 200),
149
- source: "safety-guard",
150
- };
151
- writeFileSync(penaltyFile, JSON.stringify(entry) + "\n", { flag: "a" });
152
- } catch {
153
- /* reflexion 기록 실패는 무시 — 힌트 출력에 영향 주지 않음 */
154
- }
155
- }
156
-
157
- const hints = findHints(errorText);
158
- // safety-guard 차단에는 guard가 이미 구체적 안내를 제공하므로 추가 힌트 불필요
159
- if (hints.length === 0 && !isSafetyBlock) process.exit(0);
160
-
161
- const toolName = input.tool_name || "Unknown";
162
- const parts = [];
163
- if (hints.length > 0) {
164
- parts.push(
165
- `[error-context] ${toolName} 실패 — 해결 힌트:\n` +
166
- hints.map((h) => ` → ${h}`).join("\n"),
167
- );
168
- }
169
- if (isSafetyBlock) {
170
- parts.push(
171
- "[reflexion] 이 패턴이 적응형 학습에 기록되었습니다. 다음 세션에서 동일 패턴 시 사전 차단됩니다.",
172
- );
173
- }
174
-
175
- if (parts.length === 0) process.exit(0);
176
-
177
- const output = { systemMessage: parts.join("\n") };
178
- process.stdout.write(JSON.stringify(output));
179
- }
180
-
181
- try {
182
- main();
183
- } catch {
184
- process.exit(0);
185
- }
@@ -1,88 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { existsSync, readFileSync } from "node:fs";
4
- import { basename, join } from "node:path";
5
- import { pathToFileURL } from "node:url";
6
-
7
- import { createAdaptiveEngine } from "../hub/adaptive.mjs";
8
-
9
- let engine = null;
10
- let createEngine = createAdaptiveEngine;
11
-
12
- function readStdin() {
13
- try {
14
- return readFileSync(0, "utf8");
15
- } catch {
16
- return "";
17
- }
18
- }
19
-
20
- function inferProjectSlug(cwd = process.cwd()) {
21
- const packagePath = join(cwd, "package.json");
22
- if (existsSync(packagePath)) {
23
- try {
24
- const pkg = JSON.parse(readFileSync(packagePath, "utf8"));
25
- if (typeof pkg.name === "string" && pkg.name.trim())
26
- return pkg.name.trim();
27
- } catch {}
28
- }
29
- return basename(cwd) || "default";
30
- }
31
-
32
- function getEngine() {
33
- if (engine) return engine;
34
- engine = createEngine({
35
- projectSlug: inferProjectSlug(),
36
- repoRoot: process.cwd(),
37
- });
38
- engine.startSession?.();
39
- return engine;
40
- }
41
-
42
- function buildErrorContext(event = {}) {
43
- return {
44
- exitCode: event.exitCode,
45
- stderr: String(event.stderr || "").slice(0, 500),
46
- tool: event.tool,
47
- command: String(event.command || "").slice(0, 200),
48
- timestamp: new Date().toISOString(),
49
- };
50
- }
51
-
52
- export default function hookAdaptiveCollector(event = {}) {
53
- if (Number(event.exitCode) === 0) return null;
54
- if (!event.tool || event.tool === "Read") return null;
55
-
56
- const result = getEngine().handleError(buildErrorContext(event));
57
- if (result?.diagnosed) {
58
- console.error(`[adaptive] 에러 패턴 감지: ${result.rule?.id || "unknown"}`);
59
- if (result.promoted) {
60
- console.error(`[adaptive] 규칙 승격 → Tier ${result.rule?.tier ?? "?"}`);
61
- }
62
- }
63
- return result;
64
- }
65
-
66
- export function __setAdaptiveCollectorFactoryForTests(factory) {
67
- createEngine = factory;
68
- engine = null;
69
- }
70
-
71
- export function __resetAdaptiveCollectorForTests() {
72
- createEngine = createAdaptiveEngine;
73
- engine = null;
74
- }
75
-
76
- function main() {
77
- const raw = readStdin();
78
- if (!raw.trim()) return;
79
- try {
80
- hookAdaptiveCollector(JSON.parse(raw));
81
- } catch {}
82
- }
83
-
84
- const isEntrypoint =
85
- process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url;
86
- if (isEntrypoint) {
87
- main();
88
- }
@@ -1,426 +0,0 @@
1
- #!/usr/bin/env node
2
- // hooks/hook-manager.mjs — 훅 우선순위 매니저
3
- //
4
- // 사용법:
5
- // node hook-manager.mjs scan — 현재 settings.json 훅 스캔 → JSON 리포트
6
- // node hook-manager.mjs diff — 오케스트레이터 적용 시 변경점 미리보기
7
- // node hook-manager.mjs apply — settings.json에 오케스트레이터 적용
8
- // node hook-manager.mjs restore — 백업에서 원래 settings.json 훅 복원
9
- // node hook-manager.mjs set-priority <hookId> <priority> — 특정 훅 우선순위 변경
10
- // node hook-manager.mjs toggle <hookId> — 특정 훅 활성/비활성 토글
11
- // node hook-manager.mjs status — 오케스트레이터 적용 상태 확인
12
- //
13
- // Claude 대화에서 AskUserQuestion으로 UI를 제공하며 내부적으로 이 명령들을 호출합니다.
14
-
15
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
16
- import { dirname, join } from "node:path";
17
- import { fileURLToPath } from "node:url";
18
- import { PLUGIN_ROOT } from "./lib/resolve-root.mjs";
19
-
20
- const __dirname = dirname(fileURLToPath(import.meta.url));
21
- const HOME = process.env.HOME || process.env.USERPROFILE || "";
22
- const SETTINGS_PATH = join(HOME, ".claude", "settings.json");
23
- const BACKUP_PATH = join(HOME, ".claude", "settings.hooks-backup.json");
24
- const BUNDLED_REGISTRY_PATH = join(__dirname, "hook-registry.json");
25
- const REGISTRY_PATH = join(HOME, ".claude", "cache", "hook-registry.json");
26
-
27
- function ensureUserRegistry() {
28
- if (!existsSync(REGISTRY_PATH) && existsSync(BUNDLED_REGISTRY_PATH)) {
29
- mkdirSync(dirname(REGISTRY_PATH), { recursive: true });
30
- writeFileSync(
31
- REGISTRY_PATH,
32
- readFileSync(BUNDLED_REGISTRY_PATH, "utf8"),
33
- "utf8",
34
- );
35
- }
36
- }
37
-
38
- // ── 유틸리티 ────────────────────────────────────────────────
39
-
40
- function loadJSON(path) {
41
- if (!existsSync(path)) return null;
42
- try {
43
- return JSON.parse(readFileSync(path, "utf8"));
44
- } catch {
45
- return null;
46
- }
47
- }
48
-
49
- function saveJSON(path, data) {
50
- writeFileSync(path, JSON.stringify(data, null, 2) + "\n", "utf8");
51
- }
52
-
53
- function getNodeExe() {
54
- return process.execPath || "node";
55
- }
56
-
57
- // ── scan: 현재 settings.json 훅 분석 ───────────────────────
58
-
59
- function scan() {
60
- ensureUserRegistry();
61
- const settings = loadJSON(SETTINGS_PATH);
62
- if (!settings?.hooks) {
63
- return {
64
- status: "no_hooks",
65
- message: "settings.json에 훅이 없습니다.",
66
- events: {},
67
- };
68
- }
69
-
70
- const registry = loadJSON(REGISTRY_PATH);
71
- const report = { status: "ok", events: {}, unregistered: [] };
72
-
73
- for (const [event, matchers] of Object.entries(settings.hooks)) {
74
- report.events[event] = { hooks: [], count: 0 };
75
-
76
- for (const matcher of matchers) {
77
- for (const hook of matcher.hooks || []) {
78
- const cmd = hook.command || "";
79
- const hookInfo = {
80
- event,
81
- matcher: matcher.matcher || "*",
82
- command: cmd,
83
- timeout: hook.timeout,
84
- type: hook.type || "command",
85
- source: identifySource(cmd),
86
- registryMatch: null,
87
- };
88
-
89
- // 레지스트리에서 매칭 찾기
90
- if (registry?.events?.[event]) {
91
- const match = registry.events[event].find(
92
- (r) => normalizeCmd(resolveVars(r.command)) === normalizeCmd(cmd),
93
- );
94
- if (match) {
95
- hookInfo.registryMatch = { id: match.id, priority: match.priority };
96
- } else {
97
- report.unregistered.push(hookInfo);
98
- }
99
- }
100
-
101
- report.events[event].hooks.push(hookInfo);
102
- report.events[event].count++;
103
- }
104
- }
105
- }
106
-
107
- return report;
108
- }
109
-
110
- function identifySource(cmd) {
111
- if (/triflux/i.test(cmd) || /\$\{?CLAUDE_PLUGIN_ROOT\}?/i.test(cmd))
112
- return "triflux";
113
- if (/oh-my-claudecode|omc/i.test(cmd)) return "omc";
114
- if (/session-vault/i.test(cmd)) return "session-vault";
115
- if (/compact-helper/i.test(cmd)) return "compact-helper";
116
- if (/tfx-gate/i.test(cmd)) return "omc";
117
- if (/mcp-cleanup/i.test(cmd)) return "system";
118
- return "unknown";
119
- }
120
-
121
- function normalizeCmd(cmd) {
122
- return cmd
123
- .replace(/["']/g, "")
124
- .replace(/\\/g, "/")
125
- .replace(/\s+/g, " ")
126
- .trim()
127
- .toLowerCase();
128
- }
129
-
130
- function resolveVars(cmd) {
131
- return cmd
132
- .replace(/\$\{PLUGIN_ROOT\}/g, PLUGIN_ROOT)
133
- .replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, PLUGIN_ROOT)
134
- .replace(/\$\{HOME\}/g, HOME)
135
- .replace(/\$HOME\b/g, HOME);
136
- }
137
-
138
- // ── diff: 적용 시 변경점 미리보기 ───────────────────────────
139
-
140
- function diff() {
141
- ensureUserRegistry();
142
- const settings = loadJSON(SETTINGS_PATH);
143
- if (!settings?.hooks) return { status: "no_hooks", changes: [] };
144
-
145
- const registry = loadJSON(REGISTRY_PATH);
146
- if (!registry) return { status: "no_registry", changes: [] };
147
-
148
- const changes = [];
149
- const currentEvents = Object.keys(settings.hooks);
150
- const registryEvents = Object.keys(registry.events);
151
- const allEvents = [...new Set([...currentEvents, ...registryEvents])];
152
-
153
- for (const event of allEvents) {
154
- const currentHooks = settings.hooks[event] || [];
155
- const registryHooks = registry.events[event] || [];
156
-
157
- const currentCount = currentHooks.reduce(
158
- (n, m) => n + (m.hooks?.length || 0),
159
- 0,
160
- );
161
- const registryCount = registryHooks.filter(
162
- (h) => h.enabled !== false,
163
- ).length;
164
-
165
- if (currentCount === 1 && isOrchestrator(currentHooks)) {
166
- changes.push({
167
- event,
168
- action: "already_orchestrated",
169
- currentCount,
170
- registryCount,
171
- });
172
- } else if (currentCount > 0 || registryCount > 0) {
173
- changes.push({
174
- event,
175
- action: "will_replace",
176
- currentCount,
177
- registryCount,
178
- detail: `${currentCount}개 개별 훅 → 1개 오케스트레이터 (내부 ${registryCount}개 순차 실행)`,
179
- });
180
- }
181
- }
182
-
183
- return { status: "ok", changes };
184
- }
185
-
186
- function isOrchestrator(matchers) {
187
- if (!matchers || matchers.length !== 1) return false;
188
- const hooks = matchers[0]?.hooks || [];
189
- return (
190
- hooks.length === 1 &&
191
- (hooks[0]?.command || "").includes("hook-orchestrator")
192
- );
193
- }
194
-
195
- // ── apply: 오케스트레이터 적용 ──────────────────────────────
196
-
197
- function apply() {
198
- ensureUserRegistry();
199
- const settings = loadJSON(SETTINGS_PATH);
200
- if (!settings)
201
- return { status: "error", message: "settings.json을 찾을 수 없습니다." };
202
-
203
- const registry = loadJSON(REGISTRY_PATH);
204
- if (!registry)
205
- return {
206
- status: "error",
207
- message: "hook-registry.json을 찾을 수 없습니다.",
208
- };
209
-
210
- // 백업
211
- if (settings.hooks && !existsSync(BACKUP_PATH)) {
212
- saveJSON(BACKUP_PATH, {
213
- hooks: settings.hooks,
214
- backedUpAt: new Date().toISOString(),
215
- });
216
- }
217
-
218
- // 오케스트레이터 명령 생성
219
- const nodeExe = getNodeExe();
220
- const orchestratorPath = join(PLUGIN_ROOT, "hooks", "hook-orchestrator.mjs");
221
- const orchestratorCmd = `"${nodeExe}" "${orchestratorPath}"`;
222
-
223
- // 모든 이벤트를 하나의 오케스트레이터로 통합
224
- const newHooks = {};
225
- const registryEvents = Object.keys(registry.events);
226
-
227
- // 레지스트리에 없는 기존 이벤트도 보존
228
- const allEvents = [
229
- ...new Set([...registryEvents, ...Object.keys(settings.hooks || {})]),
230
- ];
231
-
232
- for (const event of allEvents) {
233
- const registryEntries = registry.events[event] || [];
234
- const enabledEntries = registryEntries.filter((h) => h.enabled !== false);
235
-
236
- if (enabledEntries.length > 0) {
237
- // 레지스트리에 있으면 → 오케스트레이터로 교체
238
- // 가장 큰 timeout을 기준으로 오케스트레이터 timeout 설정
239
- const maxTimeout =
240
- Math.max(...enabledEntries.map((h) => h.timeout || 10)) + 5;
241
-
242
- newHooks[event] = [
243
- {
244
- matcher: "*",
245
- hooks: [
246
- {
247
- type: "command",
248
- command: orchestratorCmd,
249
- timeout: maxTimeout,
250
- },
251
- ],
252
- },
253
- ];
254
- } else {
255
- // 레지스트리에 없으면 기존 유지
256
- if (settings.hooks?.[event]) {
257
- newHooks[event] = settings.hooks[event];
258
- }
259
- }
260
- }
261
-
262
- settings.hooks = newHooks;
263
- saveJSON(SETTINGS_PATH, settings);
264
-
265
- return {
266
- status: "applied",
267
- message: `오케스트레이터 적용 완료. ${registryEvents.length}개 이벤트가 순차 실행으로 전환됩니다.`,
268
- events: registryEvents,
269
- backupPath: BACKUP_PATH,
270
- };
271
- }
272
-
273
- // ── restore: 백업에서 복원 ──────────────────────────────────
274
-
275
- function restore() {
276
- if (!existsSync(BACKUP_PATH)) {
277
- return {
278
- status: "no_backup",
279
- message: "백업 파일이 없습니다. apply 전에는 복원할 수 없습니다.",
280
- };
281
- }
282
-
283
- const backup = loadJSON(BACKUP_PATH);
284
- if (!backup?.hooks) {
285
- return { status: "error", message: "백업 파일이 손상되었습니다." };
286
- }
287
-
288
- const settings = loadJSON(SETTINGS_PATH);
289
- if (!settings)
290
- return { status: "error", message: "settings.json을 찾을 수 없습니다." };
291
-
292
- settings.hooks = backup.hooks;
293
- saveJSON(SETTINGS_PATH, settings);
294
-
295
- return {
296
- status: "restored",
297
- message: `원래 훅 설정이 복원되었습니다. (백업 시점: ${backup.backedUpAt})`,
298
- };
299
- }
300
-
301
- // ── set-priority: 우선순위 변경 ─────────────────────────────
302
-
303
- function setPriority(hookId, priority) {
304
- ensureUserRegistry();
305
- const registry = loadJSON(REGISTRY_PATH);
306
- if (!registry)
307
- return { status: "error", message: "레지스트리를 찾을 수 없습니다." };
308
-
309
- const numPriority = parseInt(priority, 10);
310
- if (Number.isNaN(numPriority))
311
- return { status: "error", message: "priority는 숫자여야 합니다." };
312
-
313
- let found = false;
314
- for (const hooks of Object.values(registry.events)) {
315
- const hook = hooks.find((h) => h.id === hookId);
316
- if (hook) {
317
- hook.priority = numPriority;
318
- found = true;
319
- break;
320
- }
321
- }
322
-
323
- if (!found)
324
- return {
325
- status: "not_found",
326
- message: `훅 '${hookId}'를 찾을 수 없습니다.`,
327
- };
328
-
329
- saveJSON(REGISTRY_PATH, registry);
330
- return {
331
- status: "ok",
332
- message: `${hookId}의 우선순위가 ${numPriority}로 변경되었습니다.`,
333
- };
334
- }
335
-
336
- // ── toggle: 활성/비활성 토글 ────────────────────────────────
337
-
338
- function toggle(hookId) {
339
- ensureUserRegistry();
340
- const registry = loadJSON(REGISTRY_PATH);
341
- if (!registry)
342
- return { status: "error", message: "레지스트리를 찾을 수 없습니다." };
343
-
344
- let found = false;
345
- let newState = false;
346
- for (const hooks of Object.values(registry.events)) {
347
- const hook = hooks.find((h) => h.id === hookId);
348
- if (hook) {
349
- hook.enabled = !(hook.enabled !== false);
350
- newState = hook.enabled;
351
- found = true;
352
- break;
353
- }
354
- }
355
-
356
- if (!found)
357
- return {
358
- status: "not_found",
359
- message: `훅 '${hookId}'를 찾을 수 없습니다.`,
360
- };
361
-
362
- saveJSON(REGISTRY_PATH, registry);
363
- return {
364
- status: "ok",
365
- message: `${hookId}: ${newState ? "활성화" : "비활성화"}`,
366
- };
367
- }
368
-
369
- // ── status: 현재 적용 상태 ──────────────────────────────────
370
-
371
- function status() {
372
- const settings = loadJSON(SETTINGS_PATH);
373
- if (!settings?.hooks) return { orchestrated: false, message: "훅 없음" };
374
-
375
- let orchestrated = 0;
376
- let individual = 0;
377
-
378
- for (const [_event, matchers] of Object.entries(settings.hooks)) {
379
- if (isOrchestrator(matchers)) {
380
- orchestrated++;
381
- } else {
382
- individual++;
383
- }
384
- }
385
-
386
- const hasBackup = existsSync(BACKUP_PATH);
387
-
388
- return {
389
- orchestrated: orchestrated > 0,
390
- orchestratedEvents: orchestrated,
391
- individualEvents: individual,
392
- hasBackup,
393
- message:
394
- orchestrated > 0
395
- ? `오케스트레이터 적용 중: ${orchestrated}개 이벤트 통합, ${individual}개 개별 유지`
396
- : `오케스트레이터 미적용. ${individual}개 이벤트가 개별 훅으로 실행 중`,
397
- };
398
- }
399
-
400
- // ── CLI 진입점 ──────────────────────────────────────────────
401
-
402
- const [, , command, ...args] = process.argv;
403
-
404
- const commands = {
405
- scan: () => scan(),
406
- diff: () => diff(),
407
- apply: () => apply(),
408
- restore: () => restore(),
409
- "set-priority": () => setPriority(args[0], args[1]),
410
- toggle: () => toggle(args[0]),
411
- status: () => status(),
412
- };
413
-
414
- if (!command || !commands[command]) {
415
- console.log(
416
- JSON.stringify({
417
- error:
418
- "사용법: node hook-manager.mjs <scan|diff|apply|restore|set-priority|toggle|status>",
419
- commands: Object.keys(commands),
420
- }),
421
- );
422
- process.exit(1);
423
- }
424
-
425
- const result = commands[command]();
426
- console.log(JSON.stringify(result, null, 2));