@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,445 @@
1
+ import { createProxyMiddleware } from 'http-proxy-middleware';
2
+
3
+ import {
4
+ applyForwardProxyResponseHeaders,
5
+ collectForwardProxyHeaders,
6
+ shouldForwardProxyResponseHeader,
7
+ } from '../../proxy-headers.js';
8
+
9
+ export const waitForSseDrain = (res, signal) => new Promise((resolve) => {
10
+ if (signal?.aborted || res.writableEnded || res.destroyed) {
11
+ resolve();
12
+ return;
13
+ }
14
+
15
+ const cleanup = () => {
16
+ res.off?.('drain', onDone);
17
+ res.off?.('close', onDone);
18
+ res.off?.('error', onDone);
19
+ signal?.removeEventListener?.('abort', onDone);
20
+ };
21
+ const onDone = () => {
22
+ cleanup();
23
+ resolve();
24
+ };
25
+
26
+ res.once?.('drain', onDone);
27
+ res.once?.('close', onDone);
28
+ res.once?.('error', onDone);
29
+ signal?.addEventListener?.('abort', onDone, { once: true });
30
+ });
31
+
32
+ export const writeSseChunkWithBackpressure = async (res, value, signal) => {
33
+ if (!value || value.length === 0 || signal?.aborted || res.writableEnded || res.destroyed) {
34
+ return false;
35
+ }
36
+
37
+ const flushed = res.write(value);
38
+ if (flushed !== false) {
39
+ return true;
40
+ }
41
+
42
+ await waitForSseDrain(res, signal);
43
+ return !signal?.aborted && !res.writableEnded && !res.destroyed;
44
+ };
45
+
46
+ export const createSseBoundaryTracker = () => {
47
+ const decoder = new TextDecoder();
48
+ let tail = '';
49
+
50
+ const normalize = (value) => value.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
51
+
52
+ return {
53
+ observe(value) {
54
+ const text = typeof value === 'string'
55
+ ? value
56
+ : decoder.decode(value, { stream: true });
57
+ if (text.length > 0) {
58
+ tail = `${tail}${normalize(text)}`;
59
+ if (tail.length > 4096) {
60
+ tail = tail.slice(-4096);
61
+ }
62
+ }
63
+ return this.isAtBoundary();
64
+ },
65
+ isAtBoundary() {
66
+ return tail.length === 0 || tail.endsWith('\n\n');
67
+ },
68
+ };
69
+ };
70
+
71
+ export const registerOpenCodeProxy = (app, deps) => {
72
+ const {
73
+ fs,
74
+ os,
75
+ path,
76
+ OPEN_CODE_READY_GRACE_MS,
77
+ getRuntime,
78
+ getOpenCodeAuthHeaders,
79
+ buildOpenCodeUrl,
80
+ ensureOpenCodeApiPrefix,
81
+ } = deps;
82
+
83
+ if (app.get('opencodeProxyConfigured')) {
84
+ return;
85
+ }
86
+
87
+ const runtime = getRuntime();
88
+ if (runtime.openCodePort) {
89
+ console.log(`[proxy] Setting up proxy to OpenCode on port ${runtime.openCodePort}`);
90
+ } else {
91
+ console.log('[proxy] Setting up OpenCode API gate (OpenCode not started yet)');
92
+ }
93
+ console.log('[proxy] Runtime state:', JSON.stringify({
94
+ openCodePort: runtime.openCodePort,
95
+ openCodeBaseUrl: runtime.openCodeBaseUrl,
96
+ isOpenCodeReady: runtime.isOpenCodeReady,
97
+ }));
98
+ app.set('opencodeProxyConfigured', true);
99
+
100
+ const isAbortError = (error) => error?.name === 'AbortError';
101
+ const FALLBACK_PROXY_TARGET = 'http://127.0.0.1:3902';
102
+
103
+ const normalizeProxyTarget = (candidate) => {
104
+ if (typeof candidate !== 'string') {
105
+ return null;
106
+ }
107
+
108
+ const trimmed = candidate.trim();
109
+ if (!trimmed) {
110
+ return null;
111
+ }
112
+
113
+ return trimmed.replace(/\/+$/, '');
114
+ };
115
+
116
+ // Keep generic proxy requests on the same upstream base URL that health checks
117
+ // and direct fetch helpers use. This avoids split-brain state where /health
118
+ // succeeds against an external host but /api/* still proxies to 127.0.0.1.
119
+ const resolveProxyTarget = () => {
120
+ try {
121
+ const resolved = normalizeProxyTarget(buildOpenCodeUrl('/', ''));
122
+ if (resolved) {
123
+ return resolved;
124
+ }
125
+ } catch {
126
+ }
127
+
128
+ const runtimeState = getRuntime();
129
+ const externalBase = normalizeProxyTarget(runtimeState.openCodeBaseUrl);
130
+ if (externalBase) {
131
+ return externalBase;
132
+ }
133
+
134
+ if (runtimeState.openCodePort) {
135
+ return `http://localhost:${runtimeState.openCodePort}`;
136
+ }
137
+
138
+ return FALLBACK_PROXY_TARGET;
139
+ };
140
+
141
+ // Log proxy target resolution for debugging
142
+ const resolveProxyTargetLogged = () => {
143
+ const target = resolveProxyTarget();
144
+ console.log('[proxy] resolveProxyTarget ->', target);
145
+ return target;
146
+ };
147
+
148
+ const forwardSseRequest = async (req, res) => {
149
+ const abortController = new AbortController();
150
+ const closeUpstream = () => abortController.abort();
151
+ let upstream = null;
152
+ let reader = null;
153
+ let heartbeatTimer = null;
154
+ let writeQueue = Promise.resolve(true);
155
+ const sseBoundary = createSseBoundaryTracker();
156
+
157
+ req.on('close', closeUpstream);
158
+
159
+ try {
160
+ const requestUrl = typeof req.originalUrl === 'string' && req.originalUrl.length > 0
161
+ ? req.originalUrl
162
+ : (typeof req.url === 'string' ? req.url : '');
163
+ const upstreamPath = requestUrl.startsWith('/api') ? requestUrl.slice(4) || '/' : requestUrl;
164
+ const headers = collectForwardProxyHeaders(req.headers, getOpenCodeAuthHeaders());
165
+ headers.accept ??= 'text/event-stream';
166
+ headers['cache-control'] ??= 'no-cache';
167
+
168
+ const targetUrl = buildOpenCodeUrl(upstreamPath, '');
169
+ console.log(`[proxy] SSE ${req.method} ${requestUrl} -> ${targetUrl}`);
170
+
171
+ upstream = await fetch(targetUrl, {
172
+ method: 'GET',
173
+ headers,
174
+ signal: abortController.signal,
175
+ });
176
+
177
+ console.log(`[proxy] SSE upstream status: ${upstream.status}, content-type: ${upstream.headers.get('content-type')}`);
178
+
179
+ res.status(upstream.status);
180
+ applyForwardProxyResponseHeaders(upstream.headers, res);
181
+
182
+ const contentType = upstream.headers.get('content-type') || 'text/event-stream';
183
+ const isEventStream = contentType.toLowerCase().includes('text/event-stream');
184
+
185
+ if (!upstream.body) {
186
+ res.end(await upstream.text().catch(() => ''));
187
+ return;
188
+ }
189
+
190
+ if (!isEventStream) {
191
+ res.end(await upstream.text());
192
+ return;
193
+ }
194
+
195
+ res.setHeader('Content-Type', contentType);
196
+ res.setHeader('Cache-Control', 'no-cache');
197
+ res.setHeader('Connection', 'keep-alive');
198
+ res.setHeader('X-Accel-Buffering', 'no');
199
+ if (typeof res.flushHeaders === 'function') {
200
+ res.flushHeaders();
201
+ }
202
+
203
+ // Disable TCP Nagle's algorithm so small SSE chunks are sent immediately
204
+ // instead of being buffered up to ~200ms by the TCP stack.
205
+ if (res.socket && typeof res.socket.setNoDelay === 'function') {
206
+ res.socket.setNoDelay(true);
207
+ }
208
+
209
+ const SSE_HEARTBEAT_INTERVAL_MS = 20_000;
210
+
211
+ const scheduleHeartbeat = () => {
212
+ heartbeatTimer = setTimeout(async () => {
213
+ if (abortController.signal.aborted || res.writableEnded || res.destroyed) {
214
+ return;
215
+ }
216
+ if (!sseBoundary.isAtBoundary()) {
217
+ scheduleHeartbeat();
218
+ return;
219
+ }
220
+ const canContinue = await enqueueSseWrite(':heartbeat\n\n');
221
+ if (canContinue) {
222
+ scheduleHeartbeat();
223
+ }
224
+ }, SSE_HEARTBEAT_INTERVAL_MS);
225
+ };
226
+
227
+ const enqueueSseWrite = (value) => {
228
+ writeQueue = writeQueue
229
+ .catch(() => false)
230
+ .then((canContinue) => {
231
+ if (!canContinue) {
232
+ return false;
233
+ }
234
+ return writeSseChunkWithBackpressure(res, value, abortController.signal);
235
+ });
236
+ return writeQueue;
237
+ };
238
+
239
+ scheduleHeartbeat();
240
+
241
+ reader = upstream.body.getReader();
242
+ while (!abortController.signal.aborted) {
243
+ const { done, value } = await reader.read();
244
+ if (done) {
245
+ break;
246
+ }
247
+ if (value && value.length > 0) {
248
+ sseBoundary.observe(value);
249
+ const canContinue = await enqueueSseWrite(value);
250
+ if (!canContinue) {
251
+ break;
252
+ }
253
+ }
254
+ }
255
+
256
+ res.end();
257
+ } catch (error) {
258
+ if (isAbortError(error)) {
259
+ return;
260
+ }
261
+ console.error('[proxy] OpenCode SSE proxy error:', error?.message ?? error);
262
+ if (!res.headersSent) {
263
+ res.status(503).json({ error: 'OpenCode service unavailable' });
264
+ } else {
265
+ res.end();
266
+ }
267
+ } finally {
268
+ if (heartbeatTimer) {
269
+ clearTimeout(heartbeatTimer);
270
+ heartbeatTimer = null;
271
+ }
272
+ req.off('close', closeUpstream);
273
+ try {
274
+ if (reader) {
275
+ await reader.cancel();
276
+ reader.releaseLock();
277
+ } else if (upstream?.body && !upstream.body.locked) {
278
+ await upstream.body.cancel();
279
+ }
280
+ } catch {
281
+ }
282
+ }
283
+ };
284
+
285
+ // Ensure API prefix is detected before proxying
286
+ app.use('/api', (_req, _res, next) => {
287
+ ensureOpenCodeApiPrefix();
288
+ next();
289
+ });
290
+
291
+ // Readiness gate — return 503 while OpenCode is starting/restarting
292
+ app.use('/api', (req, res, next) => {
293
+ if (
294
+ req.path.startsWith('/themes/custom') ||
295
+ req.path.startsWith('/push') ||
296
+ req.path.startsWith('/config/agents') ||
297
+ req.path.startsWith('/config/opencode-resolution') ||
298
+ req.path.startsWith('/config/settings') ||
299
+ req.path.startsWith('/config/skills') ||
300
+ req.path === '/config/reload' ||
301
+ req.path === '/health'
302
+ ) {
303
+ return next();
304
+ }
305
+
306
+ const runtimeState = getRuntime();
307
+ const waitElapsed = runtimeState.openCodeNotReadySince === 0 ? 0 : Date.now() - runtimeState.openCodeNotReadySince;
308
+ const stillWaiting =
309
+ (!runtimeState.isOpenCodeReady && (runtimeState.openCodeNotReadySince === 0 || waitElapsed < OPEN_CODE_READY_GRACE_MS)) ||
310
+ runtimeState.isRestartingOpenCode ||
311
+ !runtimeState.openCodePort;
312
+
313
+ if (stillWaiting) {
314
+ console.log(`[proxy] Readiness gate BLOCKED ${req.method} ${req.originalUrl} — OpenCode not ready (port=${runtimeState.openCodePort}, ready=${runtimeState.isOpenCodeReady}, restarting=${runtimeState.isRestartingOpenCode})`);
315
+ return res.status(503).json({
316
+ error: 'OpenCode is restarting',
317
+ restarting: true,
318
+ });
319
+ }
320
+
321
+ next();
322
+ });
323
+
324
+ // Windows: session merge for cross-directory session listing
325
+ if (process.platform === 'win32') {
326
+ app.get('/api/session', async (req, res, next) => {
327
+ const rawUrl = req.originalUrl || req.url || '';
328
+ if (rawUrl.includes('directory=')) return next();
329
+
330
+ try {
331
+ const authHeaders = getOpenCodeAuthHeaders();
332
+ const fetchOpts = {
333
+ method: 'GET',
334
+ headers: { Accept: 'application/json', ...authHeaders },
335
+ signal: AbortSignal.timeout(10000),
336
+ };
337
+ const globalRes = await fetch(buildOpenCodeUrl('/session', ''), fetchOpts);
338
+ const globalPayload = globalRes.ok ? await globalRes.json().catch(() => []) : [];
339
+ const globalSessions = Array.isArray(globalPayload) ? globalPayload : [];
340
+
341
+ const settingsDir = process.env.VINCI_DATA_DIR
342
+ ? path.resolve(process.env.VINCI_DATA_DIR)
343
+ : path.join(os.homedir(), '.vinci');
344
+ const settingsPath = path.join(settingsDir, 'settings.json');
345
+ let projectDirs = [];
346
+ try {
347
+ const settingsRaw = fs.readFileSync(settingsPath, 'utf8');
348
+ const settings = JSON.parse(settingsRaw);
349
+ projectDirs = (settings.projects || [])
350
+ .map((project) => (typeof project?.path === 'string' ? project.path.trim() : ''))
351
+ .filter(Boolean);
352
+ } catch {
353
+ }
354
+
355
+ const seen = new Set(
356
+ globalSessions
357
+ .map((session) => (session && typeof session.id === 'string' ? session.id : null))
358
+ .filter((id) => typeof id === 'string')
359
+ );
360
+ const extraSessions = [];
361
+ for (const dir of projectDirs) {
362
+ const candidates = Array.from(new Set([
363
+ dir,
364
+ dir.replace(/\\/g, '/'),
365
+ dir.replace(/\//g, '\\'),
366
+ ]));
367
+ for (const candidateDir of candidates) {
368
+ const encoded = encodeURIComponent(candidateDir);
369
+ try {
370
+ const dirRes = await fetch(buildOpenCodeUrl(`/session?directory=${encoded}`, ''), fetchOpts);
371
+ if (dirRes.ok) {
372
+ const dirPayload = await dirRes.json().catch(() => []);
373
+ const dirSessions = Array.isArray(dirPayload) ? dirPayload : [];
374
+ for (const session of dirSessions) {
375
+ const id = session && typeof session.id === 'string' ? session.id : null;
376
+ if (id && !seen.has(id)) {
377
+ seen.add(id);
378
+ extraSessions.push(session);
379
+ }
380
+ }
381
+ }
382
+ } catch {
383
+ }
384
+ }
385
+ }
386
+
387
+ const merged = [...globalSessions, ...extraSessions];
388
+ merged.sort((a, b) => {
389
+ const aTime = a && typeof a.time_updated === 'number' ? a.time_updated : 0;
390
+ const bTime = b && typeof b.time_updated === 'number' ? b.time_updated : 0;
391
+ return bTime - aTime;
392
+ });
393
+ console.log(`[SessionMerge] ${globalSessions.length} global + ${extraSessions.length} extra = ${merged.length} total`);
394
+ return res.json(merged);
395
+ } catch (error) {
396
+ console.log(`[SessionMerge] Error: ${error.message}, falling through`);
397
+ next();
398
+ }
399
+ });
400
+ }
401
+
402
+ app.get('/api/global/event', forwardSseRequest);
403
+ app.get('/api/event', forwardSseRequest);
404
+
405
+ // Generic proxy for non-SSE OpenCode API routes.
406
+ const apiProxy = createProxyMiddleware({
407
+ target: resolveProxyTarget(),
408
+ changeOrigin: true,
409
+ pathRewrite: { '^/api': '' },
410
+ // Dynamic target — port can change after restart
411
+ router: () => resolveProxyTargetLogged(),
412
+ on: {
413
+ proxyReq: (proxyReq, req) => {
414
+ // Inject OpenCode auth headers
415
+ const authHeaders = getOpenCodeAuthHeaders();
416
+ if (authHeaders.Authorization) {
417
+ proxyReq.setHeader('Authorization', authHeaders.Authorization);
418
+ }
419
+
420
+ // Defensive: request identity encoding from upstream OpenCode.
421
+ // This avoids compressed-body/header mismatches in multi-proxy setups.
422
+ proxyReq.setHeader('accept-encoding', 'identity');
423
+
424
+ console.log(`[proxy] ${req.method} ${req.originalUrl} -> ${proxyReq.getHeader('host')}${proxyReq.path}`);
425
+ },
426
+ proxyRes: (proxyRes) => {
427
+ for (const key of Object.keys(proxyRes.headers || {})) {
428
+ if (!shouldForwardProxyResponseHeader(key)) {
429
+ delete proxyRes.headers[key];
430
+ }
431
+ }
432
+ },
433
+ error: (err, req, res) => {
434
+ console.error(`[proxy] OpenCode proxy error for ${req.method} ${req.originalUrl}:`, err.message);
435
+ if (err.code) console.error(`[proxy] code: ${err.code}`);
436
+ if (err.stack) console.error(`[proxy] stack: ${err.stack.split('\n').slice(0, 3).join('\n')}`);
437
+ if (res && !res.headersSent && typeof res.status === 'function') {
438
+ res.status(503).json({ error: 'OpenCode service unavailable' });
439
+ }
440
+ },
441
+ },
442
+ });
443
+
444
+ app.use('/api', apiProxy);
445
+ };
@@ -0,0 +1,257 @@
1
+ const DEFAULT_PWA_APP_NAME = 'Vinci - AI Coding Assistant';
2
+ const mapPwaOrientationToManifest = (value) => {
3
+ if (value === 'portrait') {
4
+ return 'portrait-primary';
5
+ }
6
+ if (value === 'landscape') {
7
+ return 'landscape-primary';
8
+ }
9
+ return undefined;
10
+ };
11
+
12
+ export const registerPwaManifestRoute = (app, dependencies) => {
13
+ const {
14
+ process,
15
+ resolveProjectDirectory,
16
+ buildOpenCodeUrl,
17
+ getOpenCodeAuthHeaders,
18
+ readSettingsFromDiskMigrated,
19
+ normalizePwaAppName,
20
+ normalizePwaOrientation,
21
+ } = dependencies;
22
+
23
+ const recentPwaSessionsCache = new Map();
24
+
25
+ const getRecentPwaSessionShortcuts = async (req) => {
26
+ const now = Date.now();
27
+
28
+ const resolvedDirectoryResult = await resolveProjectDirectory(req).catch(() => ({ directory: null }));
29
+ const preferredDirectory = typeof resolvedDirectoryResult?.directory === 'string'
30
+ ? resolvedDirectoryResult.directory
31
+ : null;
32
+
33
+ const cacheKey = preferredDirectory ? `dir:${preferredDirectory}` : 'global';
34
+ const cached = recentPwaSessionsCache.get(cacheKey);
35
+ if (cached && now - cached.at < 5000) {
36
+ return cached.data;
37
+ }
38
+
39
+ const normalizeShortcutTitle = (value, fallback) => {
40
+ const normalized = normalizePwaAppName(value, fallback);
41
+ return normalized.length > 48 ? normalized.slice(0, 48) : normalized;
42
+ };
43
+
44
+ const toFiniteNumber = (value) => {
45
+ if (typeof value === 'number' && Number.isFinite(value)) {
46
+ return value;
47
+ }
48
+ if (typeof value === 'string' && value.trim().length > 0) {
49
+ const parsed = Number(value);
50
+ if (Number.isFinite(parsed)) {
51
+ return parsed;
52
+ }
53
+ }
54
+ return null;
55
+ };
56
+
57
+ const normalizeDirectory = (value) => {
58
+ if (typeof value !== 'string') {
59
+ return '';
60
+ }
61
+ const trimmed = value.trim();
62
+ if (!trimmed) {
63
+ return '';
64
+ }
65
+ const normalized = trimmed.replace(/\\/g, '/');
66
+ if (normalized === '/') {
67
+ return '/';
68
+ }
69
+ return normalized.length > 1 ? normalized.replace(/\/+$/, '') : normalized;
70
+ };
71
+
72
+ const sessionUpdatedAt = (session) => {
73
+ const time = session && typeof session.time === 'object' ? session.time : null;
74
+ return toFiniteNumber(time?.updated) ?? toFiniteNumber(time?.created) ?? 0;
75
+ };
76
+
77
+ const filterSessionsByDirectory = (sessions, directory) => {
78
+ const normalizedDirectory = normalizeDirectory(directory);
79
+ if (!normalizedDirectory) {
80
+ return sessions;
81
+ }
82
+
83
+ const prefix = normalizedDirectory === '/' ? '/' : `${normalizedDirectory}/`;
84
+ return sessions.filter((session) => {
85
+ const sessionDirectory = normalizeDirectory(session?.directory);
86
+ if (!sessionDirectory) {
87
+ return false;
88
+ }
89
+ return sessionDirectory === normalizedDirectory || sessionDirectory.startsWith(prefix);
90
+ });
91
+ };
92
+
93
+ const listSessions = async (directory) => {
94
+ const query = (() => {
95
+ if (typeof directory !== 'string' || directory.length === 0) {
96
+ return '';
97
+ }
98
+ const preparedDirectory = process.platform === 'win32'
99
+ ? directory.replace(/\//g, '\\\\')
100
+ : directory;
101
+ return `?directory=${encodeURIComponent(preparedDirectory)}`;
102
+ })();
103
+
104
+ const response = await fetch(buildOpenCodeUrl(`/session${query}`, ''), {
105
+ method: 'GET',
106
+ headers: {
107
+ Accept: 'application/json',
108
+ ...getOpenCodeAuthHeaders(),
109
+ },
110
+ signal: AbortSignal.timeout(2500),
111
+ });
112
+
113
+ if (!response.ok) {
114
+ return [];
115
+ }
116
+
117
+ const payload = await response.json().catch(() => null);
118
+ return Array.isArray(payload) ? payload : [];
119
+ };
120
+
121
+ try {
122
+ let payload = [];
123
+
124
+ if (preferredDirectory) {
125
+ const scopedPayload = await listSessions(preferredDirectory);
126
+ const filteredScopedPayload = filterSessionsByDirectory(scopedPayload, preferredDirectory);
127
+
128
+ if (filteredScopedPayload.length > 0) {
129
+ payload = filteredScopedPayload;
130
+ } else {
131
+ const globalPayload = await listSessions(null);
132
+ const filteredGlobalPayload = filterSessionsByDirectory(globalPayload, preferredDirectory);
133
+ payload = filteredGlobalPayload;
134
+ }
135
+ } else {
136
+ payload = await listSessions(null);
137
+ }
138
+
139
+ const seen = new Set();
140
+ const rows = [];
141
+
142
+ for (const item of payload) {
143
+ if (!item || typeof item !== 'object') {
144
+ continue;
145
+ }
146
+
147
+ const id = typeof item.id === 'string' ? item.id.trim().slice(0, 160) : '';
148
+ if (!id || seen.has(id)) {
149
+ continue;
150
+ }
151
+
152
+ seen.add(id);
153
+ const title = normalizeShortcutTitle(item.title, `Session ${rows.length + 1}`);
154
+ const updatedAt = sessionUpdatedAt(item);
155
+
156
+ rows.push({ id, title, updatedAt });
157
+ }
158
+
159
+ rows.sort((a, b) => b.updatedAt - a.updatedAt);
160
+
161
+ const shortcuts = rows.slice(0, 3).map((session) => ({
162
+ name: session.title,
163
+ short_name: session.title.length > 32 ? session.title.slice(0, 32) : session.title,
164
+ description: 'Open recent session',
165
+ url: `/?session=${encodeURIComponent(session.id)}`,
166
+ icons: [{ src: '/pwa-192.png', sizes: '192x192', type: 'image/png' }],
167
+ }));
168
+
169
+ recentPwaSessionsCache.set(cacheKey, { at: now, data: shortcuts });
170
+ return shortcuts;
171
+ } catch {
172
+ recentPwaSessionsCache.set(cacheKey, { at: now, data: [] });
173
+ return [];
174
+ }
175
+ };
176
+
177
+ app.get('/manifest.webmanifest', async (req, res) => {
178
+ const hasQueryOverride =
179
+ typeof req.query?.pwa_name === 'string'
180
+ || typeof req.query?.app_name === 'string'
181
+ || typeof req.query?.appName === 'string';
182
+
183
+ let queryValueRaw = '';
184
+ if (typeof req.query?.pwa_name === 'string') {
185
+ queryValueRaw = req.query.pwa_name;
186
+ } else if (typeof req.query?.app_name === 'string') {
187
+ queryValueRaw = req.query.app_name;
188
+ } else if (typeof req.query?.appName === 'string') {
189
+ queryValueRaw = req.query.appName;
190
+ }
191
+
192
+ const queryOverrideName = normalizePwaAppName(queryValueRaw, '');
193
+ const hasOrientationOverride = typeof req.query?.orientation === 'string';
194
+ const queryOverrideOrientation = normalizePwaOrientation(req.query?.orientation, 'system');
195
+
196
+ let storedName = '';
197
+ let storedOrientation = 'system';
198
+ try {
199
+ const settings = await readSettingsFromDiskMigrated();
200
+ storedName = normalizePwaAppName(settings?.pwaAppName, '');
201
+ storedOrientation = normalizePwaOrientation(settings?.pwaOrientation, 'system');
202
+ } catch {
203
+ storedName = '';
204
+ storedOrientation = 'system';
205
+ }
206
+
207
+ const appName = hasQueryOverride
208
+ ? (queryOverrideName || DEFAULT_PWA_APP_NAME)
209
+ : (storedName || DEFAULT_PWA_APP_NAME);
210
+ const manifestOrientation = mapPwaOrientationToManifest(
211
+ hasOrientationOverride ? queryOverrideOrientation : storedOrientation
212
+ );
213
+
214
+ const shortName = appName.length > 30 ? appName.slice(0, 30) : appName;
215
+ const recentSessionShortcuts = await getRecentPwaSessionShortcuts(req);
216
+
217
+ const manifest = {
218
+ name: appName,
219
+ short_name: shortName,
220
+ description: 'Web interface companion for OpenCode AI coding agent',
221
+ id: '/',
222
+ start_url: '/',
223
+ scope: '/',
224
+ display: 'standalone',
225
+ display_override: ['window-controls-overlay'],
226
+ background_color: '#151313',
227
+ theme_color: '#edb449',
228
+ ...(manifestOrientation ? { orientation: manifestOrientation } : {}),
229
+ icons: [
230
+ { src: '/pwa-192.png', sizes: '192x192', type: 'image/png', purpose: 'any' },
231
+ { src: '/pwa-512.png', sizes: '512x512', type: 'image/png', purpose: 'any' },
232
+ { src: '/pwa-maskable-192.png', sizes: '192x192', type: 'image/png', purpose: 'any maskable' },
233
+ { src: '/pwa-maskable-512.png', sizes: '512x512', type: 'image/png', purpose: 'any maskable' },
234
+ { src: '/apple-touch-icon-180x180.png', sizes: '180x180', type: 'image/png', purpose: 'any' },
235
+ { src: '/apple-touch-icon-152x152.png', sizes: '152x152', type: 'image/png', purpose: 'any' },
236
+ { src: '/favicon-32.png', sizes: '32x32', type: 'image/png' },
237
+ { src: '/favicon-16.png', sizes: '16x16', type: 'image/png' },
238
+ ],
239
+ shortcuts: [
240
+ {
241
+ name: 'Appearance Settings',
242
+ short_name: 'Settings',
243
+ description: 'Open appearance settings',
244
+ url: '/?settings=appearance',
245
+ icons: [{ src: '/pwa-192.png', sizes: '192x192', type: 'image/png' }],
246
+ },
247
+ ...recentSessionShortcuts,
248
+ ],
249
+ categories: ['developer', 'tools', 'productivity'],
250
+ lang: 'en',
251
+ };
252
+
253
+ res.setHeader('Cache-Control', 'no-store, must-revalidate');
254
+ res.type('application/manifest+json');
255
+ res.send(JSON.stringify(manifest));
256
+ });
257
+ };