@openchambery/web 1.19.22-beta.1 → 2.0.0-beta.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 (311) hide show
  1. package/README.md +13 -7
  2. package/bin/cli-opencode.test.js +208 -0
  3. package/bin/cli.js +62 -9
  4. package/bin/lib/cli-startup.js +105 -2
  5. package/bin/lib/cli-startup.test.js +179 -0
  6. package/dist/assets/{AgentAvatar-CRaBJXKs.js → AgentAvatar-B_JQoreg.js} +1 -1
  7. package/dist/assets/AssistantView-ClQAfGOT.js +3 -0
  8. package/dist/assets/{DiagramView-D1LGK_s2.js → DiagramView-BNnda8qO.js} +1 -1
  9. package/dist/assets/{FadeInOnReveal-B0kJo9Ii.js → FadeInOnReveal-VYQTSAkr.js} +1 -1
  10. package/dist/assets/{Icon-BgdfPhNs.js → Icon-BiEQEcih.js} +1 -1
  11. package/dist/assets/MarkdownRenderer-CJHSsWin.js +2 -0
  12. package/dist/assets/{MarkdownRendererImpl-DfoCJ1Mq.js → MarkdownRendererImpl-DwrHFGUp.js} +3 -3
  13. package/dist/assets/{MarkstreamRendererImpl-D2dsW5Lg.js → MarkstreamRendererImpl-CSnbQMu3.js} +1 -1
  14. package/dist/assets/MobileDetailNavigation-BXr6AGLT.js +1 -0
  15. package/dist/assets/{MobileShareBridge-2Yjn6wXg.js → MobileShareBridge-D7OE9OP_.js} +2 -2
  16. package/dist/assets/MobileSurface-D2jUkX9l.js +2 -0
  17. package/dist/assets/{MultiRunWindow-tTbMFowS.js → MultiRunWindow-DF3iIFCd.js} +1 -1
  18. package/dist/assets/{OnboardingScreen-CD2KtlF3.js → OnboardingScreen-BjHmlwlw.js} +1 -1
  19. package/dist/assets/OverlayScrollbar-DLtWO_Gp.js +2 -0
  20. package/dist/assets/SettingsView-BRxELjow.js +25 -0
  21. package/dist/assets/{SettingsWindow-zIa76s4t.js → SettingsWindow-BI5swspb.js} +1 -1
  22. package/dist/assets/{TerminalView-CDdzF4yi.js → TerminalView-BU3x8-fr.js} +1 -1
  23. package/dist/assets/{ToolOutputDialog-DNqA4uBl.js → ToolOutputDialog-BEWhBcRd.js} +2 -2
  24. package/dist/assets/appThemeRegistry-4MR0F7Jz.js +1 -0
  25. package/dist/assets/{button-CI7Q-Zw_.js → button-DKuz3L8l.js} +1 -1
  26. package/dist/assets/collapsible-DXbDynN4.js +3 -0
  27. package/dist/assets/context-menu-CqolsLCm.js +1 -0
  28. package/dist/assets/{desktopHosts-B1ixEylr.js → desktopHosts-CoDkOnyS.js} +1 -1
  29. package/dist/assets/{desktopRelayRestore-4JmFUw8r.js → desktopRelayRestore-DDlVKyOE.js} +1 -1
  30. package/dist/assets/{diagram-DdLO8HZ9.js → diagram-D7T8HQgj.js} +1 -1
  31. package/dist/assets/{dialog-B6pnIYmp.js → dialog-DQYl3W_o.js} +1 -1
  32. package/dist/assets/es-CN3E_zXj.js +15 -0
  33. package/dist/assets/{fileMentionTouchSelection-DNeiqns-.js → fileMentionTouchSelection-BZ6ovBYH.js} +1 -1
  34. package/dist/assets/{fileQueries-DQc8EJUf.js → fileQueries-JrpcsOCs.js} +1 -1
  35. package/dist/assets/fr-9zZvQZD2.js +13 -0
  36. package/dist/assets/gitApi-CM42qUed.js +10 -0
  37. package/dist/assets/{imageSource-BlAJJUEI.js → imageSource-CIl0FL10.js} +1 -1
  38. package/dist/assets/{input-D-__fwRi.js → input-CzQy8UhV.js} +1 -1
  39. package/dist/assets/insertionBoundaries-CtkZ9720.js +1 -0
  40. package/dist/assets/ja-Dnjk_82N.js +15 -0
  41. package/dist/assets/ko-BxCEE99C.js +15 -0
  42. package/dist/assets/main-BILgBI3B.js +169 -0
  43. package/dist/assets/main-BeveiW6F.js +2 -0
  44. package/dist/assets/markdown-worker-bS5OgGO3.js +2 -0
  45. package/dist/assets/{markdownHeightCache-BhxqJOvf.js → markdownHeightCache-Cj1I5NDw.js} +1 -1
  46. package/dist/assets/{markdownTheme-DOfGg-A7.js → markdownTheme-CKac3wKC.js} +1 -1
  47. package/dist/assets/mcpQueries-D5mQymgM.js +1 -0
  48. package/dist/assets/miniChat-BgrzM3u7.js +2 -0
  49. package/dist/assets/mobile-DtYoQE8C.js +2 -0
  50. package/dist/assets/multirun-jy8dEI96.js +4 -0
  51. package/dist/assets/{persistence-Ce3AnpEr.js → persistence-C9LQpiOS.js} +2 -2
  52. package/dist/assets/pl-CNNsWnPM.js +15 -0
  53. package/dist/assets/platform-BQghHRfp.js +6 -0
  54. package/dist/assets/pt-BR-BZB2DJMp.js +15 -0
  55. package/dist/assets/renderElectronMiniChatApp-BGrrmeME.js +2 -0
  56. package/dist/assets/renderMobileApp-u3c4GtFC.js +28 -0
  57. package/dist/assets/{runtimeConfig-CMkwoQ_X.js → runtimeConfig-BIGBRGQI.js} +1 -1
  58. package/dist/assets/{runtimeEndpointReset-ChOxt-J7.js → runtimeEndpointReset-D2savkuw.js} +12 -12
  59. package/dist/assets/{sessionChangeSummary-NnxcqCTI.js → sessionChangeSummary-eIbpSfrO.js} +1 -1
  60. package/dist/assets/{sessionLookup-BrU3rFHJ.js → sessionLookup-myHe5ltu.js} +1 -1
  61. package/dist/assets/{sortable-tabs-strip-Fnfhf3K9.js → sortable-tabs-strip-BQtmHx9r.js} +1 -1
  62. package/dist/assets/src-CLN7Z-zx.css +2 -0
  63. package/dist/assets/store-DLCiZaA6.js +15 -0
  64. package/dist/assets/{terminalApi-xjqssKke.js → terminalApi-BxcdiduX.js} +1 -1
  65. package/dist/assets/{textarea-bHYxZtfH.js → textarea-CakKvqFw.js} +1 -1
  66. package/dist/assets/{tooltip-0T5jv0cu.js → tooltip-Cx5gTbdY.js} +1 -1
  67. package/dist/assets/uk-rwQrxeYM.js +15 -0
  68. package/dist/assets/{url-_z7evzdK.js → url-CDHnjlsm.js} +1 -1
  69. package/dist/assets/{useAppFontEffects-C02YFyAv.js → useAppFontEffects-CNeo0aZs.js} +59 -45
  70. package/dist/assets/{useAssistantUIStore-CMutVh9B.js → useAssistantUIStore-asNG6K7n.js} +1 -1
  71. package/dist/assets/useDesktopWindowControlsLayout-DBJ56qIo.js +1 -0
  72. package/dist/assets/useEffectiveDirectory-B4LvR8Gt.js +1 -0
  73. package/dist/assets/{useFontPreferences-B1JYyzpK.js → useFontPreferences-CUnQ4TVm.js} +1 -1
  74. package/dist/assets/useMobileNavigationStore-ZHJPxlHt.js +414 -0
  75. package/dist/assets/{useRuntimeAPIs-DSLXJybW.js → useRuntimeAPIs-DwLtO0c_.js} +1 -1
  76. package/dist/assets/useSessionAutoCleanup-BIVmrnYU.js +2 -0
  77. package/dist/assets/{useThemeSystem-DMzWVJaW.js → useThemeSystem-BlNUWH47.js} +1 -1
  78. package/dist/assets/useUIStore-CQYKJnrg.js +1 -0
  79. package/dist/assets/{useWorkerHighlightedLines-C9z0ML1z.js → useWorkerHighlightedLines-CJZbJDW3.js} +2 -2
  80. package/dist/assets/{vendor-base-ui-CnHfUmDN.js → vendor-base-ui-CDjyofeD.js} +1 -1
  81. package/dist/assets/{vendor-cmdk-CeAGfrLo.js → vendor-cmdk-BFH0UpXX.js} +1 -1
  82. package/dist/assets/{vendor-dnd-kit-accessibility-BY5LhhUB.js → vendor-dnd-kit-accessibility-BrR3AKvM.js} +1 -1
  83. package/dist/assets/{vendor-dnd-kit-core-CvWxUPdb.js → vendor-dnd-kit-core-CNfdvk6H.js} +1 -1
  84. package/dist/assets/{vendor-dnd-kit-sortable-hHdP9lLb.js → vendor-dnd-kit-sortable-DQUaTeGj.js} +1 -1
  85. package/dist/assets/{vendor-framer-motion-DFy_BSVd.js → vendor-framer-motion-4dogsUk3.js} +1 -1
  86. package/dist/assets/{vendor-legendapp-list-WE32lwNk.js → vendor-legendapp-list-BU7MdPZR.js} +1 -1
  87. package/dist/assets/{vendor-markstream-react-DX55XLGO.js → vendor-markstream-react-qHC2FykU.js} +3 -3
  88. package/dist/assets/vendor-opencode-client-BOe2qpQb.js +12 -0
  89. package/dist/assets/{vendor-pierre-diffs-DX7XTPG3.js → vendor-pierre-diffs-CUfqC003.js} +1 -1
  90. package/dist/assets/{vendor-react-DLPElpoQ.js → vendor-react-C94A0AHl.js} +1 -1
  91. package/dist/assets/{vendor-react-drawio-ECtEwvbc.js → vendor-react-drawio-FVlWqKVE.js} +1 -1
  92. package/dist/assets/{vendor-reactuses-core-BnW6GxJt.js → vendor-reactuses-core-C2A3EjUi.js} +1 -1
  93. package/dist/assets/{vendor-sonner-DVzioF3d.js → vendor-sonner-DNqHmUQs.js} +1 -1
  94. package/dist/assets/{vendor-stream-diffs-DWy3xSmp.js → vendor-stream-diffs-CwbKsmlv.js} +2 -2
  95. package/dist/assets/{vendor-tanstack-react-query-D7pys_c9.js → vendor-tanstack-react-query-D0iFK57C.js} +1 -1
  96. package/dist/assets/{vendor-tanstack-react-virtual-tlDgH6PG.js → vendor-tanstack-react-virtual-B2kV6cnT.js} +1 -1
  97. package/dist/assets/{vendor-yaml-DIcAK3EC.js → vendor-yaml-C9KlJAlj.js} +37 -37
  98. package/dist/assets/{vendor-zustand-B4dlGEM2.js → vendor-zustand-BqLKf6ZY.js} +1 -1
  99. package/dist/assets/zh-CN-BvmBDnkD.js +15 -0
  100. package/dist/assets/zh-TW-9wcHTX0y.js +15 -0
  101. package/dist/index.html +13 -13
  102. package/dist/mini-chat.html +13 -13
  103. package/dist/mobile.html +13 -13
  104. package/package.json +2 -2
  105. package/server/index.js +392 -20
  106. package/server/lib/assistants/DOCUMENTATION.md +26 -21
  107. package/server/lib/assistants/assign.js +2 -1
  108. package/server/lib/assistants/contact-store.js +37 -19
  109. package/server/lib/assistants/contracts.js +11 -1
  110. package/server/lib/assistants/projection-history.test.js +454 -0
  111. package/server/lib/assistants/routes.js +11 -5
  112. package/server/lib/assistants/service.js +850 -294
  113. package/server/lib/assistants/service.test.js +588 -274
  114. package/server/lib/conversations/DOCUMENTATION.md +5 -5
  115. package/server/lib/conversations/service.js +87 -57
  116. package/server/lib/conversations/service.test.js +132 -100
  117. package/server/lib/conversations/validation.js +11 -1
  118. package/server/lib/event-stream/directory-ws-bridge.js +13 -2
  119. package/server/lib/event-stream/global-hub.js +51 -5
  120. package/server/lib/event-stream/runtime.js +57 -20
  121. package/server/lib/event-stream/runtime.test.js +112 -0
  122. package/server/lib/llm/DOCUMENTATION.md +95 -84
  123. package/server/lib/llm/catalog.js +111 -45
  124. package/server/lib/llm/catalog.test.js +121 -59
  125. package/server/lib/llm/completions.js +41 -12
  126. package/server/lib/llm/completions.test.js +45 -4
  127. package/server/lib/llm/generate.js +516 -453
  128. package/server/lib/llm/generate.test.js +705 -484
  129. package/server/lib/llm/routes.js +8 -4
  130. package/server/lib/message-queue/DOCUMENTATION.md +5 -5
  131. package/server/lib/message-queue/opencode-adapter.js +191 -104
  132. package/server/lib/message-queue/opencode-adapter.test.js +328 -58
  133. package/server/lib/message-queue/runtime.js +15 -2
  134. package/server/lib/message-queue/runtime.test.js +6 -6
  135. package/server/lib/message-queue/worker.js +7 -1
  136. package/server/lib/notifications/runtime.js +5 -0
  137. package/server/lib/opencode/DOCUMENTATION.md +103 -16
  138. package/server/lib/opencode/agent-document.d.ts +28 -0
  139. package/server/lib/opencode/agent-document.js +280 -0
  140. package/server/lib/opencode/agent-document.test.js +137 -0
  141. package/server/lib/opencode/agents.js +96 -0
  142. package/server/lib/opencode/auth-state-runtime.js +5 -0
  143. package/server/lib/opencode/bootstrap-runtime.js +2 -0
  144. package/server/lib/opencode/config-entity-routes.js +111 -102
  145. package/server/lib/opencode/config-entity-routes.test.js +123 -10
  146. package/server/lib/opencode/config-hot-reload.integration.test.js +105 -0
  147. package/server/lib/opencode/ensure-cli.js +238 -0
  148. package/server/lib/opencode/ensure-cli.test.js +119 -0
  149. package/server/lib/opencode/env-runtime.js +208 -111
  150. package/server/lib/opencode/env-runtime.test.js +211 -14
  151. package/server/lib/opencode/feature-routes-runtime.js +80 -5
  152. package/server/lib/opencode/feature-routes-runtime.test.js +18 -0
  153. package/server/lib/opencode/http-server-connections.js +98 -0
  154. package/server/lib/opencode/http-server-connections.test.js +321 -0
  155. package/server/lib/opencode/lifecycle.js +539 -95
  156. package/server/lib/opencode/lifecycle.test.js +698 -6
  157. package/server/lib/opencode/managed-capabilities-runtime.js +15 -2
  158. package/server/lib/opencode/managed-capabilities-runtime.test.js +1 -0
  159. package/server/lib/opencode/network-runtime.js +28 -16
  160. package/server/lib/opencode/network-runtime.test.js +68 -4
  161. package/server/lib/opencode/openchamber-routes.js +7 -1
  162. package/server/lib/opencode/opencode2-pin.js +139 -0
  163. package/server/lib/opencode/opencode2-pin.test.js +88 -0
  164. package/server/lib/opencode/opencode2-sidecar.e2e.test.js +326 -0
  165. package/server/lib/opencode/owned-runtime-upgrade.js +431 -0
  166. package/server/lib/opencode/owned-runtime-upgrade.test.js +160 -0
  167. package/server/lib/opencode/plugin-compatibility.js +15 -0
  168. package/server/lib/opencode/plugin-compatibility.test.js +21 -0
  169. package/server/lib/opencode/plugin-routes.js +3 -18
  170. package/server/lib/opencode/plugin-routes.test.js +9 -9
  171. package/server/lib/opencode/plugins.js +4 -3
  172. package/server/lib/opencode/plugins.test.js +12 -0
  173. package/server/lib/opencode/provider-catalog.js +58 -12
  174. package/server/lib/opencode/provider-catalog.test.js +48 -0
  175. package/server/lib/opencode/proxy.js +368 -28
  176. package/server/lib/opencode/proxy.test.js +338 -0
  177. package/server/lib/opencode/routes.js +303 -76
  178. package/server/lib/opencode/routes.test.js +315 -50
  179. package/server/lib/opencode/runtime-contract.js +401 -0
  180. package/server/lib/opencode/runtime-contract.test.js +214 -0
  181. package/server/lib/opencode/server-opencode-fetch.js +242 -0
  182. package/server/lib/opencode/server-opencode-fetch.test.js +130 -0
  183. package/server/lib/opencode/server-utils-runtime.js +35 -0
  184. package/server/lib/opencode/shared-service.integration.test.js +66 -0
  185. package/server/lib/opencode/shared-service.js +147 -0
  186. package/server/lib/opencode/shared-service.test.js +93 -0
  187. package/server/lib/opencode/shared.js +3 -3
  188. package/server/lib/opencode/shutdown-runtime.js +15 -6
  189. package/server/lib/opencode/shutdown-runtime.test.js +49 -1
  190. package/server/lib/opencode/skill-routes.js +26 -40
  191. package/server/lib/opencode/skill-routes.test.js +71 -7
  192. package/server/lib/opencode/skills.test.js +15 -0
  193. package/server/lib/opencode/startup-pipeline-runtime.js +4 -0
  194. package/server/lib/opencode/static-routes-runtime.js +3 -1
  195. package/server/lib/opencode/static-routes-runtime.test.js +33 -0
  196. package/server/lib/opencode/v1-migration-gate.js +157 -0
  197. package/server/lib/opencode/v1-migration-gate.test.js +111 -0
  198. package/server/lib/opencode/v2-client.js +60 -0
  199. package/server/lib/opencode/v2-plugin-host-shim/index.mjs +155 -0
  200. package/server/lib/opencode/v2-plugin-host-shim/package.json +4 -0
  201. package/server/lib/opencode/watcher.js +2 -1
  202. package/server/lib/opencode/watcher.test.js +1 -1
  203. package/server/lib/permission-auto-accept/DOCUMENTATION.md +5 -5
  204. package/server/lib/permission-auto-accept/runtime.js +14 -7
  205. package/server/lib/permission-auto-accept/runtime.test.js +27 -12
  206. package/server/lib/relay/reasoning-projection.e2e.test.ts +22 -7
  207. package/server/lib/scheduled-tasks/DOCUMENTATION.md +79 -39
  208. package/server/lib/scheduled-tasks/managed-tool-route.js +31 -1
  209. package/server/lib/scheduled-tasks/managed-tool-route.test.js +23 -0
  210. package/server/lib/scheduled-tasks/routes.js +27 -0
  211. package/server/lib/scheduled-tasks/routes.test.js +94 -0
  212. package/server/lib/scheduled-tasks/runtime.js +381 -310
  213. package/server/lib/scheduled-tasks/runtime.test.js +600 -309
  214. package/server/lib/session-goal/DOCUMENTATION.md +173 -22
  215. package/server/lib/session-goal/capability.js +57 -0
  216. package/server/lib/session-goal/capability.test.js +33 -0
  217. package/server/lib/session-goal/routes.js +240 -2
  218. package/server/lib/session-goal/routes.test.js +211 -0
  219. package/server/lib/session-goal/runtime.js +1617 -113
  220. package/server/lib/session-goal/runtime.test.js +1500 -70
  221. package/server/lib/session-index/DOCUMENTATION.md +18 -15
  222. package/server/lib/session-index/event-ingest.js +89 -16
  223. package/server/lib/session-index/event-ingest.test.js +73 -0
  224. package/server/lib/session-index/host-authority-snapshot.js +92 -0
  225. package/server/lib/session-index/routes.host-archive.test.js +201 -0
  226. package/server/lib/session-index/routes.js +63 -10
  227. package/server/lib/session-index/routes.test.js +8 -8
  228. package/server/lib/session-index/service.js +7 -1
  229. package/server/lib/session-index/sync-runtime.js +171 -41
  230. package/server/lib/session-index/sync-runtime.test.js +164 -9
  231. package/server/lib/session-metadata/DOCUMENTATION.md +86 -0
  232. package/server/lib/session-metadata/routes.js +193 -0
  233. package/server/lib/session-metadata/routes.test.js +156 -0
  234. package/server/lib/session-metadata/session-archive.d.ts +73 -0
  235. package/server/lib/session-metadata/session-archive.js +654 -0
  236. package/server/lib/session-metadata/session-archive.test.js +359 -0
  237. package/server/lib/session-metadata/session-metadata-store.d.ts +36 -0
  238. package/server/lib/session-metadata/session-metadata-store.js +375 -0
  239. package/server/lib/session-metadata/session-metadata-store.test.js +379 -0
  240. package/server/lib/session-metadata/session-projection.d.ts +41 -0
  241. package/server/lib/session-metadata/session-projection.js +255 -0
  242. package/server/lib/session-metadata/session-projection.test.js +64 -0
  243. package/server/lib/session-metadata/system-session.js +55 -0
  244. package/server/lib/session-metadata/system-session.test.js +55 -0
  245. package/server/lib/session-title/DOCUMENTATION.md +40 -7
  246. package/server/lib/session-title/native-runtime.test.js +167 -0
  247. package/server/lib/session-title/runtime.js +67 -42
  248. package/server/lib/session-title/session-access.js +46 -0
  249. package/server/lib/session-title/session-access.test.js +91 -0
  250. package/server/lib/session-turn-pages/DOCUMENTATION.md +12 -11
  251. package/server/lib/session-turn-pages/reconcile.service.js +1 -1
  252. package/server/lib/session-turn-pages/reconcile.service.test.js +16 -0
  253. package/server/lib/session-turn-pages/routes.js +123 -75
  254. package/server/lib/session-turn-pages/routes.test.js +566 -0
  255. package/server/lib/session-turn-pages/service.js +6 -2
  256. package/server/lib/session-turn-pages/service.test.js +34 -0
  257. package/server/lib/session-turn-pages/session-message-projection.d.ts +13 -0
  258. package/server/lib/session-turn-pages/session-message-projection.js +281 -0
  259. package/server/lib/small-model/DOCUMENTATION.md +101 -146
  260. package/server/lib/small-model/catalog.js +84 -54
  261. package/server/lib/small-model/catalog.test.js +111 -9
  262. package/server/lib/small-model/custom-api.js +68 -0
  263. package/server/lib/small-model/custom-api.test.js +64 -0
  264. package/server/lib/small-model/index.js +125 -241
  265. package/server/lib/small-model/resolve.js +35 -120
  266. package/server/lib/small-model/resolve.test.js +20 -110
  267. package/server/lib/small-model/routes.js +1 -3
  268. package/server/lib/small-model/summary-settings.test.js +157 -253
  269. package/server/opencode-proxy.test.js +63 -12
  270. package/dist/assets/AssistantView-BUveAgc4.js +0 -3
  271. package/dist/assets/MarkdownRenderer-BbrbDH6-.js +0 -2
  272. package/dist/assets/MobileDetailNavigation-B0aqKXd7.js +0 -1
  273. package/dist/assets/MobileSurface-DW7WfPsy.js +0 -2
  274. package/dist/assets/OverlayScrollbar-DrA0Vsg5.js +0 -2
  275. package/dist/assets/SettingsView-PkXIavr7.js +0 -25
  276. package/dist/assets/appThemeRegistry-D5Yxg4YF.js +0 -1
  277. package/dist/assets/collapsible-D5Phaykh.js +0 -1
  278. package/dist/assets/context-menu-CubSDW2g.js +0 -1
  279. package/dist/assets/es-DA3MqOo0.js +0 -15
  280. package/dist/assets/fr-Dbz0CdIA.js +0 -13
  281. package/dist/assets/gitApi-DXyC4MiV.js +0 -7
  282. package/dist/assets/insertionBoundaries-KlOVrHHt.js +0 -1
  283. package/dist/assets/ja-DumanVQA.js +0 -15
  284. package/dist/assets/ko-C0IkiUjQ.js +0 -15
  285. package/dist/assets/main-BPS5ylib.js +0 -2
  286. package/dist/assets/main-Cpd9VWYs.js +0 -169
  287. package/dist/assets/markdown-worker-CptoICmq.js +0 -2
  288. package/dist/assets/miniChat-B8_KLWiY.js +0 -2
  289. package/dist/assets/mobile-CtktKrXn.js +0 -2
  290. package/dist/assets/multirun-CYfP9Cfm.js +0 -4
  291. package/dist/assets/pl-B9No6MQp.js +0 -15
  292. package/dist/assets/platform-0w_Q9_ji.js +0 -2
  293. package/dist/assets/projectMeta-S1hJ7oBp.js +0 -3
  294. package/dist/assets/pt-BR-DyQ0WTDh.js +0 -15
  295. package/dist/assets/renderElectronMiniChatApp-iK14LWm8.js +0 -2
  296. package/dist/assets/renderMobileApp-DORLPWnb.js +0 -28
  297. package/dist/assets/src-rS3zCcsC.css +0 -2
  298. package/dist/assets/store-5B_LDnLM.js +0 -15
  299. package/dist/assets/uk-C_1KkqyP.js +0 -15
  300. package/dist/assets/useDesktopWindowControlsLayout-Dnr-jX0C.js +0 -1
  301. package/dist/assets/useEffectiveDirectory-CZLLyarc.js +0 -1
  302. package/dist/assets/useMobileNavigationStore-CoCiqNgm.js +0 -413
  303. package/dist/assets/useSessionAutoCleanup-xQyETQkG.js +0 -2
  304. package/dist/assets/useUIStore-Bxv3f23A.js +0 -1
  305. package/dist/assets/vendor-opencode-sdk-C07HQypa.js +0 -7
  306. package/dist/assets/zh-CN-uWILgBdt.js +0 -15
  307. package/dist/assets/zh-TW-MYfdvhbD.js +0 -15
  308. package/server/lib/small-model/call.js +0 -427
  309. package/server/lib/small-model/call.test.js +0 -382
  310. package/server/lib/small-model/opencode-session.js +0 -339
  311. package/server/lib/small-model/opencode-session.test.js +0 -177
