synabun 2.0.5 → 2.0.9

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 (527) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +2 -1
  3. package/THIRD-PARTY-LICENSES.md +13 -0
  4. package/docs/whatsapp.md +151 -16
  5. package/hooks/claude-code/post-plan.mjs +6 -30
  6. package/hooks/claude-code/session-start.mjs +1 -4
  7. package/hooks/claude-code/shared.mjs +112 -75
  8. package/lib/child-path.js +303 -0
  9. package/lib/claude-hooks.js +2 -1
  10. package/lib/data-home-migration.js +2 -0
  11. package/lib/desktop-app.js +265 -0
  12. package/lib/external-tools.js +7 -3
  13. package/lib/log-redact.js +102 -0
  14. package/lib/package-processes.js +670 -0
  15. package/lib/packaged-runtime.js +138 -11
  16. package/lib/runtime-dependencies.js +225 -0
  17. package/mcp-server/dist/http.d.ts +3 -1
  18. package/mcp-server/dist/http.d.ts.map +1 -1
  19. package/mcp-server/dist/http.js +15 -4
  20. package/mcp-server/dist/http.js.map +1 -1
  21. package/mcp-server/dist/index.d.ts.map +1 -1
  22. package/mcp-server/dist/index.js +9 -6
  23. package/mcp-server/dist/index.js.map +1 -1
  24. package/mcp-server/dist/services/browser-bench.d.ts +1 -1
  25. package/mcp-server/dist/services/browser-bench.d.ts.map +1 -1
  26. package/mcp-server/dist/services/browser-bench.js +1 -1
  27. package/mcp-server/dist/services/browser-bench.js.map +1 -1
  28. package/mcp-server/dist/services/budget-judgments.d.ts +108 -0
  29. package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -0
  30. package/mcp-server/dist/services/budget-judgments.js +153 -0
  31. package/mcp-server/dist/services/budget-judgments.js.map +1 -0
  32. package/mcp-server/dist/services/categories.d.ts +35 -2
  33. package/mcp-server/dist/services/categories.d.ts.map +1 -1
  34. package/mcp-server/dist/services/categories.js +77 -33
  35. package/mcp-server/dist/services/categories.js.map +1 -1
  36. package/mcp-server/dist/services/codex-config-heal.d.ts +5 -1
  37. package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
  38. package/mcp-server/dist/services/codex-config-heal.js +41 -4
  39. package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
  40. package/mcp-server/dist/services/desktop-bench.d.ts +7 -7
  41. package/mcp-server/dist/services/desktop-bench.d.ts.map +1 -1
  42. package/mcp-server/dist/services/desktop-bench.js +1 -1
  43. package/mcp-server/dist/services/desktop-bench.js.map +1 -1
  44. package/mcp-server/dist/services/identity.d.ts +11 -0
  45. package/mcp-server/dist/services/identity.d.ts.map +1 -1
  46. package/mcp-server/dist/services/identity.js +24 -0
  47. package/mcp-server/dist/services/identity.js.map +1 -1
  48. package/mcp-server/dist/services/neural-interface.d.ts +13 -0
  49. package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
  50. package/mcp-server/dist/services/neural-interface.js +34 -2
  51. package/mcp-server/dist/services/neural-interface.js.map +1 -1
  52. package/mcp-server/dist/services/typesafe-config.d.ts +1 -1
  53. package/mcp-server/dist/services/typesafe-config.d.ts.map +1 -1
  54. package/mcp-server/dist/services/typesafe-config.js +4 -1
  55. package/mcp-server/dist/services/typesafe-config.js.map +1 -1
  56. package/mcp-server/dist/tools/agents.d.ts +11 -8
  57. package/mcp-server/dist/tools/agents.d.ts.map +1 -1
  58. package/mcp-server/dist/tools/agents.js +35 -14
  59. package/mcp-server/dist/tools/agents.js.map +1 -1
  60. package/mcp-server/dist/tools/bluesky.js +1 -1
  61. package/mcp-server/dist/tools/bluesky.js.map +1 -1
  62. package/mcp-server/dist/tools/browser-advanced.d.ts +1 -1
  63. package/mcp-server/dist/tools/browser-advanced.d.ts.map +1 -1
  64. package/mcp-server/dist/tools/browser-advanced.js +1 -1
  65. package/mcp-server/dist/tools/browser-advanced.js.map +1 -1
  66. package/mcp-server/dist/tools/browser-batch.d.ts +1 -1
  67. package/mcp-server/dist/tools/browser-batch.d.ts.map +1 -1
  68. package/mcp-server/dist/tools/browser-batch.js +1 -1
  69. package/mcp-server/dist/tools/browser-batch.js.map +1 -1
  70. package/mcp-server/dist/tools/browser-cheatsheet.d.ts +1 -1
  71. package/mcp-server/dist/tools/browser-cheatsheet.d.ts.map +1 -1
  72. package/mcp-server/dist/tools/browser-cheatsheet.js +1 -1
  73. package/mcp-server/dist/tools/browser-cheatsheet.js.map +1 -1
  74. package/mcp-server/dist/tools/browser-interact.d.ts +1 -1
  75. package/mcp-server/dist/tools/browser-interact.d.ts.map +1 -1
  76. package/mcp-server/dist/tools/browser-interact.js +1 -1
  77. package/mcp-server/dist/tools/browser-interact.js.map +1 -1
  78. package/mcp-server/dist/tools/browser-navigate.d.ts +1 -1
  79. package/mcp-server/dist/tools/browser-navigate.d.ts.map +1 -1
  80. package/mcp-server/dist/tools/browser-navigate.js +1 -1
  81. package/mcp-server/dist/tools/browser-navigate.js.map +1 -1
  82. package/mcp-server/dist/tools/browser-observe.d.ts +1 -1
  83. package/mcp-server/dist/tools/browser-observe.d.ts.map +1 -1
  84. package/mcp-server/dist/tools/browser-observe.js +1 -1
  85. package/mcp-server/dist/tools/browser-observe.js.map +1 -1
  86. package/mcp-server/dist/tools/card-tools.d.ts +1 -1
  87. package/mcp-server/dist/tools/card-tools.d.ts.map +1 -1
  88. package/mcp-server/dist/tools/card-tools.js +1 -1
  89. package/mcp-server/dist/tools/card-tools.js.map +1 -1
  90. package/mcp-server/dist/tools/category.d.ts +1 -1
  91. package/mcp-server/dist/tools/category.d.ts.map +1 -1
  92. package/mcp-server/dist/tools/category.js +5 -1
  93. package/mcp-server/dist/tools/category.js.map +1 -1
  94. package/mcp-server/dist/tools/choice.d.ts +1 -1
  95. package/mcp-server/dist/tools/choice.d.ts.map +1 -1
  96. package/mcp-server/dist/tools/choice.js +1 -1
  97. package/mcp-server/dist/tools/choice.js.map +1 -1
  98. package/mcp-server/dist/tools/computer.d.ts +1 -1
  99. package/mcp-server/dist/tools/computer.d.ts.map +1 -1
  100. package/mcp-server/dist/tools/computer.js +1 -1
  101. package/mcp-server/dist/tools/computer.js.map +1 -1
  102. package/mcp-server/dist/tools/discord-channel.d.ts +1 -1
  103. package/mcp-server/dist/tools/discord-channel.d.ts.map +1 -1
  104. package/mcp-server/dist/tools/discord-channel.js +1 -1
  105. package/mcp-server/dist/tools/discord-channel.js.map +1 -1
  106. package/mcp-server/dist/tools/discord-guild.d.ts +1 -1
  107. package/mcp-server/dist/tools/discord-guild.d.ts.map +1 -1
  108. package/mcp-server/dist/tools/discord-guild.js +1 -1
  109. package/mcp-server/dist/tools/discord-guild.js.map +1 -1
  110. package/mcp-server/dist/tools/discord-member.d.ts +1 -1
  111. package/mcp-server/dist/tools/discord-member.d.ts.map +1 -1
  112. package/mcp-server/dist/tools/discord-member.js +1 -1
  113. package/mcp-server/dist/tools/discord-member.js.map +1 -1
  114. package/mcp-server/dist/tools/discord-message.d.ts +1 -1
  115. package/mcp-server/dist/tools/discord-message.d.ts.map +1 -1
  116. package/mcp-server/dist/tools/discord-message.js +1 -1
  117. package/mcp-server/dist/tools/discord-message.js.map +1 -1
  118. package/mcp-server/dist/tools/discord-onboarding.d.ts +1 -1
  119. package/mcp-server/dist/tools/discord-onboarding.d.ts.map +1 -1
  120. package/mcp-server/dist/tools/discord-onboarding.js +1 -1
  121. package/mcp-server/dist/tools/discord-onboarding.js.map +1 -1
  122. package/mcp-server/dist/tools/discord-role.d.ts +1 -1
  123. package/mcp-server/dist/tools/discord-role.d.ts.map +1 -1
  124. package/mcp-server/dist/tools/discord-role.js +1 -1
  125. package/mcp-server/dist/tools/discord-role.js.map +1 -1
  126. package/mcp-server/dist/tools/discord-thread.d.ts +1 -1
  127. package/mcp-server/dist/tools/discord-thread.d.ts.map +1 -1
  128. package/mcp-server/dist/tools/discord-thread.js +1 -1
  129. package/mcp-server/dist/tools/discord-thread.js.map +1 -1
  130. package/mcp-server/dist/tools/discord-webhook.d.ts +1 -1
  131. package/mcp-server/dist/tools/discord-webhook.d.ts.map +1 -1
  132. package/mcp-server/dist/tools/discord-webhook.js +1 -1
  133. package/mcp-server/dist/tools/discord-webhook.js.map +1 -1
  134. package/mcp-server/dist/tools/fb-groups.d.ts +1 -1
  135. package/mcp-server/dist/tools/fb-groups.d.ts.map +1 -1
  136. package/mcp-server/dist/tools/fb-groups.js +1 -1
  137. package/mcp-server/dist/tools/fb-groups.js.map +1 -1
  138. package/mcp-server/dist/tools/forget.d.ts +1 -1
  139. package/mcp-server/dist/tools/forget.d.ts.map +1 -1
  140. package/mcp-server/dist/tools/forget.js +1 -1
  141. package/mcp-server/dist/tools/forget.js.map +1 -1
  142. package/mcp-server/dist/tools/git-tools.d.ts +1 -1
  143. package/mcp-server/dist/tools/git-tools.d.ts.map +1 -1
  144. package/mcp-server/dist/tools/git-tools.js +1 -1
  145. package/mcp-server/dist/tools/git-tools.js.map +1 -1
  146. package/mcp-server/dist/tools/gsc/experience.d.ts +1 -1
  147. package/mcp-server/dist/tools/gsc/experience.d.ts.map +1 -1
  148. package/mcp-server/dist/tools/gsc/experience.js +1 -1
  149. package/mcp-server/dist/tools/gsc/experience.js.map +1 -1
  150. package/mcp-server/dist/tools/gsc/indexing.d.ts +1 -1
  151. package/mcp-server/dist/tools/gsc/indexing.d.ts.map +1 -1
  152. package/mcp-server/dist/tools/gsc/indexing.js +1 -1
  153. package/mcp-server/dist/tools/gsc/indexing.js.map +1 -1
  154. package/mcp-server/dist/tools/gsc/inspect.d.ts +1 -1
  155. package/mcp-server/dist/tools/gsc/inspect.d.ts.map +1 -1
  156. package/mcp-server/dist/tools/gsc/inspect.js +1 -1
  157. package/mcp-server/dist/tools/gsc/inspect.js.map +1 -1
  158. package/mcp-server/dist/tools/gsc/navigate.d.ts +1 -1
  159. package/mcp-server/dist/tools/gsc/navigate.d.ts.map +1 -1
  160. package/mcp-server/dist/tools/gsc/navigate.js +1 -1
  161. package/mcp-server/dist/tools/gsc/navigate.js.map +1 -1
  162. package/mcp-server/dist/tools/gsc/performance.d.ts +1 -1
  163. package/mcp-server/dist/tools/gsc/performance.d.ts.map +1 -1
  164. package/mcp-server/dist/tools/gsc/performance.js +1 -1
  165. package/mcp-server/dist/tools/gsc/performance.js.map +1 -1
  166. package/mcp-server/dist/tools/gsc/settings.d.ts +1 -1
  167. package/mcp-server/dist/tools/gsc/settings.d.ts.map +1 -1
  168. package/mcp-server/dist/tools/gsc/settings.js +1 -1
  169. package/mcp-server/dist/tools/gsc/settings.js.map +1 -1
  170. package/mcp-server/dist/tools/image-tools.d.ts +1 -1
  171. package/mcp-server/dist/tools/image-tools.d.ts.map +1 -1
  172. package/mcp-server/dist/tools/image-tools.js +1 -1
  173. package/mcp-server/dist/tools/image-tools.js.map +1 -1
  174. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts +1 -1
  175. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts.map +1 -1
  176. package/mcp-server/dist/tools/leonardo-browser-tools.js +1 -1
  177. package/mcp-server/dist/tools/leonardo-browser-tools.js.map +1 -1
  178. package/mcp-server/dist/tools/loop.d.ts +1 -1
  179. package/mcp-server/dist/tools/loop.d.ts.map +1 -1
  180. package/mcp-server/dist/tools/loop.js +1 -1
  181. package/mcp-server/dist/tools/loop.js.map +1 -1
  182. package/mcp-server/dist/tools/memories.d.ts +1 -1
  183. package/mcp-server/dist/tools/memories.d.ts.map +1 -1
  184. package/mcp-server/dist/tools/memories.js +1 -1
  185. package/mcp-server/dist/tools/memories.js.map +1 -1
  186. package/mcp-server/dist/tools/morelogin.d.ts +1 -1
  187. package/mcp-server/dist/tools/morelogin.d.ts.map +1 -1
  188. package/mcp-server/dist/tools/morelogin.js +1 -1
  189. package/mcp-server/dist/tools/morelogin.js.map +1 -1
  190. package/mcp-server/dist/tools/profile.d.ts +1 -1
  191. package/mcp-server/dist/tools/profile.d.ts.map +1 -1
  192. package/mcp-server/dist/tools/profile.js +1 -1
  193. package/mcp-server/dist/tools/profile.js.map +1 -1
  194. package/mcp-server/dist/tools/recall.d.ts +1 -1
  195. package/mcp-server/dist/tools/recall.d.ts.map +1 -1
  196. package/mcp-server/dist/tools/recall.js +1 -1
  197. package/mcp-server/dist/tools/recall.js.map +1 -1
  198. package/mcp-server/dist/tools/reflect.d.ts +1 -1
  199. package/mcp-server/dist/tools/reflect.d.ts.map +1 -1
  200. package/mcp-server/dist/tools/reflect.js +1 -1
  201. package/mcp-server/dist/tools/reflect.js.map +1 -1
  202. package/mcp-server/dist/tools/remember.d.ts +1 -1
  203. package/mcp-server/dist/tools/remember.d.ts.map +1 -1
  204. package/mcp-server/dist/tools/remember.js +13 -11
  205. package/mcp-server/dist/tools/remember.js.map +1 -1
  206. package/mcp-server/dist/tools/restore.d.ts +1 -1
  207. package/mcp-server/dist/tools/restore.d.ts.map +1 -1
  208. package/mcp-server/dist/tools/restore.js +1 -1
  209. package/mcp-server/dist/tools/restore.js.map +1 -1
  210. package/mcp-server/dist/tools/style-guide-tools.d.ts +1 -1
  211. package/mcp-server/dist/tools/style-guide-tools.d.ts.map +1 -1
  212. package/mcp-server/dist/tools/style-guide-tools.js +1 -1
  213. package/mcp-server/dist/tools/style-guide-tools.js.map +1 -1
  214. package/mcp-server/dist/tools/sync.d.ts +1 -1
  215. package/mcp-server/dist/tools/sync.d.ts.map +1 -1
  216. package/mcp-server/dist/tools/sync.js +1 -1
  217. package/mcp-server/dist/tools/sync.js.map +1 -1
  218. package/mcp-server/dist/tools/tictactoe-tools.d.ts +1 -1
  219. package/mcp-server/dist/tools/tictactoe-tools.d.ts.map +1 -1
  220. package/mcp-server/dist/tools/tictactoe-tools.js +1 -1
  221. package/mcp-server/dist/tools/tictactoe-tools.js.map +1 -1
  222. package/mcp-server/dist/tools/utils.d.ts +1 -1
  223. package/mcp-server/dist/tools/utils.d.ts.map +1 -1
  224. package/mcp-server/dist/tools/utils.js +1 -1
  225. package/mcp-server/dist/tools/utils.js.map +1 -1
  226. package/mcp-server/dist/tools/whatsapp-send-file.d.ts +17 -0
  227. package/mcp-server/dist/tools/whatsapp-send-file.d.ts.map +1 -0
  228. package/mcp-server/dist/tools/whatsapp-send-file.js +44 -0
  229. package/mcp-server/dist/tools/whatsapp-send-file.js.map +1 -0
  230. package/mcp-server/dist/tools/whiteboard-tools.d.ts +3 -3
  231. package/mcp-server/dist/tools/whiteboard-tools.d.ts.map +1 -1
  232. package/mcp-server/dist/tools/whiteboard-tools.js +1 -1
  233. package/mcp-server/dist/tools/whiteboard-tools.js.map +1 -1
  234. package/mcp-server/dist/tools/youtube/analytics.d.ts +1 -1
  235. package/mcp-server/dist/tools/youtube/analytics.d.ts.map +1 -1
  236. package/mcp-server/dist/tools/youtube/analytics.js +1 -1
  237. package/mcp-server/dist/tools/youtube/analytics.js.map +1 -1
  238. package/mcp-server/dist/tools/youtube/download.d.ts +1 -1
  239. package/mcp-server/dist/tools/youtube/download.d.ts.map +1 -1
  240. package/mcp-server/dist/tools/youtube/download.js +1 -1
  241. package/mcp-server/dist/tools/youtube/download.js.map +1 -1
  242. package/mcp-server/dist/tools/youtube/media.d.ts +1 -1
  243. package/mcp-server/dist/tools/youtube/media.d.ts.map +1 -1
  244. package/mcp-server/dist/tools/youtube/media.js +1 -1
  245. package/mcp-server/dist/tools/youtube/media.js.map +1 -1
  246. package/mcp-server/dist/tools/youtube/metadata.d.ts +1 -1
  247. package/mcp-server/dist/tools/youtube/metadata.d.ts.map +1 -1
  248. package/mcp-server/dist/tools/youtube/metadata.js +1 -1
  249. package/mcp-server/dist/tools/youtube/metadata.js.map +1 -1
  250. package/mcp-server/dist/tools/youtube/navigate.d.ts +1 -1
  251. package/mcp-server/dist/tools/youtube/navigate.d.ts.map +1 -1
  252. package/mcp-server/dist/tools/youtube/navigate.js +1 -1
  253. package/mcp-server/dist/tools/youtube/navigate.js.map +1 -1
  254. package/mcp-server/dist/tools/youtube/playlist.d.ts +1 -1
  255. package/mcp-server/dist/tools/youtube/playlist.d.ts.map +1 -1
  256. package/mcp-server/dist/tools/youtube/playlist.js +1 -1
  257. package/mcp-server/dist/tools/youtube/playlist.js.map +1 -1
  258. package/mcp-server/dist/tools/youtube/schedule.d.ts +1 -1
  259. package/mcp-server/dist/tools/youtube/schedule.d.ts.map +1 -1
  260. package/mcp-server/dist/tools/youtube/schedule.js +1 -1
  261. package/mcp-server/dist/tools/youtube/schedule.js.map +1 -1
  262. package/mcp-server/dist/tools/youtube/source.d.ts +1 -1
  263. package/mcp-server/dist/tools/youtube/source.d.ts.map +1 -1
  264. package/mcp-server/dist/tools/youtube/source.js +1 -1
  265. package/mcp-server/dist/tools/youtube/source.js.map +1 -1
  266. package/mcp-server/dist/tools/youtube/upload.d.ts +1 -1
  267. package/mcp-server/dist/tools/youtube/upload.d.ts.map +1 -1
  268. package/mcp-server/dist/tools/youtube/upload.js +4 -9
  269. package/mcp-server/dist/tools/youtube/upload.js.map +1 -1
  270. package/mcp-server/dist/tui.js +18 -5
  271. package/mcp-server/dist/tui.js.map +1 -1
  272. package/mcp-server/dist/version.d.ts +5 -0
  273. package/mcp-server/dist/version.d.ts.map +1 -0
  274. package/mcp-server/dist/version.js +21 -0
  275. package/mcp-server/dist/version.js.map +1 -0
  276. package/mcp-server/package-lock.json +3 -3
  277. package/mcp-server/package.json +3 -3
  278. package/mcp-server/run.mjs +15 -9
  279. package/mcp-server/src/http.ts +18 -3
  280. package/mcp-server/src/index.ts +9 -6
  281. package/mcp-server/src/services/browser-bench.ts +1 -1
  282. package/mcp-server/src/services/budget-judgments.ts +183 -0
  283. package/mcp-server/src/services/categories.ts +77 -32
  284. package/mcp-server/src/services/codex-config-heal.ts +35 -4
  285. package/mcp-server/src/services/desktop-bench.ts +1 -1
  286. package/mcp-server/src/services/identity.ts +27 -0
  287. package/mcp-server/src/services/neural-interface.ts +34 -2
  288. package/mcp-server/src/services/typesafe-config.ts +4 -1
  289. package/mcp-server/src/tools/agents.ts +34 -15
  290. package/mcp-server/src/tools/bluesky.ts +1 -1
  291. package/mcp-server/src/tools/browser-advanced.ts +1 -1
  292. package/mcp-server/src/tools/browser-batch.ts +1 -1
  293. package/mcp-server/src/tools/browser-cheatsheet.ts +1 -1
  294. package/mcp-server/src/tools/browser-interact.ts +1 -1
  295. package/mcp-server/src/tools/browser-navigate.ts +1 -1
  296. package/mcp-server/src/tools/browser-observe.ts +1 -1
  297. package/mcp-server/src/tools/card-tools.ts +1 -1
  298. package/mcp-server/src/tools/category.ts +6 -1
  299. package/mcp-server/src/tools/choice.ts +1 -1
  300. package/mcp-server/src/tools/computer.ts +1 -1
  301. package/mcp-server/src/tools/discord-channel.ts +1 -1
  302. package/mcp-server/src/tools/discord-guild.ts +1 -1
  303. package/mcp-server/src/tools/discord-member.ts +1 -1
  304. package/mcp-server/src/tools/discord-message.ts +1 -1
  305. package/mcp-server/src/tools/discord-onboarding.ts +1 -1
  306. package/mcp-server/src/tools/discord-role.ts +1 -1
  307. package/mcp-server/src/tools/discord-thread.ts +1 -1
  308. package/mcp-server/src/tools/discord-webhook.ts +1 -1
  309. package/mcp-server/src/tools/fb-groups.ts +1 -1
  310. package/mcp-server/src/tools/forget.ts +1 -1
  311. package/mcp-server/src/tools/git-tools.ts +1 -1
  312. package/mcp-server/src/tools/gsc/experience.ts +1 -1
  313. package/mcp-server/src/tools/gsc/indexing.ts +1 -1
  314. package/mcp-server/src/tools/gsc/inspect.ts +1 -1
  315. package/mcp-server/src/tools/gsc/navigate.ts +1 -1
  316. package/mcp-server/src/tools/gsc/performance.ts +1 -1
  317. package/mcp-server/src/tools/gsc/settings.ts +1 -1
  318. package/mcp-server/src/tools/image-tools.ts +1 -1
  319. package/mcp-server/src/tools/leonardo-browser-tools.ts +1 -1
  320. package/mcp-server/src/tools/loop.ts +1 -1
  321. package/mcp-server/src/tools/memories.ts +1 -1
  322. package/mcp-server/src/tools/morelogin.ts +1 -1
  323. package/mcp-server/src/tools/profile.ts +1 -1
  324. package/mcp-server/src/tools/recall.ts +1 -1
  325. package/mcp-server/src/tools/reflect.ts +1 -1
  326. package/mcp-server/src/tools/remember.ts +13 -12
  327. package/mcp-server/src/tools/restore.ts +1 -1
  328. package/mcp-server/src/tools/style-guide-tools.ts +1 -1
  329. package/mcp-server/src/tools/sync.ts +1 -1
  330. package/mcp-server/src/tools/tictactoe-tools.ts +1 -1
  331. package/mcp-server/src/tools/utils.ts +1 -1
  332. package/mcp-server/src/tools/whatsapp-send-file.ts +51 -0
  333. package/mcp-server/src/tools/whiteboard-tools.ts +1 -1
  334. package/mcp-server/src/tools/youtube/analytics.ts +1 -1
  335. package/mcp-server/src/tools/youtube/download.ts +1 -1
  336. package/mcp-server/src/tools/youtube/media.ts +1 -1
  337. package/mcp-server/src/tools/youtube/metadata.ts +1 -1
  338. package/mcp-server/src/tools/youtube/navigate.ts +1 -1
  339. package/mcp-server/src/tools/youtube/playlist.ts +1 -1
  340. package/mcp-server/src/tools/youtube/schedule.ts +1 -1
  341. package/mcp-server/src/tools/youtube/source.ts +1 -1
  342. package/mcp-server/src/tools/youtube/upload.ts +4 -12
  343. package/mcp-server/src/tui.ts +24 -10
  344. package/mcp-server/src/version.ts +22 -0
  345. package/neural-interface/i18n/en.json +645 -57
  346. package/neural-interface/i18n/pt-BR.json +644 -56
  347. package/neural-interface/lib/account-usage.js +163 -0
  348. package/neural-interface/lib/agent-sandbox-elevation.js +102 -0
  349. package/neural-interface/lib/agent-sandbox-setting.js +57 -0
  350. package/neural-interface/lib/assistant-api.js +274 -52
  351. package/neural-interface/lib/assistant-brains/claude.js +15 -2
  352. package/neural-interface/lib/assistant-brains/codex.js +3 -0
  353. package/neural-interface/lib/assistant-brains/opencode.js +17 -4
  354. package/neural-interface/lib/assistant-budget-planner.js +390 -0
  355. package/neural-interface/lib/assistant-budget-split.js +408 -0
  356. package/neural-interface/lib/assistant-budget.js +94 -95
  357. package/neural-interface/lib/assistant-config.js +62 -14
  358. package/neural-interface/lib/assistant-dispatch.js +432 -125
  359. package/neural-interface/lib/assistant-jev.js +26 -3
  360. package/neural-interface/lib/assistant-persona.js +68 -11
  361. package/neural-interface/lib/assistant-plan-permissions.js +8 -1
  362. package/neural-interface/lib/assistant-route-accounts.js +14 -0
  363. package/neural-interface/lib/assistant-route-gate-codex.js +3 -1
  364. package/neural-interface/lib/assistant-route-gate.js +19 -3
  365. package/neural-interface/lib/assistant-router.js +373 -42
  366. package/neural-interface/lib/assistant-runtime.js +312 -48
  367. package/neural-interface/lib/assistant-user-writer.js +4 -2
  368. package/neural-interface/lib/augmented-path.js +317 -53
  369. package/neural-interface/lib/claude-accounts.js +342 -31
  370. package/neural-interface/lib/claude-agent-bridge.js +90 -3
  371. package/neural-interface/lib/claude-executable.js +6 -1
  372. package/neural-interface/lib/claude-model-catalog.js +3 -3
  373. package/neural-interface/lib/claude-panel-auth.js +221 -0
  374. package/neural-interface/lib/claude-profile-api-key.js +62 -0
  375. package/neural-interface/lib/cli-install-api.js +137 -0
  376. package/neural-interface/lib/cli-install.js +797 -0
  377. package/neural-interface/lib/cli-path.js +130 -0
  378. package/neural-interface/lib/cli-resolver.js +744 -0
  379. package/neural-interface/lib/cli-update-planner.js +143 -7
  380. package/neural-interface/lib/codex-account-auth.js +180 -0
  381. package/neural-interface/lib/codex-accounts.js +502 -0
  382. package/neural-interface/lib/codex-capabilities.js +2 -1
  383. package/neural-interface/lib/codex-compat-rpc.js +8 -2
  384. package/neural-interface/lib/codex-mcp-approvals.js +5 -1
  385. package/neural-interface/lib/codex-panel-auth.js +62 -0
  386. package/neural-interface/lib/codex-session-settings.js +1 -1
  387. package/neural-interface/lib/codex-settings-schema.js +122 -0
  388. package/neural-interface/lib/desktop/config.js +3 -0
  389. package/neural-interface/lib/exec-loop-command.js +3 -1
  390. package/neural-interface/lib/git-bash/bzip2.js +339 -0
  391. package/neural-interface/lib/git-bash/claude-settings.js +73 -0
  392. package/neural-interface/lib/git-bash/extract-worker.js +34 -0
  393. package/neural-interface/lib/git-bash/install.js +529 -0
  394. package/neural-interface/lib/git-bash/licenses/COPYING +360 -0
  395. package/neural-interface/lib/git-bash/licenses/LGPL-2.1 +511 -0
  396. package/neural-interface/lib/git-bash/manager.js +268 -0
  397. package/neural-interface/lib/git-bash/pins.js +206 -0
  398. package/neural-interface/lib/git-bash/pins.json +34 -0
  399. package/neural-interface/lib/git-bash/resolver.js +194 -0
  400. package/neural-interface/lib/git-read.js +34 -2
  401. package/neural-interface/lib/install-details.js +163 -0
  402. package/neural-interface/lib/mcp-installer.js +14 -2
  403. package/neural-interface/lib/native-loop-providers.js +22 -4
  404. package/neural-interface/lib/native-loop-runtime.js +9 -4
  405. package/neural-interface/lib/os-path.js +319 -0
  406. package/neural-interface/lib/provider-panel-api-key-auth.js +92 -0
  407. package/neural-interface/lib/provider-panel-terminal-auth.js +96 -0
  408. package/neural-interface/lib/pty-host/core.js +2 -1
  409. package/neural-interface/lib/pty-host/manager.js +27 -2
  410. package/neural-interface/lib/remote-policy.js +20 -11
  411. package/neural-interface/lib/session-indexer.js +3 -1
  412. package/neural-interface/lib/session-title-generator.js +6 -3
  413. package/neural-interface/lib/system-about.js +340 -0
  414. package/neural-interface/lib/system-activity.js +66 -0
  415. package/neural-interface/lib/terminal-env.js +11 -3
  416. package/neural-interface/lib/terminal-path.js +55 -0
  417. package/neural-interface/lib/virtual-socket.js +12 -1
  418. package/neural-interface/lib/whatsapp/baileys-adapter.js +165 -45
  419. package/neural-interface/lib/whatsapp/bridge.js +78 -34
  420. package/neural-interface/lib/whatsapp/commands.js +13 -14
  421. package/neural-interface/lib/whatsapp/config.js +134 -20
  422. package/neural-interface/lib/whatsapp/fake-baileys.js +99 -14
  423. package/neural-interface/lib/whatsapp/host-core.js +168 -35
  424. package/neural-interface/lib/whatsapp/inbound.js +5 -3
  425. package/neural-interface/lib/whatsapp/local-file.js +524 -0
  426. package/neural-interface/lib/whatsapp/manager.js +92 -32
  427. package/neural-interface/lib/whatsapp/paths.js +11 -1
  428. package/neural-interface/lib/whatsapp/protocol.js +65 -5
  429. package/neural-interface/lib/whatsapp/redact.js +63 -0
  430. package/neural-interface/lib/whatsapp/service.js +343 -91
  431. package/neural-interface/lib/whatsapp/transcription.js +463 -0
  432. package/neural-interface/package-lock.json +2 -2
  433. package/neural-interface/package.json +1 -1
  434. package/neural-interface/public/index.html +2 -0
  435. package/neural-interface/public/index2d.html +2 -0
  436. package/neural-interface/public/offline.html +16 -1
  437. package/neural-interface/public/onboarding.html +96 -2
  438. package/neural-interface/public/shared/account-i18n.js +7 -0
  439. package/neural-interface/public/shared/account-menu.js +542 -0
  440. package/neural-interface/public/shared/account-usage.js +79 -0
  441. package/neural-interface/public/shared/account-view.js +336 -0
  442. package/neural-interface/public/shared/agent-runtime-options.js +28 -8
  443. package/neural-interface/public/shared/assistant/asst-account-menu.js +298 -0
  444. package/neural-interface/public/shared/assistant/asst-accounts.js +182 -184
  445. package/neural-interface/public/shared/assistant/asst-api.js +39 -2
  446. package/neural-interface/public/shared/assistant/asst-brain-picker.js +288 -32
  447. package/neural-interface/public/shared/assistant/asst-budget.js +259 -303
  448. package/neural-interface/public/shared/assistant/asst-mascot.js +126 -7
  449. package/neural-interface/public/shared/assistant/asst-menu.js +219 -80
  450. package/neural-interface/public/shared/assistant/asst-model-chip.js +135 -0
  451. package/neural-interface/public/shared/assistant/asst-panel.js +87 -16
  452. package/neural-interface/public/shared/assistant/asst-render.js +38 -7
  453. package/neural-interface/public/shared/assistant/asst-route.js +72 -14
  454. package/neural-interface/public/shared/assistant/asst-routes-editor.js +45 -6
  455. package/neural-interface/public/shared/assistant/asst-slash.js +1 -1
  456. package/neural-interface/public/shared/assistant/asst-state.js +1 -1
  457. package/neural-interface/public/shared/assistant/asst-styles.js +160 -29
  458. package/neural-interface/public/shared/cdx/cdx-commands.js +4 -1
  459. package/neural-interface/public/shared/cdx/cdx-features.js +22 -14
  460. package/neural-interface/public/shared/cdx/cdx-icons.js +2 -0
  461. package/neural-interface/public/shared/cdx/cdx-panel.js +49 -108
  462. package/neural-interface/public/shared/cdx/cdx-settings.js +231 -0
  463. package/neural-interface/public/shared/cdx/cdx-styles.js +172 -111
  464. package/neural-interface/public/shared/cdx/cdx-tabs.js +264 -664
  465. package/neural-interface/public/shared/cdx/cdx-ws.js +2 -0
  466. package/neural-interface/public/shared/claude-account-view.js +3 -0
  467. package/neural-interface/public/shared/cli-install-view.js +392 -0
  468. package/neural-interface/public/shared/cli-status.js +282 -22
  469. package/neural-interface/public/shared/cp/cp-events.js +2 -1
  470. package/neural-interface/public/shared/cp/cp-styles.js +46 -0
  471. package/neural-interface/public/shared/desktop-bridge.js +39 -0
  472. package/neural-interface/public/shared/desktop-titlebar-page.js +59 -0
  473. package/neural-interface/public/shared/desktop-titlebar-view.js +122 -0
  474. package/neural-interface/public/shared/desktop-titlebar.js +153 -0
  475. package/neural-interface/public/shared/desktop-update-view.js +462 -0
  476. package/neural-interface/public/shared/editor/editor-panel.js +1 -1
  477. package/neural-interface/public/shared/explorer/fx-main.js +1 -1
  478. package/neural-interface/public/shared/git-bash-view.js +145 -0
  479. package/neural-interface/public/shared/html-shell.js +1 -0
  480. package/neural-interface/public/shared/i18n.js +19 -1
  481. package/neural-interface/public/shared/install-details-view.js +86 -0
  482. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +4 -1
  483. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +36 -1
  484. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +17 -0
  485. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +169 -2
  486. package/neural-interface/public/shared/pwa-install-page.js +3 -1
  487. package/neural-interface/public/shared/settings/settings-about-pane.js +271 -0
  488. package/neural-interface/public/shared/settings/settings-about.js +254 -0
  489. package/neural-interface/public/shared/settings/settings-ia.js +15 -0
  490. package/neural-interface/public/shared/showcase-privacy.js +490 -0
  491. package/neural-interface/public/shared/storage.js +30 -3
  492. package/neural-interface/public/shared/styles.css +281 -32
  493. package/neural-interface/public/shared/synabun-mascot.js +48 -2
  494. package/neural-interface/public/shared/ui-claude-panel.js +262 -217
  495. package/neural-interface/public/shared/ui-cli-banner.js +355 -0
  496. package/neural-interface/public/shared/ui-explorer.js +2 -0
  497. package/neural-interface/public/shared/ui-git-bash.js +124 -0
  498. package/neural-interface/public/shared/ui-judgments.js +10 -1
  499. package/neural-interface/public/shared/ui-loading.js +30 -6
  500. package/neural-interface/public/shared/ui-menubar.js +13 -3
  501. package/neural-interface/public/shared/ui-navbar.js +3 -1
  502. package/neural-interface/public/shared/ui-provider-auth.js +104 -0
  503. package/neural-interface/public/shared/ui-provider-terminal-auth.js +223 -0
  504. package/neural-interface/public/shared/ui-sessions.js +9 -3
  505. package/neural-interface/public/shared/ui-settings.js +55 -3
  506. package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -1
  507. package/neural-interface/public/shared/ui-sync.js +8 -0
  508. package/neural-interface/public/shared/ui-terminal.js +17 -0
  509. package/neural-interface/public/shared/ui-tool-updates.js +5 -0
  510. package/neural-interface/public/shared/ui-update.js +598 -7
  511. package/neural-interface/public/shared/ui-whatsapp.js +21 -6
  512. package/neural-interface/public/shared/whatsapp/wa-view.js +51 -8
  513. package/neural-interface/public/shared/worker-end.js +2 -0
  514. package/neural-interface/public/sw.js +1 -1
  515. package/neural-interface/public/variant/2d/main.js +10 -1
  516. package/neural-interface/public/variant/3d/main.js +10 -1
  517. package/neural-interface/scripts/rebuild-pty.js +14 -2
  518. package/neural-interface/server.js +1384 -679
  519. package/npm-shrinkwrap.json +4424 -0
  520. package/package.json +57 -2
  521. package/postinstall.js +61 -47
  522. package/preuninstall.js +8 -35
  523. package/setup.js +80 -47
  524. package/skills/synabun/modules/idea.md +1 -1
  525. package/skills/synabun/modules/schedule.md +1 -1
  526. package/updater.cmd +2 -2
  527. package/updater.mjs +97 -3
