@gloom-sh/terminal 0.18.5 → 0.19.1

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 (570) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/package.json +1 -1
  4. package/patches/electrobun@1.18.1.patch +163 -8
  5. package/src/api-client/app-attention.ts +3 -0
  6. package/src/api-client/beneficial-owners.ts +3 -0
  7. package/src/api-client/company-kpis.ts +3 -0
  8. package/src/api-client/cot.ts +6 -2
  9. package/src/api-client/credit-documents.ts +6 -0
  10. package/src/api-client/data.ts +19 -7
  11. package/src/api-client/equity-screener.ts +24 -0
  12. package/src/api-client/fund-facts.ts +45 -4
  13. package/src/api-client/hiring.ts +3 -0
  14. package/src/api-client/index.ts +15 -0
  15. package/src/api-client/ipo.ts +24 -1
  16. package/src/api-client/market-discovery.ts +10 -0
  17. package/src/api-client/market-movers.ts +7 -0
  18. package/src/api-client/members.ts +1 -1
  19. package/src/api-client/mna.ts +3 -0
  20. package/src/api-client/paths.ts +9 -1
  21. package/src/api-client/perps.ts +43 -0
  22. package/src/api-client/research-activity.ts +3 -2
  23. package/src/api-client/roadmap.ts +85 -0
  24. package/src/api-client/types.ts +82 -2
  25. package/src/app/pane-runtime/plugin-bindings.ts +9 -5
  26. package/src/app/runtime/broker-import.ts +0 -2
  27. package/src/app/runtime/plugin-bindings.ts +73 -56
  28. package/src/architecture/core-builtin-allowlist.json +2 -2
  29. package/src/architecture/pane-tabs-allowlist.json +3 -0
  30. package/src/architecture/ttl-caches-allowlist.json +19 -0
  31. package/src/brokers/sync-broker-instance.ts +2 -8
  32. package/src/capabilities/chart-series.ts +1 -1
  33. package/src/cli/commands/crypto-hints.ts +40 -24
  34. package/src/cli/commands/market.ts +107 -38
  35. package/src/cli/commands/news-search.ts +22 -0
  36. package/src/cli/commands/options-chain.ts +48 -7
  37. package/src/cli/commands/options-leaps.ts +9 -3
  38. package/src/cli/commands/overview.ts +147 -32
  39. package/src/cli/commands/search.ts +21 -1
  40. package/src/cli/commands/themes.ts +11 -2
  41. package/src/cli/commands/ticker.ts +87 -36
  42. package/src/cli/company-name-hint.ts +41 -0
  43. package/src/cli/earnings-calendar.ts +1 -1
  44. package/src/cli/errors.ts +25 -2
  45. package/src/cli/glossary.ts +31 -2
  46. package/src/cli/history-rows.ts +57 -9
  47. package/src/cli/index-roots.ts +0 -7
  48. package/src/cli/index.ts +2 -0
  49. package/src/cli/listing-arg.ts +235 -60
  50. package/src/cli/output-file.ts +68 -0
  51. package/src/cli/pane-functions/capabilities.ts +4 -0
  52. package/src/cli/pane-functions/catalog.ts +80 -7
  53. package/src/cli/pane-functions/discovery.ts +21 -1
  54. package/src/cli/pane-functions/dom.ts +13 -3
  55. package/src/cli/pane-functions/freshness.ts +93 -4
  56. package/src/cli/pane-functions/function-help.ts +20 -10
  57. package/src/cli/pane-functions/headless.ts +17 -7
  58. package/src/cli/pane-functions/index.ts +54 -16
  59. package/src/cli/pane-functions/market-session.ts +66 -4
  60. package/src/cli/pane-functions/options.ts +19 -1
  61. package/src/cli/pane-functions/resolver.ts +19 -8
  62. package/src/cli/pane-functions/screenshot-assessment.ts +6 -4
  63. package/src/cli/pane-functions/screenshot-types.ts +5 -0
  64. package/src/cli/quote-answer.ts +22 -11
  65. package/src/cli/saved-copy-persistence.ts +88 -0
  66. package/src/cli/ticker-resolution.ts +3 -2
  67. package/src/components/chart/composite/composite-chart.tsx +4 -0
  68. package/src/components/chart/composite/interactions.ts +6 -0
  69. package/src/components/chart/composite/legend.tsx +45 -7
  70. package/src/components/chart/composite/panel-gestures.ts +33 -0
  71. package/src/components/chart/composite/panel-surface.tsx +52 -3
  72. package/src/components/chart/composite/rasterizer.ts +24 -13
  73. package/src/components/chart/composite/scene.ts +39 -4
  74. package/src/components/chart/composite/series-cues.ts +14 -0
  75. package/src/components/chart/composite/text-renderer.ts +9 -2
  76. package/src/components/chart/composite/tool-catalog.ts +36 -0
  77. package/src/components/chart/composite/tools.ts +203 -44
  78. package/src/components/chart/composite/use-composite-chart-keyboard.ts +11 -4
  79. package/src/components/command-bar/assist/inventory.ts +2 -1
  80. package/src/components/command-bar/commands/direct/index.ts +28 -13
  81. package/src/components/command-bar/list/model.ts +5 -0
  82. package/src/components/command-bar/pane-templates/items.ts +3 -2
  83. package/src/components/command-bar/routes/root/runtime.ts +6 -7
  84. package/src/components/command-bar/selection-runtime.ts +52 -3
  85. package/src/components/command-bar/surface/index.tsx +2 -0
  86. package/src/components/command-bar/workflow/collection-actions.ts +3 -3
  87. package/src/components/layout/pane/capture-stamp.ts +2 -2
  88. package/src/components/layout/pane/footer/index.tsx +22 -6
  89. package/src/components/layout/pane/footer/model.ts +83 -15
  90. package/src/components/layout/pane/status-footer.ts +35 -2
  91. package/src/components/layout/status-widget-space.ts +9 -0
  92. package/src/components/onboarding/onboarding-wizard.tsx +4 -1
  93. package/src/components/onboarding/portfolio-step/positions-panel.tsx +12 -7
  94. package/src/components/onboarding/wizard-broker-sync.ts +0 -1
  95. package/src/components/onboarding/wizard-positions.ts +3 -3
  96. package/src/components/ticker/list-table-view.tsx +8 -21
  97. package/src/components/ui/choice-dialog.tsx +23 -18
  98. package/src/components/ui/choice-groups.ts +18 -0
  99. package/src/components/ui/data-table/opentui/index.tsx +9 -7
  100. package/src/components/ui/data-table/opentui/model.ts +26 -1
  101. package/src/components/ui/data-table/types.ts +2 -0
  102. package/src/components/ui/display.tsx +18 -3
  103. package/src/components/ui/icon.tsx +4 -1
  104. package/src/components/ui/list-view.tsx +8 -4
  105. package/src/components/ui/query-bar-keys.tsx +2 -0
  106. package/src/components/ui/query-bar.tsx +1 -0
  107. package/src/components/ui/select-button.tsx +5 -1
  108. package/src/components/ui/select-field.tsx +1 -0
  109. package/src/components/ui/table-layout.ts +8 -5
  110. package/src/core/app-runtime.ts +2 -0
  111. package/src/core/app-services.ts +2 -2
  112. package/src/data/config/chart-settings.ts +1 -1
  113. package/src/data/json-ticker-repository.ts +17 -8
  114. package/src/data/plugin-cache.ts +5 -3
  115. package/src/data/saved-copies.ts +8 -0
  116. package/src/data/ticker-repository.ts +16 -9
  117. package/src/data/ticker-writes.ts +81 -0
  118. package/src/data/ttl-cache.ts +165 -0
  119. package/src/docs/snapshot.json +103 -142
  120. package/src/i18n/es.ts +20 -6
  121. package/src/i18n/ja.ts +20 -6
  122. package/src/i18n/ko.ts +20 -6
  123. package/src/i18n/zh-cn.ts +20 -6
  124. package/src/i18n/zh-tw.ts +20 -6
  125. package/src/market-data/depositary-pair.ts +27 -3
  126. package/src/market-data/fund-facts.ts +50 -0
  127. package/src/market-data/market/change-basis.ts +16 -3
  128. package/src/market-data/market/format.ts +25 -0
  129. package/src/market-data/market/freshness.ts +2 -1
  130. package/src/market-data/options-alternatives.ts +81 -4
  131. package/src/market-data/quotes/metadata.ts +7 -0
  132. package/src/news/aggregator.ts +119 -9
  133. package/src/news/cloud-session.ts +21 -0
  134. package/src/news/hooks.ts +1 -0
  135. package/src/news/news-model.ts +1 -0
  136. package/src/news/types.ts +17 -0
  137. package/src/plugins/builtin/alerts/alert-engine.ts +20 -1
  138. package/src/plugins/builtin/alerts/alert-log.ts +33 -0
  139. package/src/plugins/builtin/alerts/calendar-events.ts +10 -4
  140. package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
  141. package/src/plugins/builtin/alerts/cli.ts +362 -80
  142. package/src/plugins/builtin/alerts/command.ts +25 -1
  143. package/src/plugins/builtin/alerts/event-rows.ts +24 -3
  144. package/src/plugins/builtin/alerts/events-pane.tsx +34 -10
  145. package/src/plugins/builtin/alerts/fired-log.ts +73 -9
  146. package/src/plugins/builtin/alerts/format.ts +75 -5
  147. package/src/plugins/builtin/alerts/headless.ts +34 -12
  148. package/src/plugins/builtin/alerts/history-rows.ts +3 -2
  149. package/src/plugins/builtin/alerts/history.ts +1 -1
  150. package/src/plugins/builtin/alerts/index.tsx +79 -16
  151. package/src/plugins/builtin/alerts/metric-command.ts +77 -4
  152. package/src/plugins/builtin/alerts/metric-data.ts +54 -13
  153. package/src/plugins/builtin/alerts/metric-model.ts +163 -14
  154. package/src/plugins/builtin/alerts/metric-parity-fixture.json +1 -1
  155. package/src/plugins/builtin/alerts/pane.tsx +61 -23
  156. package/src/plugins/builtin/alerts/quotes.ts +6 -0
  157. package/src/plugins/builtin/alerts/storage.ts +12 -8
  158. package/src/plugins/builtin/alerts/types.ts +4 -0
  159. package/src/plugins/builtin/analytics/index.tsx +2 -1
  160. package/src/plugins/builtin/analytics/risk-client.ts +16 -5
  161. package/src/plugins/builtin/analytics/risk-fx.ts +70 -15
  162. package/src/plugins/builtin/analytics/risk-headless.ts +27 -0
  163. package/src/plugins/builtin/analytics/risk-model.ts +55 -9
  164. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
  165. package/src/plugins/builtin/attention/index.tsx +2 -1
  166. package/src/plugins/builtin/attention/listing.ts +14 -0
  167. package/src/plugins/builtin/broker-manager/index.tsx +7 -3
  168. package/src/plugins/builtin/buildout/cells.tsx +12 -4
  169. package/src/plugins/builtin/buildout/format.ts +38 -0
  170. package/src/plugins/builtin/buildout/model/normalizers.ts +36 -1
  171. package/src/plugins/builtin/buildout/pane/index.tsx +4 -1
  172. package/src/plugins/builtin/buildout/table-model.ts +23 -2
  173. package/src/plugins/builtin/builtin-plugin-meta.ts +6 -2
  174. package/src/plugins/builtin/cds/client.ts +142 -30
  175. package/src/plugins/builtin/cds/headless.ts +35 -8
  176. package/src/plugins/builtin/cds/model.ts +145 -18
  177. package/src/plugins/builtin/cds/pane.tsx +16 -5
  178. package/src/plugins/builtin/changelog/index.tsx +9 -6
  179. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +3 -0
  180. package/src/plugins/builtin/chart-composer/cli-options.ts +4 -0
  181. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +7 -0
  182. package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -2
  183. package/src/plugins/builtin/chart-composer/follow-binding.ts +14 -2
  184. package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -1
  185. package/src/plugins/builtin/chart-composer/headless.ts +6 -1
  186. package/src/plugins/builtin/chart-composer/index.tsx +5 -6
  187. package/src/plugins/builtin/chart-composer/pane.tsx +37 -8
  188. package/src/plugins/builtin/chart-composer/settings.ts +27 -0
  189. package/src/plugins/builtin/chat/content/command-suggestions.ts +1 -0
  190. package/src/plugins/builtin/chat/controller/index.ts +5 -1
  191. package/src/plugins/builtin/chat/controller/notifications.ts +33 -3
  192. package/src/plugins/builtin/chat/controller/state.ts +25 -2
  193. package/src/plugins/builtin/chat/controller/unread-people.ts +89 -23
  194. package/src/plugins/builtin/chat/controller/utils.ts +7 -2
  195. package/src/plugins/builtin/chat/pane-links/commands.ts +7 -1
  196. package/src/plugins/builtin/chat/status-preview.ts +79 -0
  197. package/src/plugins/builtin/chat/status-widget.tsx +147 -10
  198. package/src/plugins/builtin/chat/unread-badge.ts +51 -10
  199. package/src/plugins/builtin/clinical-trials/client.ts +49 -4
  200. package/src/plugins/builtin/clinical-trials/model.ts +86 -31
  201. package/src/plugins/builtin/clinical-trials/pane.tsx +61 -49
  202. package/src/plugins/builtin/clinical-trials/sponsor-listing.ts +90 -0
  203. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +95 -15
  204. package/src/plugins/builtin/clinical-trials/trial-pages.ts +50 -0
  205. package/src/plugins/builtin/cloud/askg/manifest.ts +2 -0
  206. package/src/plugins/builtin/company-attention/building.tsx +5 -1
  207. package/src/plugins/builtin/company-attention/client.ts +8 -9
  208. package/src/plugins/builtin/company-attention/evidence.ts +4 -1
  209. package/src/plugins/builtin/company-attention/headless.ts +2 -1
  210. package/src/plugins/builtin/company-attention/moves.tsx +5 -1
  211. package/src/plugins/builtin/company-attention/pane.tsx +12 -2
  212. package/src/plugins/builtin/company-attention/signals.tsx +5 -1
  213. package/src/plugins/builtin/company-kpis/headless.ts +8 -3
  214. package/src/plugins/builtin/company-kpis/model.ts +73 -5
  215. package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
  216. package/src/plugins/builtin/composite-plugins.ts +2 -1
  217. package/src/plugins/builtin/correlation/headless.ts +13 -5
  218. package/src/plugins/builtin/correlation/history.ts +22 -0
  219. package/src/plugins/builtin/cot/client.ts +22 -18
  220. package/src/plugins/builtin/cot/headless.ts +15 -10
  221. package/src/plugins/builtin/cot/model.ts +14 -1
  222. package/src/plugins/builtin/cot/pane.tsx +22 -14
  223. package/src/plugins/builtin/credit-boards/client.ts +6 -10
  224. package/src/plugins/builtin/credit-conditions/headless.ts +8 -2
  225. package/src/plugins/builtin/credit-conditions/index.tsx +5 -1
  226. package/src/plugins/builtin/credit-conditions/model.ts +4 -0
  227. package/src/plugins/builtin/credit-conditions/series.ts +3 -1
  228. package/src/plugins/builtin/credit-documents/detail.tsx +2 -2
  229. package/src/plugins/builtin/credit-documents/export.ts +173 -0
  230. package/src/plugins/builtin/credit-documents/headless.ts +10 -10
  231. package/src/plugins/builtin/credit-documents/model.ts +104 -14
  232. package/src/plugins/builtin/credit-documents/pane.tsx +12 -5
  233. package/src/plugins/builtin/custom-view/sources.ts +2 -0
  234. package/src/plugins/builtin/dividend-yield/client.ts +131 -23
  235. package/src/plugins/builtin/dividend-yield/headless.ts +44 -16
  236. package/src/plugins/builtin/dividend-yield/model.ts +8 -5
  237. package/src/plugins/builtin/dividend-yield/pane.tsx +38 -13
  238. package/src/plugins/builtin/dividend-yield/ranking.ts +37 -14
  239. package/src/plugins/builtin/dividend-yield/types.ts +20 -2
  240. package/src/plugins/builtin/dividend-yield/view.ts +30 -2
  241. package/src/plugins/builtin/docs/targets.ts +5 -1
  242. package/src/plugins/builtin/doe/headless.ts +3 -2
  243. package/src/plugins/builtin/doe/pane.tsx +13 -3
  244. package/src/plugins/builtin/earnings/board-model.ts +16 -15
  245. package/src/plugins/builtin/earnings/board.tsx +62 -71
  246. package/src/plugins/builtin/earnings/headless.ts +40 -4
  247. package/src/plugins/builtin/earnings/scope.ts +31 -43
  248. package/src/plugins/builtin/earnings-calls/calendar.tsx +2 -2
  249. package/src/plugins/builtin/earnings-calls/data.ts +18 -6
  250. package/src/plugins/builtin/earnings-calls/format.ts +31 -2
  251. package/src/plugins/builtin/earnings-calls/headless.ts +20 -5
  252. package/src/plugins/builtin/earnings-calls/model.ts +6 -1
  253. package/src/plugins/builtin/earnings-calls/pane.tsx +12 -4
  254. package/src/plugins/builtin/econ/calendar-model.ts +48 -1
  255. package/src/plugins/builtin/econ/calendar-source.ts +4 -2
  256. package/src/plugins/builtin/econ/detail-view.tsx +2 -2
  257. package/src/plugins/builtin/econ/figures.ts +48 -0
  258. package/src/plugins/builtin/econ/index.tsx +14 -5
  259. package/src/plugins/builtin/econ/print-watch.ts +3 -1
  260. package/src/plugins/builtin/econ/types.ts +2 -0
  261. package/src/plugins/builtin/econ-statistics/client.ts +11 -0
  262. package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
  263. package/src/plugins/builtin/econ-statistics/headless.ts +16 -6
  264. package/src/plugins/builtin/econ-statistics/pane.tsx +8 -4
  265. package/src/plugins/builtin/econ-statistics/stats.ts +39 -9
  266. package/src/plugins/builtin/equity-screener/categories.ts +179 -0
  267. package/src/plugins/builtin/equity-screener/client.ts +4 -2
  268. package/src/plugins/builtin/equity-screener/headless.ts +144 -13
  269. package/src/plugins/builtin/equity-screener/ideas.ts +49 -0
  270. package/src/plugins/builtin/equity-screener/model.ts +99 -10
  271. package/src/plugins/builtin/equity-screener/pane.tsx +68 -13
  272. package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
  273. package/src/plugins/builtin/futures/use-board-terms.ts +42 -28
  274. package/src/plugins/builtin/futures/use-front-returns.ts +45 -28
  275. package/src/plugins/builtin/fx-matrix/client.ts +6 -3
  276. package/src/plugins/builtin/fx-matrix/headless.ts +1 -1
  277. package/src/plugins/builtin/fx-matrix/index.tsx +4 -2
  278. package/src/plugins/builtin/fx-matrix/live-legs.ts +42 -3
  279. package/src/plugins/builtin/gpu/headless.ts +8 -2
  280. package/src/plugins/builtin/help/function-index.ts +4 -1
  281. package/src/plugins/builtin/help/index.tsx +62 -59
  282. package/src/plugins/builtin/help/shortcut-model.ts +1 -1
  283. package/src/plugins/builtin/help/shortcut-table.tsx +5 -6
  284. package/src/plugins/builtin/help/table-cells.tsx +13 -2
  285. package/src/plugins/builtin/holders/beneficial-client.ts +5 -1
  286. package/src/plugins/builtin/holders/beneficial-view.tsx +3 -0
  287. package/src/plugins/builtin/holders/fund-holders.ts +51 -0
  288. package/src/plugins/builtin/holders/headless.ts +22 -3
  289. package/src/plugins/builtin/holders/pane.tsx +3 -0
  290. package/src/plugins/builtin/holders/report-rows.ts +4 -1
  291. package/src/plugins/builtin/insider/headless.ts +2 -2
  292. package/src/plugins/builtin/ipo-calendar/client.ts +18 -2
  293. package/src/plugins/builtin/ipo-calendar/headless.ts +75 -11
  294. package/src/plugins/builtin/ipo-calendar/listing-ipo.ts +160 -0
  295. package/src/plugins/builtin/ipo-calendar/model.ts +98 -2
  296. package/src/plugins/builtin/iv-history/client.ts +52 -11
  297. package/src/plugins/builtin/iv-history/format.ts +36 -23
  298. package/src/plugins/builtin/iv-history/headless.ts +33 -18
  299. package/src/plugins/builtin/iv-history/index.tsx +2 -2
  300. package/src/plugins/builtin/iv-history/model.ts +163 -27
  301. package/src/plugins/builtin/iv-history/pane.tsx +10 -6
  302. package/src/plugins/builtin/iv-history/screen-pane.tsx +22 -15
  303. package/src/plugins/builtin/iv-history/universe.ts +5 -1
  304. package/src/plugins/builtin/jobs/client.ts +25 -29
  305. package/src/plugins/builtin/kelly-sizer/model.ts +12 -8
  306. package/src/plugins/builtin/kelly-sizer/pane.tsx +9 -9
  307. package/src/plugins/builtin/kelly-sizer/sections.tsx +19 -10
  308. package/src/plugins/builtin/kelly-sizer/types.ts +10 -4
  309. package/src/plugins/builtin/macro-day/headless.ts +12 -5
  310. package/src/plugins/builtin/macro-day/index.tsx +1 -1
  311. package/src/plugins/builtin/macro-day/model.ts +39 -5
  312. package/src/plugins/builtin/macro-day/pane.tsx +27 -19
  313. package/src/plugins/builtin/macro-day/release-table.ts +2 -2
  314. package/src/plugins/builtin/market-heatmap/data.ts +8 -33
  315. package/src/plugins/builtin/market-movers/client.ts +27 -7
  316. package/src/plugins/builtin/market-movers/headless.ts +34 -12
  317. package/src/plugins/builtin/market-movers/index.tsx +26 -14
  318. package/src/plugins/builtin/market-movers/min-float.ts +74 -0
  319. package/src/plugins/builtin/market-movers/model.ts +4 -1
  320. package/src/plugins/builtin/market-movers/screener.ts +22 -4
  321. package/src/plugins/builtin/market-movers/session-body.tsx +13 -8
  322. package/src/plugins/builtin/market-movers/session-table.ts +4 -3
  323. package/src/plugins/builtin/market-movers/table.ts +17 -5
  324. package/src/plugins/builtin/members/breadth-pane.tsx +2 -1
  325. package/src/plugins/builtin/mna/headless.ts +2 -0
  326. package/src/plugins/builtin/mna/pane.tsx +3 -1
  327. package/src/plugins/builtin/news/headless.ts +3 -1
  328. package/src/plugins/builtin/news/index.tsx +4 -0
  329. package/src/plugins/builtin/news/wire/categories.ts +19 -3
  330. package/src/plugins/builtin/news/wire/industry-pane.tsx +1 -0
  331. package/src/plugins/builtin/news/wire/news/footer.ts +31 -5
  332. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +6 -1
  333. package/src/plugins/builtin/news/wire/news/table.tsx +2 -2
  334. package/src/plugins/builtin/openfda/client.ts +14 -2
  335. package/src/plugins/builtin/openfda/index.tsx +3 -2
  336. package/src/plugins/builtin/openfda/model.ts +9 -0
  337. package/src/plugins/builtin/openfda/pane.tsx +42 -6
  338. package/src/plugins/builtin/options/analytics.ts +29 -5
  339. package/src/plugins/builtin/options/enrichment.ts +17 -5
  340. package/src/plugins/builtin/options/table.ts +5 -2
  341. package/src/plugins/builtin/options/types.ts +2 -0
  342. package/src/plugins/builtin/options/view.tsx +17 -4
  343. package/src/plugins/builtin/options-positioning/client.ts +11 -0
  344. package/src/plugins/builtin/options-positioning/headless.ts +21 -8
  345. package/src/plugins/builtin/options-positioning/index.tsx +1 -1
  346. package/src/plugins/builtin/options-positioning/model.ts +78 -9
  347. package/src/plugins/builtin/options-positioning/pane.tsx +21 -6
  348. package/src/plugins/builtin/options-scenario/client.ts +116 -47
  349. package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +142 -10
  350. package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +206 -55
  351. package/src/plugins/builtin/options-scenario/csp-screen.ts +63 -23
  352. package/src/plugins/builtin/options-scenario/csp.ts +8 -2
  353. package/src/plugins/builtin/options-scenario/index.tsx +9 -7
  354. package/src/plugins/builtin/options-scenario/strategy-screen.ts +532 -0
  355. package/src/plugins/builtin/pe-band/model.ts +12 -49
  356. package/src/plugins/builtin/perps/cascade-tabs.tsx +161 -0
  357. package/src/plugins/builtin/perps/cascade.ts +236 -0
  358. package/src/plugins/builtin/perps/client.ts +56 -2
  359. package/src/plugins/builtin/perps/headless.ts +65 -9
  360. package/src/plugins/builtin/perps/index.tsx +7 -5
  361. package/src/plugins/builtin/perps/model.ts +18 -4
  362. package/src/plugins/builtin/perps/pane.tsx +19 -22
  363. package/src/plugins/builtin/perps/status-line.ts +25 -0
  364. package/src/plugins/builtin/plugin-module.ts +5 -1
  365. package/src/plugins/builtin/portfolio-list/cached-account.ts +3 -1
  366. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +2 -8
  367. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -4
  368. package/src/plugins/builtin/portfolio-list/cli/render.ts +7 -5
  369. package/src/plugins/builtin/portfolio-list/pane/futures-front.ts +40 -0
  370. package/src/plugins/builtin/portfolio-list/pane/index.tsx +6 -7
  371. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +5 -2
  372. package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +3 -2
  373. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +0 -14
  374. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +5 -2
  375. package/src/plugins/builtin/portfolio-list/table.tsx +15 -9
  376. package/src/plugins/builtin/primary-dealers/client.ts +13 -38
  377. package/src/plugins/builtin/realized-vol/client.ts +13 -5
  378. package/src/plugins/builtin/realized-vol/evidence.ts +29 -4
  379. package/src/plugins/builtin/realized-vol/headless.ts +8 -4
  380. package/src/plugins/builtin/realized-vol/index.tsx +2 -2
  381. package/src/plugins/builtin/realized-vol/model.ts +37 -6
  382. package/src/plugins/builtin/realized-vol/pane.tsx +11 -9
  383. package/src/plugins/builtin/relative-rotation/client.ts +18 -4
  384. package/src/plugins/builtin/relative-rotation/headless.ts +3 -3
  385. package/src/plugins/builtin/relative-rotation/index.tsx +2 -1
  386. package/src/plugins/builtin/relative-rotation/model.ts +16 -40
  387. package/src/plugins/builtin/relative-rotation/pane.tsx +3 -2
  388. package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -2
  389. package/src/plugins/builtin/research/event-model.ts +40 -12
  390. package/src/plugins/builtin/research/headless.ts +1 -1
  391. package/src/plugins/builtin/research/index.tsx +7 -3
  392. package/src/plugins/builtin/research/relative-valuation-headless.ts +13 -26
  393. package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
  394. package/src/plugins/builtin/research/relative-valuation-pane.tsx +59 -20
  395. package/src/plugins/builtin/research/relative-valuation-peers.ts +217 -20
  396. package/src/plugins/builtin/research/relative-valuation-settings.ts +44 -0
  397. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -3
  398. package/src/plugins/builtin/reverse-dcf/model.ts +61 -6
  399. package/src/plugins/builtin/reverse-dcf/pane.tsx +7 -4
  400. package/src/plugins/builtin/risk-factors/data.ts +4 -2
  401. package/src/plugins/builtin/risk-factors/headless.ts +5 -2
  402. package/src/plugins/builtin/roadmap/index.tsx +28 -0
  403. package/src/plugins/builtin/roadmap/model.ts +67 -0
  404. package/src/plugins/builtin/roadmap/pane.tsx +244 -0
  405. package/src/plugins/builtin/scanner/flow-model.ts +50 -4
  406. package/src/plugins/builtin/scanner/flow-pane.tsx +367 -65
  407. package/src/plugins/builtin/scanner/flow-session.ts +146 -0
  408. package/src/plugins/builtin/scanner/headless.ts +204 -35
  409. package/src/plugins/builtin/scanner/index.ts +16 -5
  410. package/src/plugins/builtin/scanner/live-headless.ts +48 -5
  411. package/src/plugins/builtin/scanner/live-model.ts +11 -0
  412. package/src/plugins/builtin/scanner/live-pane.tsx +3 -3
  413. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  414. package/src/plugins/builtin/seasonality/headless.ts +10 -1
  415. package/src/plugins/builtin/sec/fund-facts-view.tsx +5 -2
  416. package/src/plugins/builtin/sec/fund-facts.ts +116 -34
  417. package/src/plugins/builtin/sec/headless.ts +4 -2
  418. package/src/plugins/builtin/sec/index.tsx +3 -0
  419. package/src/plugins/builtin/series-inventory.ts +2 -0
  420. package/src/plugins/builtin/shared/cloud-resource.ts +16 -7
  421. package/src/plugins/builtin/shared/cloud-upgrade.ts +4 -3
  422. package/src/plugins/builtin/shared/earnings-calendar.ts +6 -2
  423. package/src/plugins/builtin/shared/earnings-report.ts +2 -2
  424. package/src/plugins/builtin/shared/headless-market-data.ts +2 -2
  425. package/src/plugins/builtin/shared/job-posting.ts +1 -0
  426. package/src/plugins/builtin/shared/members-menu.tsx +3 -2
  427. package/src/plugins/builtin/shared/pro-lock-note.ts +12 -12
  428. package/src/plugins/builtin/shared/report-freshness.ts +11 -2
  429. package/src/plugins/builtin/shared/research-cells.tsx +1 -1
  430. package/src/plugins/builtin/shared/series-cache.ts +4 -4
  431. package/src/plugins/builtin/shared/ticker-choice.tsx +2 -1
  432. package/src/plugins/builtin/shared/ticker-company.ts +77 -0
  433. package/src/plugins/builtin/shared/use-quote-board.ts +2 -1
  434. package/src/plugins/builtin/shared/volatility/live-session.ts +34 -8
  435. package/src/plugins/builtin/shared/volatility/pricing.ts +19 -0
  436. package/src/plugins/builtin/shared/wall-view-lifecycle.ts +3 -1
  437. package/src/plugins/builtin/themes/model.ts +16 -7
  438. package/src/plugins/builtin/themes/pane.tsx +5 -5
  439. package/src/plugins/builtin/thirteenf/api.ts +12 -1
  440. package/src/plugins/builtin/thirteenf/data.ts +7 -7
  441. package/src/plugins/builtin/thirteenf/headless.ts +31 -4
  442. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +43 -18
  443. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +3 -2
  444. package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +62 -0
  445. package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +83 -46
  446. package/src/plugins/builtin/ticker-detail/headless.ts +31 -16
  447. package/src/plugins/builtin/ticker-detail/overview/model.ts +11 -3
  448. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +2 -1
  449. package/src/plugins/builtin/ticker-detail/pane.tsx +14 -8
  450. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -1
  451. package/src/plugins/builtin/ticker-detail/settings.ts +1 -0
  452. package/src/plugins/builtin/time-sales/pane.tsx +31 -19
  453. package/src/plugins/builtin/treasury-auctions/headless.ts +21 -12
  454. package/src/plugins/builtin/treasury-auctions/pane.tsx +9 -3
  455. package/src/plugins/builtin/treasury-daily/client.ts +41 -5
  456. package/src/plugins/builtin/treasury-daily/headless.ts +2 -2
  457. package/src/plugins/builtin/treasury-daily/index.tsx +1 -1
  458. package/src/plugins/builtin/treasury-daily/model.ts +50 -3
  459. package/src/plugins/builtin/treasury-daily/pane.tsx +81 -2
  460. package/src/plugins/builtin/vol-surface/client.ts +3 -13
  461. package/src/plugins/builtin/vol-surface/headless.ts +3 -3
  462. package/src/plugins/builtin/vol-surface/index.tsx +1 -1
  463. package/src/plugins/builtin/vol-surface/pane.tsx +7 -5
  464. package/src/plugins/builtin/volatility/index.tsx +2 -2
  465. package/src/plugins/builtin/volatility/pane.tsx +7 -4
  466. package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
  467. package/src/plugins/builtin/yield-curve/client.ts +61 -1
  468. package/src/plugins/builtin/yield-curve/headless.ts +9 -6
  469. package/src/plugins/builtin/yield-curve/index.tsx +34 -10
  470. package/src/plugins/builtin/yield-curve/swaps.tsx +9 -5
  471. package/src/plugins/catalog-browser.ts +2 -1
  472. package/src/plugins/catalog-manifest.ts +2 -1
  473. package/src/plugins/host-link.ts +1 -1
  474. package/src/plugins/marketplace/model.ts +18 -5
  475. package/src/portfolio/column-values.ts +11 -7
  476. package/src/portfolio/futures-positions.ts +6 -3
  477. package/src/portfolio/position-quantity.ts +15 -9
  478. package/src/remote/resources.ts +2 -1
  479. package/src/renderers/dom/data-table/row.tsx +26 -0
  480. package/src/renderers/dom/desktop/icons.tsx +7 -0
  481. package/src/renderers/dom/desktop/list-view.tsx +13 -10
  482. package/src/renderers/dom/desktop/query-bar.tsx +4 -2
  483. package/src/renderers/dom/desktop/select-field.tsx +4 -2
  484. package/src/renderers/dom/host/chart-surface.tsx +21 -0
  485. package/src/renderers/electrobun/bun/desktop/app-name.ts +3 -3
  486. package/src/renderers/electrobun/bun/desktop/backend.ts +21 -10
  487. package/src/renderers/electrobun/bun/desktop/host-requests.ts +15 -0
  488. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
  489. package/src/renderers/electrobun/bun/desktop/installer-folder.ts +731 -0
  490. package/src/renderers/electrobun/bun/desktop/relaunch.ts +102 -10
  491. package/src/renderers/electrobun/bun/desktop/update.ts +115 -14
  492. package/src/renderers/electrobun/bun/desktop/view-ids.ts +52 -0
  493. package/src/renderers/electrobun/bun/desktop/windows-script.ts +92 -0
  494. package/src/renderers/electrobun/bun/index.ts +26 -2
  495. package/src/renderers/electrobun/bun/window/create.ts +43 -4
  496. package/src/renderers/electrobun/bun/window/events.ts +20 -0
  497. package/src/renderers/electrobun/bun/window/windows-custom-chrome.ts +9 -2
  498. package/src/renderers/electrobun/bun/window/windows-native.ts +78 -0
  499. package/src/renderers/electrobun/bun/window/windows-quit.ts +57 -0
  500. package/src/renderers/electrobun/shared/app-name.ts +13 -0
  501. package/src/renderers/electrobun/shared/protocol.ts +12 -0
  502. package/src/renderers/electrobun/view/backend-rpc.ts +6 -0
  503. package/src/renderers/electrobun/view/main.tsx +24 -1
  504. package/src/renderers/electrobun/view/news-source.ts +5 -1
  505. package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
  506. package/src/renderers/electrobun/view/ui-host.tsx +8 -0
  507. package/src/renderers/electrobun/view/view-ids.ts +85 -0
  508. package/src/renderers/opentui/start.tsx +5 -0
  509. package/src/series/registry.ts +1 -0
  510. package/src/series/types.ts +6 -0
  511. package/src/series/validate.ts +1 -0
  512. package/src/sources/gloomberb-cloud/fred-series.ts +3 -1
  513. package/src/sources/gloomberb-cloud/index.ts +43 -8
  514. package/src/sources/gloomberb-cloud/news.ts +47 -7
  515. package/src/sources/provider-router/financials.ts +15 -3
  516. package/src/sources/provider-router/index.ts +8 -5
  517. package/src/state/ticker-cleanup.ts +50 -0
  518. package/src/sync/controller.ts +4 -3
  519. package/src/sync/core-contributors.ts +90 -31
  520. package/src/telemetry/stall-reports-dom.ts +38 -0
  521. package/src/telemetry/stall-reports-node.ts +75 -0
  522. package/src/telemetry/stall-reports.ts +214 -0
  523. package/src/test-support/preload.ts +14 -0
  524. package/src/theme/themes.ts +10 -0
  525. package/src/tickers/company-name-listing.ts +53 -13
  526. package/src/tickers/company-name.ts +50 -4
  527. package/src/tickers/index-roots.ts +7 -0
  528. package/src/tickers/listing-fate.ts +2 -2
  529. package/src/tickers/listing-venues.ts +15 -3
  530. package/src/tickers/search/index.ts +6 -2
  531. package/src/tickers/search/ranking.ts +5 -1
  532. package/src/tickers/search/upsert.ts +13 -0
  533. package/src/tickers/terminal-codes.ts +19 -0
  534. package/src/tickers/ticker-changes.ts +8 -4
  535. package/src/time-series/alignment.ts +1 -1
  536. package/src/time-series/economic.ts +18 -4
  537. package/src/time-series/field-catalog.ts +1 -1
  538. package/src/time-series/filed-trailing-eps.ts +197 -0
  539. package/src/time-series/fundamentals.ts +44 -1
  540. package/src/time-series/ice-credit.ts +3 -0
  541. package/src/time-series/price-comparison.ts +30 -2
  542. package/src/time-series/resolve-context.ts +3 -1
  543. package/src/time-series/resolve.ts +56 -4
  544. package/src/time-series/session-bar.ts +11 -0
  545. package/src/time-series/spec.ts +3 -2
  546. package/src/time-series/studies.ts +8 -2
  547. package/src/time-series/total-return.ts +160 -0
  548. package/src/time-series/transforms.ts +2 -3
  549. package/src/time-series/types.ts +22 -1
  550. package/src/types/company-from.ts +40 -0
  551. package/src/types/data-provider.ts +7 -0
  552. package/src/types/financials.ts +59 -1
  553. package/src/types/headless.ts +27 -3
  554. package/src/types/news-source.ts +1 -0
  555. package/src/types/plugin.ts +9 -0
  556. package/src/ui/host.tsx +3 -0
  557. package/src/updater/failure.ts +4 -1
  558. package/src/utils/cli-output.ts +42 -1
  559. package/src/utils/crypto-pair.ts +165 -7
  560. package/src/utils/declared-dividends.ts +1 -1
  561. package/src/utils/forward-dividend.ts +2 -0
  562. package/src/utils/fundamentals.ts +60 -8
  563. package/src/utils/fx-market-hours.ts +26 -3
  564. package/src/utils/main-thread-stall.ts +35 -2
  565. package/src/utils/market-capitalization.ts +24 -1
  566. package/src/utils/price-history.ts +7 -1
  567. package/src/utils/shell-word.ts +8 -0
  568. package/src/utils/trailing-dividend.ts +53 -4
  569. package/src/version.ts +2 -2
  570. package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +0 -17