@@ -5,10 +5,11 @@ import fs from 'node:fs/promises';
5
5
  import os from 'node:os';
6
6
  import path from 'node:path';
7
7
 
8
- vi.mock('@opencode-ai/sdk/v2', () => ({ createOpencodeClient: vi.fn() }));
8
+ vi.mock('@opencode/client', () => ({ OpenCode: { make: vi.fn() } }));
9
9
 
10
- const { createOpencodeClient } = await import('@opencode-ai/sdk/v2');
10
+ const { OpenCode } = await import('@opencode/client');
11
11
  const { registerConfigEntityRoutes } = await import('./config-entity-routes.js');
12
+ const createOpencodeClient = OpenCode.make;
12
13
 
13
14
  beforeEach(() => {
14
15
  createOpencodeClient.mockReset();
@@ -49,6 +50,24 @@ const createDependencies = (getCommandSources, configDirectory, getAgentSources
49
50
  getOpenCodePort: () => 4096,
50
51
  });
51
52
 
53
+ describe('V2 configuration persistence', () => {
54
+ it.each(['agents', 'commands', 'mcp'])('%s CRUD never restarts or reloads OpenCode', async (entity) => {
55
+ const dependencies = createDependencies(vi.fn());
56
+ dependencies.refreshOpenCodeAfterConfigChange.mockRejectedValue(new Error('must not manage the service'));
57
+ const app = express();
58
+ app.use(express.json());
59
+ registerConfigEntityRoutes(app, dependencies);
60
+ for (const method of ['post', 'patch', 'delete']) {
61
+ const response = await request(app)[method](`/api/config/${entity}/example?directory=%2Frepo`)
62
+ .send({ name: 'example', scope: 'project', type: 'local', command: ['example'] });
63
+ expect(response.status).toBe(200);
64
+ expect(response.body).toMatchObject({ success: true, requiresReload: false, application: 'watch' });
65
+ expect(response.body.requiresManualRestart).not.toBe(true);
66
+ }
67
+ expect(dependencies.refreshOpenCodeAfterConfigChange).not.toHaveBeenCalled();
68
+ });
69
+ });
70
+
52
71
  describe('config entity command metadata route', () => {
53
72
  it('returns metadata for many commands through one request', async () => {
54
73
  const app = express();
@@ -100,9 +119,9 @@ describe('config entity command metadata route', () => {
100
119
 
101
120
  expect(createOpencodeClient).toHaveBeenCalledWith(expect.objectContaining({
102
121
  baseUrl: 'http://opencode-upstream:4096',
103
- directory: '/repo',
104
122
  headers: { Authorization: 'Basic example' },
105
123
  }));
124
+ expect(createOpencodeClient.mock.calls[0][0].directory).toBeUndefined();
106
125
  expect(response.body.commands).toEqual([
107
126
  expect.objectContaining({ name: 'markdown', reference: markdownPath, description: `${'😀'.repeat(160)}…`, scope: 'project', isBuiltIn: false }),
108
127
  expect.objectContaining({ name: 'json', reference: 'json', description: 'JSON command', scope: 'project', isBuiltIn: false }),
@@ -149,14 +168,35 @@ describe('config entity command metadata route', () => {
149
168
  { name: 'long-path', reference: 'long-path' },
150
169
  ]);
151
170
  });
171
+
172
+ it('returns 502 when the command catalog is not an array instead of an empty success', async () => {
173
+ const app = express();
174
+ app.use(express.json());
175
+ createOpencodeClient.mockReturnValue({
176
+ command: { list: vi.fn(async () => ({ data: { commands: [] } })) },
177
+ });
178
+ registerConfigEntityRoutes(app, createDependencies(vi.fn()));
179
+
180
+ const response = await request(app)
181
+ .post('/api/config/commands/metadata?directory=%2Frepo')
182
+ .send({ catalog: true })
183
+ .expect(502);
184
+ expect(response.body).toEqual({ error: 'Command catalog is unavailable' });
185
+ });
152
186
  });
153
187
 
154
188
  describe('provider catalog route', () => {
155
189
  it('uses the resolved directory, OpenCode auth, SDK providers call, and explicit catalog route', async () => {
156
190
  const app = express();
157
191
  const resolveProjectDirectory = vi.fn(async () => ({ directory: '/project/from-header' }));
158
- const providers = vi.fn(async () => ({ data: { providers: [], default: {} } }));
159
- createOpencodeClient.mockReturnValue({ config: { providers } });
192
+ const location = { directory: '/project/from-header' };
193
+ const providers = vi.fn(async () => ({ data: [] }));
194
+ const models = vi.fn(async () => ({ data: [] }));
195
+ const modelDefault = vi.fn(async () => ({ data: null }));
196
+ createOpencodeClient.mockReturnValue({
197
+ provider: { list: providers },
198
+ model: { list: models, default: modelDefault },
199
+ });
160
200
  registerConfigEntityRoutes(app, {
161
201
  ...createDependencies(vi.fn()),
162
202
  resolveProjectDirectory,
@@ -171,22 +211,94 @@ describe('provider catalog route', () => {
171
211
  expect(resolveProjectDirectory).toHaveBeenCalledWith(expect.objectContaining({ query: { directory: '/repo' } }));
172
212
  expect(createOpencodeClient).toHaveBeenCalledWith(expect.objectContaining({
173
213
  baseUrl: 'http://opencode-upstream:4096',
174
- directory: '/project/from-header',
175
214
  headers: { Authorization: 'Basic example' },
176
215
  fetch: expect.any(Function),
177
216
  }));
178
- expect(providers).toHaveBeenCalledWith({ directory: '/project/from-header' });
217
+ expect(createOpencodeClient.mock.calls[0][0].directory).toBeUndefined();
218
+ expect(providers).toHaveBeenCalledWith({ location });
219
+ expect(models).toHaveBeenCalledWith({ location });
220
+ expect(modelDefault).toHaveBeenCalledWith({ location });
179
221
  expect(response.body).toEqual({ schemaVersion: 1, providers: [], default: {}, partial: false });
180
222
  });
181
223
 
224
+ it('projects ModelInfo.id as the catalog model id and default', async () => {
225
+ const app = express();
226
+ createOpencodeClient.mockReturnValue({
227
+ provider: { list: vi.fn(async () => ({ data: [{ id: 'openai', name: 'OpenAI' }] })) },
228
+ model: {
229
+ list: vi.fn(async () => ({
230
+ data: [{
231
+ id: 'gpt-5.4-mini',
232
+ modelID: 'internal-pack',
233
+ providerID: 'openai',
234
+ name: 'GPT-5.4 Mini',
235
+ }],
236
+ })),
237
+ default: vi.fn(async () => ({
238
+ data: { id: 'gpt-5.4-mini', modelID: 'internal-pack', providerID: 'openai', name: 'GPT-5.4 Mini' },
239
+ })),
240
+ },
241
+ });
242
+ registerConfigEntityRoutes(app, createDependencies(vi.fn()));
243
+
244
+ const response = await request(app).get('/api/config/catalog/providers').expect(200);
245
+ expect(response.body.providers).toEqual([{
246
+ id: 'openai',
247
+ name: 'OpenAI',
248
+ models: { 'gpt-5.4-mini': { id: 'gpt-5.4-mini', name: 'GPT-5.4 Mini' } },
249
+ }]);
250
+ expect(response.body.default).toEqual({ openai: 'gpt-5.4-mini' });
251
+ expect(JSON.stringify(response.body)).not.toContain('internal-pack');
252
+ });
253
+
254
+ it('projects v2 thinking variants into the provider catalog and strips request payloads', async () => {
255
+ const app = express();
256
+ createOpencodeClient.mockReturnValue({
257
+ provider: { list: vi.fn(async () => ({ data: [{ id: 'openai', name: 'OpenAI' }] })) },
258
+ model: {
259
+ list: vi.fn(async () => ({
260
+ data: [{
261
+ id: 'gpt-5.4',
262
+ modelID: 'internal-pack',
263
+ providerID: 'openai',
264
+ name: 'GPT 5.4',
265
+ capabilities: { tools: true, input: ['text', 'image'], output: ['text'] },
266
+ variants: [
267
+ { id: 'low', settings: { reasoningEffort: 'low', apiKey: 'variant-settings-sentinel' } },
268
+ { id: 'high', headers: { Authorization: 'variant-headers-sentinel' }, body: { store: false } },
269
+ ],
270
+ }],
271
+ })),
272
+ default: vi.fn(async () => ({ data: null })),
273
+ },
274
+ });
275
+ registerConfigEntityRoutes(app, createDependencies(vi.fn()));
276
+
277
+ const response = await request(app).get('/api/config/catalog/providers').expect(200);
278
+ expect(response.body.providers[0].models['gpt-5.4'].variants).toEqual({ low: {}, high: {} });
279
+ expect(response.body.providers[0].models['gpt-5.4'].capabilities).toEqual({
280
+ toolcall: true,
281
+ input: { text: true, image: true },
282
+ output: { text: true },
283
+ });
284
+ expect(JSON.stringify(response.body)).not.toContain('sentinel');
285
+ expect(JSON.stringify(response.body)).not.toContain('internal-pack');
286
+ });
287
+
182
288
  it('returns 502 for SDK failure and malformed catalog responses', async () => {
183
289
  const app = express();
184
- createOpencodeClient.mockReturnValue({ config: { providers: vi.fn(async () => ({ data: { providers: {} } })) } });
290
+ createOpencodeClient.mockReturnValue({
291
+ provider: { list: vi.fn(async () => ({ data: {} })) },
292
+ model: { list: vi.fn(async () => ({ data: [] })), default: vi.fn(async () => ({ data: null })) },
293
+ });
185
294
  registerConfigEntityRoutes(app, createDependencies(vi.fn()));
186
295
  await request(app).get('/api/config/catalog/providers').expect(502);
187
296
 
188
297
  const failingApp = express();
189
- createOpencodeClient.mockReturnValue({ config: { providers: vi.fn(async () => { throw new Error('upstream unavailable'); }) } });
298
+ createOpencodeClient.mockReturnValue({
299
+ provider: { list: vi.fn(async () => { throw new Error('upstream unavailable'); }) },
300
+ model: { list: vi.fn(async () => ({ data: [] })), default: vi.fn(async () => ({ data: null })) },
301
+ });
190
302
  registerConfigEntityRoutes(failingApp, createDependencies(vi.fn()));
191
303
  await request(failingApp).get('/api/config/catalog/providers').expect(502);
192
304
  });
@@ -194,7 +306,8 @@ describe('provider catalog route', () => {
194
306
  it('returns 502 for SDK error envelopes that include data', async () => {
195
307
  const app = express();
196
308
  createOpencodeClient.mockReturnValue({
197
- config: { providers: vi.fn(async () => ({ error: { message: 'upstream sentinel' }, data: { providers: [], default: {} } })) },
309
+ provider: { list: vi.fn(async () => { throw new Error('upstream sentinel'); }) },
310
+ model: { list: vi.fn(async () => ({ data: [] })), default: vi.fn(async () => ({ data: null })) },
198
311
  });
199
312
  registerConfigEntityRoutes(app, createDependencies(vi.fn()));
200
313
 
@@ -0,0 +1,105 @@
1
+ import { test, expect } from 'vitest';
2
+ import { OpenCode } from '@opencode/client';
3
+ import { spawn } from 'node:child_process';
4
+ import { once } from 'node:events';
5
+ import { randomBytes } from 'node:crypto';
6
+ import fs from 'node:fs/promises';
7
+ import os from 'node:os';
8
+ import path from 'node:path';
9
+
10
+ // Opt-in, isolated real V2 server. No user config, credentials, model calls,
11
+ // shared service registration, or npm plugin installs are used.
12
+ test.skipIf(!process.env.OPENCHAMBER_TEST_OPENCODE_BINARY)('V2 applies credentials, global config and plugin changes in the same process', async () => {
13
+ const temporary = path.join(os.tmpdir(), 'opencode');
14
+ await fs.mkdir(temporary, { recursive: true });
15
+ const root = await fs.mkdtemp(path.join(temporary, 'config-hot-reload-'));
16
+ const config = path.join(root, 'config', 'opencode');
17
+ const directory = path.join(root, 'project');
18
+ await fs.mkdir(config, { recursive: true });
19
+ await fs.mkdir(directory, { recursive: true });
20
+ const password = randomBytes(24).toString('hex');
21
+ const child = spawn(process.env.OPENCHAMBER_TEST_OPENCODE_BINARY, ['serve', '--hostname', '127.0.0.1', '--port', '0'], {
22
+ cwd: directory,
23
+ env: {
24
+ PATH: process.env.PATH,
25
+ HOME: root,
26
+ TMPDIR: temporary,
27
+ XDG_CONFIG_HOME: path.join(root, 'config'),
28
+ XDG_DATA_HOME: path.join(root, 'data'),
29
+ XDG_STATE_HOME: path.join(root, 'state'),
30
+ XDG_CACHE_HOME: path.join(root, 'cache'),
31
+ OPENCODE_CONFIG_DIR: config,
32
+ OPENCODE_SERVER_PASSWORD: password,
33
+ OPENCODE_DISABLE_MODELS_FETCH: '1',
34
+ },
35
+ stdio: ['ignore', 'pipe', 'pipe'],
36
+ });
37
+ let output = '';
38
+ let spawnError;
39
+ child.on('error', (error) => { spawnError = error; });
40
+ child.stdout.on('data', (data) => { output = (output + data).slice(-8192); });
41
+ child.stderr.on('data', () => undefined);
42
+ const closed = once(child, 'close').catch(() => undefined);
43
+ const until = async (read, accepts, label) => {
44
+ const deadline = Date.now() + 20_000;
45
+ while (Date.now() < deadline) {
46
+ if (spawnError) throw spawnError;
47
+ if (child.exitCode !== null) throw new Error(`Isolated OpenCode exited during ${label}`);
48
+ const value = await read();
49
+ if (accepts(value)) return value;
50
+ await new Promise((resolve) => setTimeout(resolve, 100));
51
+ }
52
+ throw new Error(`Timed out waiting for ${label}`);
53
+ };
54
+ const write = async (info) => {
55
+ const file = path.join(config, 'opencode.json');
56
+ await fs.writeFile(`${file}.tmp`, JSON.stringify(info));
57
+ await fs.rename(`${file}.tmp`, file);
58
+ };
59
+ try {
60
+ const origin = await until(
61
+ async () => output.match(/(?:server listening on|opencode server listening on) (http:\/\/[^\s]+)/)?.[1],
62
+ Boolean, 'listener',
63
+ );
64
+ const client = OpenCode.make({
65
+ baseUrl: origin,
66
+ headers: { Authorization: `Basic ${Buffer.from(`opencode:${password}`).toString('base64')}` },
67
+ fetch: (input, init) => fetch(input, { ...init, signal: AbortSignal.timeout(15_000) }),
68
+ });
69
+ const location = { directory };
70
+ await client.command.list({ location });
71
+ const pid = child.pid;
72
+ const commands = { 'hot-reload-probe': { template: 'Do not execute anything.', description: 'one' } };
73
+ await write({ commands });
74
+ await until(() => client.command.list({ location }), (result) => result.data.some((entry) => entry.name === 'hot-reload-probe'), 'global command create');
75
+ await write({ commands: { 'hot-reload-probe': { ...commands['hot-reload-probe'], description: 'two' } } });
76
+ await until(() => client.command.list({ location }), (result) => result.data.some((entry) => entry.name === 'hot-reload-probe' && entry.description === 'two'), 'command update');
77
+
78
+ await write({ commands, plugins: ['*', '-opencode.config.command'] });
79
+ await until(() => client.command.list({ location }), (result) => !result.data.some((entry) => entry.name === 'hot-reload-probe'), 'plugin disable');
80
+ await write({ commands });
81
+ await until(() => client.command.list({ location }), (result) => result.data.some((entry) => entry.name === 'hot-reload-probe'), 'plugin re-enable');
82
+
83
+ await client.integration.connect.key({ integrationID: 'anthropic', key: 'isolated-test-placeholder', location });
84
+ const integration = await until(() => client.integration.get({ integrationID: 'anthropic', location }),
85
+ (result) => result.data.connections.some((connection) => connection.type === 'credential'), 'credential connection');
86
+ for (const connection of integration.data.connections) {
87
+ if (connection.type === 'credential') await client.credential.remove({ credentialID: connection.id });
88
+ }
89
+ await until(() => client.integration.get({ integrationID: 'anthropic', location }),
90
+ (result) => result.data.connections.length === 0, 'credential removal');
91
+ await write({ commands, mcp: { servers: { 'hot-reload-mcp': { type: 'local', command: ['unused-test-command'], disabled: true } } } });
92
+ await until(() => client.mcp.list({ location }), (result) => result.data.some((server) => server.name === 'hot-reload-mcp'), 'MCP config add');
93
+ await write({});
94
+ await until(() => client.mcp.list({ location }), (result) => !result.data.some((server) => server.name === 'hot-reload-mcp'), 'MCP config removal');
95
+ await until(() => client.command.list({ location }), (result) => !result.data.some((entry) => entry.name === 'hot-reload-probe'), 'command removal');
96
+ expect(child.pid).toBe(pid);
97
+ expect(child.exitCode).toBeNull();
98
+ } finally {
99
+ child.kill('SIGTERM');
100
+ const kill = setTimeout(() => child.kill('SIGKILL'), 5000);
101
+ await closed;
102
+ clearTimeout(kill);
103
+ await fs.rm(root, { recursive: true, force: true });
104
+ }
105
+ }, 120_000);
@@ -0,0 +1,238 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import {
6
+ PINNED_OPENCODE2_VERSION,
7
+ isAcceptableOpenCode2HealthVersion,
8
+ isOpenCode2VersionAtLeast,
9
+ npmPackageForOpenCode2,
10
+ openCodeBinaryName,
11
+ parseOpenCode2VersionOutput,
12
+ } from './opencode2-pin.js';
13
+
14
+ const INSTALL_FETCH_TIMEOUT_MS = 30_000;
15
+
16
+ export function isOpenCode2AutoInstallEnabled(env = process.env) {
17
+ const value = typeof env.OPENCHAMBER_OPENCODE2_AUTO_INSTALL === 'string'
18
+ ? env.OPENCHAMBER_OPENCODE2_AUTO_INSTALL.trim().toLowerCase()
19
+ : '';
20
+ return value !== '0' && value !== 'false' && value !== 'no';
21
+ }
22
+
23
+ export function resolveOpenChamberDataDir(env = process.env, homedir = os.homedir) {
24
+ if (typeof env.OPENCHAMBER_DATA_DIR === 'string' && env.OPENCHAMBER_DATA_DIR.trim()) {
25
+ return path.resolve(env.OPENCHAMBER_DATA_DIR.trim());
26
+ }
27
+ const home = typeof homedir === 'function' ? homedir() : homedir;
28
+ return path.join(home, '.config', 'openchamber');
29
+ }
30
+
31
+ export function installedOpenCode2BinaryPath(version = PINNED_OPENCODE2_VERSION, options = {}) {
32
+ const dataDir = options.dataDir || resolveOpenChamberDataDir(options.env, options.homedir);
33
+ return path.join(dataDir, 'opencode-cli', version, openCodeBinaryName(options.platform));
34
+ }
35
+
36
+ export function readOpenCode2BinaryVersion(binaryPath, options = {}) {
37
+ if (typeof binaryPath !== 'string' || !binaryPath.trim()) return '';
38
+ const run = typeof options.spawnSync === 'function' ? options.spawnSync : spawnSync;
39
+ try {
40
+ const result = run(binaryPath, ['--version'], {
41
+ encoding: 'utf8',
42
+ stdio: ['ignore', 'pipe', 'pipe'],
43
+ timeout: 8_000,
44
+ windowsHide: true,
45
+ });
46
+ if (result.status !== 0) return '';
47
+ return parseOpenCode2VersionOutput(`${result.stdout || ''}\n${result.stderr || ''}`);
48
+ } catch {
49
+ return '';
50
+ }
51
+ }
52
+
53
+ const resolveNpmRegistry = (options = {}) => {
54
+ const env = options.env || process.env;
55
+ const electronRoot = options.electronRoot;
56
+ const candidates = [
57
+ env.NPM_CONFIG_REGISTRY,
58
+ options.cwd ? path.join(options.cwd, '.npmrc') : null,
59
+ electronRoot ? path.join(electronRoot, '.npmrc') : null,
60
+ path.join(os.homedir(), '.npmrc'),
61
+ ].filter(Boolean);
62
+ for (const candidate of candidates) {
63
+ try {
64
+ if (!String(candidate).endsWith('.npmrc')) {
65
+ if (/^https?:\/\//.test(candidate)) return String(candidate).replace(/\/+$/, '');
66
+ continue;
67
+ }
68
+ if (!fs.existsSync(candidate)) continue;
69
+ const match = fs.readFileSync(candidate, 'utf8').match(/^\s*registry\s*=\s*(\S+)\s*$/m);
70
+ if (match) return match[1].replace(/\/+$/, '');
71
+ } catch {
72
+ // Ignore unreadable npmrc candidates and fall back to the default registry.
73
+ }
74
+ }
75
+ return 'https://registry.npmjs.org';
76
+ };
77
+
78
+ const downloadBuffer = async (url, fetchImpl, timeoutMs) => {
79
+ const response = await fetchImpl(url, { signal: AbortSignal.timeout(timeoutMs) });
80
+ if (!response.ok) {
81
+ throw new Error(`Failed to download ${url}: ${response.status} ${response.statusText}`);
82
+ }
83
+ return Buffer.from(await response.arrayBuffer());
84
+ };
85
+
86
+ const extractArchive = (archivePath, destination, run) => {
87
+ fs.rmSync(destination, { recursive: true, force: true });
88
+ fs.mkdirSync(destination, { recursive: true });
89
+ if (archivePath.endsWith('.zip')) {
90
+ if (process.platform === 'win32') {
91
+ run('powershell.exe', [
92
+ '-NoLogo',
93
+ '-NoProfile',
94
+ '-NonInteractive',
95
+ '-ExecutionPolicy',
96
+ 'Bypass',
97
+ '-Command',
98
+ `Expand-Archive -LiteralPath ${JSON.stringify(archivePath)} -DestinationPath ${JSON.stringify(destination)} -Force`,
99
+ ]);
100
+ return;
101
+ }
102
+ run('unzip', ['-q', archivePath, '-d', destination]);
103
+ return;
104
+ }
105
+ if (archivePath.endsWith('.tar.gz') || archivePath.endsWith('.tgz')) {
106
+ run('tar', ['-xzf', archivePath, '-C', destination]);
107
+ return;
108
+ }
109
+ throw new Error(`Unsupported opencode2 CLI archive: ${archivePath}`);
110
+ };
111
+
112
+ const findBinary = (root, binaryName) => {
113
+ const entries = fs.readdirSync(root, { withFileTypes: true });
114
+ for (const entry of entries) {
115
+ const fullPath = path.join(root, entry.name);
116
+ if (entry.isFile() && entry.name.toLowerCase() === binaryName.toLowerCase()) {
117
+ return fullPath;
118
+ }
119
+ if (entry.isDirectory()) {
120
+ const found = findBinary(fullPath, binaryName);
121
+ if (found) return found;
122
+ }
123
+ }
124
+ return null;
125
+ };
126
+
127
+ export async function installPinnedOpenCode2Cli(options = {}) {
128
+ const version = options.version || PINNED_OPENCODE2_VERSION;
129
+ const platform = options.platform || process.platform;
130
+ const arch = options.arch || process.arch;
131
+ const dataDir = options.dataDir || resolveOpenChamberDataDir(options.env, options.homedir);
132
+ const binaryName = openCodeBinaryName(platform);
133
+ const outputBinary = installedOpenCode2BinaryPath(version, { dataDir, platform });
134
+ const run = typeof options.spawnSync === 'function' ? options.spawnSync : spawnSync;
135
+ const fetchImpl = typeof options.fetch === 'function' ? options.fetch : fetch;
136
+ const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : INSTALL_FETCH_TIMEOUT_MS;
137
+
138
+ const cacheDir = path.join(dataDir, 'opencode-cli', '.cache', version, `${platform}-${arch}`);
139
+ const archivePath = path.join(cacheDir, 'opencode2-cli.tgz');
140
+ const packageName = npmPackageForOpenCode2(platform, arch);
141
+
142
+ if (!fs.existsSync(archivePath)) {
143
+ const registry = resolveNpmRegistry(options);
144
+ const metadataResponse = await fetchImpl(`${registry}/${encodeURIComponent(packageName)}/${version}`, {
145
+ signal: AbortSignal.timeout(timeoutMs),
146
+ });
147
+ if (!metadataResponse.ok) {
148
+ throw new Error(
149
+ `Failed to resolve ${packageName}@${version} on ${registry}: ${metadataResponse.status} ${metadataResponse.statusText}`,
150
+ );
151
+ }
152
+ const metadata = await metadataResponse.json();
153
+ const tarball = metadata?.dist?.tarball;
154
+ if (typeof tarball !== 'string' || !/^https?:\/\//.test(tarball)) {
155
+ throw new Error(`No tarball found for ${packageName}@${version} on ${registry}`);
156
+ }
157
+ const bytes = await downloadBuffer(tarball, fetchImpl, timeoutMs);
158
+ fs.mkdirSync(path.dirname(archivePath), { recursive: true });
159
+ const temp = `${archivePath}.tmp`;
160
+ fs.writeFileSync(temp, bytes);
161
+ fs.renameSync(temp, archivePath);
162
+ }
163
+
164
+ const extractDir = options.extractDir || path.join(cacheDir, 'extract');
165
+ if (typeof options.extract === 'function') {
166
+ await options.extract(archivePath, extractDir);
167
+ } else {
168
+ extractArchive(archivePath, extractDir, (command, args) => {
169
+ const result = run(command, args, {
170
+ encoding: 'utf8',
171
+ stdio: 'pipe',
172
+ windowsHide: true,
173
+ });
174
+ if (result.status !== 0) {
175
+ const stderr = result.stderr ? `\n${result.stderr.trim()}` : '';
176
+ const stdout = result.stdout ? `\n${result.stdout.trim()}` : '';
177
+ throw new Error(`Command failed: ${command} ${args.join(' ')}${stderr}${stdout}`);
178
+ }
179
+ return result;
180
+ });
181
+ }
182
+
183
+ const extractedBinary = findBinary(extractDir, binaryName);
184
+ if (!extractedBinary) {
185
+ throw new Error(`Archive ${archivePath} did not contain ${binaryName} or opencode`);
186
+ }
187
+
188
+ fs.mkdirSync(path.dirname(outputBinary), { recursive: true });
189
+ fs.copyFileSync(extractedBinary, outputBinary);
190
+ if (platform !== 'win32') {
191
+ fs.chmodSync(outputBinary, 0o755);
192
+ }
193
+
194
+ const preparedVersion = readOpenCode2BinaryVersion(outputBinary, options);
195
+ if (preparedVersion !== version) {
196
+ throw new Error(`Installed opencode2 version mismatch: expected ${version}, got ${preparedVersion || 'unknown'}`);
197
+ }
198
+ return outputBinary;
199
+ }
200
+
201
+ /**
202
+ * Reuse any discovered acceptable 2.x CLI. Install the pin only when nothing
203
+ * usable is already installed (global PATH, explicit override, or cache).
204
+ */
205
+ export async function ensurePinnedOpenCode2Cli(input = {}) {
206
+ const pin = input.pin || PINNED_OPENCODE2_VERSION;
207
+ const discoveredPath = typeof input.discoveredPath === 'string' ? input.discoveredPath.trim() : '';
208
+ const autoInstall = input.autoInstall ?? isOpenCode2AutoInstallEnabled(input.env);
209
+ const readVersion = typeof input.readVersion === 'function'
210
+ ? input.readVersion
211
+ : (binaryPath) => readOpenCode2BinaryVersion(binaryPath, input);
212
+ const install = typeof input.install === 'function' ? input.install : installPinnedOpenCode2Cli;
213
+
214
+ if (discoveredPath) {
215
+ const version = readVersion(discoveredPath);
216
+ if (isAcceptableOpenCode2HealthVersion(version)) {
217
+ return { path: discoveredPath, version, source: 'discovered', installed: false };
218
+ }
219
+ }
220
+
221
+ const cached = installedOpenCode2BinaryPath(pin, input);
222
+ const cachedVersion = readVersion(cached);
223
+ if (cachedVersion === pin || isOpenCode2VersionAtLeast(cachedVersion, pin)) {
224
+ return { path: cached, version: cachedVersion, source: 'installed', installed: false };
225
+ }
226
+
227
+ if (!autoInstall) {
228
+ const error = new Error(
229
+ `Unable to locate opencode ${pin}. Set OPENCODE_BINARY or install @opencode/cli@${pin}.`,
230
+ );
231
+ error.code = 'OPENCODE_CLI_MISSING';
232
+ throw error;
233
+ }
234
+
235
+ const installedPath = await install({ ...input, version: pin });
236
+ const version = readVersion(installedPath) || pin;
237
+ return { path: installedPath, version, source: 'installed', installed: true };
238
+ }
@@ -0,0 +1,119 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { afterEach, describe, expect, it } from 'vitest';
5
+ import { PINNED_OPENCODE2_VERSION } from './opencode2-pin.js';
6
+ import {
7
+ ensurePinnedOpenCode2Cli,
8
+ installedOpenCode2BinaryPath,
9
+ isOpenCode2AutoInstallEnabled,
10
+ resolveOpenChamberDataDir,
11
+ } from './ensure-cli.js';
12
+
13
+ const tempDirs = [];
14
+
15
+ const createTempDir = (prefix) => {
16
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
17
+ tempDirs.push(dir);
18
+ return dir;
19
+ };
20
+
21
+ afterEach(() => {
22
+ for (const dir of tempDirs.splice(0)) {
23
+ fs.rmSync(dir, { recursive: true, force: true });
24
+ }
25
+ });
26
+
27
+ describe('ensurePinnedOpenCode2Cli', () => {
28
+ it('keeps a discovered binary that already meets the pin', async () => {
29
+ const result = await ensurePinnedOpenCode2Cli({
30
+ discoveredPath: '/opt/opencode2',
31
+ pin: '2.0.12',
32
+ readVersion: () => '2.0.12',
33
+ install: async () => {
34
+ throw new Error('should not install');
35
+ },
36
+ });
37
+ expect(result).toEqual({
38
+ path: '/opt/opencode2',
39
+ version: '2.0.12',
40
+ source: 'discovered',
41
+ installed: false,
42
+ });
43
+ });
44
+
45
+ it('reuses an older global 2.x binary instead of reinstalling the pin', async () => {
46
+ const result = await ensurePinnedOpenCode2Cli({
47
+ discoveredPath: '/opt/old-opencode',
48
+ pin: '2.0.12',
49
+ readVersion: () => '2.0.5',
50
+ install: async () => {
51
+ throw new Error('should not install');
52
+ },
53
+ });
54
+ expect(result.source).toBe('discovered');
55
+ expect(result.path).toBe('/opt/old-opencode');
56
+ });
57
+
58
+ it('installs the pin only when no usable 2.x binary is present', async () => {
59
+ const dataDir = createTempDir('openchamber-ensure-cli-');
60
+ const installed = path.join(dataDir, 'opencode-cli', PINNED_OPENCODE2_VERSION, 'opencode');
61
+ let installedOnce = false;
62
+ const result = await ensurePinnedOpenCode2Cli({
63
+ discoveredPath: '',
64
+ pin: PINNED_OPENCODE2_VERSION,
65
+ dataDir,
66
+ platform: 'linux',
67
+ readVersion: (binaryPath) => {
68
+ if (binaryPath === installed && installedOnce) return PINNED_OPENCODE2_VERSION;
69
+ return '';
70
+ },
71
+ install: async () => {
72
+ installedOnce = true;
73
+ return installed;
74
+ },
75
+ });
76
+ expect(result).toEqual({
77
+ path: installed,
78
+ version: PINNED_OPENCODE2_VERSION,
79
+ source: 'installed',
80
+ installed: true,
81
+ });
82
+ });
83
+
84
+ it('reuses a cached pin without downloading', async () => {
85
+ const dataDir = createTempDir('openchamber-ensure-cached-');
86
+ const cached = installedOpenCode2BinaryPath(PINNED_OPENCODE2_VERSION, { dataDir, platform: 'linux' });
87
+ const result = await ensurePinnedOpenCode2Cli({
88
+ pin: PINNED_OPENCODE2_VERSION,
89
+ dataDir,
90
+ platform: 'linux',
91
+ readVersion: (binaryPath) => (binaryPath === cached ? PINNED_OPENCODE2_VERSION : ''),
92
+ install: async () => {
93
+ throw new Error('should not install');
94
+ },
95
+ });
96
+ expect(result.source).toBe('installed');
97
+ expect(result.installed).toBe(false);
98
+ expect(result.path).toBe(cached);
99
+ });
100
+
101
+ it('fails closed when auto-install is disabled and nothing usable is present', async () => {
102
+ await expect(ensurePinnedOpenCode2Cli({
103
+ pin: PINNED_OPENCODE2_VERSION,
104
+ autoInstall: false,
105
+ dataDir: createTempDir('openchamber-ensure-disabled-'),
106
+ readVersion: () => '',
107
+ })).rejects.toMatchObject({ code: 'OPENCODE_CLI_MISSING' });
108
+ });
109
+
110
+ it('treats OPENCHAMBER_OPENCODE2_AUTO_INSTALL=0 as disabled', () => {
111
+ expect(isOpenCode2AutoInstallEnabled({ OPENCHAMBER_OPENCODE2_AUTO_INSTALL: '0' })).toBe(false);
112
+ expect(isOpenCode2AutoInstallEnabled({})).toBe(true);
113
+ });
114
+
115
+ it('resolves the OpenChamber data dir from OPENCHAMBER_DATA_DIR', () => {
116
+ const dataDir = createTempDir('openchamber-data-dir-');
117
+ expect(resolveOpenChamberDataDir({ OPENCHAMBER_DATA_DIR: dataDir })).toBe(path.resolve(dataDir));
118
+ });
119
+ });