@wooojin/forgen 0.1.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 (268) hide show
  1. package/.claude-plugin/plugin.json +20 -0
  2. package/CHANGELOG.md +353 -0
  3. package/CONTRIBUTING.md +98 -0
  4. package/LICENSE +21 -0
  5. package/README.ja.md +469 -0
  6. package/README.ko.md +469 -0
  7. package/README.md +483 -0
  8. package/README.zh.md +469 -0
  9. package/agents/analyst.md +98 -0
  10. package/agents/architect.md +62 -0
  11. package/agents/code-reviewer.md +120 -0
  12. package/agents/code-simplifier.md +197 -0
  13. package/agents/critic.md +70 -0
  14. package/agents/debugger.md +117 -0
  15. package/agents/designer.md +131 -0
  16. package/agents/executor.md +54 -0
  17. package/agents/explore.md +145 -0
  18. package/agents/git-master.md +212 -0
  19. package/agents/performance-reviewer.md +172 -0
  20. package/agents/planner.md +29 -0
  21. package/agents/qa-tester.md +158 -0
  22. package/agents/refactoring-expert.md +168 -0
  23. package/agents/scientist.md +144 -0
  24. package/agents/security-reviewer.md +137 -0
  25. package/agents/test-engineer.md +153 -0
  26. package/agents/verifier.md +133 -0
  27. package/agents/writer.md +184 -0
  28. package/commands/api-design.md +268 -0
  29. package/commands/architecture-decision.md +314 -0
  30. package/commands/ci-cd.md +270 -0
  31. package/commands/code-review.md +233 -0
  32. package/commands/compound.md +117 -0
  33. package/commands/database.md +263 -0
  34. package/commands/debug-detective.md +99 -0
  35. package/commands/docker.md +274 -0
  36. package/commands/documentation.md +276 -0
  37. package/commands/ecomode.md +51 -0
  38. package/commands/frontend.md +271 -0
  39. package/commands/git-master.md +90 -0
  40. package/commands/incident-response.md +292 -0
  41. package/commands/migrate.md +101 -0
  42. package/commands/performance.md +288 -0
  43. package/commands/refactor.md +105 -0
  44. package/commands/security-review.md +288 -0
  45. package/commands/tdd.md +183 -0
  46. package/commands/testing-strategy.md +265 -0
  47. package/dist/cli.d.ts +2 -0
  48. package/dist/cli.js +295 -0
  49. package/dist/core/auto-compound-runner.d.ts +12 -0
  50. package/dist/core/auto-compound-runner.js +460 -0
  51. package/dist/core/config-hooks.d.ts +10 -0
  52. package/dist/core/config-hooks.js +112 -0
  53. package/dist/core/config-injector.d.ts +50 -0
  54. package/dist/core/config-injector.js +455 -0
  55. package/dist/core/doctor.d.ts +1 -0
  56. package/dist/core/doctor.js +163 -0
  57. package/dist/core/errors.d.ts +81 -0
  58. package/dist/core/errors.js +133 -0
  59. package/dist/core/global-config.d.ts +43 -0
  60. package/dist/core/global-config.js +25 -0
  61. package/dist/core/harness.d.ts +24 -0
  62. package/dist/core/harness.js +621 -0
  63. package/dist/core/init.d.ts +7 -0
  64. package/dist/core/init.js +37 -0
  65. package/dist/core/inspect-cli.d.ts +7 -0
  66. package/dist/core/inspect-cli.js +47 -0
  67. package/dist/core/legacy-detector.d.ts +33 -0
  68. package/dist/core/legacy-detector.js +66 -0
  69. package/dist/core/logger.d.ts +34 -0
  70. package/dist/core/logger.js +121 -0
  71. package/dist/core/mcp-config.d.ts +44 -0
  72. package/dist/core/mcp-config.js +177 -0
  73. package/dist/core/notepad.d.ts +31 -0
  74. package/dist/core/notepad.js +88 -0
  75. package/dist/core/paths.d.ts +85 -0
  76. package/dist/core/paths.js +101 -0
  77. package/dist/core/plugin-detector.d.ts +44 -0
  78. package/dist/core/plugin-detector.js +226 -0
  79. package/dist/core/runtime-detector.d.ts +8 -0
  80. package/dist/core/runtime-detector.js +49 -0
  81. package/dist/core/scope-resolver.d.ts +8 -0
  82. package/dist/core/scope-resolver.js +45 -0
  83. package/dist/core/session-logger.d.ts +6 -0
  84. package/dist/core/session-logger.js +111 -0
  85. package/dist/core/session-store.d.ts +28 -0
  86. package/dist/core/session-store.js +218 -0
  87. package/dist/core/settings-lock.d.ts +18 -0
  88. package/dist/core/settings-lock.js +125 -0
  89. package/dist/core/spawn.d.ts +3 -0
  90. package/dist/core/spawn.js +135 -0
  91. package/dist/core/types.d.ts +108 -0
  92. package/dist/core/types.js +1 -0
  93. package/dist/core/uninstall.d.ts +4 -0
  94. package/dist/core/uninstall.js +307 -0
  95. package/dist/core/v1-bootstrap.d.ts +26 -0
  96. package/dist/core/v1-bootstrap.js +155 -0
  97. package/dist/engine/compound-cli.d.ts +24 -0
  98. package/dist/engine/compound-cli.js +250 -0
  99. package/dist/engine/compound-extractor.d.ts +68 -0
  100. package/dist/engine/compound-extractor.js +860 -0
  101. package/dist/engine/compound-lifecycle.d.ts +32 -0
  102. package/dist/engine/compound-lifecycle.js +305 -0
  103. package/dist/engine/compound-loop.d.ts +32 -0
  104. package/dist/engine/compound-loop.js +511 -0
  105. package/dist/engine/match-eval-log.d.ts +139 -0
  106. package/dist/engine/match-eval-log.js +270 -0
  107. package/dist/engine/phrase-blocklist.d.ts +119 -0
  108. package/dist/engine/phrase-blocklist.js +208 -0
  109. package/dist/engine/skill-promoter.d.ts +20 -0
  110. package/dist/engine/skill-promoter.js +115 -0
  111. package/dist/engine/solution-format.d.ts +160 -0
  112. package/dist/engine/solution-format.js +432 -0
  113. package/dist/engine/solution-index.d.ts +13 -0
  114. package/dist/engine/solution-index.js +252 -0
  115. package/dist/engine/solution-matcher.d.ts +364 -0
  116. package/dist/engine/solution-matcher.js +656 -0
  117. package/dist/engine/solution-writer.d.ts +76 -0
  118. package/dist/engine/solution-writer.js +157 -0
  119. package/dist/engine/term-matcher.d.ts +81 -0
  120. package/dist/engine/term-matcher.js +268 -0
  121. package/dist/engine/term-normalizer.d.ts +116 -0
  122. package/dist/engine/term-normalizer.js +171 -0
  123. package/dist/fgx.d.ts +6 -0
  124. package/dist/fgx.js +42 -0
  125. package/dist/forge/cli.d.ts +11 -0
  126. package/dist/forge/cli.js +100 -0
  127. package/dist/forge/evidence-processor.d.ts +21 -0
  128. package/dist/forge/evidence-processor.js +87 -0
  129. package/dist/forge/mismatch-detector.d.ts +44 -0
  130. package/dist/forge/mismatch-detector.js +83 -0
  131. package/dist/forge/onboarding-cli.d.ts +6 -0
  132. package/dist/forge/onboarding-cli.js +89 -0
  133. package/dist/forge/onboarding.d.ts +25 -0
  134. package/dist/forge/onboarding.js +122 -0
  135. package/dist/hooks/compound-reflection.d.ts +45 -0
  136. package/dist/hooks/compound-reflection.js +82 -0
  137. package/dist/hooks/context-guard.d.ts +24 -0
  138. package/dist/hooks/context-guard.js +156 -0
  139. package/dist/hooks/dangerous-patterns.json +18 -0
  140. package/dist/hooks/db-guard.d.ts +17 -0
  141. package/dist/hooks/db-guard.js +105 -0
  142. package/dist/hooks/hook-config.d.ts +29 -0
  143. package/dist/hooks/hook-config.js +92 -0
  144. package/dist/hooks/hook-registry.d.ts +43 -0
  145. package/dist/hooks/hook-registry.js +31 -0
  146. package/dist/hooks/hooks-generator.d.ts +49 -0
  147. package/dist/hooks/hooks-generator.js +99 -0
  148. package/dist/hooks/intent-classifier.d.ts +12 -0
  149. package/dist/hooks/intent-classifier.js +62 -0
  150. package/dist/hooks/keyword-detector.d.ts +25 -0
  151. package/dist/hooks/keyword-detector.js +389 -0
  152. package/dist/hooks/notepad-injector.d.ts +18 -0
  153. package/dist/hooks/notepad-injector.js +51 -0
  154. package/dist/hooks/permission-handler.d.ts +14 -0
  155. package/dist/hooks/permission-handler.js +114 -0
  156. package/dist/hooks/post-tool-failure.d.ts +11 -0
  157. package/dist/hooks/post-tool-failure.js +118 -0
  158. package/dist/hooks/post-tool-handlers.d.ts +17 -0
  159. package/dist/hooks/post-tool-handlers.js +115 -0
  160. package/dist/hooks/post-tool-use.d.ts +29 -0
  161. package/dist/hooks/post-tool-use.js +151 -0
  162. package/dist/hooks/pre-compact.d.ts +10 -0
  163. package/dist/hooks/pre-compact.js +165 -0
  164. package/dist/hooks/pre-tool-use.d.ts +31 -0
  165. package/dist/hooks/pre-tool-use.js +325 -0
  166. package/dist/hooks/prompt-injection-filter.d.ts +56 -0
  167. package/dist/hooks/prompt-injection-filter.js +287 -0
  168. package/dist/hooks/rate-limiter.d.ts +21 -0
  169. package/dist/hooks/rate-limiter.js +86 -0
  170. package/dist/hooks/secret-filter.d.ts +14 -0
  171. package/dist/hooks/secret-filter.js +65 -0
  172. package/dist/hooks/session-recovery.d.ts +27 -0
  173. package/dist/hooks/session-recovery.js +406 -0
  174. package/dist/hooks/shared/atomic-write.d.ts +41 -0
  175. package/dist/hooks/shared/atomic-write.js +148 -0
  176. package/dist/hooks/shared/context-budget.d.ts +37 -0
  177. package/dist/hooks/shared/context-budget.js +45 -0
  178. package/dist/hooks/shared/file-lock.d.ts +56 -0
  179. package/dist/hooks/shared/file-lock.js +253 -0
  180. package/dist/hooks/shared/hook-response.d.ts +33 -0
  181. package/dist/hooks/shared/hook-response.js +62 -0
  182. package/dist/hooks/shared/injection-caps.d.ts +39 -0
  183. package/dist/hooks/shared/injection-caps.js +52 -0
  184. package/dist/hooks/shared/plugin-signal.d.ts +23 -0
  185. package/dist/hooks/shared/plugin-signal.js +104 -0
  186. package/dist/hooks/shared/read-stdin.d.ts +8 -0
  187. package/dist/hooks/shared/read-stdin.js +63 -0
  188. package/dist/hooks/shared/sanitize-id.d.ts +7 -0
  189. package/dist/hooks/shared/sanitize-id.js +9 -0
  190. package/dist/hooks/shared/sanitize.d.ts +7 -0
  191. package/dist/hooks/shared/sanitize.js +22 -0
  192. package/dist/hooks/skill-injector.d.ts +38 -0
  193. package/dist/hooks/skill-injector.js +285 -0
  194. package/dist/hooks/slop-detector.d.ts +18 -0
  195. package/dist/hooks/slop-detector.js +93 -0
  196. package/dist/hooks/solution-injector.d.ts +58 -0
  197. package/dist/hooks/solution-injector.js +436 -0
  198. package/dist/hooks/subagent-tracker.d.ts +10 -0
  199. package/dist/hooks/subagent-tracker.js +90 -0
  200. package/dist/i18n/index.d.ts +43 -0
  201. package/dist/i18n/index.js +224 -0
  202. package/dist/lib.d.ts +14 -0
  203. package/dist/lib.js +14 -0
  204. package/dist/mcp/server.d.ts +8 -0
  205. package/dist/mcp/server.js +40 -0
  206. package/dist/mcp/solution-reader.d.ts +90 -0
  207. package/dist/mcp/solution-reader.js +273 -0
  208. package/dist/mcp/tools.d.ts +16 -0
  209. package/dist/mcp/tools.js +302 -0
  210. package/dist/preset/facet-catalog.d.ts +17 -0
  211. package/dist/preset/facet-catalog.js +46 -0
  212. package/dist/preset/preset-manager.d.ts +31 -0
  213. package/dist/preset/preset-manager.js +111 -0
  214. package/dist/renderer/inspect-renderer.d.ts +11 -0
  215. package/dist/renderer/inspect-renderer.js +123 -0
  216. package/dist/renderer/rule-renderer.d.ts +18 -0
  217. package/dist/renderer/rule-renderer.js +159 -0
  218. package/dist/store/evidence-store.d.ts +23 -0
  219. package/dist/store/evidence-store.js +58 -0
  220. package/dist/store/profile-store.d.ts +12 -0
  221. package/dist/store/profile-store.js +53 -0
  222. package/dist/store/recommendation-store.d.ts +22 -0
  223. package/dist/store/recommendation-store.js +64 -0
  224. package/dist/store/rule-store.d.ts +22 -0
  225. package/dist/store/rule-store.js +62 -0
  226. package/dist/store/session-state-store.d.ts +11 -0
  227. package/dist/store/session-state-store.js +44 -0
  228. package/dist/store/types.d.ts +159 -0
  229. package/dist/store/types.js +7 -0
  230. package/hooks/hook-registry.json +21 -0
  231. package/hooks/hooks.json +185 -0
  232. package/package.json +89 -0
  233. package/plugin.json +20 -0
  234. package/scripts/postinstall.js +826 -0
  235. package/skills/api-design/SKILL.md +262 -0
  236. package/skills/architecture-decision/SKILL.md +309 -0
  237. package/skills/ci-cd/SKILL.md +264 -0
  238. package/skills/code-review/SKILL.md +228 -0
  239. package/skills/compound/SKILL.md +101 -0
  240. package/skills/database/SKILL.md +257 -0
  241. package/skills/debug-detective/SKILL.md +95 -0
  242. package/skills/docker/SKILL.md +268 -0
  243. package/skills/documentation/SKILL.md +270 -0
  244. package/skills/ecomode/SKILL.md +46 -0
  245. package/skills/frontend/SKILL.md +265 -0
  246. package/skills/git-master/SKILL.md +86 -0
  247. package/skills/incident-response/SKILL.md +286 -0
  248. package/skills/migrate/SKILL.md +96 -0
  249. package/skills/performance/SKILL.md +282 -0
  250. package/skills/refactor/SKILL.md +100 -0
  251. package/skills/security-review/SKILL.md +282 -0
  252. package/skills/tdd/SKILL.md +178 -0
  253. package/skills/testing-strategy/SKILL.md +260 -0
  254. package/starter-pack/solutions/starter-api-error-responses.md +37 -0
  255. package/starter-pack/solutions/starter-async-patterns.md +40 -0
  256. package/starter-pack/solutions/starter-caching-strategy.md +40 -0
  257. package/starter-pack/solutions/starter-code-review-checklist.md +39 -0
  258. package/starter-pack/solutions/starter-debugging-systematic.md +40 -0
  259. package/starter-pack/solutions/starter-dependency-injection.md +40 -0
  260. package/starter-pack/solutions/starter-error-handling-patterns.md +38 -0
  261. package/starter-pack/solutions/starter-git-atomic-commits.md +36 -0
  262. package/starter-pack/solutions/starter-input-validation.md +40 -0
  263. package/starter-pack/solutions/starter-n-plus-one-queries.md +37 -0
  264. package/starter-pack/solutions/starter-refactor-safely.md +38 -0
  265. package/starter-pack/solutions/starter-secret-management.md +37 -0
  266. package/starter-pack/solutions/starter-separation-of-concerns.md +36 -0
  267. package/starter-pack/solutions/starter-tdd-red-green-refactor.md +40 -0
  268. package/starter-pack/solutions/starter-typescript-strict-types.md +39 -0
