@thevinci/web 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (337) hide show
  1. package/README.md +197 -0
  2. package/bin/cli-entry.js +55 -0
  3. package/bin/cli-output.js +145 -0
  4. package/bin/cli.js +4887 -0
  5. package/bin/cli.test.js +64 -0
  6. package/dist/apple-touch-icon-120x120.png +0 -0
  7. package/dist/apple-touch-icon-152x152.png +0 -0
  8. package/dist/apple-touch-icon-167x167.png +0 -0
  9. package/dist/apple-touch-icon-180x180.png +0 -0
  10. package/dist/apple-touch-icon.png +0 -0
  11. package/dist/apple-touch-icon.svg +528 -0
  12. package/dist/assets/JsonTreeView-CSm9OzXG.js +1 -0
  13. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  14. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  15. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  16. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  17. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  18. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  19. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  20. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  21. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  22. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  23. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  24. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  25. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  26. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  27. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  28. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  29. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  30. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  31. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  32. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  33. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  34. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  35. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  36. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  37. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  38. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  39. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  40. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  41. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  42. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  43. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  44. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  45. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  46. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  47. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  48. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  49. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  50. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  51. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  52. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  53. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  54. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  55. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  56. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  57. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  58. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  59. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  60. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  61. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  62. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  63. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  64. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  65. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  66. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  67. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  68. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  69. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  70. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  71. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  72. package/dist/assets/MarkdownRendererImpl-DensKOLc.js +6 -0
  73. package/dist/assets/MultiRunWindow-Bo7THayo.js +1 -0
  74. package/dist/assets/OnboardingScreen-BDqmzTVR.js +2 -0
  75. package/dist/assets/SettingsWindow-coz__Ykw.js +1 -0
  76. package/dist/assets/TerminalView-DrZ-i3Dr.js +1 -0
  77. package/dist/assets/ToolOutputDialog-Eglzslt3.js +16 -0
  78. package/dist/assets/es-4o9ciP61.js +15 -0
  79. package/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  80. package/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  81. package/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  82. package/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  83. package/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
  84. package/dist/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
  85. package/dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
  86. package/dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
  87. package/dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
  88. package/dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
  89. package/dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
  90. package/dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
  91. package/dist/assets/index-DLTDToSP.css +1 -0
  92. package/dist/assets/index-DgiFEKGN.js +1 -0
  93. package/dist/assets/ko-B20imCHE.js +15 -0
  94. package/dist/assets/main-BV3KOtdA.css +1 -0
  95. package/dist/assets/main-CDKJj0sH.js +226 -0
  96. package/dist/assets/main-LC-PSNVM.js +2 -0
  97. package/dist/assets/miniChat-CQUiG_cr.js +2 -0
  98. package/dist/assets/modelPrefsAutoSave-Dm799vzR.js +6986 -0
  99. package/dist/assets/pl-DQJ7LSzj.js +15 -0
  100. package/dist/assets/pt-BR-OmjHUz9y.js +15 -0
  101. package/dist/assets/renderElectronMiniChatApp-CARbeW0G.js +2 -0
  102. package/dist/assets/uk-BNFxOlO4.js +15 -0
  103. package/dist/assets/vendor--DBfsbEis.css +1 -0
  104. package/dist/assets/vendor-.bun-B9l0ZNi2.js +4094 -0
  105. package/dist/assets/wasm-CG6Dc4jp.js +1 -0
  106. package/dist/assets/wasmSttWorker-Dtlxac_K.js +1 -0
  107. package/dist/assets/wasmSttWorker-oo7Dm_jy.js +1806 -0
  108. package/dist/assets/worker-CbT6TVo7.js +155 -0
  109. package/dist/assets/zh-CN-C6T-Ac7F.js +15 -0
  110. package/dist/favicon-16.png +0 -0
  111. package/dist/favicon-32.png +0 -0
  112. package/dist/favicon.png +0 -0
  113. package/dist/favicon.svg +528 -0
  114. package/dist/index.html +607 -0
  115. package/dist/logo-dark-192x192.png +0 -0
  116. package/dist/logo-dark-512x512.png +0 -0
  117. package/dist/logo-dark-512x512.svg +528 -0
  118. package/dist/logo-light-192x192.png +0 -0
  119. package/dist/logo-light-512x512.png +0 -0
  120. package/dist/logo-light-512x512.svg +528 -0
  121. package/dist/mini-chat.html +16 -0
  122. package/dist/pwa-192.png +0 -0
  123. package/dist/pwa-512.png +0 -0
  124. package/dist/pwa-maskable-192.png +0 -0
  125. package/dist/pwa-maskable-512.png +0 -0
  126. package/dist/site.webmanifest +21 -0
  127. package/dist/sw.js +1 -0
  128. package/package.json +118 -0
  129. package/public/apple-touch-icon-120x120.png +0 -0
  130. package/public/apple-touch-icon-152x152.png +0 -0
  131. package/public/apple-touch-icon-167x167.png +0 -0
  132. package/public/apple-touch-icon-180x180.png +0 -0
  133. package/public/apple-touch-icon.png +0 -0
  134. package/public/apple-touch-icon.svg +528 -0
  135. package/public/favicon-16.png +0 -0
  136. package/public/favicon-32.png +0 -0
  137. package/public/favicon.png +0 -0
  138. package/public/favicon.svg +528 -0
  139. package/public/logo-dark-192x192.png +0 -0
  140. package/public/logo-dark-512x512.png +0 -0
  141. package/public/logo-dark-512x512.svg +528 -0
  142. package/public/logo-light-192x192.png +0 -0
  143. package/public/logo-light-512x512.png +0 -0
  144. package/public/logo-light-512x512.svg +528 -0
  145. package/public/pwa-192.png +0 -0
  146. package/public/pwa-512.png +0 -0
  147. package/public/pwa-maskable-192.png +0 -0
  148. package/public/pwa-maskable-512.png +0 -0
  149. package/public/site.webmanifest +21 -0
  150. package/server/TERMINAL_WS_PROTOCOL.md +48 -0
  151. package/server/index.d.ts +39 -0
  152. package/server/index.js +1311 -0
  153. package/server/lib/cloudflare-tunnel.js +650 -0
  154. package/server/lib/event-stream/DOCUMENTATION.md +61 -0
  155. package/server/lib/event-stream/directory-ws-bridge.js +185 -0
  156. package/server/lib/event-stream/global-hub.js +158 -0
  157. package/server/lib/event-stream/global-hub.test.js +140 -0
  158. package/server/lib/event-stream/global-ws-bridge.js +206 -0
  159. package/server/lib/event-stream/index.js +25 -0
  160. package/server/lib/event-stream/protocol.js +131 -0
  161. package/server/lib/event-stream/protocol.test.js +182 -0
  162. package/server/lib/event-stream/runtime.js +180 -0
  163. package/server/lib/event-stream/runtime.test.js +512 -0
  164. package/server/lib/event-stream/upstream-reader.js +226 -0
  165. package/server/lib/event-stream/upstream-reader.test.js +276 -0
  166. package/server/lib/fs/DOCUMENTATION.md +36 -0
  167. package/server/lib/fs/routes.js +1040 -0
  168. package/server/lib/fs/search.js +238 -0
  169. package/server/lib/git/DOCUMENTATION.md +152 -0
  170. package/server/lib/git/credentials.js +74 -0
  171. package/server/lib/git/identity-storage.js +112 -0
  172. package/server/lib/git/index.js +6 -0
  173. package/server/lib/git/routes.js +972 -0
  174. package/server/lib/git/service.js +3432 -0
  175. package/server/lib/git/service.test.js +39 -0
  176. package/server/lib/github/DOCUMENTATION.md +171 -0
  177. package/server/lib/github/auth.js +307 -0
  178. package/server/lib/github/device-flow.js +50 -0
  179. package/server/lib/github/index.js +24 -0
  180. package/server/lib/github/octokit.js +10 -0
  181. package/server/lib/github/pr-status.js +519 -0
  182. package/server/lib/github/repo/fork-detection.js +102 -0
  183. package/server/lib/github/repo/index.js +55 -0
  184. package/server/lib/github/routes.js +1560 -0
  185. package/server/lib/magic-prompts/routes.js +63 -0
  186. package/server/lib/magic-prompts/runtime.js +119 -0
  187. package/server/lib/notifications/DOCUMENTATION.md +122 -0
  188. package/server/lib/notifications/emitter-runtime.js +102 -0
  189. package/server/lib/notifications/index.js +4 -0
  190. package/server/lib/notifications/message.js +52 -0
  191. package/server/lib/notifications/message.test.js +34 -0
  192. package/server/lib/notifications/push-runtime.js +304 -0
  193. package/server/lib/notifications/routes.js +315 -0
  194. package/server/lib/notifications/runtime.js +566 -0
  195. package/server/lib/notifications/template-runtime.js +349 -0
  196. package/server/lib/notifications/template-runtime.test.js +26 -0
  197. package/server/lib/opencode/DOCUMENTATION.md +362 -0
  198. package/server/lib/opencode/agents.js +634 -0
  199. package/server/lib/opencode/auth-state-runtime.js +88 -0
  200. package/server/lib/opencode/auth.js +83 -0
  201. package/server/lib/opencode/bootstrap-runtime.js +131 -0
  202. package/server/lib/opencode/cli-entry-runtime.js +43 -0
  203. package/server/lib/opencode/cli-options.js +128 -0
  204. package/server/lib/opencode/commands.js +339 -0
  205. package/server/lib/opencode/config-entity-routes.js +370 -0
  206. package/server/lib/opencode/core-routes.js +500 -0
  207. package/server/lib/opencode/core-routes.test.js +26 -0
  208. package/server/lib/opencode/env-config.js +74 -0
  209. package/server/lib/opencode/env-keys.js +68 -0
  210. package/server/lib/opencode/env-runtime.js +1162 -0
  211. package/server/lib/opencode/env-runtime.test.js +116 -0
  212. package/server/lib/opencode/feature-routes-runtime.js +244 -0
  213. package/server/lib/opencode/hmr-state-runtime.js +85 -0
  214. package/server/lib/opencode/index.js +66 -0
  215. package/server/lib/opencode/lifecycle.js +1019 -0
  216. package/server/lib/opencode/lifecycle.test.js +240 -0
  217. package/server/lib/opencode/mcp.js +278 -0
  218. package/server/lib/opencode/network-runtime.js +104 -0
  219. package/server/lib/opencode/network-runtime.test.js +37 -0
  220. package/server/lib/opencode/opencode-resolution-runtime.js +71 -0
  221. package/server/lib/opencode/path-utils.js +100 -0
  222. package/server/lib/opencode/path-utils.test.js +71 -0
  223. package/server/lib/opencode/project-directory-runtime.js +124 -0
  224. package/server/lib/opencode/project-icon-routes.js +399 -0
  225. package/server/lib/opencode/project-icon-routes.test.js +107 -0
  226. package/server/lib/opencode/providers.js +96 -0
  227. package/server/lib/opencode/proxy.js +445 -0
  228. package/server/lib/opencode/pwa-manifest-routes.js +257 -0
  229. package/server/lib/opencode/pwa-manifest-routes.test.js +133 -0
  230. package/server/lib/opencode/routes.js +541 -0
  231. package/server/lib/opencode/server-startup-runtime.js +156 -0
  232. package/server/lib/opencode/server-utils-runtime.js +168 -0
  233. package/server/lib/opencode/server-utils-runtime.test.js +135 -0
  234. package/server/lib/opencode/session-runtime.js +356 -0
  235. package/server/lib/opencode/session-runtime.test.js +151 -0
  236. package/server/lib/opencode/settings-helpers.js +770 -0
  237. package/server/lib/opencode/settings-helpers.test.js +109 -0
  238. package/server/lib/opencode/settings-normalization-runtime.js +428 -0
  239. package/server/lib/opencode/settings-runtime.js +826 -0
  240. package/server/lib/opencode/settings-runtime.test.js +85 -0
  241. package/server/lib/opencode/shared.js +615 -0
  242. package/server/lib/opencode/shutdown-runtime.js +139 -0
  243. package/server/lib/opencode/shutdown-runtime.test.js +58 -0
  244. package/server/lib/opencode/skill-routes.js +701 -0
  245. package/server/lib/opencode/skills.js +548 -0
  246. package/server/lib/opencode/startup-pipeline-runtime.js +130 -0
  247. package/server/lib/opencode/static-routes-runtime.js +65 -0
  248. package/server/lib/opencode/theme-runtime.js +167 -0
  249. package/server/lib/opencode/tunnel-auth.js +591 -0
  250. package/server/lib/opencode/tunnel-wiring-runtime.js +94 -0
  251. package/server/lib/opencode/vinci-routes.js +76 -0
  252. package/server/lib/opencode/watcher.js +115 -0
  253. package/server/lib/opencode/watcher.test.js +239 -0
  254. package/server/lib/preview/proxy-runtime.js +1333 -0
  255. package/server/lib/preview/proxy-runtime.test.js +144 -0
  256. package/server/lib/projects/project-config.js +567 -0
  257. package/server/lib/projects/project-config.test.js +175 -0
  258. package/server/lib/projects/project-id.js +13 -0
  259. package/server/lib/quota/DOCUMENTATION.md +58 -0
  260. package/server/lib/quota/index.js +25 -0
  261. package/server/lib/quota/providers/claude.js +107 -0
  262. package/server/lib/quota/providers/codex.js +113 -0
  263. package/server/lib/quota/providers/copilot.js +165 -0
  264. package/server/lib/quota/providers/google/api.js +92 -0
  265. package/server/lib/quota/providers/google/auth.js +108 -0
  266. package/server/lib/quota/providers/google/index.js +124 -0
  267. package/server/lib/quota/providers/google/transforms.js +109 -0
  268. package/server/lib/quota/providers/index.js +168 -0
  269. package/server/lib/quota/providers/interface.js +55 -0
  270. package/server/lib/quota/providers/kimi.js +108 -0
  271. package/server/lib/quota/providers/minimax-cn-coding-plan.js +140 -0
  272. package/server/lib/quota/providers/minimax-coding-plan.js +139 -0
  273. package/server/lib/quota/providers/nanogpt.js +124 -0
  274. package/server/lib/quota/providers/ollama-cloud.js +112 -0
  275. package/server/lib/quota/providers/openai.js +91 -0
  276. package/server/lib/quota/providers/openrouter.js +92 -0
  277. package/server/lib/quota/providers/zai.js +91 -0
  278. package/server/lib/quota/providers/zhipuai-coding-plan.js +133 -0
  279. package/server/lib/quota/providers/zhipuai.js +114 -0
  280. package/server/lib/quota/routes.js +27 -0
  281. package/server/lib/quota/utils/auth.js +50 -0
  282. package/server/lib/quota/utils/formatters.js +85 -0
  283. package/server/lib/quota/utils/formatters.test.js +54 -0
  284. package/server/lib/quota/utils/index.js +10 -0
  285. package/server/lib/quota/utils/transformers.js +55 -0
  286. package/server/lib/scheduled-tasks/DOCUMENTATION.md +44 -0
  287. package/server/lib/scheduled-tasks/routes.js +235 -0
  288. package/server/lib/scheduled-tasks/runtime.js +773 -0
  289. package/server/lib/scheduled-tasks/runtime.test.js +100 -0
  290. package/server/lib/security/request-security.js +115 -0
  291. package/server/lib/session-folders/routes.js +63 -0
  292. package/server/lib/session-folders/routes.test.js +102 -0
  293. package/server/lib/skills-catalog/DOCUMENTATION.md +178 -0
  294. package/server/lib/skills-catalog/cache.js +29 -0
  295. package/server/lib/skills-catalog/clawdhub/api.js +158 -0
  296. package/server/lib/skills-catalog/clawdhub/index.js +30 -0
  297. package/server/lib/skills-catalog/clawdhub/install.js +238 -0
  298. package/server/lib/skills-catalog/clawdhub/scan.js +113 -0
  299. package/server/lib/skills-catalog/curated-sources.js +21 -0
  300. package/server/lib/skills-catalog/git.js +77 -0
  301. package/server/lib/skills-catalog/index.js +42 -0
  302. package/server/lib/skills-catalog/install.js +294 -0
  303. package/server/lib/skills-catalog/scan.js +221 -0
  304. package/server/lib/skills-catalog/source.js +87 -0
  305. package/server/lib/terminal/DOCUMENTATION.md +76 -0
  306. package/server/lib/terminal/index.js +31 -0
  307. package/server/lib/terminal/output-replay-buffer.js +78 -0
  308. package/server/lib/terminal/output-replay-buffer.test.js +75 -0
  309. package/server/lib/terminal/runtime.js +850 -0
  310. package/server/lib/terminal/runtime.test.js +96 -0
  311. package/server/lib/terminal/terminal-ws-protocol.js +68 -0
  312. package/server/lib/terminal/terminal-ws-protocol.test.js +145 -0
  313. package/server/lib/text/DOCUMENTATION.md +35 -0
  314. package/server/lib/text/summarization.js +138 -0
  315. package/server/lib/text/summarization.test.js +34 -0
  316. package/server/lib/tts/DOCUMENTATION.md +146 -0
  317. package/server/lib/tts/base-url.js +62 -0
  318. package/server/lib/tts/capability-runtime.js +31 -0
  319. package/server/lib/tts/index.js +19 -0
  320. package/server/lib/tts/routes.js +261 -0
  321. package/server/lib/tts/routes.test.js +53 -0
  322. package/server/lib/tts/service.js +178 -0
  323. package/server/lib/tts/stt.js +75 -0
  324. package/server/lib/tunnels/DOCUMENTATION.md +18 -0
  325. package/server/lib/tunnels/index.js +166 -0
  326. package/server/lib/tunnels/managed-config.js +201 -0
  327. package/server/lib/tunnels/providers/cloudflare.js +260 -0
  328. package/server/lib/tunnels/registry.js +51 -0
  329. package/server/lib/tunnels/routes.js +605 -0
  330. package/server/lib/tunnels/types.js +219 -0
  331. package/server/lib/ui-auth/DOCUMENTATION.md +38 -0
  332. package/server/lib/ui-auth/ui-auth.js +673 -0
  333. package/server/lib/ui-auth/ui-passkeys.js +545 -0
  334. package/server/opencode-proxy.test.js +151 -0
  335. package/server/proxy-headers.js +61 -0
  336. package/server/proxy-headers.test.js +58 -0
  337. package/server/sse-routes.test.js +152 -0