@@ -0,0 +1,797 @@
1
+ // ═══════════════════════════════════════════
2
+ // SynaBun — install a missing CLI from its vendor
3
+ // ═══════════════════════════════════════════
4
+ //
5
+ // What the side panels' Install button runs (POST /api/cli/install/:tool,
6
+ // lib/cli-install-api.js) when Claude Code, Codex or OpenCode is not installed.
7
+ // Each tool is installed the way its vendor documents for the system, from the
8
+ // vendor's own source, with nothing else needed (no Node, npm, Homebrew or
9
+ // Scoop). The sources are fixed here; a request only names the tool.
10
+ //
11
+ // Claude Code Anthropic's native installer: https://claude.ai/install.sh
12
+ // (bash) on macOS and Linux, https://claude.ai/install.ps1
13
+ // (powershell.exe -NoProfile -ExecutionPolicy Bypass -File) on
14
+ // Windows. Both check the binary they download against the
15
+ // SHA-256 of Anthropic's signed release manifest.
16
+ // Codex OpenAI's standalone installer (learn.chatgpt.com/docs/codex/cli),
17
+ // the install.sh / install.ps1 published with every
18
+ // openai/codex release: taken from the latest release and
19
+ // checked against the SHA-256 GitHub publishes for it, or, when
20
+ // GitHub's API does not answer, from https://chatgpt.com/codex/
21
+ // (the same file). The installer checks the package it fetches.
22
+ // OpenCode the install script https://opencode.ai/install (bash) on
23
+ // macOS and Linux. On Windows, where that script does not run:
24
+ // the newest OpenCode 1 release archive for the processor
25
+ // (opencode-windows-x64-baseline.zip, which runs on every x64
26
+ // CPU, or opencode-windows-arm64.zip) from the anomalyco/opencode
27
+ // GitHub releases, checked against its published SHA-256, its
28
+ // opencode.exe read out without an unzip program (the backups'
29
+ // streaming reader) and put in %USERPROFILE%\.opencode\bin, the
30
+ // folder the official installer uses (and the resolver looks in).
31
+ //
32
+ // Every download is HTTPS, every redirect too, and only to the vendor's own
33
+ // hosts. Nothing runs as administrator; everything lands in the user's own
34
+ // folders. Install consents to adding the resolved CLI's folder to persistent
35
+ // PATH when the vendor did not do so. That is a visible final step, using the
36
+ // same writer as Add to PATH; a failed PATH write leaves the CLI installed and
37
+ // reports a warning. Installers run without a terminal and without questions
38
+ // (CODEX_NON_INTERACTIVE=1, PowerShell -NonInteractive).
39
+ //
40
+ // One install per tool at a time. Every failure is a CliInstallError with a
41
+ // `code` the page has words for. Nothing here throws to a caller of start().
42
+
43
+ import { spawn } from 'node:child_process';
44
+ import { createHash, randomBytes } from 'node:crypto';
45
+ import { createWriteStream, existsSync, mkdirSync, rmSync, renameSync, unlinkSync } from 'node:fs';
46
+ import { mkdtemp, open } from 'node:fs/promises';
47
+ import os from 'node:os';
48
+ import path from 'node:path';
49
+ import { openZipArchive } from './backup-zip-reader.js';
50
+ import { buildInstallCommand } from './cli-update-planner.js';
51
+ import { packagedPathLast, rememberedPackagedPath, withPathEnv } from './augmented-path.js';
52
+
53
+ export const CLI_INSTALL_ERROR_CODES = Object.freeze([
54
+ 'unsupported', // no official one-click method for this system
55
+ 'busy', // an install of this tool is already running
56
+ 'offline', // the vendor's server was not reached
57
+ 'http', // it answered, but not with the file
58
+ 'checksum', // what arrived is not the published file
59
+ 'interrupted', // the transfer ended early
60
+ 'timeout', // nothing arrived for too long, or the installer ran too long
61
+ 'installer-failed', // the vendor's installer exited with an error
62
+ 'not-found-after-install', // it finished, but no working CLI is found
63
+ 'unpack', // the archive did not hold the program
64
+ 'disk', // a file could not be written
65
+ 'cancelled', // stopped (SynaBun shutting down)
66
+ 'internal', // anything else
67
+ ]);
68
+
69
+ export class CliInstallError extends Error {
70
+ constructor(code, message, { cause, detail } = {}) {
71
+ super(message, cause ? { cause } : undefined);
72
+ this.name = 'CliInstallError';
73
+ this.code = CLI_INSTALL_ERROR_CODES.includes(code) ? code : 'internal';
74
+ if (detail !== undefined) this.detail = detail;
75
+ }
76
+ }
77
+
78
+ /** The vendor's install page for each tool (the banner's "Install guide"). */
79
+ export const CLI_INSTALL_GUIDES = Object.freeze({
80
+ 'claude-code': 'https://code.claude.com/docs/en/setup',
81
+ codex: 'https://learn.chatgpt.com/docs/codex/cli',
82
+ opencode: 'https://opencode.ai/docs/',
83
+ gemini: 'https://github.com/google-gemini/gemini-cli',
84
+ });
85
+
86
+ const GITHUB_DOWNLOAD_HOSTS = ['github.com', 'objects.githubusercontent.com', 'release-assets.githubusercontent.com', 'github-releases.githubusercontent.com'];
87
+ const MAX_SCRIPT_BYTES = 2 * 1024 * 1024;
88
+ const MAX_ARCHIVE_BYTES = 600 * 1024 * 1024;
89
+ const MAX_JSON_BYTES = 8 * 1024 * 1024;
90
+ const MAX_REDIRECTS = 6;
91
+ const LOG_KEEP = 200;
92
+ const SUPPORTED = { darwin: ['x64', 'arm64'], linux: ['x64', 'arm64'], win32: ['x64', 'arm64'] };
93
+
94
+ const hostIs = (...hosts) => (url) => hosts.includes(url.hostname);
95
+
96
+ /**
97
+ * The one-click method for a tool on this system, or null when there is none
98
+ * (Gemini CLI, a 32-bit or other system). Pure: tests ask it for every OS.
99
+ */
100
+ export function installPlanFor(toolKey, { platform = process.platform, arch = process.arch, env = process.env, home } = {}) {
101
+ if (!(SUPPORTED[platform] || []).includes(arch)) return null;
102
+ const windows = platform === 'win32';
103
+ const homeDir = home || (windows ? env.USERPROFILE : env.HOME) || os.homedir();
104
+ switch (toolKey) {
105
+ case 'claude-code':
106
+ return {
107
+ tool: toolKey,
108
+ kind: 'script',
109
+ interpreter: windows ? 'powershell' : 'bash',
110
+ url: windows ? 'https://claude.ai/install.ps1' : 'https://claude.ai/install.sh',
111
+ allow: hostIs('claude.ai', 'downloads.claude.ai'),
112
+ source: windows ? 'https://claude.ai/install.ps1' : 'https://claude.ai/install.sh',
113
+ label: 'Anthropic native installer',
114
+ env: {},
115
+ };
116
+ case 'codex':
117
+ return {
118
+ tool: toolKey,
119
+ kind: 'script',
120
+ interpreter: windows ? 'powershell' : 'sh',
121
+ url: windows ? 'https://chatgpt.com/codex/install.ps1' : 'https://chatgpt.com/codex/install.sh',
122
+ allow: hostIs('chatgpt.com', 'releases.openai.com'),
123
+ release: { repo: 'openai/codex', asset: windows ? 'install.ps1' : 'install.sh' },
124
+ source: windows ? 'https://chatgpt.com/codex/install.ps1' : 'https://chatgpt.com/codex/install.sh',
125
+ label: 'OpenAI standalone installer',
126
+ env: { CODEX_NON_INTERACTIVE: '1' },
127
+ };
128
+ case 'opencode':
129
+ if (windows) {
130
+ return {
131
+ tool: toolKey,
132
+ kind: 'archive',
133
+ interpreter: null,
134
+ repo: 'anomalyco/opencode',
135
+ tag: /^v1\.\d+\.\d+$/,
136
+ assets: arch === 'arm64' ? ['opencode-windows-arm64.zip'] : ['opencode-windows-x64-baseline.zip', 'opencode-windows-x64.zip'],
137
+ member: 'opencode.exe',
138
+ // The official installer's folder; $OPENCODE_INSTALL_DIR, which older installers honoured, when set.
139
+ dir: env.OPENCODE_INSTALL_DIR || path.win32.join(homeDir, '.opencode', 'bin'),
140
+ source: 'https://github.com/anomalyco/opencode/releases',
141
+ label: 'OpenCode release archive',
142
+ env: {},
143
+ };
144
+ }
145
+ return {
146
+ tool: toolKey,
147
+ kind: 'script',
148
+ interpreter: 'bash',
149
+ url: 'https://opencode.ai/install',
150
+ allow: (url) => url.hostname === 'opencode.ai' || (url.hostname === 'raw.githubusercontent.com' && url.pathname.startsWith('/anomalyco/opencode/')),
151
+ source: 'https://opencode.ai/install',
152
+ label: 'OpenCode install script',
153
+ env: {},
154
+ };
155
+ default:
156
+ return null;
157
+ }
158
+ }
159
+
160
+ /** What the status of a tool says about its one-click install. */
161
+ export function describeInstall(toolKey, options) {
162
+ const plan = installPlanFor(toolKey, options);
163
+ return plan
164
+ ? { available: true, method: plan.kind, source: plan.source, interpreter: plan.interpreter, label: plan.label }
165
+ : { available: false, method: null, source: null, interpreter: null, label: null };
166
+ }
167
+
168
+ // ── Transfers ───────────────────────────────────────────────────────────────
169
+
170
+ function errorCode(error) {
171
+ return error?.code || error?.cause?.code || null;
172
+ }
173
+
174
+ const DISK_CODES = new Set(['ENOSPC', 'EACCES', 'EPERM', 'EROFS', 'EDQUOT', 'EEXIST', 'EISDIR', 'ENOTDIR']);
175
+
176
+ function asInstallError(error, code, message) {
177
+ if (error instanceof CliInstallError) return error;
178
+ return new CliInstallError(code, message, { cause: error, detail: errorCode(error) || error?.message });
179
+ }
180
+
181
+ function nextHop(response, from, allow) {
182
+ const location = response.headers.get('location');
183
+ let next = null;
184
+ try { next = new URL(location, from); } catch { next = null; }
185
+ if (!location || !next) throw new CliInstallError('http', 'The download server sent a redirect without an address.');
186
+ if (next.protocol !== 'https:') throw new CliInstallError('http', `The download server redirected to an address that is not HTTPS (${next.protocol}); it was not followed.`);
187
+ if (!allow(next)) throw new CliInstallError('http', `The download server redirected to ${next.hostname}, which is not one of the vendor's own; it was not followed.`);
188
+ return next;
189
+ }
190
+
191
+ async function writeAll(handle, buffer) {
192
+ let at = 0;
193
+ while (at < buffer.length) at += (await handle.write(buffer, at, buffer.length - at)).bytesWritten;
194
+ }
195
+
196
+ /**
197
+ * Download `url` into `destination` (a new file), HTTPS only and only through
198
+ * hosts `allow` accepts. Checks the size and, when given, the SHA-256.
199
+ * Resolves { sha256, size, url } (the address the file came from).
200
+ */
201
+ export async function downloadFile({
202
+ url,
203
+ allow,
204
+ destination,
205
+ maxBytes,
206
+ expectedSize = null,
207
+ sha256 = null,
208
+ fetchImpl = fetch,
209
+ signal,
210
+ onProgress = () => {},
211
+ stallMs = 60_000,
212
+ accept = 'application/octet-stream',
213
+ }) {
214
+ let current;
215
+ try { current = new URL(url); } catch { throw new CliInstallError('internal', `Not an address: ${url}`); }
216
+ if (current.protocol !== 'https:' || !allow(current)) throw new CliInstallError('internal', `${url} is not one of the vendor's HTTPS addresses.`);
217
+ const stop = new AbortController();
218
+ const onAbort = () => stop.abort(new CliInstallError('cancelled', 'The download was stopped.'));
219
+ if (signal?.aborted) onAbort(); else signal?.addEventListener('abort', onAbort, { once: true });
220
+ let timer = null;
221
+ const alive = () => {
222
+ clearTimeout(timer);
223
+ timer = setTimeout(() => stop.abort(new CliInstallError('timeout', `Nothing arrived from ${current.hostname} for ${Math.round(stallMs / 1000)} seconds.`)), stallMs);
224
+ timer.unref?.();
225
+ };
226
+ let handle = null;
227
+ let made = false;
228
+ let answered = false;
229
+ let received = 0;
230
+ const hash = createHash('sha256');
231
+ try {
232
+ alive();
233
+ let response = null;
234
+ for (let hop = 0; ; hop++) {
235
+ response = await fetchImpl(current, { redirect: 'manual', signal: stop.signal, headers: { accept, 'accept-encoding': 'identity', 'user-agent': 'synabun' } });
236
+ if (response.status < 300 || response.status >= 400) break;
237
+ const next = nextHop(response, current, allow);
238
+ try { await response.body?.cancel(); } catch { /* nothing to drain */ }
239
+ if (hop >= MAX_REDIRECTS) throw new CliInstallError('http', 'The download server redirected too many times.');
240
+ current = next;
241
+ alive();
242
+ }
243
+ if (response.status !== 200 || !response.body) {
244
+ try { await response.body?.cancel(); } catch { /* nothing to drain */ }
245
+ throw new CliInstallError('http', `${current.hostname} answered ${response.status} instead of the file.`, { detail: String(response.status) });
246
+ }
247
+ const announced = Number(response.headers.get('content-length'));
248
+ if (Number.isFinite(announced) && announced > 0 && ((expectedSize && announced !== expectedSize) || announced > maxBytes)) {
249
+ try { await response.body.cancel(); } catch { /* nothing to drain */ }
250
+ throw new CliInstallError('checksum', `${current.hostname} offered ${announced} bytes${expectedSize ? `; the published file has ${expectedSize}` : ''}. It was not downloaded.`);
251
+ }
252
+ answered = true;
253
+ handle = await open(destination, 'wx', 0o600);
254
+ made = true;
255
+ const total = expectedSize || (Number.isFinite(announced) && announced > 0 ? announced : 0);
256
+ onProgress({ received: 0, total });
257
+ // Read through a reader of our own, cancelled on a stall or a stop: the transfer ends then whatever the
258
+ // transport does with the signal.
259
+ const reader = response.body.getReader();
260
+ const onStop = () => { reader.cancel(stop.signal.reason).catch(() => {}); };
261
+ stop.signal.addEventListener('abort', onStop, { once: true });
262
+ try {
263
+ for (;;) {
264
+ const { done, value } = await reader.read();
265
+ if (stop.signal.aborted) throw stop.signal.reason;
266
+ if (done) break;
267
+ const chunk = Buffer.from(value.buffer, value.byteOffset, value.byteLength);
268
+ alive();
269
+ received += chunk.length;
270
+ if (received > (expectedSize || maxBytes)) throw new CliInstallError('checksum', 'The download is larger than the published file. It was stopped and removed.');
271
+ hash.update(chunk);
272
+ await writeAll(handle, chunk);
273
+ onProgress({ received, total });
274
+ }
275
+ } finally {
276
+ stop.signal.removeEventListener('abort', onStop);
277
+ }
278
+ if (expectedSize && received !== expectedSize) throw new CliInstallError('interrupted', `The download ended after ${received} of ${expectedSize} bytes.`);
279
+ const digest = hash.digest('hex');
280
+ if (sha256 && digest !== String(sha256).toLowerCase()) {
281
+ throw new CliInstallError('checksum', 'The download does not match the SHA-256 its vendor published. It was removed and never run.', { detail: digest });
282
+ }
283
+ await handle.sync();
284
+ await handle.close();
285
+ handle = null;
286
+ return { sha256: digest, size: received, url: current.href };
287
+ } catch (error) {
288
+ try { await handle?.close(); } catch { /* closed */ }
289
+ handle = null;
290
+ if (made) rmSync(destination, { force: true });
291
+ if (stop.signal.aborted && stop.signal.reason instanceof CliInstallError) throw stop.signal.reason;
292
+ if (error instanceof CliInstallError) throw error;
293
+ if (DISK_CODES.has(errorCode(error))) throw asInstallError(error, 'disk', `The download could not be saved (${errorCode(error)}).`);
294
+ if (answered) throw new CliInstallError('interrupted', `The download from ${current.hostname} ended after ${received} bytes.`, { cause: error, detail: errorCode(error) || error?.message });
295
+ throw asInstallError(error, 'offline', `${current.hostname} could not be reached.`);
296
+ } finally {
297
+ clearTimeout(timer);
298
+ signal?.removeEventListener('abort', onAbort);
299
+ }
300
+ }
301
+
302
+ /** GET a GitHub API document (api.github.com only, followed redirects stay there). */
303
+ export async function fetchGithubJson(url, { fetchImpl = fetch, signal, timeoutMs = 20_000 } = {}) {
304
+ const stop = new AbortController();
305
+ const timer = setTimeout(() => stop.abort(), timeoutMs);
306
+ timer.unref?.();
307
+ const onAbort = () => stop.abort();
308
+ signal?.addEventListener('abort', onAbort, { once: true });
309
+ try {
310
+ const response = await fetchImpl(url, { signal: stop.signal, headers: { accept: 'application/vnd.github+json', 'x-github-api-version': '2022-11-28', 'user-agent': 'synabun' } });
311
+ const final = new URL(response.url || url);
312
+ if (final.protocol !== 'https:' || final.hostname !== 'api.github.com') throw new CliInstallError('http', 'GitHub\'s API answered from another address.');
313
+ if (!response.ok) throw new CliInstallError('http', `GitHub's API answered ${response.status}${response.status === 403 || response.status === 429 ? ' (rate limited)' : ''}.`, { detail: String(response.status) });
314
+ const text = await response.text();
315
+ if (text.length > MAX_JSON_BYTES) throw new CliInstallError('http', 'GitHub\'s API answered with too much.');
316
+ return JSON.parse(text);
317
+ } catch (error) {
318
+ if (error instanceof CliInstallError) throw error;
319
+ if (signal?.aborted) throw new CliInstallError('cancelled', 'Stopped.');
320
+ if (error instanceof SyntaxError) throw new CliInstallError('http', 'GitHub\'s API answered with something that is not JSON.');
321
+ throw asInstallError(error, 'offline', 'GitHub\'s API could not be reached.');
322
+ } finally {
323
+ clearTimeout(timer);
324
+ signal?.removeEventListener('abort', onAbort);
325
+ }
326
+ }
327
+
328
+ /** "sha256:abc…" (GitHub's asset digest) → "abc…", or null. */
329
+ export function sha256OfDigest(digest) {
330
+ const match = String(digest || '').match(/^sha256:([0-9a-f]{64})$/i);
331
+ return match ? match[1].toLowerCase() : null;
332
+ }
333
+
334
+ /** The named asset of a release, when it is downloaded from that repository's own releases. */
335
+ export function releaseAsset(release, name, repo) {
336
+ const asset = (release?.assets || []).find(item => item?.name === name);
337
+ if (!asset) return null;
338
+ const url = String(asset.browser_download_url || '');
339
+ if (!url.startsWith(`https://github.com/${repo}/releases/download/`)) return null;
340
+ return { name, url, size: Number(asset.size) || null, sha256: sha256OfDigest(asset.digest) };
341
+ }
342
+
343
+ /**
344
+ * The newest OpenCode 1 release (not a draft, not a prerelease; OpenCode 2 is
345
+ * a different program SynaBun's panel does not drive) that carries one of
346
+ * `assets`, the first listed preferred.
347
+ */
348
+ export function pickOpencodeRelease(releases, { assets, repo = 'anomalyco/opencode', tag = /^v1\.\d+\.\d+$/ } = {}) {
349
+ for (const release of Array.isArray(releases) ? releases : []) {
350
+ if (!release || release.draft || release.prerelease || !tag.test(String(release.tag_name || ''))) continue;
351
+ for (const name of assets || []) {
352
+ const asset = releaseAsset(release, name, repo);
353
+ if (asset) return { tag: release.tag_name, version: String(release.tag_name).replace(/^v/, ''), asset };
354
+ }
355
+ }
356
+ return null;
357
+ }
358
+
359
+ // ── Zip ─────────────────────────────────────────────────────────────────────
360
+
361
+ const unpack = (message) => new CliInstallError('unpack', message);
362
+
363
+ /**
364
+ * Read one member out of a zip archive into `destination` (a new file),
365
+ * without an unzip program and without using a name of the archive as a path:
366
+ * only the entry called exactly `member` is read, through the streaming
367
+ * reader of backups (backup-zip-reader.js: ZIP64, stored or deflated, checked
368
+ * against its size and CRC-32).
369
+ */
370
+ export async function extractZipMember({ archive, member, destination, maxSize = 1024 ** 3, mode = 0o755 }) {
371
+ let zip;
372
+ try { zip = await openZipArchive(archive); } catch (error) { throw unpack(`The download is not a readable zip archive (${error?.message || error}).`); }
373
+ let opened = false;
374
+ try {
375
+ const entry = zip.getEntry(member);
376
+ if (!entry) throw unpack(`The archive holds no ${member}.`);
377
+ if (entry.uncompressedSize > maxSize) throw unpack(`The archive's ${member} is larger than expected.`);
378
+ const out = createWriteStream(destination, { flags: 'wx', mode });
379
+ out.on('open', () => { opened = true; });
380
+ await zip.pipeEntry(entry, out);
381
+ return { size: entry.uncompressedSize };
382
+ } catch (error) {
383
+ if (opened) rmSync(destination, { force: true });
384
+ if (error instanceof CliInstallError) throw error;
385
+ if (DISK_CODES.has(errorCode(error))) throw asInstallError(error, 'disk', `The program could not be written (${errorCode(error)}).`);
386
+ throw unpack(`The archive's ${member} could not be read (${error?.message || error}).`);
387
+ } finally {
388
+ try { await zip.close(); } catch { /* closed */ }
389
+ }
390
+ }
391
+
392
+ // ── Output ──────────────────────────────────────────────────────────────────
393
+
394
+ const ANSI = /\x1b\[[0-9;?]*[ -\/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[()][A-Za-z0-9]|\x1b[=>78DEHMNOZc]/g;
395
+
396
+ /**
397
+ * Turns an installer's output into lines: escapes stripped, a carriage return
398
+ * (a progress bar redrawing itself) keeping only what came after it, empty
399
+ * lines dropped, each line at most 500 characters.
400
+ */
401
+ export function createLineSplitter(onLine) {
402
+ let pending = '';
403
+ const emit = (raw) => {
404
+ // A line's own CR (Windows' CRLF) ends it; a CR inside it is a redraw.
405
+ const segments = raw.replace(/\r+$/, '').split('\r');
406
+ const text = segments[segments.length - 1].replace(ANSI, '').replace(/[\x00-\x08\x0b-\x1f\x7f]/g, '').trimEnd();
407
+ if (text.trim()) onLine(text.slice(0, 500));
408
+ };
409
+ return {
410
+ push(chunk) {
411
+ pending += String(chunk);
412
+ let index;
413
+ while ((index = pending.indexOf('\n')) >= 0) {
414
+ emit(pending.slice(0, index));
415
+ pending = pending.slice(index + 1);
416
+ }
417
+ if (pending.length > 16_384) { emit(pending); pending = ''; }
418
+ },
419
+ end() {
420
+ if (pending) emit(pending);
421
+ pending = '';
422
+ },
423
+ };
424
+ }
425
+
426
+ function sameDir(a, b, platform) {
427
+ const clean = (value) => {
428
+ const text = String(value || '').replace(/[\\/]+$/, '');
429
+ return platform === 'win32' ? text.toLowerCase().replace(/\//g, '\\') : text;
430
+ };
431
+ return clean(a) === clean(b);
432
+ }
433
+
434
+ /** Whether `dir` is on a PATH list. */
435
+ export function onPathList(dir, entries, platform = process.platform) {
436
+ return (entries || []).some(entry => sameDir(entry, dir, platform));
437
+ }
438
+
439
+ // ── The installer ───────────────────────────────────────────────────────────
440
+
441
+ /**
442
+ * @param {object} deps
443
+ * @param {{ resolve: Function }} deps.resolver cli-resolver.js: the final check (force)
444
+ * @param {() => Promise<string[]>} [deps.readOsPath] the OS PATH a new terminal gets, read afresh
445
+ * @param {(tool: string, ok: boolean, status: object|null) => void} [deps.onFinished]
446
+ */
447
+ export function createCliInstaller({
448
+ resolver,
449
+ platform = process.platform,
450
+ arch = process.arch,
451
+ env = process.env,
452
+ home = (platform === 'win32' ? env.USERPROFILE : env.HOME) || os.homedir(),
453
+ tmpRoot = os.tmpdir(),
454
+ fetchImpl = fetch,
455
+ spawnImpl = spawn,
456
+ readOsPath = async () => [],
457
+ addToPath = null,
458
+ installerPath = null,
459
+ onFinished = () => {},
460
+ log = () => {},
461
+ timeoutMs = 15 * 60_000,
462
+ stallMs = 60_000,
463
+ } = {}) {
464
+ if (!resolver?.resolve) throw new Error('createCliInstaller needs the CLI resolver');
465
+ const jobs = new Map();
466
+ const windows = platform === 'win32';
467
+ const planFor = (toolKey) => installPlanFor(toolKey, { platform, arch, env, home });
468
+ const guideOf = (toolKey) => CLI_INSTALL_GUIDES[toolKey] || null;
469
+ const manualOf = (toolKey) => buildInstallCommand(toolKey, 'unknown', { platform });
470
+
471
+ function publicJob(job) {
472
+ return {
473
+ tool: job.tool,
474
+ state: job.state,
475
+ startedAt: job.startedAt,
476
+ endedAt: job.endedAt,
477
+ step: job.step,
478
+ code: job.code,
479
+ error: job.error,
480
+ method: job.plan.kind,
481
+ source: job.source || job.plan.source,
482
+ log: job.log.slice(-40),
483
+ path: job.path || null,
484
+ };
485
+ }
486
+
487
+ function emit(job, event) {
488
+ if (event.type === 'done' && event.path) job.path = event.path;
489
+ if (event.type === 'log') {
490
+ job.log.push(event.line);
491
+ if (job.log.length > LOG_KEEP) job.log.splice(0, job.log.length - LOG_KEEP);
492
+ }
493
+ if (event.type === 'step') job.step = event.step;
494
+ for (const listener of [...job.listeners]) {
495
+ try { listener(event); } catch { /* a closed stream */ }
496
+ }
497
+ }
498
+
499
+ function stepper(job) {
500
+ return {
501
+ step: (step) => emit(job, { type: 'step', step }),
502
+ line: (line) => emit(job, { type: 'log', line }),
503
+ progress: (() => {
504
+ let last = 0;
505
+ return ({ received, total }) => {
506
+ const now = Date.now();
507
+ if (received !== total && now - last < 250) return;
508
+ last = now;
509
+ emit(job, { type: 'progress', received, total });
510
+ };
511
+ })(),
512
+ };
513
+ }
514
+
515
+ function interpreterFor(plan) {
516
+ if (plan.interpreter === 'powershell') {
517
+ const root = env.SystemRoot || env.SYSTEMROOT || 'C:\\Windows';
518
+ return { file: path.win32.join(root, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe'), args: (script) => ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', script] };
519
+ }
520
+ const candidates = plan.interpreter === 'sh' ? ['/bin/sh', '/usr/bin/sh'] : ['/bin/bash', '/usr/bin/bash', '/usr/local/bin/bash', '/opt/homebrew/bin/bash'];
521
+ const file = candidates.find(candidate => existsSync(candidate));
522
+ if (!file) throw new CliInstallError('unsupported', `${plan.interpreter} is not installed on this computer; the ${plan.label} needs it.`);
523
+ return { file, args: (script) => [script] };
524
+ }
525
+
526
+ /** PATH for an installer: the one a new terminal has (so its own "is it on PATH?" checks see the truth), plus the system's folders. */
527
+ function installerPathValue(osEntries) {
528
+ if (installerPath) return installerPath;
529
+ const delimiter = windows ? ';' : ':';
530
+ const base = osEntries.length ? osEntries : String(env.PATH || env.Path || '').split(delimiter);
531
+ const system = windows
532
+ ? [path.win32.join(env.SystemRoot || 'C:\\Windows', 'System32'), path.win32.join(env.SystemRoot || 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0'), env.SystemRoot || 'C:\\Windows']
533
+ : ['/usr/bin', '/bin', '/usr/sbin', '/sbin'];
534
+ const seen = new Set();
535
+ const value = [...base, ...system].filter((entry) => {
536
+ const key = windows ? String(entry).toLowerCase() : String(entry);
537
+ if (!entry || seen.has(key)) return false;
538
+ seen.add(key);
539
+ return true;
540
+ }).join(delimiter);
541
+ // A packaged application's own Node runtime and npm, after everything of the user's: an installer that looks for
542
+ // node or npm finds the user's own first, and these on a machine that has none (lib/augmented-path.js).
543
+ const packaged = rememberedPackagedPath();
544
+ return packaged.last.length || packaged.extra.length ? packagedPathLast(value, { platform, ...packaged }) : value;
545
+ }
546
+
547
+ async function fetchScript(job, plan, workDir, { step, line, progress }) {
548
+ const file = path.join(workDir, windows ? 'install.ps1' : 'install.sh');
549
+ step('download');
550
+ let source = plan.url;
551
+ let expected = null;
552
+ let allow = plan.allow;
553
+ if (plan.release) {
554
+ try {
555
+ const release = await fetchGithubJson(`https://api.github.com/repos/${plan.release.repo}/releases/latest`, { fetchImpl, signal: job.abort.signal });
556
+ const asset = releaseAsset(release, plan.release.asset, plan.release.repo);
557
+ if (asset?.sha256) {
558
+ source = asset.url;
559
+ expected = asset.sha256;
560
+ allow = (url) => GITHUB_DOWNLOAD_HOSTS.includes(url.hostname);
561
+ line(`${plan.label}: ${plan.release.asset} of ${plan.release.repo} ${release.tag_name || ''}`.trim());
562
+ }
563
+ } catch (error) {
564
+ if (error?.code === 'cancelled') throw error;
565
+ line(`GitHub's release list did not answer (${error?.message || error}); downloading ${plan.url} instead.`);
566
+ }
567
+ }
568
+ job.source = source;
569
+ const got = await downloadFile({ url: source, allow, destination: file, maxBytes: MAX_SCRIPT_BYTES, sha256: expected, fetchImpl, signal: job.abort.signal, onProgress: progress, stallMs, accept: '*/*' });
570
+ step('verify');
571
+ if (expected) line(`SHA-256 matches the published one (${got.sha256.slice(0, 12)}…).`);
572
+ const handle = await open(file, 'r');
573
+ const head = Buffer.alloc(64);
574
+ try { await handle.read(head, 0, 64, 0); } finally { await handle.close(); }
575
+ const start = head.toString('utf8').replace(/^\uFEFF/, '').trimStart();
576
+ // A region the vendor does not serve, a portal or a proxy answers with a page: never run that.
577
+ if (start.startsWith('<') || (!windows && !start.startsWith('#!'))) {
578
+ throw new CliInstallError('http', `${new URL(got.url).hostname} answered with a web page, not the installer (the download service may not be available in this region).`);
579
+ }
580
+ return file;
581
+ }
582
+
583
+ function runScript(job, plan, scriptFile, workDir, osEntries, { line }) {
584
+ const interpreter = interpreterFor(plan);
585
+ const childEnv = { ...withPathEnv({ ...env, ...plan.env }, installerPathValue(osEntries), platform), TERM: 'dumb', NO_COLOR: '1' };
586
+ delete childEnv.FORCE_COLOR;
587
+ return new Promise((resolve, reject) => {
588
+ let child;
589
+ try {
590
+ child = spawnImpl(interpreter.file, interpreter.args(scriptFile), {
591
+ cwd: workDir,
592
+ env: childEnv,
593
+ stdio: ['ignore', 'pipe', 'pipe'],
594
+ windowsHide: true,
595
+ // POSIX: a session of its own, without a terminal, so nothing can ask on /dev/tty, and the
596
+ // whole group can be stopped.
597
+ detached: !windows,
598
+ });
599
+ } catch (error) {
600
+ reject(asInstallError(error, 'installer-failed', `${interpreter.file} could not be started.`));
601
+ return;
602
+ }
603
+ job.child = child;
604
+ const out = createLineSplitter(line);
605
+ const err = createLineSplitter(line);
606
+ child.stdout?.on('data', chunk => out.push(chunk.toString('utf8')));
607
+ child.stderr?.on('data', chunk => err.push(chunk.toString('utf8')));
608
+ let settled = false;
609
+ const timer = setTimeout(() => {
610
+ stopChild(child, windows, spawnImpl);
611
+ finish(new CliInstallError('timeout', `The installer was still running after ${Math.round(timeoutMs / 60_000)} minutes and was stopped.`));
612
+ }, timeoutMs);
613
+ timer.unref?.();
614
+ const onAbort = () => { stopChild(child, windows, spawnImpl); finish(new CliInstallError('cancelled', 'The install was stopped.')); };
615
+ job.abort.signal.addEventListener('abort', onAbort, { once: true });
616
+ function finish(error, code) {
617
+ if (settled) return;
618
+ settled = true;
619
+ clearTimeout(timer);
620
+ job.abort.signal.removeEventListener('abort', onAbort);
621
+ out.end();
622
+ err.end();
623
+ job.child = null;
624
+ if (error) reject(error);
625
+ else if (code !== 0) reject(new CliInstallError('installer-failed', `The ${plan.label} stopped with exit code ${code}.`, { detail: code }));
626
+ else resolve();
627
+ }
628
+ child.on('error', error => finish(asInstallError(error, 'installer-failed', `${interpreter.file} could not be started (${errorCode(error) || error?.message}).`)));
629
+ child.on('close', (code, signal) => finish(null, code ?? (signal ? 128 : 1)));
630
+ });
631
+ }
632
+
633
+ async function installArchive(job, plan, workDir, { step, line, progress }) {
634
+ step('download');
635
+ const releases = await fetchGithubJson(`https://api.github.com/repos/${plan.repo}/releases?per_page=20`, { fetchImpl, signal: job.abort.signal });
636
+ const picked = pickOpencodeRelease(releases, { assets: plan.assets, repo: plan.repo, tag: plan.tag });
637
+ if (!picked) throw new CliInstallError('http', `No OpenCode 1 release of ${plan.repo} carries ${plan.assets.join(' or ')}.`);
638
+ job.source = picked.asset.url;
639
+ line(`OpenCode ${picked.version}: ${picked.asset.name}`);
640
+ const archive = path.join(workDir, picked.asset.name);
641
+ const got = await downloadFile({
642
+ url: picked.asset.url,
643
+ allow: (url) => GITHUB_DOWNLOAD_HOSTS.includes(url.hostname),
644
+ destination: archive,
645
+ maxBytes: MAX_ARCHIVE_BYTES,
646
+ expectedSize: picked.asset.size,
647
+ sha256: picked.asset.sha256,
648
+ fetchImpl,
649
+ signal: job.abort.signal,
650
+ onProgress: progress,
651
+ stallMs,
652
+ });
653
+ step('verify');
654
+ line(picked.asset.sha256 ? `SHA-256 matches the published one (${got.sha256.slice(0, 12)}…).` : 'GitHub publishes no SHA-256 for this file; its size matches.');
655
+ step('extract');
656
+ try { mkdirSync(plan.dir, { recursive: true }); } catch (error) { throw asInstallError(error, 'disk', `${plan.dir} could not be created (${errorCode(error)}).`); }
657
+ const part = path.join(plan.dir, `.${plan.member}.${randomBytes(4).toString('hex')}.part`);
658
+ await extractZipMember({ archive, member: plan.member, destination: part });
659
+ step('place');
660
+ const head = Buffer.alloc(2);
661
+ const handle = await open(part, 'r');
662
+ try { await handle.read(head, 0, 2, 0); } finally { await handle.close(); }
663
+ if (head[0] !== 0x4d || head[1] !== 0x5a) {
664
+ rmSync(part, { force: true });
665
+ throw new CliInstallError('unpack', `The archive's ${plan.member} is not a Windows program.`);
666
+ }
667
+ const target = path.join(plan.dir, plan.member);
668
+ try {
669
+ renameSync(part, target);
670
+ } catch (error) {
671
+ rmSync(part, { force: true });
672
+ throw asInstallError(error, 'disk', `${target} could not be written (${errorCode(error)}: is an OpenCode still running from there?).`);
673
+ }
674
+ // Files SynaBun writes carry no Mark of the Web; a copy that does (a mirror, an antivirus) is cleared so it can start.
675
+ try { unlinkSync(`${target}:Zone.Identifier`); } catch { /* none */ }
676
+ line(`Placed ${target}`);
677
+ }
678
+
679
+ async function run(job) {
680
+ const { plan, tool } = job;
681
+ const io = stepper(job);
682
+ let workDir = null;
683
+ let before = [];
684
+ try {
685
+ emit(job, { type: 'start', tool, method: plan.kind, source: plan.source, label: plan.label, platform, arch });
686
+ try { before = await readOsPath(); } catch { before = []; }
687
+ workDir = await mkdtemp(path.join(tmpRoot, `synabun-cli-install-${tool}-`));
688
+ if (plan.kind === 'script') {
689
+ const script = await fetchScript(job, plan, workDir, io);
690
+ io.step('run');
691
+ await runScript(job, plan, script, workDir, before, io);
692
+ } else {
693
+ await installArchive(job, plan, workDir, io);
694
+ }
695
+ io.step('check');
696
+ const status = await resolver.resolve(tool, { force: true });
697
+ if (status?.state !== 'installed') {
698
+ const where = status?.path ? ` ${status.path}: ${status.error || 'it does not answer --version'}` : ' no working copy was found in any known place';
699
+ throw new CliInstallError('not-found-after-install', `The installer finished, but${where}.`);
700
+ }
701
+ let after = [];
702
+ try { after = await readOsPath(); } catch { after = []; }
703
+ const dir = status.path ? path[windows ? 'win32' : 'posix'].dirname(status.path) : null;
704
+ let onPath = dir ? onPathList(dir, after, platform) : false;
705
+ let added = onPath && !onPathList(dir, before, platform);
706
+ let warning = null;
707
+ if (!onPath && addToPath) {
708
+ io.step('path');
709
+ try {
710
+ const result = await addToPath(status);
711
+ onPath = result.onPath;
712
+ added = result.added;
713
+ warning = result.warning || null;
714
+ if (added) emit(job, { type: 'path', dir, added: true });
715
+ if (warning) io.line(warning);
716
+ } catch (error) {
717
+ warning = error.message;
718
+ io.line(`PATH could not be updated: ${warning}`);
719
+ }
720
+ }
721
+ job.state = 'done';
722
+ job.endedAt = new Date().toISOString();
723
+ emit(job, { type: 'done', ok: true, status, path: { dir, onPath, added, ...(warning ? { warning } : {}) } });
724
+ try { onFinished(tool, true, status); } catch { /* the caller's */ }
725
+ } catch (failure) {
726
+ const error = failure instanceof CliInstallError ? failure : asInstallError(failure, DISK_CODES.has(errorCode(failure)) ? 'disk' : 'internal', String(failure?.message || failure));
727
+ job.state = 'failed';
728
+ job.endedAt = new Date().toISOString();
729
+ job.code = error.code;
730
+ job.error = error.message;
731
+ log(`[cli-install] ${tool} failed (${error.code}): ${error.message}`);
732
+ emit(job, { type: 'done', ok: false, code: error.code, error: error.message, manualCommand: manualOf(tool), guideUrl: guideOf(tool), log: job.log.slice(-12) });
733
+ try { onFinished(tool, false, null); } catch { /* the caller's */ }
734
+ } finally {
735
+ job.listeners.clear();
736
+ if (workDir) { try { rmSync(workDir, { recursive: true, force: true }); } catch { /* temp */ } }
737
+ }
738
+ }
739
+
740
+ return {
741
+ describe: (toolKey) => describeInstall(toolKey, { platform, arch, env, home }),
742
+ guideUrl: guideOf,
743
+ manualCommand: manualOf,
744
+ jobs() {
745
+ return Object.fromEntries([...jobs.entries()].map(([tool, job]) => [tool, publicJob(job)]));
746
+ },
747
+ running(toolKey) {
748
+ return jobs.get(toolKey)?.state === 'running';
749
+ },
750
+ /**
751
+ * Start installing `toolKey`; `onEvent` hears every event of this run
752
+ * (start, step, progress, log, done). Throws CliInstallError 'busy' /
753
+ * 'unsupported' before anything starts.
754
+ */
755
+ start(toolKey, { onEvent = null } = {}) {
756
+ const plan = planFor(toolKey);
757
+ if (!plan) throw new CliInstallError('unsupported', `SynaBun has no one-click install of ${toolKey} for ${platform}-${arch}.`);
758
+ if (jobs.get(toolKey)?.state === 'running') throw new CliInstallError('busy', 'An install of this CLI is already running.');
759
+ const job = {
760
+ tool: toolKey,
761
+ plan,
762
+ state: 'running',
763
+ startedAt: new Date().toISOString(),
764
+ endedAt: null,
765
+ step: null,
766
+ code: null,
767
+ error: null,
768
+ source: null,
769
+ log: [],
770
+ listeners: new Set(onEvent ? [onEvent] : []),
771
+ abort: new AbortController(),
772
+ child: null,
773
+ };
774
+ jobs.set(toolKey, job);
775
+ job.done = run(job);
776
+ return { job: publicJob(job), done: job.done, unsubscribe: () => { if (onEvent) job.listeners.delete(onEvent); } };
777
+ },
778
+ /** Stop every running install (SynaBun is shutting down). */
779
+ stopAll() {
780
+ for (const job of jobs.values()) if (job.state === 'running') job.abort.abort();
781
+ },
782
+ };
783
+ }
784
+
785
+ /** End an installer and everything it started. */
786
+ function stopChild(child, windows, spawnImpl) {
787
+ if (!child?.pid) return;
788
+ if (windows) {
789
+ try { spawnImpl('taskkill', ['/pid', String(child.pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' }); } catch { /* gone */ }
790
+ return;
791
+ }
792
+ try { process.kill(-child.pid, 'SIGTERM'); } catch { try { child.kill('SIGTERM'); } catch { /* gone */ } }
793
+ const hard = setTimeout(() => {
794
+ try { process.kill(-child.pid, 'SIGKILL'); } catch { try { child.kill('SIGKILL'); } catch { /* gone */ } }
795
+ }, 5000);
796
+ hard.unref?.();
797
+ }