peaks-loop 4.1.2 → 4.1.3

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 (323) hide show
  1. package/.claude-plugin/marketplace.json +136 -47
  2. package/CHANGELOG.md +56 -0
  3. package/README-en.md +1 -1
  4. package/README.md +1 -1
  5. package/contracts/readonly-argv-whitelist.json +211 -0
  6. package/dist/cli/commands/audit-artifact-runners.d.ts +16 -0
  7. package/dist/cli/commands/audit-artifact-runners.js +170 -0
  8. package/dist/cli/commands/audit-command-shared.d.ts +97 -0
  9. package/dist/cli/commands/audit-command-shared.js +60 -0
  10. package/dist/cli/commands/audit-commands.d.ts +7 -31
  11. package/dist/cli/commands/audit-commands.js +64 -473
  12. package/dist/cli/commands/audit-goal-runners.d.ts +17 -0
  13. package/dist/cli/commands/audit-goal-runners.js +114 -0
  14. package/dist/cli/commands/audit-red-lines-runners.d.ts +11 -0
  15. package/dist/cli/commands/audit-red-lines-runners.js +43 -0
  16. package/dist/cli/commands/audit-refusal-data.d.ts +23 -0
  17. package/dist/cli/commands/audit-refusal-data.js +69 -0
  18. package/dist/cli/commands/audit-static-runners.d.ts +13 -0
  19. package/dist/cli/commands/audit-static-runners.js +139 -0
  20. package/dist/cli/commands/await-runners.d.ts +11 -0
  21. package/dist/cli/commands/await-runners.js +148 -0
  22. package/dist/cli/commands/code-auto-compact-command.d.ts +3 -0
  23. package/dist/cli/commands/code-auto-compact-command.js +124 -0
  24. package/dist/cli/commands/code-auto-compact-envelope.d.ts +22 -0
  25. package/dist/cli/commands/code-auto-compact-envelope.js +39 -0
  26. package/dist/cli/commands/code-context-audit-command.d.ts +3 -0
  27. package/dist/cli/commands/code-context-audit-command.js +68 -0
  28. package/dist/cli/commands/code-context-now-command.d.ts +3 -0
  29. package/dist/cli/commands/code-context-now-command.js +124 -0
  30. package/dist/cli/commands/code-context-now-envelope.d.ts +66 -0
  31. package/dist/cli/commands/code-context-now-envelope.js +136 -0
  32. package/dist/cli/commands/code-context-now-mapping.d.ts +27 -0
  33. package/dist/cli/commands/code-context-now-mapping.js +43 -0
  34. package/dist/cli/commands/code-emit-handoff-command.d.ts +3 -0
  35. package/dist/cli/commands/code-emit-handoff-command.js +99 -0
  36. package/dist/cli/commands/code-gate-step-08-command.d.ts +3 -0
  37. package/dist/cli/commands/code-gate-step-08-command.js +139 -0
  38. package/dist/cli/commands/code-post-compact-detect-command.d.ts +3 -0
  39. package/dist/cli/commands/code-post-compact-detect-command.js +50 -0
  40. package/dist/cli/commands/code-runtime-commands.d.ts +9 -43
  41. package/dist/cli/commands/code-runtime-commands.js +20 -672
  42. package/dist/cli/commands/code-runtime-session.d.ts +1 -0
  43. package/dist/cli/commands/code-runtime-session.js +15 -0
  44. package/dist/cli/commands/compact-command.d.ts +6 -2
  45. package/dist/cli/commands/compact-command.js +12 -559
  46. package/dist/cli/commands/compact-dry-run-command.d.ts +3 -0
  47. package/dist/cli/commands/compact-dry-run-command.js +87 -0
  48. package/dist/cli/commands/compact-force-command.d.ts +3 -0
  49. package/dist/cli/commands/compact-force-command.js +98 -0
  50. package/dist/cli/commands/compact-harness-window-command.d.ts +3 -0
  51. package/dist/cli/commands/compact-harness-window-command.js +174 -0
  52. package/dist/cli/commands/compact-history-command.d.ts +3 -0
  53. package/dist/cli/commands/compact-history-command.js +89 -0
  54. package/dist/cli/commands/compact-read-commands.d.ts +3 -0
  55. package/dist/cli/commands/compact-read-commands.js +123 -0
  56. package/dist/cli/commands/compact-settle-command.d.ts +3 -0
  57. package/dist/cli/commands/compact-settle-command.js +130 -0
  58. package/dist/cli/commands/compact-shared.d.ts +9 -0
  59. package/dist/cli/commands/compact-shared.js +26 -0
  60. package/dist/cli/commands/core/skill-command.d.ts +1 -44
  61. package/dist/cli/commands/core/skill-command.js +13 -541
  62. package/dist/cli/commands/core/skill-context-verdict.d.ts +38 -0
  63. package/dist/cli/commands/core/skill-context-verdict.js +63 -0
  64. package/dist/cli/commands/core/skill-generated-config-notice.d.ts +41 -0
  65. package/dist/cli/commands/core/skill-generated-config-notice.js +75 -0
  66. package/dist/cli/commands/core/skill-heartbeat-commands.d.ts +3 -0
  67. package/dist/cli/commands/core/skill-heartbeat-commands.js +50 -0
  68. package/dist/cli/commands/core/skill-presence-commands.d.ts +3 -0
  69. package/dist/cli/commands/core/skill-presence-commands.js +123 -0
  70. package/dist/cli/commands/core/skill-presence-housekeeping-commands.d.ts +3 -0
  71. package/dist/cli/commands/core/skill-presence-housekeeping-commands.js +165 -0
  72. package/dist/cli/commands/core/skill-registry-commands.d.ts +3 -0
  73. package/dist/cli/commands/core/skill-registry-commands.js +117 -0
  74. package/dist/cli/commands/core/skill-runbook-command.d.ts +3 -0
  75. package/dist/cli/commands/core/skill-runbook-command.js +29 -0
  76. package/dist/cli/commands/dispatch-command-spec.d.ts +2 -0
  77. package/dist/cli/commands/dispatch-command-spec.js +62 -0
  78. package/dist/cli/commands/dispatch-commands.js +159 -775
  79. package/dist/cli/commands/dispatch-detached-branch.d.ts +3 -0
  80. package/dist/cli/commands/dispatch-detached-branch.js +58 -0
  81. package/dist/cli/commands/dispatch-envelope.d.ts +36 -0
  82. package/dist/cli/commands/dispatch-envelope.js +104 -0
  83. package/dist/cli/commands/dispatch-isolation-lease.d.ts +17 -0
  84. package/dist/cli/commands/dispatch-isolation-lease.js +149 -0
  85. package/dist/cli/commands/dispatch-preflight.d.ts +7 -0
  86. package/dist/cli/commands/dispatch-preflight.js +105 -0
  87. package/dist/cli/commands/dispatch-prompt-blocks.d.ts +10 -0
  88. package/dist/cli/commands/dispatch-prompt-blocks.js +56 -0
  89. package/dist/cli/commands/dispatch-prompt-composition.d.ts +30 -0
  90. package/dist/cli/commands/dispatch-prompt-composition.js +171 -0
  91. package/dist/cli/commands/dispatch-record-write.d.ts +24 -0
  92. package/dist/cli/commands/dispatch-record-write.js +96 -0
  93. package/dist/cli/commands/dispatch-tool-call.d.ts +15 -0
  94. package/dist/cli/commands/dispatch-tool-call.js +128 -0
  95. package/dist/cli/commands/evolution-command-shared.d.ts +78 -0
  96. package/dist/cli/commands/evolution-command-shared.js +107 -0
  97. package/dist/cli/commands/evolution-commands.d.ts +11 -1
  98. package/dist/cli/commands/evolution-commands.js +19 -350
  99. package/dist/cli/commands/evolution-evaluate-command.d.ts +24 -0
  100. package/dist/cli/commands/evolution-evaluate-command.js +138 -0
  101. package/dist/cli/commands/evolution-propose-command.d.ts +23 -0
  102. package/dist/cli/commands/evolution-propose-command.js +145 -0
  103. package/dist/cli/commands/evolution-status-command.d.ts +17 -0
  104. package/dist/cli/commands/evolution-status-command.js +41 -0
  105. package/dist/cli/commands/evolution-verdict-commands.d.ts +30 -0
  106. package/dist/cli/commands/evolution-verdict-commands.js +100 -0
  107. package/dist/cli/commands/finalize-accumulator.d.ts +44 -0
  108. package/dist/cli/commands/finalize-accumulator.js +55 -0
  109. package/dist/cli/commands/finalize-branches.d.ts +34 -0
  110. package/dist/cli/commands/finalize-branches.js +119 -0
  111. package/dist/cli/commands/finalize-runners.d.ts +3 -0
  112. package/dist/cli/commands/finalize-runners.js +92 -0
  113. package/dist/cli/commands/finalize-selection.d.ts +65 -0
  114. package/dist/cli/commands/finalize-selection.js +35 -0
  115. package/dist/cli/commands/gate-commands.js +26 -8
  116. package/dist/cli/commands/job-checkpoint-commands.d.ts +3 -0
  117. package/dist/cli/commands/job-checkpoint-commands.js +157 -0
  118. package/dist/cli/commands/job-command-shared.d.ts +109 -0
  119. package/dist/cli/commands/job-command-shared.js +72 -0
  120. package/dist/cli/commands/job-commands.d.ts +4 -64
  121. package/dist/cli/commands/job-commands.js +42 -552
  122. package/dist/cli/commands/job-cost-check-command.d.ts +3 -0
  123. package/dist/cli/commands/job-cost-check-command.js +41 -0
  124. package/dist/cli/commands/job-init-status-commands.d.ts +3 -0
  125. package/dist/cli/commands/job-init-status-commands.js +149 -0
  126. package/dist/cli/commands/job-run-commands.d.ts +3 -0
  127. package/dist/cli/commands/job-run-commands.js +97 -0
  128. package/dist/cli/commands/job-state-root.d.ts +35 -0
  129. package/dist/cli/commands/job-state-root.js +76 -0
  130. package/dist/cli/commands/loop-eval-capture-index.d.ts +4 -0
  131. package/dist/cli/commands/loop-eval-capture-index.js +44 -0
  132. package/dist/cli/commands/loop-eval-commands.d.ts +8 -5
  133. package/dist/cli/commands/loop-eval-commands.js +12 -626
  134. package/dist/cli/commands/loop-eval-eval-command.d.ts +3 -0
  135. package/dist/cli/commands/loop-eval-eval-command.js +168 -0
  136. package/dist/cli/commands/loop-eval-monotonic-command.d.ts +3 -0
  137. package/dist/cli/commands/loop-eval-monotonic-command.js +90 -0
  138. package/dist/cli/commands/loop-eval-parents.d.ts +7 -0
  139. package/dist/cli/commands/loop-eval-parents.js +20 -0
  140. package/dist/cli/commands/loop-eval-run-code.d.ts +7 -0
  141. package/dist/cli/commands/loop-eval-run-code.js +40 -0
  142. package/dist/cli/commands/loop-eval-run-command.d.ts +3 -0
  143. package/dist/cli/commands/loop-eval-run-command.js +113 -0
  144. package/dist/cli/commands/loop-eval-spec-commands.d.ts +3 -0
  145. package/dist/cli/commands/loop-eval-spec-commands.js +165 -0
  146. package/dist/cli/commands/loop-eval-spec-lint-command.d.ts +3 -0
  147. package/dist/cli/commands/loop-eval-spec-lint-command.js +42 -0
  148. package/dist/cli/commands/loop-eval-workflow-commands.d.ts +3 -0
  149. package/dist/cli/commands/loop-eval-workflow-commands.js +141 -0
  150. package/dist/cli/commands/mcp-commands.d.ts +10 -0
  151. package/dist/cli/commands/mcp-commands.js +157 -0
  152. package/dist/cli/commands/request-command-options.d.ts +49 -0
  153. package/dist/cli/commands/request-command-options.js +1 -0
  154. package/dist/cli/commands/request-commands.d.ts +3 -9
  155. package/dist/cli/commands/request-commands.js +17 -522
  156. package/dist/cli/commands/request-init-command.d.ts +3 -0
  157. package/dist/cli/commands/request-init-command.js +100 -0
  158. package/dist/cli/commands/request-lint-command.d.ts +3 -0
  159. package/dist/cli/commands/request-lint-command.js +50 -0
  160. package/dist/cli/commands/request-list-command.d.ts +10 -0
  161. package/dist/cli/commands/request-list-command.js +53 -0
  162. package/dist/cli/commands/request-repair-status-command.d.ts +3 -0
  163. package/dist/cli/commands/request-repair-status-command.js +61 -0
  164. package/dist/cli/commands/request-show-command.d.ts +3 -0
  165. package/dist/cli/commands/request-show-command.js +56 -0
  166. package/dist/cli/commands/request-transition-command.d.ts +3 -0
  167. package/dist/cli/commands/request-transition-command.js +122 -0
  168. package/dist/cli/commands/request-transition-failures.d.ts +8 -0
  169. package/dist/cli/commands/request-transition-failures.js +90 -0
  170. package/dist/cli/commands/request-transition-hooks.d.ts +50 -0
  171. package/dist/cli/commands/request-transition-hooks.js +115 -0
  172. package/dist/cli/commands/request-transition-preflight.d.ts +10 -0
  173. package/dist/cli/commands/request-transition-preflight.js +107 -0
  174. package/dist/cli/commands/scan-command-shared.d.ts +96 -0
  175. package/dist/cli/commands/scan-command-shared.js +83 -0
  176. package/dist/cli/commands/scan-commands.d.ts +3 -2
  177. package/dist/cli/commands/scan-commands.js +23 -463
  178. package/dist/cli/commands/scan-inventory-commands.d.ts +3 -0
  179. package/dist/cli/commands/scan-inventory-commands.js +96 -0
  180. package/dist/cli/commands/scan-project-commands.d.ts +3 -0
  181. package/dist/cli/commands/scan-project-commands.js +65 -0
  182. package/dist/cli/commands/scan-scope-commands.d.ts +3 -0
  183. package/dist/cli/commands/scan-scope-commands.js +149 -0
  184. package/dist/cli/commands/scan-scope-helpers.d.ts +8 -0
  185. package/dist/cli/commands/scan-scope-helpers.js +54 -0
  186. package/dist/cli/commands/scan-structural-commands.d.ts +3 -0
  187. package/dist/cli/commands/scan-structural-commands.js +113 -0
  188. package/dist/cli/commands/share-commands.d.ts +12 -68
  189. package/dist/cli/commands/share-commands.js +47 -556
  190. package/dist/cli/commands/share-error-hints.d.ts +14 -0
  191. package/dist/cli/commands/share-error-hints.js +38 -0
  192. package/dist/cli/commands/share-identity.d.ts +13 -0
  193. package/dist/cli/commands/share-identity.js +22 -0
  194. package/dist/cli/commands/share-record-paths.d.ts +37 -0
  195. package/dist/cli/commands/share-record-paths.js +68 -0
  196. package/dist/cli/commands/share-runners.d.ts +3 -0
  197. package/dist/cli/commands/share-runners.js +158 -0
  198. package/dist/cli/commands/shared-read-runners.d.ts +3 -0
  199. package/dist/cli/commands/shared-read-runners.js +51 -0
  200. package/dist/cli/commands/slice-check-command.d.ts +3 -0
  201. package/dist/cli/commands/slice-check-command.js +54 -0
  202. package/dist/cli/commands/slice-commands.d.ts +2 -22
  203. package/dist/cli/commands/slice-commands.js +18 -517
  204. package/dist/cli/commands/slice-decompose-artifacts.d.ts +18 -0
  205. package/dist/cli/commands/slice-decompose-artifacts.js +79 -0
  206. package/dist/cli/commands/slice-decompose-command.d.ts +3 -0
  207. package/dist/cli/commands/slice-decompose-command.js +123 -0
  208. package/dist/cli/commands/slice-list-commands.d.ts +3 -0
  209. package/dist/cli/commands/slice-list-commands.js +182 -0
  210. package/dist/cli/commands/slice-pick-command.d.ts +3 -0
  211. package/dist/cli/commands/slice-pick-command.js +60 -0
  212. package/dist/cli/commands/slice-plan-command.d.ts +24 -0
  213. package/dist/cli/commands/slice-plan-command.js +111 -0
  214. package/dist/cli/commands/workflow-commands.d.ts +10 -1
  215. package/dist/cli/commands/workflow-commands.js +6 -594
  216. package/dist/cli/commands/workflow-gate-commands.d.ts +3 -0
  217. package/dist/cli/commands/workflow-gate-commands.js +116 -0
  218. package/dist/cli/commands/workflow-plan-helpers.d.ts +48 -0
  219. package/dist/cli/commands/workflow-plan-helpers.js +103 -0
  220. package/dist/cli/commands/workflow-planning-commands.d.ts +3 -0
  221. package/dist/cli/commands/workflow-planning-commands.js +56 -0
  222. package/dist/cli/commands/workflow-swarm-actions.d.ts +7 -0
  223. package/dist/cli/commands/workflow-swarm-actions.js +109 -0
  224. package/dist/cli/commands/workflow-swarm-commands.d.ts +3 -0
  225. package/dist/cli/commands/workflow-swarm-commands.js +138 -0
  226. package/dist/cli/commands/workflow-tech-actions.d.ts +10 -0
  227. package/dist/cli/commands/workflow-tech-actions.js +150 -0
  228. package/dist/cli/commands/workspace/init-actions.d.ts +10 -0
  229. package/dist/cli/commands/workspace/init-actions.js +199 -0
  230. package/dist/cli/commands/workspace/init-codegraph-stake.d.ts +10 -0
  231. package/dist/cli/commands/workspace/init-codegraph-stake.js +54 -0
  232. package/dist/cli/commands/workspace/init-command.d.ts +8 -124
  233. package/dist/cli/commands/workspace/init-command.js +59 -623
  234. package/dist/cli/commands/workspace/init-context.d.ts +58 -0
  235. package/dist/cli/commands/workspace/init-context.js +121 -0
  236. package/dist/cli/commands/workspace/init-envelope.d.ts +3 -0
  237. package/dist/cli/commands/workspace/init-envelope.js +34 -0
  238. package/dist/cli/commands/workspace/init-failures.d.ts +3 -0
  239. package/dist/cli/commands/workspace/init-failures.js +84 -0
  240. package/dist/cli/commands/workspace/init-hooks-decision.d.ts +24 -0
  241. package/dist/cli/commands/workspace/init-hooks-decision.js +189 -0
  242. package/dist/cli/commands/workspace/init-options.d.ts +102 -0
  243. package/dist/cli/commands/workspace/init-options.js +1 -0
  244. package/dist/cli/commands/worktree-lease-commands.d.ts +10 -6
  245. package/dist/cli/commands/worktree-lease-commands.js +42 -512
  246. package/dist/cli/commands/worktree-lease-gc-command.d.ts +12 -0
  247. package/dist/cli/commands/worktree-lease-gc-command.js +117 -0
  248. package/dist/cli/commands/worktree-lease-git.d.ts +22 -0
  249. package/dist/cli/commands/worktree-lease-git.js +48 -0
  250. package/dist/cli/commands/worktree-lease-list-command.d.ts +12 -0
  251. package/dist/cli/commands/worktree-lease-list-command.js +100 -0
  252. package/dist/cli/commands/worktree-lease-load.d.ts +37 -0
  253. package/dist/cli/commands/worktree-lease-load.js +28 -0
  254. package/dist/cli/commands/worktree-lease-release-command.d.ts +13 -0
  255. package/dist/cli/commands/worktree-lease-release-command.js +105 -0
  256. package/dist/cli/commands/worktree-lease-renew-command.d.ts +11 -0
  257. package/dist/cli/commands/worktree-lease-renew-command.js +99 -0
  258. package/dist/cli/commands/worktree-lease-session.d.ts +22 -0
  259. package/dist/cli/commands/worktree-lease-session.js +29 -0
  260. package/dist/cli/commands/worktree-lease-spawn-command.d.ts +11 -0
  261. package/dist/cli/commands/worktree-lease-spawn-command.js +119 -0
  262. package/dist/cli/commands/worktree-lease-status-command.d.ts +11 -0
  263. package/dist/cli/commands/worktree-lease-status-command.js +86 -0
  264. package/dist/cli/commands/worktree-lease-store.d.ts +19 -0
  265. package/dist/cli/commands/worktree-lease-store.js +24 -0
  266. package/dist/cli/program.js +5 -0
  267. package/dist/services/distribution/mcp-install-runner.d.ts +21 -0
  268. package/dist/services/distribution/mcp-install-runner.js +59 -0
  269. package/dist/services/distribution/mcp-install.d.ts +95 -0
  270. package/dist/services/distribution/mcp-install.js +179 -0
  271. package/dist/services/distribution/plugin-manifest.d.ts +97 -0
  272. package/dist/services/distribution/plugin-manifest.js +126 -0
  273. package/dist/services/hooks/mcp-surface-gate.d.ts +19 -0
  274. package/dist/services/hooks/mcp-surface-gate.js +87 -0
  275. package/dist/services/ide/adapters/claude-code-adapter.d.ts +4 -35
  276. package/dist/services/ide/adapters/claude-code-adapter.js +48 -171
  277. package/dist/services/ide/adapters/claude-code-local-state.d.ts +28 -0
  278. package/dist/services/ide/adapters/claude-code-local-state.js +112 -0
  279. package/dist/services/ide/adapters/claude-code-model-window.d.ts +37 -0
  280. package/dist/services/ide/adapters/claude-code-model-window.js +113 -0
  281. package/dist/services/ide/ide-mcp-install-types.d.ts +78 -0
  282. package/dist/services/ide/ide-mcp-install-types.js +104 -0
  283. package/dist/services/ide/ide-types.d.ts +45 -1
  284. package/dist/services/ide/mcp-tool-matcher.d.ts +34 -0
  285. package/dist/services/ide/mcp-tool-matcher.js +84 -0
  286. package/dist/services/lint/npx-resolver.js +7 -0
  287. package/dist/services/mcp/cli-executor.d.ts +36 -0
  288. package/dist/services/mcp/cli-executor.js +97 -0
  289. package/dist/services/mcp/json-rpc.d.ts +81 -0
  290. package/dist/services/mcp/json-rpc.js +116 -0
  291. package/dist/services/mcp/memory-tool.d.ts +3 -0
  292. package/dist/services/mcp/memory-tool.js +62 -0
  293. package/dist/services/mcp/server-main.d.ts +1 -0
  294. package/dist/services/mcp/server-main.js +66 -0
  295. package/dist/services/mcp/server.d.ts +22 -0
  296. package/dist/services/mcp/server.js +152 -0
  297. package/dist/services/mcp/surface.d.ts +62 -0
  298. package/dist/services/mcp/surface.js +198 -0
  299. package/dist/services/mcp/tool-core.d.ts +42 -0
  300. package/dist/services/mcp/tool-core.js +137 -0
  301. package/dist/services/mcp/tools.d.ts +13 -0
  302. package/dist/services/mcp/tools.js +58 -0
  303. package/dist/services/readonly-surface/argv-guard.d.ts +32 -0
  304. package/dist/services/readonly-surface/argv-guard.js +164 -0
  305. package/dist/services/readonly-surface/readonly-whitelist.d.ts +147 -0
  306. package/dist/services/readonly-surface/readonly-whitelist.js +107 -0
  307. package/dist/services/readonly-surface/whitelist-generator.d.ts +64 -0
  308. package/dist/services/readonly-surface/whitelist-generator.js +212 -0
  309. package/dist/services/skills/hooks-codegate-superpowers.d.ts +1 -0
  310. package/dist/services/skills/hooks-codegate-superpowers.js +26 -0
  311. package/dist/services/skills/hooks-settings-service.js +10 -2
  312. package/package.json +17 -6
  313. package/scripts/install-skills-bundles.mjs +233 -0
  314. package/scripts/install-skills-fs.mjs +148 -0
  315. package/scripts/install-skills-ide.mjs +279 -0
  316. package/scripts/install-skills-output-style-default.mjs +196 -0
  317. package/scripts/install-skills-output-styles.mjs +121 -0
  318. package/scripts/install-skills-report.mjs +42 -0
  319. package/scripts/install-skills-user-config.mjs +198 -0
  320. package/scripts/install-skills.mjs +36 -1066
  321. package/scripts/watch-tree.mjs +140 -0
  322. package/scripts/watch.mjs +4 -126
  323. package/skills/peaks-status/SKILL.md +31 -13
