@gloom-sh/terminal 0.19.1 → 0.19.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 (554) hide show
  1. package/README.md +7 -1
  2. package/package.json +2 -1
  3. package/patches/electrobun@1.18.1.patch +108 -1
  4. package/src/account/upgrade-dialog.tsx +4 -23
  5. package/src/api-client/coalesce.ts +118 -0
  6. package/src/api-client/credit-boards.ts +13 -0
  7. package/src/api-client/crypto-etf-flows.ts +6 -0
  8. package/src/api-client/crypto-markets.ts +40 -0
  9. package/src/api-client/data.ts +201 -14
  10. package/src/api-client/equity-screener.ts +10 -0
  11. package/src/api-client/errors.ts +58 -0
  12. package/src/api-client/gpu.ts +29 -0
  13. package/src/api-client/index.ts +5 -2
  14. package/src/api-client/plan-access.ts +10 -1
  15. package/src/api-client/plan-rules.ts +35 -0
  16. package/src/api-client/research-activity.ts +1 -32
  17. package/src/api-client/types.ts +21 -1
  18. package/src/app/attention-notice.tsx +94 -0
  19. package/src/app/runtime/startup.ts +3 -1
  20. package/src/app.tsx +23 -1
  21. package/src/architecture/core-builtin-allowlist.json +2 -1
  22. package/src/capabilities/chart-series.ts +2 -1
  23. package/src/capabilities/factories.ts +1 -0
  24. package/src/cli/ascii-output.ts +42 -0
  25. package/src/cli/base-converter.ts +10 -2
  26. package/src/cli/command-options.ts +16 -1
  27. package/src/cli/commands/crypto-hints.ts +23 -4
  28. package/src/cli/commands/market.ts +101 -27
  29. package/src/cli/commands/news-search.ts +24 -5
  30. package/src/cli/commands/overview.ts +14 -10
  31. package/src/cli/commands/search.ts +84 -25
  32. package/src/cli/commands/system.ts +22 -30
  33. package/src/cli/commands/ticker.ts +49 -9
  34. package/src/cli/commands/version.ts +33 -0
  35. package/src/cli/context.ts +6 -9
  36. package/src/cli/data-dir.ts +7 -0
  37. package/src/cli/desktop-pane-shot-api-policy.ts +85 -0
  38. package/src/cli/desktop-pane-shot-browser.ts +320 -0
  39. package/src/cli/desktop-pane-shot-routes.ts +26 -1
  40. package/src/cli/desktop-pane-shot.ts +196 -256
  41. package/src/cli/early-command.ts +42 -0
  42. package/src/cli/earnings-calendar.ts +9 -0
  43. package/src/cli/entry.ts +19 -13
  44. package/src/cli/errors.ts +12 -2
  45. package/src/cli/glossary.ts +2 -1
  46. package/src/cli/help.ts +1 -1
  47. package/src/cli/history-export.ts +22 -2
  48. package/src/cli/history-rows.ts +189 -11
  49. package/src/cli/index.ts +18 -1
  50. package/src/cli/known-gaps.ts +51 -5
  51. package/src/cli/listing-arg.ts +63 -10
  52. package/src/cli/options.ts +1 -1
  53. package/src/cli/output-file.ts +60 -13
  54. package/src/cli/pane-functions/catalog.ts +14 -3
  55. package/src/cli/pane-functions/data.ts +1 -1
  56. package/src/cli/pane-functions/dom.ts +45 -0
  57. package/src/cli/pane-functions/freshness.ts +46 -9
  58. package/src/cli/pane-functions/function-help.ts +17 -9
  59. package/src/cli/pane-functions/headless.ts +22 -14
  60. package/src/cli/pane-functions/index.ts +6 -2
  61. package/src/cli/pane-functions/options.ts +6 -2
  62. package/src/cli/pane-functions/resolver.ts +27 -5
  63. package/src/cli/pane-functions/screenshot-assessment.ts +33 -0
  64. package/src/cli/pane-functions/screenshot-bridge.ts +94 -13
  65. package/src/cli/pane-functions/screenshot-http.ts +70 -0
  66. package/src/cli/pane-functions/screenshot-render.ts +19 -5
  67. package/src/cli/quote-answer.ts +49 -6
  68. package/src/cli/registry.ts +41 -13
  69. package/src/cli/result.ts +12 -6
  70. package/src/cli/ticker-resolution.ts +2 -1
  71. package/src/cli/types.ts +2 -0
  72. package/src/components/chart/composite/format.ts +3 -0
  73. package/src/components/chart-table/header.tsx +1 -1
  74. package/src/components/chart-table/layout.ts +28 -11
  75. package/src/components/command-bar/action-runtime.ts +4 -0
  76. package/src/components/command-bar/commands/direct/index.ts +18 -33
  77. package/src/components/command-bar/commands/direct/runtime.ts +5 -0
  78. package/src/components/command-bar/commands/registry.ts +10 -1
  79. package/src/components/command-bar/routes/root/command-items.ts +9 -4
  80. package/src/components/command-bar/routes/root/runtime.ts +51 -6
  81. package/src/components/command-bar/selection-runtime.ts +12 -29
  82. package/src/components/command-bar/surface/index.tsx +9 -3
  83. package/src/components/company-logo.tsx +2 -1
  84. package/src/components/layout/detached-pane-shell.tsx +2 -2
  85. package/src/components/layout/header.tsx +10 -5
  86. package/src/components/layout/market-summary.ts +25 -7
  87. package/src/components/layout/pane/footer/index.tsx +8 -2
  88. package/src/components/layout/pane/footer/model.ts +73 -6
  89. package/src/components/layout/pane/header.tsx +2 -2
  90. package/src/components/layout/pane/status-footer.ts +2 -1
  91. package/src/components/market-board/index.tsx +3 -2
  92. package/src/components/onboarding/desks-step.tsx +12 -4
  93. package/src/components/onboarding/first-run-workspace.ts +81 -4
  94. package/src/components/onboarding/onboarding-frame.tsx +4 -2
  95. package/src/components/onboarding/onboarding-keyboard.ts +8 -5
  96. package/src/components/onboarding/onboarding-wizard.tsx +60 -26
  97. package/src/components/onboarding/portfolio-step/positions-panel.tsx +7 -2
  98. package/src/components/onboarding/run-setup-again.ts +24 -0
  99. package/src/components/onboarding/use-onboarding-progress.ts +33 -15
  100. package/src/components/onboarding/use-onboarding-upgrade.ts +2 -6
  101. package/src/components/onboarding/wizard-model.ts +57 -3
  102. package/src/components/onboarding/wizard-positions.ts +1 -1
  103. package/src/components/ui/delayed-mark.tsx +56 -0
  104. package/src/components/ui/display.tsx +16 -5
  105. package/src/components/ui/external-link.tsx +3 -6
  106. package/src/components/ui/icon.tsx +16 -3
  107. package/src/components/ui/logo-mark.tsx +8 -5
  108. package/src/components/ui/stat-grid.tsx +263 -19
  109. package/src/core/app-services.ts +11 -2
  110. package/src/data/app-persistence.ts +9 -7
  111. package/src/data/config/store/node.ts +11 -5
  112. package/src/data/config/store/normalize.ts +16 -3
  113. package/src/data/plugin-cache.ts +15 -0
  114. package/src/data/plugin-state-store.ts +81 -24
  115. package/src/data/profile-permissions.ts +47 -0
  116. package/src/data/resource-store.ts +165 -90
  117. package/src/data/session-store.ts +31 -8
  118. package/src/data/sqlite/access.ts +324 -0
  119. package/src/data/sqlite/checkpointer.ts +285 -0
  120. package/src/data/sqlite/database.ts +80 -4
  121. package/src/data/sqlite/retry.ts +50 -9
  122. package/src/data/ticker-store.ts +57 -23
  123. package/src/docs/snapshot.json +191 -78
  124. package/src/i18n/es.ts +18 -2
  125. package/src/i18n/ja.ts +18 -2
  126. package/src/i18n/ko.ts +18 -2
  127. package/src/i18n/zh-cn.ts +18 -2
  128. package/src/i18n/zh-tw.ts +18 -2
  129. package/src/layout/desk-keys.ts +4 -1
  130. package/src/layout/desks.ts +38 -5
  131. package/src/layout-marketplace/gallery-desktop.tsx +45 -16
  132. package/src/market-data/batched-reads.ts +16 -0
  133. package/src/market-data/coordinator/auxiliary.ts +4 -2
  134. package/src/market-data/coordinator/index.ts +11 -2
  135. package/src/market-data/futures-board.ts +1 -1
  136. package/src/market-data/futures-contract.ts +3 -1
  137. package/src/market-data/market/index-level.ts +6 -4
  138. package/src/market-data/published-apac-sessions.ts +6 -0
  139. package/src/market-data/realized-volatility.ts +4 -2
  140. package/src/news/aggregator.ts +7 -4
  141. package/src/news/news-model.ts +11 -0
  142. package/src/news/story-id.ts +22 -0
  143. package/src/news/ticker-symbols.ts +17 -1
  144. package/src/notifications/app-notifier.ts +35 -14
  145. package/src/plugins/auto-update-node.ts +10 -6
  146. package/src/plugins/builtin/account-management/form-components.tsx +1 -1
  147. package/src/plugins/builtin/account-management/index.tsx +2 -3
  148. package/src/plugins/builtin/account-management/model.ts +1 -1
  149. package/src/plugins/builtin/alerts/cli-forms.ts +15 -1
  150. package/src/plugins/builtin/alerts/events-pane.tsx +7 -5
  151. package/src/plugins/builtin/alerts/events.ts +10 -2
  152. package/src/plugins/builtin/alerts/index.tsx +5 -5
  153. package/src/plugins/builtin/alerts/research-builder.ts +5 -1
  154. package/src/plugins/builtin/alerts/research-rules.ts +18 -0
  155. package/src/plugins/builtin/alerts/targets.ts +4 -1
  156. package/src/plugins/builtin/attention/headless.ts +1 -1
  157. package/src/plugins/builtin/attention/index.tsx +2 -4
  158. package/src/plugins/builtin/attention/listing.ts +1 -1
  159. package/src/plugins/builtin/attention/series.ts +1 -1
  160. package/src/plugins/builtin/awards/index.tsx +1 -2
  161. package/src/plugins/builtin/backtest/client.ts +1 -0
  162. package/src/plugins/builtin/backtest/index.tsx +1 -2
  163. package/src/plugins/builtin/backtest/rules.ts +3 -1
  164. package/src/plugins/builtin/backtest/window.ts +8 -7
  165. package/src/plugins/builtin/broker-manager/detail.tsx +16 -3
  166. package/src/plugins/builtin/broker-manager/index.tsx +1 -1
  167. package/src/plugins/builtin/buildout/cells.tsx +5 -0
  168. package/src/plugins/builtin/buildout/company/flow.tsx +367 -0
  169. package/src/plugins/builtin/buildout/company/id.ts +2 -0
  170. package/src/plugins/builtin/buildout/company/model.ts +247 -0
  171. package/src/plugins/builtin/buildout/company/pane.tsx +317 -0
  172. package/src/plugins/builtin/buildout/data-runtime.ts +31 -10
  173. package/src/plugins/builtin/buildout/facilities.ts +309 -0
  174. package/src/plugins/builtin/buildout/format.ts +9 -0
  175. package/src/plugins/builtin/buildout/index.tsx +51 -5
  176. package/src/plugins/builtin/buildout/model/index.ts +62 -10
  177. package/src/plugins/builtin/buildout/model/normalizers.ts +5 -0
  178. package/src/plugins/builtin/buildout/model/types.ts +11 -2
  179. package/src/plugins/builtin/buildout/pane/index.tsx +91 -20
  180. package/src/plugins/builtin/buildout/pane/status.tsx +1 -1
  181. package/src/plugins/builtin/buildout/pane/themes.tsx +110 -0
  182. package/src/plugins/builtin/buildout/table-model.ts +26 -2
  183. package/src/plugins/builtin/builtin-plugin-meta.ts +16 -0
  184. package/src/plugins/builtin/cds/headless.ts +10 -2
  185. package/src/plugins/builtin/cds/model.ts +7 -3
  186. package/src/plugins/builtin/central-bank-rates/index.tsx +1 -2
  187. package/src/plugins/builtin/central-bank-rates/pane.tsx +2 -1
  188. package/src/plugins/builtin/central-bank-rates/series.ts +2 -3
  189. package/src/plugins/builtin/changelog/index.tsx +3 -2
  190. package/src/plugins/builtin/chart-composer/cli-options.ts +16 -0
  191. package/src/plugins/builtin/chart-composer/headless-schema.ts +10 -0
  192. package/src/plugins/builtin/chart-composer/headless.ts +88 -21
  193. package/src/plugins/builtin/chart-composer/index.tsx +2 -3
  194. package/src/plugins/builtin/chart-composer/pane.tsx +30 -2
  195. package/src/plugins/builtin/chart-composer/series-expression.ts +3 -2
  196. package/src/plugins/builtin/chart-composer/session-range.ts +5 -2
  197. package/src/plugins/builtin/chart-composer/studies.ts +1 -1
  198. package/src/plugins/builtin/chat/controller/index.ts +1 -1
  199. package/src/plugins/builtin/chat/controller/messages.ts +15 -4
  200. package/src/plugins/builtin/chat/ghost-user.ts +8 -0
  201. package/src/plugins/builtin/chat/index.tsx +5 -5
  202. package/src/plugins/builtin/clinical-trials/index.tsx +1 -2
  203. package/src/plugins/builtin/cloud/company-picker.tsx +1 -1
  204. package/src/plugins/builtin/cloud/index.tsx +1 -2
  205. package/src/plugins/builtin/cloud/plugin.tsx +6 -4
  206. package/src/plugins/builtin/cloud/team/module.tsx +1 -1
  207. package/src/plugins/builtin/cloud/thesis/module.tsx +1 -1
  208. package/src/plugins/builtin/cloud/trial-offer-status-widget.tsx +24 -1
  209. package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +39 -15
  210. package/src/plugins/builtin/comment-letters/index.tsx +1 -2
  211. package/src/plugins/builtin/commodity-balances/client.ts +4 -1
  212. package/src/plugins/builtin/commodity-balances/index.tsx +3 -3
  213. package/src/plugins/builtin/commodity-balances/pane.tsx +2 -2
  214. package/src/plugins/builtin/congress-trades/aggregates.ts +13 -2
  215. package/src/plugins/builtin/congress-trades/detail.tsx +20 -4
  216. package/src/plugins/builtin/congress-trades/headless.ts +2 -0
  217. package/src/plugins/builtin/congress-trades/model.ts +92 -64
  218. package/src/plugins/builtin/congress-trades/table.tsx +14 -2
  219. package/src/plugins/builtin/connections/index.ts +2 -3
  220. package/src/plugins/builtin/cot/client.ts +28 -2
  221. package/src/plugins/builtin/cot/headless.ts +4 -1
  222. package/src/plugins/builtin/cot/index.tsx +5 -4
  223. package/src/plugins/builtin/cot/model.ts +58 -7
  224. package/src/plugins/builtin/cot/pane.tsx +6 -2
  225. package/src/plugins/builtin/cpi/client.ts +6 -5
  226. package/src/plugins/builtin/cpi/index.tsx +1 -2
  227. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  228. package/src/plugins/builtin/credit-boards/client.ts +7 -2
  229. package/src/plugins/builtin/credit-boards/headless.ts +25 -12
  230. package/src/plugins/builtin/credit-boards/model.ts +29 -0
  231. package/src/plugins/builtin/credit-boards/series.ts +3 -4
  232. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +9 -4
  233. package/src/plugins/builtin/credit-conditions/client.ts +22 -14
  234. package/src/plugins/builtin/crypto-board/bitcoin-network.ts +143 -0
  235. package/src/plugins/builtin/crypto-board/client.ts +5 -5
  236. package/src/plugins/builtin/crypto-board/headless.ts +4 -3
  237. package/src/plugins/builtin/crypto-board/index.tsx +4 -2
  238. package/src/plugins/builtin/crypto-board/series.ts +6 -4
  239. package/src/plugins/builtin/crypto-etf-flows/client.ts +9 -2
  240. package/src/plugins/builtin/crypto-etf-flows/headless.ts +61 -6
  241. package/src/plugins/builtin/crypto-etf-flows/index.tsx +1 -2
  242. package/src/plugins/builtin/crypto-etf-flows/model.ts +14 -1
  243. package/src/plugins/builtin/crypto-etf-flows/pane.tsx +8 -5
  244. package/src/plugins/builtin/debt-maturities/headless.ts +4 -3
  245. package/src/plugins/builtin/debt-maturities/model.ts +17 -1
  246. package/src/plugins/builtin/debt-maturities/pane.tsx +18 -4
  247. package/src/plugins/builtin/debt-maturities/series.ts +3 -3
  248. package/src/plugins/builtin/debug/index.tsx +1 -1
  249. package/src/plugins/builtin/dividend-yield/client.ts +32 -2
  250. package/src/plugins/builtin/dividend-yield/headless.ts +58 -33
  251. package/src/plugins/builtin/dividend-yield/index.tsx +37 -2
  252. package/src/plugins/builtin/dividend-yield/pane.tsx +32 -13
  253. package/src/plugins/builtin/dividend-yield/ranking-reads.ts +73 -0
  254. package/src/plugins/builtin/dividend-yield/ranking-view.tsx +163 -0
  255. package/src/plugins/builtin/dividend-yield/ranking.ts +1 -1
  256. package/src/plugins/builtin/dividend-yield/view.ts +21 -0
  257. package/src/plugins/builtin/docs/index.tsx +2 -3
  258. package/src/plugins/builtin/doe/client.ts +6 -5
  259. package/src/plugins/builtin/doe/headless.ts +16 -3
  260. package/src/plugins/builtin/doe/index.tsx +5 -4
  261. package/src/plugins/builtin/doe/prices.ts +2 -3
  262. package/src/plugins/builtin/doe/series.ts +4 -4
  263. package/src/plugins/builtin/earnings/board.tsx +2 -1
  264. package/src/plugins/builtin/earnings/history.tsx +2 -1
  265. package/src/plugins/builtin/earnings-calls/calendar.tsx +10 -2
  266. package/src/plugins/builtin/earnings-calls/data.ts +19 -11
  267. package/src/plugins/builtin/earnings-calls/pane.tsx +27 -14
  268. package/src/plugins/builtin/earnings-ripple/client.ts +32 -7
  269. package/src/plugins/builtin/earnings-ripple/index.tsx +1 -2
  270. package/src/plugins/builtin/econ/calendar-model.ts +29 -6
  271. package/src/plugins/builtin/econ/detail-view.tsx +16 -2
  272. package/src/plugins/builtin/econ-statistics/index.tsx +1 -2
  273. package/src/plugins/builtin/equity-screener/client.ts +1 -1
  274. package/src/plugins/builtin/equity-screener/headless.ts +1 -0
  275. package/src/plugins/builtin/equity-screener/ideas.ts +30 -1
  276. package/src/plugins/builtin/equity-screener/index.tsx +1 -2
  277. package/src/plugins/builtin/equity-screener/model.ts +28 -4
  278. package/src/plugins/builtin/equity-screener/pane.tsx +5 -0
  279. package/src/plugins/builtin/equity-screener/screen-check.ts +23 -1
  280. package/src/plugins/builtin/estimate-revisions/index.tsx +1 -2
  281. package/src/plugins/builtin/executives/data.ts +6 -1
  282. package/src/plugins/builtin/executives/pane.tsx +9 -21
  283. package/src/plugins/builtin/exposure/holdings.ts +35 -13
  284. package/src/plugins/builtin/exposure/index.tsx +1 -2
  285. package/src/plugins/builtin/fear-greed/cache.ts +4 -0
  286. package/src/plugins/builtin/fear-greed/cli.ts +44 -0
  287. package/src/plugins/builtin/fear-greed/index.tsx +5 -53
  288. package/src/plugins/builtin/fear-greed/pane.tsx +2 -1
  289. package/src/plugins/builtin/fear-greed/series.ts +6 -4
  290. package/src/plugins/builtin/fixed-income/index.tsx +1 -2
  291. package/src/plugins/builtin/futures/board-reads.ts +25 -0
  292. package/src/plugins/builtin/futures/coalesced-bump.ts +44 -0
  293. package/src/plugins/builtin/futures/index.tsx +15 -0
  294. package/src/plugins/builtin/futures/model.ts +14 -0
  295. package/src/plugins/builtin/futures/use-board-terms.ts +22 -15
  296. package/src/plugins/builtin/futures/use-front-returns.ts +20 -14
  297. package/src/plugins/builtin/futures-curve/headless.ts +6 -1
  298. package/src/plugins/builtin/futures-curve/index.tsx +1 -2
  299. package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
  300. package/src/plugins/builtin/gpu/board.tsx +51 -12
  301. package/src/plugins/builtin/gpu/client.ts +19 -3
  302. package/src/plugins/builtin/gpu/equities.tsx +5 -2
  303. package/src/plugins/builtin/gpu/headless.ts +14 -2
  304. package/src/plugins/builtin/gpu/index.tsx +1 -2
  305. package/src/plugins/builtin/gpu/model.ts +20 -2
  306. package/src/plugins/builtin/gpu/pane.tsx +1 -1
  307. package/src/plugins/builtin/gpu/series.ts +2 -3
  308. package/src/plugins/builtin/help/index.tsx +1 -1
  309. package/src/plugins/builtin/holders/beneficial-view.tsx +1 -2
  310. package/src/plugins/builtin/holders/index-holders.ts +24 -12
  311. package/src/plugins/builtin/holders/index.tsx +2 -3
  312. package/src/plugins/builtin/ipo-calendar/index.tsx +1 -2
  313. package/src/plugins/builtin/iv-history/index.tsx +1 -2
  314. package/src/plugins/builtin/jodi/client.ts +4 -1
  315. package/src/plugins/builtin/jodi/index.tsx +1 -2
  316. package/src/plugins/builtin/kelly-sizer/index.tsx +1 -2
  317. package/src/plugins/builtin/layout-manager/index.tsx +9 -8
  318. package/src/plugins/builtin/macro-day/pane.tsx +4 -2
  319. package/src/plugins/builtin/market-halts/index.tsx +1 -2
  320. package/src/plugins/builtin/market-heatmap/index.tsx +11 -2
  321. package/src/plugins/builtin/market-movers/client.ts +1 -0
  322. package/src/plugins/builtin/market-movers/index.tsx +16 -5
  323. package/src/plugins/builtin/market-movers/screener.ts +23 -6
  324. package/src/plugins/builtin/market-movers/session-body.tsx +6 -2
  325. package/src/plugins/builtin/market-valuation/index.tsx +1 -2
  326. package/src/plugins/builtin/members/index.tsx +1 -2
  327. package/src/plugins/builtin/money-markets/index.tsx +1 -2
  328. package/src/plugins/builtin/money-markets/pane.tsx +2 -1
  329. package/src/plugins/builtin/money-markets/series.ts +3 -5
  330. package/src/plugins/builtin/news/headless.ts +7 -0
  331. package/src/plugins/builtin/news/wire/index.tsx +1 -1
  332. package/src/plugins/builtin/news/wire/news/pop-out.tsx +9 -3
  333. package/src/plugins/builtin/notes/index.tsx +3 -6
  334. package/src/plugins/builtin/openfda/index.tsx +1 -2
  335. package/src/plugins/builtin/options/view.tsx +6 -5
  336. package/src/plugins/builtin/options-calculator/index.tsx +1 -2
  337. package/src/plugins/builtin/options-positioning/index.tsx +1 -2
  338. package/src/plugins/builtin/options-scenario/index.tsx +2 -4
  339. package/src/plugins/builtin/perps/index.tsx +1 -2
  340. package/src/plugins/builtin/plugin-marketplace/index.tsx +2 -3
  341. package/src/plugins/builtin/plugin-marketplace/pane.tsx +2 -1
  342. package/src/plugins/builtin/plugin-module.ts +16 -2
  343. package/src/plugins/builtin/portfolio-list/cli/render.ts +53 -3
  344. package/src/plugins/builtin/portfolio-list/index.tsx +1 -2
  345. package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -0
  346. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -2
  347. package/src/plugins/builtin/power/gas-client.ts +2 -1
  348. package/src/plugins/builtin/power/index.tsx +1 -2
  349. package/src/plugins/builtin/power/prices-client.ts +3 -2
  350. package/src/plugins/builtin/rate-path/index.tsx +1 -2
  351. package/src/plugins/builtin/realized-vol/headless.ts +2 -0
  352. package/src/plugins/builtin/realized-vol/index.tsx +1 -2
  353. package/src/plugins/builtin/realized-vol/model.ts +16 -2
  354. package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
  355. package/src/plugins/builtin/relative-rotation/index.tsx +1 -2
  356. package/src/plugins/builtin/research/analyst-headless.ts +33 -6
  357. package/src/plugins/builtin/research/analyst-model.ts +37 -2
  358. package/src/plugins/builtin/research/analyst-pane.tsx +25 -5
  359. package/src/plugins/builtin/research/index.tsx +5 -8
  360. package/src/plugins/builtin/research-search/index.tsx +1 -2
  361. package/src/plugins/builtin/revenue-breakdown/headless.ts +22 -8
  362. package/src/plugins/builtin/revenue-breakdown/model.ts +23 -0
  363. package/src/plugins/builtin/reverse-dcf/pane.tsx +4 -2
  364. package/src/plugins/builtin/risk-factors/data.ts +8 -4
  365. package/src/plugins/builtin/risk-factors/index.tsx +3 -1
  366. package/src/plugins/builtin/risk-factors/pane.tsx +5 -8
  367. package/src/plugins/builtin/roadmap/index.tsx +2 -3
  368. package/src/plugins/builtin/roadmap/pane.tsx +49 -2
  369. package/src/plugins/builtin/scanner/hilo-model.ts +18 -1
  370. package/src/plugins/builtin/scanner/hilo-pane.tsx +12 -2
  371. package/src/plugins/builtin/scanner/live-pane.tsx +7 -4
  372. package/src/plugins/builtin/scanner/lock-note.ts +26 -3
  373. package/src/plugins/builtin/sec/filing-content.ts +38 -6
  374. package/src/plugins/builtin/sec/headless.ts +5 -1
  375. package/src/plugins/builtin/sec/index.tsx +4 -2
  376. package/src/plugins/builtin/sec/model.ts +2 -0
  377. package/src/plugins/builtin/sectors/index.tsx +11 -5
  378. package/src/plugins/builtin/series-inventory.ts +2 -0
  379. package/src/plugins/builtin/shared/cloud-resource.ts +73 -8
  380. package/src/plugins/builtin/shared/cloud-upgrade.ts +24 -1
  381. package/src/plugins/builtin/shared/forming-bars.ts +29 -0
  382. package/src/plugins/builtin/shared/fred-points.ts +6 -1
  383. package/src/plugins/builtin/shared/fx-rate-freshness.ts +17 -0
  384. package/src/plugins/builtin/shared/pro-lock-note.ts +23 -8
  385. package/src/plugins/builtin/shared/series-detail-chart.tsx +4 -2
  386. package/src/plugins/builtin/shared/series-warning.ts +13 -4
  387. package/src/plugins/builtin/shared/use-quote-board.ts +5 -0
  388. package/src/plugins/builtin/short-interest/publications-pane.tsx +2 -1
  389. package/src/plugins/builtin/short-volume/pane.tsx +2 -1
  390. package/src/plugins/builtin/short-volume/series.ts +2 -3
  391. package/src/plugins/builtin/social-mentions/client.ts +15 -10
  392. package/src/plugins/builtin/social-mentions/index.tsx +1 -2
  393. package/src/plugins/builtin/social-mentions/pane.tsx +2 -1
  394. package/src/plugins/builtin/social-mentions/series.ts +2 -3
  395. package/src/plugins/builtin/supply-chain/client.ts +22 -1
  396. package/src/plugins/builtin/supply-chain/graph-client.ts +22 -1
  397. package/src/plugins/builtin/supply-chain/index.tsx +1 -2
  398. package/src/plugins/builtin/thirteenf/api.ts +127 -29
  399. package/src/plugins/builtin/thirteenf/data.ts +165 -35
  400. package/src/plugins/builtin/thirteenf/index.tsx +2 -4
  401. package/src/plugins/builtin/thirteenf/model.ts +2 -0
  402. package/src/plugins/builtin/thirteenf/overlap-pane.tsx +5 -1
  403. package/src/plugins/builtin/thirteenf/pane.tsx +41 -16
  404. package/src/plugins/builtin/thirteenf/types.ts +14 -0
  405. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +22 -6
  406. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +81 -11
  407. package/src/plugins/builtin/ticker-detail/financials/model.ts +35 -2
  408. package/src/plugins/builtin/ticker-detail/financials/ratios.ts +53 -18
  409. package/src/plugins/builtin/ticker-detail/headless.ts +70 -11
  410. package/src/plugins/builtin/ticker-detail/index.tsx +5 -10
  411. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +16 -6
  412. package/src/plugins/builtin/time-sales/index.tsx +1 -2
  413. package/src/plugins/builtin/time-sales/pane.tsx +3 -1
  414. package/src/plugins/builtin/trace-bonds/headless.ts +11 -2
  415. package/src/plugins/builtin/trace-bonds/model.ts +44 -10
  416. package/src/plugins/builtin/trace-bonds/pane.tsx +12 -7
  417. package/src/plugins/builtin/treasury-auctions/cache.ts +20 -8
  418. package/src/plugins/builtin/treasury-auctions/client.ts +3 -3
  419. package/src/plugins/builtin/treasury-auctions/headless.ts +3 -1
  420. package/src/plugins/builtin/treasury-auctions/pane.tsx +10 -4
  421. package/src/plugins/builtin/vol-surface/client.ts +18 -3
  422. package/src/plugins/builtin/vol-surface/index.tsx +1 -2
  423. package/src/plugins/builtin/volatility/client.ts +43 -16
  424. package/src/plugins/builtin/volatility/headless.ts +5 -2
  425. package/src/plugins/builtin/volatility/index.tsx +1 -2
  426. package/src/plugins/builtin/volatility/model.ts +17 -6
  427. package/src/plugins/builtin/volatility/pane.tsx +6 -2
  428. package/src/plugins/builtin/volatility/series.ts +2 -1
  429. package/src/plugins/builtin/world-indices/footer.ts +6 -1
  430. package/src/plugins/builtin/world-venue-map/client.ts +10 -1
  431. package/src/plugins/builtin/world-venue-map/index.ts +6 -2
  432. package/src/plugins/builtin/world-venue-map/layered-pane.tsx +48 -7
  433. package/src/plugins/builtin/world-venue-map/layers.ts +17 -0
  434. package/src/plugins/builtin/world-venue-map/local-layers.ts +141 -0
  435. package/src/plugins/builtin/world-venue-map/map-symbols.ts +18 -0
  436. package/src/plugins/builtin/world-venue-map/map.tsx +4 -2
  437. package/src/plugins/builtin/world-venue-map/scope.ts +63 -0
  438. package/src/plugins/builtin/yield-curve/client.ts +4 -6
  439. package/src/plugins/builtin/yield-curve/curves.ts +16 -2
  440. package/src/plugins/builtin/yield-curve/headless.ts +3 -3
  441. package/src/plugins/bundle.ts +52 -2
  442. package/src/plugins/catalog-browser.ts +2 -2
  443. package/src/plugins/catalog-cli.ts +20 -0
  444. package/src/plugins/host-link.ts +13 -1
  445. package/src/plugins/installer.ts +64 -42
  446. package/src/plugins/loader.ts +43 -11
  447. package/src/plugins/peer-imports.ts +201 -0
  448. package/src/plugins/peer-retry.ts +26 -29
  449. package/src/plugins/plugin-process.ts +128 -0
  450. package/src/plugins/redirection-guard.ts +45 -0
  451. package/src/remote/endpoint.ts +16 -3
  452. package/src/renderers/browser/main.tsx +3 -2
  453. package/src/renderers/cloudflare/http-proxy.ts +7 -123
  454. package/src/renderers/dom/desktop/icons.tsx +5 -2
  455. package/src/renderers/dom/host/image-surface.tsx +46 -21
  456. package/src/renderers/dom/styles.css +7 -0
  457. package/src/renderers/dom/toast-host.tsx +2 -0
  458. package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
  459. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +23 -1
  460. package/src/renderers/electrobun/bun/desktop/initialization.ts +12 -3
  461. package/src/renderers/electrobun/bun/desktop/rpc-timing.ts +88 -0
  462. package/src/renderers/electrobun/bun/desktop/update.ts +20 -4
  463. package/src/renderers/electrobun/bun/desktop/view-ids.ts +8 -4
  464. package/src/renderers/electrobun/bun/external-plugins.ts +4 -0
  465. package/src/renderers/electrobun/bun/window/attention.ts +54 -0
  466. package/src/renderers/electrobun/bun/window/create.ts +6 -2
  467. package/src/renderers/electrobun/bun/window/native-timing.ts +48 -0
  468. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +82 -4
  469. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +13 -5
  470. package/src/renderers/electrobun/view/desktop/window/snapshot.ts +3 -0
  471. package/src/renderers/electrobun/view/main.tsx +7 -2
  472. package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
  473. package/src/renderers/electrobun/view/ui-host.tsx +9 -2
  474. package/src/renderers/opentui/browser-open.ts +10 -4
  475. package/src/renderers/opentui/image/surface.tsx +1 -1
  476. package/src/renderers/opentui/start.tsx +12 -6
  477. package/src/renderers/opentui/toast-host.tsx +2 -1
  478. package/src/series/registry.ts +18 -0
  479. package/src/series/resolve.ts +2 -1
  480. package/src/series/standalone.ts +10 -1
  481. package/src/series/types.ts +12 -0
  482. package/src/series/validate.ts +11 -2
  483. package/src/sources/gloomberb-cloud/fred-series.ts +5 -3
  484. package/src/sources/gloomberb-cloud/index.ts +28 -5
  485. package/src/sources/gloomberb-cloud/news.ts +24 -3
  486. package/src/sources/gloomberb-cloud/normalizers.ts +17 -2
  487. package/src/sources/provider-errors.ts +26 -3
  488. package/src/sources/provider-router/history.ts +5 -0
  489. package/src/sources/provider-router/index.ts +13 -0
  490. package/src/sync/core-contributors.ts +32 -0
  491. package/src/telemetry/attention-counts.ts +64 -6
  492. package/src/telemetry/attention-setting.ts +33 -0
  493. package/src/telemetry/crash-reports-node.ts +3 -1
  494. package/src/telemetry/stall-clocks.ts +93 -0
  495. package/src/telemetry/stall-reports-node.ts +25 -9
  496. package/src/telemetry/stall-reports.ts +218 -13
  497. package/src/test-support/outage.ts +8 -0
  498. package/src/test-support/preload.ts +9 -0
  499. package/src/tickers/company-name-listing.ts +59 -29
  500. package/src/tickers/company-name.ts +84 -1
  501. package/src/tickers/listing-fate.ts +14 -3
  502. package/src/tickers/saved-instrument.ts +17 -0
  503. package/src/tickers/search/home-listing.ts +104 -8
  504. package/src/tickers/search/index.ts +5 -2
  505. package/src/tickers/search/types.ts +6 -1
  506. package/src/time-series/bar-aggregation.ts +7 -1
  507. package/src/time-series/base-series.ts +3 -3
  508. package/src/time-series/chart-bounds.ts +1 -1
  509. package/src/time-series/chart-data.ts +5 -2
  510. package/src/time-series/chart-seed.ts +1 -1
  511. package/src/time-series/coin-session-close.ts +211 -0
  512. package/src/time-series/live-bars.ts +13 -2
  513. package/src/time-series/live-quotes.ts +1 -1
  514. package/src/time-series/market.ts +5 -3
  515. package/src/time-series/official-close.ts +159 -0
  516. package/src/time-series/price-comparison.ts +75 -8
  517. package/src/time-series/resolve.ts +32 -4
  518. package/src/time-series/series-presentation.ts +1 -1
  519. package/src/time-series/studies/formula.ts +205 -0
  520. package/src/time-series/studies/indicators.ts +299 -0
  521. package/src/time-series/studies/leverage.ts +75 -0
  522. package/src/time-series/studies/pair.ts +138 -0
  523. package/src/time-series/studies/resolve.ts +333 -0
  524. package/src/time-series/studies/shared.ts +141 -0
  525. package/src/time-series/studies/units.ts +243 -0
  526. package/src/time-series/studies/vwap.ts +274 -0
  527. package/src/time-series/types.ts +7 -0
  528. package/src/types/config.ts +18 -2
  529. package/src/types/data-provider.ts +23 -0
  530. package/src/types/financials.ts +40 -3
  531. package/src/types/headless.ts +42 -3
  532. package/src/types/plugin.ts +32 -3
  533. package/src/ui/host.tsx +17 -1
  534. package/src/ui/toast.tsx +26 -1
  535. package/src/utils/ascii-text.ts +140 -0
  536. package/src/utils/cli-output.ts +91 -18
  537. package/src/utils/concurrency.ts +24 -0
  538. package/src/utils/exchanges.ts +6 -0
  539. package/src/utils/external-url.ts +20 -7
  540. package/src/utils/financial-statements.ts +73 -16
  541. package/src/utils/fundamentals.ts +17 -5
  542. package/src/utils/futures-generic.ts +4 -4
  543. package/src/utils/http-proxy-upstream.ts +124 -0
  544. package/src/utils/lease-liabilities.ts +54 -0
  545. package/src/utils/main-thread-stall.ts +122 -5
  546. package/src/utils/perf-marks.ts +58 -12
  547. package/src/utils/price-history-integrity.ts +47 -1
  548. package/src/utils/private-files.ts +141 -0
  549. package/src/utils/public-address.ts +99 -0
  550. package/src/account/upgrade-personalization.ts +0 -57
  551. package/src/account/upgrade-tickers.ts +0 -124
  552. package/src/api-client/upgrade-experiment.ts +0 -91
  553. package/src/plugins/builtin/cloud/browser.tsx +0 -3
  554. package/src/time-series/studies.ts +0 -1628