@@ -0,0 +1,826 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Forgen — postinstall script
4
+ *
5
+ * npm i -g forgen 시 자동 실행.
6
+ * forgen CLI(harness)를 거치지 않고 claude를 직접 실행해도
7
+ * 슬래시 명령, 훅, 디렉토리 구조가 모두 동작하도록 보장합니다.
8
+ *
9
+ * 크로스 플랫폼 지원: Windows, macOS, Linux (sudo 포함)
10
+ *
11
+ * 설계 결정:
12
+ * - forge overlay 없이 기본 스킬만 설치 (overlay는 harness 실행 시 적용)
13
+ * - 사용자가 수정한 파일(<!-- forgen-managed --> 마커 없음)은 보존
14
+ * - settings.json의 기존 non-forgen 설정은 보존
15
+ * - 실패해도 npm install을 깨뜨리지 않음 (silent failure)
16
+ */
17
+
18
+ import { readFileSync, readdirSync, writeFileSync, mkdirSync, existsSync, rmSync, symlinkSync, cpSync, lstatSync, statSync, copyFileSync, renameSync } from 'node:fs';
19
+ import { join, dirname } from 'node:path';
20
+ import { homedir, platform } from 'node:os';
21
+ import { execFileSync } from 'node:child_process';
22
+ import { fileURLToPath } from 'node:url';
23
+
24
+ const __dirname = dirname(fileURLToPath(import.meta.url));
25
+ const PKG_ROOT = join(__dirname, '..');
26
+ const IS_WINDOWS = platform() === 'win32';
27
+
28
+ /** SUDO_USER 유효성 검증 — 커맨드 인젝션 방지 */
29
+ const SAFE_USERNAME_RE = /^[a-zA-Z0-9_-]+$/;
30
+ function getSafeSudoUser() {
31
+ const sudoUser = process.env.SUDO_USER;
32
+ if (!sudoUser) return null;
33
+ if (!SAFE_USERNAME_RE.test(sudoUser)) return null;
34
+ return sudoUser;
35
+ }
36
+
37
+ /**
38
+ * sudo npm i -g 시 homedir()이 /root를 반환하는 문제 해결.
39
+ * SUDO_USER가 있으면 실제 유저의 홈 디렉토리를 찾는다.
40
+ * Windows에서는 sudo가 없으므로 homedir() 그대로 사용.
41
+ *
42
+ * 보안: execFileSync를 사용하여 쉘 보간 없이 실행.
43
+ */
44
+ function resolveHome() {
45
+ if (IS_WINDOWS) return homedir();
46
+
47
+ const sudoUser = getSafeSudoUser();
48
+ if (sudoUser && process.getuid?.() === 0) {
49
+ try {
50
+ if (platform() === 'darwin') {
51
+ const out = execFileSync('dscl', ['.', '-read', `/Users/${sudoUser}`, 'NFSHomeDirectory'], { encoding: 'utf-8' });
52
+ const home = out.trim().split(':').pop()?.trim();
53
+ if (home) return home;
54
+ } else {
55
+ const out = execFileSync('getent', ['passwd', sudoUser], { encoding: 'utf-8' });
56
+ const home = out.trim().split(':')[5];
57
+ if (home) return home;
58
+ }
59
+ } catch { /* fallback */ }
60
+ return platform() === 'darwin'
61
+ ? join('/Users', sudoUser)
62
+ : join('/home', sudoUser);
63
+ }
64
+ return homedir();
65
+ }
66
+
67
+ /**
68
+ * sudo로 생성된 파일/디렉토리를 실제 유저 소유로 변경.
69
+ * 이렇게 하지 않으면 유저가 나중에 settings.json 등을 수정할 수 없음.
70
+ *
71
+ * 보안: execFileSync를 사용하여 쉘 보간 없이 실행.
72
+ */
73
+ function fixOwnership(...paths) {
74
+ if (IS_WINDOWS) return;
75
+ const sudoUser = getSafeSudoUser();
76
+ if (!sudoUser || process.getuid?.() !== 0) return;
77
+
78
+ try {
79
+ const uid = execFileSync('id', ['-u', sudoUser], { encoding: 'utf-8' }).trim();
80
+ const gid = execFileSync('id', ['-g', sudoUser], { encoding: 'utf-8' }).trim();
81
+ for (const p of paths) {
82
+ if (existsSync(p)) {
83
+ execFileSync('chown', ['-R', `${uid}:${gid}`, p], { stdio: 'ignore' });
84
+ }
85
+ }
86
+ } catch { /* best effort */ }
87
+ }
88
+
89
+ const HOME = resolveHome();
90
+
91
+ // ── Paths ──
92
+ const SKILLS_DIR = join(PKG_ROOT, 'commands');
93
+ const DIST_HOOKS = join(PKG_ROOT, 'dist', 'hooks');
94
+ const COMMANDS_DIR = join(HOME, '.claude', 'commands', 'forgen');
95
+ const CLAUDE_DIR = join(HOME, '.claude');
96
+ const PLUGINS_DIR = join(CLAUDE_DIR, 'plugins');
97
+ const SETTINGS_PATH = join(CLAUDE_DIR, 'settings.json');
98
+ const COMPOUND_HOME = join(HOME, '.forgen');
99
+
100
+ // ── 0.5. Migrate legacy storage (~/.tenetx/, ~/.compound/) → ~/.forgen/ ──
101
+ function migrateLegacyStorage() {
102
+ const legacyDirs = [join(HOME, '.tenetx'), join(HOME, '.compound')];
103
+ for (const legacyHome of legacyDirs) {
104
+ try { if (lstatSync(legacyHome).isSymbolicLink()) continue; } catch { continue; }
105
+ if (!existsSync(legacyHome) || !statSync(legacyHome).isDirectory()) continue;
106
+
107
+ mkdirSync(COMPOUND_HOME, { recursive: true });
108
+ try {
109
+ for (const entry of readdirSync(legacyHome, { withFileTypes: true })) {
110
+ const src = join(legacyHome, entry.name);
111
+ const dest = join(COMPOUND_HOME, entry.name);
112
+ if (existsSync(dest)) continue;
113
+ if (entry.isDirectory()) cpSync(src, dest, { recursive: true });
114
+ else if (entry.isFile()) copyFileSync(src, dest);
115
+ }
116
+ } catch { /* ignore copy errors */ }
117
+
118
+ const backupPath = legacyHome + '.bak';
119
+ try {
120
+ if (!existsSync(backupPath)) {
121
+ renameSync(legacyHome, backupPath);
122
+ symlinkSync(COMPOUND_HOME, legacyHome, 'dir');
123
+ }
124
+ } catch { /* ignore symlink errors */ }
125
+ }
126
+ }
127
+
128
+ // ── 1. Ensure directories ──
129
+ function ensureDirectories() {
130
+ const dirs = [
131
+ COMPOUND_HOME,
132
+ join(COMPOUND_HOME, 'me'),
133
+ join(COMPOUND_HOME, 'me', 'solutions'),
134
+ join(COMPOUND_HOME, 'me', 'behavior'),
135
+ join(COMPOUND_HOME, 'me', 'rules'),
136
+ join(COMPOUND_HOME, 'me', 'skills'),
137
+ join(COMPOUND_HOME, 'sessions'),
138
+ join(COMPOUND_HOME, 'state'),
139
+ join(COMPOUND_HOME, 'handoffs'),
140
+ join(COMPOUND_HOME, 'lab'),
141
+ CLAUDE_DIR,
142
+ ];
143
+ for (const dir of dirs) {
144
+ mkdirSync(dir, { recursive: true });
145
+ }
146
+ }
147
+
148
+ // ── 1.5. Starter Knowledge Pack ──
149
+ // 새 사용자(솔루션 < 5개)에게 범용 개발 패턴 솔루션을 기본 제공
150
+ function installStarterPack() {
151
+ const solutionsDir = join(COMPOUND_HOME, 'me', 'solutions');
152
+ const starterDir = join(PKG_ROOT, 'starter-pack', 'solutions');
153
+
154
+ if (!existsSync(starterDir)) return 0;
155
+
156
+ // 기존 솔루션이 5개 이상이면 설치하지 않음 (이미 사용 중인 유저)
157
+ let existing = 0;
158
+ if (existsSync(solutionsDir)) {
159
+ existing = readdirSync(solutionsDir).filter(f => f.endsWith('.md')).length;
160
+ }
161
+ if (existing >= 5) return 0;
162
+
163
+ mkdirSync(solutionsDir, { recursive: true });
164
+ const starterFiles = readdirSync(starterDir).filter(f => f.endsWith('.md'));
165
+ let installed = 0;
166
+
167
+ for (const file of starterFiles) {
168
+ const dest = join(solutionsDir, file);
169
+ if (!existsSync(dest)) {
170
+ cpSync(join(starterDir, file), dest);
171
+ installed++;
172
+ }
173
+ }
174
+
175
+ return installed;
176
+ }
177
+
178
+ // ── 2. Register as Claude Code plugin ──
179
+ // omc, claude-hud 등 동작하는 플러그인의 실제 구조를 그대로 따름:
180
+ // .claude-plugin/plugin.json — 메타데이터 + skills 경로
181
+ // hooks/hooks.json — ${CLAUDE_PLUGIN_ROOT} 기반 훅 정의
182
+ // skills/{name}/SKILL.md — 스킬 파일 (서브디렉토리 구조)
183
+ // commands/*.md — 슬래시 커맨드
184
+ //
185
+ // 설계 결정: 캐시 디렉토리를 PKG_ROOT로 symlink하여 dist/, node_modules/ 접근 보장.
186
+ // symlink 실패 시 (sudo, cross-device 등) 필수 파일만 복사.
187
+ function registerPlugin() {
188
+ // .claude-plugin/plugin.json 필수 — 없으면 표준 구조가 아님
189
+ if (!existsSync(join(PKG_ROOT, '.claude-plugin', 'plugin.json'))) return false;
190
+
191
+ const pkg = JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf-8'));
192
+ const version = pkg.version ?? '0.0.0';
193
+
194
+ // skills/ 디렉토리 생성 (commands/*.md → skills/{name}/SKILL.md)
195
+ generateSkillsDir();
196
+
197
+ // 캐시 경로: ~/.claude/plugins/cache/forgen-local/forgen/{version}/
198
+ const cacheParent = join(PLUGINS_DIR, 'cache', 'forgen-local', 'forgen');
199
+ const CACHE_DIR = join(cacheParent, version);
200
+
201
+ // 이전 잔재 완전 제거
202
+ try { rmSync(cacheParent, { recursive: true, force: true }); } catch { /* ignore */ }
203
+ mkdirSync(join(cacheParent), { recursive: true });
204
+
205
+ // 1차: symlink (개발 환경, dist/node_modules 접근 가능)
206
+ let linked = false;
207
+ try {
208
+ symlinkSync(PKG_ROOT, CACHE_DIR, 'dir');
209
+ linked = true;
210
+ } catch {
211
+ // symlink 실패 → 복사 fallback
212
+ }
213
+
214
+ if (!linked) {
215
+ // 2차: 필수 디렉토리 복사
216
+ mkdirSync(CACHE_DIR, { recursive: true });
217
+ const copyDirs = ['.claude-plugin', 'hooks', 'skills', 'commands', 'agents'];
218
+ for (const dir of copyDirs) {
219
+ const src = join(PKG_ROOT, dir);
220
+ if (existsSync(src)) {
221
+ cpSync(src, join(CACHE_DIR, dir), { recursive: true });
222
+ }
223
+ }
224
+ // dist/ 복사 (훅 실행에 필요)
225
+ if (existsSync(join(PKG_ROOT, 'dist'))) {
226
+ cpSync(join(PKG_ROOT, 'dist'), join(CACHE_DIR, 'dist'), { recursive: true });
227
+ }
228
+ // 핵심 의존성 복사 (symlink 실패 시 필요)
229
+ const coreDeps = ['js-yaml', '@modelcontextprotocol', 'zod'];
230
+ mkdirSync(join(CACHE_DIR, 'node_modules'), { recursive: true });
231
+ for (const dep of coreDeps) {
232
+ const depSrc = join(PKG_ROOT, 'node_modules', dep);
233
+ if (existsSync(depSrc)) {
234
+ cpSync(depSrc, join(CACHE_DIR, 'node_modules', dep), { recursive: true });
235
+ }
236
+ }
237
+ }
238
+
239
+ // installed_plugins.json에 등록
240
+ const installedPath = join(PLUGINS_DIR, 'installed_plugins.json');
241
+ let installed = { version: 2, plugins: {} };
242
+ if (existsSync(installedPath)) {
243
+ try { installed = JSON.parse(readFileSync(installedPath, 'utf-8')); } catch { /* ignore */ }
244
+ }
245
+
246
+ const pluginKey = 'forgen@forgen-local';
247
+ installed.plugins = installed.plugins ?? {};
248
+ installed.plugins[pluginKey] = [{
249
+ scope: 'user',
250
+ installPath: CACHE_DIR,
251
+ version,
252
+ installedAt: new Date().toISOString(),
253
+ lastUpdated: new Date().toISOString(),
254
+ }];
255
+
256
+ mkdirSync(PLUGINS_DIR, { recursive: true });
257
+ writeFileSync(installedPath, JSON.stringify(installed, null, 2));
258
+
259
+ return true;
260
+ }
261
+
262
+ /** settings 객체에 enabledPlugins를 적용합니다 (settings.json 쓰기는 main에서 일괄 수행). */
263
+ function applyPluginSettings(settings) {
264
+ const pluginKey = 'forgen@forgen-local';
265
+ const enabled = settings.enabledPlugins ?? {};
266
+ enabled[pluginKey] = true;
267
+ settings.enabledPlugins = enabled;
268
+ }
269
+
270
+ /**
271
+ * 설치된 다른 플러그인을 감지하여 겹치는 스킬 목록을 반환.
272
+ * Returns Map<skillName, pluginName>.
273
+ *
274
+ * 설계 결정: 빌드 이전 postinstall 단계에서 실행되므로
275
+ * TypeScript 소스 import 없이 순수 파일시스템 체크만 사용.
276
+ */
277
+ function detectPluginConflicts() {
278
+ const conflicts = new Map();
279
+
280
+ // oh-my-claudecode: ~/.omc 또는 .omc(프로젝트 루트) 존재 여부 확인
281
+ const omcGlobal = join(HOME, '.omc');
282
+ const omcLocal = join(process.cwd(), '.omc');
283
+ if (existsSync(omcGlobal) || existsSync(omcLocal)) {
284
+ const omcSkills = [
285
+ 'autopilot', 'team', 'code-review', 'tdd', 'debug-detective',
286
+ 'refactor', 'security-review', 'git-master', 'migrate', 'pipeline', 'ultrawork',
287
+ ];
288
+ for (const skill of omcSkills) {
289
+ conflicts.set(skill, 'oh-my-claudecode');
290
+ }
291
+ }
292
+
293
+ // claude-mem: ~/.claude-mem 존재 여부 확인
294
+ const claudeMem = join(HOME, '.claude-mem');
295
+ if (existsSync(claudeMem)) {
296
+ // claude-mem과 겹치는 스킬이 추가되면 여기에 등록
297
+ }
298
+
299
+ // superpowers: ~/.codex/superpowers/ 존재 여부 확인
300
+ const superpowers = join(HOME, '.codex', 'superpowers');
301
+ if (existsSync(superpowers)) {
302
+ for (const skill of ['tdd', 'debug-detective', 'refactor', 'code-review']) {
303
+ conflicts.set(skill, 'superpowers');
304
+ }
305
+ }
306
+
307
+ // feature-dev (official Anthropic plugin): ~/.claude/plugins/feature-dev/ 존재 여부 확인
308
+ const featureDev = join(HOME, '.claude', 'plugins', 'feature-dev');
309
+ if (existsSync(featureDev)) {
310
+ conflicts.set('pipeline', 'feature-dev');
311
+ }
312
+
313
+ // code-review plugin (official Anthropic plugin): ~/.claude/plugins/code-review/ 존재 여부 확인
314
+ const codeReviewPlugin = join(HOME, '.claude', 'plugins', 'code-review');
315
+ if (existsSync(codeReviewPlugin)) {
316
+ conflicts.set('code-review', 'code-review-plugin');
317
+ }
318
+
319
+ // commit-commands (official Anthropic plugin): ~/.claude/plugins/commit-commands/ 존재 여부 확인
320
+ const commitCommands = join(HOME, '.claude', 'plugins', 'commit-commands');
321
+ if (existsSync(commitCommands)) {
322
+ conflicts.set('git-master', 'commit-commands');
323
+ }
324
+
325
+ return conflicts;
326
+ }
327
+
328
+ /**
329
+ * commands/*.md → skills/{name}/SKILL.md 변환.
330
+ * Claude Code 플러그인은 skills/{name}/SKILL.md 구조로 스킬을 인식.
331
+ */
332
+ function generateSkillsDir() {
333
+ const skillsSrc = join(PKG_ROOT, 'commands');
334
+ const skillsDst = join(PKG_ROOT, 'skills');
335
+ if (!existsSync(skillsSrc)) return;
336
+
337
+ // 기존 skills/ 제거 후 재생성
338
+ try { rmSync(skillsDst, { recursive: true, force: true }); } catch { /* ignore */ }
339
+
340
+ const conflicts = detectPluginConflicts();
341
+ let skipped = 0;
342
+
343
+ for (const file of readdirSync(skillsSrc).filter(f => f.endsWith('.md'))) {
344
+ const name = file.replace('.md', '');
345
+
346
+ // 다른 플러그인이 동일 스킬을 제공하면 건너뜀
347
+ if (conflicts.has(name)) {
348
+ const pluginName = conflicts.get(name);
349
+ console.log(`[forgen] Skipping skill "${name}" — provided by ${pluginName}`);
350
+ skipped++;
351
+ continue;
352
+ }
353
+
354
+ const raw = readFileSync(join(skillsSrc, file), 'utf-8');
355
+
356
+ // description 추출
357
+ const descMatch = raw.match(/description:\s*(.+)/);
358
+ const desc = descMatch?.[1]?.trim() ?? name;
359
+
360
+ // frontmatter 이후 본문 추출
361
+ const bodyMatch = raw.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
362
+ const body = bodyMatch?.[1]?.trim() ?? raw;
363
+
364
+ // skills/{name}/SKILL.md 생성 (Claude Code 표준)
365
+ const skillDir = join(skillsDst, name);
366
+ mkdirSync(skillDir, { recursive: true });
367
+ writeFileSync(join(skillDir, 'SKILL.md'), `---\nname: ${name}\ndescription: ${desc}\n---\n\n${body}\n`);
368
+ }
369
+
370
+ if (skipped > 0) {
371
+ console.log(`[forgen] ${skipped} overlapping skills skipped. Compound knowledge engine remains active.`);
372
+ }
373
+ }
374
+
375
+ // ── 3. Generate hooks/hooks.json dynamically ──
376
+
377
+ /**
378
+ * HOOK_REGISTRY — hooks/hook-registry.json에서 로드.
379
+ * 단일 소스 오브 트루스: hook-registry.ts와 동일 파일을 읽습니다.
380
+ * 중복/불일치 완전 제거.
381
+ */
382
+ let HOOK_REGISTRY = [];
383
+ try {
384
+ HOOK_REGISTRY = JSON.parse(readFileSync(join(PKG_ROOT, 'hooks', 'hook-registry.json'), 'utf-8'));
385
+ } catch {
386
+ console.warn('[forgen] hook-registry.json not found, skipping hook generation');
387
+ }
388
+
389
+ /**
390
+ * hook-config.ts의 isHookEnabled 로직 인라인 구현.
391
+ * 우선순위: 개별 훅 > 티어 > 레거시 > 기본값 true
392
+ * compound-core 훅은 tier 설정으로 비활성화 불가.
393
+ */
394
+ function isHookEnabledFromConfig(hookName, hookTier, config) {
395
+ if (!config) return true;
396
+
397
+ const hooksSection = config['hooks'];
398
+ // 1) 개별 훅 설정 (v2: hooks 섹션)
399
+ if (hooksSection?.[hookName]?.['enabled'] === false) return false;
400
+ if (hooksSection?.[hookName]?.['enabled'] === true) return true;
401
+
402
+ // 2) 티어 설정 — compound-core는 tier 비활성화 무시
403
+ if (hookTier !== 'compound-core') {
404
+ const tiers = config['tiers'];
405
+ if (tiers?.[hookTier]?.['enabled'] === false) return false;
406
+ }
407
+
408
+ // 3) 레거시 형식 (최상위 hookName.enabled)
409
+ if (config[hookName]?.['enabled'] === false) return false;
410
+
411
+ return true;
412
+ }
413
+
414
+ /**
415
+ * 플러그인별 충돌 훅 목록 (plugin-detector.ts의 overlappingHooks와 동기화 필요).
416
+ * 다른 플러그인이 감지되어도 이 목록에 있는 훅만 자동 비활성화.
417
+ */
418
+ const PLUGIN_HOOK_CONFLICTS = {
419
+ 'oh-my-claudecode': ['intent-classifier', 'keyword-detector', 'skill-injector'],
420
+ };
421
+
422
+ /**
423
+ * hooks/hooks.json을 동적으로 생성합니다.
424
+ *
425
+ * 동작:
426
+ * 1. ~/.compound/hook-config.json 로드 (없으면 모두 활성화)
427
+ * 2. detectPluginConflicts()로 다른 플러그인 존재 여부 판별
428
+ * 3. 활성 훅을 이벤트별 그룹으로 변환하여 hooks.json 작성
429
+ *
430
+ * 설계 결정:
431
+ * - compound-core 훅은 절대 자동 비활성화 안 함
432
+ * - script 필드에 공백이 있으면 파일 경로와 인수로 분리
433
+ * 예: "hooks/subagent-tracker.js stop" →
434
+ * node "${CLAUDE_PLUGIN_ROOT}/dist/hooks/subagent-tracker.js" stop
435
+ * - 다른 플러그인이 감지되어도 해당 플러그인과 실제 충돌하는 훅만 비활성
436
+ * (plugin-detector.ts의 overlappingHooks와 동일한 좁은 기준)
437
+ */
438
+ function generateAndWriteHooksJson() {
439
+ const pluginRoot = '${CLAUDE_PLUGIN_ROOT}/dist';
440
+ const hooksJsonPath = join(PKG_ROOT, 'hooks', 'hooks.json');
441
+
442
+ // hook-config.json 로드 (실패 시 null → 모두 기본 활성)
443
+ let hookConfig = null;
444
+ const hookConfigPath = join(COMPOUND_HOME, 'hook-config.json');
445
+ if (existsSync(hookConfigPath)) {
446
+ try { hookConfig = JSON.parse(readFileSync(hookConfigPath, 'utf-8')); } catch { /* ignore */ }
447
+ }
448
+
449
+ // detectPluginConflicts()는 skill→plugin Map이므로, 플러그인 집합을 추출해 훅 충돌 구성
450
+ const skillConflicts = detectPluginConflicts();
451
+ const hasOtherPlugins = skillConflicts.size > 0;
452
+ const detectedPluginNames = new Set(skillConflicts.values());
453
+ const hookConflicts = new Map();
454
+ for (const [plugin, hooks] of Object.entries(PLUGIN_HOOK_CONFLICTS)) {
455
+ if (detectedPluginNames.has(plugin)) {
456
+ for (const h of hooks) hookConflicts.set(h, plugin);
457
+ }
458
+ }
459
+
460
+ // 활성 훅 필터링
461
+ const activeHooks = HOOK_REGISTRY.filter(hook => {
462
+ // 1) hook-config.json 기반 개별/티어 비활성화
463
+ if (!isHookEnabledFromConfig(hook.name, hook.tier, hookConfig)) return false;
464
+
465
+ // 2) 다른 플러그인과 실제 충돌하는 workflow 훅만 비활성 (compound-critical 제외)
466
+ // hooks-generator.ts의 hookConflicts.has(hook.name) 조건과 동일한 좁은 기준
467
+ if (hasOtherPlugins && hook.tier === 'workflow' && hookConflicts.has(hook.name) && !hook.compoundCritical) return false;
468
+
469
+ return true;
470
+ });
471
+
472
+ // 이벤트별 그룹핑 (registry 순서 유지)
473
+ const byEvent = new Map();
474
+ for (const hook of activeHooks) {
475
+ if (!byEvent.has(hook.event)) byEvent.set(hook.event, []);
476
+ byEvent.get(hook.event).push(hook);
477
+ }
478
+
479
+ // hooks.json 구조 조립 — matcher별 그룹핑 (best practice: 도구별 필터링)
480
+ const hooks = {};
481
+ for (const [event, entries] of byEvent) {
482
+ const byMatcher = new Map();
483
+ for (const h of entries) {
484
+ const m = h.matcher || '*';
485
+ if (!byMatcher.has(m)) byMatcher.set(m, []);
486
+ byMatcher.get(m).push(h);
487
+ }
488
+ hooks[event] = [...byMatcher.entries()].map(([matcher, matcherEntries]) => ({
489
+ matcher,
490
+ hooks: matcherEntries.map(h => {
491
+ const spaceIdx = h.script.indexOf(' ');
492
+ let command;
493
+ if (spaceIdx === -1) {
494
+ command = `node "${pluginRoot}/${h.script}"`;
495
+ } else {
496
+ const scriptPath = h.script.slice(0, spaceIdx);
497
+ const args = h.script.slice(spaceIdx + 1);
498
+ command = `node "${pluginRoot}/${scriptPath}" ${args}`;
499
+ }
500
+ return { type: 'command', command, timeout: h.timeout };
501
+ }),
502
+ }));
503
+ }
504
+
505
+ const total = HOOK_REGISTRY.length;
506
+ const active = activeHooks.length;
507
+ const result = {
508
+ description: `Forgen harness hooks (auto-generated, ${active}/${total} active)`,
509
+ hooks,
510
+ };
511
+
512
+ mkdirSync(join(PKG_ROOT, 'hooks'), { recursive: true });
513
+ writeFileSync(hooksJsonPath, JSON.stringify(result, null, 2) + '\n');
514
+
515
+ return { active, total };
516
+ }
517
+
518
+ // ── 4. Install slash commands ──
519
+ function buildCommandContent(skillContent, skillName) {
520
+ const descMatch = skillContent.match(/description:\s*(.+)/);
521
+ const desc = descMatch?.[1]?.trim() ?? skillName;
522
+ return `# ${desc}\n\n<!-- forgen-managed -->\n\nActivate Forgen "${skillName}" mode for the task: $ARGUMENTS\n\n${skillContent}`;
523
+ }
524
+
525
+ function safeWriteCommand(cmdPath, content) {
526
+ if (existsSync(cmdPath)) {
527
+ const existing = readFileSync(cmdPath, 'utf-8');
528
+ if (!existing.includes('<!-- forgen-managed -->')) return false;
529
+ }
530
+ writeFileSync(cmdPath, content);
531
+ return true;
532
+ }
533
+
534
+ function installSlashCommands() {
535
+ if (!existsSync(SKILLS_DIR)) return 0;
536
+ mkdirSync(COMMANDS_DIR, { recursive: true });
537
+
538
+ const skills = readdirSync(SKILLS_DIR).filter((f) => f.endsWith('.md'));
539
+ let installed = 0;
540
+
541
+ for (const file of skills) {
542
+ const skillName = file.replace('.md', '');
543
+ const skillContent = readFileSync(join(SKILLS_DIR, file), 'utf-8');
544
+ const cmdContent = buildCommandContent(skillContent, skillName);
545
+ if (safeWriteCommand(join(COMMANDS_DIR, file), cmdContent)) {
546
+ installed++;
547
+ }
548
+ }
549
+ return installed;
550
+ }
551
+
552
+ // ── 3. Inject hooks into settings.json ──
553
+
554
+ /** 훅 경로가 forgen dist/hooks를 가리키는지 판별 (Windows \ 와 Unix / 모두 처리) */
555
+ function isForgenHook(entry) {
556
+ // [\\/] 패턴으로 양쪽 구분자 모두 매칭 (harness.ts와 동일 전략)
557
+ const HOOK_PATTERN = /[\\/]dist[\\/]hooks[\\/].*\.js/;
558
+ const check = (cmd) => HOOK_PATTERN.test(cmd) && (cmd.includes(PKG_ROOT) || cmd.includes('forgen') || cmd.includes('tenetx'));
559
+ if (typeof entry.command === 'string' && check(entry.command)) return true;
560
+ if (Array.isArray(entry.hooks)) {
561
+ return entry.hooks.some((h) => typeof h.command === 'string' && check(h.command));
562
+ }
563
+ return false;
564
+ }
565
+
566
+ /**
567
+ * settings.json에 forgen 훅을 절대 경로로 직접 등록합니다.
568
+ *
569
+ * 설계 결정:
570
+ * - 플러그인 hooks.json의 ${CLAUDE_PLUGIN_ROOT}는 Claude Code 버전에 따라
571
+ * 해석되지 않을 수 있으므로, settings.json에 절대 경로로 직접 등록.
572
+ * - PKG_ROOT는 npm i -g 시 글로벌 node_modules 경로,
573
+ * npm link 시 개발 디렉토리 경로로 자동 해석.
574
+ * - 기존 non-forgen 훅은 보존.
575
+ */
576
+ function applyHookSettings(settings) {
577
+ if (!existsSync(DIST_HOOKS)) return false;
578
+
579
+ // 1) 기존 forgen 훅 제거 (이전 버전 잔재 + 현재 버전 모두)
580
+ const hooksConfig = settings.hooks ?? {};
581
+ for (const [event, entries] of Object.entries(hooksConfig)) {
582
+ if (Array.isArray(entries)) {
583
+ hooksConfig[event] = entries.filter(h => !isForgenHook(h));
584
+ if (hooksConfig[event].length === 0) delete hooksConfig[event];
585
+ }
586
+ }
587
+
588
+ // 2) hook-registry.json 기반으로 활성 훅을 settings.json에 등록
589
+ // registry의 script 필드는 "hooks/xxx.js" 형식 → dist/ 기준으로 join
590
+ const distDir = join(PKG_ROOT, 'dist');
591
+
592
+ let hookConfig = null;
593
+ const hookConfigPath = join(COMPOUND_HOME, 'hook-config.json');
594
+ if (existsSync(hookConfigPath)) {
595
+ try { hookConfig = JSON.parse(readFileSync(hookConfigPath, 'utf-8')); } catch { /* ignore */ }
596
+ }
597
+
598
+ const skillConflicts = detectPluginConflicts();
599
+ const detectedPluginNames = new Set(skillConflicts.values());
600
+ const hookConflicts = new Map();
601
+ for (const [plugin, hooks] of Object.entries(PLUGIN_HOOK_CONFLICTS)) {
602
+ if (detectedPluginNames.has(plugin)) {
603
+ for (const h of hooks) hookConflicts.set(h, plugin);
604
+ }
605
+ }
606
+
607
+ const activeHooks = HOOK_REGISTRY.filter(hook => {
608
+ if (!isHookEnabledFromConfig(hook.name, hook.tier, hookConfig)) return false;
609
+ if (skillConflicts.size > 0 && hook.tier === 'workflow' && hookConflicts.has(hook.name) && !hook.compoundCritical) return false;
610
+ return true;
611
+ });
612
+
613
+ // 이벤트별 그룹핑
614
+ const byEvent = new Map();
615
+ for (const hook of activeHooks) {
616
+ if (!byEvent.has(hook.event)) byEvent.set(hook.event, []);
617
+ byEvent.get(hook.event).push(hook);
618
+ }
619
+
620
+ // settings.json hooks 구조 조립 (절대 경로 사용)
621
+ for (const [event, entries] of byEvent) {
622
+ const byMatcher = new Map();
623
+ for (const h of entries) {
624
+ const m = h.matcher || '';
625
+ if (!byMatcher.has(m)) byMatcher.set(m, []);
626
+ byMatcher.get(m).push(h);
627
+ }
628
+
629
+ if (!hooksConfig[event]) hooksConfig[event] = [];
630
+
631
+ for (const [matcher, matcherEntries] of byMatcher) {
632
+ hooksConfig[event].push({
633
+ matcher,
634
+ hooks: matcherEntries.map(h => {
635
+ const spaceIdx = h.script.indexOf(' ');
636
+ let command;
637
+ if (spaceIdx === -1) {
638
+ command = `node "${join(distDir, h.script)}"`;
639
+ } else {
640
+ const scriptPath = h.script.slice(0, spaceIdx);
641
+ const args = h.script.slice(spaceIdx + 1);
642
+ command = `node "${join(distDir, scriptPath)}" ${args}`;
643
+ }
644
+ return { type: 'command', command, timeout: h.timeout };
645
+ }),
646
+ });
647
+ }
648
+ }
649
+
650
+ settings.hooks = hooksConfig;
651
+
652
+ // env에 COMPOUND_HARNESS 마커 설정
653
+ const env = settings.env ?? {};
654
+ env.COMPOUND_HARNESS = '1';
655
+ settings.env = env;
656
+
657
+ return true;
658
+ }
659
+
660
+ // ── MCP Server Registration ──
661
+
662
+ /**
663
+ * ~/.claude.json의 mcpServers에 forgen-compound MCP 서버를 등록합니다.
664
+ *
665
+ * 설계 결정 (실증 검증 완료):
666
+ * - Claude Code는 settings.json의 mcpServers를 읽지 않음
667
+ * - MCP 서버는 반드시 ~/.claude.json에 등록해야 Claude Code가 인식
668
+ * - context7 등 동작하는 MCP 서버와 동일한 등록 경로
669
+ * - npm bin 경로(forgen-mcp) 우선, 없으면 node + 절대 경로 fallback
670
+ */
671
+ function applyMcpToClaudeJson() {
672
+ const mcpServerPath = join(PKG_ROOT, 'dist', 'mcp', 'server.js');
673
+ if (!existsSync(mcpServerPath)) return false;
674
+
675
+ // W-V3: 항상 node + 절대경로 사용 — PATH 변경/nvm 전환에도 안정적
676
+ const mcpCommand = 'node';
677
+ const mcpArgs = [mcpServerPath];
678
+
679
+ const claudeJsonPath = join(HOME, '.claude.json');
680
+ let claudeJson = {};
681
+ if (existsSync(claudeJsonPath)) {
682
+ try { claudeJson = JSON.parse(readFileSync(claudeJsonPath, 'utf-8')); } catch { /* ignore */ }
683
+ }
684
+
685
+ const mcpServers = claudeJson.mcpServers ?? {};
686
+ mcpServers['forgen-compound'] = {
687
+ command: mcpCommand,
688
+ args: mcpArgs,
689
+ };
690
+ claudeJson.mcpServers = mcpServers;
691
+
692
+ writeFileSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2));
693
+ fixOwnership(claudeJsonPath);
694
+ return true;
695
+ }
696
+
697
+ /** settings.json에서 레거시 mcpServers 항목 정리 (더 이상 사용하지 않음) */
698
+ function cleanLegacyMcpFromSettings(settings) {
699
+ if (settings.mcpServers?.['forgen-compound']) {
700
+ delete settings.mcpServers['forgen-compound'];
701
+ if (Object.keys(settings.mcpServers).length === 0) {
702
+ delete settings.mcpServers;
703
+ }
704
+ }
705
+ }
706
+
707
+ // ── Main ──
708
+
709
+ /**
710
+ * settings.json을 한 번 읽고, 모든 변경을 적용한 후, 한 번만 씁니다.
711
+ * 이전 방식(3곳에서 각각 read-modify-write)은 중간에 다른 프로세스가
712
+ * settings.json을 수정하면 데이터 유실 가능성이 있었습니다.
713
+ */
714
+ function main() {
715
+ // W-V2: 로컬 설치 시 전역 설정 수정 방지
716
+ if (!process.env.npm_config_global && process.env.INIT_CWD && process.env.INIT_CWD !== PKG_ROOT) {
717
+ // npm install (로컬) — postinstall 스킵
718
+ return;
719
+ }
720
+ migrateLegacyStorage();
721
+ ensureDirectories();
722
+
723
+ // ── 1. settings.json 한 번 읽기 ──
724
+ let settings = {};
725
+ if (existsSync(SETTINGS_PATH)) {
726
+ try { settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8')); } catch { /* 파싱 실패 시 빈 설정 */ }
727
+ }
728
+
729
+ // ── 2. 플러그인 등록 (installed_plugins.json + skills) ──
730
+ let plugin = false;
731
+ try {
732
+ plugin = registerPlugin();
733
+ if (plugin) applyPluginSettings(settings);
734
+ } catch (err) {
735
+ console.error(`[forgen] plugin registration failed: ${err?.message ?? err}`);
736
+ }
737
+
738
+ // ── 3. hooks.json 동적 생성 ──
739
+ //
740
+ // A4 guard (2026-04-09): skip regeneration when running inside the
741
+ // forgen source tree itself. Pre-A4 fix, every `npm install` /
742
+ // `npm audit fix` run from the dev checkout clobbered the package's
743
+ // own committed `hooks/hooks.json` with a file reflecting the
744
+ // developer's LOCAL plugin state (which usually has `oh-my-claudecode`
745
+ // or similar installed for testing). That 17/19-active file then
746
+ // shipped in the next npm tarball and broke keyword-detector for
747
+ // every user whose postinstall didn't complete (sudo issues,
748
+ // read-only FS, etc.). The checked-in file should always be the
749
+ // pristine 19/19 baseline; per-user plugin conflict resolution is
750
+ // handled at runtime via `forgen config hooks`. Detect "we're in the
751
+ // source tree" via the presence of a sibling `.git/` directory next
752
+ // to the package root — a regular npm install never has that.
753
+ let hooksJsonResult = null;
754
+ const isSourceCheckout = existsSync(join(PKG_ROOT, '.git'));
755
+ if (isSourceCheckout) {
756
+ // no-op in dev: leave the committed file untouched
757
+ } else {
758
+ try {
759
+ hooksJsonResult = generateAndWriteHooksJson();
760
+ } catch (err) {
761
+ console.error(`[forgen] hooks.json generation failed: ${err?.message ?? err}`);
762
+ }
763
+ }
764
+
765
+ // ── 4. 슬래시 커맨드 설치 ──
766
+ let commands = 0;
767
+ try {
768
+ commands = installSlashCommands();
769
+ } catch (err) {
770
+ console.error(`[forgen] slash commands failed: ${err?.message ?? err}`);
771
+ }
772
+
773
+ // ── 5. settings에 훅 설정 적용 ──
774
+ let hooks = false;
775
+ try {
776
+ hooks = applyHookSettings(settings);
777
+ } catch (err) {
778
+ console.error(`[forgen] hooks settings failed: ${err?.message ?? err}`);
779
+ }
780
+
781
+ // ── 6. MCP 서버를 ~/.claude.json에 등록 (settings.json이 아닌 올바른 경로) ──
782
+ let mcp = false;
783
+ try {
784
+ mcp = applyMcpToClaudeJson();
785
+ cleanLegacyMcpFromSettings(settings);
786
+ } catch (err) {
787
+ console.error(`[forgen] MCP server registration failed: ${err?.message ?? err}`);
788
+ }
789
+
790
+ // ── 7. settings.json 한 번 쓰기 ──
791
+ try {
792
+ mkdirSync(dirname(SETTINGS_PATH), { recursive: true });
793
+ writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
794
+ } catch (err) {
795
+ console.error(`[forgen] settings.json write failed: ${err?.message ?? err}`);
796
+ }
797
+
798
+ // ── 8. Starter Knowledge Pack 설치 (솔루션 < 5개일 때만) ──
799
+ let starterInstalled = 0;
800
+ try {
801
+ starterInstalled = installStarterPack();
802
+ } catch (err) {
803
+ console.error(`[forgen] starter pack failed: ${err?.message ?? err}`);
804
+ }
805
+
806
+ // sudo 실행 시 파일 소유권을 실제 유저로 변경
807
+ fixOwnership(join(HOME, '.claude'), join(HOME, '.forgen'));
808
+
809
+ const parts = [];
810
+ if (plugin) parts.push('plugin');
811
+ if (hooksJsonResult) parts.push(`hooks.json (${hooksJsonResult.active}/${hooksJsonResult.total} active)`);
812
+ if (commands > 0) parts.push(`${commands} slash commands`);
813
+ if (hooks) parts.push('hooks');
814
+ if (mcp) parts.push('MCP compound');
815
+ if (starterInstalled > 0) parts.push(`${starterInstalled} starter solutions`);
816
+ if (parts.length > 0) {
817
+ console.log(`[forgen] Installed: ${parts.join(', ')} → ${HOME}`);
818
+ }
819
+ }
820
+
821
+ try {
822
+ main();
823
+ } catch (err) {
824
+ // postinstall 실패가 npm install을 깨뜨리지 않되, 원인은 표시
825
+ console.error(`[forgen] postinstall warning: ${err?.message ?? err}`);
826
+ }