@@ -0,0 +1,240 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { afterEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ const spawnMock = vi.fn();
5
+
6
+ vi.mock('node:child_process', () => ({
7
+ spawn: spawnMock,
8
+ spawnSync: vi.fn(),
9
+ }));
10
+
11
+ const { createOpenCodeLifecycleRuntime } = await import('./lifecycle.js');
12
+
13
+ const originalOpencodeBinary = process.env.OPENCODE_BINARY;
14
+ const originalPath = process.env.PATH;
15
+
16
+ afterEach(() => {
17
+ spawnMock.mockReset();
18
+ if (typeof originalOpencodeBinary === 'string') {
19
+ process.env.OPENCODE_BINARY = originalOpencodeBinary;
20
+ } else {
21
+ delete process.env.OPENCODE_BINARY;
22
+ }
23
+
24
+ if (typeof originalPath === 'string') {
25
+ process.env.PATH = originalPath;
26
+ } else {
27
+ delete process.env.PATH;
28
+ }
29
+ });
30
+
31
+ const createMockChild = () => {
32
+ const child = new EventEmitter();
33
+ child.stdout = new EventEmitter();
34
+ child.stderr = new EventEmitter();
35
+ child.exitCode = null;
36
+ child.signalCode = null;
37
+ child.pid = 12345;
38
+ child.kill = vi.fn(() => {
39
+ child.signalCode = 'SIGTERM';
40
+ queueMicrotask(() => child.emit('close', null, 'SIGTERM'));
41
+ return true;
42
+ });
43
+ return child;
44
+ };
45
+
46
+ const createRuntime = (overrides = {}) => {
47
+ const state = {
48
+ openCodeWorkingDirectory: '/tmp/project',
49
+ openCodeProcess: null,
50
+ openCodePort: null,
51
+ openCodeBaseUrl: null,
52
+ currentRestartPromise: null,
53
+ isRestartingOpenCode: false,
54
+ openCodeApiPrefix: '',
55
+ openCodeApiPrefixDetected: false,
56
+ openCodeApiDetectionTimer: null,
57
+ lastOpenCodeError: null,
58
+ isOpenCodeReady: false,
59
+ openCodeNotReadySince: 0,
60
+ isExternalOpenCode: false,
61
+ isShuttingDown: false,
62
+ healthCheckInterval: null,
63
+ expressApp: null,
64
+ useWslForOpencode: false,
65
+ resolvedWslBinary: null,
66
+ resolvedWslOpencodePath: null,
67
+ resolvedWslDistro: null,
68
+ };
69
+
70
+ return createOpenCodeLifecycleRuntime({
71
+ state,
72
+ env: {
73
+ ENV_CONFIGURED_OPENCODE_PORT: 45678,
74
+ ENV_CONFIGURED_OPENCODE_HOST: null,
75
+ ENV_EFFECTIVE_PORT: 3001,
76
+ ENV_CONFIGURED_OPENCODE_HOSTNAME: '127.0.0.1',
77
+ ENV_SKIP_OPENCODE_START: false,
78
+ },
79
+ syncToHmrState: vi.fn(),
80
+ syncFromHmrState: vi.fn(),
81
+ getOpenCodeAuthHeaders: () => ({}),
82
+ buildOpenCodeUrl: (route) => `http://127.0.0.1:45678${route}`,
83
+ waitForReady: vi.fn(async () => true),
84
+ normalizeApiPrefix: vi.fn(() => ''),
85
+ applyOpencodeBinaryFromSettings: vi.fn(async () => null),
86
+ ensureOpencodeCliEnv: vi.fn(),
87
+ ensureLocalOpenCodeServerPassword: vi.fn(async () => 'password'),
88
+ buildWslExecArgs: vi.fn((args) => args),
89
+ resolveWslExecutablePath: vi.fn(),
90
+ resolveManagedOpenCodeLaunchSpec: vi.fn((binary) => ({ binary, args: [], wrapperType: null })),
91
+ setOpenCodePort: vi.fn((port) => {
92
+ state.openCodePort = port;
93
+ }),
94
+ setDetectedOpenCodeApiPrefix: vi.fn(),
95
+ setupProxy: vi.fn(),
96
+ ensureOpenCodeApiPrefix: vi.fn(),
97
+ clearResolvedOpenCodeBinary: vi.fn(),
98
+ buildAugmentedPath: vi.fn(() => '/home/user/.bun/bin:/usr/local/bin:/usr/bin'),
99
+ buildManagedOpenCodePath: vi.fn(() => '/home/user/.bun/bin:/usr/local/bin:/usr/bin'),
100
+ getManagedOpenCodeShellEnvSnapshot: vi.fn(() => ({
101
+ PATH: '/home/user/.bun/bin:/usr/local/bin:/usr/bin',
102
+ SHELL_ONLY: 'yes',
103
+ OPENCODE_SERVER_PASSWORD: 'shell-password',
104
+ })),
105
+ ...overrides,
106
+ });
107
+ };
108
+
109
+ describe('OpenCode lifecycle', () => {
110
+ it('launches managed OpenCode with the managed PATH', async () => {
111
+ delete process.env.OPENCODE_BINARY;
112
+ const child = createMockChild();
113
+ spawnMock.mockImplementationOnce(() => {
114
+ queueMicrotask(() => {
115
+ child.stdout.emit('data', 'opencode server listening on http://127.0.0.1:45678\n');
116
+ });
117
+ return child;
118
+ });
119
+
120
+ const runtime = createRuntime();
121
+ const server = await runtime.startOpenCode();
122
+ const [binary, args, options] = spawnMock.mock.calls[0];
123
+
124
+ expect(binary).toBe('opencode');
125
+ expect(args).toEqual(['serve', '--hostname', '127.0.0.1', '--port', '45678']);
126
+ expect(options.env.PATH).toBe('/home/user/.bun/bin:/usr/local/bin:/usr/bin');
127
+ expect(options.env.SHELL_ONLY).toBe('yes');
128
+ expect(options.env.OPENCODE_SERVER_PASSWORD).toBe('password');
129
+
130
+ await server.close();
131
+ });
132
+
133
+ it('falls back to buildAugmentedPath when buildManagedOpenCodePath is not provided', async () => {
134
+ delete process.env.OPENCODE_BINARY;
135
+ const child = createMockChild();
136
+ spawnMock.mockImplementationOnce(() => {
137
+ queueMicrotask(() => {
138
+ child.stdout.emit('data', 'opencode server listening on http://127.0.0.1:45678\n');
139
+ });
140
+ return child;
141
+ });
142
+
143
+ const runtime = createRuntime({
144
+ buildManagedOpenCodePath: undefined,
145
+ buildAugmentedPath: vi.fn(() => '/home/user/.cargo/bin:/usr/local/bin'),
146
+ });
147
+ const server = await runtime.startOpenCode();
148
+ const [, , options] = spawnMock.mock.calls[0];
149
+
150
+ expect(options.env.PATH).toBe('/home/user/.cargo/bin:/usr/local/bin');
151
+
152
+ await server.close();
153
+ });
154
+
155
+ it('falls back to process.env.PATH when neither build function is provided', async () => {
156
+ delete process.env.OPENCODE_BINARY;
157
+ process.env.PATH = '/usr/bin:/bin';
158
+ const child = createMockChild();
159
+ spawnMock.mockImplementationOnce(() => {
160
+ queueMicrotask(() => {
161
+ child.stdout.emit('data', 'opencode server listening on http://127.0.0.1:45678\n');
162
+ });
163
+ return child;
164
+ });
165
+
166
+ const runtime = createRuntime({
167
+ buildManagedOpenCodePath: undefined,
168
+ buildAugmentedPath: undefined,
169
+ });
170
+ const server = await runtime.startOpenCode();
171
+ const [, , options] = spawnMock.mock.calls[0];
172
+
173
+ expect(options.env.PATH).toBe('/usr/bin:/bin');
174
+
175
+ await server.close();
176
+ });
177
+
178
+ it('reports the binary when managed OpenCode exits before becoming ready', async () => {
179
+ delete process.env.OPENCODE_BINARY;
180
+ const firstChild = createMockChild();
181
+ const secondChild = createMockChild();
182
+ spawnMock.mockImplementationOnce(() => {
183
+ queueMicrotask(() => {
184
+ firstChild.emit('exit', null, 'SIGTERM');
185
+ });
186
+ return firstChild;
187
+ });
188
+ spawnMock.mockImplementationOnce(() => {
189
+ queueMicrotask(() => {
190
+ secondChild.emit('exit', null, 'SIGTERM');
191
+ });
192
+ return secondChild;
193
+ });
194
+
195
+ const runtime = createRuntime();
196
+
197
+ await expect(runtime.startOpenCode()).rejects.toThrow('OpenCode process exited before serving with signal SIGTERM. Binary used: opencode. No stdout/stderr captured');
198
+ expect(spawnMock).toHaveBeenCalledTimes(2);
199
+ });
200
+
201
+ it('does not retry managed startup when the configured OpenCode binary is invalid', async () => {
202
+ delete process.env.OPENCODE_BINARY;
203
+ const error = new Error('Configured OpenCode binary not found: /missing/opencode');
204
+ error.code = 'OPENCODE_BINARY_INVALID';
205
+ const applyOpencodeBinaryFromSettings = vi.fn(async () => {
206
+ throw error;
207
+ });
208
+
209
+ const runtime = createRuntime({ applyOpencodeBinaryFromSettings });
210
+
211
+ await expect(runtime.startOpenCode()).rejects.toThrow('Configured OpenCode binary not found: /missing/opencode');
212
+ expect(applyOpencodeBinaryFromSettings).toHaveBeenCalledTimes(1);
213
+ expect(applyOpencodeBinaryFromSettings).toHaveBeenCalledWith({ strict: true });
214
+ expect(spawnMock).not.toHaveBeenCalled();
215
+ });
216
+
217
+ it('retries managed OpenCode startup once after a pre-ready exit', async () => {
218
+ delete process.env.OPENCODE_BINARY;
219
+ const firstChild = createMockChild();
220
+ const secondChild = createMockChild();
221
+ spawnMock.mockImplementationOnce(() => {
222
+ queueMicrotask(() => {
223
+ firstChild.emit('exit', null, 'SIGTERM');
224
+ });
225
+ return firstChild;
226
+ });
227
+ spawnMock.mockImplementationOnce(() => {
228
+ queueMicrotask(() => {
229
+ secondChild.stdout.emit('data', 'opencode server listening on http://127.0.0.1:45678\n');
230
+ });
231
+ return secondChild;
232
+ });
233
+
234
+ const runtime = createRuntime();
235
+ const server = await runtime.startOpenCode();
236
+
237
+ expect(spawnMock).toHaveBeenCalledTimes(2);
238
+ await server.close();
239
+ });
240
+ });
@@ -0,0 +1,278 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import {
4
+ CONFIG_FILE,
5
+ AGENT_SCOPE,
6
+ readConfigFile,
7
+ readConfigLayers,
8
+ getJsonEntrySource,
9
+ getJsonWriteTarget,
10
+ writeConfig,
11
+ } from './shared.js';
12
+
13
+ // ============== MCP CONFIG HELPERS ==============
14
+
15
+ /**
16
+ * Validate MCP server name
17
+ */
18
+ function validateMcpName(name) {
19
+ if (!name || typeof name !== 'string') {
20
+ throw new Error('MCP server name is required');
21
+ }
22
+ if (!/^[a-z0-9][a-z0-9_-]*[a-z0-9]$|^[a-z0-9]$/.test(name)) {
23
+ throw new Error('MCP server name must be lowercase alphanumeric with hyphens/underscores');
24
+ }
25
+ }
26
+
27
+ /**
28
+ * List all MCP server configs from user-level opencode.json
29
+ */
30
+ function resolveMcpScopeFromPath(layers, sourcePath) {
31
+ if (!sourcePath) return null;
32
+ return sourcePath === layers.paths.projectPath ? AGENT_SCOPE.PROJECT : AGENT_SCOPE.USER;
33
+ }
34
+
35
+ function ensureProjectMcpConfigPath(workingDirectory) {
36
+ const configDir = path.join(workingDirectory, '.opencode');
37
+ if (!fs.existsSync(configDir)) {
38
+ fs.mkdirSync(configDir, { recursive: true });
39
+ }
40
+ return path.join(configDir, 'opencode.json');
41
+ }
42
+
43
+ function listMcpConfigs(workingDirectory) {
44
+ const layers = readConfigLayers(workingDirectory);
45
+ const mcp = layers?.mergedConfig?.mcp || {};
46
+
47
+ return Object.entries(mcp)
48
+ .filter(([, entry]) => entry && typeof entry === 'object' && !Array.isArray(entry))
49
+ .map(([name, entry]) => {
50
+ const source = getJsonEntrySource(layers, 'mcp', name);
51
+ return {
52
+ name,
53
+ ...buildMcpEntry(entry),
54
+ scope: resolveMcpScopeFromPath(layers, source.path),
55
+ };
56
+ });
57
+ }
58
+
59
+ /**
60
+ * Get a single MCP server config by name
61
+ */
62
+ function getMcpConfig(name, workingDirectory) {
63
+ const layers = readConfigLayers(workingDirectory);
64
+ const entry = layers?.mergedConfig?.mcp?.[name];
65
+
66
+ if (!entry) {
67
+ return null;
68
+ }
69
+ const source = getJsonEntrySource(layers, 'mcp', name);
70
+ return {
71
+ name,
72
+ ...buildMcpEntry(entry),
73
+ scope: resolveMcpScopeFromPath(layers, source.path),
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Create a new MCP server config entry
79
+ */
80
+ function createMcpConfig(name, mcpConfig, workingDirectory, scope) {
81
+ validateMcpName(name);
82
+
83
+ const layers = readConfigLayers(workingDirectory);
84
+ const source = getJsonEntrySource(layers, 'mcp', name);
85
+ if (source.exists) {
86
+ throw new Error(`MCP server "${name}" already exists`);
87
+ }
88
+
89
+ let targetPath = CONFIG_FILE;
90
+ let config = {};
91
+
92
+ if (scope === AGENT_SCOPE.PROJECT) {
93
+ if (!workingDirectory) {
94
+ throw new Error('Project scope requires working directory');
95
+ }
96
+ targetPath = ensureProjectMcpConfigPath(workingDirectory);
97
+ config = fs.existsSync(targetPath) ? readConfigFile(targetPath) : {};
98
+ } else {
99
+ const jsonTarget = getJsonWriteTarget(layers, AGENT_SCOPE.USER);
100
+ targetPath = jsonTarget.path || CONFIG_FILE;
101
+ config = jsonTarget.config || {};
102
+ }
103
+
104
+ if (!config.mcp || typeof config.mcp !== 'object' || Array.isArray(config.mcp)) {
105
+ config.mcp = {};
106
+ }
107
+
108
+ const { name: _ignoredName, ...entryData } = mcpConfig;
109
+ config.mcp[name] = buildMcpEntry(entryData);
110
+
111
+ writeConfig(config, targetPath);
112
+ console.log(`Created MCP server config: ${name}`);
113
+ }
114
+
115
+ /**
116
+ * Update an existing MCP server config entry
117
+ */
118
+ function updateMcpConfig(name, updates, workingDirectory) {
119
+ const layers = readConfigLayers(workingDirectory);
120
+ const source = getJsonEntrySource(layers, 'mcp', name);
121
+
122
+ if (!source.exists) {
123
+ throw new Error(`MCP server "${name}" not found`);
124
+ }
125
+
126
+ const targetPath = source.path || CONFIG_FILE;
127
+ const config = source.config || (fs.existsSync(targetPath) ? readConfigFile(targetPath) : {});
128
+
129
+ if (!config.mcp || typeof config.mcp !== 'object' || Array.isArray(config.mcp)) {
130
+ config.mcp = {};
131
+ }
132
+
133
+ const existing = config.mcp[name];
134
+ const { name: _ignoredName, ...updateData } = updates;
135
+
136
+ config.mcp[name] = buildMcpEntry({ ...existing, ...updateData });
137
+
138
+ writeConfig(config, targetPath);
139
+ console.log(`Updated MCP server config: ${name}`);
140
+ }
141
+
142
+ /**
143
+ * Delete an MCP server config entry
144
+ */
145
+ function deleteMcpConfig(name, workingDirectory) {
146
+ const layers = readConfigLayers(workingDirectory);
147
+ const source = getJsonEntrySource(layers, 'mcp', name);
148
+ const targetPath = source.path || CONFIG_FILE;
149
+ const config = source.config || (fs.existsSync(targetPath) ? readConfigFile(targetPath) : {});
150
+
151
+ if (!config.mcp || typeof config.mcp !== 'object' || config.mcp[name] === undefined) {
152
+ throw new Error(`MCP server "${name}" not found`);
153
+ }
154
+
155
+ delete config.mcp[name];
156
+
157
+ if (Object.keys(config.mcp).length === 0) {
158
+ delete config.mcp;
159
+ }
160
+
161
+ writeConfig(config, targetPath);
162
+ console.log(`Deleted MCP server config: ${name}`);
163
+ }
164
+
165
+ /**
166
+ * Build a clean MCP entry object, omitting undefined/null values
167
+ */
168
+ function buildMcpEntry(data) {
169
+ const entry = (data && typeof data === 'object' && !Array.isArray(data))
170
+ ? { ...data }
171
+ : {};
172
+
173
+ delete entry.name;
174
+ delete entry.scope;
175
+
176
+ // type is required
177
+ entry.type = data.type === 'remote' ? 'remote' : 'local';
178
+
179
+ if (entry.type === 'local') {
180
+ // command must be a non-empty array of strings
181
+ if (Array.isArray(data.command) && data.command.length > 0) {
182
+ entry.command = data.command.map(String);
183
+ } else {
184
+ delete entry.command;
185
+ }
186
+
187
+ delete entry.url;
188
+ delete entry.headers;
189
+ delete entry.oauth;
190
+ delete entry.timeout;
191
+ } else {
192
+ // remote: url required
193
+ if (data.url && typeof data.url === 'string') {
194
+ entry.url = data.url.trim();
195
+ } else {
196
+ delete entry.url;
197
+ }
198
+
199
+ delete entry.command;
200
+
201
+ if (data.headers && typeof data.headers === 'object' && !Array.isArray(data.headers)) {
202
+ const cleaned = {};
203
+ for (const [k, v] of Object.entries(data.headers)) {
204
+ if (k && v !== undefined && v !== null) {
205
+ cleaned[k] = String(v);
206
+ }
207
+ }
208
+ if (Object.keys(cleaned).length > 0) {
209
+ entry.headers = cleaned;
210
+ } else {
211
+ delete entry.headers;
212
+ }
213
+ } else if (data.headers === undefined) {
214
+ delete entry.headers;
215
+ }
216
+
217
+ if (data.oauth === false) {
218
+ entry.oauth = false;
219
+ } else if (data.oauth && typeof data.oauth === 'object' && !Array.isArray(data.oauth)) {
220
+ const oauth = {};
221
+ if (typeof data.oauth.clientId === 'string' && data.oauth.clientId.trim()) {
222
+ oauth.clientId = data.oauth.clientId.trim();
223
+ }
224
+ if (typeof data.oauth.clientSecret === 'string' && data.oauth.clientSecret.trim()) {
225
+ oauth.clientSecret = data.oauth.clientSecret.trim();
226
+ }
227
+ if (typeof data.oauth.scope === 'string' && data.oauth.scope.trim()) {
228
+ oauth.scope = data.oauth.scope.trim();
229
+ }
230
+ if (typeof data.oauth.redirectUri === 'string' && data.oauth.redirectUri.trim()) {
231
+ oauth.redirectUri = data.oauth.redirectUri.trim();
232
+ }
233
+ if (Object.keys(oauth).length > 0) {
234
+ entry.oauth = oauth;
235
+ } else {
236
+ delete entry.oauth;
237
+ }
238
+ } else if (data.oauth === undefined) {
239
+ delete entry.oauth;
240
+ }
241
+
242
+ if (typeof data.timeout === 'number' && Number.isFinite(data.timeout) && data.timeout > 0) {
243
+ entry.timeout = data.timeout;
244
+ } else if (data.timeout === undefined || data.timeout === null || data.timeout === '') {
245
+ delete entry.timeout;
246
+ }
247
+ }
248
+
249
+ // environment: flat Record<string, string>
250
+ if (data.environment && typeof data.environment === 'object' && !Array.isArray(data.environment)) {
251
+ const cleaned = {};
252
+ for (const [k, v] of Object.entries(data.environment)) {
253
+ if (k && v !== undefined && v !== null) {
254
+ cleaned[k] = String(v);
255
+ }
256
+ }
257
+ if (Object.keys(cleaned).length > 0) {
258
+ entry.environment = cleaned;
259
+ } else {
260
+ delete entry.environment;
261
+ }
262
+ } else if (data.environment === undefined) {
263
+ delete entry.environment;
264
+ }
265
+
266
+ // enabled defaults to true
267
+ entry.enabled = data.enabled !== false;
268
+
269
+ return entry;
270
+ }
271
+
272
+ export {
273
+ listMcpConfigs,
274
+ getMcpConfig,
275
+ createMcpConfig,
276
+ updateMcpConfig,
277
+ deleteMcpConfig,
278
+ };
@@ -0,0 +1,104 @@
1
+ export const createOpenCodeNetworkRuntime = (deps) => {
2
+ const {
3
+ state,
4
+ getOpenCodeAuthHeaders,
5
+ } = deps;
6
+
7
+ const normalizeApiPrefix = (prefix) => {
8
+ if (!prefix) {
9
+ return '';
10
+ }
11
+
12
+ if (prefix.includes('://')) {
13
+ try {
14
+ const parsed = new URL(prefix);
15
+ return normalizeApiPrefix(parsed.pathname);
16
+ } catch {
17
+ return '';
18
+ }
19
+ }
20
+
21
+ const trimmed = prefix.trim();
22
+ if (!trimmed || trimmed === '/') {
23
+ return '';
24
+ }
25
+ const withLeading = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
26
+ return withLeading.endsWith('/') ? withLeading.slice(0, -1) : withLeading;
27
+ };
28
+
29
+ const waitForReady = async (url, timeoutMs = 10000) => {
30
+ const start = Date.now();
31
+ while (Date.now() - start < timeoutMs) {
32
+ let timeout = null;
33
+ try {
34
+ const controller = new AbortController();
35
+ timeout = setTimeout(() => controller.abort(), 3000);
36
+ const response = await fetch(`${url.replace(/\/+$/, '')}/global/health`, {
37
+ method: 'GET',
38
+ headers: {
39
+ Accept: 'application/json',
40
+ ...getOpenCodeAuthHeaders(),
41
+ },
42
+ signal: controller.signal,
43
+ });
44
+ clearTimeout(timeout);
45
+ timeout = null;
46
+
47
+ if (response.ok) {
48
+ const body = await response.json().catch(() => null);
49
+ if (body?.healthy === true) {
50
+ return true;
51
+ }
52
+ }
53
+ } catch {
54
+ } finally {
55
+ if (timeout) {
56
+ clearTimeout(timeout);
57
+ }
58
+ }
59
+ await new Promise((resolve) => setTimeout(resolve, 100));
60
+ }
61
+ return false;
62
+ };
63
+
64
+ const setDetectedOpenCodeApiPrefix = () => {
65
+ state.openCodeApiPrefix = '';
66
+ state.openCodeApiPrefixDetected = true;
67
+ if (state.openCodeApiDetectionTimer) {
68
+ clearTimeout(state.openCodeApiDetectionTimer);
69
+ state.openCodeApiDetectionTimer = null;
70
+ }
71
+ };
72
+
73
+ const buildOpenCodeUrl = (path, prefixOverride) => {
74
+ if (!state.openCodePort) {
75
+ throw new Error('OpenCode port is not available');
76
+ }
77
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
78
+ const prefix = normalizeApiPrefix(prefixOverride !== undefined ? prefixOverride : '');
79
+ const fullPath = `${prefix}${normalizedPath}`;
80
+ const base = state.openCodeBaseUrl ?? `http://localhost:${state.openCodePort}`;
81
+ return `${base}${fullPath}`;
82
+ };
83
+
84
+ const detectOpenCodeApiPrefix = () => {
85
+ state.openCodeApiPrefixDetected = true;
86
+ state.openCodeApiPrefix = '';
87
+ return true;
88
+ };
89
+
90
+ const ensureOpenCodeApiPrefix = () => detectOpenCodeApiPrefix();
91
+
92
+ const scheduleOpenCodeApiDetection = () => {
93
+ return;
94
+ };
95
+
96
+ return {
97
+ waitForReady,
98
+ normalizeApiPrefix,
99
+ setDetectedOpenCodeApiPrefix,
100
+ buildOpenCodeUrl,
101
+ ensureOpenCodeApiPrefix,
102
+ scheduleOpenCodeApiDetection,
103
+ };
104
+ };
@@ -0,0 +1,37 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import { createOpenCodeNetworkRuntime } from './network-runtime.js';
4
+
5
+ const createRuntime = () => createOpenCodeNetworkRuntime({
6
+ state: {
7
+ openCodePort: 4096,
8
+ openCodeBaseUrl: null,
9
+ openCodeApiPrefix: '',
10
+ openCodeApiPrefixDetected: false,
11
+ openCodeApiDetectionTimer: null,
12
+ },
13
+ getOpenCodeAuthHeaders: () => ({}),
14
+ });
15
+
16
+ describe('OpenCode network runtime', () => {
17
+ afterEach(() => {
18
+ vi.useRealTimers();
19
+ vi.unstubAllGlobals();
20
+ });
21
+
22
+ it('clears the probe abort timer when readiness fetch rejects', async () => {
23
+ vi.useFakeTimers();
24
+ vi.setSystemTime(0);
25
+ vi.stubGlobal('fetch', vi.fn(async () => {
26
+ throw new Error('offline');
27
+ }));
28
+
29
+ const runtime = createRuntime();
30
+ const readyPromise = runtime.waitForReady('http://127.0.0.1:4096', 1);
31
+
32
+ await vi.advanceTimersByTimeAsync(100);
33
+ await expect(readyPromise).resolves.toBe(false);
34
+
35
+ expect(vi.getTimerCount()).toBe(0);
36
+ });
37
+ });