@@ -1,3 +1,4 @@
1
+ import { asciiText, localeReadsUtf8 } from "./ascii-text";
1
2
  type CliAlign = "left" | "right" | "center";
2
3
 
3
4
  export interface CliTableColumn {
@@ -45,6 +46,8 @@ export interface CliTableOptions {
45
46
  indent?: number;
46
47
  /** Lines printed under each row, by row index, indented and muted: a footnote that belongs to the row. */
47
48
  below?: ReadonlyArray<readonly string[] | undefined>;
49
+ /** Each line under a row stays one line, cut to the table's width, rather than wrapping: a story's summary. */
50
+ cutBelow?: boolean;
48
51
  }
49
52
 
50
53
  export type CliStatEntry = readonly [label: string, value: string];
@@ -73,7 +76,12 @@ const NUMBER_JOINER = /^(?:\/|·|→|↑|↓|[-–]|to|up|down)$/;
73
76
  const PLAIN_EMPTY_CELL = "—";
74
77
  let colorEnabledOverride: boolean | null = null;
75
78
  let widthOverride: number | null = null;
79
+ /** The widths `COLUMNS` is taken at for piped output, as `--width` takes them; anything else is ignored. */
80
+ const MIN_COLUMNS_VARIABLE = 20;
81
+ const MAX_COLUMNS_VARIABLE = 1000;
76
82
  let plainOverride: boolean | null = null;
83
+ /** Whether the terminal reads only ASCII (`localeReadsUtf8`); undefined until first asked. */
84
+ let asciiLocale: boolean | undefined;
77
85
  /** Output is being rendered for a file (`renderForFile`), which has no terminal width. */
78
86
  let renderingForFile = false;
79
87
 
@@ -143,6 +151,23 @@ export function cliPlainEnabled(): boolean {
143
151
  return plainOverride === true;
144
152
  }
145
153
 
154
+ /** Tests say whether the terminal reads only ASCII; null asks the environment again. */
155
+ export function setCliAsciiLocaleOverride(value: boolean | null): void {
156
+ asciiLocale = value ?? undefined;
157
+ }
158
+
159
+ /**
160
+ * Plain output to a terminal that cannot show UTF-8 (a C locale, a Windows
161
+ * code page such as 437): it is written in ASCII (`asciiText`), so a dash, a
162
+ * euro or a cent sign does not print as `—`. Output rendered for a file
163
+ * keeps its UTF-8.
164
+ */
165
+ export function cliAsciiEnabled(): boolean {
166
+ if (plainOverride !== true || renderingForFile) return false;
167
+ asciiLocale ??= !localeReadsUtf8();
168
+ return asciiLocale;
169
+ }
170
+
146
171
  /**
147
172
  * Whether output is plain without `--plain` or `--no-plain`: NO_COLOR is set
148
173
  * to a value (an empty one is unset, as the convention says), or stdout is
@@ -185,14 +210,20 @@ export function plainWords(text: string): string {
185
210
  }
186
211
 
187
212
  /** A table header's sort mark in words: `MKT VAL ▼` is "MKT VAL (sorted descending)". */
188
- function plainHeader(header: string): string {
213
+ function plainHeader(header: string, ascii: boolean): string {
189
214
  const sorted = header.replace(/\s*([▲▼])\s*$/, (_match, mark: string) => ` (sorted ${mark === "▲" ? "ascending" : "descending"})`);
190
- return plainWords(sorted.trimStart());
215
+ const worded = plainWords(sorted.trimStart());
216
+ return ascii ? asciiText(worded) : worded;
191
217
  }
192
218
 
193
- /** A plain table's cell: arrows in words, and "—" for no value. */
194
- function plainCell(cell: string): string {
195
- return EMPTY_CELL.test(stripAnsi(cell).trim()) ? PLAIN_EMPTY_CELL : plainWords(cell);
219
+ /**
220
+ * A plain table's cell: arrows in words, and "—" for no value. In ASCII
221
+ * (`cliAsciiEnabled`) before the columns are measured, so `EUR211.25` lines
222
+ * up as `€211.25` did.
223
+ */
224
+ function plainCell(cell: string, ascii: boolean): string {
225
+ const plain = EMPTY_CELL.test(stripAnsi(cell).trim()) ? PLAIN_EMPTY_CELL : plainWords(cell);
226
+ return ascii ? asciiText(plain) : plain;
196
227
  }
197
228
 
198
229
  function applyAnsi(text: string, code: string): string {
@@ -217,11 +248,24 @@ export function setCliWidthOverride(value: number | null): void {
217
248
  /** Columns the output is fitted to: `--width`, else the terminal's, or null when output is piped or redirected. */
218
249
  export function cliTerminalWidth(): number | null {
219
250
  if (widthOverride != null) return widthOverride;
220
- if (renderingForFile || !process.stdout.isTTY) return null;
251
+ if (renderingForFile) return null;
252
+ if (!process.stdout.isTTY) return columnsVariable();
221
253
  const columns = process.stdout.columns;
222
254
  return typeof columns === "number" && columns > 0 ? columns : null;
223
255
  }
224
256
 
257
+ /**
258
+ * `COLUMNS=60 gloom ... | less`: piped output is fitted to the width the
259
+ * variable sets, as `--width` does, so a screen reader or a pager gets the
260
+ * same narrow layout a terminal does. Unset or not a width, output stays whole.
261
+ */
262
+ function columnsVariable(): number | null {
263
+ const value = process.env.COLUMNS?.trim();
264
+ if (!value || !/^\d+$/.test(value)) return null;
265
+ const columns = Number(value);
266
+ return columns >= MIN_COLUMNS_VARIABLE && columns <= MAX_COLUMNS_VARIABLE ? columns : null;
267
+ }
268
+
225
269
  function padDisplay(text: string, width: number, align: CliAlign = "left"): string {
226
270
  const padding = Math.max(0, width - visibleLength(text));
227
271
  if (padding === 0) return text;
@@ -263,7 +307,8 @@ export function truncateDisplay(text: string, width: number): string {
263
307
 
264
308
  /**
265
309
  * Word-wraps text to `width` cells. Lines that already fit are kept verbatim, so aligned
266
- * columns survive; continuation lines keep the paragraph's leading indent.
310
+ * columns survive; continuation lines keep the paragraph's leading indent, and words on one
311
+ * line keep the spaces between them (a heading's ` · `).
267
312
  */
268
313
  export function wrapText(text: string, width: number): string[] {
269
314
  const lines: string[] = [];
@@ -273,7 +318,13 @@ export function wrapText(text: string, width: number): string[] {
273
318
  continue;
274
319
  }
275
320
  const indent = /^ */.exec(paragraph)![0];
276
- const words = paragraph.slice(indent.length).split(/ +/).filter(Boolean);
321
+ // Each word with the spaces before it, which a line break drops. A separator (`·`) ends its line, never starts one.
322
+ const words: { gap: string; word: string }[] = [];
323
+ for (const [, gap, word] of paragraph.slice(indent.length).matchAll(/( *)([^ ]+)/g)) {
324
+ const last = words.at(-1);
325
+ if (last && word === "·") last.word += `${gap}${word}`;
326
+ else words.push({ gap: gap!, word: word! });
327
+ }
277
328
  if (words.length === 0) {
278
329
  lines.push("");
279
330
  continue;
@@ -281,16 +332,16 @@ export function wrapText(text: string, width: number): string[] {
281
332
  let line = indent;
282
333
  let lineWidth = indent.length;
283
334
  let lineHasWord = false;
284
- for (const word of words) {
335
+ for (const { gap, word } of words) {
285
336
  const wordWidth = visibleLength(word);
286
- if (lineHasWord && lineWidth + 1 + wordWidth > width) {
337
+ if (lineHasWord && lineWidth + gap.length + wordWidth > width) {
287
338
  lines.push(line);
288
339
  line = indent;
289
340
  lineWidth = indent.length;
290
341
  lineHasWord = false;
291
342
  }
292
- line += lineHasWord ? ` ${word}` : word;
293
- lineWidth += (lineHasWord ? 1 : 0) + wordWidth;
343
+ line += lineHasWord ? `${gap}${word}` : word;
344
+ lineWidth += (lineHasWord ? gap.length : 0) + wordWidth;
294
345
  lineHasWord = true;
295
346
  }
296
347
  lines.push(line);
@@ -512,6 +563,22 @@ function wrapCell(text: string, width: number): string[] {
512
563
  });
513
564
  }
514
565
 
566
+ /**
567
+ * The columns that say what a row is: the leading ones up to the first figure
568
+ * after a word (`#`, Coin and Name before Price; Symbol and Name before Last).
569
+ */
570
+ function rowIdentityColumns(columns: readonly CliTableColumn[]): Set<number> {
571
+ const identity = new Set<number>();
572
+ let named = false;
573
+ for (const [index, column] of columns.entries()) {
574
+ const figure = column.align === "right";
575
+ if (figure && named) break;
576
+ identity.add(index);
577
+ named ||= !figure;
578
+ }
579
+ return identity;
580
+ }
581
+
515
582
  interface TableLayout {
516
583
  kept: TableEntry[];
517
584
  stacked: TableEntry[];
@@ -532,13 +599,18 @@ function movesSooner(left: readonly number[], right: readonly number[]): boolean
532
599
  * (a name, a title) before dates and figures, then rightmost first. The
533
600
  * first column stays. The list ends at the first layout that keeps every
534
601
  * column it shows whole on one line: moving more only adds lines.
602
+ *
603
+ * Plain output (`--plain`, read aloud by a screen reader) keeps the columns
604
+ * that say what a row is on its first line, after only the ones too wide to
605
+ * sit there: a row is heard as "BTC Bitcoin" before its figures.
535
606
  */
536
- function tableLayouts(entries: TableEntry[], available: number): TableLayout[] {
607
+ function tableLayouts(entries: TableEntry[], available: number, identity: ReadonlySet<number> = new Set()): TableLayout[] {
537
608
  const fit = (kept: TableEntry[]) => fitColumnWidths(kept.map((entry) => entry.width), kept.map((entry) => entry.floor), available);
538
609
  const first = entries[0]!;
539
610
  const room = available - first.floor - COLUMN_GAP;
540
611
  const order = (entry: TableEntry) => [
541
612
  entry.floor > room ? 1 : 0,
613
+ identity.has(entry.index) ? 0 : 1,
542
614
  entry.column.stackPriority ?? 0,
543
615
  entry.text && entry.width > SOFT_MIN_COLUMN_WIDTH ? 1 : 0,
544
616
  entry.index,
@@ -601,7 +673,7 @@ function renderTableLayout(
601
673
  const belowIndent = `${indent}${" ".repeat(COLUMN_GAP)}`;
602
674
  const belowWidth = Math.max(1, (available ?? 0) - COLUMN_GAP);
603
675
  const renderBelow = (index: number) => (options.below?.[index] ?? []).flatMap((line) => (
604
- available == null ? [line] : wrapText(line, belowWidth)
676
+ available == null ? [line] : options.cutBelow ? [truncateDisplay(line, belowWidth)] : wrapText(line, belowWidth)
605
677
  )).map((line) => `${belowIndent}${cliStyles.muted(line)}`.trimEnd());
606
678
  // Columns moved under the rows print there as `Header value`, as many to a line as fit.
607
679
  const renderStacked = (cells: string[], rowIndex: number): string[] => {
@@ -645,9 +717,10 @@ function renderTableLayout(
645
717
  */
646
718
  export function renderTable(columns: CliTableColumn[], rows: string[][], options: CliTableOptions = {}): string {
647
719
  if (cliPlainEnabled()) {
648
- columns = columns.map((column) => ({ ...column, header: plainHeader(column.header) }));
649
- rows = rows.map((row) => columns.map((_, index) => plainCell(row[index] ?? "")));
650
- if (options.below) options = { ...options, below: options.below.map((lines) => lines?.map(plainWords)) };
720
+ const ascii = cliAsciiEnabled();
721
+ columns = columns.map((column) => ({ ...column, header: plainHeader(column.header, ascii) }));
722
+ rows = rows.map((row) => columns.map((_, index) => plainCell(row[index] ?? "", ascii)));
723
+ if (options.below) options = { ...options, below: options.below.map((lines) => lines?.map((line) => ascii ? asciiText(plainWords(line)) : plainWords(line))) };
651
724
  }
652
725
  const maxWidth = options.maxWidth === undefined ? cliTerminalWidth() : options.maxWidth;
653
726
  const available = maxWidth == null ? null : maxWidth - (options.indent ?? 0);
@@ -674,7 +747,7 @@ export function renderTable(columns: CliTableColumn[], rows: string[][], options
674
747
  });
675
748
  let best = "";
676
749
  let bestLines = Infinity;
677
- for (const layout of tableLayouts(entries, available)) {
750
+ for (const layout of tableLayouts(entries, available, cliPlainEnabled() ? rowIdentityColumns(columns) : undefined)) {
678
751
  const stacked = layout.stacked.filter((entry) => entry.column.stackRows !== 0).sort((left, right) => left.index - right.index);
679
752
  const text = renderTableLayout(columns, rows, options, layout.kept, stacked, available, true);
680
753
  const lines = text.split("\n").length;
@@ -23,3 +23,27 @@ export function chunked<T>(items: readonly T[], size: number): T[][] {
23
23
  for (let index = 0; index < items.length; index += size) chunks.push(items.slice(index, index + size));
24
24
  return chunks;
25
25
  }
26
+
27
+ /**
28
+ * One gate that every caller shares: at most `limit` tasks run at once, and
29
+ * the rest wait in the order they came. Unlike mapWithConcurrency, separate
30
+ * callers (two panes, a pane and a report) count against the same limit. A
31
+ * finished task hands its slot straight to the next waiting one, so a caller
32
+ * arriving in between never takes it.
33
+ */
34
+ export function createConcurrencyLimiter(limit: number): <T>(task: () => Promise<T>) => Promise<T> {
35
+ const slots = Math.max(1, Math.floor(limit));
36
+ let active = 0;
37
+ const waiting: Array<() => void> = [];
38
+ return async (task) => {
39
+ if (active < slots) active += 1;
40
+ else await new Promise<void>((resolve) => waiting.push(resolve));
41
+ try {
42
+ return await task();
43
+ } finally {
44
+ const next = waiting.shift();
45
+ if (next) next();
46
+ else active -= 1;
47
+ }
48
+ };
49
+ }
@@ -93,6 +93,12 @@ export function isKnownExchangeCode(value?: string): boolean {
93
93
  return !!normalized && KNOWN_EXCHANGE_CODES.has(normalized);
94
94
  }
95
95
 
96
+ /** "Warsaw Stock Exchange" for a venue with a name, the canonical code otherwise (NASDAQ, whose name is its code). */
97
+ export function exchangeName(value?: string): string {
98
+ const code = canonicalExchange(value);
99
+ return EXCHANGE_NAMES[code] ?? code;
100
+ }
101
+
96
102
  /** "Euronext Paris (EPA)" for a venue with a name, the canonical code otherwise. */
97
103
  export function exchangeLabel(value?: string): string {
98
104
  const code = canonicalExchange(value);
@@ -1,14 +1,13 @@
1
1
  /**
2
2
  * Scheme validation for links handed to the operating system.
3
3
  *
4
- * Opening a link natively means spawning `open` / `xdg-open` / `cmd /c start`
5
- * with a string that usually came from an external feed: RSS items, prediction
6
- * markets, news, filings, Substack. Two things make an unvalidated string
7
- * dangerous there — `file://` launches local content, and Windows routes the
8
- * argument through cmd's parser.
4
+ * Opening a link natively means spawning `open` / `xdg-open` / `rundll32` with
5
+ * a string that usually came from an external feed: RSS items, prediction
6
+ * markets, news, filings, Substack. `file://` and the other local schemes
7
+ * launch local content, so only http(s) goes through.
9
8
  *
10
- * Restricting to http(s) removes both. Returns the normalised URL so callers
11
- * spawn the parsed form rather than the raw input.
9
+ * Returns the normalised URL so callers spawn the parsed form rather than the
10
+ * raw input.
12
11
  */
13
12
  export function safeExternalUrl(value: string): string | null {
14
13
  if (!value.trim()) return null;
@@ -22,3 +21,17 @@ export function safeExternalUrl(value: string): string | null {
22
21
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
23
22
  return parsed.toString();
24
23
  }
24
+
25
+ /**
26
+ * The command that hands an http(s) link to the system's own opener, with the
27
+ * link as the last argument, or null for any other link. Windows opens it
28
+ * through `url.dll` rather than `cmd /c start`: `&`, `|`, `^` and `%` are part
29
+ * of a link, and a command line would read them as its own.
30
+ */
31
+ export function systemOpenCommand(rawUrl: string, platform: NodeJS.Platform): string[] | null {
32
+ const url = safeExternalUrl(rawUrl);
33
+ if (!url) return null;
34
+ if (platform === "darwin") return ["open", url];
35
+ if (platform === "win32") return ["rundll32.exe", "url.dll,FileProtocolHandler", url];
36
+ return ["xdg-open", url];
37
+ }
@@ -1,31 +1,88 @@
1
- import type { FinancialStatement, StatementFill, StatementFillField } from "../types/financials";
1
+ import type { FinancialStatement, StatementFill, StatementFillField, TickerFinancials } from "../types/financials";
2
+ import { settleWithin } from "./async-deadline";
3
+ import { logFailure } from "./log-failure";
2
4
  import { copyIncomeField, incomeFieldOwner, INCOME_STATEMENT_FIELDS, isIncomeStatementField } from "./income-statement";
3
5
  import { mergeStatementGaps } from "./statement-gaps";
4
6
 
5
7
  export const FINANCIAL_VINTAGE_NOTICE = "Latest available statements may include restatements. Historical as-of values are not reconstructed.";
6
8
 
9
+ /** The oldest dated period of a report's columns (a TTM column has no date), or null when none has one. */
10
+ function oldestPeriod(dates: readonly string[]): string | null {
11
+ return dates.filter((date) => /^\d{4}-\d{2}-\d{2}/.test(date)).map((date) => date.slice(0, 10)).sort()[0] ?? null;
12
+ }
13
+
7
14
  /**
8
15
  * The closing lines of a statements report that say how far back it goes, from
9
- * the oldest period it served: the report holds no older ones, and `GF` reads
10
- * one metric over more periods, as far as its own history reaches. Empty when
11
- * no period has a date, so a report with nothing in it says nothing.
16
+ * the oldest period it served: the report holds no older ones. `older` says
17
+ * whether `GF`, which reads one metric from the listing's extended history
18
+ * (`hasOlderStatements`), goes further back: it is pointed to only then, and
19
+ * when it does not, the listing has no older ones at all (IAG on LSE: its
20
+ * statements start in 2022, in GF too). Null when that could not be told in
21
+ * time: the report then says nothing of older periods. Empty too when no
22
+ * period has a date, so a report with nothing in it says nothing.
12
23
  */
13
24
  export function statementHistoryNotes(
14
25
  period: "annual" | "quarterly",
15
26
  dates: readonly string[],
16
27
  symbol: string,
28
+ older: boolean | null,
17
29
  ): string[] {
18
- const oldest = dates.filter((date) => /^\d{4}-\d{2}-\d{2}/.test(date)).map((date) => date.slice(0, 10)).sort()[0];
19
- if (!oldest) return [];
20
- return period === "annual"
21
- ? [
22
- `annual history from ${oldest.slice(0, 4)}; older years not served`,
23
- `earlier years, one metric at a time: gloom fn GF ${symbol} --period annual --periods 40`,
24
- ]
25
- : [
26
- `quarterly history from ${oldest}; older quarters not served`,
27
- `earlier quarters, one metric at a time: gloom fn GF ${symbol} --period quarterly --periods 40`,
28
- ];
30
+ const oldest = oldestPeriod(dates);
31
+ if (!oldest || older === null) return [];
32
+ const [from, unit] = period === "annual" ? [oldest.slice(0, 4), "years"] : [oldest, "quarters"];
33
+ if (older === false) return [`${period} history from ${from}; older ${unit} not available for this listing`];
34
+ return [
35
+ `${period} history from ${from}; older ${unit} not served`,
36
+ ...older ? [`earlier ${unit}, one metric at a time: gloom fn GF ${symbol} --period ${period} --periods 40`] : [],
37
+ ];
38
+ }
39
+
40
+ /**
41
+ * How long a statements report waits on the extended history for its
42
+ * older-periods note, as a lock note waits on its routes' checks: a read still
43
+ * out then leaves the report on time, without the note. The read goes on and
44
+ * warms the cache for the next report.
45
+ */
46
+ const OLDER_STATEMENTS_WAIT_MS = 3_000;
47
+
48
+ /**
49
+ * Whether the listing's extended statement history, which `GF` reads, has a
50
+ * period older than the oldest of `dates`, a report's columns: a US filer's
51
+ * filings reach years back; a listing abroad has nothing past what the report
52
+ * served. Null when the history could not be read within `waitMs`.
53
+ */
54
+ export async function hasOlderStatements(
55
+ loadExtended: () => Promise<Pick<TickerFinancials, "annualStatements" | "quarterlyStatements">>,
56
+ period: "annual" | "quarterly",
57
+ dates: readonly string[],
58
+ waitMs = OLDER_STATEMENTS_WAIT_MS,
59
+ ): Promise<boolean | null> {
60
+ const oldest = oldestPeriod(dates);
61
+ if (!oldest) return null;
62
+ const read = Promise.resolve().then(loadExtended).catch((error: unknown) => {
63
+ logFailure("utils.financial-statements", "Reading the extended statement history failed")(error);
64
+ throw error;
65
+ });
66
+ const extended = await settleWithin(read, waitMs);
67
+ if (!extended) return null;
68
+ const rows = period === "annual" ? extended.annualStatements : extended.quarterlyStatements;
69
+ return rows.some((row) => /^\d{4}-\d{2}-\d{2}/.test(row.date) && row.date.slice(0, 10) < oldest);
70
+ }
71
+
72
+ /** Venues whose companies report half-yearly and yearly, with no quarterly statements: Australia's (ASX, and Cboe Australia, which trades ASX companies) and New Zealand's. */
73
+ const HALF_YEARLY_VENUES = new Set(["ASX", "CXA", "NZX"]);
74
+ const DAY_MS = 86_400_000;
75
+
76
+ /**
77
+ * Whether a listing's company reports half-yearly rather than quarterly: it
78
+ * lists on a venue whose companies do (Qantas on ASX), or the periods of its
79
+ * "quarterly" statements are half a year apart.
80
+ */
81
+ export function reportsHalfYearly(exchange: string | null | undefined, quarterly: readonly Pick<FinancialStatement, "date">[]): boolean {
82
+ if (exchange && HALF_YEARLY_VENUES.has(exchange.toUpperCase())) return true;
83
+ const times = quarterly.map(({ date }) => Date.parse(`${date.slice(0, 10)}T00:00:00Z`)).filter(Number.isFinite).sort((a, b) => a - b);
84
+ const gaps = times.slice(1).map((time, index) => (time - times[index]!) / DAY_MS);
85
+ return gaps.length > 0 && gaps.every((days) => days > 150 && days < 215);
29
86
  }
30
87
 
31
88
  /** How old a statement's latest period may be before a report says how old it is. */
@@ -106,7 +163,7 @@ function retainedFill(merged: FinancialStatement, rows: Array<FinancialStatement
106
163
 
107
164
  export const SEC_EPS_BASIS_NOTICE = "SEC EPS uses corroborated split-adjusted share bases. Unverified bases are unavailable.";
108
165
 
109
- const STATEMENT_METADATA_KEYS = new Set(["date", "dateSource", "providerDate", "dateEvidence", "currency", "availableAt", "fieldAvailability", "epsBasis", "fieldSources", "unavailableFields", "withdrawnObservations", "operatingResult", "operatingResultAggregation", "earningsResult", "unavailableEarnings", "filledFrom"]);
166
+ const STATEMENT_METADATA_KEYS = new Set(["date", "dateSource", "accountingStandard", "providerDate", "dateEvidence", "currency", "availableAt", "fieldAvailability", "epsBasis", "fieldSources", "unavailableFields", "withdrawnObservations", "operatingResult", "operatingResultAggregation", "earningsResult", "unavailableEarnings", "filledFrom"]);
110
167
  const NEARBY_PERIOD_END_MS = 7 * 24 * 60 * 60 * 1_000;
111
168
 
112
169
  /** An explicit field map is authoritative: omitted fields have unknown availability. */
@@ -45,7 +45,10 @@ export function enterpriseValueCheck(
45
45
  // Not meaningful needs no figure of its own: a source EV below zero may come with no cap or currency.
46
46
  if (check.status === "not-meaningful") return withdrawn ? check : undefined;
47
47
  if (!check.currency) return undefined;
48
- if (check.status === "withdrawn") return withdrawn && finiteNumber(check.computed) && finiteNumber(check.source) ? check : undefined;
48
+ // A withdrawn EV comes with the figure it was checked against, or with why no balance sheet checked it.
49
+ if (check.status === "withdrawn") {
50
+ return withdrawn && ((finiteNumber(check.computed) && finiteNumber(check.source)) || !!check.reason?.trim()) ? check : undefined;
51
+ }
49
52
  if (check.status === "computed") {
50
53
  return finiteNumber(check.computed) && fundamentals?.enterpriseValue === check.computed ? check : undefined;
51
54
  }
@@ -87,8 +90,9 @@ export function enterpriseValueLabel(fundamentals: Parameters<typeof enterpriseV
87
90
  /**
88
91
  * The reconciliation line under a labelled, withdrawn or not meaningful EV: "EV 61.75B USD (provider) vs 63.75B
89
92
  * computed from market cap + total debt - cash (balance sheet 2026-06-30).", "EV N/M: cash and short-term investments
90
- * exceed market cap + total debt (balance sheet 2026-06-30)." For an EV withdrawn without a check, that the source
91
- * failed validation. Undefined when the EV matches, is unchecked or computed, or has no check. Read it from the
93
+ * exceed market cap + total debt (balance sheet 2026-06-30)." A balance sheet in another currency names the rate it
94
+ * was converted at ("balance sheet 2026-06-30, EUR at 0.8614 GBP"). A withdrawn EV no balance sheet checked gives
95
+ * Gloom Cloud's reason; one withdrawn without a check, that the source failed validation. Undefined when the EV matches, is unchecked or computed, or has no check. Read it from the
92
96
  * fundamentals the rows show (atRegularSessionPrice moves the check with the EV and the market cap), so its figures
93
97
  * are theirs.
94
98
  */
@@ -97,14 +101,22 @@ export function enterpriseValueReconciliationNote(fundamentals: Parameters<typeo
97
101
  if (notMeaningful) return `EV N/M: ${notMeaningful.charAt(0).toLowerCase()}${notMeaningful.slice(1)}.`;
98
102
  const check = enterpriseValueCheck(fundamentals);
99
103
  if (!check && enterpriseValueWithdrawn(fundamentals)) return "EV unavailable: the provider's figure failed validation.";
104
+ const reason = check?.reason?.trim().replace(/\.$/, "");
105
+ if (check?.status === "withdrawn" && !finiteNumber(check.computed) && reason) {
106
+ return `EV unavailable: ${reason.charAt(0).toLowerCase()}${reason.slice(1)}.`;
107
+ }
100
108
  if (!check || !finiteNumber(check.computed) || !finiteNumber(check.source)
101
109
  || (check.status !== "differs" && check.status !== "withdrawn")) return undefined;
102
110
  const source = `${formatCompact(check.source)} ${check.currency}`;
103
111
  const computed = formatCompact(check.computed);
104
- const basis = `market cap + total debt - cash${check.balanceSheetDate ? ` (balance sheet ${check.balanceSheetDate})` : ""}`;
112
+ const leases = finiteNumber(check.leases) && check.leases > 0 ? " + leases" : "";
113
+ const fx = check.fx && finiteNumber(check.fx.rate) ? `, ${check.fx.from} at ${check.fx.rate.toPrecision(4)} ${check.currency}` : "";
114
+ const sheet = check.balanceSheetDate ? ` (balance sheet ${check.balanceSheetDate}${fx})` : "";
115
+ const basis = `market cap + total debt${leases} - cash${sheet}`;
105
116
  if (check.status === "differs") return `EV ${source} (provider) vs ${computed} computed from ${basis}.`;
106
117
  const ratio = check.computed > 0 ? ` is ${(check.source / check.computed).toFixed(1)}x` : " is far from";
107
- return `EV unavailable: the provider's ${source}${ratio} the ${computed} computed from ${basis}.`;
118
+ const twice = check.convertedTwice && check.convertedFrom ? `; it converts ${check.convertedFrom} to ${check.currency} twice` : "";
119
+ return `EV unavailable: the provider's ${source}${ratio} the ${computed} computed from ${basis}${twice}.`;
108
120
  }
109
121
 
110
122
  /**
@@ -170,9 +170,9 @@ export function futuresGenericPriceBasis(generic: Pick<FuturesGeneric, "root">):
170
170
 
171
171
  /**
172
172
  * How a futures listing's price reads, whichever form it takes: a generic by
173
- * its root (VX1 points, TY1 32nds), and a continuous (`ES=F`, `SXO=F`) or
174
- * listed contract (`SXOZ26.CME`) of an index, sector or industry root in index
175
- * points, on the root's own venue or with none. The quote keeps its currency
173
+ * its root (VX1 points, TY1 32nds), and a continuous (`ES=F`, `ZN=F`) or
174
+ * listed contract (`ZNZ26.CBT`) in the same units as its root's generic,
175
+ * on the root's own venue or with none. The quote keeps its currency
176
176
  * for what a tick is worth. Null for every other listing, a security that
177
177
  * shares a root's code on another venue (XAU on Toronto, the XAR ETF) included.
178
178
  */
@@ -180,7 +180,7 @@ export function futuresListingPriceBasis(symbol: string, exchange?: string | nul
180
180
  const generic = futuresGenericListing(symbol, exchange);
181
181
  if (generic) return futuresGenericPriceBasis(generic);
182
182
  const listing = futuresListingVenue(symbol, exchange);
183
- return listing?.root && ROOT_VENUES[listing.root] === listing.venue && POINT_ROOTS.has(listing.root) ? "points" : null;
183
+ return listing?.root && ROOT_VENUES[listing.root] === listing.venue ? futuresGenericPriceBasis({ root: listing.root }) : null;
184
184
  }
185
185
 
186
186
  /** Whether a root's date rules count back from first notice or from last trade. */
@@ -0,0 +1,124 @@
1
+ import type { ProxiedRequestInit } from "./http-proxy-response";
2
+
3
+ /** Set by the caller's browser or meaningful only to the hop it came from. */
4
+ const STRIPPED_REQUEST_HEADERS = new Set([
5
+ "connection",
6
+ "content-length",
7
+ "host",
8
+ "keep-alive",
9
+ "proxy-authorization",
10
+ "te",
11
+ "trailer",
12
+ "transfer-encoding",
13
+ "upgrade",
14
+ ]);
15
+ /** Dropped when a redirect leaves the origin the plugin set them for. */
16
+ const CROSS_ORIGIN_STRIPPED_HEADERS = ["authorization", "cookie", "proxy-authorization"];
17
+ /** Dropped when a redirect turns the request into a bodiless GET. */
18
+ const REQUEST_BODY_HEADERS = ["content-encoding", "content-language", "content-location", "content-type"];
19
+ const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
20
+ const MAX_REDIRECTS = 5;
21
+
22
+ /** Whether a proxy may reach a URL: the URL to fetch, or why not and the status to answer with. */
23
+ export type ProxyTargetCheck = (url: string) => ProxyTargetVerdict | Promise<ProxyTargetVerdict>;
24
+ type ProxyTargetVerdict = { url: URL } | { error: string; status: number };
25
+
26
+ export type ProxyUpstreamResult = { response: Response } | { error: string; status: number };
27
+
28
+ function upstreamHeaders(raw: Record<string, string>): Headers {
29
+ const headers = new Headers();
30
+ for (const [name, value] of Object.entries(raw)) {
31
+ if (STRIPPED_REQUEST_HEADERS.has(name.toLowerCase())) continue;
32
+ try {
33
+ headers.set(name, value);
34
+ } catch {
35
+ // Intentional ignore: a header name the runtime refuses is dropped, as a browser drops an unsettable header.
36
+ }
37
+ }
38
+ return headers;
39
+ }
40
+
41
+ /**
42
+ * Fetches a proxied plugin request, following redirects one hop at a time
43
+ * instead of letting `fetch` do it, so every `Location` passes the same
44
+ * `check` as the first URL. A host that redirects elsewhere (an open
45
+ * redirect, a moved API) would otherwise carry the request anywhere.
46
+ *
47
+ * Method and header handling mirror the Fetch spec: 303, and 301/302 after a
48
+ * POST, become a bodiless GET; credentials the plugin set are dropped when the
49
+ * redirect leaves the origin they were meant for.
50
+ */
51
+ export async function fetchCheckingRedirects(
52
+ first: URL,
53
+ init: ProxiedRequestInit,
54
+ options: { fetch: typeof fetch; check: ProxyTargetCheck; signal: AbortSignal },
55
+ ): Promise<ProxyUpstreamResult> {
56
+ const headers = upstreamHeaders(init.headers);
57
+ const mode = init.redirect ?? "follow";
58
+ let url = first;
59
+ let method = init.method;
60
+ let body = init.body;
61
+
62
+ for (let followed = 0; ; followed += 1) {
63
+ const response = await options.fetch(url, { method, headers, body, redirect: "manual", signal: options.signal });
64
+ const location = REDIRECT_STATUSES.has(response.status) ? response.headers.get("location") : null;
65
+ // "manual" hands the redirect back to the plugin. If it follows it, that
66
+ // comes back through here as a new request and is checked like one.
67
+ if (location === null || mode === "manual") return { response };
68
+
69
+ await response.body?.cancel().catch(() => { /* Intentional ignore: the redirect's own body is never read, only dropped. */ });
70
+ if (mode === "error") return { error: "The upstream response was a redirect.", status: 502 };
71
+ if (followed >= MAX_REDIRECTS) return { error: "The upstream redirected too many times.", status: 502 };
72
+
73
+ let next: string;
74
+ try {
75
+ next = new URL(location, url).href;
76
+ } catch {
77
+ // Intentional ignore: a Location that does not parse is reported as an invalid redirect, never followed.
78
+ return { error: "The upstream sent an invalid redirect.", status: 502 };
79
+ }
80
+ const target = await options.check(next);
81
+ if ("error" in target) return { error: `The upstream redirect was refused. ${target.error}`, status: 502 };
82
+
83
+ if (target.url.origin !== url.origin) {
84
+ for (const name of CROSS_ORIGIN_STRIPPED_HEADERS) headers.delete(name);
85
+ }
86
+ if (
87
+ (response.status === 303 && method !== "GET" && method !== "HEAD")
88
+ || ((response.status === 301 || response.status === 302) && method === "POST")
89
+ ) {
90
+ method = "GET";
91
+ body = undefined;
92
+ for (const name of REQUEST_BODY_HEADERS) headers.delete(name);
93
+ }
94
+ url = target.url;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Reads a body, giving up once it passes `maxBytes`. Returns null when it was
100
+ * too large, after cancelling the stream so the rest is never pulled.
101
+ */
102
+ export async function readBytesWithin(body: ReadableStream<Uint8Array> | null, maxBytes: number): Promise<Uint8Array | null> {
103
+ if (!body) return new Uint8Array(0);
104
+ const reader = body.getReader();
105
+ const chunks: Uint8Array[] = [];
106
+ let size = 0;
107
+ for (;;) {
108
+ const { done, value } = await reader.read();
109
+ if (done) break;
110
+ size += value.byteLength;
111
+ if (size > maxBytes) {
112
+ await reader.cancel().catch(() => { /* Intentional ignore: the rest of a body past the cap is dropped unread. */ });
113
+ return null;
114
+ }
115
+ chunks.push(value);
116
+ }
117
+ const bytes = new Uint8Array(size);
118
+ let offset = 0;
119
+ for (const chunk of chunks) {
120
+ bytes.set(chunk, offset);
121
+ offset += chunk.byteLength;
122
+ }
123
+ return bytes;
124
+ }