@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
@@ -0,0 +1,63 @@
1
+ import { useMemo } from "react";
2
+ import { useAppSelector, usePaneInstance } from "../../../state/app/context";
3
+ import { usePaneTickerIdentity } from "../../../state/hooks/pane-ticker";
4
+ import { usePluginPaneState } from "../../../public/react";
5
+ import type { PaneInstanceConfig } from "../../../types/config";
6
+ import { parseLayerTokens, LAYERS_SETTING_KEY } from "./layers";
7
+ import { isLocalGeoLayer, type GeoScope } from "./local-layers";
8
+
9
+ /**
10
+ * Pane state a ticker source may publish beside `cursorSymbol`: the group the
11
+ * selected company belongs to, which a linked map can widen to. TheBuildout
12
+ * publishes the theme its table lists.
13
+ */
14
+ export const GEO_SCOPE_STATE_KEY = "geoScope";
15
+
16
+ type GeoScopeMode = "company" | "theme";
17
+
18
+ function publishedScope(value: unknown): GeoScope | null {
19
+ if (!value || typeof value !== "object") return null;
20
+ const { key, label } = value as Partial<GeoScope>;
21
+ return typeof key === "string" && key && typeof label === "string" ? { key, label } : null;
22
+ }
23
+
24
+ /** A map with one of the app's own layers on can be linked to a list's or a scanner's selection. */
25
+ export function mapFollowsTicker(pane: PaneInstanceConfig): boolean {
26
+ return parseLayerTokens(pane.settings?.[LAYERS_SETTING_KEY]).some(isLocalGeoLayer);
27
+ }
28
+
29
+ export interface LinkedGeoScope {
30
+ /** What the map's own layers show; null shows everything. */
31
+ scope: GeoScope | null;
32
+ /** The company the link resolves to. */
33
+ company: GeoScope | null;
34
+ /** The group its source publishes for that company. */
35
+ theme: GeoScope | null;
36
+ mode: GeoScopeMode;
37
+ setMode: (mode: GeoScopeMode) => void;
38
+ }
39
+
40
+ /**
41
+ * What a linked map narrows its own layers to: the company the link resolves
42
+ * to, or the theme its source publishes once the pane is switched to it (and
43
+ * whenever there is no company). An unlinked map has neither and shows
44
+ * everything.
45
+ */
46
+ export function useLinkedGeoScope(enabled: boolean): LinkedGeoScope {
47
+ const instance = usePaneInstance();
48
+ const linked = enabled && (instance?.binding?.kind === "follow" || instance?.binding?.kind === "fixed");
49
+ const sourceId = linked && instance?.binding?.kind === "follow" ? instance.binding.sourceInstanceId : null;
50
+ const published = useAppSelector((state) => (sourceId ? state.paneState[sourceId]?.[GEO_SCOPE_STATE_KEY] ?? null : null));
51
+ const { symbol } = usePaneTickerIdentity();
52
+ const [mode, setMode] = usePluginPaneState<GeoScopeMode>("geoScopeMode", "company");
53
+ const theme = linked ? publishedScope(published) : null;
54
+ const companySymbol = linked ? symbol : null;
55
+ const themeKey = theme?.key ?? null;
56
+ const themeLabel = theme?.label ?? "";
57
+ return useMemo(() => {
58
+ const company = companySymbol ? { key: `company:${companySymbol}`, label: companySymbol } : null;
59
+ const group = themeKey ? { key: themeKey, label: themeLabel } : null;
60
+ const scope = mode === "theme" ? group ?? company : company ?? group;
61
+ return { scope, company, theme: group, mode, setMode };
62
+ }, [companySymbol, mode, setMode, themeKey, themeLabel]);
63
+ }
@@ -1,7 +1,6 @@
1
1
  import { apiClient } from "../../../api-client";
2
2
  import type { SwapCurvePayload, SwapTenor } from "../../../api-client/swap-curve";
3
- import { isPermanentClientError } from "../../../api-client/errors";
4
- import { createPluginCache, type PluginCacheResult } from "../../../data/plugin-cache";
3
+ import { createPluginCache, savedCopyForTransientFailure, type PluginCacheResult } from "../../../data/plugin-cache";
5
4
  import { cachedCloudResource, loadCloudResource, unavailableOnServer } from "../shared/cloud-resource";
6
5
  import { validateSwapCurve } from "./swaps-model";
7
6
  import { loadCurveData, type CurveData, type CurveId } from "./curves";