@@ -0,0 +1,731 @@
1
+ import { lstat, mkdir, readdir, readFile, rm, writeFile } from "fs/promises";
2
+ import { tmpdir } from "os";
3
+ import { win32 } from "path";
4
+ import { logFailure } from "../../../../utils/log-failure";
5
+ import { DESKTOP_APP_IDENTIFIER, DESKTOP_APP_IDENTIFIERS } from "../../shared/app-name";
6
+ import { psString, runScriptAfterExit, runScriptInBackground, writePowerShellScript } from "./windows-script";
7
+
8
+ /**
9
+ * The Windows installer (scripts/windows-installer.iss) puts the app in its
10
+ * own folder, `Programs\Gloom` unless the user picks another, and its Start
11
+ * menu and Desktop shortcuts, the `gloom` command on PATH and its uninstall
12
+ * entry lead there. Electrobun's updater knows nothing of it: it installs
13
+ * every update into its own folder, `<app data>\<identifier>\<channel>\app`,
14
+ * and opens it from there. The next start from a shortcut opened the build
15
+ * from before the update, which offered the same update again, and
16
+ * uninstalling left the updated copy behind.
17
+ *
18
+ * So a build that finds itself in the updater's folder while the installer's
19
+ * install is recorded quits before any window shows, and a script moves it
20
+ * into the installer's folder and opens it from there. A build running from
21
+ * the installer's folder removes a copy an earlier update left in the
22
+ * updater's. Either way the shortcuts and uninstall entry Electrobun's own
23
+ * setup made for its folder, if any, follow to the installer's.
24
+ *
25
+ * The app quits for the move only once the script has checked it can make
26
+ * it, and tries a build at most three times, ten minutes apart: a move that
27
+ * cannot happen never makes it quit and open again at every start.
28
+ */
29
+
30
+ /** The installer's AppId, which names its uninstall entry (windows-installer.iss). */
31
+ export const WINDOWS_INSTALLER_APP_ID = "{D7B2304C-840B-4E5F-956A-3D1C66E67B35}";
32
+ const INSTALLER_ENTRY = `${WINDOWS_INSTALLER_APP_ID}_is1`;
33
+
34
+ export interface InstallerFolderContext {
35
+ platform: string;
36
+ execPath: string;
37
+ homeDir: string;
38
+ /** `%LOCALAPPDATA%`, where Electrobun keeps the updater's folder. */
39
+ localAppData?: string;
40
+ /** The folder the installer's uninstall entry records, or null; read from the registry when left out. */
41
+ readInstallerFolder?: () => Promise<string | null>;
42
+ }
43
+
44
+ export type InstallerFolderMove =
45
+ /** Not an installer install, or already in its folder with nothing left elsewhere. */
46
+ | { kind: "none" }
47
+ /** The app runs from the updater's folder, and quits for the script to move it and open it again. */
48
+ | { kind: "relaunching" }
49
+ /** A copy left in the updater's folder goes in the background. */
50
+ | { kind: "removing" }
51
+ /** The app runs from where it is: a try failed a moment ago, tries failed too often for this build, or the installer's folder is in use. */
52
+ | { kind: "skipped" }
53
+ | { kind: "failed" };
54
+
55
+ /** How long a try at moving the app keeps it from trying again, so a move that cannot happen never makes it quit in a loop. */
56
+ const RETRY_AFTER_MS = 10 * 60_000;
57
+ /** Tries at moving one build before it stays in the updater's folder until the next update. */
58
+ const MAX_MOVE_TRIES = 3;
59
+ /** How long after the start a copy left in the updater's folder goes, out of the launch's way. */
60
+ const REMOVAL_DELAY_MS = 10_000;
61
+ const LAUNCHER = "bin\\launcher.exe";
62
+
63
+ function samePath(left: string, right: string): boolean {
64
+ return win32.resolve(left).toLowerCase() === win32.resolve(right).toLowerCase();
65
+ }
66
+
67
+ async function exists(path: string): Promise<boolean> {
68
+ return lstat(path).then(() => true, () => false);
69
+ }
70
+
71
+ function isDesktopIdentifierName(name: string): boolean {
72
+ return DESKTOP_APP_IDENTIFIERS.some((identifier) => identifier.toLowerCase() === name.toLowerCase());
73
+ }
74
+
75
+ /**
76
+ * The updater's folder the app runs from, from its `bin\bun.exe`:
77
+ * `<local app data>\<identifier>\<channel>\app`, under either identifier the
78
+ * app has carried. Null anywhere else.
79
+ */
80
+ export function updaterAppFolder(execPath: string, localAppData: string): { folder: string; channel: string } | null {
81
+ const relative = win32.relative(localAppData, execPath);
82
+ if (!relative || relative.startsWith("..") || win32.isAbsolute(relative)) return null;
83
+ const [identifier, channel, app, bin, ...rest] = relative.split("\\");
84
+ if (!identifier || !channel || rest.length !== 1 || !isDesktopIdentifierName(identifier)) return null;
85
+ if (app?.toLowerCase() !== "app" || bin?.toLowerCase() !== "bin") return null;
86
+ return { folder: win32.join(localAppData, identifier, channel, "app"), channel };
87
+ }
88
+
89
+ /**
90
+ * The updater's folders for `channel` under either identifier, and what a
91
+ * removal that did not finish left of one (`app.removing-*`).
92
+ */
93
+ async function updaterFolders(localAppData: string, channel: string): Promise<string[]> {
94
+ const folders: string[] = [];
95
+ for (const identifier of DESKTOP_APP_IDENTIFIERS) {
96
+ const parent = win32.join(localAppData, identifier, channel);
97
+ for (const entry of await readdir(parent).catch(() => [] as string[])) {
98
+ const name = entry.toLowerCase();
99
+ if (name === "app" || name.startsWith("app.removing-")) folders.push(win32.join(parent, entry));
100
+ }
101
+ }
102
+ return folders;
103
+ }
104
+
105
+ /** The value `name` holds in a `.reg` export, unescaped; null when it holds none or no string. */
106
+ export function regExportValue(text: string, name: string): string | null {
107
+ const prefix = `${JSON.stringify(name)}=`;
108
+ for (const line of text.split(/\r?\n/)) {
109
+ if (!line.startsWith(prefix)) continue;
110
+ const value = line.slice(prefix.length);
111
+ if (value.length < 2 || !value.startsWith("\"") || !value.endsWith("\"")) return null;
112
+ return value.slice(1, -1).replace(/\\(.)/g, "$1");
113
+ }
114
+ return null;
115
+ }
116
+
117
+ /**
118
+ * The folder the installer's uninstall entry records. `reg export` writes
119
+ * UTF-16, where `reg query` would print a path with an accent in the console's
120
+ * code page.
121
+ */
122
+ async function readInstallerFolderFromRegistry(): Promise<string | null> {
123
+ const file = win32.join(tmpdir(), `gloom-installer-entry-${process.pid}.reg`);
124
+ try {
125
+ const key = `HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${INSTALLER_ENTRY}`;
126
+ const result = Bun.spawnSync(["reg.exe", "export", key, file, "/y"], { stdio: ["ignore", "ignore", "ignore"], windowsHide: true });
127
+ if (!result.success) return null;
128
+ return regExportValue(new TextDecoder("utf-16le").decode(await readFile(file)), "Inno Setup: App Path");
129
+ } finally {
130
+ await rm(file, { force: true });
131
+ }
132
+ }
133
+
134
+ /**
135
+ * The installer's folder, when it holds the app. A move cut short (a power
136
+ * cut between its renames) can leave its launcher in what the move set aside
137
+ * there: the next move puts that back before anything else.
138
+ */
139
+ async function installerFolder(read: () => Promise<string | null>): Promise<string | null> {
140
+ const folder = await read().catch(() => null);
141
+ if (!folder) return null;
142
+ if (await exists(win32.join(folder, LAUNCHER))) return win32.resolve(folder);
143
+ for (const entry of await readdir(folder).catch(() => [] as string[])) {
144
+ if (/^\.gloom-previous-/i.test(entry) && await exists(win32.join(folder, entry, LAUNCHER))) return win32.resolve(folder);
145
+ }
146
+ return null;
147
+ }
148
+
149
+ async function buildChannel(appFolder: string): Promise<string> {
150
+ const info = await readFile(win32.join(appFolder, "Resources", "version.json"), "utf8").then((text) => JSON.parse(text) as { channel?: unknown }, () => null);
151
+ return typeof info?.channel === "string" && info.channel ? info.channel : "stable";
152
+ }
153
+
154
+ interface InstallerFolderScript {
155
+ /** The installer's folder. */
156
+ target: string;
157
+ /** The updater's folders to empty into it or remove; with `afterExit`, the first holds the running app. */
158
+ sources: string[];
159
+ /** This build's app data folder: the script's log goes there, and a folder it removes is moved there first. */
160
+ folder: string;
161
+ log: string;
162
+ /**
163
+ * The app runs from `sources[0]`: the script waits for everything running
164
+ * from there to quit, moves it into `target` and opens it again. `cancel`
165
+ * is the file the app writes when it stops waiting for the script.
166
+ */
167
+ afterExit?: { cancel: string; taskName: string };
168
+ }
169
+
170
+ // PowerShell 5.1. The build goes into the installer's folder by renames only:
171
+ // from another drive it is copied beside the installed build first. A folder
172
+ // of the build (bin, Resources) replaces the installed one whole when that
173
+ // holds nothing but the app's files; in one that also holds other files (an
174
+ // install in C:\Tools, whose bin was already there) only the app's files are
175
+ // replaced. What it replaces waits beside it, in the installer's folder,
176
+ // until all of the build is in, and goes back if it is not. Then the app
177
+ // opens again, and the rest (shortcuts, uninstall entries, the copy it came
178
+ // from) follows.
179
+ const SCRIPT_BODY = String.raw`
180
+ $Launcher = 'bin\launcher.exe'
181
+ $FileList = 'Resources\update-files.txt'
182
+ $UninstallPath = 'Software\Microsoft\Windows\CurrentVersion\Uninstall'
183
+ $Journal = New-Object System.Collections.Generic.List[object]
184
+ $Exited = $false
185
+ $Opened = $false
186
+
187
+ # UTF-8 without a byte order mark: the app reads the answer from the first
188
+ # line, and a path with an accent in it reads back as it is.
189
+ $LogEncoding = New-Object System.Text.UTF8Encoding $false
190
+
191
+ function Write-Log([string]$Message) {
192
+ for ($Attempt = 0; $Attempt -lt 10; $Attempt += 1) {
193
+ try { [IO.File]::AppendAllText($Log, (Get-Date).ToString('o') + ' ' + $Message + [Environment]::NewLine, $LogEncoding); return } catch { Start-Sleep -Milliseconds 100 }
194
+ }
195
+ }
196
+
197
+ function Test-Under([string]$Path, [string]$Root) {
198
+ if (-not $Path) { return $false }
199
+ return $Path.Equals($Root, [StringComparison]::OrdinalIgnoreCase) -or $Path.StartsWith($Root + '\', [StringComparison]::OrdinalIgnoreCase)
200
+ }
201
+
202
+ # The processes whose program is in $Root, whatever started them.
203
+ function Get-RunningUnder([string]$Root) {
204
+ @(Get-Process -ErrorAction SilentlyContinue | Where-Object { try { Test-Under $_.Path $Root } catch { $false } })
205
+ }
206
+
207
+ # The processes running one of the build's programs from the installer's
208
+ # folder, such as the gloom command: they keep its files from being replaced.
209
+ # Another program the user keeps in that folder does not.
210
+ function Get-AppRunningInTarget {
211
+ @(Get-RunningUnder $Target | Where-Object { $BuildFiles.Contains($_.Path.Substring($Target.Length + 1)) })
212
+ }
213
+
214
+ function Wait-Until([int]$Seconds, [scriptblock]$Condition) {
215
+ for ($Wait = 0; $Wait -lt ($Seconds * 2); $Wait += 1) {
216
+ if (& $Condition) { return $true }
217
+ Start-Sleep -Milliseconds 500
218
+ }
219
+ return [bool](& $Condition)
220
+ }
221
+
222
+ function Invoke-Retried([scriptblock]$Action, [string]$What) {
223
+ for ($Attempt = 1; $true; $Attempt += 1) {
224
+ try { & $Action | Out-Null; return } catch {
225
+ if ($Attempt -ge 20) { throw ('could not ' + $What + ': ' + $_.Exception.Message) }
226
+ Start-Sleep -Milliseconds 500
227
+ }
228
+ }
229
+ }
230
+
231
+ function Get-Stamp { (Get-Date).ToUniversalTime().ToString('yyyyMMddHHmmss') + '-' + [guid]::NewGuid().ToString('N').Substring(0, 8) }
232
+
233
+ function Test-Folder([string]$Path) {
234
+ $Item = Get-Item -LiteralPath $Path -Force -ErrorAction SilentlyContinue
235
+ return ($null -ne $Item) -and $Item.PSIsContainer -and -not ($Item.Attributes -band [IO.FileAttributes]::ReparsePoint)
236
+ }
237
+
238
+ # The files in $Root, as paths after $Prefix. A link is a file here: it is not followed.
239
+ function Get-Files([string]$Root, [string]$Prefix) {
240
+ foreach ($Item in @(Get-ChildItem -LiteralPath $Root -Force)) {
241
+ $Relative = if ($Prefix) { $Prefix + '\' + $Item.Name } else { $Item.Name }
242
+ if ($Item.PSIsContainer -and -not ($Item.Attributes -band [IO.FileAttributes]::ReparsePoint)) { Get-Files $Item.FullName $Relative } else { $Relative }
243
+ }
244
+ }
245
+
246
+ function Remove-Tree([string]$Path) {
247
+ if (Test-Path -LiteralPath $Path) { Invoke-Retried { Remove-Item -LiteralPath $Path -Recurse -Force } ('remove ' + $Path) }
248
+ }
249
+
250
+ # Makes the missing folders up to $Path, each noted to take back.
251
+ function New-Folder([string]$Path) {
252
+ if (Test-Path -LiteralPath $Path) { return }
253
+ New-Folder (Split-Path -Parent $Path)
254
+ New-Item -ItemType Directory -Path $Path | Out-Null
255
+ $Journal.Add(@($null, $Path))
256
+ }
257
+
258
+ # A rename, noted to take back. Nothing lands inside a folder in its way.
259
+ function Move-Noted([string]$From, [string]$To) {
260
+ if (Test-Path -LiteralPath $To) { throw ($To + ' is in the way') }
261
+ New-Folder (Split-Path -Parent $To)
262
+ Invoke-Retried { Move-Item -LiteralPath $From -Destination $To } ('move ' + $From)
263
+ $Journal.Add(@($From, $To))
264
+ }
265
+
266
+ function Undo-Noted {
267
+ for ($Index = $Journal.Count - 1; $Index -ge 0; $Index -= 1) {
268
+ $Step = $Journal[$Index]
269
+ try {
270
+ if ($null -eq $Step[0]) {
271
+ [IO.Directory]::Delete($Step[1], $false)
272
+ } else {
273
+ if (Test-Path -LiteralPath $Step[0]) { throw ($Step[0] + ' is in the way') }
274
+ Invoke-Retried { Move-Item -LiteralPath $Step[1] -Destination $Step[0] } ('put back ' + $Step[0])
275
+ }
276
+ } catch { Write-Log ('could not take back a step: ' + $_.Exception.Message) }
277
+ }
278
+ $Journal.Clear()
279
+ }
280
+
281
+ # The files earlier updates put in the installer's folder, as they listed them.
282
+ function Read-FileList {
283
+ $Path = Join-Path $Target $FileList
284
+ if (-not (Test-Path -LiteralPath $Path)) { return }
285
+ foreach ($Line in [IO.File]::ReadAllLines($Path)) {
286
+ $Line = $Line.Trim()
287
+ if ($Line -and $Line -notmatch '^[\\/]|:|\.\.') { $Line }
288
+ }
289
+ }
290
+
291
+ function Install-Build([string]$Source) {
292
+ $Stamp = Get-Stamp
293
+ $From = $Source
294
+ $Next = $null
295
+ if (-not [IO.Path]::GetPathRoot($Source).Equals([IO.Path]::GetPathRoot($Target), [StringComparison]::OrdinalIgnoreCase)) {
296
+ $Next = Join-Path $Target ('.gloom-next-' + $Stamp)
297
+ try {
298
+ New-Item -ItemType Directory -Path $Next | Out-Null
299
+ foreach ($Entry in @(Get-ChildItem -LiteralPath $Source -Force)) {
300
+ Copy-Item -LiteralPath $Entry.FullName -Destination (Join-Path $Next $Entry.Name) -Recurse -Force
301
+ }
302
+ } catch {
303
+ $Reason = $_.Exception.Message
304
+ try { Remove-Tree $Next } catch { Write-Log ('could not remove ' + $Next + ': ' + $_.Exception.Message) }
305
+ throw ('could not copy the build into ' + $Target + ': ' + $Reason)
306
+ }
307
+ $From = $Next
308
+ }
309
+
310
+ $NewFiles = @(Get-Files $From '')
311
+ $Listed = @(Read-FileList)
312
+ $Own = New-Object 'System.Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase)
313
+ foreach ($File in ($NewFiles + $Listed + @($FileList))) { [void]$Own.Add($File) }
314
+ $Aside = Join-Path $Target ('.gloom-previous-' + $Stamp)
315
+ $Journal.Clear()
316
+ try {
317
+ foreach ($Entry in @(Get-ChildItem -LiteralPath $From -Force)) {
318
+ $Name = $Entry.Name
319
+ $Old = Join-Path $Target $Name
320
+ if (-not (Test-Path -LiteralPath $Old)) {
321
+ Move-Noted $Entry.FullName $Old
322
+ } elseif (-not $Entry.PSIsContainer) {
323
+ if (Test-Folder $Old) { throw ($Old + ' is a folder') }
324
+ Move-Noted $Old (Join-Path $Aside $Name)
325
+ Move-Noted $Entry.FullName $Old
326
+ } else {
327
+ if (-not (Test-Folder $Old)) { throw ($Old + ' is not a folder') }
328
+ $Others = @(Get-Files $Old $Name | Where-Object { -not $Own.Contains($_) })
329
+ if ($Others.Count -eq 0) {
330
+ Move-Noted $Old (Join-Path $Aside $Name)
331
+ Move-Noted $Entry.FullName $Old
332
+ Write-Log ('replaced ' + $Old)
333
+ } else {
334
+ Write-Log ('kept ' + $Others.Count + ' file(s) in ' + $Old + ' that are not the app''s, such as ' + $Others[0])
335
+ foreach ($Relative in @(Get-Files $Entry.FullName $Name)) {
336
+ $Place = Join-Path $Target $Relative
337
+ if (Test-Path -LiteralPath $Place) {
338
+ if (Test-Folder $Place) { throw ($Place + ' is a folder') }
339
+ Move-Noted $Place (Join-Path $Aside $Relative)
340
+ }
341
+ Move-Noted (Join-Path $From $Relative) $Place
342
+ }
343
+ }
344
+ }
345
+ }
346
+ } catch {
347
+ $Reason = $_.Exception.Message
348
+ Undo-Noted
349
+ if ($Next) { try { Remove-Tree $Next } catch { Write-Log ('could not remove ' + $Next + ': ' + $_.Exception.Message) } }
350
+ throw $Reason
351
+ }
352
+ $Journal.Clear()
353
+
354
+ # For the uninstaller, which removes what it installed itself: what updates
355
+ # put here, the files of this build and those of earlier ones still here.
356
+ try {
357
+ $Files = New-Object 'System.Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase)
358
+ foreach ($File in $NewFiles) { [void]$Files.Add($File) }
359
+ foreach ($File in $Listed) { if (Test-Path -LiteralPath (Join-Path $Target $File)) { [void]$Files.Add($File) } }
360
+ [IO.File]::WriteAllLines((Join-Path $Target $FileList), [string[]]@($Files | Sort-Object))
361
+ } catch { Write-Log ('could not list the files for the uninstaller: ' + $_.Exception.Message) }
362
+ foreach ($Leftover in @($Aside, $Next)) {
363
+ if ($Leftover) {
364
+ try { Remove-Tree $Leftover } catch { Write-Log ($_.Exception.Message + '; the app removes it at its next start') }
365
+ }
366
+ }
367
+ }
368
+
369
+ # What a move that did not finish left in the installer's folder.
370
+ # A move cut short (a power cut between the two renames of bin, or an undo
371
+ # that failed) can leave the installed build without some of its files, in
372
+ # what it set aside. A folder or file of the build missing from the
373
+ # installer's folder goes back; file by file only while the launcher is
374
+ # missing, so a file a newer build dropped is not brought back beside it.
375
+ function Restore-SetAside([string]$Aside) {
376
+ try {
377
+ foreach ($Entry in @(Get-ChildItem -LiteralPath $Aside -Force)) {
378
+ $Place = Join-Path $Target $Entry.Name
379
+ if (-not (Test-Path -LiteralPath $Place)) {
380
+ Invoke-Retried { Move-Item -LiteralPath $Entry.FullName -Destination $Place } ('put back ' + $Place)
381
+ Write-Log ('put back ' + $Place)
382
+ }
383
+ }
384
+ if (Test-Path -LiteralPath (Join-Path $Target $Launcher)) { return }
385
+ foreach ($Relative in @(Get-Files $Aside '')) {
386
+ $Place = Join-Path $Target $Relative
387
+ if (Test-Path -LiteralPath $Place) { continue }
388
+ New-Item -ItemType Directory -Force -Path (Split-Path -Parent $Place) | Out-Null
389
+ Invoke-Retried { Move-Item -LiteralPath (Join-Path $Aside $Relative) -Destination $Place } ('put back ' + $Place)
390
+ Write-Log ('put back ' + $Place)
391
+ }
392
+ } catch { Write-Log ('could not put back what ' + $Aside + ' holds: ' + $_.Exception.Message) }
393
+ }
394
+
395
+ # What a move that did not finish left in the installer's folder: a copy of
396
+ # a build on its way in, and what it set aside, newest first. Nothing set
397
+ # aside goes while the installed build has no launcher: it may be the only
398
+ # copy of it.
399
+ function Remove-Staging {
400
+ $Leftovers = @(Get-ChildItem -LiteralPath $Target -Directory -Force -ErrorAction SilentlyContinue |
401
+ Where-Object { $_.Name -like '.gloom-next-*' -or $_.Name -like '.gloom-previous-*' } | Sort-Object Name -Descending)
402
+ foreach ($Leftover in $Leftovers) {
403
+ if ((Get-RunningUnder $Leftover.FullName).Count -gt 0) { continue }
404
+ if ($Leftover.Name -like '.gloom-previous-*') {
405
+ Restore-SetAside $Leftover.FullName
406
+ if (-not (Test-Path -LiteralPath (Join-Path $Target $Launcher))) {
407
+ Write-Log ('kept ' + $Leftover.FullName + ': the installed build has no ' + $Launcher)
408
+ continue
409
+ }
410
+ }
411
+ try { Remove-Tree $Leftover.FullName; Write-Log ('removed ' + $Leftover.FullName) } catch { Write-Log $_.Exception.Message }
412
+ }
413
+ }
414
+
415
+ function Open-App([string]$Path) {
416
+ $script:Opened = $true
417
+ try {
418
+ Start-Process -FilePath $Path -WorkingDirectory (Split-Path -Parent $Path)
419
+ Write-Log ('opened ' + $Path)
420
+ } catch { Write-Log ('could not open ' + $Path + ': ' + $_.Exception.Message) }
421
+ }
422
+
423
+ # A value naming a path in $From, naming $To instead; null for any other.
424
+ function Get-Retargeted([string]$Value, [string]$From, [string]$To) {
425
+ if (-not $Value) { return $null }
426
+ $Pattern = [regex]::Escape($From) + '(?=[\\/",;]|$)'
427
+ $Evaluator = [System.Text.RegularExpressions.MatchEvaluator] ({ param($Found) $To }.GetNewClosure())
428
+ $Next = [regex]::Replace($Value, $Pattern, $Evaluator, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
429
+ if ($Next -ceq $Value) { return $null }
430
+ return $Next
431
+ }
432
+
433
+ # Electrobun's setup makes Desktop and Start menu shortcuts into its folder.
434
+ function Update-Shortcuts([string]$From, [string]$To) {
435
+ $Shell = New-Object -ComObject WScript.Shell
436
+ $Folders = @(
437
+ [Environment]::GetFolderPath('Desktop'),
438
+ [Environment]::GetFolderPath('Programs'),
439
+ (Join-Path $env:APPDATA 'Microsoft\Internet Explorer\Quick Launch')
440
+ ) | Where-Object { $_ -and (Test-Path -LiteralPath $_) } | Sort-Object -Unique
441
+ foreach ($Folder in $Folders) {
442
+ foreach ($Link in @(Get-ChildItem -LiteralPath $Folder -Filter '*.lnk' -Recurse -File -Force -ErrorAction SilentlyContinue)) {
443
+ try {
444
+ $Shortcut = $Shell.CreateShortcut($Link.FullName)
445
+ $Changed = $false
446
+ foreach ($Field in @('TargetPath', 'WorkingDirectory', 'IconLocation', 'Arguments')) {
447
+ $Next = Get-Retargeted ([string]$Shortcut.$Field) $From $To
448
+ if ($Next) { $Shortcut.$Field = $Next; $Changed = $true }
449
+ }
450
+ if ($Changed) { $Shortcut.Save(); Write-Log ('pointed ' + $Link.FullName + ' at ' + $To) }
451
+ } catch { Write-Log ('could not update ' + $Link.FullName + ': ' + $_.Exception.Message) }
452
+ }
453
+ }
454
+ }
455
+
456
+ # The uninstall entry Electrobun's setup made for its folder goes with the
457
+ # folder; the installer's own entry is the one that removes the app.
458
+ function Remove-UninstallEntries([string]$Folder) {
459
+ $Uninstall = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey($UninstallPath, $true)
460
+ if (-not $Uninstall) { return }
461
+ try {
462
+ foreach ($Name in @($Uninstall.GetSubKeyNames())) {
463
+ if ($Name -eq $InstallerEntry) { continue }
464
+ $Key = $Uninstall.OpenSubKey($Name)
465
+ if (-not $Key) { continue }
466
+ $Location = [string]$Key.GetValue('InstallLocation', '')
467
+ $Key.Close()
468
+ if (Test-Under $Location.TrimEnd('\') $Folder) {
469
+ try { $Uninstall.DeleteSubKeyTree($Name); Write-Log ('removed the uninstall entry ' + $Name) } catch { Write-Log ('could not remove the uninstall entry ' + $Name + ': ' + $_.Exception.Message) }
470
+ }
471
+ }
472
+ } finally { $Uninstall.Close() }
473
+ }
474
+
475
+ # Apps & features shows the version the installer's entry names.
476
+ function Set-InstalledVersion {
477
+ try {
478
+ $Version = [string](Get-Content -LiteralPath (Join-Path $Target 'Resources\version.json') -Raw | ConvertFrom-Json).version
479
+ $Key = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey($UninstallPath + '\' + $InstallerEntry, $true)
480
+ if ($Key) {
481
+ if ($Version) { $Key.SetValue('DisplayVersion', $Version); Write-Log ('the uninstall entry names version ' + $Version) }
482
+ $Key.Close()
483
+ }
484
+ } catch { Write-Log ('could not update the uninstall entry: ' + $_.Exception.Message) }
485
+ }
486
+
487
+ # Moved into this build's app data folder first, out of the way of an update
488
+ # installing into the updater's folder meanwhile, and of anything else that
489
+ # moves the folders under an identifier.
490
+ function Remove-Source([string]$Source) {
491
+ if (-not (Test-Path -LiteralPath $Source)) { return }
492
+ if ((Get-RunningUnder $Source).Count -gt 0) { Write-Log ('left ' + $Source + ': the app runs from it'); return }
493
+ $Doomed = $Source
494
+ if ((Split-Path -Leaf $Source) -notlike 'app.removing-*') {
495
+ New-Item -ItemType Directory -Force -Path $AppData | Out-Null
496
+ $Doomed = Join-Path $AppData ('app.removing-' + (Get-Stamp))
497
+ Invoke-Retried { Move-Item -LiteralPath $Source -Destination $Doomed } ('set aside ' + $Source)
498
+ }
499
+ Invoke-Retried { Remove-Item -LiteralPath $Doomed -Recurse -Force } ('remove ' + $Doomed)
500
+ Write-Log ('removed ' + $Source)
501
+ }
502
+
503
+ # One script at a time: the app opened in the installer's folder removes the
504
+ # copy it came from too, and waits for this one.
505
+ $Mutex = New-Object System.Threading.Mutex($false, 'Local\GloomInstallerFolder')
506
+ $Locked = $false
507
+ try {
508
+ New-Item -ItemType Directory -Force -Path (Split-Path -Parent $Log) | Out-Null
509
+ try { $Locked = $Mutex.WaitOne($(if ($AfterExit) { 5000 } else { 120000 })) } catch { $Locked = ($_.Exception.GetBaseException() -is [System.Threading.AbandonedMutexException]) }
510
+ if ($AfterExit) {
511
+ # The app quits only on "started": anything that keeps the move from
512
+ # happening is said before, and the app runs on.
513
+ if (Test-Path -LiteralPath $Cancel) { Write-Log 'the app stopped waiting before this started'; return }
514
+ if (-not $Locked) { Write-Log 'busy: another move of the app runs'; return }
515
+ if (-not (Test-Path -LiteralPath (Join-Path $Sources[0] $Launcher))) { Write-Log ('declined: no ' + $Launcher + ' in ' + $Sources[0]); return }
516
+ $BuildFiles = New-Object 'System.Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase)
517
+ foreach ($File in @(Get-Files $Sources[0] '')) { [void]$BuildFiles.Add($File) }
518
+ # A build that just quit there for an update can take a moment to go.
519
+ if (-not (Wait-Until 8 { (Get-AppRunningInTarget).Count -eq 0 })) {
520
+ Write-Log ('busy: ' + @(Get-AppRunningInTarget)[0].Path + ' runs'); return
521
+ }
522
+ try {
523
+ $Probe = Join-Path $Target ('.gloom-next-' + (Get-Stamp))
524
+ New-Item -ItemType Directory -Path $Probe | Out-Null
525
+ [IO.Directory]::Delete($Probe, $false)
526
+ } catch { Write-Log ('declined: cannot write in ' + $Target + ': ' + $_.Exception.Message); return }
527
+ Write-Log ('started ' + $PID)
528
+ # The app and its WebView helpers, by where they run from. Having seen
529
+ # "started", the app quits, however long that takes: a slow quit still
530
+ # ends with it open again. Only one that stopped waiting for this, and
531
+ # wrote $Cancel, runs on, and is left so.
532
+ $Deadline = (Get-Date).AddMinutes(30)
533
+ while ((Get-RunningUnder $Sources[0]).Count -gt 0 -and -not (Test-Path -LiteralPath $Cancel) -and (Get-Date) -lt $Deadline) {
534
+ Start-Sleep -Milliseconds 500
535
+ }
536
+ if (Test-Path -LiteralPath $Cancel) { Write-Log 'the app stopped waiting; it stays where it is'; return }
537
+ if ((Get-RunningUnder $Sources[0]).Count -gt 0) { throw 'the app did not quit' }
538
+ $Exited = $true
539
+ if (-not (Wait-Until 30 { (Get-AppRunningInTarget).Count -eq 0 })) { throw ('the app runs from ' + $Target) }
540
+ Remove-Staging
541
+ Install-Build $Sources[0]
542
+ Write-Log ('moved ' + $Sources[0] + ' into ' + $Target)
543
+ Open-App (Join-Path $Target $Launcher)
544
+ Set-InstalledVersion
545
+ } else {
546
+ if (-not $Locked) { throw 'another move of the app runs' }
547
+ Write-Log ('started ' + $PID)
548
+ }
549
+ foreach ($Source in $Sources) {
550
+ try {
551
+ Update-Shortcuts $Source $Target
552
+ Remove-UninstallEntries $Source
553
+ Remove-Source $Source
554
+ } catch { Write-Log ('could not remove ' + $Source + ': ' + $_.Exception.Message) }
555
+ }
556
+ Remove-Staging
557
+ Write-Log 'done'
558
+ } catch {
559
+ if ($AfterExit) { Write-Log ('move failed: ' + $_.Exception.Message) } else { Write-Log ('removal failed: ' + $_.Exception.Message) }
560
+ # The app opens again only if it quit for this, from where it was.
561
+ if ($Exited -and -not $Opened) {
562
+ $Open = Join-Path $Sources[0] $Launcher
563
+ if (-not (Test-Path -LiteralPath $Open)) { $Open = Join-Path $Target $Launcher }
564
+ Open-App $Open
565
+ }
566
+ } finally {
567
+ $ErrorActionPreference = 'Continue'
568
+ if ($Locked) { try { $Mutex.ReleaseMutex() } catch { } }
569
+ $Mutex.Dispose()
570
+ if ($TaskName) { schtasks.exe /delete /tn $TaskName /f | Out-Null }
571
+ Remove-Item -LiteralPath $PSCommandPath -Force -ErrorAction SilentlyContinue
572
+ }
573
+ `;
574
+
575
+ /** The script that moves the app into the installer's folder or removes the updater's copy, with its paths written in. */
576
+ export function installerFolderScript({ target, sources, folder, log, afterExit }: InstallerFolderScript): string {
577
+ const header = [
578
+ "$ErrorActionPreference = 'Stop'",
579
+ `$Target = ${psString(target)}`,
580
+ `$Sources = @(${sources.map(psString).join(", ")})`,
581
+ `$AppData = ${psString(folder)}`,
582
+ `$Log = ${psString(log)}`,
583
+ `$InstallerEntry = ${psString(INSTALLER_ENTRY)}`,
584
+ `$AfterExit = ${afterExit ? "$true" : "$false"}`,
585
+ `$Cancel = ${psString(afterExit?.cancel ?? "")}`,
586
+ `$TaskName = ${psString(afterExit?.taskName ?? "")}`,
587
+ ];
588
+ return `${header.join("\n")}\n${SCRIPT_BODY}`;
589
+ }
590
+
591
+ /** The tries at moving a build into the installer's folder, kept across starts. */
592
+ export interface MoveTries {
593
+ /** The build they were for: its version and hash. */
594
+ build: string;
595
+ /** Tries that failed, or would have: each one the app may have quit for. */
596
+ failed: number;
597
+ /** When the last one started, in ms. */
598
+ last: number;
599
+ }
600
+
601
+ /** Whether the app may try to move `build` now, after `tries`. */
602
+ export function mayTryMove(tries: MoveTries | null, build: string, now: number): boolean {
603
+ if (!tries || tries.build !== build) return true;
604
+ return tries.failed < MAX_MOVE_TRIES && now - tries.last >= RETRY_AFTER_MS;
605
+ }
606
+
607
+ /**
608
+ * `tries` with one more for `build` at `now`. Every try counts as failed when
609
+ * it starts: one that works leaves this build's updater folder for good. One
610
+ * the script put off because the installer's folder was in use does not.
611
+ */
612
+ export function noteMoveTry(tries: MoveTries | null, build: string, now: number, counts = true): MoveTries {
613
+ const failed = tries?.build === build ? tries.failed : 0;
614
+ return { build, failed: failed + (counts ? 1 : 0), last: now };
615
+ }
616
+
617
+ async function readMoveTries(path: string): Promise<MoveTries | null> {
618
+ const value = await readFile(path, "utf8").then((text) => JSON.parse(text) as Partial<MoveTries>, () => null);
619
+ if (!value || typeof value.build !== "string" || typeof value.failed !== "number" || typeof value.last !== "number") return null;
620
+ return { build: value.build, failed: value.failed, last: value.last };
621
+ }
622
+
623
+ async function writeMoveTries(path: string, tries: MoveTries): Promise<void> {
624
+ await writeFile(path, JSON.stringify(tries), "utf8");
625
+ }
626
+
627
+ /** The build in `appFolder`, by its version and hash. */
628
+ async function buildId(appFolder: string): Promise<string> {
629
+ const info = await readFile(win32.join(appFolder, "Resources", "version.json"), "utf8").then((text) => JSON.parse(text) as { version?: unknown; hash?: unknown }, () => null);
630
+ return `${String(info?.version ?? "")}+${String(info?.hash ?? "")}`;
631
+ }
632
+
633
+ /** Where the script, its log and the tries go: the app data folder of this build's identifier. */
634
+ function scriptFolder(localAppData: string, channel: string): string {
635
+ return win32.join(localAppData, DESKTOP_APP_IDENTIFIER, channel);
636
+ }
637
+
638
+ async function moveIntoInstallerFolder(
639
+ localAppData: string,
640
+ running: { folder: string; channel: string },
641
+ target: string,
642
+ ): Promise<InstallerFolderMove> {
643
+ const folder = scriptFolder(localAppData, running.channel);
644
+ const triesPath = win32.join(folder, "installer-folder-tries.json");
645
+ const build = await buildId(running.folder);
646
+ const tries = await readMoveTries(triesPath);
647
+ const now = Date.now();
648
+ if (!mayTryMove(tries, build, now)) return { kind: "skipped" };
649
+ // Noted before the app can quit for it: a try that cannot be noted is not made.
650
+ try {
651
+ await mkdir(folder, { recursive: true });
652
+ await writeMoveTries(triesPath, noteMoveTry(tries, build, now));
653
+ } catch (error) {
654
+ logFailure("desktop-installer-folder", "Could not note a try at moving the app into the installer's folder")(error);
655
+ return { kind: "failed" };
656
+ }
657
+ const log = win32.join(folder, "installer-folder.log");
658
+ const cancel = win32.join(folder, "installer-folder.cancel");
659
+ await rm(log, { force: true });
660
+ await rm(cancel, { force: true });
661
+ const others = (await updaterFolders(localAppData, running.channel)).filter((path) => !samePath(path, running.folder));
662
+ const taskName = `GloomInstallerFolder-${now}`;
663
+ const script = win32.join(folder, "installer-folder-move.ps1");
664
+ try {
665
+ await writePowerShellScript(script, installerFolderScript({
666
+ target,
667
+ sources: [running.folder, ...others],
668
+ folder,
669
+ log,
670
+ afterExit: { cancel, taskName },
671
+ }));
672
+ const answer = await runScriptAfterExit(script, log, cancel, taskName);
673
+ if (answer.kind === "started") return { kind: "relaunching" };
674
+ if (answer.kind === "busy") await writeMoveTries(triesPath, noteMoveTry(tries, build, now, false)).catch(() => {});
675
+ console.error(`[desktop] the app stays where it is, for now: ${answer.reason}`);
676
+ return { kind: "skipped" };
677
+ } catch (error) {
678
+ logFailure("desktop-installer-folder", "Could not run the script that moves the app into the installer's folder")(error);
679
+ return { kind: "failed" };
680
+ }
681
+ }
682
+
683
+ function removeUpdaterCopies(localAppData: string, channel: string, target: string, sources: string[]): InstallerFolderMove {
684
+ const timer = setTimeout(() => {
685
+ const folder = scriptFolder(localAppData, channel);
686
+ const script = win32.join(folder, "installer-folder-cleanup.ps1");
687
+ const log = win32.join(folder, "installer-folder-cleanup.log");
688
+ void mkdir(folder, { recursive: true })
689
+ .then(() => rm(log, { force: true }))
690
+ .then(() => writePowerShellScript(script, installerFolderScript({ target, sources, folder, log })))
691
+ .then(() => runScriptInBackground(script))
692
+ .then((code) => {
693
+ if (code !== 0) throw new Error(`the script exited with ${code}`);
694
+ })
695
+ .catch(logFailure("desktop-installer-folder", "Could not remove the copy of the app an update left"));
696
+ }, REMOVAL_DELAY_MS);
697
+ timer.unref?.();
698
+ return { kind: "removing" };
699
+ }
700
+
701
+ /**
702
+ * Puts an installer install's app in the installer's folder. Anywhere else,
703
+ * and with nothing left in the updater's folder, it returns without writing
704
+ * anything. On `relaunching` the caller quits at once, before any window
705
+ * opens.
706
+ */
707
+ export async function settleInstallerFolder(context: InstallerFolderContext): Promise<InstallerFolderMove> {
708
+ if (context.platform !== "win32") return { kind: "none" };
709
+ const localAppData = context.localAppData || win32.join(context.homeDir, "AppData", "Local");
710
+ const read = context.readInstallerFolder ?? readInstallerFolderFromRegistry;
711
+
712
+ const running = updaterAppFolder(context.execPath, localAppData);
713
+ if (running) {
714
+ const target = await installerFolder(read);
715
+ if (!target || samePath(target, running.folder)) return { kind: "none" };
716
+ return moveIntoInstallerFolder(localAppData, running, target);
717
+ }
718
+
719
+ const bin = win32.dirname(context.execPath);
720
+ if (win32.basename(bin).toLowerCase() !== "bin") return { kind: "none" };
721
+ const app = win32.dirname(bin);
722
+ const channel = await buildChannel(app);
723
+ const copies = await updaterFolders(localAppData, channel);
724
+ const leftovers = (await readdir(app).catch(() => [] as string[])).filter((name) => /^\.gloom-(next|previous)-/i.test(name));
725
+ if (copies.length === 0 && leftovers.length === 0) return { kind: "none" };
726
+ // Only from the installer's folder: a copy run from anywhere else (a
727
+ // portable one) leaves an install from Electrobun's own setup alone.
728
+ const target = await installerFolder(read);
729
+ if (!target || !samePath(target, app)) return { kind: "none" };
730
+ return removeUpdaterCopies(localAppData, channel, target, copies);
731
+ }