@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,1162 @@
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 { mergePathValues } from './path-utils.js';
6
+
7
+ export const createOpenCodeEnvRuntime = (deps) => {
8
+ const {
9
+ state,
10
+ normalizeDirectoryPath,
11
+ readSettingsFromDiskMigrated,
12
+ ENV_CONFIGURED_OPENCODE_WSL_DISTRO,
13
+ } = deps;
14
+
15
+ const parseNullSeparatedEnvSnapshot = (raw) => {
16
+ if (typeof raw !== 'string' || raw.length === 0) {
17
+ return null;
18
+ }
19
+
20
+ const result = {};
21
+ const entries = raw.split('\0');
22
+ for (const entry of entries) {
23
+ if (!entry) {
24
+ continue;
25
+ }
26
+ const idx = entry.indexOf('=');
27
+ if (idx <= 0) {
28
+ continue;
29
+ }
30
+ const key = entry.slice(0, idx);
31
+ const value = entry.slice(idx + 1);
32
+ result[key] = value;
33
+ }
34
+
35
+ return Object.keys(result).length > 0 ? result : null;
36
+ };
37
+
38
+ const isExecutable = (filePath) => {
39
+ try {
40
+ const stat = fs.statSync(filePath);
41
+ if (!stat.isFile()) return false;
42
+ if (process.platform === 'win32') {
43
+ const ext = path.extname(filePath).toLowerCase();
44
+ if (!ext) return true;
45
+ return ['.exe', '.cmd', '.bat', '.com'].includes(ext);
46
+ }
47
+ fs.accessSync(filePath, fs.constants.X_OK);
48
+ return true;
49
+ } catch {
50
+ return false;
51
+ }
52
+ };
53
+
54
+ const searchPathFor = (binaryName) => {
55
+ const trimmed = typeof binaryName === 'string' ? binaryName.trim() : '';
56
+ if (!trimmed) {
57
+ return null;
58
+ }
59
+
60
+ const current = process.env.PATH || '';
61
+ const parts = current.split(path.delimiter).filter(Boolean);
62
+ const candidateNames = [trimmed];
63
+
64
+ if (process.platform === 'win32' && !path.extname(trimmed)) {
65
+ const pathExt = process.env.PATHEXT || process.env.PathExt || '.COM;.EXE;.BAT;.CMD';
66
+ for (const ext of pathExt.split(';')) {
67
+ const normalizedExt = ext.trim();
68
+ if (!normalizedExt) continue;
69
+ const candidateName = `${trimmed}${normalizedExt.startsWith('.') ? normalizedExt : `.${normalizedExt}`}`;
70
+ if (!candidateNames.some((existing) => existing.toLowerCase() === candidateName.toLowerCase())) {
71
+ candidateNames.push(candidateName);
72
+ }
73
+ }
74
+ }
75
+
76
+ for (const dir of parts) {
77
+ for (const candidateName of candidateNames) {
78
+ const candidate = path.join(dir, candidateName);
79
+ if (isExecutable(candidate)) {
80
+ return candidate;
81
+ }
82
+ }
83
+ }
84
+ return null;
85
+ };
86
+
87
+ const prependToPath = (dir) => {
88
+ const trimmed = typeof dir === 'string' ? dir.trim() : '';
89
+ if (!trimmed) return;
90
+ const current = process.env.PATH || '';
91
+ const parts = current.split(path.delimiter).filter(Boolean);
92
+ if (parts.includes(trimmed)) return;
93
+ process.env.PATH = [trimmed, ...parts].join(path.delimiter);
94
+ };
95
+
96
+ const getWindowsShellEnvSnapshot = () => {
97
+ const parseResult = (stdout) => parseNullSeparatedEnvSnapshot(typeof stdout === 'string' ? stdout : '');
98
+
99
+ const psScript =
100
+ "Get-ChildItem Env: | ForEach-Object { [Console]::Out.Write($_.Name); [Console]::Out.Write('='); [Console]::Out.Write($_.Value); [Console]::Out.Write([char]0) }";
101
+
102
+ const powershellCandidates = [
103
+ 'pwsh.exe',
104
+ 'powershell.exe',
105
+ path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe'),
106
+ ];
107
+
108
+ for (const shellPath of powershellCandidates) {
109
+ try {
110
+ const result = spawnSync(shellPath, ['-NoLogo', '-Command', psScript], {
111
+ encoding: 'utf8',
112
+ stdio: ['ignore', 'pipe', 'pipe'],
113
+ maxBuffer: 10 * 1024 * 1024,
114
+ windowsHide: true,
115
+ });
116
+ if (result.status !== 0) {
117
+ continue;
118
+ }
119
+ const parsed = parseResult(result.stdout);
120
+ if (parsed) {
121
+ return parsed;
122
+ }
123
+ } catch {
124
+ }
125
+ }
126
+
127
+ const comspec = process.env.ComSpec || 'cmd.exe';
128
+ try {
129
+ const result = spawnSync(comspec, ['/d', '/s', '/c', 'set'], {
130
+ encoding: 'utf8',
131
+ stdio: ['ignore', 'pipe', 'pipe'],
132
+ maxBuffer: 10 * 1024 * 1024,
133
+ windowsHide: true,
134
+ });
135
+ if (result.status === 0 && typeof result.stdout === 'string' && result.stdout.length > 0) {
136
+ return parseNullSeparatedEnvSnapshot(result.stdout.replace(/\r?\n/g, '\0'));
137
+ }
138
+ } catch {
139
+ }
140
+
141
+ return null;
142
+ };
143
+
144
+ const getLoginShellEnvSnapshot = () => {
145
+ if (state.cachedLoginShellEnvSnapshot !== undefined) {
146
+ return state.cachedLoginShellEnvSnapshot;
147
+ }
148
+
149
+ if (process.platform === 'win32') {
150
+ const windowsSnapshot = getWindowsShellEnvSnapshot();
151
+ state.cachedLoginShellEnvSnapshot = windowsSnapshot;
152
+ return windowsSnapshot;
153
+ }
154
+
155
+ const shellCandidates = [process.env.SHELL, '/bin/zsh', '/bin/bash', '/bin/sh'].filter(Boolean);
156
+
157
+ for (const shellPath of shellCandidates) {
158
+ if (!isExecutable(shellPath)) {
159
+ continue;
160
+ }
161
+
162
+ try {
163
+ const result = spawnSync(shellPath, ['-lic', 'env -0'], {
164
+ encoding: 'utf8',
165
+ stdio: ['ignore', 'pipe', 'pipe'],
166
+ maxBuffer: 10 * 1024 * 1024,
167
+ windowsHide: true,
168
+ });
169
+
170
+ if (result.status !== 0) {
171
+ continue;
172
+ }
173
+
174
+ const parsed = parseNullSeparatedEnvSnapshot(result.stdout || '');
175
+ if (parsed) {
176
+ state.cachedLoginShellEnvSnapshot = parsed;
177
+ return parsed;
178
+ }
179
+ } catch {
180
+ }
181
+ }
182
+
183
+ state.cachedLoginShellEnvSnapshot = null;
184
+ return null;
185
+ };
186
+
187
+ const applyLoginShellEnvSnapshot = () => {
188
+ const snapshot = getLoginShellEnvSnapshot();
189
+ if (!snapshot) {
190
+ return;
191
+ }
192
+
193
+ const skipKeys = new Set(['PWD', 'OLDPWD', 'SHLVL', '_']);
194
+ for (const [key, value] of Object.entries(snapshot)) {
195
+ if (skipKeys.has(key)) {
196
+ continue;
197
+ }
198
+ const existing = process.env[key];
199
+ if (typeof existing === 'string' && existing.length > 0) {
200
+ continue;
201
+ }
202
+ process.env[key] = value;
203
+ }
204
+
205
+ const currentPath = process.env.PATH || '';
206
+ const shellPath = snapshot.PATH || '';
207
+ if (!shellPath) {
208
+ return;
209
+ }
210
+
211
+ process.env.PATH = mergePathValues(shellPath, currentPath, path.delimiter);
212
+ };
213
+
214
+ const isWslExecutableValue = (value) => {
215
+ if (typeof value !== 'string') return false;
216
+ const trimmed = value.trim();
217
+ if (!trimmed) return false;
218
+ return /(^|[\\/])wsl(\.exe)?$/i.test(trimmed);
219
+ };
220
+
221
+ const clearWslOpencodeResolution = () => {
222
+ state.useWslForOpencode = false;
223
+ state.resolvedWslBinary = null;
224
+ state.resolvedWslOpencodePath = null;
225
+ state.resolvedWslDistro = null;
226
+ };
227
+
228
+ const resolveWslExecutablePath = () => {
229
+ if (process.platform !== 'win32') {
230
+ return null;
231
+ }
232
+
233
+ const explicit = [process.env.WSL_BINARY, process.env.VINCI_WSL_BINARY]
234
+ .map((v) => (typeof v === 'string' ? v.trim() : ''))
235
+ .filter(Boolean);
236
+
237
+ for (const candidate of explicit) {
238
+ if (isExecutable(candidate)) {
239
+ return candidate;
240
+ }
241
+ }
242
+
243
+ try {
244
+ const result = spawnSync('where', ['wsl'], {
245
+ encoding: 'utf8',
246
+ stdio: ['ignore', 'pipe', 'pipe'],
247
+ windowsHide: true,
248
+ });
249
+ if (result.status === 0) {
250
+ const lines = (result.stdout || '')
251
+ .split(/\r?\n/)
252
+ .map((line) => line.trim())
253
+ .filter(Boolean);
254
+ const found = lines.find((line) => isExecutable(line));
255
+ if (found) {
256
+ return found;
257
+ }
258
+ }
259
+ } catch {
260
+ }
261
+
262
+ const systemRoot = process.env.SystemRoot || 'C:\\Windows';
263
+ const fallback = path.join(systemRoot, 'System32', 'wsl.exe');
264
+ if (isExecutable(fallback)) {
265
+ return fallback;
266
+ }
267
+
268
+ return null;
269
+ };
270
+
271
+ const buildWslExecArgs = (execArgs, distroOverride = null) => {
272
+ const distro = typeof distroOverride === 'string' && distroOverride.trim().length > 0
273
+ ? distroOverride.trim()
274
+ : ENV_CONFIGURED_OPENCODE_WSL_DISTRO;
275
+
276
+ const prefix = distro ? ['-d', distro] : [];
277
+ return [...prefix, '--exec', ...execArgs];
278
+ };
279
+
280
+ const probeWslForOpencode = () => {
281
+ if (process.platform !== 'win32') {
282
+ return null;
283
+ }
284
+
285
+ const wslBinary = resolveWslExecutablePath();
286
+ if (!wslBinary) {
287
+ return null;
288
+ }
289
+
290
+ try {
291
+ const result = spawnSync(
292
+ wslBinary,
293
+ buildWslExecArgs(['sh', '-lc', 'command -v opencode']),
294
+ {
295
+ encoding: 'utf8',
296
+ stdio: ['ignore', 'pipe', 'pipe'],
297
+ timeout: 6000,
298
+ windowsHide: true,
299
+ }
300
+ );
301
+
302
+ if (result.status !== 0) {
303
+ return null;
304
+ }
305
+
306
+ const lines = (result.stdout || '')
307
+ .split(/\r?\n/)
308
+ .map((line) => line.trim())
309
+ .filter(Boolean);
310
+ const found = lines[0] || '';
311
+ if (!found) {
312
+ return null;
313
+ }
314
+
315
+ return {
316
+ wslBinary,
317
+ opencodePath: found,
318
+ distro: ENV_CONFIGURED_OPENCODE_WSL_DISTRO,
319
+ };
320
+ } catch {
321
+ return null;
322
+ }
323
+ };
324
+
325
+ const applyWslOpencodeResolution = ({ wslBinary, opencodePath, source = 'wsl', distro = null } = {}) => {
326
+ const resolvedWsl = wslBinary || resolveWslExecutablePath();
327
+ if (!resolvedWsl) {
328
+ return null;
329
+ }
330
+
331
+ state.useWslForOpencode = true;
332
+ state.resolvedWslBinary = resolvedWsl;
333
+ state.resolvedWslOpencodePath = typeof opencodePath === 'string' && opencodePath.trim().length > 0
334
+ ? opencodePath.trim()
335
+ : 'opencode';
336
+ state.resolvedWslDistro = typeof distro === 'string' && distro.trim().length > 0 ? distro.trim() : ENV_CONFIGURED_OPENCODE_WSL_DISTRO;
337
+ state.resolvedOpencodeBinary = `wsl:${state.resolvedWslOpencodePath}`;
338
+ state.resolvedOpencodeBinarySource = source;
339
+
340
+ delete process.env.OPENCODE_BINARY;
341
+ return state.resolvedOpencodeBinary;
342
+ };
343
+
344
+ const resolveOpencodeCliPath = () => {
345
+ const explicit = [
346
+ process.env.OPENCODE_BINARY,
347
+ process.env.OPENCODE_PATH,
348
+ process.env.VINCI_OPENCODE_PATH,
349
+ process.env.VINCI_OPENCODE_BIN,
350
+ ]
351
+ .map((v) => (typeof v === 'string' ? v.trim() : ''))
352
+ .filter(Boolean);
353
+
354
+ for (const candidate of explicit) {
355
+ if (isExecutable(candidate)) {
356
+ clearWslOpencodeResolution();
357
+ state.resolvedOpencodeBinarySource = 'env';
358
+ return candidate;
359
+ }
360
+ }
361
+
362
+ const resolvedFromPath = searchPathFor('opencode');
363
+ if (resolvedFromPath) {
364
+ clearWslOpencodeResolution();
365
+ state.resolvedOpencodeBinarySource = 'path';
366
+ return resolvedFromPath;
367
+ }
368
+
369
+ const home = os.homedir();
370
+ const unixFallbacks = [
371
+ path.join(home, '.opencode', 'bin', 'opencode'),
372
+ path.join(home, '.bun', 'bin', 'opencode'),
373
+ path.join(home, '.local', 'bin', 'opencode'),
374
+ path.join(home, 'bin', 'opencode'),
375
+ '/opt/homebrew/bin/opencode',
376
+ '/usr/local/bin/opencode',
377
+ '/usr/bin/opencode',
378
+ '/bin/opencode',
379
+ ];
380
+
381
+ const winFallbacks = (() => {
382
+ const userProfile = process.env.USERPROFILE || home;
383
+ const appData = process.env.APPDATA || '';
384
+ const localAppData = process.env.LOCALAPPDATA || '';
385
+ const programData = process.env.ProgramData || 'C:\\ProgramData';
386
+
387
+ return [
388
+ path.join(userProfile, '.opencode', 'bin', 'opencode.exe'),
389
+ path.join(userProfile, '.opencode', 'bin', 'opencode.cmd'),
390
+ path.join(appData, 'npm', 'opencode.cmd'),
391
+ path.join(userProfile, 'scoop', 'shims', 'opencode.cmd'),
392
+ path.join(programData, 'chocolatey', 'bin', 'opencode.exe'),
393
+ path.join(programData, 'chocolatey', 'bin', 'opencode.cmd'),
394
+ path.join(userProfile, '.bun', 'bin', 'opencode.exe'),
395
+ path.join(userProfile, '.bun', 'bin', 'opencode.cmd'),
396
+ localAppData ? path.join(localAppData, 'Programs', 'opencode', 'opencode.exe') : '',
397
+ ].filter(Boolean);
398
+ })();
399
+
400
+ const fallbacks = process.platform === 'win32' ? winFallbacks : unixFallbacks;
401
+ for (const candidate of fallbacks) {
402
+ if (isExecutable(candidate)) {
403
+ clearWslOpencodeResolution();
404
+ state.resolvedOpencodeBinarySource = 'fallback';
405
+ return candidate;
406
+ }
407
+ }
408
+
409
+ if (process.platform === 'win32') {
410
+ try {
411
+ const result = spawnSync('where', ['opencode'], {
412
+ encoding: 'utf8',
413
+ stdio: ['ignore', 'pipe', 'pipe'],
414
+ windowsHide: true,
415
+ });
416
+ if (result.status === 0) {
417
+ const lines = (result.stdout || '')
418
+ .split(/\r?\n/)
419
+ .map((line) => line.trim())
420
+ .filter(Boolean);
421
+ const found = lines.find((line) => isExecutable(line));
422
+ if (found) {
423
+ clearWslOpencodeResolution();
424
+ state.resolvedOpencodeBinarySource = 'where';
425
+ return found;
426
+ }
427
+ }
428
+ } catch {
429
+ }
430
+ const wsl = probeWslForOpencode();
431
+ if (wsl) {
432
+ return applyWslOpencodeResolution({
433
+ wslBinary: wsl.wslBinary,
434
+ opencodePath: wsl.opencodePath,
435
+ source: 'wsl',
436
+ distro: wsl.distro,
437
+ });
438
+ }
439
+ return null;
440
+ }
441
+
442
+ const shells = [process.env.SHELL, '/bin/zsh', '/bin/bash', '/bin/sh'].filter(Boolean);
443
+ for (const shell of shells) {
444
+ if (!isExecutable(shell)) continue;
445
+ try {
446
+ const result = spawnSync(shell, ['-lic', 'command -v opencode'], {
447
+ encoding: 'utf8',
448
+ stdio: ['ignore', 'pipe', 'pipe'],
449
+ windowsHide: true,
450
+ });
451
+ if (result.status === 0) {
452
+ const found = (result.stdout || '').trim().split(/\s+/).pop() || '';
453
+ if (found && isExecutable(found)) {
454
+ clearWslOpencodeResolution();
455
+ state.resolvedOpencodeBinarySource = 'shell';
456
+ return found;
457
+ }
458
+ }
459
+ } catch {
460
+ }
461
+ }
462
+
463
+ return null;
464
+ };
465
+
466
+ const resolveNodeCliPath = () => {
467
+ const explicit = [process.env.NODE_BINARY, process.env.VINCI_NODE_BINARY]
468
+ .map((v) => (typeof v === 'string' ? v.trim() : ''))
469
+ .filter(Boolean);
470
+
471
+ for (const candidate of explicit) {
472
+ if (isExecutable(candidate)) {
473
+ return candidate;
474
+ }
475
+ }
476
+
477
+ const resolvedFromPath = searchPathFor('node');
478
+ if (resolvedFromPath) {
479
+ return resolvedFromPath;
480
+ }
481
+
482
+ const unixFallbacks = ['/opt/homebrew/bin/node', '/usr/local/bin/node', '/usr/bin/node', '/bin/node'];
483
+ for (const candidate of unixFallbacks) {
484
+ if (isExecutable(candidate)) {
485
+ return candidate;
486
+ }
487
+ }
488
+
489
+ if (process.platform === 'win32') {
490
+ try {
491
+ const result = spawnSync('where', ['node'], {
492
+ encoding: 'utf8',
493
+ stdio: ['ignore', 'pipe', 'pipe'],
494
+ windowsHide: true,
495
+ });
496
+ if (result.status === 0) {
497
+ const lines = (result.stdout || '')
498
+ .split(/\r?\n/)
499
+ .map((line) => line.trim())
500
+ .filter(Boolean);
501
+ const found = lines.find((line) => isExecutable(line));
502
+ if (found) return found;
503
+ }
504
+ } catch {
505
+ }
506
+ return null;
507
+ }
508
+
509
+ const shells = [process.env.SHELL, '/bin/zsh', '/bin/bash', '/bin/sh'].filter(Boolean);
510
+ for (const shell of shells) {
511
+ if (!isExecutable(shell)) continue;
512
+ try {
513
+ const result = spawnSync(shell, ['-lic', 'command -v node'], {
514
+ encoding: 'utf8',
515
+ stdio: ['ignore', 'pipe', 'pipe'],
516
+ windowsHide: true,
517
+ });
518
+ if (result.status === 0) {
519
+ const found = (result.stdout || '').trim().split(/\s+/).pop() || '';
520
+ if (found && isExecutable(found)) {
521
+ return found;
522
+ }
523
+ }
524
+ } catch {
525
+ }
526
+ }
527
+
528
+ return null;
529
+ };
530
+
531
+ const resolveBunCliPath = () => {
532
+ const explicit = [process.env.BUN_BINARY, process.env.VINCI_BUN_BINARY]
533
+ .map((v) => (typeof v === 'string' ? v.trim() : ''))
534
+ .filter(Boolean);
535
+
536
+ for (const candidate of explicit) {
537
+ if (isExecutable(candidate)) {
538
+ return candidate;
539
+ }
540
+ }
541
+
542
+ const resolvedFromPath = searchPathFor('bun');
543
+ if (resolvedFromPath) {
544
+ return resolvedFromPath;
545
+ }
546
+
547
+ const home = os.homedir();
548
+ const unixFallbacks = [
549
+ path.join(home, '.bun', 'bin', 'bun'),
550
+ '/opt/homebrew/bin/bun',
551
+ '/usr/local/bin/bun',
552
+ '/usr/bin/bun',
553
+ '/bin/bun',
554
+ ];
555
+ for (const candidate of unixFallbacks) {
556
+ if (isExecutable(candidate)) {
557
+ return candidate;
558
+ }
559
+ }
560
+
561
+ if (process.platform === 'win32') {
562
+ const userProfile = process.env.USERPROFILE || home;
563
+ const winFallbacks = [
564
+ path.join(userProfile, '.bun', 'bin', 'bun.exe'),
565
+ path.join(userProfile, '.bun', 'bin', 'bun.cmd'),
566
+ ];
567
+ for (const candidate of winFallbacks) {
568
+ if (isExecutable(candidate)) return candidate;
569
+ }
570
+
571
+ try {
572
+ const result = spawnSync('where', ['bun'], {
573
+ encoding: 'utf8',
574
+ stdio: ['ignore', 'pipe', 'pipe'],
575
+ windowsHide: true,
576
+ });
577
+ if (result.status === 0) {
578
+ const lines = (result.stdout || '')
579
+ .split(/\r?\n/)
580
+ .map((line) => line.trim())
581
+ .filter(Boolean);
582
+ const found = lines.find((line) => isExecutable(line));
583
+ if (found) return found;
584
+ }
585
+ } catch {
586
+ }
587
+ return null;
588
+ }
589
+
590
+ const shells = [process.env.SHELL, '/bin/zsh', '/bin/bash', '/bin/sh'].filter(Boolean);
591
+ for (const shell of shells) {
592
+ if (!isExecutable(shell)) continue;
593
+ try {
594
+ const result = spawnSync(shell, ['-lic', 'command -v bun'], {
595
+ encoding: 'utf8',
596
+ stdio: ['ignore', 'pipe', 'pipe'],
597
+ windowsHide: true,
598
+ });
599
+ if (result.status === 0) {
600
+ const found = (result.stdout || '').trim().split(/\s+/).pop() || '';
601
+ if (found && isExecutable(found)) {
602
+ return found;
603
+ }
604
+ }
605
+ } catch {
606
+ }
607
+ }
608
+
609
+ return null;
610
+ };
611
+
612
+ const ensureBunCliEnv = () => {
613
+ if (state.resolvedBunBinary) {
614
+ return state.resolvedBunBinary;
615
+ }
616
+
617
+ const resolved = resolveBunCliPath();
618
+ if (resolved) {
619
+ prependToPath(path.dirname(resolved));
620
+ state.resolvedBunBinary = resolved;
621
+ return resolved;
622
+ }
623
+
624
+ return null;
625
+ };
626
+
627
+ const ensureNodeCliEnv = () => {
628
+ if (state.resolvedNodeBinary) {
629
+ return state.resolvedNodeBinary;
630
+ }
631
+
632
+ const resolved = resolveNodeCliPath();
633
+ if (resolved) {
634
+ prependToPath(path.dirname(resolved));
635
+ state.resolvedNodeBinary = resolved;
636
+ return resolved;
637
+ }
638
+
639
+ return null;
640
+ };
641
+
642
+ const WINDOWS_BATCH_EXTENSIONS = new Set(['.cmd', '.bat', '.com']);
643
+
644
+ const normalizeExecutableCandidate = (value) => {
645
+ if (typeof value !== 'string') {
646
+ return null;
647
+ }
648
+ const trimmed = value.trim();
649
+ if (!trimmed) {
650
+ return null;
651
+ }
652
+ return isExecutable(trimmed) ? trimmed : null;
653
+ };
654
+
655
+ const getWindowsNativeOpencodePackageNames = () => {
656
+ if (process.arch === 'arm64') {
657
+ return ['opencode-windows-arm64'];
658
+ }
659
+ if (process.arch === 'x64') {
660
+ // Prefer the baseline build when bypassing package-manager wrappers so the
661
+ // direct binary still runs on hosts without AVX2 support.
662
+ return ['opencode-windows-x64-baseline', 'opencode-windows-x64'];
663
+ }
664
+ return [];
665
+ };
666
+
667
+ const resolveNativeOpencodeBinaryFromNodeModules = (nodeModulesDir) => {
668
+ if (typeof nodeModulesDir !== 'string' || nodeModulesDir.trim().length === 0) {
669
+ return null;
670
+ }
671
+
672
+ for (const packageName of getWindowsNativeOpencodePackageNames()) {
673
+ const candidate = path.join(nodeModulesDir, packageName, 'bin', 'opencode.exe');
674
+ if (isExecutable(candidate)) {
675
+ return candidate;
676
+ }
677
+ }
678
+
679
+ return null;
680
+ };
681
+
682
+ const resolveOpencodeNodeLaunchSpecFromNodeModules = (nodeModulesDir) => {
683
+ if (typeof nodeModulesDir !== 'string' || nodeModulesDir.trim().length === 0) {
684
+ return null;
685
+ }
686
+
687
+ const launcher = path.join(nodeModulesDir, 'opencode-ai', 'bin', 'opencode');
688
+ if (!isExecutable(launcher) && !fs.existsSync(launcher)) {
689
+ return null;
690
+ }
691
+
692
+ const nodeBinary = ensureNodeCliEnv() || resolveNodeCliPath() || 'node';
693
+ return {
694
+ binary: nodeBinary,
695
+ args: [launcher],
696
+ wrapperType: 'node-launcher',
697
+ };
698
+ };
699
+
700
+ const resolveNodeModulesDirFromCmdWrapper = (wrapperPath) => {
701
+ if (!wrapperPath || typeof wrapperPath !== 'string') {
702
+ return null;
703
+ }
704
+
705
+ try {
706
+ const content = fs.readFileSync(wrapperPath, 'utf8');
707
+ const launcherMatch = content.match(/node_modules[\\/]+opencode-ai[\\/]+bin[\\/]+opencode/i);
708
+ if (!launcherMatch) {
709
+ return null;
710
+ }
711
+
712
+ const launcherPath = path.resolve(path.dirname(wrapperPath), launcherMatch[0]);
713
+ return path.dirname(path.dirname(path.dirname(launcherPath)));
714
+ } catch {
715
+ return null;
716
+ }
717
+ };
718
+
719
+ const resolveOpencodeNodeModulesDir = (opencodePath) => {
720
+ if (typeof opencodePath !== 'string' || opencodePath.trim().length === 0) {
721
+ return null;
722
+ }
723
+
724
+ const normalized = path.resolve(opencodePath);
725
+ const lower = normalized.toLowerCase();
726
+ const fileDir = path.dirname(normalized);
727
+ const nodeModulesCandidates = [];
728
+ const pushCandidate = (candidate) => {
729
+ if (typeof candidate !== 'string' || candidate.trim().length === 0) {
730
+ return;
731
+ }
732
+ if (!nodeModulesCandidates.includes(candidate)) {
733
+ nodeModulesCandidates.push(candidate);
734
+ }
735
+ };
736
+
737
+ if (lower.includes(`${path.sep}.bun${path.sep}bin${path.sep}opencode`)) {
738
+ const bunRoot = path.dirname(path.dirname(normalized));
739
+ pushCandidate(path.join(bunRoot, 'install', 'global', 'node_modules'));
740
+ }
741
+
742
+ if (lower.endsWith(`${path.sep}node_modules${path.sep}.bin${path.sep}opencode`)
743
+ || lower.endsWith(`${path.sep}node_modules${path.sep}.bin${path.sep}opencode.cmd`)
744
+ || lower.endsWith(`${path.sep}node_modules${path.sep}.bin${path.sep}opencode.bat`)
745
+ || lower.endsWith(`${path.sep}node_modules${path.sep}.bin${path.sep}opencode.exe`)) {
746
+ pushCandidate(path.dirname(fileDir));
747
+ }
748
+
749
+ if (lower.endsWith(`${path.sep}node_modules${path.sep}opencode-ai${path.sep}bin${path.sep}opencode`)) {
750
+ pushCandidate(path.dirname(path.dirname(fileDir)));
751
+ }
752
+
753
+ if (path.basename(fileDir).toLowerCase() === 'npm') {
754
+ pushCandidate(path.join(fileDir, 'node_modules'));
755
+ }
756
+
757
+ if (WINDOWS_BATCH_EXTENSIONS.has(path.extname(normalized).toLowerCase())) {
758
+ pushCandidate(resolveNodeModulesDirFromCmdWrapper(normalized));
759
+ }
760
+
761
+ for (const candidate of nodeModulesCandidates) {
762
+ if (resolveNativeOpencodeBinaryFromNodeModules(candidate) || resolveOpencodeNodeLaunchSpecFromNodeModules(candidate)) {
763
+ return candidate;
764
+ }
765
+ }
766
+
767
+ return null;
768
+ };
769
+
770
+ const resolveManagedOpenCodeLaunchSpec = (opencodePath) => {
771
+ const fallbackBinary = typeof opencodePath === 'string' && opencodePath.trim().length > 0
772
+ ? opencodePath.trim()
773
+ : 'opencode';
774
+
775
+ if (process.platform !== 'win32') {
776
+ return { binary: fallbackBinary, args: [], wrapperType: null };
777
+ }
778
+
779
+ const ext = path.extname(fallbackBinary).toLowerCase();
780
+ const candidatePaths = [fallbackBinary];
781
+ if (WINDOWS_BATCH_EXTENSIONS.has(ext)) {
782
+ candidatePaths.push(fallbackBinary.slice(0, -ext.length) + '.exe');
783
+ }
784
+
785
+ for (const candidate of candidatePaths) {
786
+ const nodeModulesDir = resolveOpencodeNodeModulesDir(candidate);
787
+ const nativeBinary = resolveNativeOpencodeBinaryFromNodeModules(nodeModulesDir);
788
+ if (nativeBinary) {
789
+ return {
790
+ binary: nativeBinary,
791
+ args: [],
792
+ wrapperType: nativeBinary === fallbackBinary ? null : 'native-wrapper',
793
+ };
794
+ }
795
+
796
+ const nodeLaunchSpec = resolveOpencodeNodeLaunchSpecFromNodeModules(nodeModulesDir);
797
+ if (nodeLaunchSpec) {
798
+ return nodeLaunchSpec;
799
+ }
800
+
801
+ const interpreter = opencodeShimInterpreter(candidate);
802
+ if (interpreter === 'node') {
803
+ return {
804
+ binary: ensureNodeCliEnv() || resolveNodeCliPath() || 'node',
805
+ args: [candidate],
806
+ wrapperType: 'node-shebang',
807
+ };
808
+ }
809
+ if (interpreter === 'bun') {
810
+ return {
811
+ binary: ensureBunCliEnv() || resolveBunCliPath() || 'bun',
812
+ args: [candidate],
813
+ wrapperType: 'bun-shebang',
814
+ };
815
+ }
816
+
817
+ const directBinary = normalizeExecutableCandidate(candidate);
818
+ if (directBinary) {
819
+ return {
820
+ binary: directBinary,
821
+ args: [],
822
+ wrapperType: directBinary === fallbackBinary ? null : 'executable-wrapper',
823
+ };
824
+ }
825
+ }
826
+
827
+ return { binary: fallbackBinary, args: [], wrapperType: null };
828
+ };
829
+
830
+ const readShebang = (opencodePath) => {
831
+ if (!opencodePath || typeof opencodePath !== 'string') {
832
+ return null;
833
+ }
834
+ try {
835
+ const fd = fs.openSync(opencodePath, 'r');
836
+ try {
837
+ const buf = Buffer.alloc(256);
838
+ const bytes = fs.readSync(fd, buf, 0, buf.length, 0);
839
+ const head = buf.subarray(0, bytes).toString('utf8');
840
+ const firstLine = head.split(/\r?\n/, 1)[0] || '';
841
+ if (!firstLine.startsWith('#!')) {
842
+ return null;
843
+ }
844
+ const shebang = firstLine.slice(2).trim();
845
+ if (!shebang) {
846
+ return null;
847
+ }
848
+ return shebang;
849
+ } finally {
850
+ try {
851
+ fs.closeSync(fd);
852
+ } catch {
853
+ }
854
+ }
855
+ } catch {
856
+ return null;
857
+ }
858
+ };
859
+
860
+ const opencodeShimInterpreter = (opencodePath) => {
861
+ const shebang = readShebang(opencodePath);
862
+ if (!shebang) return null;
863
+ if (/\bnode\b/i.test(shebang)) return 'node';
864
+ if (/\bbun\b/i.test(shebang)) return 'bun';
865
+ return null;
866
+ };
867
+
868
+ const ensureOpencodeShimRuntime = (opencodePath) => {
869
+ const runtime = opencodeShimInterpreter(opencodePath);
870
+ if (runtime === 'node') {
871
+ ensureNodeCliEnv();
872
+ }
873
+ if (runtime === 'bun') {
874
+ ensureBunCliEnv();
875
+ }
876
+ };
877
+
878
+ const isMacOpenCodeAppBundlePath = (candidate) => {
879
+ if (process.platform !== 'darwin' || typeof candidate !== 'string') {
880
+ return false;
881
+ }
882
+ return /\/OpenCode\.app\/Contents\/MacOS\/(?:OpenCode|opencode-cli)$/i.test(candidate);
883
+ };
884
+
885
+ const createConfiguredOpencodeBinaryError = (raw, normalized) => {
886
+ const configured = typeof raw === 'string' ? raw.trim() : '';
887
+ const candidate = typeof normalized === 'string' && normalized.trim().length > 0 ? normalized.trim() : configured;
888
+ const messageSuffix = 'Vinci needs the standalone opencode CLI. Install it and set settings.opencodeBinary to the CLI path, for example ~/.opencode/bin/opencode, or leave the setting empty to use PATH lookup.';
889
+ const error = (() => {
890
+ if (isMacOpenCodeAppBundlePath(candidate) || isMacOpenCodeAppBundlePath(configured)) {
891
+ return new Error(`Configured OpenCode binary points at the macOS desktop app bundle, not the CLI: ${candidate}. ${messageSuffix}`);
892
+ }
893
+
894
+ try {
895
+ const configuredStat = fs.statSync(configured);
896
+ if (configuredStat.isDirectory()) {
897
+ return new Error(`Configured OpenCode binary directory does not contain an executable ${process.platform === 'win32' ? 'opencode.exe' : 'opencode'}: ${configured}. ${messageSuffix}`);
898
+ }
899
+ } catch {
900
+ }
901
+
902
+ try {
903
+ const stat = fs.statSync(candidate);
904
+ if (stat.isDirectory()) {
905
+ return new Error(`Configured OpenCode binary directory does not contain an executable ${process.platform === 'win32' ? 'opencode.exe' : 'opencode'}: ${candidate}. ${messageSuffix}`);
906
+ }
907
+ if (!stat.isFile()) {
908
+ return new Error(`Configured OpenCode binary is not a file: ${candidate}. ${messageSuffix}`);
909
+ }
910
+ return new Error(`Configured OpenCode binary is not executable: ${candidate}. ${messageSuffix}`);
911
+ } catch {
912
+ return new Error(`Configured OpenCode binary not found: ${candidate}. ${messageSuffix}`);
913
+ }
914
+ })();
915
+ error.code = 'OPENCODE_BINARY_INVALID';
916
+ return error;
917
+ };
918
+
919
+ const createConfiguredWslOpencodeError = (raw) => new Error(
920
+ `Configured settings.opencodeBinary uses WSL but Vinci could not resolve a WSL OpenCode command: ${raw}. Ensure WSL is available and opencode is installed in the configured distro.`
921
+ );
922
+
923
+ const normalizeOpencodeBinarySetting = (raw) => {
924
+ if (typeof raw !== 'string') {
925
+ return null;
926
+ }
927
+ const trimmed = normalizeDirectoryPath(raw).trim();
928
+ if (!trimmed) {
929
+ return '';
930
+ }
931
+
932
+ try {
933
+ const stat = fs.statSync(trimmed);
934
+ if (stat.isDirectory()) {
935
+ const bin = process.platform === 'win32' ? 'opencode.exe' : 'opencode';
936
+ return path.join(trimmed, bin);
937
+ }
938
+ } catch {
939
+ }
940
+
941
+ return trimmed;
942
+ };
943
+
944
+ const applyOpencodeBinaryFromSettings = async (options = {}) => {
945
+ const strict = options?.strict === true;
946
+ try {
947
+ const settings = await readSettingsFromDiskMigrated();
948
+ if (!settings || typeof settings !== 'object') {
949
+ return null;
950
+ }
951
+ if (!Object.prototype.hasOwnProperty.call(settings, 'opencodeBinary')) {
952
+ return null;
953
+ }
954
+
955
+ const normalized = normalizeOpencodeBinarySetting(settings.opencodeBinary);
956
+
957
+ if (normalized === '') {
958
+ delete process.env.OPENCODE_BINARY;
959
+ state.resolvedOpencodeBinary = null;
960
+ state.resolvedOpencodeBinarySource = null;
961
+ clearWslOpencodeResolution();
962
+ return null;
963
+ }
964
+
965
+ const raw = typeof settings.opencodeBinary === 'string' ? settings.opencodeBinary.trim() : '';
966
+ const explicitWslPath = process.platform === 'win32' && typeof raw === 'string'
967
+ ? raw.match(/^wsl:\s*(.+)$/i)
968
+ : null;
969
+
970
+ if (explicitWslPath && explicitWslPath[1] && explicitWslPath[1].trim().length > 0) {
971
+ const probe = probeWslForOpencode();
972
+ const applied = applyWslOpencodeResolution({
973
+ wslBinary: probe?.wslBinary || resolveWslExecutablePath(),
974
+ opencodePath: explicitWslPath[1].trim(),
975
+ source: 'settings-wsl-path',
976
+ distro: probe?.distro || ENV_CONFIGURED_OPENCODE_WSL_DISTRO,
977
+ });
978
+ if (applied) {
979
+ return applied;
980
+ }
981
+ if (strict) {
982
+ throw createConfiguredWslOpencodeError(raw);
983
+ }
984
+ }
985
+
986
+ if (process.platform === 'win32' && (isWslExecutableValue(raw) || isWslExecutableValue(normalized || ''))) {
987
+ const probe = probeWslForOpencode();
988
+ const applied = applyWslOpencodeResolution({
989
+ wslBinary: probe?.wslBinary || normalized || raw || null,
990
+ opencodePath: probe?.opencodePath || 'opencode',
991
+ source: 'settings-wsl',
992
+ distro: probe?.distro || ENV_CONFIGURED_OPENCODE_WSL_DISTRO,
993
+ });
994
+ if (applied) {
995
+ return applied;
996
+ }
997
+ if (strict) {
998
+ throw createConfiguredWslOpencodeError(raw);
999
+ }
1000
+ }
1001
+
1002
+ if (normalized && isExecutable(normalized) && !isMacOpenCodeAppBundlePath(normalized)) {
1003
+ clearWslOpencodeResolution();
1004
+ process.env.OPENCODE_BINARY = normalized;
1005
+ prependToPath(path.dirname(normalized));
1006
+ state.resolvedOpencodeBinary = normalized;
1007
+ state.resolvedOpencodeBinarySource = 'settings';
1008
+ ensureOpencodeShimRuntime(normalized);
1009
+ return normalized;
1010
+ }
1011
+
1012
+ if (raw) {
1013
+ if (strict) {
1014
+ throw createConfiguredOpencodeBinaryError(raw, normalized);
1015
+ }
1016
+ console.warn(`Configured settings.opencodeBinary is not executable: ${raw}`);
1017
+ }
1018
+ } catch (error) {
1019
+ if (strict) {
1020
+ throw error;
1021
+ }
1022
+ }
1023
+
1024
+ return null;
1025
+ };
1026
+
1027
+ const ensureOpencodeCliEnv = () => {
1028
+ if (state.resolvedOpencodeBinary) {
1029
+ if (state.useWslForOpencode) {
1030
+ return state.resolvedOpencodeBinary;
1031
+ }
1032
+ ensureOpencodeShimRuntime(state.resolvedOpencodeBinary);
1033
+ return state.resolvedOpencodeBinary;
1034
+ }
1035
+
1036
+ const existing = typeof process.env.OPENCODE_BINARY === 'string' ? process.env.OPENCODE_BINARY.trim() : '';
1037
+ if (existing && isExecutable(existing)) {
1038
+ clearWslOpencodeResolution();
1039
+ state.resolvedOpencodeBinary = existing;
1040
+ state.resolvedOpencodeBinarySource = state.resolvedOpencodeBinarySource || 'env';
1041
+ prependToPath(path.dirname(existing));
1042
+ ensureOpencodeShimRuntime(existing);
1043
+ return state.resolvedOpencodeBinary;
1044
+ }
1045
+
1046
+ const resolved = resolveOpencodeCliPath();
1047
+ if (resolved) {
1048
+ if (state.useWslForOpencode) {
1049
+ state.resolvedOpencodeBinary = resolved;
1050
+ state.resolvedOpencodeBinarySource = state.resolvedOpencodeBinarySource || 'wsl';
1051
+ console.log(`Resolved opencode CLI via WSL: ${state.resolvedWslOpencodePath || 'opencode'}`);
1052
+ return resolved;
1053
+ }
1054
+
1055
+ process.env.OPENCODE_BINARY = resolved;
1056
+ prependToPath(path.dirname(resolved));
1057
+ ensureOpencodeShimRuntime(resolved);
1058
+ state.resolvedOpencodeBinary = resolved;
1059
+ state.resolvedOpencodeBinarySource = state.resolvedOpencodeBinarySource || 'unknown';
1060
+ console.log(`Resolved opencode CLI: ${resolved}`);
1061
+ return resolved;
1062
+ }
1063
+
1064
+ clearWslOpencodeResolution();
1065
+ return null;
1066
+ };
1067
+
1068
+ const resolveGitBinaryForSpawn = () => {
1069
+ if (process.platform !== 'win32') {
1070
+ return 'git';
1071
+ }
1072
+
1073
+ if (state.resolvedGitBinary) {
1074
+ return state.resolvedGitBinary;
1075
+ }
1076
+
1077
+ const explicit = [process.env.GIT_BINARY, process.env.VINCI_GIT_BINARY]
1078
+ .map((value) => (typeof value === 'string' ? value.trim() : ''))
1079
+ .filter(Boolean);
1080
+ for (const candidate of explicit) {
1081
+ if (isExecutable(candidate)) {
1082
+ state.resolvedGitBinary = candidate;
1083
+ return state.resolvedGitBinary;
1084
+ }
1085
+ }
1086
+
1087
+ const candidates = [];
1088
+ const normalizeGitCandidate = (candidate) => {
1089
+ if (typeof candidate !== 'string') {
1090
+ return '';
1091
+ }
1092
+ const trimmed = candidate.trim();
1093
+ if (!trimmed) {
1094
+ return '';
1095
+ }
1096
+ const ext = path.extname(trimmed).toLowerCase();
1097
+ if (ext === '.cmd' || ext === '.bat' || ext === '.com') {
1098
+ const exeCandidate = trimmed.slice(0, -ext.length) + '.exe';
1099
+ if (isExecutable(exeCandidate)) {
1100
+ return exeCandidate;
1101
+ }
1102
+ }
1103
+ return trimmed;
1104
+ };
1105
+
1106
+ const pathCandidate = normalizeGitCandidate(searchPathFor('git'));
1107
+ if (pathCandidate && isExecutable(pathCandidate)) {
1108
+ candidates.push(pathCandidate);
1109
+ }
1110
+
1111
+ const pathExeCandidate = normalizeGitCandidate(searchPathFor('git.exe'));
1112
+ if (pathExeCandidate && isExecutable(pathExeCandidate)) {
1113
+ candidates.push(pathExeCandidate);
1114
+ }
1115
+
1116
+ const programRoots = [
1117
+ process.env.ProgramFiles,
1118
+ process.env['ProgramFiles(x86)'],
1119
+ process.env.LocalAppData,
1120
+ ]
1121
+ .map((value) => (typeof value === 'string' ? value.trim() : ''))
1122
+ .filter(Boolean);
1123
+ for (const root of programRoots) {
1124
+ const installCandidates = [
1125
+ path.join(root, 'Git', 'cmd', 'git.exe'),
1126
+ path.join(root, 'Git', 'bin', 'git.exe'),
1127
+ path.join(root, 'Git', 'mingw64', 'bin', 'git.exe'),
1128
+ path.join(root, 'Programs', 'Git', 'cmd', 'git.exe'),
1129
+ path.join(root, 'Programs', 'Git', 'bin', 'git.exe'),
1130
+ ];
1131
+ for (const candidate of installCandidates) {
1132
+ const normalized = normalizeGitCandidate(candidate);
1133
+ if (normalized && isExecutable(normalized)) {
1134
+ candidates.push(normalized);
1135
+ }
1136
+ }
1137
+ }
1138
+
1139
+ const preferredExe = candidates.find((candidate) => candidate.toLowerCase().endsWith('.exe'));
1140
+ state.resolvedGitBinary = preferredExe || candidates[0] || 'git.exe';
1141
+ return state.resolvedGitBinary;
1142
+ };
1143
+
1144
+ const clearResolvedOpenCodeBinary = () => {
1145
+ state.resolvedOpencodeBinary = null;
1146
+ };
1147
+
1148
+ return {
1149
+ applyLoginShellEnvSnapshot,
1150
+ ensureOpencodeCliEnv,
1151
+ applyOpencodeBinaryFromSettings,
1152
+ getLoginShellEnvSnapshot,
1153
+ resolveOpencodeCliPath,
1154
+ resolveManagedOpenCodeLaunchSpec,
1155
+ isExecutable,
1156
+ searchPathFor,
1157
+ resolveGitBinaryForSpawn,
1158
+ resolveWslExecutablePath,
1159
+ buildWslExecArgs,
1160
+ clearResolvedOpenCodeBinary,
1161
+ };
1162
+ };