@@ -52,12 +51,11 @@ export interface SavedCopyAnswer<T> {
52
51
  const curveKey = (curve: CurveId, requestedDate: string) => `${curve}:${requestedDate || "latest"}`;
53
52
  /**
54
53
  * A copy is the saved one by where it came from, whatever its failure said. A
55
- * refusal asking again will not change (access denied, an unknown curve) is
56
- * said rather than covered by the copy, as `loadCloudResource` does.
54
+ * refusal (access denied, an unknown curve) or a reply that fails validation
55
+ * is said rather than covered by the copy, as `loadCloudResource` does.
57
56
  */
58
57
  function answer<T>(result: PluginCacheResult<T>): SavedCopyAnswer<T> {
59
- const saved = result.source === "stale-fallback";
60
- if (saved && isPermanentClientError(result.error)) throw result.error;
58
+ const saved = savedCopyForTransientFailure(result).source === "stale-fallback";
61
59
  return { data: result.data, savedAt: saved ? result.fetchedAt : null };
62
60
  }
63
61
 
@@ -44,7 +44,12 @@ export function curveOption(id: CurveId): CurveOption {
44
44
  return CURVE_OPTIONS.find((option) => option.id === id) ?? CURVE_OPTIONS[0]!;
45
45
  }
46
46
 
47
- /** `GC`, `GC 2026-01-02`, `GC JGB`, `GC TIPS 2024-01-02`: a curve word and a date, either order. */
47
+ /**
48
+ * `GC`, `GC 2026-01-02`, `GC JGB`, `GC TIPS 2024-01-02`: a curve word and a
49
+ * date, either order. Words with no digit are no date: one that names no
50
+ * curve (`GC world`, `GC "South Africa"`) lists the curves and points at the
51
+ * world tab, which has the other countries' government yields.
52
+ */
48
53
  export function parseCurveArgument(value: unknown, now = new Date()): { curve: CurveId | null; date: string } {
49
54
  const words = typeof value === "string" ? value.trim().split(/\s+/).filter(Boolean) : [];
50
55
  let curve: CurveId | null = null;
@@ -54,7 +59,16 @@ export function parseCurveArgument(value: unknown, now = new Date()): { curve: C
54
59
  if (match && !curve) curve = match.id;
55
60
  else rest.push(word);
56
61
  }
57
- return { curve, date: yieldCurveDate(rest.join(" ") || null, now) };
62
+ const text = rest.join(" ");
63
+ if (text && !/\d/.test(text) && text.toLowerCase() !== "latest") throw new Error(unknownCurveMessage(text));
64
+ return { curve, date: yieldCurveDate(text || null, now) };
65
+ }
66
+
67
+ /** `Unknown curve "world"`, with the curves there are and where the rest of the world's yields are. */
68
+ export function unknownCurveMessage(word: string): string {
69
+ const names = CURVE_OPTIONS.map((option) => option.aliases[0]);
70
+ return `Unknown curve "${word}"; the curves are ${names.slice(0, -1).join(", ")} and ${names.at(-1)}. `
71
+ + "Other countries' government bond yields are on the world tab: gloom fn GC --tab world.";
58
72
  }
59
73
 
60
74
  /**
@@ -3,8 +3,8 @@ import type {
3
3
  HeadlessPaneDefinition,
4
4
  } from "../../../types/plugin";
5
5
  import {
6
- compareDate, curveCutoff, CURVE_OPTIONS, curveIdOf, curveOption, curveSpreadFigures, curveTenorMove, loadComparePoints,
7
- parseCompareInput, parseCurveArgument, type CurveData, type CurveId,
6
+ compareDate, curveCutoff, curveIdOf, curveOption, curveSpreadFigures, curveTenorMove, loadComparePoints,
7
+ parseCompareInput, parseCurveArgument, unknownCurveMessage, type CurveData, type CurveId,
8
8
  } from "./curves";
9
9
  import { yieldTenorRows, type YieldLookbackCurves } from "./chart";
10
10
  import { curveAsOf, type YieldPoint, yieldCurveErrors } from "./treasury-data";
@@ -194,7 +194,7 @@ export function createYieldCurveHeadless(
194
194
  const argument = parseCurveArgument(args.argument);
195
195
  const optionCurve = args.options.curve == null || args.options.curve === "" ? null : curveIdOf(args.options.curve);
196
196
  if (args.options.curve && !optionCurve) {
197
- throw new Error(`Unknown curve ${String(args.options.curve)}; use ${CURVE_OPTIONS.map((option) => option.aliases[0]).join(", ")}.`);
197
+ throw new Error(unknownCurveMessage(String(args.options.curve)));
198
198
  }
199
199
  const curve = optionCurve ?? argument.curve ?? "us";
200
200
  const requestedDate = args.options.date != null ? yieldCurveDate(args.options.date, new Date(), "--date") : argument.date;
@@ -1,10 +1,16 @@
1
1
  import { mkdir } from "fs/promises";
2
- import { join } from "path";
2
+ import { basename, join } from "path";
3
3
 
4
4
  import { explainPluginLoadError } from "./compat";
5
5
  import { hostPublicModules } from "./host-link";
6
6
  import { resolvePluginBrowserEntry, resolvePluginEntry } from "./loader";
7
+ import { siblingImportsBundlerPlugin } from "./peer-imports";
7
8
  import { PLUGIN_HOST_GLOBAL, SHARED_SPECIFIERS, importPluginHostModule } from "./host-modules";
9
+ import {
10
+ PLUGIN_PROCESS_IDENTIFIER,
11
+ pluginProcessDeclaration,
12
+ pluginProcessModuleSource,
13
+ } from "./plugin-process";
8
14
 
9
15
  /**
10
16
  * Compiles an external plugin for a renderer that cannot read the filesystem.
@@ -90,6 +96,7 @@ export interface BundlePluginResult {
90
96
  function createSharedModuleResolver(
91
97
  exportNamesFor: (specifier: string) => Promise<readonly string[]>,
92
98
  onShared?: (specifier: string) => void,
99
+ options: { providesProcess?: boolean } = {},
93
100
  ): import("bun").BunPlugin {
94
101
  const namespace = "gloom-host";
95
102
  const shared = new Set<string>(SHARED_SPECIFIERS);
@@ -118,6 +125,19 @@ function createSharedModuleResolver(
118
125
  contents: buildSharedModuleSource(args.path, await exportNamesFor(args.path)),
119
126
  loader: "js",
120
127
  }));
128
+
129
+ if (!options.providesProcess) return;
130
+ // `import process from "node:process"` reads the same shim as the bare
131
+ // global. Bun's own browser stand-in has no default export, so without
132
+ // this such a plugin did not compile for the desktop at all.
133
+ build.onResolve({ filter: /^(?:node:)?process$/ }, () => ({
134
+ path: "process",
135
+ namespace: "gloom-process",
136
+ }));
137
+ build.onLoad({ filter: /.*/, namespace: "gloom-process" }, () => ({
138
+ contents: pluginProcessModuleSource(),
139
+ loader: "js",
140
+ }));
121
141
  },