@@ -1,1071 +1,41 @@
1
- #!/usr/bin/env node
2
- import {
3
- closeSync,
4
- constants,
5
- existsSync,
6
- fchmodSync,
7
- fstatSync,
8
- lstatSync,
9
- mkdirSync,
10
- openSync,
11
- readFileSync,
12
- realpathSync,
13
- readdirSync,
14
- renameSync,
15
- unlinkSync,
16
- writeFileSync
17
- } from 'node:fs';
18
- import { randomUUID } from 'node:crypto';
1
+ // scripts/install-skills.mjs
2
+ //
3
+ // The npm postinstall entry. What stays HERE, and why it cannot move:
4
+ //
5
+ // 1. The two per-platform fan-outs. Their `ideId === 'claude-code'` branches are the
6
+ // env-var override the 1.x-compat contract is written on, and they are pinned BY
7
+ // FILE NAME by `tests/unit/runtime/vendor-neutral-identity-guard.test.ts` — moving
8
+ // them would either drop a pinned decision or add one to a file the ratchet does
9
+ // not know about. Their bodies are unchanged.
10
+ // 2. The CLI entry block. It identifies its own path (`import.meta.url` against
11
+ // `process.argv[1]`), which is exactly what the module's own location means, so it
12
+ // cannot live anywhere else.
13
+ //
14
+ // Everything else moved to a sibling by responsibility; the public surface below is a
15
+ // re-export, so no importer changes.
16
+ //
19
17
  import { homedir } from 'node:os';
