@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,850 @@
1
+ import { WebSocketServer } from 'ws';
2
+ import {
3
+ TERMINAL_INPUT_WS_MAX_PAYLOAD_BYTES,
4
+ TERMINAL_INPUT_WS_PATH,
5
+ TERMINAL_OUTPUT_REPLAY_MAX_BYTES,
6
+ appendTerminalOutputReplayChunk,
7
+ createTerminalOutputReplayBuffer,
8
+ createTerminalInputWsControlFrame,
9
+ isRebindRateLimited,
10
+ listTerminalOutputReplayChunksSince,
11
+ normalizeTerminalInputWsMessageToText,
12
+ parseRequestPathname,
13
+ pruneRebindTimestamps,
14
+ readTerminalInputWsControlFrame,
15
+ } from './index.js';
16
+
17
+ export function createTerminalRuntime({
18
+ app,
19
+ server,
20
+ express,
21
+ fs,
22
+ path,
23
+ uiAuthController,
24
+ buildAugmentedPath,
25
+ searchPathFor,
26
+ isExecutable,
27
+ isRequestOriginAllowed,
28
+ rejectWebSocketUpgrade,
29
+ TERMINAL_INPUT_WS_HEARTBEAT_INTERVAL_MS,
30
+ TERMINAL_INPUT_WS_REBIND_WINDOW_MS,
31
+ TERMINAL_INPUT_WS_MAX_REBINDS_PER_WINDOW,
32
+ }) {
33
+ let ptyProviderPromise = null;
34
+ const getPtyProvider = async () => {
35
+ if (ptyProviderPromise) {
36
+ return ptyProviderPromise;
37
+ }
38
+
39
+ ptyProviderPromise = (async () => {
40
+ const isBunRuntime = typeof globalThis.Bun !== 'undefined';
41
+
42
+ if (isBunRuntime) {
43
+ try {
44
+ const bunPty = await import('bun-pty');
45
+ console.log('Using bun-pty for terminal sessions');
46
+ return { spawn: bunPty.spawn, backend: 'bun-pty' };
47
+ } catch (error) {
48
+ console.warn('bun-pty unavailable, falling back to node-pty');
49
+ }
50
+ }
51
+
52
+ try {
53
+ const nodePty = await import('node-pty');
54
+ console.log('Using node-pty for terminal sessions');
55
+ return { spawn: nodePty.spawn, backend: 'node-pty' };
56
+ } catch (error) {
57
+ console.error('Failed to load node-pty:', error && error.message ? error.message : error);
58
+ if (isBunRuntime) {
59
+ throw new Error('No PTY backend available. Install bun-pty or node-pty.');
60
+ }
61
+ throw new Error('node-pty is not available. Run: npm rebuild node-pty (or install Bun for bun-pty)');
62
+ }
63
+ })();
64
+
65
+ return ptyProviderPromise;
66
+ };
67
+
68
+ const getTerminalShellCandidates = () => {
69
+ if (process.platform === 'win32') {
70
+ const windowsCandidates = [
71
+ process.env.VINCI_TERMINAL_SHELL,
72
+ process.env.SHELL,
73
+ process.env.ComSpec,
74
+ path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe'),
75
+ 'pwsh.exe',
76
+ 'powershell.exe',
77
+ 'cmd.exe',
78
+ ].filter(Boolean);
79
+
80
+ const resolved = [];
81
+ const seen = new Set();
82
+ for (const candidateRaw of windowsCandidates) {
83
+ const candidate = String(candidateRaw).trim();
84
+ if (!candidate) continue;
85
+
86
+ const lookedUp = candidate.includes('\\') || candidate.includes('/')
87
+ ? candidate
88
+ : searchPathFor(candidate);
89
+ const executable = lookedUp && isExecutable(lookedUp) ? lookedUp : (isExecutable(candidate) ? candidate : null);
90
+ if (!executable || seen.has(executable)) continue;
91
+ seen.add(executable);
92
+ resolved.push(executable);
93
+ }
94
+ return resolved;
95
+ }
96
+
97
+ const unixCandidates = [
98
+ process.env.VINCI_TERMINAL_SHELL,
99
+ process.env.SHELL,
100
+ '/bin/zsh',
101
+ '/bin/bash',
102
+ '/bin/sh',
103
+ 'zsh',
104
+ 'bash',
105
+ 'sh',
106
+ ].filter(Boolean);
107
+
108
+ const resolved = [];
109
+ const seen = new Set();
110
+ for (const candidateRaw of unixCandidates) {
111
+ const candidate = String(candidateRaw).trim();
112
+ if (!candidate) continue;
113
+
114
+ const lookedUp = candidate.includes('/') ? candidate : searchPathFor(candidate);
115
+ const executable = lookedUp && isExecutable(lookedUp) ? lookedUp : (isExecutable(candidate) ? candidate : null);
116
+ if (!executable || seen.has(executable)) continue;
117
+ seen.add(executable);
118
+ resolved.push(executable);
119
+ }
120
+
121
+ return resolved;
122
+ };
123
+
124
+ const utf8LocaleFallback = process.platform === 'darwin' ? 'en_US.UTF-8' : 'C.UTF-8';
125
+ const lcCtypeFallback = process.platform === 'darwin' ? 'UTF-8' : 'C.UTF-8';
126
+
127
+ const spawnTerminalPtyWithFallback = (pty, { cols, rows, cwd, env }) => {
128
+ const shellCandidates = getTerminalShellCandidates();
129
+ if (shellCandidates.length === 0) {
130
+ throw new Error('No executable shell found for terminal session');
131
+ }
132
+
133
+ let lastError = null;
134
+ for (const shell of shellCandidates) {
135
+ try {
136
+ const ptyOptions = {
137
+ name: 'xterm-256color',
138
+ cols: cols || 80,
139
+ rows: rows || 24,
140
+ cwd,
141
+ env: {
142
+ ...env,
143
+ TERM: 'xterm-256color',
144
+ COLORTERM: 'truecolor',
145
+ LANG: env.LANG || process.env.LANG || utf8LocaleFallback,
146
+ LC_CTYPE: env.LC_CTYPE || process.env.LC_CTYPE || lcCtypeFallback,
147
+ },
148
+ };
149
+
150
+ if (process.platform === 'win32') {
151
+ ptyOptions.useConpty = true;
152
+ }
153
+
154
+ const ptyProcess = pty.spawn(shell, [], ptyOptions);
155
+
156
+ return { ptyProcess, shell };
157
+ } catch (error) {
158
+ lastError = error;
159
+ console.warn(`Failed to spawn PTY using shell ${shell}:`, error && error.message ? error.message : error);
160
+ }
161
+ }
162
+
163
+ const baseMessage = lastError && lastError.message ? lastError.message : 'PTY spawn failed';
164
+ throw new Error(`Failed to spawn terminal PTY with available shells (${shellCandidates.join(', ')}): ${baseMessage}`);
165
+ };
166
+
167
+ const terminalSessions = new Map();
168
+ const terminalWsConnections = new Set();
169
+ const MAX_TERMINAL_SESSIONS = 20;
170
+ const TERMINAL_IDLE_TIMEOUT = 30 * 60 * 1000;
171
+ const terminalRuntimeName = typeof globalThis.Bun === 'undefined' ? 'node' : 'bun';
172
+ const sanitizeTerminalEnv = (env) => {
173
+ const next = { ...env };
174
+ delete next.BASH_XTRACEFD;
175
+ delete next.BASH_ENV;
176
+ delete next.ENV;
177
+ return next;
178
+ };
179
+ const terminalTransportCapabilities = {
180
+ input: {
181
+ preferred: 'ws',
182
+ transports: ['http', 'ws'],
183
+ ws: {
184
+ path: TERMINAL_INPUT_WS_PATH,
185
+ v: 2,
186
+ enc: 'text+json-bin-control',
187
+ },
188
+ },
189
+ stream: {
190
+ preferred: 'ws',
191
+ transports: ['sse', 'ws'],
192
+ ws: {
193
+ path: TERMINAL_INPUT_WS_PATH,
194
+ v: 2,
195
+ enc: 'text+json-bin-control',
196
+ },
197
+ },
198
+ };
199
+
200
+ const killTerminalProcess = (ptyProcess, mode = 'term') => {
201
+ if (!ptyProcess) return;
202
+
203
+ // Best-effort: try killing the process group first so child processes
204
+ // started by shells (e.g. preview dev servers) don't orphan.
205
+ if (process.platform !== 'win32') {
206
+ const pid = ptyProcess.pid;
207
+ if (typeof pid === 'number' && Number.isFinite(pid) && pid > 0) {
208
+ try {
209
+ process.kill(-pid, mode === 'kill' ? 'SIGKILL' : 'SIGTERM');
210
+ } catch {
211
+ }
212
+ }
213
+ }
214
+
215
+ try {
216
+ // node-pty accepts an optional signal string; bun-pty ignores extra args.
217
+ ptyProcess.kill(mode === 'kill' ? 'SIGKILL' : undefined);
218
+ } catch {
219
+ }
220
+ };
221
+
222
+ const sendTerminalInputWsControl = (socket, payload) => {
223
+ if (!socket || socket.readyState !== 1) {
224
+ return;
225
+ }
226
+
227
+ try {
228
+ socket.send(createTerminalInputWsControlFrame(payload), { binary: true });
229
+ } catch {
230
+ }
231
+ };
232
+
233
+ let terminalInputWsServer = new WebSocketServer({
234
+ noServer: true,
235
+ maxPayload: TERMINAL_INPUT_WS_MAX_PAYLOAD_BYTES,
236
+ });
237
+
238
+ terminalInputWsServer.on('connection', (socket) => {
239
+ const connectionState = {
240
+ socket,
241
+ boundSessionId: null,
242
+ invalidFrames: 0,
243
+ rebindTimestamps: [],
244
+ replayCursorBySession: new Map(),
245
+ lastActivityAt: Date.now(),
246
+ };
247
+
248
+ terminalWsConnections.add(connectionState);
249
+
250
+ sendTerminalInputWsControl(socket, { t: 'ok', v: 2 });
251
+
252
+ const heartbeatInterval = setInterval(() => {
253
+ if (socket.readyState !== 1) {
254
+ return;
255
+ }
256
+
257
+ try {
258
+ socket.ping();
259
+ } catch {
260
+ }
261
+ }, TERMINAL_INPUT_WS_HEARTBEAT_INTERVAL_MS);
262
+
263
+ socket.on('pong', () => {
264
+ connectionState.lastActivityAt = Date.now();
265
+ });
266
+
267
+ socket.on('message', (message, isBinary) => {
268
+ connectionState.lastActivityAt = Date.now();
269
+
270
+ if (isBinary) {
271
+ const controlMessage = readTerminalInputWsControlFrame(message);
272
+ if (!controlMessage || typeof controlMessage.t !== 'string') {
273
+ connectionState.invalidFrames += 1;
274
+ sendTerminalInputWsControl(socket, {
275
+ t: 'e',
276
+ c: 'BAD_FRAME',
277
+ f: connectionState.invalidFrames >= 10,
278
+ });
279
+ if (connectionState.invalidFrames >= 10) {
280
+ socket.close(1008, 'protocol violation');
281
+ }
282
+ return;
283
+ }
284
+
285
+ if (controlMessage.t === 'p') {
286
+ sendTerminalInputWsControl(socket, { t: 'po', v: 2 });
287
+ return;
288
+ }
289
+
290
+ if (controlMessage.t !== 'b' || typeof controlMessage.s !== 'string') {
291
+ connectionState.invalidFrames += 1;
292
+ sendTerminalInputWsControl(socket, {
293
+ t: 'e',
294
+ c: 'BAD_FRAME',
295
+ f: connectionState.invalidFrames >= 10,
296
+ });
297
+ if (connectionState.invalidFrames >= 10) {
298
+ socket.close(1008, 'protocol violation');
299
+ }
300
+ return;
301
+ }
302
+
303
+ const now = Date.now();
304
+ connectionState.rebindTimestamps = pruneRebindTimestamps(
305
+ connectionState.rebindTimestamps,
306
+ now,
307
+ TERMINAL_INPUT_WS_REBIND_WINDOW_MS
308
+ );
309
+
310
+ if (isRebindRateLimited(connectionState.rebindTimestamps, TERMINAL_INPUT_WS_MAX_REBINDS_PER_WINDOW)) {
311
+ sendTerminalInputWsControl(socket, { t: 'e', c: 'RATE_LIMIT', f: false });
312
+ return;
313
+ }
314
+
315
+ const nextSessionId = controlMessage.s.trim();
316
+ const targetSession = terminalSessions.get(nextSessionId);
317
+ if (!targetSession) {
318
+ connectionState.boundSessionId = null;
319
+ sendTerminalInputWsControl(socket, { t: 'e', c: 'SESSION_NOT_FOUND', f: false });
320
+ return;
321
+ }
322
+
323
+ const replaySinceRaw =
324
+ typeof controlMessage.r === 'number' && Number.isFinite(controlMessage.r)
325
+ ? Math.max(0, Math.trunc(controlMessage.r))
326
+ : 0;
327
+ const rememberedReplayCursor = connectionState.replayCursorBySession.get(nextSessionId) ?? 0;
328
+ const replaySince = Math.max(replaySinceRaw, rememberedReplayCursor);
329
+
330
+ connectionState.rebindTimestamps.push(now);
331
+ connectionState.boundSessionId = nextSessionId;
332
+ sendTerminalInputWsControl(socket, {
333
+ t: 'bok',
334
+ v: 2,
335
+ s: nextSessionId,
336
+ runtime: terminalRuntimeName,
337
+ ptyBackend: targetSession.ptyBackend || 'unknown',
338
+ });
339
+
340
+ const replayChunks = listTerminalOutputReplayChunksSince(targetSession.outputReplayBuffer, replaySince);
341
+ for (const replayChunk of replayChunks) {
342
+ try {
343
+ socket.send(replayChunk.data);
344
+ connectionState.replayCursorBySession.set(nextSessionId, replayChunk.id);
345
+ } catch {
346
+ break;
347
+ }
348
+ }
349
+ return;
350
+ }
351
+
352
+ const payload = normalizeTerminalInputWsMessageToText(message);
353
+ if (payload.length === 0) {
354
+ return;
355
+ }
356
+
357
+ if (!connectionState.boundSessionId) {
358
+ sendTerminalInputWsControl(socket, { t: 'e', c: 'NOT_BOUND', f: false });
359
+ return;
360
+ }
361
+
362
+ const session = terminalSessions.get(connectionState.boundSessionId);
363
+ if (!session) {
364
+ connectionState.boundSessionId = null;
365
+ sendTerminalInputWsControl(socket, { t: 'e', c: 'SESSION_NOT_FOUND', f: false });
366
+ return;
367
+ }
368
+
369
+ try {
370
+ session.ptyProcess.write(payload);
371
+ session.lastActivity = Date.now();
372
+ } catch {
373
+ sendTerminalInputWsControl(socket, { t: 'e', c: 'WRITE_FAIL', f: false });
374
+ }
375
+ });
376
+
377
+ socket.on('close', () => {
378
+ clearInterval(heartbeatInterval);
379
+ connectionState.boundSessionId = null;
380
+ terminalWsConnections.delete(connectionState);
381
+ });
382
+
383
+ socket.on('error', (error) => {
384
+ void error;
385
+ });
386
+ });
387
+
388
+ const upgradeHandler = (req, socket, head) => {
389
+ const pathname = parseRequestPathname(req.url);
390
+ if (pathname !== TERMINAL_INPUT_WS_PATH) {
391
+ return;
392
+ }
393
+
394
+ const handleUpgrade = async () => {
395
+ try {
396
+ if (uiAuthController?.enabled) {
397
+ // Must be awaited: this call performs async token verification.
398
+ const sessionToken = await uiAuthController?.ensureSessionToken?.(req, null);
399
+ if (!sessionToken) {
400
+ rejectWebSocketUpgrade(socket, 401, 'UI authentication required');
401
+ return;
402
+ }
403
+
404
+ const originAllowed = await isRequestOriginAllowed(req);
405
+ if (!originAllowed) {
406
+ rejectWebSocketUpgrade(socket, 403, 'Invalid origin');
407
+ return;
408
+ }
409
+ }
410
+
411
+ if (!terminalInputWsServer) {
412
+ rejectWebSocketUpgrade(socket, 500, 'Terminal WebSocket unavailable');
413
+ return;
414
+ }
415
+
416
+ terminalInputWsServer.handleUpgrade(req, socket, head, (ws) => {
417
+ terminalInputWsServer.emit('connection', ws, req);
418
+ });
419
+ } catch {
420
+ rejectWebSocketUpgrade(socket, 500, 'Upgrade failed');
421
+ }
422
+ };
423
+
424
+ void handleUpgrade();
425
+ };
426
+
427
+ server.on('upgrade', upgradeHandler);
428
+
429
+ const wireTerminalSession = (sessionId, session) => {
430
+ session.ptyProcess.onData((data) => {
431
+ session.lastActivity = Date.now();
432
+ const replayChunk = appendTerminalOutputReplayChunk(
433
+ session.outputReplayBuffer,
434
+ data,
435
+ TERMINAL_OUTPUT_REPLAY_MAX_BYTES
436
+ );
437
+
438
+ for (const wsConnection of terminalWsConnections) {
439
+ if (wsConnection.boundSessionId !== sessionId) {
440
+ continue;
441
+ }
442
+
443
+ if (!wsConnection.socket || wsConnection.socket.readyState !== 1) {
444
+ continue;
445
+ }
446
+
447
+ try {
448
+ wsConnection.socket.send(data);
449
+ if (replayChunk) {
450
+ wsConnection.replayCursorBySession.set(sessionId, replayChunk.id);
451
+ }
452
+ } catch {
453
+ }
454
+ }
455
+ });
456
+
457
+ session.ptyProcess.onExit(({ exitCode, signal }) => {
458
+ console.log(`Terminal session ${sessionId} exited with code ${exitCode}, signal ${signal}`);
459
+ for (const wsConnection of terminalWsConnections) {
460
+ if (wsConnection.boundSessionId !== sessionId) {
461
+ continue;
462
+ }
463
+
464
+ wsConnection.boundSessionId = null;
465
+ wsConnection.replayCursorBySession.delete(sessionId);
466
+ sendTerminalInputWsControl(wsConnection.socket, {
467
+ t: 'x',
468
+ v: 2,
469
+ s: sessionId,
470
+ exitCode,
471
+ signal,
472
+ });
473
+ }
474
+
475
+ terminalSessions.delete(sessionId);
476
+ });
477
+ };
478
+
479
+ const idleSweepInterval = setInterval(() => {
480
+ const now = Date.now();
481
+ for (const [sessionId, session] of terminalSessions.entries()) {
482
+ if (now - session.lastActivity > TERMINAL_IDLE_TIMEOUT) {
483
+ console.log(`Cleaning up idle terminal session: ${sessionId}`);
484
+ try {
485
+ killTerminalProcess(session.ptyProcess, 'term');
486
+ } catch (error) {
487
+
488
+ }
489
+ terminalSessions.delete(sessionId);
490
+ }
491
+ }
492
+ }, 5 * 60 * 1000);
493
+
494
+ app.post('/api/terminal/create', async (req, res) => {
495
+ try {
496
+ if (terminalSessions.size >= MAX_TERMINAL_SESSIONS) {
497
+ return res.status(429).json({ error: 'Maximum terminal sessions reached' });
498
+ }
499
+
500
+ const { cwd, cols, rows } = req.body;
501
+ if (!cwd) {
502
+ return res.status(400).json({ error: 'cwd is required' });
503
+ }
504
+
505
+ try {
506
+ const stats = await fs.promises.stat(cwd);
507
+ if (!stats.isDirectory()) {
508
+ return res.status(400).json({ error: 'Invalid working directory' });
509
+ }
510
+ } catch {
511
+ return res.status(400).json({ error: 'Invalid working directory' });
512
+ }
513
+
514
+ const sessionId = Math.random().toString(36).substring(2, 15) +
515
+ Math.random().toString(36).substring(2, 15);
516
+
517
+ const envPath = buildAugmentedPath();
518
+ const resolvedEnv = sanitizeTerminalEnv({ ...process.env, PATH: envPath });
519
+
520
+ const pty = await getPtyProvider();
521
+ const { ptyProcess, shell } = spawnTerminalPtyWithFallback(pty, {
522
+ cols,
523
+ rows,
524
+ cwd,
525
+ env: resolvedEnv,
526
+ });
527
+
528
+ const session = {
529
+ ptyProcess,
530
+ ptyBackend: pty.backend,
531
+ cwd,
532
+ lastActivity: Date.now(),
533
+ clients: new Set(),
534
+ outputReplayBuffer: createTerminalOutputReplayBuffer(),
535
+ };
536
+
537
+ terminalSessions.set(sessionId, session);
538
+ wireTerminalSession(sessionId, session);
539
+
540
+ console.log(`Created terminal session: ${sessionId} in ${cwd} using shell ${shell}`);
541
+ res.json({ sessionId, cols: cols || 80, rows: rows || 24, capabilities: terminalTransportCapabilities });
542
+ } catch (error) {
543
+ console.error('Failed to create terminal session:', error);
544
+ res.status(500).json({ error: error.message || 'Failed to create terminal session' });
545
+ }
546
+ });
547
+
548
+ app.get('/api/terminal/:sessionId/stream', (req, res) => {
549
+ const { sessionId } = req.params;
550
+ const session = terminalSessions.get(sessionId);
551
+
552
+ if (!session) {
553
+ return res.status(404).json({ error: 'Terminal session not found' });
554
+ }
555
+
556
+ res.setHeader('Content-Type', 'text/event-stream');
557
+ res.setHeader('Cache-Control', 'no-cache');
558
+ res.setHeader('Connection', 'keep-alive');
559
+ res.setHeader('X-Accel-Buffering', 'no');
560
+
561
+ const clientId = Math.random().toString(36).substring(7);
562
+ session.clients.add(clientId);
563
+ session.lastActivity = Date.now();
564
+
565
+ const ptyBackend = session.ptyBackend || 'unknown';
566
+ res.write(`data: ${JSON.stringify({ type: 'connected', runtime: terminalRuntimeName, ptyBackend })}\n\n`);
567
+
568
+ const heartbeatInterval = setInterval(() => {
569
+ try {
570
+
571
+ res.write(': heartbeat\n\n');
572
+ } catch (error) {
573
+ console.error(`Heartbeat failed for client ${clientId}:`, error);
574
+ clearInterval(heartbeatInterval);
575
+ }
576
+ }, 15000);
577
+
578
+ let cleanedUp = false;
579
+ let dataDisposable = null;
580
+ let exitDisposable = null;
581
+ const cleanup = () => {
582
+ if (cleanedUp) {
583
+ return;
584
+ }
585
+
586
+ cleanedUp = true;
587
+ clearInterval(heartbeatInterval);
588
+ session.clients.delete(clientId);
589
+
590
+ if (dataDisposable && typeof dataDisposable.dispose === 'function') {
591
+ dataDisposable.dispose();
592
+ }
593
+ if (exitDisposable && typeof exitDisposable.dispose === 'function') {
594
+ exitDisposable.dispose();
595
+ }
596
+
597
+ try {
598
+ res.end();
599
+ } catch (error) {
600
+
601
+ }
602
+
603
+ console.log(`Client ${clientId} disconnected from terminal session ${sessionId}`);
604
+ };
605
+
606
+ const dataHandler = (data) => {
607
+ try {
608
+ session.lastActivity = Date.now();
609
+ const ok = res.write(`data: ${JSON.stringify({ type: 'data', data })}\n\n`);
610
+ if (!ok && session.ptyProcess && typeof session.ptyProcess.pause === 'function') {
611
+ session.ptyProcess.pause();
612
+ res.once('drain', () => {
613
+ if (session.ptyProcess && typeof session.ptyProcess.resume === 'function') {
614
+ session.ptyProcess.resume();
615
+ }
616
+ });
617
+ }
618
+ } catch (error) {
619
+ console.error(`Error sending data to client ${clientId}:`, error);
620
+ cleanup();
621
+ }
622
+ };
623
+
624
+ const exitHandler = ({ exitCode, signal }) => {
625
+ try {
626
+ res.write(`data: ${JSON.stringify({ type: 'exit', exitCode, signal })}\n\n`);
627
+ res.end();
628
+ } catch (error) {
629
+
630
+ }
631
+ cleanup();
632
+ };
633
+
634
+ dataDisposable = session.ptyProcess.onData(dataHandler);
635
+ if (cleanedUp && dataDisposable && typeof dataDisposable.dispose === 'function') {
636
+ dataDisposable.dispose();
637
+ }
638
+
639
+ exitDisposable = session.ptyProcess.onExit(exitHandler);
640
+ if (cleanedUp && exitDisposable && typeof exitDisposable.dispose === 'function') {
641
+ exitDisposable.dispose();
642
+ }
643
+
644
+ req.on('close', cleanup);
645
+ req.on('error', cleanup);
646
+
647
+ console.log(`Terminal connected: session=${sessionId} client=${clientId} runtime=${terminalRuntimeName} pty=${ptyBackend}`);
648
+ });
649
+
650
+ app.post('/api/terminal/:sessionId/input', express.text({ type: '*/*' }), (req, res) => {
651
+ const { sessionId } = req.params;
652
+ const session = terminalSessions.get(sessionId);
653
+
654
+ if (!session) {
655
+ return res.status(404).json({ error: 'Terminal session not found' });
656
+ }
657
+
658
+ const data = typeof req.body === 'string' ? req.body : '';
659
+
660
+ try {
661
+ session.ptyProcess.write(data);
662
+ session.lastActivity = Date.now();
663
+ res.json({ success: true });
664
+ } catch (error) {
665
+ console.error('Failed to write to terminal:', error);
666
+ res.status(500).json({ error: error.message || 'Failed to write to terminal' });
667
+ }
668
+ });
669
+
670
+ app.post('/api/terminal/:sessionId/resize', (req, res) => {
671
+ const { sessionId } = req.params;
672
+ const session = terminalSessions.get(sessionId);
673
+
674
+ if (!session) {
675
+ return res.status(404).json({ error: 'Terminal session not found' });
676
+ }
677
+
678
+ const { cols, rows } = req.body;
679
+ if (!cols || !rows) {
680
+ return res.status(400).json({ error: 'cols and rows are required' });
681
+ }
682
+
683
+ try {
684
+ session.ptyProcess.resize(cols, rows);
685
+ session.lastActivity = Date.now();
686
+ res.json({ success: true, cols, rows });
687
+ } catch (error) {
688
+ console.error('Failed to resize terminal:', error);
689
+ res.status(500).json({ error: error.message || 'Failed to resize terminal' });
690
+ }
691
+ });
692
+
693
+ app.delete('/api/terminal/:sessionId', (req, res) => {
694
+ const { sessionId } = req.params;
695
+ const session = terminalSessions.get(sessionId);
696
+
697
+ if (!session) {
698
+ return res.status(404).json({ error: 'Terminal session not found' });
699
+ }
700
+
701
+ try {
702
+ killTerminalProcess(session.ptyProcess, 'term');
703
+ terminalSessions.delete(sessionId);
704
+ console.log(`Closed terminal session: ${sessionId}`);
705
+ res.json({ success: true });
706
+ } catch (error) {
707
+ console.error('Failed to close terminal:', error);
708
+ res.status(500).json({ error: error.message || 'Failed to close terminal' });
709
+ }
710
+ });
711
+
712
+ app.post('/api/terminal/:sessionId/restart', async (req, res) => {
713
+ const { sessionId } = req.params;
714
+ const { cwd, cols, rows } = req.body;
715
+
716
+ if (!cwd) {
717
+ return res.status(400).json({ error: 'cwd is required' });
718
+ }
719
+
720
+ const existingSession = terminalSessions.get(sessionId);
721
+ if (existingSession) {
722
+ try {
723
+ killTerminalProcess(existingSession.ptyProcess, 'term');
724
+ } catch (error) {
725
+ }
726
+ terminalSessions.delete(sessionId);
727
+ }
728
+
729
+ try {
730
+ try {
731
+ const stats = await fs.promises.stat(cwd);
732
+ if (!stats.isDirectory()) {
733
+ return res.status(400).json({ error: 'Invalid working directory: not a directory' });
734
+ }
735
+ } catch (error) {
736
+ return res.status(400).json({ error: 'Invalid working directory: not accessible' });
737
+ }
738
+
739
+ const newSessionId = Math.random().toString(36).substring(2, 15) +
740
+ Math.random().toString(36).substring(2, 15);
741
+
742
+ const envPath = buildAugmentedPath();
743
+ const resolvedEnv = sanitizeTerminalEnv({ ...process.env, PATH: envPath });
744
+
745
+ const pty = await getPtyProvider();
746
+ const { ptyProcess, shell } = spawnTerminalPtyWithFallback(pty, {
747
+ cols,
748
+ rows,
749
+ cwd,
750
+ env: resolvedEnv,
751
+ });
752
+
753
+ const session = {
754
+ ptyProcess,
755
+ ptyBackend: pty.backend,
756
+ cwd,
757
+ lastActivity: Date.now(),
758
+ clients: new Set(),
759
+ outputReplayBuffer: createTerminalOutputReplayBuffer(),
760
+ };
761
+
762
+ terminalSessions.set(newSessionId, session);
763
+ wireTerminalSession(newSessionId, session);
764
+
765
+ console.log(`Restarted terminal session: ${sessionId} -> ${newSessionId} in ${cwd} using shell ${shell}`);
766
+ res.json({ sessionId: newSessionId, cols: cols || 80, rows: rows || 24, capabilities: terminalTransportCapabilities });
767
+ } catch (error) {
768
+ console.error('Failed to restart terminal session:', error);
769
+ res.status(500).json({ error: error.message || 'Failed to restart terminal session' });
770
+ }
771
+ });
772
+
773
+ app.post('/api/terminal/force-kill', (req, res) => {
774
+ const { sessionId, cwd } = req.body;
775
+ let killedCount = 0;
776
+
777
+ if (sessionId) {
778
+ const session = terminalSessions.get(sessionId);
779
+ if (session) {
780
+ try {
781
+ killTerminalProcess(session.ptyProcess, 'kill');
782
+ } catch (error) {
783
+ }
784
+ terminalSessions.delete(sessionId);
785
+ killedCount++;
786
+ }
787
+ } else if (cwd) {
788
+ for (const [id, session] of terminalSessions) {
789
+ if (session.cwd === cwd) {
790
+ try {
791
+ killTerminalProcess(session.ptyProcess, 'kill');
792
+ } catch (error) {
793
+ }
794
+ terminalSessions.delete(id);
795
+ killedCount++;
796
+ }
797
+ }
798
+ } else {
799
+ for (const [id, session] of terminalSessions) {
800
+ try {
801
+ killTerminalProcess(session.ptyProcess, 'kill');
802
+ } catch (error) {
803
+ }
804
+ terminalSessions.delete(id);
805
+ killedCount++;
806
+ }
807
+ }
808
+
809
+ console.log(`Force killed ${killedCount} terminal session(s)`);
810
+ res.json({ success: true, killedCount });
811
+ });
812
+
813
+ const shutdown = async () => {
814
+ server.off('upgrade', upgradeHandler);
815
+
816
+ if (idleSweepInterval) {
817
+ clearInterval(idleSweepInterval);
818
+ }
819
+
820
+ for (const [sessionId, session] of terminalSessions.entries()) {
821
+ try {
822
+ killTerminalProcess(session.ptyProcess, 'kill');
823
+ } catch {
824
+ }
825
+ terminalSessions.delete(sessionId);
826
+ }
827
+
828
+ if (!terminalInputWsServer) {
829
+ return;
830
+ }
831
+
832
+ try {
833
+ for (const client of terminalInputWsServer.clients) {
834
+ try {
835
+ client.terminate();
836
+ } catch {
837
+ }
838
+ }
839
+
840
+ await new Promise((resolve) => {
841
+ terminalInputWsServer.close(() => resolve());
842
+ });
843
+ } catch {
844
+ } finally {
845
+ terminalInputWsServer = null;
846
+ }
847
+ };
848
+
849
+ return { shutdown };
850
+ }