@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,519 @@
1
+ import { getRemotes, getStatus } from '../git/index.js';
2
+ import { resolveGitHubRepoFromDirectory } from './repo/index.js';
3
+
4
+ const REPO_DEFAULT_BRANCH_TTL_MS = 5 * 60_000;
5
+ const defaultBranchCache = new Map();
6
+ const repoMetadataCache = new Map();
7
+
8
+ const normalizeText = (value) => typeof value === 'string' ? value.trim() : '';
9
+ const normalizeLower = (value) => normalizeText(value).toLowerCase();
10
+ const normalizeRepoKey = (owner, repo) => {
11
+ const normalizedOwner = normalizeLower(owner);
12
+ const normalizedRepo = normalizeLower(repo);
13
+ if (!normalizedOwner || !normalizedRepo) {
14
+ return '';
15
+ }
16
+ return `${normalizedOwner}/${normalizedRepo}`;
17
+ };
18
+ const parseTrackingRemoteName = (trackingBranch) => {
19
+ const normalized = normalizeText(trackingBranch);
20
+ if (!normalized) {
21
+ return '';
22
+ }
23
+ const slashIndex = normalized.indexOf('/');
24
+ if (slashIndex <= 0) {
25
+ return '';
26
+ }
27
+ return normalized.slice(0, slashIndex).trim();
28
+ };
29
+
30
+ const parseTrackingBranchName = (trackingBranch) => {
31
+ const normalized = normalizeText(trackingBranch);
32
+ if (!normalized) {
33
+ return '';
34
+ }
35
+ const slashIndex = normalized.indexOf('/');
36
+ if (slashIndex <= 0 || slashIndex >= normalized.length - 1) {
37
+ return '';
38
+ }
39
+ return normalized.slice(slashIndex + 1).trim();
40
+ };
41
+
42
+ const pushUnique = (collection, value, keyFn = normalizeLower) => {
43
+ const normalizedValue = normalizeText(value);
44
+ if (!normalizedValue) {
45
+ return;
46
+ }
47
+ const nextKey = keyFn(normalizedValue);
48
+ if (!nextKey) {
49
+ return;
50
+ }
51
+ if (collection.some((item) => keyFn(item) === nextKey)) {
52
+ return;
53
+ }
54
+ collection.push(normalizedValue);
55
+ };
56
+
57
+ const rankRemoteNames = (remoteNames, explicitRemoteName, trackingRemoteName) => {
58
+ const ranked = [];
59
+ pushUnique(ranked, explicitRemoteName);
60
+
61
+ if (trackingRemoteName) {
62
+ pushUnique(ranked, trackingRemoteName);
63
+ }
64
+
65
+ pushUnique(ranked, 'origin');
66
+ pushUnique(ranked, 'upstream');
67
+ remoteNames.forEach((name) => pushUnique(ranked, name));
68
+ return ranked;
69
+ };
70
+
71
+ const getHeadOwner = (pr) => {
72
+ const repoOwner = normalizeText(pr?.head?.repo?.owner?.login);
73
+ if (repoOwner) {
74
+ return repoOwner;
75
+ }
76
+ const userOwner = normalizeText(pr?.head?.user?.login);
77
+ if (userOwner) {
78
+ return userOwner;
79
+ }
80
+ const headLabel = normalizeText(pr?.head?.label);
81
+ const separatorIndex = headLabel.indexOf(':');
82
+ if (separatorIndex > 0) {
83
+ return headLabel.slice(0, separatorIndex).trim();
84
+ }
85
+ return '';
86
+ };
87
+
88
+ const getHeadRepoKey = (pr, fallbackRepoName) => {
89
+ const repoOwner = normalizeText(pr?.head?.repo?.owner?.login);
90
+ const repoName = normalizeText(pr?.head?.repo?.name);
91
+ if (repoOwner && repoName) {
92
+ return normalizeRepoKey(repoOwner, repoName);
93
+ }
94
+ const headLabel = normalizeText(pr?.head?.label);
95
+ const separatorIndex = headLabel.indexOf(':');
96
+ if (separatorIndex > 0) {
97
+ const labelOwner = headLabel.slice(0, separatorIndex).trim();
98
+ if (labelOwner && fallbackRepoName) {
99
+ return normalizeRepoKey(labelOwner, fallbackRepoName);
100
+ }
101
+ }
102
+ return '';
103
+ };
104
+
105
+ const buildSourceMatcher = (sourceCandidates) => {
106
+ const repoRank = new Map();
107
+ const ownerRank = new Map();
108
+
109
+ sourceCandidates.forEach((candidate, index) => {
110
+ const repoKey = normalizeRepoKey(candidate.repo?.owner, candidate.repo?.repo);
111
+ if (repoKey && !repoRank.has(repoKey)) {
112
+ repoRank.set(repoKey, index);
113
+ }
114
+ const owner = normalizeLower(candidate.repo?.owner);
115
+ if (owner && !ownerRank.has(owner)) {
116
+ ownerRank.set(owner, index);
117
+ }
118
+ });
119
+
120
+ const matches = (pr, fallbackRepoName) => {
121
+ const repoKey = getHeadRepoKey(pr, fallbackRepoName);
122
+ if (repoKey && repoRank.has(repoKey)) {
123
+ return true;
124
+ }
125
+ const owner = normalizeLower(getHeadOwner(pr));
126
+ return Boolean(owner) && ownerRank.has(owner);
127
+ };
128
+
129
+ const compare = (left, right, fallbackRepoName) => {
130
+ const leftRepoRank = repoRank.get(getHeadRepoKey(left, fallbackRepoName));
131
+ const rightRepoRank = repoRank.get(getHeadRepoKey(right, fallbackRepoName));
132
+ const leftRepoScore = typeof leftRepoRank === 'number' ? leftRepoRank : Number.POSITIVE_INFINITY;
133
+ const rightRepoScore = typeof rightRepoRank === 'number' ? rightRepoRank : Number.POSITIVE_INFINITY;
134
+ if (leftRepoScore !== rightRepoScore) {
135
+ return leftRepoScore - rightRepoScore;
136
+ }
137
+
138
+ const leftOwnerRank = ownerRank.get(normalizeLower(getHeadOwner(left)));
139
+ const rightOwnerRank = ownerRank.get(normalizeLower(getHeadOwner(right)));
140
+ const leftOwnerScore = typeof leftOwnerRank === 'number' ? leftOwnerRank : Number.POSITIVE_INFINITY;
141
+ const rightOwnerScore = typeof rightOwnerRank === 'number' ? rightOwnerRank : Number.POSITIVE_INFINITY;
142
+ if (leftOwnerScore !== rightOwnerScore) {
143
+ return leftOwnerScore - rightOwnerScore;
144
+ }
145
+
146
+ return 0;
147
+ };
148
+
149
+ return { matches, compare };
150
+ };
151
+
152
+ const getRepoDefaultBranch = async (octokit, repo) => {
153
+ const repoKey = normalizeRepoKey(repo?.owner, repo?.repo);
154
+ if (!repoKey) {
155
+ return null;
156
+ }
157
+
158
+ const cached = defaultBranchCache.get(repoKey);
159
+ if (cached && Date.now() - cached.fetchedAt < REPO_DEFAULT_BRANCH_TTL_MS) {
160
+ return cached.defaultBranch;
161
+ }
162
+
163
+ try {
164
+ const response = await octokit.rest.repos.get({
165
+ owner: repo.owner,
166
+ repo: repo.repo,
167
+ });
168
+ const defaultBranch = normalizeText(response?.data?.default_branch) || null;
169
+ defaultBranchCache.set(repoKey, {
170
+ defaultBranch,
171
+ fetchedAt: Date.now(),
172
+ });
173
+ return defaultBranch;
174
+ } catch {
175
+ return null;
176
+ }
177
+ };
178
+
179
+ const getRepoMetadata = async (octokit, repo) => {
180
+ const repoKey = normalizeRepoKey(repo?.owner, repo?.repo);
181
+ if (!repoKey) {
182
+ return null;
183
+ }
184
+
185
+ const cached = repoMetadataCache.get(repoKey);
186
+ if (cached && Date.now() - cached.fetchedAt < REPO_DEFAULT_BRANCH_TTL_MS) {
187
+ return cached.data;
188
+ }
189
+
190
+ try {
191
+ const response = await octokit.rest.repos.get({
192
+ owner: repo.owner,
193
+ repo: repo.repo,
194
+ });
195
+ const data = response?.data ?? null;
196
+ repoMetadataCache.set(repoKey, {
197
+ data,
198
+ fetchedAt: Date.now(),
199
+ });
200
+ return data;
201
+ } catch (error) {
202
+ if (error?.status === 403 || error?.status === 404) {
203
+ repoMetadataCache.set(repoKey, {
204
+ data: null,
205
+ fetchedAt: Date.now(),
206
+ });
207
+ return null;
208
+ }
209
+ throw error;
210
+ }
211
+ };
212
+
213
+ const resolveRemoteCandidates = async (directory, rankedRemoteNames) => {
214
+ const results = [];
215
+ const seenRepoKeys = new Set();
216
+
217
+ for (const remoteName of rankedRemoteNames) {
218
+ const resolved = await resolveGitHubRepoFromDirectory(directory, remoteName).catch(() => ({ repo: null }));
219
+ const repo = resolved?.repo || null;
220
+ const repoKey = normalizeRepoKey(repo?.owner, repo?.repo);
221
+ if (!repo || !repoKey || seenRepoKeys.has(repoKey)) {
222
+ continue;
223
+ }
224
+ seenRepoKeys.add(repoKey);
225
+ results.push({
226
+ remoteName,
227
+ repo,
228
+ });
229
+ }
230
+
231
+ return results;
232
+ };
233
+
234
+ const expandRepoNetwork = async (octokit, candidates) => {
235
+ const expanded = [];
236
+ const seenRepoKeys = new Set();
237
+
238
+ const pushCandidate = (repo, remoteName, priority) => {
239
+ const repoKey = normalizeRepoKey(repo?.owner, repo?.repo);
240
+ if (!repoKey || seenRepoKeys.has(repoKey)) {
241
+ return;
242
+ }
243
+ seenRepoKeys.add(repoKey);
244
+ expanded.push({ repo, remoteName, priority });
245
+ };
246
+
247
+ for (const candidate of candidates) {
248
+ const metadata = await getRepoMetadata(octokit, candidate.repo);
249
+ if (!metadata) {
250
+ continue;
251
+ }
252
+
253
+ pushCandidate(candidate.repo, candidate.remoteName, candidate.priority);
254
+
255
+ const parent = metadata?.parent;
256
+ if (parent?.owner?.login && parent?.name) {
257
+ pushCandidate({
258
+ owner: parent.owner.login,
259
+ repo: parent.name,
260
+ url: parent.html_url || `https://github.com/${parent.owner.login}/${parent.name}`,
261
+ }, candidate.remoteName, candidate.priority + 0.1);
262
+ }
263
+
264
+ const source = metadata?.source;
265
+ if (source?.owner?.login && source?.name) {
266
+ pushCandidate({
267
+ owner: source.owner.login,
268
+ repo: source.name,
269
+ url: source.html_url || `https://github.com/${source.owner.login}/${source.name}`,
270
+ }, candidate.remoteName, candidate.priority + 0.2);
271
+ }
272
+ }
273
+
274
+ return expanded.sort((left, right) => left.priority - right.priority);
275
+ };
276
+
277
+ const safeListPulls = async (octokit, options) => {
278
+ try {
279
+ const response = await octokit.rest.pulls.list(options);
280
+ return Array.isArray(response?.data) ? response.data : [];
281
+ } catch (error) {
282
+ if (error?.status === 404 || error?.status === 403) {
283
+ return [];
284
+ }
285
+ throw error;
286
+ }
287
+ };
288
+
289
+ const parseRepoFromApiUrl = (value) => {
290
+ const normalized = normalizeText(value);
291
+ if (!normalized) {
292
+ return null;
293
+ }
294
+ try {
295
+ const url = new URL(normalized);
296
+ const parts = url.pathname.replace(/^\/+/, '').split('/').filter(Boolean);
297
+ if (parts.length < 2 || parts[0] !== 'repos') {
298
+ return null;
299
+ }
300
+ const owner = parts[1];
301
+ const repo = parts[2];
302
+ if (!owner || !repo) {
303
+ return null;
304
+ }
305
+ return { owner, repo };
306
+ } catch {
307
+ return null;
308
+ }
309
+ };
310
+
311
+ // Track repos where the GitHub Search API returned 403 (token lacks scope for that org)
312
+ const _searchApiDisabledRepos = new Map();
313
+ const SEARCH_API_RETRY_MS = 5 * 60 * 1000; // retry after 5 minutes
314
+
315
+ const searchFallbackPr = async ({ octokit, branch, repoNames }) => {
316
+ // Build a repo key to check/store 403 status per-repo
317
+ const repoKey = [...repoNames].sort().join(',').toLowerCase();
318
+
319
+ // Skip if this repo set returned 403 recently
320
+ const disabledAt = _searchApiDisabledRepos.get(repoKey);
321
+ if (disabledAt && Date.now() - disabledAt < SEARCH_API_RETRY_MS) {
322
+ return null;
323
+ }
324
+
325
+ const normalizedRepoNames = new Set(repoNames.map((name) => normalizeLower(name)).filter(Boolean));
326
+
327
+ for (const state of ['open', 'closed']) {
328
+ let response;
329
+ try {
330
+ response = await octokit.rest.search.issuesAndPullRequests({
331
+ q: `is:pr state:${state} head:${branch}`,
332
+ per_page: 20,
333
+ });
334
+ // If we get here, search API works for this repo — clear the disabled flag
335
+ _searchApiDisabledRepos.delete(repoKey);
336
+ } catch (error) {
337
+ if (error?.status === 403) {
338
+ _searchApiDisabledRepos.set(repoKey, Date.now());
339
+ return null;
340
+ }
341
+ if (error?.status === 404) {
342
+ continue;
343
+ }
344
+ throw error;
345
+ }
346
+
347
+ const items = Array.isArray(response?.data?.items) ? response.data.items : [];
348
+ for (const item of items) {
349
+ const repo = parseRepoFromApiUrl(item?.repository_url);
350
+ if (!repo) {
351
+ continue;
352
+ }
353
+ if (normalizedRepoNames.size > 0 && !normalizedRepoNames.has(normalizeLower(repo.repo))) {
354
+ continue;
355
+ }
356
+ try {
357
+ const prResponse = await octokit.rest.pulls.get({
358
+ owner: repo.owner,
359
+ repo: repo.repo,
360
+ pull_number: item.number,
361
+ });
362
+ const pr = prResponse?.data;
363
+ if (!pr || normalizeText(pr.head?.ref) !== branch) {
364
+ continue;
365
+ }
366
+ return {
367
+ repo: {
368
+ owner: repo.owner,
369
+ repo: repo.repo,
370
+ url: `https://github.com/${repo.owner}/${repo.repo}`,
371
+ },
372
+ pr,
373
+ };
374
+ } catch (error) {
375
+ if (error?.status === 403 || error?.status === 404) {
376
+ continue;
377
+ }
378
+ throw error;
379
+ }
380
+ }
381
+ }
382
+
383
+ return null;
384
+ };
385
+
386
+ const findFirstMatchingPr = async ({ octokit, target, branch, sourceCandidates }) => {
387
+ const matcher = buildSourceMatcher(sourceCandidates);
388
+ const sourceOwners = [];
389
+ sourceCandidates.forEach((candidate) => pushUnique(sourceOwners, candidate.repo?.owner));
390
+
391
+ const pickPreferred = (prs) => prs
392
+ .filter((pr) => normalizeText(pr?.head?.ref) === branch)
393
+ .filter((pr) => matcher.matches(pr, target.repo.repo))
394
+ .sort((left, right) => matcher.compare(left, right, target.repo.repo))[0] ?? null;
395
+
396
+ for (const state of ['open', 'closed']) {
397
+ for (const owner of sourceOwners) {
398
+ const directCandidates = await safeListPulls(octokit, {
399
+ owner: target.repo.owner,
400
+ repo: target.repo.repo,
401
+ state,
402
+ head: `${owner}:${branch}`,
403
+ per_page: 100,
404
+ });
405
+ const direct = pickPreferred(directCandidates);
406
+ if (direct) {
407
+ return direct;
408
+ }
409
+ }
410
+
411
+ const fallbackCandidates = await safeListPulls(octokit, {
412
+ owner: target.repo.owner,
413
+ repo: target.repo.repo,
414
+ state,
415
+ per_page: 100,
416
+ });
417
+ const fallback = pickPreferred(fallbackCandidates);
418
+ if (fallback) {
419
+ return fallback;
420
+ }
421
+ }
422
+
423
+ return null;
424
+ };
425
+
426
+ export async function resolveGitHubPrStatus({ octokit, directory, branch, remoteName }) {
427
+ const normalizedBranch = normalizeText(branch);
428
+ const normalizedRemoteName = normalizeText(remoteName) || 'origin';
429
+
430
+ const [status, remotes] = await Promise.all([
431
+ getStatus(directory).catch(() => null),
432
+ getRemotes(directory).catch(() => []),
433
+ ]);
434
+
435
+ const trackingRemoteName = parseTrackingRemoteName(status?.tracking);
436
+ const trackingBranchName = parseTrackingBranchName(status?.tracking);
437
+ const branchCandidates = [];
438
+ pushUnique(branchCandidates, normalizedBranch);
439
+ pushUnique(branchCandidates, trackingBranchName);
440
+ const rankedRemoteNames = rankRemoteNames(
441
+ Array.isArray(remotes) ? remotes.map((remote) => remote?.name).filter(Boolean) : [],
442
+ normalizedRemoteName,
443
+ trackingRemoteName,
444
+ );
445
+
446
+ const resolvedRemoteTargets = await resolveRemoteCandidates(directory, rankedRemoteNames);
447
+ const resolvedTargets = await expandRepoNetwork(
448
+ octokit,
449
+ resolvedRemoteTargets.map((target, index) => ({ ...target, priority: index })),
450
+ );
451
+ if (resolvedTargets.length === 0) {
452
+ return {
453
+ repo: null,
454
+ pr: null,
455
+ defaultBranch: null,
456
+ resolvedRemoteName: null,
457
+ };
458
+ }
459
+
460
+ const sourceCandidates = resolvedTargets.slice();
461
+
462
+ let fallbackRepo = resolvedTargets[0].repo;
463
+ let fallbackRemoteName = resolvedTargets[0].remoteName;
464
+ let fallbackDefaultBranch = await getRepoDefaultBranch(octokit, fallbackRepo);
465
+
466
+ for (const target of resolvedTargets) {
467
+ const defaultBranch = await getRepoDefaultBranch(octokit, target.repo);
468
+ if (!fallbackRepo) {
469
+ fallbackRepo = target.repo;
470
+ fallbackRemoteName = target.remoteName;
471
+ fallbackDefaultBranch = defaultBranch;
472
+ }
473
+
474
+ const hasCrossRepoSource = sourceCandidates.some((candidate) => normalizeRepoKey(candidate.repo?.owner, candidate.repo?.repo) !== normalizeRepoKey(target.repo?.owner, target.repo?.repo));
475
+ for (const candidateBranch of branchCandidates) {
476
+ if (defaultBranch && defaultBranch === candidateBranch && !hasCrossRepoSource) {
477
+ continue;
478
+ }
479
+
480
+ const pr = await findFirstMatchingPr({
481
+ octokit,
482
+ target,
483
+ branch: candidateBranch,
484
+ sourceCandidates,
485
+ });
486
+ if (pr) {
487
+ return {
488
+ repo: target.repo,
489
+ pr,
490
+ defaultBranch,
491
+ resolvedRemoteName: target.remoteName,
492
+ };
493
+ }
494
+ }
495
+ }
496
+
497
+ for (const candidateBranch of branchCandidates) {
498
+ const fallbackSearch = await searchFallbackPr({
499
+ octokit,
500
+ branch: candidateBranch,
501
+ repoNames: resolvedTargets.map((target) => target.repo.repo),
502
+ });
503
+ if (fallbackSearch) {
504
+ return {
505
+ repo: fallbackSearch.repo,
506
+ pr: fallbackSearch.pr,
507
+ defaultBranch: await getRepoDefaultBranch(octokit, fallbackSearch.repo),
508
+ resolvedRemoteName: null,
509
+ };
510
+ }
511
+ }
512
+
513
+ return {
514
+ repo: fallbackRepo,
515
+ pr: null,
516
+ defaultBranch: fallbackDefaultBranch,
517
+ resolvedRemoteName: fallbackRemoteName,
518
+ };
519
+ }
@@ -0,0 +1,102 @@
1
+ import { resolveGitHubRepoFromDirectory } from './index.js';
2
+
3
+ const REPO_METADATA_TTL_MS = 5 * 60_000;
4
+ const REPO_METADATA_CACHE_MAX_ENTRIES = 200;
5
+ const repoMetadataCache = new Map();
6
+
7
+ const setRepoMetadataCache = (repoKey, data) => {
8
+ if (repoMetadataCache.size >= REPO_METADATA_CACHE_MAX_ENTRIES && !repoMetadataCache.has(repoKey)) {
9
+ const oldest = repoMetadataCache.entries().next().value;
10
+ if (oldest) {
11
+ repoMetadataCache.delete(oldest[0]);
12
+ }
13
+ }
14
+ repoMetadataCache.set(repoKey, { data, fetchedAt: Date.now() });
15
+ };
16
+
17
+ const normalizeRepoKey = (owner, repo) => {
18
+ const o = typeof owner === 'string' ? owner.trim().toLowerCase() : '';
19
+ const r = typeof repo === 'string' ? repo.trim().toLowerCase() : '';
20
+ if (!o || !r) return '';
21
+ return `${o}/${r}`;
22
+ };
23
+
24
+ const getRepoMetadata = async (octokit, repo) => {
25
+ const repoKey = normalizeRepoKey(repo?.owner, repo?.repo);
26
+ if (!repoKey) return null;
27
+
28
+ const cached = repoMetadataCache.get(repoKey);
29
+ if (cached && Date.now() - cached.fetchedAt < REPO_METADATA_TTL_MS) {
30
+ return cached.data;
31
+ }
32
+
33
+ try {
34
+ const response = await octokit.rest.repos.get({
35
+ owner: repo.owner,
36
+ repo: repo.repo,
37
+ });
38
+ const data = response?.data ?? null;
39
+ setRepoMetadataCache(repoKey, data);
40
+ return data;
41
+ } catch (error) {
42
+ if (error?.status === 403 || error?.status === 404) {
43
+ setRepoMetadataCache(repoKey, null);
44
+ return null;
45
+ }
46
+ throw error;
47
+ }
48
+ };
49
+
50
+ /**
51
+ * Resolve the repo network for a directory. If the origin repo is a fork,
52
+ * includes the parent/source (upstream) repo in the result.
53
+ *
54
+ * @param {import('@octokit/rest').Octokit} octokit
55
+ * @param {string} directory
56
+ * @param {string} [remoteName='origin']
57
+ * @returns {Promise<Array<{ owner: string, repo: string, url: string, source: string }> | null>}
58
+ * Array of repos to query (origin first, then upstream), or null if not a fork.
59
+ */
60
+ export async function resolveRepoNetwork(octokit, directory, remoteName = 'origin') {
61
+ const { repo } = await resolveGitHubRepoFromDirectory(directory, remoteName).catch(() => ({ repo: null }));
62
+ if (!repo) return null;
63
+
64
+ const metadata = await getRepoMetadata(octokit, repo);
65
+ if (!metadata) return [{ ...repo, source: 'origin' }];
66
+
67
+ const result = [{ ...repo, source: 'origin' }];
68
+ const seenKeys = new Set([normalizeRepoKey(repo.owner, repo.repo)]);
69
+
70
+ const parent = metadata?.parent;
71
+ if (parent?.owner?.login && parent?.name) {
72
+ const key = normalizeRepoKey(parent.owner.login, parent.name);
73
+ if (!seenKeys.has(key)) {
74
+ seenKeys.add(key);
75
+ result.push({
76
+ owner: parent.owner.login,
77
+ repo: parent.name,
78
+ url: parent.html_url || `https://github.com/${parent.owner.login}/${parent.name}`,
79
+ source: 'upstream',
80
+ });
81
+ }
82
+ }
83
+
84
+ const source = metadata?.source;
85
+ if (source?.owner?.login && source?.name) {
86
+ const key = normalizeRepoKey(source.owner.login, source.name);
87
+ if (!seenKeys.has(key)) {
88
+ seenKeys.add(key);
89
+ result.push({
90
+ owner: source.owner.login,
91
+ repo: source.name,
92
+ url: source.html_url || `https://github.com/${source.owner.login}/${source.name}`,
93
+ source: 'upstream',
94
+ });
95
+ }
96
+ }
97
+
98
+ // If no parent/source found, repo is not a fork
99
+ if (result.length === 1) return null;
100
+
101
+ return result;
102
+ }
@@ -0,0 +1,55 @@
1
+ import { getRemoteUrl } from '../../git/index.js';
2
+
3
+ export const parseGitHubRemoteUrl = (raw) => {
4
+ if (typeof raw !== 'string') {
5
+ return null;
6
+ }
7
+ const value = raw.trim();
8
+ if (!value) {
9
+ return null;
10
+ }
11
+
12
+ // git@github.com:OWNER/REPO.git
13
+ if (value.startsWith('git@github.com:')) {
14
+ const rest = value.slice('git@github.com:'.length);
15
+ const cleaned = rest.endsWith('.git') ? rest.slice(0, -4) : rest;
16
+ const [owner, repo] = cleaned.split('/');
17
+ if (!owner || !repo) return null;
18
+ return { owner, repo, url: `https://github.com/${owner}/${repo}` };
19
+ }
20
+
21
+ // ssh://git@github.com/OWNER/REPO.git
22
+ if (value.startsWith('ssh://git@github.com/')) {
23
+ const rest = value.slice('ssh://git@github.com/'.length);
24
+ const cleaned = rest.endsWith('.git') ? rest.slice(0, -4) : rest;
25
+ const [owner, repo] = cleaned.split('/');
26
+ if (!owner || !repo) return null;
27
+ return { owner, repo, url: `https://github.com/${owner}/${repo}` };
28
+ }
29
+
30
+ // https://github.com/OWNER/REPO(.git)
31
+ try {
32
+ const url = new URL(value);
33
+ if (url.hostname !== 'github.com') {
34
+ return null;
35
+ }
36
+ const path = url.pathname.replace(/^\/+/, '').replace(/\/+$/, '');
37
+ const cleaned = path.endsWith('.git') ? path.slice(0, -4) : path;
38
+ const [owner, repo] = cleaned.split('/');
39
+ if (!owner || !repo) return null;
40
+ return { owner, repo, url: `https://github.com/${owner}/${repo}` };
41
+ } catch {
42
+ return null;
43
+ }
44
+ };
45
+
46
+ export async function resolveGitHubRepoFromDirectory(directory, remoteName = 'origin') {
47
+ const remoteUrl = await getRemoteUrl(directory, remoteName).catch(() => null);
48
+ if (!remoteUrl) {
49
+ return { repo: null, remoteUrl: null };
50
+ }
51
+ return {
52
+ repo: parseGitHubRemoteUrl(remoteUrl),
53
+ remoteUrl,
54
+ };
55
+ }