20
- import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
21
- import { fileURLToPath, pathToFileURL } from 'node:url';
22
-
23
- // The canonical store — `~/.peaks/{skills,agents,output-styles}`. Skills are
24
- // linked to it instead of to `<packageRoot>/skills/<name>`; see the loop in
25
- // `installBundledSkills` for why the target moved and why repair is now
26
- // unconditional. `canonical-store-link.mjs` is the same contract for the two
27
- // families whose entry is a single FILE rather than a tree, and it is where the
28
- // "the symlink was refused, here is the report" fallback lives. MUST both stay in
29
- // `package.json#files`: this file IS the npm postinstall, and an unpublished
30
- // sibling import breaks every user's install.
31
- import { reconcileCanonicalEntry } from './canonical-store.mjs';
32
- import { reconcileCanonicalFileEntry } from './canonical-store-link.mjs';
33
- import { pruneBundledEntries } from './canonical-store-prune.mjs';
34
-
35
- function getPathStats(path) {
36
- try {
37
- return lstatSync(path);
38
- } catch {
39
- return null;
40
- }
41
- }
42
-
43
- function validateOpenFile(fd, path, errorMessage) {
44
- const fdStats = fstatSync(fd);
45
- const pathStats = lstatSync(path);
46
- if (
47
- !fdStats.isFile() ||
48
- !pathStats.isFile() ||
49
- fdStats.dev !== pathStats.dev ||
50
- fdStats.ino !== pathStats.ino
51
- ) {
52
- throw new Error(errorMessage);
53
- }
54
- if (fdStats.nlink !== 1 || pathStats.nlink !== 1) {
55
- throw new Error(`${errorMessage}: hardlinked file`);
56
- }
57
- }
58
-
59
- function getSafeReadOpenFlags() {
60
- return typeof constants.O_NOFOLLOW === 'number'
61
- ? constants.O_RDONLY | constants.O_NOFOLLOW
62
- : constants.O_RDONLY;
63
- }
64
-
65
- function readFileSafely(path, errorMessage) {
66
- const fd = openSync(path, getSafeReadOpenFlags());
67
- try {
68
- validateOpenFile(fd, path, errorMessage);
69
- return readFileSync(fd, 'utf8');
70
- } finally {
71
- closeSync(fd);
72
- }
73
- }
74
-
75
- function validateInstallRoot(targetRoot, label) {
76
- const rootStats = lstatSync(targetRoot);
77
- if (rootStats.isSymbolicLink()) {
78
- throw new Error(`${label} install root must not be a symlink`);
79
- }
80
- if (!rootStats.isDirectory()) {
81
- throw new Error(`${label} install root must be a directory`);
82
- }
83
- return rootStats;
84
- }
85
-
86
- function createInstallRootValidator(targetRoot, label) {
87
- const expectedStats = validateInstallRoot(targetRoot, label);
88
- return () => {
89
- const rootStats = validateInstallRoot(targetRoot, label);
90
- if (rootStats.dev !== expectedStats.dev || rootStats.ino !== expectedStats.ino) {
91
- throw new Error(`${label} install root changed during write`);
92
- }
93
- };
94
- }
95
-
96
- function createInstallResult() {
97
- return { installed: [], skipped: [], pruned: [], fallbacks: [] };
98
- }
99
-
100
- function resolvePackageRoot(options = {}) {
101
- return resolve(options.packageRoot ?? join(dirname(fileURLToPath(import.meta.url)), '..'));
102
- }
103
-
104
- function readPackageVersion(packageRoot = resolvePackageRoot()) {
105
- const packageJson = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8'));
106
- if (typeof packageJson.version !== 'string' || packageJson.version.length === 0) {
107
- throw new Error('package.json version must be a non-empty string');
108
- }
109
-
110
- return packageJson.version;
111
- }
112
-
113
- function createConfigDefaults(packageRoot) {
114
- return {
115
- version: readPackageVersion(packageRoot),
116
- currentWorkspace: null,
117
- workspaces: [],
118
- language: 'en',
119
- economyMode: true,
120
- swarmMode: true,
121
- tokens: {},
122
- proxy: {}
123
- };
124
- }
125
-
126
- function createConfigResult(overrides = {}) {
127
- return { created: false, updated: false, skipped: false, ...overrides };
128
- }
129
-
130
- function isInsidePath(childPath, parentPath) {
131
- const relativePath = relative(parentPath, childPath);
132
- return relativePath === '' || (!relativePath.startsWith('..') && !isAbsolute(relativePath));
133
- }
134
-
135
- function isPlainObject(value) {
136
- return value !== null && typeof value === 'object' && !Array.isArray(value);
137
- }
138
-
139
- function mergeMissingConfigValues(existing, defaults) {
140
- return Object.entries(defaults).reduce(
141
- (next, [key, defaultValue]) => {
142
- if (!(key in next)) {
143
- return { ...next, [key]: defaultValue };
144
- }
145
-
146
- const existingValue = next[key];
147
- if (isPlainObject(existingValue) && isPlainObject(defaultValue)) {
148
- return { ...next, [key]: mergeMissingConfigValues(existingValue, defaultValue) };
149
- }
150
-
151
- return next;
152
- },
153
- { ...existing }
154
- );
155
- }
156
-
157
- function readConfigFile(configPath, label) {
158
- if (!existsSync(configPath)) {
159
- return null;
160
- }
161
-
162
- try {
163
- const parsed = JSON.parse(
164
- readFileSafely(configPath, `${label} config path changed during read`)
165
- );
166
- if (!isPlainObject(parsed)) {
167
- throw new Error(`${label} config must contain a JSON object`);
168
- }
169
-
170
- return parsed;
171
- } catch (error) {
172
- const message =
173
- error instanceof SyntaxError
174
- ? `${label} config must contain valid JSON`
175
- : error instanceof Error
176
- ? error.message
177
- : String(error);
178
- throw new Error(message);
179
- }
180
- }
181
-
182
- function validateConfigPath(root, peaksRoot, configPath, label) {
183
- const rootReal = realpathSync(root);
184
- const peaksStats = lstatSync(peaksRoot);
185
- const peaksReal = realpathSync(peaksRoot);
186
- if (
187
- !peaksStats.isDirectory() ||
188
- peaksStats.isSymbolicLink() ||
189
- peaksReal !== resolve(rootReal, '.peaks')
190
- ) {
191
- throw new Error(`${label} config path must stay inside the ${label.toLowerCase()} root`);
192
- }
193
-
194
- const configStats = getPathStats(configPath);
195
- if (configStats?.isSymbolicLink()) {
196
- throw new Error(`${label} config path must not be a symlink`);
197
- }
198
- if (configStats && !configStats.isFile()) {
199
- throw new Error(`${label} config path must be a file`);
200
- }
201
- if (configStats) {
202
- if (configStats.nlink !== 1) {
203
- throw new Error(`${label} config path must not be hardlinked`);
204
- }
205
- const configReal = realpathSync(configPath);
206
- if (!isInsidePath(configReal, rootReal) || !isInsidePath(configReal, peaksReal)) {
207
- throw new Error(`${label} config path must stay inside the ${label.toLowerCase()} root`);
208
- }
209
- }
210
- }
211
-
212
- function validateUserConfigPaths(userRoot, peaksRoot, configPath) {
213
- validateConfigPath(userRoot, peaksRoot, configPath, 'User');
214
- }
215
-
216
- function getSafeTempOpenFlags() {
217
- const baseFlags = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL;
218
- return typeof constants.O_NOFOLLOW === 'number' ? baseFlags | constants.O_NOFOLLOW : baseFlags;
219
- }
220
-
221
- function writeFileAtomically(configPath, content, errorMessage, validateBeforeWrite) {
222
- validateBeforeWrite();
223
-
224
- const tempPath = `${configPath}.${process.pid}.${randomUUID()}.tmp`;
225
- let fd = openSync(tempPath, getSafeTempOpenFlags(), 0o600);
226
- let renamed = false;
227
- let closeError = null;
228
- try {
229
- validateOpenFile(fd, tempPath, errorMessage);
230
- fchmodSync(fd, 0o600);
231
- writeFileSync(fd, content, 'utf8');
232
- const writeFd = fd;
233
- fd = null;
234
- closeSync(writeFd);
235
- validateBeforeWrite();
236
- const readFd = openSync(tempPath, getSafeReadOpenFlags());
237
- try {
238
- validateOpenFile(readFd, tempPath, errorMessage);
239
- } finally {
240
- closeSync(readFd);
241
- }
242
- renameSync(tempPath, configPath);
243
- renamed = true;
244
- } finally {
245
- if (fd !== null) {
246
- try {
247
- closeSync(fd);
248
- } catch (error) {
249
- closeError = error;
250
- }
251
- }
252
- try {
253
- if (!renamed && existsSync(tempPath)) {
254
- unlinkSync(tempPath);
255
- }
256
- } finally {
257
- if (closeError) {
258
- throw closeError;
259
- }
260
- }
261
- }
262
- }
263
-
264
- function writeUserConfig(userRoot, peaksRoot, configPath, content) {
265
- writeFileAtomically(configPath, content, 'User config path changed during write', () =>
266
- validateUserConfigPaths(userRoot, peaksRoot, configPath)
267
- );
268
- }
269
-
270
- function resolveProjectRoot(options) {
271
- const projectRoot = options.projectRoot ?? process.env.PEAKS_PROJECT_ROOT ?? process.env.INIT_CWD;
272
- return projectRoot ? resolve(projectRoot) : null;
273
- }
274
-
275
- /**
276
- * Slice #011: Detect the installed IDE for the postinstall dispatch layer.
277
- *
278
- * Mirrors `src/services/ide/ide-detector.ts:detectInstalledIde` (cwd heuristic)
279
- * but inlined here because `install-skills.mjs` is a plain `.mjs` script and
280
- * cannot import the TS service at runtime. The dispatch:
281
- * - Look for `.claude`, `.trae`, `.codex`, `.cursor`, `.qoder`,
282
- * `.tongyi-lingma` in the project root in that insertion order
283
- * (matches the registry's adapter order in `src/services/ide/ide-registry.ts`).
284
- * - Returns the first match, or `null` if no adapter's directory is present.
285
- *
286
- * If the resolved IDE has no `skillInstall` declared (Trae in slice 1.3.2),
287
- * the caller falls back to the legacy `~/.claude/{skills,output-styles}` path
288
- * + emits a stderr warning. The dispatch is conservative: the env-var
289
- * overrides `PEAKS_CLAUDE_SKILLS_DIR` / `PEAKS_CLAUDE_OUTPUT_STYLES_DIR`
290
- * continue to work, and the legacy default is preserved.
291
- *
292
- * 2026-09-15 (D12 fix): `hermes` and `openclaw` have install profiles in
293
- * `IDE_SKILL_INSTALL_PROFILES` but were absent from this table. A profile
294
- * that no detector can ever return is reachable only through the
295
- * every-platform fan-out — i.e. it is installed for users who do NOT have
296
- * the tool and skipped for nobody. Both are listed here now, so detection
297
- * and installation answer the same question with the same table.
298
- *
299
- * This table is ALSO the presence oracle for `isPlatformPresent` below: a
300
- * directory here in the project root means the user has that tool.
301
- */
302
- export const IDE_DETECTION_DIRS = [
303
- { id: 'claude-code', dir: '.claude' },
304
- { id: 'trae', dir: '.trae' },
305
- { id: 'trae-cn', dir: '.trae-cn' },
306
- { id: 'codex', dir: '.codex' },
307
- { id: 'cursor', dir: '.cursor' },
308
- { id: 'qoder', dir: '.qoder' },
309
- { id: 'tongyi-lingma', dir: '.tongyi-lingma' },
310
- { id: 'zcode', dir: '.zcode' },
311
- { id: 'hermes', dir: '.hermes' },
312
- { id: 'openclaw', dir: '.openclaw' }
313
- ];
314
-
315
- /**
316
- * Per-IDE skill install paths. Per peaks-loop tenet
317
- * "minimal-user-operation" (2026-06-11), the user should
318
- * never have to run a per-platform install command — the
319
- * `npm i -g peaks-loop` postinstall iterates the platforms
320
- * the user HAS and symlinks the peaks-* skill family into each
321
- * (see `isPlatformPresent`; D9/2026-09-15 made "has" load-bearing).
322
- *
323
- * 1.x had only `claude-code` (the other 5 entries were
324
- * `null`); real Trae users reported the Trae skill
325
- * directory was never populated. 2.0 fixes this by giving
326
- * every platform a canonical install path.
327
- */
328
- export const IDE_SKILL_INSTALL_PROFILES = {
329
- 'claude-code': {
330
- // `alwaysPresent` — see `isPlatformPresent`. Declared HERE, in the
331
- // vendor's own row, rather than as an `ideId === 'claude-code'` branch in
332
- // the consumer: the guard at
333
- // `tests/unit/runtime/vendor-neutral-identity-guard.test.ts` pins identity
334
- // DECISIONS to the vendor declarations, and a comparison in the consumer
335
- // is exactly the re-injection shape it exists to catch. A future vendor
336
- // opts into the same policy by setting this field.
337
- alwaysPresent: true,
338
- skillsDir: join(homedir(), '.claude', 'skills'),
339
- outputStylesDir: join(homedir(), '.claude', 'output-styles'),
340
- agentsDir: join(homedir(), '.claude', 'agents'),
341
- envVar: 'PEAKS_CLAUDE_SKILLS_DIR',
342
- outputStylesEnvVar: 'PEAKS_CLAUDE_OUTPUT_STYLES_DIR',
343
- agentsEnvVar: 'PEAKS_CLAUDE_AGENTS_DIR'
344
- },
345
- trae: {
346
- skillsDir: join(homedir(), '.trae', 'skills'),
347
- outputStylesDir: join(homedir(), '.trae', 'output-styles'),
348
- agentsDir: join(homedir(), '.trae', 'agents'),
349
- envVar: 'PEAKS_TRAE_SKILLS_DIR',
350
- outputStylesEnvVar: 'PEAKS_TRAE_OUTPUT_STYLES_DIR',
351
- agentsEnvVar: 'PEAKS_TRAE_AGENTS_DIR'
352
- },
353
- 'trae-cn': {
354
- skillsDir: join(homedir(), '.trae-cn', 'skills'),
355
- outputStylesDir: join(homedir(), '.trae-cn', 'output-styles'),
356
- agentsDir: join(homedir(), '.trae-cn', 'agents'),
357
- envVar: 'PEAKS_TRAE_CN_SKILLS_DIR',
358
- outputStylesEnvVar: 'PEAKS_TRAE_CN_OUTPUT_STYLES_DIR',
359
- agentsEnvVar: 'PEAKS_TRAE_CN_AGENTS_DIR'
360
- },
361
- codex: {
362
- skillsDir: join(homedir(), '.codex', 'skills'),
363
- outputStylesDir: join(homedir(), '.codex', 'output-styles'),
364
- agentsDir: join(homedir(), '.codex', 'agents'),
365
- envVar: 'PEAKS_CODEX_SKILLS_DIR',
366
- outputStylesEnvVar: 'PEAKS_CODEX_OUTPUT_STYLES_DIR',
367
- agentsEnvVar: 'PEAKS_CODEX_AGENTS_DIR'
368
- },
369
- cursor: {
370
- skillsDir: join(homedir(), '.cursor', 'skills'),
371
- outputStylesDir: join(homedir(), '.cursor', 'output-styles'),
372
- agentsDir: join(homedir(), '.cursor', 'agents'),
373
- envVar: 'PEAKS_CURSOR_SKILLS_DIR',
374
- outputStylesEnvVar: 'PEAKS_CURSOR_OUTPUT_STYLES_DIR',
375
- agentsEnvVar: 'PEAKS_CURSOR_AGENTS_DIR'
376
- },
377
- qoder: {
378
- skillsDir: join(homedir(), '.qoder', 'skills'),
379
- outputStylesDir: join(homedir(), '.qoder', 'output-styles'),
380
- envVar: 'PEAKS_QODER_SKILLS_DIR',
381
- outputStylesEnvVar: 'PEAKS_QODER_OUTPUT_STYLES_DIR'
382
- },
383
- 'tongyi-lingma': {
384
- skillsDir: join(homedir(), '.tongyi-lingma', 'skills'),
385
- outputStylesDir: join(homedir(), '.tongyi-lingma', 'output-styles'),
386
- envVar: 'PEAKS_TONGYI_SKILLS_DIR',
387
- outputStylesEnvVar: 'PEAKS_TONGYI_OUTPUT_STYLES_DIR'
388
- },
389
- hermes: {
390
- skillsDir: join(homedir(), '.hermes', 'skills'),
391
- outputStylesDir: join(homedir(), '.hermes', 'output-styles'),
392
- envVar: 'PEAKS_HERMES_SKILLS_DIR',
393
- outputStylesEnvVar: 'PEAKS_HERMES_OUTPUT_STYLES_DIR'
394
- },
395
- openclaw: {
396
- skillsDir: join(homedir(), '.openclaw', 'skills'),
397
- outputStylesDir: join(homedir(), '.openclaw', 'output-styles'),
398
- envVar: 'PEAKS_OPENCLAW_SKILLS_DIR',
399
- outputStylesEnvVar: 'PEAKS_OPENCLAW_OUTPUT_STYLES_DIR'
400
- },
401
- zcode: {
402
- skillsDir: join(homedir(), '.zcode', 'skills'),
403
- outputStylesDir: join(homedir(), '.zcode', 'output-styles'),
404
- agentsDir: join(homedir(), '.zcode', 'agents'),
405
- envVar: 'PEAKS_ZCODE_SKILLS_DIR',
406
- outputStylesEnvVar: 'PEAKS_ZCODE_OUTPUT_STYLES_DIR',
407
- agentsEnvVar: 'PEAKS_ZCODE_AGENTS_DIR'
408
- }
409
- };
410
-
411
- function detectInstalledIdeId(projectRoot) {
412
- if (!projectRoot) return null;
413
- for (const { id, dir } of IDE_DETECTION_DIRS) {
414
- if (existsSync(join(projectRoot, dir))) {
415
- return id;
416
- }
417
- }
418
- return null;
419
- }
420
-
421
- function resolveIdeSkillInstallProfile(ideId) {
422
- if (ideId === null) return null;
423
- return IDE_SKILL_INSTALL_PROFILES[ideId] ?? null;
424
- }
425
-
426
- /**
427
- * Is this platform actually installed FOR? (D9 fix, 2026-09-15.)
428
- *
429
- * Before this gate, the postinstall ran `mkdirSync(targetRoot, {recursive:
430
- * true})` for ALL 10 profiles unconditionally, so `npm i -g peaks-loop` created
431
- * `~/.hermes`, `~/.openclaw`, `~/.qoder`, `~/.tongyi-lingma` and `~/.zcode` in
432
- * the home of a user who has never installed those tools. The peaks-loop tenet
433
- * is "the user never has to run a per-platform install command" — it is NOT
434
- * "peaks-loop guesses which tools you have". Creating a directory for a tool
435
- * the user does not own is the tenet misread, and it is visible on disk.
436
- *
437
- * Present means EITHER:
438
- * - the tool's marker directory exists in the project root (the same
439
- * directory `detectInstalledIdeId` consults above), OR
440
- * - the tool's own home directory (`~/.<tool>`, derived from its profile)
441
- * already exists on this machine.
442
- *
443
- * `claude-code` is unconditionally present: it is peaks-loop's primary runtime
444
- * and the documented legacy install target, so a machine with no `~/.claude`
445
- * yet still gets the trees the user is installing peaks-loop for. That fact is
446
- * declared as `alwaysPresent: true` IN THE VENDOR'S OWN PROFILE ROW — not as an
447
- * `ideId === 'claude-code'` branch here. This file sits outside the adapter
448
- * layer, so an identity comparison in it is the re-injection shape
449
- * `tests/unit/runtime/vendor-neutral-identity-guard.test.ts` pins; reading the
450
- * vendor's declaration keeps the policy in one place and vendor-neutral.
451
- *
452
- * A named target bypasses this gate entirely — `options.targetRoot`,
453
- * `options.ideId`, and the `PEAKS_<TOOL>_SKILLS_DIR` env vars are callers who
454
- * have already said which platform they mean; guessing a second time there
455
- * would break the 1.x back-compat contract that slice 2026-06-12 restored.
456
- */
457
- function isPlatformPresent(ideId, profile, projectRoot) {
458
- if (profile.alwaysPresent === true) return true;
459
- const detection = IDE_DETECTION_DIRS.find((entry) => entry.id === ideId);
460
- if (
461
- detection !== undefined &&
462
- projectRoot !== null &&
463
- existsSync(join(projectRoot, detection.dir))
464
- ) {
465
- return true;
466
- }
467
- return existsSync(dirname(profile.skillsDir));
468
- }
469
-
470
- function warnUnverifiedIde(ideId, projectRoot) {
471
- process.stderr.write(
472
- `peaks install-skills: IDE '${ideId}' has no skillInstall profile declared; ` +
473
- `falling back to the legacy Claude Code path (~/.claude/skills + ~/.claude/output-styles) ` +
474
- `for project '${projectRoot}'. This is a slice #011 follow-up gap; ` +
475
- `see .peaks/memory/ide-adapter-resource-profile-framework.md.\n`
476
- );
477
- }
478
-
479
- function warnNoIdeDetected(projectRoot) {
480
- process.stderr.write(
481
- `peaks install-skills: no IDE detected in '${projectRoot ?? '(project root unknown)'}'; ` +
482
- `installing to the legacy Claude Code path (~/.claude/skills + ~/.claude/output-styles). ` +
483
- `Set PEAKS_CLAUDE_SKILLS_DIR / PEAKS_CLAUDE_OUTPUT_STYLES_DIR to override.\n`
484
- );
485
- }
486
-
487
- function writeMergedConfig(configPath, label, defaults, writeConfig) {
488
- const existing = readConfigFile(configPath, label);
489
- const next = {
490
- ...(existing === null ? defaults : mergeMissingConfigValues(existing, defaults)),
491
- version: defaults.version
492
- };
493
- const currentJson = existing === null ? null : `${JSON.stringify(existing, null, 2)}\n`;
494
- const nextJson = `${JSON.stringify(next, null, 2)}\n`;
495
-
496
- if (currentJson === nextJson) {
497
- return createConfigResult();
498
- }
499
-
500
- writeConfig(nextJson);
501
- return createConfigResult(existing === null ? { created: true } : { updated: true });
502
- }
503
-
504
- export function installUserConfig(options = {}) {
505
- if (
506
- process.env.PEAKS_SKIP_SKILL_INSTALL === '1' ||
507
- process.env.PEAKS_SKIP_USER_CONFIG_INSTALL === '1'
508
- ) {
509
- return createConfigResult({ skipped: true });
510
- }
511
-
512
- const userRoot = resolve(options.userRoot ?? homedir());
513
- const peaksRoot = resolve(userRoot, '.peaks');
514
- const configPath = resolve(peaksRoot, 'config.json');
515
- if (!isInsidePath(configPath, userRoot)) {
516
- throw new Error('User config path must stay inside the user root');
517
- }
518
-
519
- if (!existsSync(peaksRoot)) {
520
- mkdirSync(peaksRoot, { recursive: true });
521
- }
522
- validateUserConfigPaths(userRoot, peaksRoot, configPath);
18
+ import { join, resolve } from 'node:path';
19
+ import { pathToFileURL } from 'node:url';
523
20
 
524
- return writeMergedConfig(
525
- configPath,
526
- 'User',
527
- createConfigDefaults(options.packageRoot),
528
- (content) => writeUserConfig(userRoot, peaksRoot, configPath, content)
529
- );
530
- }
531
-
532
- /*
533
- * D11 (2026-09-15): `installProjectConfig` — plus its two private helpers
534
- * `writeProjectConfig` and `validateProjectConfigPaths` — was DELETED here.
535
- *
536
- * It was dead: definition + `export`, and zero call sites in the whole repo
537
- * (verified by a repo-wide grep for the identifier, excluding node_modules
538
- * and dist). The `PEAKS_SKIP_PROJECT_CONFIG_INSTALL` env var existed only to
539
- * disable it.
540
- *
541
- * Deleted rather than wired, because wiring it would be the bigger defect:
542
- * `npm i -g peaks-loop` executed from inside a user's repo would write
543
- * `<project>/.peaks/config.json` as a side effect of a GLOBAL install. The
544
- * project-level config is owned by `peaks workspace init` and the config
545
- * service at runtime; a postinstall reaching into the current working
546
- * directory to create project state is the opposite of the "minimal user
547
- * operation" tenet — an operation the user never asked for.
548
- *
549
- * The user-config sibling (`installUserConfig`, below) is live and is what
550
- * the postinstall actually calls.
551
- */
552
-
553
- export function installBundledSkills(options = {}) {
554
- const packageRoot = resolvePackageRoot(options);
555
- const skillsRoot = join(packageRoot, 'skills');
556
-
557
- if (process.env.PEAKS_SKIP_SKILL_INSTALL === '1' || !existsSync(skillsRoot)) {
558
- return createInstallResult();
559
- }
560
-
561
- // Slice #011: IDE-aware dispatch. Precedence (highest first):
562
- // 1. explicit options.targetRoot (test / hook override)
563
- // 2. options.ideId skillInstall.skillsDir (per-IDE dispatch)
564
- // 3. PEAKS_CLAUDE_SKILLS_DIR env var (legacy back-compat)
565
- // 4. detected IDE's skillInstall.skillsDir (auto-detect from projectRoot)
566
- // 5. legacy default (~/.claude/skills) (no-IDE fallback)
567
- const projectRoot = resolveProjectRoot(options);
568
- const detectedIdeId = detectInstalledIdeId(projectRoot);
569
- const detectedProfile = resolveIdeSkillInstallProfile(detectedIdeId);
570
-
571
- if (
572
- options.targetRoot === undefined &&
573
- options.ideId === undefined &&
574
- detectedProfile === null &&
575
- detectedIdeId !== null
576
- ) {
577
- warnUnverifiedIde(detectedIdeId, projectRoot ?? '(project root unknown)');
578
- }
579
- if (
580
- options.targetRoot === undefined &&
581
- options.ideId === undefined &&
582
- detectedIdeId === null &&
583
- projectRoot !== null
584
- ) {
585
- warnNoIdeDetected(projectRoot);
586
- }
587
-
588
- const profileSkillsDir = detectedProfile?.skillsDir ?? null;
589
- const targetRoot = resolve(
590
- options.targetRoot ??
591
- (options.ideId !== undefined
592
- ? (resolveIdeSkillInstallProfile(options.ideId)?.skillsDir ?? null)
593
- : null) ??
594
- process.env.PEAKS_CLAUDE_SKILLS_DIR ??
595
- profileSkillsDir ??
596
- join(homedir(), '.claude', 'skills')
597
- );
598
-
599
- const installed = [];
600
- const skipped = [];
601
- mkdirSync(targetRoot, { recursive: true });
602
- const validateSkillsRoot = createInstallRootValidator(targetRoot, 'Peaks skills');
603
-
604
- // After the v2.13.0 bee-demote (commit de0872b), the role skills
605
- // (peaks-prd, peaks-rd, peaks-qa, peaks-ui, peaks-sc, peaks-txt)
606
- // moved under `skills/bee/<role>/` while user-facing helpers stayed
607
- // at `skills/<name>/`. Build the install candidate list by walking
608
- // top-level entries (user-facing helpers) AND `skills/bee/<role>`
609
- // (demoted role skills). Each candidate is installed under its
610
- // basename so the postinstall links `~/.claude/skills/peaks-rd`
611
- // to `skills/bee/peaks-rd` (rather than `skills/bee`).
612
- const beeRoot = join(skillsRoot, 'bee');
613
- /** @type {Array<{ skillName: string, sourcePath: string }>} */
614
- const candidates = [];
615
- for (const skillName of readdirSync(skillsRoot)) {
616
- if (skillName === 'bee') continue;
617
- const sourcePath = join(skillsRoot, skillName);
618
- const skillFile = join(sourcePath, 'SKILL.md');
619
- if (!lstatSync(sourcePath).isDirectory() || !existsSync(skillFile)) continue;
620
- candidates.push({ skillName, sourcePath });
621
- }
622
- if (existsSync(beeRoot) && lstatSync(beeRoot).isDirectory()) {
623
- for (const skillName of readdirSync(beeRoot)) {
624
- const sourcePath = join(beeRoot, skillName);
625
- const skillFile = join(sourcePath, 'SKILL.md');
626
- if (!lstatSync(sourcePath).isDirectory() || !existsSync(skillFile)) continue;
627
- // De-dupe: a top-level helper with the same name wins (preserves
628
- // the existing install contract for any helper that shares a name
629
- // with a demoted role skill, e.g. legacy overlap).
630
- if (candidates.some((c) => c.skillName === skillName)) continue;
631
- candidates.push({ skillName, sourcePath });
632
- }
633
- }
634
-
635
- for (const { skillName, sourcePath } of candidates) {
636
- const targetPath = join(targetRoot, skillName);
637
-
638
- // Slice 2 (`agents-canonical-store`) — the entry points at the CANONICAL
639
- // STORE (`~/.peaks/skills/<name>`), not at `<packageRoot>/skills/<name>`,
640
- // because `<packageRoot>` changes on every `npm i -g peaks-loop@latest` and
641
- // the links must bind to a PATH peaks-loop owns, not to a VERSION. Repair is
642
- // the only behaviour: an entry peaks-loop owns (sidecar matches, or the link
643
- // dangles) is relinked onto the store; only a real directory the user authored
644
- // is left alone and reported `skipped`. `options.reconcileJunctions` is
645
- // therefore READ NOWHERE here — still accepted (`sync-service.ts` forwards it)
646
- // so passing it changes nothing, and removing it is a CLI-surface change this
647
- // slice does not own.
648
- validateSkillsRoot();
649
- const reconciled = reconcileCanonicalEntry({
650
- kind: 'skills',
651
- name: skillName,
652
- sourcePath,
653
- linkPath: targetPath
654
- });
655
- validateSkillsRoot();
656
-
657
- if (reconciled.linkAction === 'skipped') {
658
- skipped.push(skillName);
659
- continue;
660
- }
661
- installed.push(skillName);
662
- }
663
-
664
- // Slice 3 — PRUNE. The loop above only ever ADDS. A skill removed from the
665
- // package left its canonical copy, every IDE link and every sidecar on the
666
- // user's machine forever, because nothing walked the DESTINATIONS. This does,
667
- // and deletes only what `isManagedEntry` proves is ours.
668
- const { pruned } = pruneBundledEntries({
669
- kind: 'skills',
670
- keepNames: candidates.map((candidate) => candidate.skillName),
671
- linkDirs: [targetRoot]
672
- });
673
-
674
- return { installed, skipped, pruned };
675
- }
676
-
677
- /**
678
- * DISPATCH STRATEGY — the three paths in this file, and why they differ
679
- * (D10, written down 2026-09-15).
680
- *
681
- * This script installs three kinds of asset, and each one resolves its target
682
- * by a different rule. That was undocumented and untested; it is now
683
- * documented here and each rule is stated with its reason.
684
- *
685
- * 1. SKILLS (`installBundledSkillsForAllPlatforms`)
686
- * Fan out to every platform the user HAS (`isPlatformPresent`).
687
- * Reason: a skill directory is per-tool; the same skill is legitimately
688
- * wanted in several tools at once, and peaks cannot know which tool the
689
- * user will open next. Fan-out is the tenet; presence is the licence.
690
- *
691
- * 2. AGENTS (`installBundledAgentsForAllPlatforms`)
692
- * Fan out to present platforms, INTERSECTED with the profiles that
693
- * declare `agentsDir`. Reason: sub-agent loaders are not a universal
694
- * concept — only 6 of 10 profiles have one. The intersection is the fan
695
- * of (1) with a capability filter, not a third rule.
696
- *
697
- * 3. OUTPUT STYLES (`installBundledOutputStyles`, this function)
698
- * ONE target: the IDE detected in the project root, else the legacy
699
- * `~/.claude/output-styles`. Reason: an output style is not a
700
- * per-tool asset — it is registered as THE active style for the
701
- * session by writing a single `outputStyle` key into
702
- * `~/.claude/settings.json` (`installBundledOutputStyleDefault`). A
703
- * second copy in `~/.trae/output-styles` would be a file nothing reads
704
- * and a setting no one consults; fanning it out would be storing the
705
- * same answer in ten places and reading it from one.
706
- *
707
- * So: (1) and (2) share one predicate and differ only by capability; (3) is
708
- * deliberately single-target, and this comment is the reason the brief asked
709
- * for. If a future platform needs a different rule, state it here.
710
- */
711
- export function installBundledOutputStyles(options = {}) {
712
- const packageRoot = resolvePackageRoot(options);
713
- const outputStylesRoot = join(packageRoot, 'output-styles');
714
-
715
- if (process.env.PEAKS_SKIP_SKILL_INSTALL === '1' || !existsSync(outputStylesRoot)) {
716
- return createInstallResult();
717
- }
718
-
719
- // Slice #011: IDE-aware dispatch. Same precedence as installBundledSkills.
720
- const projectRoot = resolveProjectRoot(options);
721
- const detectedIdeId = detectInstalledIdeId(projectRoot);
722
- const detectedProfile = resolveIdeSkillInstallProfile(detectedIdeId);
723
-
724
- const profileOutputStylesDir = detectedProfile?.outputStylesDir ?? null;
725
- const targetRoot = resolve(
726
- options.targetRoot ??
727
- (options.ideId !== undefined
728
- ? (resolveIdeSkillInstallProfile(options.ideId)?.outputStylesDir ?? null)
729
- : null) ??
730
- process.env.PEAKS_CLAUDE_OUTPUT_STYLES_DIR ??
731
- profileOutputStylesDir ??
732
- join(homedir(), '.claude', 'output-styles')
733
- );
734
-
735
- const installed = [];
736
- const skipped = [];
737
- const fallbacks = [];
738
- mkdirSync(targetRoot, { recursive: true });
739
- const validateOutputStylesRoot = createInstallRootValidator(targetRoot, 'Peaks output styles');
740
-
741
- // Slice 4 (`agents-canonical-store`) — same move slice 2 made for skills, with the
742
- // one difference the brief calls out: a style is a FILE, so the link is a file
743
- // symlink, which Windows only grants with developer mode or Administrator.
744
- // `reconcileCanonicalFileEntry` is the whole policy — canonical copy, link, and the
745
- // reported fallback to a real copy when the host refuses the link. The loop below
746
- // no longer decides ownership, and that is deliberate: the decision it used to make
747
- // was `resolve(marker.sourcePath) === resolve(sourcePath)`, a string comparison
748
- // against a version-scoped package path that went false forever on the first
749
- // upgrade and silently froze the style.
750
- const bundledStyleNames = [];
751
- for (const outputStyleName of readdirSync(outputStylesRoot)) {
752
- const sourcePath = join(outputStylesRoot, outputStyleName);
753
- if (!lstatSync(sourcePath).isFile() || !outputStyleName.endsWith('.md')) continue;
754
- bundledStyleNames.push(outputStyleName);
755
-
756
- validateOutputStylesRoot();
757
- const reconciled = reconcileCanonicalFileEntry({
758
- kind: 'output-styles',
759
- name: outputStyleName,
760
- sourcePath,
761
- linkPath: join(targetRoot, outputStyleName),
762
- createFileLink: options.createFileLink
763
- });
764
- validateOutputStylesRoot();
765
-
766
- if (reconciled.linkAction === 'skipped') {
767
- skipped.push(outputStyleName);
768
- continue;
769
- }
770
- installed.push(outputStyleName);
771
- if (reconciled.fallback !== null) {
772
- fallbacks.push({ name: outputStyleName, ...reconciled.fallback });
773
- }
774
- }
775
-
776
- // The delete half, for the same reason skills have one: the loop above only ever
777
- // adds. A style removed from the package used to leave its canonical copy, its IDE
778
- // link and both sidecars behind forever.
779
- const { pruned } = pruneBundledEntries({
780
- kind: 'output-styles',
781
- keepNames: bundledStyleNames,
782
- linkDirs: [targetRoot]
783
- });
784
-
785
- return { installed, skipped, pruned, fallbacks };
786
- }
787
-
788
- /**
789
- * Slice 2026-08-02 — auto-register bundled output style.
790
- *
791
- * Real user feedback (2026-08-02): `npm i -g peaks-loop@latest` on a
792
- * fresh 0-1 project copied `peaks-skill-swarm.md` into
793
- * `~/.claude/output-styles/` but never wrote
794
- * `outputStyle: 'peaks-skill-swarm'` into `~/.claude/settings.json`.
795
- * Result: Claude Code loaded no Peaks output style in new sessions.
796
- *
797
- * The fix: when `peaks-skill-swarm.md` is present in the bundled output
798
- * styles directory AND `~/.claude/settings.json` does NOT yet declare
799
- * `outputStyle`, merge in `outputStyle: 'peaks-skill-swarm'` while
800
- * preserving every other key. If the user already set `outputStyle`,
801
- * leave it alone — the user is the source of truth.
802
- *
803
- * Resolution precedence:
804
- * 1. options.settingsFile (explicit override / test hook)
805
- * 2. PEAKS_CLAUDE_SETTINGS_FILE env var (CI override)
806
- * 3. homedir-relative `<homedir>/.claude/settings.json` (default)
807
- *
808
- * Soft-fail contract: every error path (missing file, malformed JSON,
809
- * missing bundled style file, permission denied) is logged to stderr
810
- * and returns `{ skipped: true, ... }` instead of throwing. The
811
- * postinstall must never fail on this step because the bundled file
812
- * itself was already written successfully.
813
- */
814
- function resolveSettingsFilePath(options = {}) {
815
- if (typeof options.settingsFile === 'string' && options.settingsFile.length > 0) {
816
- return resolve(options.settingsFile);
817
- }
818
- const envOverride = process.env.PEAKS_CLAUDE_SETTINGS_FILE;
819
- if (typeof envOverride === 'string' && envOverride.length > 0) {
820
- return resolve(envOverride);
821
- }
822
- return resolve(homedir(), '.claude', 'settings.json');
823
- }
824
-
825
- export function installBundledOutputStyleDefault(options = {}) {
826
- if (
827
- process.env.PEAKS_SKIP_SKILL_INSTALL === '1' ||
828
- process.env.PEAKS_SKIP_OUTPUT_STYLE_DEFAULT === '1'
829
- ) {
830
- return { skipped: true, reason: 'PEAKS_SKIP_OUTPUT_STYLE_DEFAULT=1' };
831
- }
832
-
833
- const bundledStylesDir = resolve(
834
- options.targetRoot ??
835
- process.env.PEAKS_CLAUDE_OUTPUT_STYLES_DIR ??
836
- join(homedir(), '.claude', 'output-styles')
837
- );
838
- const settingsPath = resolveSettingsFilePath(options);
839
- const bundledStyleName = 'peaks-skill-swarm.md';
840
- const bundledStylePath = join(bundledStylesDir, bundledStyleName);
841
-
842
- // Read the existing settings.json (if any) FIRST so a malformed file
843
- // surfaces as a parse error (more actionable to the user than the
844
- // generic "bundled style not present" message). Malformed JSON is a
845
- // user-authored file we MUST NOT clobber — log and skip.
846
- let existing = null;
847
- if (existsSync(settingsPath)) {
848
- const stats = getPathStats(settingsPath);
849
- if (!stats || !stats.isFile() || stats.isSymbolicLink() || stats.nlink !== 1) {
850
- process.stderr.write(
851
- `peaks install-skills: refusing to overwrite non-regular settings.json at ${settingsPath}\n`
852
- );
853
- return { skipped: true, reason: 'settings.json is not a regular file' };
854
- }
855
- try {
856
- const raw = readFileSafely(settingsPath, 'Claude settings.json path changed during read');
857
- const parsed = JSON.parse(raw);
858
- if (!isPlainObject(parsed)) {
859
- process.stderr.write(
860
- `peaks install-skills: refusing to overwrite settings.json that is not a JSON object at ${settingsPath}\n`
861
- );
862
- return { skipped: true, reason: 'settings.json is not a JSON object' };
863
- }
864
- existing = parsed;
865
- } catch (error) {
866
- const message = error instanceof Error ? error.message : String(error);
867
- process.stderr.write(
868
- `peaks install-skills: failed to parse settings.json at ${settingsPath}: ${message}; leaving untouched\n`
869
- );
870
- return { skipped: true, reason: 'settings.json parse error' };
871
- }
872
- }
873
-
874
- // Pre-condition: the bundled style file must be present in the
875
- // dispatched target. If it's not, we MUST NOT inject
876
- // `outputStyle: 'peaks-skill-swarm'` — Claude Code would then fail
877
- // to load the style and fall back to default anyway, leaving the
878
- // user with broken state. Skipping is the safe default.
879
- if (!existsSync(bundledStylePath)) {
880
- return { skipped: true, reason: `bundled output style not present at ${bundledStylePath}` };
881
- }
882
-
883
- // User-authored outputStyle wins — never overwrite.
884
- if (
885
- existing !== null &&
886
- typeof existing.outputStyle === 'string' &&
887
- existing.outputStyle.length > 0
888
- ) {
889
- return {
890
- skipped: true,
891
- reason: `user-defined outputStyle already set: ${existing.outputStyle}`
892
- };
893
- }
894
-
895
- const next =
896
- existing === null
897
- ? { outputStyle: 'peaks-skill-swarm' }
898
- : { ...existing, outputStyle: 'peaks-skill-swarm' };
899
-
900
- const existingJson = existing === null ? null : `${JSON.stringify(existing, null, 2)}\n`;
901
- const nextJson = `${JSON.stringify(next, null, 2)}\n`;
902
- if (existingJson === nextJson) {
903
- return { skipped: true, reason: 'no change' };
904
- }
905
-
906
- try {
907
- writeFileAtomically(
908
- settingsPath,
909
- nextJson,
910
- 'Claude settings.json path changed during write',
911
- () => {
912
- if (!existsSync(settingsPath)) return;
913
- const stats = getPathStats(settingsPath);
914
- if (!stats || !stats.isFile() || stats.isSymbolicLink() || stats.nlink !== 1) {
915
- throw new Error('Claude settings.json path is not a regular file');
916
- }
917
- }
918
- );
919
- } catch (error) {
920
- const message = error instanceof Error ? error.message : String(error);
921
- process.stderr.write(
922
- `peaks install-skills: failed to write settings.json at ${settingsPath}: ${message}\n`
923
- );
924
- return { skipped: true, reason: 'write error', error: message };
925
- }
926
-
927
- return {
928
- installed: true,
929
- created: existing === null,
930
- updated: existing !== null,
931
- settingsPath,
932
- outputStyle: 'peaks-skill-swarm'
933
- };
934
- }
935
-
936
- /**
937
- * Slice 7/7 — bundled agents (Claude Code sub-agent prompts).
938
- *
939
- * Writes into the IDE's sub-agent loader directory (`~/.claude/agents/` for Claude
940
- * Code, and whatever `agentsDir` the detected profile declares). The bytes live in
941
- * the canonical store (`~/.peaks/agents/<name>`) and the IDE entry is a link to it.
942
- *
943
- * Ownership policy — ONE predicate, in `canonical-store-link.mjs::isOwnedFileEntry`,
944
- * and it is deliberately not a string comparison:
945
- * - entry resolves to the canonical copy → leave it (idempotent run)
946
- * - entry is a link we wrote (sidecar agrees, dangles) → relink onto the store
947
- * - entry is a real file with OUR sidecar beside it → re-point it at the store
948
- * - entry is a real file with a LEGACY sidecar whose
949
- * recorded package path no longer resolves → same: repair it
950
- * - entry is a real file we never wrote (no sidecar) → skip (user-authored)
951
- * - entry is a real file whose sidecar names a path
952
- * that STILL resolves → skip (a second live install)
953
- *
954
- * The two "skip" arms above are the ones the old code got wrong in opposite
955
- * directions: it skipped whenever the package path merely DIFFERED, which froze
956
- * every entry on the first upgrade, silently and permanently. See
957
- * `isOwnedFileEntry` for why a vanished package path is proof of an earlier install.
958
- */
959
- export function installBundledAgents(options = {}) {
960
- const packageRoot = resolvePackageRoot(options);
961
- const agentsRoot = join(packageRoot, 'agents');
962
-
963
- // Per-IDE env-var override (claude-code only today): PEAKS_CLAUDE_AGENTS_DIR.
964
- // Universal escape hatch: PEAKS_SKIP_AGENT_INSTALL=1 (parallel to
965
- // PEAKS_SKIP_SKILL_INSTALL).
966
- if (
967
- process.env.PEAKS_SKIP_SKILL_INSTALL === '1' ||
968
- process.env.PEAKS_SKIP_AGENT_INSTALL === '1' ||
969
- !existsSync(agentsRoot)
970
- ) {
971
- return createInstallResult();
972
- }
973
-
974
- // Slice #011: IDE-aware dispatch. Same precedence as installBundledOutputStyles.
975
- const projectRoot = resolveProjectRoot(options);
976
- const detectedIdeId = detectInstalledIdeId(projectRoot);
977
- const detectedProfile = resolveIdeSkillInstallProfile(detectedIdeId);
978
-
979
- const profileAgentsDir = detectedProfile?.agentsDir ?? null;
980
- const targetRoot = resolve(
981
- options.targetRoot ??
982
- (options.ideId !== undefined
983
- ? (resolveIdeSkillInstallProfile(options.ideId)?.agentsDir ?? null)
984
- : null) ??
985
- process.env.PEAKS_CLAUDE_AGENTS_DIR ??
986
- profileAgentsDir ??
987
- join(homedir(), '.claude', 'agents')
988
- );
989
-
990
- const installed = [];
991
- const skipped = [];
992
- const fallbacks = [];
993
- mkdirSync(targetRoot, { recursive: true });
994
- const validateAgentsRoot = createInstallRootValidator(targetRoot, 'Peaks agents');
995
-
996
- // Slice 4 (`agents-canonical-store`) — the canonical store owns the bytes and the
997
- // IDE entry points at it. Everything this loop used to decide about ownership is
998
- // now `reconcileCanonicalFileEntry`'s, and the reason is the defect the brief
999
- // names as this slice's core: the old test was
1000
- // `resolve(marker.sourcePath) === resolve(sourcePath)`, a STRING comparison against
1001
- // the package path of the installing version — a path with a node version inside it
1002
- // (`…\nvm\v24.21.0\node_modules\peaks-loop\agents\…`). It went false the first time
1003
- // the package moved and stayed false, so the agent silently never updated again.
1004
- const bundledAgentNames = [];
1005
- for (const agentFileName of readdirSync(agentsRoot)) {
1006
- const sourcePath = join(agentsRoot, agentFileName);
1007
- if (!lstatSync(sourcePath).isFile() || !agentFileName.endsWith('.md')) continue;
1008
- bundledAgentNames.push(agentFileName);
1009
-
1010
- validateAgentsRoot();
1011
- const reconciled = reconcileCanonicalFileEntry({
1012
- kind: 'agents',
1013
- name: agentFileName,
1014
- sourcePath,
1015
- linkPath: join(targetRoot, agentFileName),
1016
- createFileLink: options.createFileLink
1017
- });
1018
- validateAgentsRoot();
1019
-
1020
- if (reconciled.linkAction === 'skipped') {
1021
- skipped.push(agentFileName);
1022
- continue;
1023
- }
1024
- installed.push(agentFileName);
1025
- if (reconciled.fallback !== null) {
1026
- fallbacks.push({ name: agentFileName, ...reconciled.fallback });
1027
- }
1028
- }
1029
-
1030
- // The delete half: an agent the package stops shipping used to leave its canonical
1031
- // copy, its IDE entry and both sidecars on the machine forever.
1032
- const { pruned } = pruneBundledEntries({
1033
- kind: 'agents',
1034
- keepNames: bundledAgentNames,
1035
- linkDirs: [targetRoot]
1036
- });
1037
-
1038
- return { installed, skipped, pruned, fallbacks };
1039
- }
1040
-
1041
- /**
1042
- * A fallback is not a failure — but it must never LOOK like a plain success. The
1043
- * entry is a real copy instead of a link, and the postinstall says so, naming each
1044
- * one. Silence here would be the same shape as the defect this slice fixes: an
1045
- * outcome nobody can observe from the outside.
1046
- */
1047
- function reportCopyFallbacks(label, fallbacks) {
1048
- if (fallbacks.length === 0) return;
1049
- process.stderr.write(
1050
- `Peaks ${label}: ${fallbacks.map((entry) => entry.name).join(', ')} ` +
1051
- 'installed as REAL COPIES because this host refused a symlink ' +
1052
- `(${fallbacks[0].code ?? 'no code returned'})\n`
1053
- );
1054
- }
1055
-
1056
- /**
1057
- * The other half of the same rule, for the other bucket. A `skipped` entry means the
1058
- * canonical copy WAS written and the IDE entry was NOT — a half migration. Whatever the
1059
- * cause (a file the user authored, or an entry whose sidecar names a second live
1060
- * installation), it must not be invisible: the whole defect this slice fixes was an
1061
- * outcome nobody could observe from outside.
1062
- */
1063
- function reportLeftAlone(label, skipped) {
1064
- if (skipped.length === 0) return;
1065
- process.stderr.write(
1066
- `Peaks ${label}: left alone, not peaks-loop's to replace: ${skipped.join(', ')}\n`
1067
- );
1068
- }
21
+ import { installBundledAgents, installBundledSkills } from './install-skills-bundles.mjs';
22
+ import {
23
+ IDE_SKILL_INSTALL_PROFILES,
24
+ isPlatformPresent,
25
+ resolveProjectRoot
26
+ } from './install-skills-ide.mjs';
27
+ import { installBundledOutputStyles } from './install-skills-output-styles.mjs';
28
+ import { installBundledOutputStyleDefault } from './install-skills-output-style-default.mjs';
29
+ import { reportCopyFallbacks, reportLeftAlone } from './install-skills-report.mjs';
30
+ import { createConfigResult, installUserConfig } from './install-skills-user-config.mjs';
31
+
32
+ // THE PUBLIC SURFACE, re-exported so no importer changes. IDE_DETECTION_DIRS is
33
+ // not read here, so it is re-exported without a local binding.
34
+ export { IDE_DETECTION_DIRS } from './install-skills-ide.mjs';
35
+ export { IDE_SKILL_INSTALL_PROFILES };
36
+ export { installBundledSkills, installBundledAgents };
37
+ export { installUserConfig };
38
+ export { installBundledOutputStyles, installBundledOutputStyleDefault };
1069
39
 
1070
40
  /**
1071
41
  * Per-platform fan-out — iterate the platforms the user HAS and call