gloomberb 0.18.1 → 0.18.3

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 (416) hide show
  1. package/package.json +1 -1
  2. package/patches/electrobun@1.18.1.patch +106 -5
  3. package/src/api-client/commodity-balances.ts +6 -0
  4. package/src/api-client/company-kpis.ts +9 -0
  5. package/src/api-client/crypto-markets.ts +9 -0
  6. package/src/api-client/data.ts +21 -5
  7. package/src/api-client/equity-screener.ts +2 -0
  8. package/src/api-client/index.ts +2 -0
  9. package/src/api-client/request.ts +5 -3
  10. package/src/api-client/telemetry.ts +17 -0
  11. package/src/api-client/types.ts +5 -0
  12. package/src/app/global-shortcuts.ts +1 -16
  13. package/src/app/pane-runtime/plugin-bindings.ts +3 -1
  14. package/src/app/pane-runtime/ticker-open-runtime.ts +17 -5
  15. package/src/app/runtime/plugin-bindings.ts +6 -2
  16. package/src/app/runtime/update.ts +28 -8
  17. package/src/architecture/pane-tabs-allowlist.json +65 -0
  18. package/src/brokers/logos.ts +66 -0
  19. package/src/brokers/profile-form.ts +3 -0
  20. package/src/brokers/remote-broker-adapter.ts +13 -3
  21. package/src/capabilities/index.ts +1 -1
  22. package/src/capabilities/service-capabilities.ts +7 -1
  23. package/src/cli/base-converter.ts +60 -18
  24. package/src/cli/commands/market.ts +354 -187
  25. package/src/cli/commands/news-search.ts +35 -8
  26. package/src/cli/commands/system.ts +35 -1
  27. package/src/cli/commands/ticker.ts +167 -60
  28. package/src/cli/currency-hints.ts +27 -0
  29. package/src/cli/fx-pair.ts +41 -4
  30. package/src/cli/glossary.ts +57 -4
  31. package/src/cli/help.ts +1 -1
  32. package/src/cli/helpers.ts +36 -3
  33. package/src/cli/index.ts +12 -1
  34. package/src/cli/issuer-membership.ts +103 -0
  35. package/src/cli/known-gaps.ts +37 -1
  36. package/src/cli/listing-arg.ts +51 -3
  37. package/src/cli/name-entries.ts +51 -12
  38. package/src/cli/pane-functions/capabilities.ts +2 -0
  39. package/src/cli/pane-functions/catalog.ts +19 -1
  40. package/src/cli/pane-functions/discovery.ts +15 -1
  41. package/src/cli/pane-functions/dom.ts +11 -5
  42. package/src/cli/pane-functions/freshness.ts +4 -4
  43. package/src/cli/pane-functions/function-help.ts +27 -3
  44. package/src/cli/pane-functions/headless.ts +31 -11
  45. package/src/cli/pane-functions/index.ts +67 -7
  46. package/src/cli/pane-functions/lock-note.ts +7 -4
  47. package/src/cli/pane-functions/resolver.ts +21 -6
  48. package/src/cli/quote-answer.ts +179 -0
  49. package/src/cli/report-tables.ts +136 -18
  50. package/src/cli/result.ts +12 -4
  51. package/src/cli/series-search.ts +2 -1
  52. package/src/cli/symbol-list.ts +47 -11
  53. package/src/cli/types.ts +4 -0
  54. package/src/components/chart/composite/composite-chart.tsx +25 -7
  55. package/src/components/chart/composite/drawing-store.ts +26 -6
  56. package/src/components/chart/composite/format.ts +13 -1
  57. package/src/components/chart/composite/interactions.ts +2 -1
  58. package/src/components/chart/composite/level-labels.ts +30 -0
  59. package/src/components/chart/composite/levels.ts +17 -6
  60. package/src/components/chart/composite/panel-surface.tsx +69 -8
  61. package/src/components/chart/composite/types.ts +6 -0
  62. package/src/components/chart/composite/use-composite-chart-keyboard.ts +1 -1
  63. package/src/components/command-bar/routes/root/command-items.ts +32 -6
  64. package/src/components/command-bar/routes/ticker-search/actions.ts +6 -0
  65. package/src/components/data-table/view.tsx +23 -3
  66. package/src/components/index.ts +3 -0
  67. package/src/components/layout/detached-pane-shell.tsx +1 -0
  68. package/src/components/layout/floating-pane.tsx +1 -0
  69. package/src/components/layout/header.tsx +228 -55
  70. package/src/components/layout/pane/content.tsx +16 -14
  71. package/src/components/layout/pane/footer/index.tsx +22 -3
  72. package/src/components/layout/pane/footer/model.ts +99 -4
  73. package/src/components/layout/pane/header-tabs.tsx +15 -1
  74. package/src/components/layout/pane/header.tsx +10 -3
  75. package/src/components/layout/pane/index.tsx +1 -0
  76. package/src/components/layout/pane/pane-tabs.tsx +9 -1
  77. package/src/components/onboarding/onboarding-frame.tsx +9 -11
  78. package/src/components/onboarding/use-onboarding-broker-form.ts +5 -2
  79. package/src/components/onboarding/wizard-model.ts +10 -3
  80. package/src/components/ui/button.tsx +11 -4
  81. package/src/components/ui/choice-dialog.tsx +8 -5
  82. package/src/components/ui/display.tsx +11 -6
  83. package/src/components/ui/fields.tsx +100 -20
  84. package/src/components/ui/icon.tsx +6 -1
  85. package/src/components/ui/list-row-lines.tsx +87 -0
  86. package/src/components/ui/list-view.tsx +14 -0
  87. package/src/components/ui/logo-mark.tsx +64 -0
  88. package/src/components/ui/number-step.ts +54 -0
  89. package/src/components/ui/select-button.tsx +5 -4
  90. package/src/components/ui/select-field.tsx +3 -3
  91. package/src/components/ui/stat-grid.tsx +28 -3
  92. package/src/components/ui/status.tsx +20 -4
  93. package/src/components/ui/tabs.tsx +9 -1
  94. package/src/components/ui/toggle.tsx +17 -3
  95. package/src/core/app-services.ts +4 -0
  96. package/src/core/state/app/instrument.ts +8 -3
  97. package/src/data/config/store/normalize.ts +4 -1
  98. package/src/data/plugin-cache.ts +32 -6
  99. package/src/docs/snapshot.json +5 -5
  100. package/src/i18n/es.ts +31 -3
  101. package/src/i18n/ja.ts +31 -3
  102. package/src/i18n/ko.ts +31 -3
  103. package/src/i18n/zh-cn.ts +32 -4
  104. package/src/i18n/zh-tw.ts +32 -4
  105. package/src/market-data/commodity-names.ts +80 -0
  106. package/src/market-data/coordinator/auxiliary.ts +3 -1
  107. package/src/market-data/coordinator/chart.ts +3 -1
  108. package/src/market-data/futures-board.ts +38 -1
  109. package/src/market-data/futures-contract.ts +91 -0
  110. package/src/market-data/fx-closes.ts +78 -1
  111. package/src/market-data/market/change-basis.ts +219 -0
  112. package/src/market-data/market/format.ts +28 -12
  113. package/src/market-data/market/freshness.ts +38 -0
  114. package/src/market-data/market/status.ts +55 -9
  115. package/src/market-data/market/year-to-date.ts +46 -0
  116. package/src/market-data/quotes/contributions.ts +12 -0
  117. package/src/market-data/quotes/delisting.ts +34 -0
  118. package/src/market-data/quotes/live-valuation.ts +23 -0
  119. package/src/market-data/quotes/resolution.ts +8 -2
  120. package/src/market-data/tax-equivalent.ts +10 -2
  121. package/src/notifications/app-notifier.ts +2 -2
  122. package/src/notifications/notification-log.ts +8 -6
  123. package/src/plugins/builtin/alerts/alert-engine.ts +3 -1
  124. package/src/plugins/builtin/alerts/cli.ts +26 -18
  125. package/src/plugins/builtin/alerts/events.ts +1 -0
  126. package/src/plugins/builtin/alerts/headless.ts +1 -1
  127. package/src/plugins/builtin/alerts/index.tsx +13 -4
  128. package/src/plugins/builtin/alerts/research-rules.ts +1 -0
  129. package/src/plugins/builtin/analytics/risk-pane.tsx +10 -7
  130. package/src/plugins/builtin/attention/pane.tsx +16 -14
  131. package/src/plugins/builtin/attention/trending.tsx +8 -6
  132. package/src/plugins/builtin/awards/pane.tsx +6 -6
  133. package/src/plugins/builtin/backtest/client.ts +5 -2
  134. package/src/plugins/builtin/backtest/export.ts +1 -1
  135. package/src/plugins/builtin/backtest/window.ts +16 -3
  136. package/src/plugins/builtin/broker-manager/add-flow.tsx +40 -8
  137. package/src/plugins/builtin/broker-manager/detail.tsx +26 -1
  138. package/src/plugins/builtin/broker-manager/index.tsx +20 -9
  139. package/src/plugins/builtin/broker-manager/model.ts +5 -0
  140. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -1
  141. package/src/plugins/builtin/broker-manager/table.ts +5 -1
  142. package/src/plugins/builtin/broker-trading/account-picker.tsx +91 -0
  143. package/src/plugins/builtin/broker-trading/chart-levels.ts +54 -0
  144. package/src/plugins/builtin/broker-trading/command.ts +103 -0
  145. package/src/plugins/builtin/broker-trading/controller.ts +688 -0
  146. package/src/plugins/builtin/broker-trading/dock.tsx +184 -0
  147. package/src/plugins/builtin/broker-trading/headless.ts +76 -0
  148. package/src/plugins/builtin/broker-trading/index.tsx +77 -0
  149. package/src/plugins/builtin/broker-trading/order-feed.ts +383 -0
  150. package/src/plugins/builtin/broker-trading/orders-data.ts +133 -0
  151. package/src/plugins/builtin/broker-trading/orders.tsx +437 -0
  152. package/src/plugins/builtin/broker-trading/overview-orders.tsx +121 -0
  153. package/src/plugins/builtin/broker-trading/pane-model.ts +207 -0
  154. package/src/plugins/builtin/broker-trading/pane.tsx +507 -0
  155. package/src/plugins/builtin/broker-trading/strategy.ts +153 -0
  156. package/src/plugins/builtin/broker-trading/ticket-parts.tsx +117 -0
  157. package/src/plugins/builtin/broker-trading/ticket.tsx +578 -0
  158. package/src/plugins/builtin/buildout/data-runtime.ts +1 -1
  159. package/src/plugins/builtin/buildout/format.ts +1 -0
  160. package/src/plugins/builtin/buildout/model/index.ts +16 -7
  161. package/src/plugins/builtin/buildout/pane/index.tsx +11 -7
  162. package/src/plugins/builtin/buildout/pane/status.tsx +3 -3
  163. package/src/plugins/builtin/catalysts/client.ts +2 -2
  164. package/src/plugins/builtin/catalysts/pane.tsx +3 -3
  165. package/src/plugins/builtin/chart-composer/cli-options.ts +19 -0
  166. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
  167. package/src/plugins/builtin/chart-composer/headless-schema.ts +3 -3
  168. package/src/plugins/builtin/chart-composer/headless.ts +2 -1
  169. package/src/plugins/builtin/chart-composer/option-range.ts +39 -0
  170. package/src/plugins/builtin/chart-composer/pane.tsx +32 -11
  171. package/src/plugins/builtin/chart-composer/presets.ts +2 -2
  172. package/src/plugins/builtin/chart-composer/series-catalog.ts +4 -2
  173. package/src/plugins/builtin/chart-composer/shortcuts.ts +3 -3
  174. package/src/plugins/builtin/chart-composer/viewport-labels.ts +5 -2
  175. package/src/plugins/builtin/chat/index.tsx +1 -1
  176. package/src/plugins/builtin/chat/status-widget.tsx +1 -1
  177. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +6 -1
  178. package/src/plugins/builtin/chat/unread-inbox.ts +1 -1
  179. package/src/plugins/builtin/commodity-balances/client.ts +6 -4
  180. package/src/plugins/builtin/commodity-balances/headless.ts +41 -7
  181. package/src/plugins/builtin/commodity-balances/index.tsx +19 -4
  182. package/src/plugins/builtin/commodity-balances/model.ts +22 -0
  183. package/src/plugins/builtin/commodity-balances/pane.tsx +39 -9
  184. package/src/plugins/builtin/company-attention/app-rank.tsx +4 -5
  185. package/src/plugins/builtin/company-attention/pane.tsx +13 -11
  186. package/src/plugins/builtin/company-kpis/client.ts +3 -2
  187. package/src/plugins/builtin/company-kpis/headless.ts +5 -7
  188. package/src/plugins/builtin/company-kpis/model.ts +85 -2
  189. package/src/plugins/builtin/company-kpis/pane.tsx +30 -15
  190. package/src/plugins/builtin/composite-plugins.ts +3 -2
  191. package/src/plugins/builtin/correlation/headless.ts +2 -1
  192. package/src/plugins/builtin/correlation/relationship/model.ts +1 -0
  193. package/src/plugins/builtin/correlation/settings.ts +1 -0
  194. package/src/plugins/builtin/cot/headless.ts +1 -1
  195. package/src/plugins/builtin/cot/index.tsx +1 -1
  196. package/src/plugins/builtin/cot/model.ts +35 -12
  197. package/src/plugins/builtin/cot/pane.tsx +4 -3
  198. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  199. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +2 -1
  200. package/src/plugins/builtin/credit-conditions/index.tsx +2 -1
  201. package/src/plugins/builtin/credit-documents/pane.tsx +8 -7
  202. package/src/plugins/builtin/crypto-board/client.ts +33 -2
  203. package/src/plugins/builtin/crypto-board/headless.ts +5 -2
  204. package/src/plugins/builtin/crypto-board/model.ts +28 -8
  205. package/src/plugins/builtin/debt-maturities/pane.tsx +4 -3
  206. package/src/plugins/builtin/dividend-yield/headless.ts +30 -8
  207. package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
  208. package/src/plugins/builtin/doe/pane.tsx +1 -1
  209. package/src/plugins/builtin/earnings-calls/headless.ts +4 -4
  210. package/src/plugins/builtin/earnings-calls/lock-note.ts +12 -3
  211. package/src/plugins/builtin/econ/calendar-model.ts +35 -0
  212. package/src/plugins/builtin/econ/index.tsx +31 -4
  213. package/src/plugins/builtin/econ/print-watch.ts +279 -0
  214. package/src/plugins/builtin/equity-screener/headless.ts +30 -5
  215. package/src/plugins/builtin/equity-screener/model.ts +68 -2
  216. package/src/plugins/builtin/equity-screener/pane.tsx +129 -119
  217. package/src/plugins/builtin/estimate-revisions/headless.ts +3 -2
  218. package/src/plugins/builtin/estimate-revisions/lock-note.ts +29 -7
  219. package/src/plugins/builtin/exposure/client.ts +4 -1
  220. package/src/plugins/builtin/exposure/editor.tsx +3 -3
  221. package/src/plugins/builtin/exposure/evidence.ts +4 -1
  222. package/src/plugins/builtin/exposure/holdings.ts +3 -1
  223. package/src/plugins/builtin/exposure/pane.tsx +43 -21
  224. package/src/plugins/builtin/filing-events/index.tsx +9 -3
  225. package/src/plugins/builtin/futures/table.tsx +6 -3
  226. package/src/plugins/builtin/futures-curve/pane.tsx +2 -2
  227. package/src/plugins/builtin/gpu/board.tsx +4 -1
  228. package/src/plugins/builtin/gpu/changes.tsx +10 -10
  229. package/src/plugins/builtin/gpu/equities.tsx +4 -1
  230. package/src/plugins/builtin/gpu/history.tsx +4 -2
  231. package/src/plugins/builtin/gpu/pane.tsx +5 -3
  232. package/src/plugins/builtin/holders/client.ts +2 -1
  233. package/src/plugins/builtin/holders/headless.ts +2 -1
  234. package/src/plugins/builtin/holders/thirteenf-match.ts +3 -1
  235. package/src/plugins/builtin/insider/base-values.ts +76 -0
  236. package/src/plugins/builtin/insider/client.ts +3 -0
  237. package/src/plugins/builtin/insider/headless.ts +48 -10
  238. package/src/plugins/builtin/insider/index.tsx +17 -8
  239. package/src/plugins/builtin/insider/issuer.ts +34 -4
  240. package/src/plugins/builtin/insider/signals-client.ts +2 -2
  241. package/src/plugins/builtin/insider/table-model.ts +35 -13
  242. package/src/plugins/builtin/insider/table.tsx +5 -2
  243. package/src/plugins/builtin/iv-history/events.ts +145 -0
  244. package/src/plugins/builtin/iv-history/format.ts +3 -0
  245. package/src/plugins/builtin/iv-history/headless.ts +91 -7
  246. package/src/plugins/builtin/jobs/headless.ts +2 -2
  247. package/src/plugins/builtin/jobs/lock-note.ts +14 -3
  248. package/src/plugins/builtin/jodi/pane.tsx +4 -4
  249. package/src/plugins/builtin/market-halts/model.ts +31 -6
  250. package/src/plugins/builtin/market-halts/pane.tsx +3 -0
  251. package/src/plugins/builtin/market-movers/client.ts +4 -3
  252. package/src/plugins/builtin/market-movers/screener.ts +4 -3
  253. package/src/plugins/builtin/market-valuation/pane.tsx +2 -1
  254. package/src/plugins/builtin/members/pane.tsx +8 -3
  255. package/src/plugins/builtin/news/wire/feed-config.ts +29 -12
  256. package/src/plugins/builtin/news/wire/index.tsx +7 -2
  257. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +11 -3
  258. package/src/plugins/builtin/news/wire/rss/cli.ts +3 -2
  259. package/src/plugins/builtin/news/wire/rss/source.ts +13 -4
  260. package/src/plugins/builtin/notes/cli.ts +1 -1
  261. package/src/plugins/builtin/notes/export.ts +17 -6
  262. package/src/plugins/builtin/notes/files.ts +16 -5
  263. package/src/plugins/builtin/options/enrichment-model.ts +10 -9
  264. package/src/plugins/builtin/options/footer.ts +25 -20
  265. package/src/plugins/builtin/options/live-quotes.ts +33 -18
  266. package/src/plugins/builtin/options/model-spot.ts +70 -4
  267. package/src/plugins/builtin/options/view.tsx +118 -35
  268. package/src/plugins/builtin/options-calculator/pane.tsx +7 -4
  269. package/src/plugins/builtin/options-positioning/headless.ts +11 -3
  270. package/src/plugins/builtin/options-positioning/model.ts +24 -0
  271. package/src/plugins/builtin/options-positioning/pane.tsx +10 -12
  272. package/src/plugins/builtin/options-scenario/client.ts +19 -1
  273. package/src/plugins/builtin/options-scenario/headless.ts +13 -6
  274. package/src/plugins/builtin/options-scenario/pane.tsx +54 -11
  275. package/src/plugins/builtin/plugin-marketplace/pane.tsx +5 -4
  276. package/src/plugins/builtin/portfolio-list/account-metrics.ts +3 -0
  277. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -5
  278. package/src/plugins/builtin/portfolio-list/cli/render.ts +34 -10
  279. package/src/plugins/builtin/portfolio-list/headless.ts +1 -1
  280. package/src/plugins/builtin/portfolio-list/pane/data.ts +23 -1
  281. package/src/plugins/builtin/portfolio-list/pane/index.tsx +33 -14
  282. package/src/plugins/builtin/portfolio-list/settings.ts +20 -2
  283. package/src/plugins/builtin/portfolio-list/summary/index.tsx +12 -0
  284. package/src/plugins/builtin/portfolio-list/summary/totals.ts +24 -3
  285. package/src/plugins/builtin/power/client.ts +2 -2
  286. package/src/plugins/builtin/power/pane.tsx +36 -31
  287. package/src/plugins/builtin/primary-dealers/client.ts +137 -62
  288. package/src/plugins/builtin/primary-dealers/headless.ts +30 -5
  289. package/src/plugins/builtin/primary-dealers/index.tsx +3 -2
  290. package/src/plugins/builtin/primary-dealers/model.ts +179 -129
  291. package/src/plugins/builtin/primary-dealers/pane.tsx +135 -15
  292. package/src/plugins/builtin/research/analyst-model.ts +5 -1
  293. package/src/plugins/builtin/reverse-dcf/client.ts +2 -1
  294. package/src/plugins/builtin/reverse-dcf/model.ts +4 -3
  295. package/src/plugins/builtin/risk-factors/index.tsx +12 -3
  296. package/src/plugins/builtin/scanner/feed.ts +6 -1
  297. package/src/plugins/builtin/scanner/headless.ts +1 -1
  298. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  299. package/src/plugins/builtin/seasonality/client.ts +2 -1
  300. package/src/plugins/builtin/sectors/client.ts +4 -3
  301. package/src/plugins/builtin/series-inventory.ts +1 -0
  302. package/src/plugins/builtin/shared/cloud-denied.ts +7 -6
  303. package/src/plugins/builtin/shared/cloud-resource.ts +15 -2
  304. package/src/plugins/builtin/shared/cloud-upgrade.ts +10 -11
  305. package/src/plugins/builtin/shared/free-routes.ts +24 -0
  306. package/src/plugins/builtin/shared/headless-options.ts +11 -0
  307. package/src/plugins/builtin/shared/pro-lock-note.ts +89 -7
  308. package/src/plugins/builtin/shared/research-trading.tsx +64 -0
  309. package/src/plugins/builtin/shared/series-list-detail.tsx +3 -1
  310. package/src/plugins/builtin/shared/volatility/options.ts +2 -1
  311. package/src/plugins/builtin/social-mentions/client.ts +2 -1
  312. package/src/plugins/builtin/social-mentions/headless.ts +1 -1
  313. package/src/plugins/builtin/social-mentions/lock-note.ts +1 -1
  314. package/src/plugins/builtin/themes/headless.ts +8 -2
  315. package/src/plugins/builtin/themes/model.ts +43 -10
  316. package/src/plugins/builtin/themes/pane.tsx +8 -6
  317. package/src/plugins/builtin/ticker-detail/overview/model.ts +44 -10
  318. package/src/plugins/builtin/ticker-detail/overview/option-contract.ts +64 -0
  319. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +82 -21
  320. package/src/plugins/builtin/ticker-detail/pane.tsx +21 -14
  321. package/src/plugins/builtin/ticker-detail/quote-footer.ts +5 -1
  322. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +37 -8
  323. package/src/plugins/builtin/time-sales/pane.tsx +2 -2
  324. package/src/plugins/builtin/trace-bonds/client.ts +14 -3
  325. package/src/plugins/builtin/trace-bonds/headless.ts +1 -1
  326. package/src/plugins/builtin/vol-surface/client.ts +23 -3
  327. package/src/plugins/builtin/vol-surface/headless.ts +33 -14
  328. package/src/plugins/builtin/vol-surface/model.ts +30 -10
  329. package/src/plugins/builtin/vol-surface/pane.tsx +35 -15
  330. package/src/plugins/builtin/vol-surface/stored.ts +34 -3
  331. package/src/plugins/builtin/vol-surface/tables.ts +14 -0
  332. package/src/plugins/builtin-editorial.ts +1 -1
  333. package/src/plugins/compat.ts +3 -0
  334. package/src/plugins/external-runtime.ts +5 -0
  335. package/src/plugins/loader.ts +6 -0
  336. package/src/plugins/plugin-icon.ts +61 -0
  337. package/src/plugins/registry/host-actions.ts +2 -1
  338. package/src/plugins/registry/index.ts +9 -1
  339. package/src/plugins/runtime/context.tsx +5 -0
  340. package/src/plugins/runtime/index.tsx +3 -0
  341. package/src/portfolio/column-values.ts +13 -4
  342. package/src/portfolio/cost-fx.ts +5 -28
  343. package/src/public/__snapshots__/public-api.test.ts.snap +5 -0
  344. package/src/public/broker.ts +8 -0
  345. package/src/public/market-data.ts +5 -0
  346. package/src/renderers/dom/desktop/control-styles.ts +7 -1
  347. package/src/renderers/dom/desktop/controls.tsx +40 -8
  348. package/src/renderers/dom/desktop/icons.tsx +14 -0
  349. package/src/renderers/dom/desktop/list-view.tsx +51 -19
  350. package/src/renderers/dom/desktop/menu.tsx +10 -1
  351. package/src/renderers/dom/desktop/query-bar.tsx +2 -0
  352. package/src/renderers/dom/desktop/select-field.tsx +8 -6
  353. package/src/renderers/dom/host/style.ts +6 -1
  354. package/src/renderers/dom/host/tabs.tsx +22 -5
  355. package/src/renderers/dom/host/top-surface.ts +127 -0
  356. package/src/renderers/dom/styles.css +51 -5
  357. package/src/renderers/electrobun/bun/core-capabilities.ts +21 -1
  358. package/src/renderers/electrobun/bun/desktop/backend.ts +10 -0
  359. package/src/renderers/electrobun/bun/desktop/update.ts +210 -30
  360. package/src/renderers/electrobun/bun/external-plugins.ts +3 -0
  361. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  362. package/src/renderers/electrobun/view/broker-remote-client.ts +16 -0
  363. package/src/renderers/electrobun/view/external-plugins.ts +1 -0
  364. package/src/renderers/electrobun/view/news-source.ts +4 -2
  365. package/src/renderers/electrobun/view/remote/asset-data-client.ts +8 -1
  366. package/src/renderers/opentui/start.tsx +7 -0
  367. package/src/series/validate.ts +2 -2
  368. package/src/sources/gloomberb-cloud/index.ts +57 -17
  369. package/src/sources/gloomberb-cloud/normalizers.ts +4 -0
  370. package/src/sources/history-result.ts +3 -0
  371. package/src/sources/provider-errors.ts +3 -1
  372. package/src/sources/provider-router/history.ts +49 -6
  373. package/src/sources/provider-router/index.ts +18 -1
  374. package/src/state/app/activity.ts +13 -0
  375. package/src/sync/controller.ts +4 -0
  376. package/src/sync/core-contributors.ts +62 -11
  377. package/src/sync/local-only.ts +121 -0
  378. package/src/sync/react.ts +6 -0
  379. package/src/sync/types.ts +3 -0
  380. package/src/telemetry/crash-reports.ts +4 -0
  381. package/src/telemetry/update-failures-node.ts +59 -0
  382. package/src/telemetry/update-failures.ts +307 -0
  383. package/src/tickers/company-name-listing.ts +9 -7
  384. package/src/tickers/company-name.ts +43 -0
  385. package/src/tickers/listing-fate.ts +57 -8
  386. package/src/tickers/search/index.ts +24 -3
  387. package/src/time-series/price-comparison.ts +35 -0
  388. package/src/time-series/resolve.ts +52 -11
  389. package/src/time-series/spec.ts +1 -1
  390. package/src/time-series/types.ts +5 -0
  391. package/src/types/broker.ts +56 -1
  392. package/src/types/config.ts +6 -0
  393. package/src/types/data-provider.ts +11 -1
  394. package/src/types/financials.ts +23 -0
  395. package/src/types/headless.ts +19 -2
  396. package/src/types/instrument.ts +4 -0
  397. package/src/types/issuer-listings.ts +28 -0
  398. package/src/types/plugin.ts +24 -6
  399. package/src/types/price-history.ts +6 -1
  400. package/src/types/trading.ts +67 -0
  401. package/src/ui/host.tsx +4 -0
  402. package/src/updater/failure.ts +163 -0
  403. package/src/updater/index.ts +87 -23
  404. package/src/utils/base-currency.ts +108 -0
  405. package/src/utils/currency-names.ts +128 -0
  406. package/src/utils/currency-pair.ts +28 -1
  407. package/src/utils/currency-units.ts +32 -1
  408. package/src/utils/dom-screenshot.ts +22 -9
  409. package/src/utils/exchange-notation.generated.ts +360 -0
  410. package/src/utils/exchanges.ts +15 -322
  411. package/src/utils/format.ts +31 -1
  412. package/src/utils/instrument-identity.ts +38 -1
  413. package/src/utils/options.ts +46 -1
  414. package/src/utils/price-history.ts +4 -0
  415. package/src/utils/reported-money.ts +8 -4
  416. package/src/utils/sec.ts +68 -0