122
142
  };
123
143
  }
@@ -144,6 +164,11 @@ export async function bundleExternalPlugin(
144
164
  */
145
165
  minify?: boolean;
146
166
  define?: Record<string, string>;
167
+ /**
168
+ * Named in the errors the browser's `process` stand-in throws. Defaults to
169
+ * the plugin's folder name.
170
+ */
171
+ pluginName?: string;
147
172
  /**
148
173
  * `bun` compiles the native entry the terminal imports instead. Only the
149
174
  * registry check uses it, to cover the native half of a plugin that has
@@ -161,7 +186,16 @@ export async function bundleExternalPlugin(
161
186
 
162
187
  await mkdir(outDir, { recursive: true });
163
188
 
189
+ // The browser has no `process`, which the terminal gives every plugin. Each
190
+ // bundle gets its own stand-in (see plugin-process.ts); a bundle compiled
191
+ // for Bun runs where the real one exists.
192
+ const providesProcess = (options.target ?? "browser") === "browser";
193
+ const pluginName = options.pluginName ?? basename(pluginDir);
194
+
164
195
  const shared = new Set<string>();
196
+ // A sibling plugin it imports is compiled from the sibling's folder, as
197
+ // the loader imports it, not through the plugin's link (peer-imports.ts).
198
+ const siblings = siblingImportsBundlerPlugin(pluginDir);
165
199
  const result = await Bun.build({
166
200
  entrypoints: [entry],
167
201
  outdir: outDir,
@@ -174,12 +208,19 @@ export async function bundleExternalPlugin(
174
208
  // `errors`. Returning it lets the check below report what actually broke,
175
209
  // such as an import this host version does not export.
176
210
  throw: false,
177
- ...(options.define ? { define: options.define } : {}),
211
+ // The more specific `process.env.NODE_ENV` a caller defines still wins,
212
+ // so dead branches keyed on it stay dropped.
213
+ define: {
214
+ ...(providesProcess ? { process: PLUGIN_PROCESS_IDENTIFIER } : {}),
215
+ ...options.define,
216
+ },
178
217
  plugins: [
179
218
  createSharedModuleResolver(
180
219
  options.exportNamesFor ?? hostExportNames,
181
220
  (specifier) => shared.add(specifier),
221
+ { providesProcess },
182
222
  ),
223
+ ...(siblings ? [siblings] : []),
183
224
  ],
184
225
  });
185
226
 
@@ -188,6 +229,15 @@ export async function bundleExternalPlugin(
188
229
  const output = result.outputs.find((entryOutput) => entryOutput.kind === "entry-point");
189
230
  if (!output) throw new Error(`Bundling ${pluginDir} produced no entry point`);
190
231
 
232
+ // Only a bundle that reads `process` carries the stand-in, so the rest of
233
+ // the plugins the web app serves stay as small as they were.
234
+ if (providesProcess) {
235
+ const code = await Bun.file(output.path).text();
236
+ if (code.includes(PLUGIN_PROCESS_IDENTIFIER)) {
237
+ await Bun.write(output.path, pluginProcessDeclaration(pluginName) + code);
238
+ }
239
+ }
240
+
191
241
  return {
192
242
  outputPath: output.path,
193
243
  shared: [...shared].sort(),
@@ -43,7 +43,7 @@ import { ipoCalendarPlugin } from "./builtin/ipo-calendar";
43
43
  import { clinicalTrialsPlugin } from "./builtin/clinical-trials";
44
44
  import { commentLettersPlugin } from "./builtin/comment-letters";
45
45
  import { openFdaPlugin } from "./builtin/openfda";
46
- import { browserGloomberbCloudPlugin } from "./builtin/cloud/browser";
46
+ import { gloomberbCloudPlugin } from "./builtin/cloud";
47
47
  import { changelogModule } from "./builtin/changelog";
48
48
  import { roadmapModule } from "./builtin/roadmap";
49
49
  import { chartComposerModule } from "./builtin/chart-composer";
@@ -298,7 +298,7 @@ const browserEarningsPlugin = composeBuiltinPlugin({
298
298
  * browser-safe public API are absent rather than registered behind stubs.
299
299
  */
300
300
  export const browserBuiltinPlugins: readonly GloomPlugin[] = [
301
- browserGloomberbCloudPlugin,
301
+ gloomberbCloudPlugin,
302
302
  browserPortfolioPlugin,
303
303
  browserTickerCorePlugin,
304
304
  browserOptionsVolatilityPlugin,
@@ -0,0 +1,20 @@
1
+ import type { CliCommandDef } from "../types/plugin";
2
+ import { fearGreedPluginMeta, newsPluginMeta, notesPluginMeta, portfolioPluginMeta } from "./builtin/builtin-plugin-meta";
3
+ import { ALERTS_PLUGIN_ID } from "./builtin/alerts/constants";
4
+ import { alertsCliCommand } from "./builtin/alerts/cli";
5
+ import { fearGreedCliCommand } from "./builtin/fear-greed/cli";
6
+ import { rssCliCommand } from "./builtin/news/wire/rss/cli";
7
+ import { notesCliCommand } from "./builtin/notes/cli";
8
+ import { portfolioCliCommand } from "./builtin/portfolio-list/cli/portfolio-command";
9
+ import { watchlistCliCommand } from "./builtin/portfolio-list/cli/watchlist-command";
10
+
11
+ /** Command definitions shared with the plugins, without loading their pane components. */
12
+ export function getBuiltinCliCommands(): { pluginId: string; commands: CliCommandDef[] }[] {
13
+ return [
14
+ { pluginId: portfolioPluginMeta.id, commands: [portfolioCliCommand, watchlistCliCommand] },
15
+ { pluginId: newsPluginMeta.id, commands: [rssCliCommand] },
16
+ { pluginId: notesPluginMeta.id, commands: [notesCliCommand] },
17
+ { pluginId: fearGreedPluginMeta.id, commands: [fearGreedCliCommand] },
18
+ { pluginId: ALERTS_PLUGIN_ID, commands: [alertsCliCommand] },
19
+ ];
20
+ }
@@ -225,6 +225,8 @@ export interface PeerLinkState {
225
225
  target: string | null;
226
226
  /** The link resolves to the sibling's folder. */
227
227
  linked: boolean;
228
+ /** Why reading through the link failed, when it did: it can be in place and still not lead anywhere. */
229
+ resolveError?: string;
228
230
  /** Links this process wrote at that path, and when it last did. */
229
231
  writes: number;
230
232
  lastWriteAt?: number;
@@ -272,11 +274,21 @@ export function describePeerLink(pluginDir: string, peer: string, pluginsDir: st
272
274
  link = stats.isSymbolicLink() ? (process.platform === "win32" ? "junction" : "symlink") : "file";
273
275
  }
274
276
  const isLinkType = link === "junction" || link === "symlink";
277
+ const linked = directory !== null && alreadyLinked(linkPath, join(pluginsDir, directory));
278
+ let resolveError: string | undefined;
279
+ if (isLinkType && !linked) {
280
+ try {
281
+ statSync(linkPath);
282
+ } catch (error) {
283
+ resolveError = errorCode(error);
284
+ }
285
+ }
275
286
  return {
276
287
  ...base,
277
288
  link,
278
289
  target: isLinkType ? describeTarget(linkPath, pluginsDir) : null,
279
- linked: directory !== null && alreadyLinked(linkPath, join(pluginsDir, directory)),
290
+ linked,
291
+ ...(resolveError ? { resolveError } : {}),
280
292
  };
281
293
  }
282
294
 
@@ -7,6 +7,7 @@ import { findAbsorbedPlugin, type AbsorbedPlugin } from "./absorbed";
7
7
  import type { PluginPin } from "./marketplace/store";
8
8
  import { bunCommand, recordDependencyInstall } from "./dependencies";
9
9
  import { linkHostPackages, missingPeerPlugins } from "./host-link";
10
+ import { importSiblingsFromTheirFolders } from "./peer-imports";
10
11
  import {
11
12
  findAbsorbedCheckout,
12
13
  getPluginsDir,
@@ -42,7 +43,7 @@ function ensurePluginsDir() {
42
43
  }
43
44
  }
44
45
 
45
- const GITHUB_SEGMENT_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
46
+ const GITHUB_SEGMENT_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*(?![\s\S])/;
46
47
  const COMMIT_PATTERN = /^[0-9a-f]{7,40}$/i;
47
48
 
48
49
  export function validatePluginDirectoryName(name: string): string {
@@ -114,6 +115,13 @@ interface RunResult {
114
115
  stderr: string;
115
116
  }
116
117
 
118
+ /** Resolves git before a command changes into a plugin checkout. */
119
+ function gitExecutable(): string {
120
+ const executable = Bun.which("git");
121
+ if (!executable) throw Object.assign(new Error(gitMissingMessage()), { code: "ENOENT" });
122
+ return executable;
123
+ }
124
+
117
125
  /**
118
126
  * Runs a command without blocking the event loop: a fetch or `bun install`
119
127
  * takes seconds, and the terminal UI and the desktop's Bun process keep
@@ -128,7 +136,7 @@ function run(
128
136
  env?: NodeJS.ProcessEnv,
129
137
  ): Promise<RunResult> {
130
138
  return new Promise((resolveRun, reject) => {
131
- const child = spawn(command, args, { cwd, env, stdio: ["ignore", output.stdout, output.stderr] });
139
+ const child = spawn(command === "git" ? gitExecutable() : command, args, { cwd, env, stdio: ["ignore", output.stdout, output.stderr] });
132
140
  let stdout = "";
133
141
  let stderr = "";
134
142
  child.stdout?.setEncoding("utf-8").on("data", (chunk: string) => { stdout += chunk; });
@@ -141,7 +149,13 @@ function run(
141
149
  /** Whether a `git` command is on the PATH; plugins from GitHub need one to install or update. */
142
150
  export function gitInstalled(): boolean {
143
151
  if (typeof Bun !== "undefined") return Bun.which("git") !== null;
144
- return gitSucceeds(["--version"], process.cwd());
152
+ try {
153
+ execFileSync("git", ["--version"], { stdio: "ignore" });
154
+ return true;
155
+ } catch {
156
+ // Intentional ignore: a git that does not start is the answer this asks for.
157
+ return false;
158
+ }
145
159
  }
146
160
 
147
161
  /** What a plugin install says when there is no git to run: what to install, then to try again. */
@@ -171,14 +185,9 @@ async function git(args: string[], cwd: string, quiet: boolean, failure: string)
171
185
  return result.stdout.trim();
172
186
  }
173
187
 
174
- function isOnBranch(dir: string): boolean {
175
- try {
176
- execFileSync("git", ["symbolic-ref", "-q", "HEAD"], { cwd: dir, stdio: "ignore" });
177
- return true;
178
- } catch {
179
- // Intentional ignore: Detached HEAD is expected after a pinned install.
180
- return false;
181
- }
188
+ /** False on a detached HEAD, which a pinned install leaves. */
189
+ function isOnBranch(dir: string): Promise<boolean> {
190
+ return gitSucceedsAsync(["symbolic-ref", "-q", "HEAD"], dir);
182
191
  }
183
192
 
184
193
  function pinMatches(commit: string | null, expected: string | undefined): boolean {
@@ -208,27 +217,27 @@ async function checkoutCommit(targetDir: string, commit: string, quiet: boolean)
208
217
  await git(["checkout", "--detach", "--force", commit], targetDir, quiet, `Could not check out ${commit.slice(0, 7)}`);
209
218
  }
210
219
 
211
- function gitSucceeds(args: string[], cwd: string): boolean {
212
- try {
213
- execFileSync("git", args, { cwd, stdio: "ignore" });
214
- return true;
215
- } catch {
216
- // Intentional ignore: Non-ancestor and shallow histories use package versions and commit dates instead.
217
- return false;
218
- }
220
+
221
+ /*
222
+ * The update path runs in the app (the terminal, the desktop's Bun process),
223
+ * where a sync git call holds the thread for the whole process start: tens of
224
+ * milliseconds each on Windows, several per plugin. These never block it.
225
+ */
226
+ async function gitSucceedsAsync(args: string[], cwd: string): Promise<boolean> {
227
+ const result = await run("git", args, cwd, { stdout: "pipe", stderr: "pipe" })
228
+ .catch(logFailure("plugin-installer", "Could not run git", null));
229
+ return result?.code === 0;
219
230
  }
220
231
 
221
- function gitOutput(args: string[], cwd: string): string | null {
222
- try {
223
- return execFileSync("git", args, { cwd, stdio: ["ignore", "pipe", "ignore"] }).toString("utf-8").trim();
224
- } catch {
225
- // Intentional ignore: Shallow or manifest-free checkouts may lack historical metadata.
226
- return null;
227
- }
232
+ /** Null when git fails: shallow or manifest-free checkouts may lack historical metadata. */
233
+ async function gitOutputAsync(args: string[], cwd: string): Promise<string | null> {
234
+ const result = await run("git", args, cwd, { stdout: "pipe", stderr: "pipe" })
235
+ .catch(logFailure("plugin-installer", "Could not run git", null));
236
+ return result?.code === 0 ? result.stdout.trim() : null;
228
237
  }
229
238
 
230
- function versionAt(dir: string, commit: string): string | null {
231
- const text = gitOutput(["show", `${commit}:package.json`], dir);
239
+ async function versionAt(dir: string, commit: string): Promise<string | null> {
240
+ const text = await gitOutputAsync(["show", `${commit}:package.json`], dir);
232
241
  if (!text) return null;
233
242
  try {
234
243
  const version = JSON.parse(text).version;
@@ -247,15 +256,15 @@ function versionAt(dir: string, commit: string): string | null {
247
256
  * it; otherwise the versions the two commits declare decide, then, when
248
257
  * those match, their commit dates.
249
258
  */
250
- export function describeOlderCommit(dir: string, installed: string, candidate: string, label = candidate.slice(0, 7)): string | null {
251
- if (installed === candidate || gitSucceeds(["merge-base", "--is-ancestor", installed, candidate], dir)) return null;
252
- const installedVersion = versionAt(dir, installed);
253
- const byVersion = compareSemver(versionAt(dir, candidate), installedVersion);
259
+ export async function describeOlderCommit(dir: string, installed: string, candidate: string, label = candidate.slice(0, 7)): Promise<string | null> {
260
+ if (installed === candidate || await gitSucceedsAsync(["merge-base", "--is-ancestor", installed, candidate], dir)) return null;
261
+ const installedVersion = await versionAt(dir, installed);
262
+ const byVersion = compareSemver(await versionAt(dir, candidate), installedVersion);
254
263
  const older = () => `the registry's ${label} is older than the installed ${byVersion ? installedVersion : installed.slice(0, 7)}`;
255
- if (gitSucceeds(["merge-base", "--is-ancestor", candidate, installed], dir)) return older();
264
+ if (await gitSucceedsAsync(["merge-base", "--is-ancestor", candidate, installed], dir)) return older();
256
265
  if (byVersion !== null && byVersion !== 0) return byVersion < 0 ? older() : null;
257
- const candidateTime = Number(gitOutput(["show", "-s", "--format=%ct", candidate], dir));
258
- const installedTime = Number(gitOutput(["show", "-s", "--format=%ct", installed], dir));
266
+ const candidateTime = Number(await gitOutputAsync(["show", "-s", "--format=%ct", candidate], dir));
267
+ const installedTime = Number(await gitOutputAsync(["show", "-s", "--format=%ct", installed], dir));
259
268
  return candidateTime > 0 && installedTime > 0 && candidateTime < installedTime ? older() : null;
260
269
  }
261
270
 
@@ -296,6 +305,9 @@ async function readPluginExport(targetDir: string): Promise<Pick<GloomPlugin, "i
296
305
  if (missingPeerPlugins(targetDir).length > 0) return null;
297
306
  const entryFile = await resolvePluginEntry(targetDir);
298
307
  if (!entryFile) return null;
308
+ // The siblings from their folders, as the loader imports them, so the
309
+ // modules this caches are the ones a load in this process reuses.
310
+ importSiblingsFromTheirFolders(targetDir);
299
311
  // Fresh so an update is validated against the new code, not the module Bun
300
312
  // cached when the previous version loaded.
301
313
  const plugin = pluginFromModule(await import(`${entryFile}?validate=${Date.now()}`));
@@ -403,19 +415,29 @@ export interface PluginUpdateResult extends PluginDirectoryInfo {
403
415
  kept?: string;
404
416
  }
405
417
 
418
+ function githubRepoFromUrl(url: string): string | null {
419
+ const match = /github\.com[:/]([^/]+)\/([^/]+?)(?:\.git)?$/i.exec(url.trim());
420
+ return match ? `${match[1]}/${match[2]}` : null;
421
+ }
422
+
406
423
  /** `owner/repo` from the checkout's origin remote, or null for anything that is not a GitHub clone. */
407
424
  export function readPluginRemote(pluginDir: string): string | null {
408
425
  try {
409
- const url = execFileSync("git", ["config", "--get", "remote.origin.url"], { cwd: pluginDir, stdio: ["ignore", "pipe", "ignore"] })
410
- .toString("utf-8").trim();
411
- const match = /github\.com[:/]([^/]+)\/([^/]+?)(?:\.git)?$/i.exec(url);
412
- return match ? `${match[1]}/${match[2]}` : null;
426
+ return githubRepoFromUrl(execFileSync(gitExecutable(), ["config", "--get", "remote.origin.url"], { cwd: pluginDir, stdio: ["ignore", "pipe", "ignore"] })
427
+ .toString("utf-8"));
413
428
  } catch {
414
429
  // Intentional ignore: Hand-copied plugins and local repositories may have no origin remote.
415
430
  return null;
416
431
  }
417
432
  }
418
433
 
434
+ /** `readPluginRemote` without holding the thread, for the app's own update check. */
435
+ export async function readPluginRemoteAsync(pluginDir: string): Promise<string | null> {
436
+ // No origin remote (a hand-copied plugin, a local repository) is a failed git, and no repo.
437
+ const url = await gitOutputAsync(["config", "--get", "remote.origin.url"], pluginDir);
438
+ return url ? githubRepoFromUrl(url) : null;
439
+ }
440
+
419
441
  /** The sha from `git ls-remote origin HEAD`; null when the answer is not one. */
420
442
  export function parseRemoteHead(output: string): string | null {
421
443
  const sha = output.trim().split(/\s+/)[0] ?? "";
@@ -439,7 +461,7 @@ export async function readPluginRemoteHead(name: string): Promise<string | null>
439
461
  if (!existsSync(join(dir, ".git"))) return null;
440
462
  if (lstatSync(dir, { throwIfNoEntry: false })?.isSymbolicLink()) return null;
441
463
  try {
442
- const { stdout } = await execFile("git", ["ls-remote", "origin", "HEAD"], {
464
+ const { stdout } = await execFile(gitExecutable(), ["ls-remote", "origin", "HEAD"], {
443
465
  cwd: dir,
444
466
  timeout: REMOTE_HEAD_TIMEOUT_MS,
445
467
  env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_ASKPASS: "echo" },
@@ -500,7 +522,7 @@ export async function updatePlugin(name: string, options: PluginInstallOptions =
500
522
  const commit = await fetchPin(targetDir, pin, quiet);
501
523
  // Updates only move forward: a checkout installed from the default
502
524
  // branch can be ahead of the tag the registry reviewed.
503
- const kept = before ? describeOlderCommit(targetDir, before, commit, pin.ref ?? commit.slice(0, 7)) : null;
525
+ const kept = before ? await describeOlderCommit(targetDir, before, commit, pin.ref ?? commit.slice(0, 7)) : null;
504
526
  if (kept) {
505
527
  if (!quiet) console.log(cliStyles.muted(`Kept ${name}: ${kept}.`));
506
528
  return { ...describe(targetDir, name, null), before, changed: false, kept };
@@ -510,7 +532,7 @@ export async function updatePlugin(name: string, options: PluginInstallOptions =
510
532
  // Not in the registry, so there is nothing reviewed to land on: follow the
511
533
  // remote's default branch, whether the checkout is on a branch or detached
512
534
  // from an earlier pin.
513
- if (isOnBranch(targetDir)) {
535
+ if (await isOnBranch(targetDir)) {
514
536
  await git(["pull", "--ff-only"], targetDir, quiet, `Failed to update ${name}`);
515
537
  } else {
516
538
  await git(["fetch", "--depth", "1", "origin", "HEAD"], targetDir, quiet, `Failed to fetch ${name}`);
@@ -15,8 +15,10 @@ import {
15
15
  missingPluginDependencies,
16
16
  settleDependencyInstalls,
17
17
  } from "./dependencies";
18
- import { declaredPeerPlugins, describePeerLink, isLinkedPeerModule, linkHostPackages, relinkPeerPlugin } from "./host-link";
18
+ import { declaredPeerPlugins, describePeerLink, installedPeerPlugins, isLinkedPeerModule, linkHostPackages, relinkPeerPlugin } from "./host-link";
19
+ import { importSiblingsFromTheirFolders } from "./peer-imports";
19
20
  import { refreshResolverFolders, rekeyedEntry, withoutRetryQuery } from "./peer-retry";
21
+ import { redirectionGuard } from "./redirection-guard";
20
22
  import { pluginDirectoryNames } from "./plugin-names";
21
23
  import { pluginFromModule, pluginSupportsTarget } from "./plugin-export";
22
24
 
@@ -258,7 +260,7 @@ function hasStaleModules(pluginDir: string, commit: string | null): boolean {
258
260
  */
259
261
  const retriedEntries = new Map<string, string>();
260
262
 
261
- /** Plugin folders whose recovery this process already reported; once is enough to explain it. */
263
+ /** Plugin folders whose recovery, or link that did not resolve, this process already reported; once is enough to explain it. */
262
264
  const reportedRecoveries = new Set<string>();
263
265
 
264
266
  /** Loads running now, which a crash report counts: two at once share the links they rewrite. */
@@ -299,30 +301,55 @@ function failedPeer(pluginDir: string, specifier: string | undefined): string |
299
301
  /**
300
302
  * The state of the link to `peer` and around it, for the crash report of an
301
303
  * import that failed on it: whether the sibling's folder is there, what the
302
- * link is and where it points, whether the retry rewrote it, how often this
303
- * process wrote it before, which `bun install` runs this process made, how
304
- * many loads ran at once, and whether this is the first launch of the version.
304
+ * link is and where it points, why reading through it failed, whether the
305
+ * retry rewrote it, how often this process wrote it before, which `bun
306
+ * install` runs this process made, how many loads ran at once, whether this
307
+ * is the first launch of the version, and on Windows whether this process
308
+ * may follow a junction (redirection-guard.ts). `how` leads: the retry's
309
+ * outcome, or how a plugin that loaded imported the sibling.
305
310
  */
306
- function describePeerFailure(pluginDir: string, peer: string, relinked: string, retry: string): string {
311
+ function describePeerLinkState(pluginDir: string, peer: string, how: string, relinked?: string): string {
307
312
  const link = describePeerLink(pluginDir, peer);
308
313
  const now = Date.now();
309
314
  const installs = describeDependencyInstalls(now);
315
+ const guard = redirectionGuard();
310
316
  const fields = [
311
- `retry=${retry}`,
317
+ how,
312
318
  `peer=${peer}`,
313
319
  `dir=${link.directory ?? "missing"}`,
314
320
  `link=${link.link}${link.target ? `->${link.target}` : ""}${link.linked ? "" : "(not-resolving)"}`,
315
- `relinked=${relinked}`,
321
+ ...(link.resolveError ? [`link-stat=${link.resolveError}`] : []),
322
+ ...(relinked ? [`relinked=${relinked}`] : []),
316
323
  `link-writes=${link.writes}${link.lastWriteAt ? `@${((now - link.lastWriteAt) / 1000).toFixed(1)}s` : ""}`,
317
324
  ...(link.lastError ? [`link-error=${link.lastError}`] : []),
318
325
  `installs=${installs.length > 0 ? installs.slice(0, 4).join(",") : "none"}`,
319
326
  `loads=${loadsInFlight}`,
320
327
  `first-launch=${firstLaunchOfVersion === null ? "unknown" : firstLaunchOfVersion ? "yes" : "no"}`,
321
328
  ...(typeof Bun !== "undefined" ? [`bun=${Bun.version}`] : []),
329
+ ...(guard ? [`redirection-guard=${guard}`] : []),
322
330
  ];
323
331
  return fields.join(" ");
324
332
  }
325
333
 
334
+ /**
335
+ * Reports, once per plugin and process, a plugin that loaded while its link
336
+ * to a sibling it imports does not resolve. The import itself went to the
337
+ * sibling's folder (peer-imports.ts), so nothing failed; the report says the
338
+ * link would have failed it, and why, which is how the first launch after an
339
+ * update looks when the old failure is gone.
340
+ */
341
+ function reportUnresolvedSiblingLinks(pluginDir: string, directory: string): void {
342
+ if (reportedRecoveries.has(pluginDir)) return;
343
+ for (const { peer } of installedPeerPlugins(pluginDir)) {
344
+ if (describePeerLink(pluginDir, peer).linked) continue;
345
+ reportedRecoveries.add(pluginDir);
346
+ const detail = describePeerLinkState(pluginDir, peer, "peer-import=folder");
347
+ loaderLog.info(`Loaded ${directory} with ${peer} read from its folder; the link to it does not resolve [${detail}]`);
348
+ reportCrash(new Error(`Loaded ${directory} past a link to ${peer} that does not resolve`), { kind: "plugin", plugin: directory, detail });
349
+ return;
350
+ }
351
+ }
352
+
326
353
  type PeerRetry =
327
354
  | { module: unknown; detail: string }
328
355
  | { error: unknown; detail: string };
@@ -353,14 +380,14 @@ async function retryPeerImport(pluginDir: string, entryFile: string, peer: strin
353
380
  refreshResolverFolders(pluginDir, [peer]);
354
381
  const specifier = rekeyedEntry(entryFile, pluginDir);
355
382
  if (!specifier) {
356
- return { error: new Error("This process cannot import a plugin again."), detail: describePeerFailure(pluginDir, peer, relinked, "unavailable") };
383
+ return { error: new Error("This process cannot import a plugin again."), detail: describePeerLinkState(pluginDir, peer, "retry=unavailable", relinked) };
357
384
  }
358
385
  try {
359
386
  const module = await import(specifier);
360
387
  retriedEntries.set(pluginDir, specifier);
361
- return { module, detail: describePeerFailure(pluginDir, peer, relinked, "loaded") };
388
+ return { module, detail: describePeerLinkState(pluginDir, peer, "retry=loaded", relinked) };
362
389
  } catch (error) {
363
- return { error, detail: describePeerFailure(pluginDir, peer, relinked, "failed") };
390
+ return { error, detail: describePeerLinkState(pluginDir, peer, "retry=failed", relinked) };
364
391
  }
365
392
  }
366
393
 
@@ -475,6 +502,10 @@ async function loadPluginDirectory(
475
502
  };
476
503
  };
477
504
 
505
+ // A sibling it imports is read from the sibling's folder, not through the
506
+ // link just made: a process can be unable to follow that link at all.
507
+ importSiblingsFromTheirFolders(pluginDir);
508
+
478
509
  let module: unknown;
479
510
  try {
480
511
  const retried = options.fresh ? undefined : retriedEntries.get(pluginDir);
@@ -496,6 +527,7 @@ async function loadPluginDirectory(
496
527
  }
497
528
  module = retry.module;
498
529
  }
530
+ reportUnresolvedSiblingLinks(pluginDir, directory);
499
531
 
500
532
  try {
501
533
  const plugin = pluginFromModule(module);