@@ -3,6 +3,36 @@ import type { BrokerContractRef } from "./instrument";
3
3
  type BrokerOrderAction = "BUY" | "SELL";
4
4
  export type BrokerOrderType = "MKT" | "LMT" | "STP" | "STP LMT";
5
5
 
6
+ /** Explicit account classification for order confirmation. Missing values are treated as unknown. */
7
+ export type BrokerTradingMode = "simulation" | "live" | "unknown";
8
+
9
+ /** Synchronous ticket metadata. Adapters may vary this by contract and order type. */
10
+ export interface BrokerTradingCapabilities {
11
+ enabled: boolean;
12
+ disabledReason?: string;
13
+ orderTypes: BrokerOrderType[];
14
+ tif: string[];
15
+ extendedHours?: boolean;
16
+ /** When set, extended hours are available only for these time-in-force values. */
17
+ extendedHoursTif?: string[];
18
+ fractionalQuantity?: boolean;
19
+ /** Explicitly permits sells beyond a known long position. Omission blocks short sales. */
20
+ shortSelling?: boolean;
21
+ minQuantity?: number;
22
+ quantityStep?: number;
23
+ priceDecimals?: number;
24
+ /** Broker-reported price increment for this instrument/order. Omit if unknown. */
25
+ priceStep?: number;
26
+ modify?: boolean;
27
+ cancel?: boolean;
28
+ executionKind?: "fills" | "order-summaries";
29
+ /**
30
+ * Multi-leg option orders take up to this many legs (at least 2). Declared
31
+ * for a combo contract (`secType: "BAG"`); omit when the broker takes none.
32
+ */
33
+ maxLegs?: number;
34
+ }
35
+
6
36
  export interface BrokerCashBalance {
7
37
  currency: string;
8
38
  quantity: number;
@@ -13,6 +43,9 @@ export interface BrokerCashBalance {
13
43
  export interface BrokerAccount {
14
44
  accountId: string;
15
45
  name: string;
46
+ tradingMode?: BrokerTradingMode;
47
+ /** Broker-reported account classification, such as Cash or Margin. Omit when unavailable. */
48
+ accountType?: string;
16
49
  currency?: string;
17
50
  /** "cloud" is a connection held by the user's Gloom Cloud account. */
18
51
  source?: "gateway" | "flex" | "cloud";
@@ -64,10 +97,25 @@ export interface BrokerPortfolioPerformance {
64
97
  points: BrokerPortfolioPerformancePoint[];
65
98
  }
66
99
 
100
+ /**
101
+ * One leg of a multi-leg option order. The action is what happens to this
102
+ * contract when the order fills; the ratio is its contracts per spread.
103
+ */
104
+ export interface BrokerOrderLeg {
105
+ contract: BrokerContractRef;
106
+ action: BrokerOrderAction;
107
+ ratio: number;
108
+ }
109
+
67
110
  export interface BrokerOrderRequest {
68
111
  brokerInstanceId?: string;
69
112
  accountId?: string;
113
+ /** A combo contract (`secType: "BAG"`, the underlying's symbol) when the order has legs. */
70
114
  contract: BrokerContractRef;
115
+ /**
116
+ * BUY pays a net debit and SELL receives a net credit. With legs, the
117
+ * legs' own actions say which contracts are bought and sold.
118
+ */
71
119
  action: BrokerOrderAction;
72
120
  orderType: BrokerOrderType;
73
121
  quantity: number;
@@ -75,9 +123,23 @@ export interface BrokerOrderRequest {
75
123
  stopPrice?: number;
76
124
  tif?: string;
77
125
  outsideRth?: boolean;
126
+ /**
127
+ * 2 or more option legs filled as one order. `quantity` counts spreads and
128
+ * `limitPrice` is the positive net price of one spread.
129
+ */
130
+ legs?: BrokerOrderLeg[];
78
131
  }
79
132
 
80
133
  export interface BrokerOrderPreview {
134
+ currency?: string;
135
+ estimatedCost?: number;
136
+ fees?: number;
137
+ buyingPowerBefore?: number;
138
+ buyingPowerAfter?: number;
139
+ buyingPowerImpact?: number;
140
+ warnings?: string[];
141
+ /** Blocking broker validation errors. A ticket must not submit when these are present. */
142
+ errors?: string[];
81
143
  initMarginBefore?: number;
82
144
  initMarginAfter?: number;
83
145
  maintMarginBefore?: number;
@@ -91,6 +153,8 @@ export interface BrokerOrderPreview {
91
153
 
92
154
  export interface BrokerOrder {
93
155
  orderId: number;
156
+ /** Broker-native identity, scoped to profile and account. Numeric orderId remains the mutation handle. */
157
+ brokerOrderId?: string;
94
158
  brokerInstanceId?: string;
95
159
  accountId?: string;
96
160
  status: string;
@@ -108,12 +172,15 @@ export interface BrokerOrder {
108
172
  reviewUrl?: string;
109
173
  updatedAt: number;
110
174
  contract: BrokerContractRef;
175
+ /** A multi-leg order's legs, as on its request. */
176
+ legs?: BrokerOrderLeg[];
111
177
  }
112
178
 
113
179
  export interface BrokerExecution {
114
180
  execId: string;
115
181
  brokerInstanceId?: string;
116
182
  orderId?: number;
183
+ brokerOrderId?: string;
117
184
  accountId?: string;
118
185
  side: string;
119
186
  shares: number;
package/src/ui/host.tsx CHANGED
@@ -447,6 +447,8 @@ export interface AsciiTextProps extends BoxProps {
447
447
  interface HostTabItem {
448
448
  label: string;
449
449
  value: string;
450
+ /** Artwork before the label, such as a logo; decoration only. */
451
+ leading?: ReactNode;
450
452
  disabled?: boolean;
451
453
  reorderable?: boolean;
452
454
  onClose?: (value: string) => void;
@@ -611,6 +613,8 @@ export interface HostMenuItem {
611
613
  checked?: boolean;
612
614
  /** Tints the label, e.g. a status in its tone; the words carry the meaning. */
613
615
  color?: string;
616
+ /** Artwork before the label, such as a logo; decoration only. */
617
+ leading?: ReactNode;
614
618
  }
615
619
 
616
620
  export interface HostMenuProps {
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Why an update step failed, as a fixed phase and code rather than the error
3
+ * text: the header says the reason in a few words, and error tracking gets
4
+ * the same two values, which carry no path, URL or account name.
5
+ */
6
+
7
+ /** The step that failed. `relaunch` is getting back into the app after an install. */
8
+ export type UpdateFailurePhase = "check" | "download" | "verify" | "apply" | "relaunch";
9
+
10
+ const UPDATE_FAILURE_CODES = [
11
+ /** No route to the release server: offline, DNS, refused, TLS. */
12
+ "offline",
13
+ "timeout",
14
+ /** GitHub answered 403 or 429, its limit for requests without an account. */
15
+ "rate_limited",
16
+ "server_error",
17
+ /** The release has no such file (404). */
18
+ "not_found",
19
+ /** Any other refusal from the release server. */
20
+ "http_error",
21
+ /** The release list or manifest did not read as one. */
22
+ "bad_response",
23
+ /** The release has no build for this platform, or none with a checksum. */
24
+ "no_build",
25
+ /** The download stopped before the whole file arrived. */
26
+ "interrupted",
27
+ /** The file arrived whole but does not match its checksum, or does not unpack. */
28
+ "corrupt",
29
+ "permission_denied",
30
+ /** The macOS app runs from where it cannot be replaced: a disk image, App Translocation, another volume. */
31
+ "wrong_location",
32
+ /** The downloaded update and the installed app are on different disks, so the one cannot be moved onto the other. */
33
+ "cross_device",
34
+ "read_only",
35
+ "disk_full",
36
+ /** Another process holds the files being replaced. */
37
+ "file_in_use",
38
+ /** The update downloaded earlier is no longer where the install expects it. */
39
+ "staged_missing",
40
+ /** A restart into the update came back on the old version. */
41
+ "not_applied",
42
+ /** The app quit with a downloaded update waiting and was opened again on the old version. */
43
+ "not_restarted",
44
+ /** The install finished but the app did not come back by itself. */
45
+ "relaunch_missed",
46
+ "unknown",
47
+ ] as const;
48
+
49
+ export type UpdateFailureCode = typeof UPDATE_FAILURE_CODES[number];
50
+
51
+ export interface UpdateFailure {
52
+ phase: UpdateFailurePhase;
53
+ code: UpdateFailureCode;
54
+ }
55
+
56
+ /** What an HTTP status from the release server means for an update. */
57
+ export function httpStatusFailureCode(status: number): UpdateFailureCode {
58
+ if (status === 403 || status === 429) return "rate_limited";
59
+ if (status === 404 || status === 410) return "not_found";
60
+ if (status >= 500) return "server_error";
61
+ return "http_error";
62
+ }
63
+
64
+ const OFFLINE_ERRNO = new Set([
65
+ "ENOTFOUND",
66
+ "EAI_AGAIN",
67
+ "ECONNREFUSED",
68
+ "ENETUNREACH",
69
+ "EHOSTUNREACH",
70
+ "ENETDOWN",
71
+ // Bun's fetch
72
+ "ConnectionRefused",
73
+ "FailedToOpenSocket",
74
+ "UnableToConnect",
75
+ "CERT_HAS_EXPIRED",
76
+ "UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
77
+ "SELF_SIGNED_CERT_IN_CHAIN",
78
+ ]);
79
+ const INTERRUPTED_ERRNO = new Set(["ECONNRESET", "EPIPE", "ECONNABORTED", "ConnectionClosed"]);
80
+
81
+ /**
82
+ * The code for an error a step threw, from its system error code first and its
83
+ * message second. Only the code leaves the machine; the message stays local.
84
+ */
85
+ export function classifyUpdateError(error: unknown): UpdateFailureCode {
86
+ const record = (error && typeof error === "object" ? error : {}) as { code?: unknown; name?: unknown; message?: unknown };
87
+ const message = typeof record.message === "string" ? record.message : typeof error === "string" ? error : "";
88
+ // A system error code, or one named in the message ("Failed to replace app: EACCES: ...").
89
+ const errno = typeof record.code === "string" ? record.code : /\b(E[A-Z]{3,})\b/.exec(message)?.[1] ?? "";
90
+ switch (errno) {
91
+ case "EACCES":
92
+ case "EPERM":
93
+ return "permission_denied";
94
+ case "EROFS":
95
+ return "read_only";
96
+ case "ENOSPC":
97
+ case "EDQUOT":
98
+ return "disk_full";
99
+ case "EBUSY":
100
+ case "ETXTBSY":
101
+ return "file_in_use";
102
+ case "EXDEV":
103
+ return "cross_device";
104
+ case "ETIMEDOUT":
105
+ return "timeout";
106
+ }
107
+ if (OFFLINE_ERRNO.has(errno)) return "offline";
108
+ if (INTERRUPTED_ERRNO.has(errno)) return "interrupted";
109
+ if (record.name === "TimeoutError" || record.name === "AbortError") return "timeout";
110
+
111
+ const status = /\b(?:HTTP|returned|status|failed:?)\s*\(?(\d{3})\b/i.exec(message)?.[1];
112
+ if (status) return httpStatusFailureCode(Number(status));
113
+ if (/checksum mismatch|hash mismatch/i.test(message)) return "corrupt";
114
+ if (/zstd|decompress|gunzip|incorrect header|unexpected end of (?:file|data)/i.test(message)) return "corrupt";
115
+ if (/timed? ?out/i.test(message)) return "timeout";
116
+ if (/rate limit/i.test(message)) return "rate_limited";
117
+ if (/read-only file system/i.test(message)) return "read_only";
118
+ if (/cross-device/i.test(message)) return "cross_device";
119
+ if (/permission denied|operation not permitted|access is denied/i.test(message)) return "permission_denied";
120
+ if (/no space left|disk full/i.test(message)) return "disk_full";
121
+ if (/resource busy|text file busy|being used by another process/i.test(message)) return "file_in_use";
122
+ if (/connection (?:reset|closed|aborted)|socket hang up|premature close|terminated/i.test(message)) return "interrupted";
123
+ if (/unable to connect|fetch failed|failed to fetch|network|getaddrinfo|could not resolve|certificate/i.test(message)) return "offline";
124
+ if (/invalid update\.json|unexpected .*payload|json/i.test(message)) return "bad_response";
125
+ return "unknown";
126
+ }
127
+
128
+ /** When the failure has no more specific reason: what the step was. */
129
+ const PHASE_REASONS: Record<UpdateFailurePhase, string> = {
130
+ check: "check failed",
131
+ download: "download failed",
132
+ verify: "could not verify the download",
133
+ apply: "could not install",
134
+ relaunch: "could not restart",
135
+ };
136
+
137
+ const CODE_REASONS: Partial<Record<UpdateFailureCode, string>> = {
138
+ offline: "no connection",
139
+ timeout: "connection timed out",
140
+ rate_limited: "GitHub rate limit",
141
+ server_error: "release server error",
142
+ not_found: "release server error",
143
+ http_error: "release server error",
144
+ bad_response: "unreadable release data",
145
+ no_build: "no build for this system",
146
+ interrupted: "download interrupted",
147
+ corrupt: "download corrupted",
148
+ permission_denied: "no permission to replace the app",
149
+ wrong_location: "move the app to Applications",
150
+ cross_device: "app and update on different disks",
151
+ read_only: "install folder is read-only",
152
+ disk_full: "disk full",
153
+ file_in_use: "app files in use",
154
+ staged_missing: "downloaded update missing",
155
+ };
156
+
157
+ /**
158
+ * The reason in a few words, in English (the caller translates it), sized
159
+ * for the header: "download interrupted", "no permission to replace the app".
160
+ */
161
+ export function updateFailureReason(failure: UpdateFailure): string {
162
+ return CODE_REASONS[failure.code] ?? PHASE_REASONS[failure.phase];
163
+ }
@@ -1,5 +1,13 @@
1
1
  import type { UpdateChannel } from "../types/config";
2
+ import { reportUpdateFailure, type UpdateInstallKind } from "../telemetry/update-failures";
2
3
  import { compareSemver, isPrereleaseVersion } from "../utils/semver";
4
+ import {
5
+ classifyUpdateError,
6
+ httpStatusFailureCode,
7
+ type UpdateFailure,
8
+ type UpdateFailureCode,
9
+ type UpdateFailurePhase,
10
+ } from "./failure";
3
11
  import {
4
12
  GITHUB_LATEST_RELEASE_API_URL,
5
13
  GITHUB_RELEASES_API_URL,
@@ -49,6 +57,8 @@ export interface UpdateProgress {
49
57
  message?: string;
50
58
  /** With `ready`: this host can apply the update and relaunch on request. */
51
59
  canRestart?: boolean;
60
+ /** With `error`: the step that failed and why, which the header says in a few words. */
61
+ failure?: UpdateFailure;
52
62
  }
53
63
 
54
64
  export type UpdateAction =
@@ -68,7 +78,7 @@ export type UpdateCheckResult =
68
78
  /** `notice`: why a newer-looking state is not an update, such as a nightly build waiting for a stable release. */
69
79
  | { kind: "current"; notice?: string }
70
80
  | { kind: "disabled" }
71
- | { kind: "error"; error: string };
81
+ | { kind: "error"; error: string; failure?: UpdateFailure };
72
82
 
73
83
  /**
74
84
  * On the stable channel a nightly build is never replaced by an older stable
@@ -302,6 +312,23 @@ export function canSelfUpdate(release: Pick<ReleaseInfo, "updateAction"> | null
302
312
  return release?.updateAction.kind === "self" || release?.updateAction.kind === "desktop";
303
313
  }
304
314
 
315
+ /**
316
+ * Whether the install-update key has something to retry: a release this build
317
+ * can install itself whose last attempt failed. A fresh one downloads on its
318
+ * own, and a manual or managed one names its own command. (A downloaded
319
+ * desktop update has its own use of the key: restarting into it.)
320
+ */
321
+ export function canRetryUpdate(state: {
322
+ updateAvailable: ReleaseInfo | null;
323
+ updateProgress: UpdateProgress | null;
324
+ updateCheckInProgress: boolean;
325
+ }): boolean {
326
+ return !!state.updateAvailable
327
+ && state.updateProgress?.phase === "error"
328
+ && !state.updateCheckInProgress
329
+ && canSelfUpdate(state.updateAvailable);
330
+ }
331
+
305
332
  /** A downloaded update this host can install and relaunch into when the user asks. */
306
333
  export function canRestartToApply(progress: UpdateProgress | null | undefined): boolean {
307
334
  return progress?.phase === "ready" && progress.canRestart === true;
@@ -344,6 +371,11 @@ export async function checkForUpdateDetailed(
344
371
  return { kind: "disabled" };
345
372
  }
346
373
  const nightly = channel === "nightly" && updateAction.kind === "self";
374
+ const failed = (error: string, code: UpdateFailureCode, targetVersion?: string): UpdateCheckResult => {
375
+ const failure: UpdateFailure = { phase: "check", code };
376
+ reportUpdateFailure(failure, { targetVersion, channel: nightly ? "nightly" : "stable" });
377
+ return { kind: "error", error, failure };
378
+ };
347
379
 
348
380
  let timeout: ReturnType<typeof setTimeout> | undefined;
349
381
  try {
@@ -356,7 +388,7 @@ export async function checkForUpdateDetailed(
356
388
  });
357
389
 
358
390
  if (!res.ok) {
359
- return { kind: "error", error: `GitHub returned ${res.status}` };
391
+ return failed(`GitHub returned ${res.status}`, httpStatusFailureCode(res.status));
360
392
  }
361
393
 
362
394
  const payload = await res.json();
@@ -379,16 +411,10 @@ export async function checkForUpdateDetailed(
379
411
 
380
412
  const asset = resolveReleaseAsset(data.assets);
381
413
  if (!asset) {
382
- return {
383
- kind: "error",
384
- error: `No compatible release asset found for ${getAssetBaseName()}`,
385
- };
414
+ return failed(`No compatible release asset found for ${getAssetBaseName()}`, "no_build", version);
386
415
  }
387
416
  if (!asset.checksum) {
388
- return {
389
- kind: "error",
390
- error: `Release asset ${asset.name} is missing a valid SHA-256 digest`,
391
- };
417
+ return failed(`Release asset ${asset.name} is missing a valid SHA-256 digest`, "no_build", version);
392
418
  }
393
419
 
394
420
  return {
@@ -406,12 +432,10 @@ export async function checkForUpdateDetailed(
406
432
  };
407
433
  } catch (error: unknown) {
408
434
  if (error instanceof Error && error.name === "AbortError") {
409
- return { kind: "error", error: "Update check timed out" };
435
+ return failed("Update check timed out", "timeout");
410
436
  }
411
- return {
412
- kind: "error",
413
- error: error instanceof Error ? error.message : "Update check failed",
414
- };
437
+ const code = classifyUpdateError(error);
438
+ return failed(error instanceof Error ? error.message : "Update check failed", code === "unknown" && error instanceof SyntaxError ? "bad_response" : code);
415
439
  } finally {
416
440
  if (timeout) clearTimeout(timeout);
417
441
  }
@@ -507,14 +531,20 @@ export async function performUpdate(
507
531
  }
508
532
 
509
533
  const checksum = /^[a-f\d]{64}$/i.exec(release.checksum ?? "")?.[0].toLowerCase();
534
+ const fail = (failure: UpdateFailure, error: string) => {
535
+ reportUpdateFailure(failure, { targetVersion: release.version, channel: release.channel });
536
+ onProgress({ phase: "error", error, failure });
537
+ };
510
538
  if (!checksum) {
511
- onProgress({ phase: "error", error: "Self-update requires a valid SHA-256 checksum." });
539
+ fail({ phase: "verify", code: "no_build" }, "Self-update requires a valid SHA-256 checksum.");
512
540
  return;
513
541
  }
514
542
 
515
543
  const updatePath = execPath + ".update";
516
544
  const oldPath = execPath + ".old";
517
545
  let unlinkUpdatePath: ((path: string) => void) | null = null;
546
+ /** The step under way, which a thrown error is reported against. */
547
+ let phase: UpdateFailurePhase = "download";
518
548
 
519
549
  try {
520
550
  // Not import(): Bun folds even a specifier held in a variable and bundles
@@ -531,7 +561,7 @@ export async function performUpdate(
531
561
 
532
562
  const res = await fetch(release.downloadUrl);
533
563
  if (!res.ok || !res.body) {
534
- throw new Error(`Download failed: ${res.status}`);
564
+ throw new UpdateStepError({ phase: "download", code: httpStatusFailureCode(res.status) }, `Download failed: ${res.status}`);
535
565
  }
536
566
 
537
567
  const contentLength = Number(res.headers.get("content-length") || 0);
@@ -540,7 +570,18 @@ export async function performUpdate(
540
570
  let received = 0;
541
571
 
542
572
  while (true) {
543
- const { done, value } = await reader.read();
573
+ let chunk: Awaited<ReturnType<typeof reader.read>>;
574
+ try {
575
+ chunk = await reader.read();
576
+ } catch (error) {
577
+ // The connection broke partway: whatever it says, the file is incomplete.
578
+ const code = classifyUpdateError(error);
579
+ throw new UpdateStepError(
580
+ { phase: "download", code: code === "timeout" ? code : "interrupted" },
581
+ error instanceof Error ? error.message : "Download interrupted",
582
+ );
583
+ }
584
+ const { done, value } = chunk;
544
585
  if (done) break;
545
586
  chunks.push(value);
546
587
  received += value.length;
@@ -551,7 +592,14 @@ export async function performUpdate(
551
592
  });
552
593
  }
553
594
  }
595
+ if (contentLength > 0 && received < contentLength) {
596
+ throw new UpdateStepError(
597
+ { phase: "download", code: "interrupted" },
598
+ `Download stopped at ${received} of ${contentLength} bytes`,
599
+ );
600
+ }
554
601
 
602
+ phase = "verify";
555
603
  const downloaded = new Uint8Array(received);
556
604
  let offset = 0;
557
605
  for (const chunk of chunks) {
@@ -560,11 +608,12 @@ export async function performUpdate(
560
608
  }
561
609
  const actual = createHash("sha256").update(downloaded).digest("hex");
562
610
  if (actual !== checksum) {
563
- throw new Error(`Checksum mismatch: expected ${checksum}, got ${actual}`);
611
+ throw new UpdateStepError({ phase: "verify", code: "corrupt" }, `Checksum mismatch: expected ${checksum}, got ${actual}`);
564
612
  }
565
613
  const nextBinary = release.compressed
566
614
  ? new Uint8Array(gunzipSync(downloaded))
567
615
  : downloaded;
616
+ phase = "apply";
568
617
  await Bun.write(updatePath, nextBinary);
569
618
  chmodSync(updatePath, 0o755);
570
619
 
@@ -600,9 +649,24 @@ export async function performUpdate(
600
649
  try {
601
650
  unlinkUpdatePath?.(updatePath);
602
651
  } catch { /* Intentional ignore: Remove a partial update download while the original failure is reported through update progress. */ }
603
- onProgress({
604
- phase: "error",
605
- error: err instanceof Error ? err.message : "Update failed",
606
- });
652
+ const failure = err instanceof UpdateStepError ? err.failure : { phase, code: classifyUpdateError(err) };
653
+ fail(failure, err instanceof Error ? err.message : "Update failed");
607
654
  }
608
655
  }
656
+
657
+ /** A failed step whose code is known where it is thrown. */
658
+ class UpdateStepError extends Error {
659
+ constructor(readonly failure: UpdateFailure, message: string) {
660
+ super(message);
661
+ this.name = "UpdateStepError";
662
+ }
663
+ }
664
+
665
+ /** How the running terminal app was installed, as update failure reports name it. */
666
+ export function terminalInstallKind(action: UpdateAction | null = detectUpdateAction()): UpdateInstallKind {
667
+ if (!action) return "terminal-source";
668
+ if (action.kind === "self") return "terminal-binary";
669
+ if (action.kind === "managed") return "terminal-managed";
670
+ if (action.kind === "manual") return "terminal-package-manager";
671
+ return "unknown";
672
+ }
@@ -0,0 +1,108 @@
1
+ import { currenciesNamed } from "./currency-names";
2
+ import { isCurrencyCode } from "./currency-pair";
3
+ import { isIsoCurrencyCode } from "./crypto-pair";
4
+
5
+ /**
6
+ * Signs people write for a currency, as they read them: C$ and US$ as
7
+ * Canadians and Americans write them, S$, NZ$, R$. These settle a sign the
8
+ * runtime gives another currency too (C$ is also the cordoba's). The runtime
9
+ * adds CA$, A$, HK$, €, £, ₹ and the rest, so a sign several currencies use
10
+ * ($, ¥) names all of them.
11
+ */
12
+ const EXTRA_SIGNS: ReadonlyArray<readonly [sign: string, code: string]> = [
13
+ ["US$", "USD"], ["U$S", "USD"], ["C$", "CAD"], ["CAN$", "CAD"], ["AU$", "AUD"], ["S$", "SGD"], ["SG$", "SGD"],
14
+ ["NZ$", "NZD"], ["R$", "BRL"], ["NT$", "TWD"], ["RMB", "CNY"], ["Fr", "CHF"], ["SFr", "CHF"], ["zł", "PLN"],
15
+ ["kr", "SEK"], ["kr", "NOK"], ["kr", "DKK"], ["kr", "ISK"], ["Kč", "CZK"], ["Ft", "HUF"], ["RM", "MYR"],
16
+ ["£", "GBP"], ["¥", "JPY"], ["¥", "CNY"],
17
+ ["$", "USD"], ["$", "CAD"], ["$", "AUD"], ["$", "NZD"], ["$", "HKD"], ["$", "SGD"], ["$", "MXN"],
18
+ ];
19
+
20
+ /** The most traded currencies, first among those a shared sign ($, ¥, kr) names. */
21
+ const SIGN_ORDER = ["USD", "EUR", "JPY", "GBP", "CNY", "CAD", "AUD", "CHF", "HKD", "SGD", "NZD", "MXN", "SEK", "NOK", "DKK"];
22
+
23
+ let signIndex: ReadonlyMap<string, readonly string[]> | undefined;
24
+
25
+ function signKey(sign: string): string {
26
+ return sign.normalize("NFC").replace(/\s+/g, "").toUpperCase();
27
+ }
28
+
29
+ function signs(): ReadonlyMap<string, readonly string[]> {
30
+ if (signIndex) return signIndex;
31
+ const built = new Map<string, string[]>();
32
+ const curated = new Set(EXTRA_SIGNS.map(([sign]) => signKey(sign)));
33
+ const add = (sign: string, code: string, extra = false) => {
34
+ const key = signKey(sign);
35
+ // A code is its own spelling, not a sign: CHF reads as the code.
36
+ if (!key || key === code || (!extra && curated.has(key))) return;
37
+ const codes = built.get(key) ?? [];
38
+ if (!codes.includes(code)) codes.push(code);
39
+ built.set(key, codes);
40
+ };
41
+ const codes = typeof Intl.supportedValuesOf === "function" ? Intl.supportedValuesOf("currency") : [];
42
+ for (const code of codes) {
43
+ if (!isCurrencyCode(code)) continue;
44
+ for (const currencyDisplay of ["symbol", "narrowSymbol"] as const) {
45
+ try {
46
+ const sign = new Intl.NumberFormat("en-US", { style: "currency", currency: code, currencyDisplay })
47
+ .formatToParts(0).find((part) => part.type === "currency")?.value;
48
+ if (sign) add(sign, code);
49
+ } catch {
50
+ // Intentional ignore: a code the runtime lists but cannot format has no sign to read.
51
+ }
52
+ }
53
+ }
54
+ for (const [sign, code] of EXTRA_SIGNS) add(sign, code, true);
55
+ const rank = (code: string) => {
56
+ const at = SIGN_ORDER.indexOf(code);
57
+ return at < 0 ? SIGN_ORDER.length : at;
58
+ };
59
+ for (const codesForSign of built.values()) codesForSign.sort((left, right) => rank(left) - rank(right));
60
+ signIndex = built;
61
+ return built;
62
+ }
63
+
64
+ /** The currencies a name (`Canadian dollar`, `yen`) or a sign (`CA$`, `C$`, `€`) stands for, most traded first. */
65
+ function currenciesForNameOrSign(typed: string): string[] {
66
+ const named = currenciesNamed(typed).map(({ code }) => code);
67
+ if (named.length > 0) return named;
68
+ return [...signs().get(signKey(typed.trim())) ?? []];
69
+ }
70
+
71
+ /** What a typed base currency is: its ISO 4217 code, or why it is none and the codes it may have meant. */
72
+ export type BaseCurrencyReading =
73
+ | { code: string }
74
+ | { error: string; suggestions: string[] };
75
+
76
+ /**
77
+ * A base currency as `config set baseCurrency` takes it: an ISO 4217 code in
78
+ * any case (`cad` is CAD). A currency's name or sign (`Canadian dollar`,
79
+ * `CA$`) is refused with the code it stands for, so the setting is always a
80
+ * code the conversions can price; so is a code that is no currency (XAU, XYZ).
81
+ */
82
+ export function readBaseCurrency(typed: string): BaseCurrencyReading {
83
+ const text = typed.trim();
84
+ const upper = text.toUpperCase();
85
+ if (/^[A-Z]{3}$/.test(upper) && isCurrencyCode(upper)) return { code: upper };
86
+ if (/^[A-Z]{3}$/.test(upper) && isIsoCurrencyCode(upper)) {
87
+ return { error: `${upper} is a metal, drawing-right or test code, not a currency.`, suggestions: [] };
88
+ }
89
+ const suggestions = text ? currenciesForNameOrSign(text) : [];
90
+ const shown = text ? JSON.stringify(text) : "An empty value";
91
+ if (suggestions.length === 1) return { error: `${shown} is not a currency code. Did you mean ${suggestions[0]}?`, suggestions };
92
+ if (suggestions.length > 1) {
93
+ return { error: `${shown} is not a currency code; it can be ${suggestions.slice(0, 6).join(", ")}.`, suggestions };
94
+ }
95
+ return { error: `${shown} is not an ISO 4217 currency code.`, suggestions };
96
+ }
97
+
98
+ /**
99
+ * A base currency as it was saved, as the setting reads it now: a code in
100
+ * capitals, and a name or sign an older version stored (`Canadian dollar`,
101
+ * `CA$`, `cad`) as the one code it stands for. Anything else stays as saved,
102
+ * for `doctor` and `config get` to flag, rather than turning into USD.
103
+ */
104
+ export function normalizeStoredBaseCurrency(value: string): string {
105
+ const reading = readBaseCurrency(value);
106
+ if ("code" in reading) return reading.code;
107
+ return reading.suggestions.length === 1 ? reading.suggestions[0]! : value.trim();
108
+ }