@gloom-sh/terminal 0.19.2 → 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 (472) hide show
  1. package/README.md +7 -1
  2. package/package.json +1 -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 +20 -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.tsx +23 -1
  20. package/src/architecture/core-builtin-allowlist.json +2 -1
  21. package/src/capabilities/factories.ts +1 -0
  22. package/src/cli/ascii-output.ts +42 -0
  23. package/src/cli/base-converter.ts +10 -2
  24. package/src/cli/command-options.ts +16 -1
  25. package/src/cli/commands/crypto-hints.ts +23 -4
  26. package/src/cli/commands/market.ts +92 -23
  27. package/src/cli/commands/news-search.ts +24 -5
  28. package/src/cli/commands/overview.ts +9 -4
  29. package/src/cli/commands/search.ts +71 -12
  30. package/src/cli/commands/system.ts +22 -30
  31. package/src/cli/commands/ticker.ts +49 -9
  32. package/src/cli/commands/version.ts +33 -0
  33. package/src/cli/context.ts +5 -9
  34. package/src/cli/data-dir.ts +7 -0
  35. package/src/cli/desktop-pane-shot-api-policy.ts +85 -0
  36. package/src/cli/desktop-pane-shot-browser.ts +320 -0
  37. package/src/cli/desktop-pane-shot-routes.ts +26 -1
  38. package/src/cli/desktop-pane-shot.ts +196 -256
  39. package/src/cli/early-command.ts +42 -0
  40. package/src/cli/earnings-calendar.ts +9 -0
  41. package/src/cli/entry.ts +19 -13
  42. package/src/cli/errors.ts +12 -2
  43. package/src/cli/help.ts +1 -1
  44. package/src/cli/history-export.ts +22 -2
  45. package/src/cli/history-rows.ts +154 -4
  46. package/src/cli/index.ts +18 -1
  47. package/src/cli/known-gaps.ts +40 -5
  48. package/src/cli/listing-arg.ts +46 -9
  49. package/src/cli/options.ts +1 -1
  50. package/src/cli/output-file.ts +60 -13
  51. package/src/cli/pane-functions/catalog.ts +14 -3
  52. package/src/cli/pane-functions/data.ts +1 -1
  53. package/src/cli/pane-functions/dom.ts +45 -0
  54. package/src/cli/pane-functions/freshness.ts +46 -9
  55. package/src/cli/pane-functions/function-help.ts +17 -9
  56. package/src/cli/pane-functions/headless.ts +22 -14
  57. package/src/cli/pane-functions/index.ts +6 -2
  58. package/src/cli/pane-functions/options.ts +6 -2
  59. package/src/cli/pane-functions/resolver.ts +27 -5
  60. package/src/cli/pane-functions/screenshot-assessment.ts +33 -0
  61. package/src/cli/pane-functions/screenshot-bridge.ts +94 -13
  62. package/src/cli/pane-functions/screenshot-http.ts +70 -0
  63. package/src/cli/pane-functions/screenshot-render.ts +19 -5
  64. package/src/cli/quote-answer.ts +20 -0
  65. package/src/cli/registry.ts +41 -13
  66. package/src/cli/result.ts +12 -6
  67. package/src/cli/ticker-resolution.ts +2 -1
  68. package/src/cli/types.ts +2 -0
  69. package/src/components/chart/composite/format.ts +3 -0
  70. package/src/components/chart-table/header.tsx +1 -1
  71. package/src/components/chart-table/layout.ts +28 -11
  72. package/src/components/command-bar/action-runtime.ts +4 -0
  73. package/src/components/command-bar/commands/direct/index.ts +18 -33
  74. package/src/components/command-bar/commands/direct/runtime.ts +5 -0
  75. package/src/components/command-bar/commands/registry.ts +10 -1
  76. package/src/components/command-bar/routes/root/command-items.ts +9 -4
  77. package/src/components/command-bar/routes/root/runtime.ts +24 -1
  78. package/src/components/command-bar/surface/index.tsx +9 -3
  79. package/src/components/company-logo.tsx +2 -1
  80. package/src/components/layout/header.tsx +8 -3
  81. package/src/components/layout/market-summary.ts +25 -7
  82. package/src/components/layout/pane/footer/index.tsx +7 -2
  83. package/src/components/layout/pane/footer/model.ts +40 -5
  84. package/src/components/market-board/index.tsx +3 -2
  85. package/src/components/onboarding/desks-step.tsx +12 -4
  86. package/src/components/onboarding/first-run-workspace.ts +81 -4
  87. package/src/components/onboarding/onboarding-frame.tsx +2 -1
  88. package/src/components/onboarding/onboarding-keyboard.ts +8 -5
  89. package/src/components/onboarding/onboarding-wizard.tsx +54 -23
  90. package/src/components/onboarding/portfolio-step/positions-panel.tsx +7 -2
  91. package/src/components/onboarding/run-setup-again.ts +24 -0
  92. package/src/components/onboarding/use-onboarding-progress.ts +33 -15
  93. package/src/components/onboarding/use-onboarding-upgrade.ts +2 -6
  94. package/src/components/onboarding/wizard-model.ts +57 -3
  95. package/src/components/onboarding/wizard-positions.ts +1 -1
  96. package/src/components/ui/delayed-mark.tsx +56 -0
  97. package/src/components/ui/external-link.tsx +3 -6
  98. package/src/components/ui/icon.tsx +8 -1
  99. package/src/components/ui/logo-mark.tsx +8 -5
  100. package/src/components/ui/stat-grid.tsx +263 -19
  101. package/src/data/config/store/node.ts +11 -5
  102. package/src/data/config/store/normalize.ts +16 -3
  103. package/src/data/profile-permissions.ts +47 -0
  104. package/src/data/resource-store.ts +34 -31
  105. package/src/data/sqlite/access.ts +19 -8
  106. package/src/data/sqlite/checkpointer.ts +95 -8
  107. package/src/data/sqlite/database.ts +40 -7
  108. package/src/docs/snapshot.json +191 -78
  109. package/src/i18n/es.ts +18 -2
  110. package/src/i18n/ja.ts +18 -2
  111. package/src/i18n/ko.ts +18 -2
  112. package/src/i18n/zh-cn.ts +18 -2
  113. package/src/i18n/zh-tw.ts +18 -2
  114. package/src/layout/desk-keys.ts +4 -1
  115. package/src/layout/desks.ts +38 -5
  116. package/src/layout-marketplace/gallery-desktop.tsx +45 -16
  117. package/src/market-data/batched-reads.ts +16 -0
  118. package/src/market-data/coordinator/auxiliary.ts +4 -2
  119. package/src/market-data/coordinator/index.ts +11 -2
  120. package/src/market-data/futures-board.ts +1 -1
  121. package/src/market-data/futures-contract.ts +3 -1
  122. package/src/market-data/market/index-level.ts +6 -4
  123. package/src/market-data/published-apac-sessions.ts +6 -0
  124. package/src/news/aggregator.ts +7 -4
  125. package/src/news/news-model.ts +11 -0
  126. package/src/news/story-id.ts +22 -0
  127. package/src/news/ticker-symbols.ts +17 -1
  128. package/src/notifications/app-notifier.ts +35 -14
  129. package/src/plugins/builtin/account-management/form-components.tsx +1 -1
  130. package/src/plugins/builtin/account-management/index.tsx +2 -3
  131. package/src/plugins/builtin/account-management/model.ts +1 -1
  132. package/src/plugins/builtin/alerts/cli-forms.ts +15 -1
  133. package/src/plugins/builtin/alerts/events-pane.tsx +7 -5
  134. package/src/plugins/builtin/alerts/events.ts +10 -2
  135. package/src/plugins/builtin/alerts/index.tsx +5 -5
  136. package/src/plugins/builtin/alerts/research-builder.ts +5 -1
  137. package/src/plugins/builtin/alerts/research-rules.ts +18 -0
  138. package/src/plugins/builtin/alerts/targets.ts +4 -1
  139. package/src/plugins/builtin/attention/headless.ts +1 -1
  140. package/src/plugins/builtin/attention/index.tsx +2 -4
  141. package/src/plugins/builtin/attention/listing.ts +1 -1
  142. package/src/plugins/builtin/attention/series.ts +1 -1
  143. package/src/plugins/builtin/awards/index.tsx +1 -2
  144. package/src/plugins/builtin/backtest/client.ts +1 -0
  145. package/src/plugins/builtin/backtest/index.tsx +1 -2
  146. package/src/plugins/builtin/backtest/rules.ts +3 -1
  147. package/src/plugins/builtin/backtest/window.ts +8 -7
  148. package/src/plugins/builtin/broker-manager/detail.tsx +16 -3
  149. package/src/plugins/builtin/broker-manager/index.tsx +1 -1
  150. package/src/plugins/builtin/buildout/cells.tsx +5 -0
  151. package/src/plugins/builtin/buildout/company/flow.tsx +367 -0
  152. package/src/plugins/builtin/buildout/company/id.ts +2 -0
  153. package/src/plugins/builtin/buildout/company/model.ts +247 -0
  154. package/src/plugins/builtin/buildout/company/pane.tsx +317 -0
  155. package/src/plugins/builtin/buildout/data-runtime.ts +31 -10
  156. package/src/plugins/builtin/buildout/facilities.ts +309 -0
  157. package/src/plugins/builtin/buildout/format.ts +9 -0
  158. package/src/plugins/builtin/buildout/index.tsx +51 -5
  159. package/src/plugins/builtin/buildout/model/index.ts +62 -10
  160. package/src/plugins/builtin/buildout/model/normalizers.ts +5 -0
  161. package/src/plugins/builtin/buildout/model/types.ts +11 -2
  162. package/src/plugins/builtin/buildout/pane/index.tsx +91 -20
  163. package/src/plugins/builtin/buildout/pane/status.tsx +1 -1
  164. package/src/plugins/builtin/buildout/pane/themes.tsx +110 -0
  165. package/src/plugins/builtin/buildout/table-model.ts +26 -2
  166. package/src/plugins/builtin/builtin-plugin-meta.ts +16 -0
  167. package/src/plugins/builtin/cds/headless.ts +10 -2
  168. package/src/plugins/builtin/cds/model.ts +7 -3
  169. package/src/plugins/builtin/central-bank-rates/index.tsx +1 -2
  170. package/src/plugins/builtin/central-bank-rates/pane.tsx +2 -1
  171. package/src/plugins/builtin/changelog/index.tsx +3 -2
  172. package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -2
  173. package/src/plugins/builtin/chart-composer/headless.ts +88 -21
  174. package/src/plugins/builtin/chart-composer/index.tsx +2 -3
  175. package/src/plugins/builtin/chart-composer/pane.tsx +26 -2
  176. package/src/plugins/builtin/chart-composer/series-expression.ts +3 -2
  177. package/src/plugins/builtin/chart-composer/studies.ts +1 -1
  178. package/src/plugins/builtin/chat/controller/index.ts +1 -1
  179. package/src/plugins/builtin/chat/controller/messages.ts +15 -4
  180. package/src/plugins/builtin/chat/ghost-user.ts +8 -0
  181. package/src/plugins/builtin/chat/index.tsx +5 -5
  182. package/src/plugins/builtin/clinical-trials/index.tsx +1 -2
  183. package/src/plugins/builtin/cloud/company-picker.tsx +1 -1
  184. package/src/plugins/builtin/cloud/index.tsx +1 -2
  185. package/src/plugins/builtin/cloud/plugin.tsx +6 -4
  186. package/src/plugins/builtin/cloud/team/module.tsx +1 -1
  187. package/src/plugins/builtin/cloud/thesis/module.tsx +1 -1
  188. package/src/plugins/builtin/cloud/trial-offer-status-widget.tsx +24 -1
  189. package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +39 -15
  190. package/src/plugins/builtin/comment-letters/index.tsx +1 -2
  191. package/src/plugins/builtin/commodity-balances/index.tsx +3 -3
  192. package/src/plugins/builtin/commodity-balances/pane.tsx +2 -2
  193. package/src/plugins/builtin/congress-trades/aggregates.ts +13 -2
  194. package/src/plugins/builtin/congress-trades/detail.tsx +20 -4
  195. package/src/plugins/builtin/congress-trades/headless.ts +2 -0
  196. package/src/plugins/builtin/congress-trades/model.ts +92 -64
  197. package/src/plugins/builtin/congress-trades/table.tsx +14 -2
  198. package/src/plugins/builtin/connections/index.ts +2 -3
  199. package/src/plugins/builtin/cot/index.tsx +1 -2
  200. package/src/plugins/builtin/cpi/index.tsx +1 -2
  201. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  202. package/src/plugins/builtin/credit-boards/client.ts +7 -2
  203. package/src/plugins/builtin/credit-boards/headless.ts +25 -12
  204. package/src/plugins/builtin/credit-boards/model.ts +29 -0
  205. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +9 -4
  206. package/src/plugins/builtin/crypto-board/bitcoin-network.ts +143 -0
  207. package/src/plugins/builtin/crypto-board/headless.ts +4 -3
  208. package/src/plugins/builtin/crypto-board/index.tsx +4 -2
  209. package/src/plugins/builtin/crypto-board/series.ts +2 -0
  210. package/src/plugins/builtin/crypto-etf-flows/client.ts +9 -2
  211. package/src/plugins/builtin/crypto-etf-flows/headless.ts +61 -6
  212. package/src/plugins/builtin/crypto-etf-flows/index.tsx +1 -2
  213. package/src/plugins/builtin/crypto-etf-flows/model.ts +14 -1
  214. package/src/plugins/builtin/crypto-etf-flows/pane.tsx +8 -5
  215. package/src/plugins/builtin/debt-maturities/headless.ts +4 -3
  216. package/src/plugins/builtin/debt-maturities/model.ts +17 -1
  217. package/src/plugins/builtin/debt-maturities/pane.tsx +18 -4
  218. package/src/plugins/builtin/debug/index.tsx +1 -1
  219. package/src/plugins/builtin/dividend-yield/client.ts +32 -2
  220. package/src/plugins/builtin/dividend-yield/headless.ts +58 -33
  221. package/src/plugins/builtin/dividend-yield/index.tsx +37 -2
  222. package/src/plugins/builtin/dividend-yield/pane.tsx +32 -13
  223. package/src/plugins/builtin/dividend-yield/ranking-reads.ts +73 -0
  224. package/src/plugins/builtin/dividend-yield/ranking-view.tsx +163 -0
  225. package/src/plugins/builtin/dividend-yield/ranking.ts +1 -1
  226. package/src/plugins/builtin/dividend-yield/view.ts +21 -0
  227. package/src/plugins/builtin/docs/index.tsx +2 -3
  228. package/src/plugins/builtin/doe/headless.ts +16 -3
  229. package/src/plugins/builtin/doe/index.tsx +5 -4
  230. package/src/plugins/builtin/earnings/board.tsx +2 -1
  231. package/src/plugins/builtin/earnings/history.tsx +2 -1
  232. package/src/plugins/builtin/earnings-ripple/client.ts +32 -7
  233. package/src/plugins/builtin/earnings-ripple/index.tsx +1 -2
  234. package/src/plugins/builtin/econ/calendar-model.ts +29 -6
  235. package/src/plugins/builtin/econ-statistics/index.tsx +1 -2
  236. package/src/plugins/builtin/equity-screener/client.ts +1 -1
  237. package/src/plugins/builtin/equity-screener/headless.ts +1 -0
  238. package/src/plugins/builtin/equity-screener/ideas.ts +30 -1
  239. package/src/plugins/builtin/equity-screener/index.tsx +1 -2
  240. package/src/plugins/builtin/equity-screener/model.ts +28 -4
  241. package/src/plugins/builtin/equity-screener/pane.tsx +5 -0
  242. package/src/plugins/builtin/equity-screener/screen-check.ts +23 -1
  243. package/src/plugins/builtin/estimate-revisions/index.tsx +1 -2
  244. package/src/plugins/builtin/exposure/holdings.ts +35 -13
  245. package/src/plugins/builtin/exposure/index.tsx +1 -2
  246. package/src/plugins/builtin/fear-greed/cli.ts +44 -0
  247. package/src/plugins/builtin/fear-greed/index.tsx +5 -53
  248. package/src/plugins/builtin/fear-greed/pane.tsx +2 -1
  249. package/src/plugins/builtin/fixed-income/index.tsx +1 -2
  250. package/src/plugins/builtin/futures/board-reads.ts +25 -0
  251. package/src/plugins/builtin/futures/coalesced-bump.ts +44 -0
  252. package/src/plugins/builtin/futures/index.tsx +15 -0
  253. package/src/plugins/builtin/futures/model.ts +14 -0
  254. package/src/plugins/builtin/futures/use-board-terms.ts +22 -15
  255. package/src/plugins/builtin/futures/use-front-returns.ts +20 -14
  256. package/src/plugins/builtin/futures-curve/headless.ts +6 -1
  257. package/src/plugins/builtin/futures-curve/index.tsx +1 -2
  258. package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
  259. package/src/plugins/builtin/gpu/board.tsx +51 -12
  260. package/src/plugins/builtin/gpu/client.ts +15 -2
  261. package/src/plugins/builtin/gpu/equities.tsx +5 -2
  262. package/src/plugins/builtin/gpu/headless.ts +14 -2
  263. package/src/plugins/builtin/gpu/index.tsx +1 -2
  264. package/src/plugins/builtin/gpu/model.ts +20 -2
  265. package/src/plugins/builtin/gpu/pane.tsx +1 -1
  266. package/src/plugins/builtin/help/index.tsx +1 -1
  267. package/src/plugins/builtin/holders/index-holders.ts +24 -12
  268. package/src/plugins/builtin/holders/index.tsx +2 -3
  269. package/src/plugins/builtin/ipo-calendar/index.tsx +1 -2
  270. package/src/plugins/builtin/iv-history/index.tsx +1 -2
  271. package/src/plugins/builtin/jodi/index.tsx +1 -2
  272. package/src/plugins/builtin/kelly-sizer/index.tsx +1 -2
  273. package/src/plugins/builtin/layout-manager/index.tsx +9 -8
  274. package/src/plugins/builtin/macro-day/pane.tsx +4 -2
  275. package/src/plugins/builtin/market-halts/index.tsx +1 -2
  276. package/src/plugins/builtin/market-heatmap/index.tsx +11 -2
  277. package/src/plugins/builtin/market-movers/index.tsx +7 -1
  278. package/src/plugins/builtin/market-movers/session-body.tsx +4 -1
  279. package/src/plugins/builtin/market-valuation/index.tsx +1 -2
  280. package/src/plugins/builtin/members/index.tsx +1 -2
  281. package/src/plugins/builtin/money-markets/index.tsx +1 -2
  282. package/src/plugins/builtin/money-markets/pane.tsx +2 -1
  283. package/src/plugins/builtin/news/headless.ts +7 -0
  284. package/src/plugins/builtin/news/wire/index.tsx +1 -1
  285. package/src/plugins/builtin/news/wire/news/pop-out.tsx +9 -3
  286. package/src/plugins/builtin/notes/index.tsx +3 -6
  287. package/src/plugins/builtin/openfda/index.tsx +1 -2
  288. package/src/plugins/builtin/options/view.tsx +6 -5
  289. package/src/plugins/builtin/options-calculator/index.tsx +1 -2
  290. package/src/plugins/builtin/options-positioning/index.tsx +1 -2
  291. package/src/plugins/builtin/options-scenario/index.tsx +2 -4
  292. package/src/plugins/builtin/perps/index.tsx +1 -2
  293. package/src/plugins/builtin/plugin-marketplace/index.tsx +2 -3
  294. package/src/plugins/builtin/plugin-marketplace/pane.tsx +2 -1
  295. package/src/plugins/builtin/plugin-module.ts +16 -2
  296. package/src/plugins/builtin/portfolio-list/cli/render.ts +53 -3
  297. package/src/plugins/builtin/portfolio-list/index.tsx +1 -2
  298. package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -0
  299. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -2
  300. package/src/plugins/builtin/power/index.tsx +1 -2
  301. package/src/plugins/builtin/rate-path/index.tsx +1 -2
  302. package/src/plugins/builtin/realized-vol/headless.ts +2 -0
  303. package/src/plugins/builtin/realized-vol/index.tsx +1 -2
  304. package/src/plugins/builtin/realized-vol/model.ts +16 -2
  305. package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
  306. package/src/plugins/builtin/relative-rotation/index.tsx +1 -2
  307. package/src/plugins/builtin/research/analyst-headless.ts +33 -6
  308. package/src/plugins/builtin/research/analyst-model.ts +37 -2
  309. package/src/plugins/builtin/research/analyst-pane.tsx +25 -5
  310. package/src/plugins/builtin/research/index.tsx +5 -8
  311. package/src/plugins/builtin/research-search/index.tsx +1 -2
  312. package/src/plugins/builtin/revenue-breakdown/headless.ts +22 -8
  313. package/src/plugins/builtin/revenue-breakdown/model.ts +23 -0
  314. package/src/plugins/builtin/reverse-dcf/pane.tsx +4 -2
  315. package/src/plugins/builtin/roadmap/index.tsx +2 -3
  316. package/src/plugins/builtin/roadmap/pane.tsx +49 -2
  317. package/src/plugins/builtin/scanner/hilo-model.ts +18 -1
  318. package/src/plugins/builtin/scanner/hilo-pane.tsx +12 -2
  319. package/src/plugins/builtin/scanner/live-pane.tsx +7 -4
  320. package/src/plugins/builtin/sec/filing-content.ts +38 -6
  321. package/src/plugins/builtin/sec/headless.ts +5 -1
  322. package/src/plugins/builtin/sec/index.tsx +4 -2
  323. package/src/plugins/builtin/sec/model.ts +2 -0
  324. package/src/plugins/builtin/sectors/index.tsx +11 -5
  325. package/src/plugins/builtin/series-inventory.ts +2 -0
  326. package/src/plugins/builtin/shared/cloud-resource.ts +60 -0
  327. package/src/plugins/builtin/shared/cloud-upgrade.ts +24 -1
  328. package/src/plugins/builtin/shared/forming-bars.ts +29 -0
  329. package/src/plugins/builtin/shared/fx-rate-freshness.ts +17 -0
  330. package/src/plugins/builtin/shared/series-detail-chart.tsx +4 -2
  331. package/src/plugins/builtin/shared/use-quote-board.ts +5 -0
  332. package/src/plugins/builtin/short-interest/publications-pane.tsx +2 -1
  333. package/src/plugins/builtin/short-volume/pane.tsx +2 -1
  334. package/src/plugins/builtin/social-mentions/index.tsx +1 -2
  335. package/src/plugins/builtin/social-mentions/pane.tsx +2 -1
  336. package/src/plugins/builtin/supply-chain/client.ts +22 -1
  337. package/src/plugins/builtin/supply-chain/graph-client.ts +22 -1
  338. package/src/plugins/builtin/supply-chain/index.tsx +1 -2
  339. package/src/plugins/builtin/thirteenf/api.ts +116 -13
  340. package/src/plugins/builtin/thirteenf/data.ts +132 -30
  341. package/src/plugins/builtin/thirteenf/index.tsx +2 -4
  342. package/src/plugins/builtin/thirteenf/model.ts +2 -0
  343. package/src/plugins/builtin/thirteenf/pane.tsx +20 -11
  344. package/src/plugins/builtin/thirteenf/types.ts +12 -0
  345. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +17 -4
  346. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +81 -11
  347. package/src/plugins/builtin/ticker-detail/headless.ts +11 -3
  348. package/src/plugins/builtin/ticker-detail/index.tsx +5 -10
  349. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +16 -6
  350. package/src/plugins/builtin/time-sales/index.tsx +1 -2
  351. package/src/plugins/builtin/time-sales/pane.tsx +3 -1
  352. package/src/plugins/builtin/trace-bonds/headless.ts +11 -2
  353. package/src/plugins/builtin/trace-bonds/model.ts +44 -10
  354. package/src/plugins/builtin/trace-bonds/pane.tsx +12 -7
  355. package/src/plugins/builtin/treasury-auctions/pane.tsx +2 -2
  356. package/src/plugins/builtin/vol-surface/client.ts +18 -3
  357. package/src/plugins/builtin/vol-surface/index.tsx +1 -2
  358. package/src/plugins/builtin/volatility/index.tsx +1 -2
  359. package/src/plugins/builtin/world-indices/footer.ts +6 -1
  360. package/src/plugins/builtin/world-venue-map/client.ts +10 -1
  361. package/src/plugins/builtin/world-venue-map/index.ts +6 -2
  362. package/src/plugins/builtin/world-venue-map/layered-pane.tsx +48 -7
  363. package/src/plugins/builtin/world-venue-map/layers.ts +17 -0
  364. package/src/plugins/builtin/world-venue-map/local-layers.ts +141 -0
  365. package/src/plugins/builtin/world-venue-map/map-symbols.ts +18 -0
  366. package/src/plugins/builtin/world-venue-map/map.tsx +4 -2
  367. package/src/plugins/builtin/world-venue-map/scope.ts +63 -0
  368. package/src/plugins/builtin/yield-curve/curves.ts +16 -2
  369. package/src/plugins/builtin/yield-curve/headless.ts +3 -3
  370. package/src/plugins/bundle.ts +52 -2
  371. package/src/plugins/catalog-browser.ts +2 -2
  372. package/src/plugins/catalog-cli.ts +20 -0
  373. package/src/plugins/host-link.ts +13 -1
  374. package/src/plugins/installer.ts +15 -4
  375. package/src/plugins/loader.ts +43 -11
  376. package/src/plugins/peer-imports.ts +201 -0
  377. package/src/plugins/peer-retry.ts +26 -29
  378. package/src/plugins/plugin-process.ts +128 -0
  379. package/src/plugins/redirection-guard.ts +45 -0
  380. package/src/remote/endpoint.ts +16 -3
  381. package/src/renderers/browser/main.tsx +3 -2
  382. package/src/renderers/cloudflare/http-proxy.ts +7 -123
  383. package/src/renderers/dom/desktop/icons.tsx +2 -0
  384. package/src/renderers/dom/host/image-surface.tsx +46 -21
  385. package/src/renderers/dom/styles.css +7 -0
  386. package/src/renderers/dom/toast-host.tsx +2 -0
  387. package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
  388. package/src/renderers/electrobun/bun/desktop/initialization.ts +3 -2
  389. package/src/renderers/electrobun/bun/desktop/rpc-timing.ts +88 -0
  390. package/src/renderers/electrobun/bun/desktop/update.ts +20 -4
  391. package/src/renderers/electrobun/bun/desktop/view-ids.ts +8 -4
  392. package/src/renderers/electrobun/bun/external-plugins.ts +4 -0
  393. package/src/renderers/electrobun/bun/window/create.ts +4 -2
  394. package/src/renderers/electrobun/bun/window/native-timing.ts +48 -0
  395. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +82 -4
  396. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +13 -5
  397. package/src/renderers/electrobun/view/desktop/window/snapshot.ts +3 -0
  398. package/src/renderers/electrobun/view/main.tsx +7 -2
  399. package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
  400. package/src/renderers/opentui/browser-open.ts +10 -4
  401. package/src/renderers/opentui/image/surface.tsx +1 -1
  402. package/src/renderers/opentui/start.tsx +12 -6
  403. package/src/renderers/opentui/toast-host.tsx +2 -1
  404. package/src/series/registry.ts +18 -0
  405. package/src/series/standalone.ts +10 -1
  406. package/src/series/types.ts +7 -0
  407. package/src/series/validate.ts +11 -2
  408. package/src/sources/gloomberb-cloud/index.ts +28 -5
  409. package/src/sources/gloomberb-cloud/news.ts +24 -3
  410. package/src/sources/gloomberb-cloud/normalizers.ts +17 -2
  411. package/src/sources/provider-errors.ts +26 -3
  412. package/src/sources/provider-router/history.ts +5 -0
  413. package/src/sources/provider-router/index.ts +13 -0
  414. package/src/sync/core-contributors.ts +32 -0
  415. package/src/telemetry/attention-counts.ts +64 -6
  416. package/src/telemetry/attention-setting.ts +33 -0
  417. package/src/telemetry/crash-reports-node.ts +3 -1
  418. package/src/telemetry/stall-clocks.ts +93 -0
  419. package/src/telemetry/stall-reports-node.ts +12 -7
  420. package/src/telemetry/stall-reports.ts +34 -5
  421. package/src/test-support/preload.ts +9 -0
  422. package/src/tickers/company-name.ts +19 -0
  423. package/src/tickers/listing-fate.ts +14 -3
  424. package/src/tickers/saved-instrument.ts +17 -0
  425. package/src/tickers/search/home-listing.ts +13 -2
  426. package/src/tickers/search/index.ts +4 -1
  427. package/src/tickers/search/types.ts +6 -1
  428. package/src/time-series/bar-aggregation.ts +7 -1
  429. package/src/time-series/base-series.ts +3 -3
  430. package/src/time-series/chart-bounds.ts +1 -1
  431. package/src/time-series/chart-data.ts +5 -2
  432. package/src/time-series/chart-seed.ts +1 -1
  433. package/src/time-series/coin-session-close.ts +211 -0
  434. package/src/time-series/live-bars.ts +13 -2
  435. package/src/time-series/live-quotes.ts +1 -1
  436. package/src/time-series/official-close.ts +159 -0
  437. package/src/time-series/price-comparison.ts +55 -6
  438. package/src/time-series/resolve.ts +12 -3
  439. package/src/time-series/series-presentation.ts +1 -1
  440. package/src/time-series/studies/formula.ts +205 -0
  441. package/src/time-series/studies/indicators.ts +299 -0
  442. package/src/time-series/studies/leverage.ts +75 -0
  443. package/src/time-series/studies/pair.ts +138 -0
  444. package/src/time-series/studies/resolve.ts +333 -0
  445. package/src/time-series/studies/shared.ts +141 -0
  446. package/src/time-series/studies/units.ts +243 -0
  447. package/src/time-series/studies/vwap.ts +274 -0
  448. package/src/types/config.ts +18 -2
  449. package/src/types/data-provider.ts +23 -0
  450. package/src/types/financials.ts +22 -0
  451. package/src/types/headless.ts +42 -3
  452. package/src/types/plugin.ts +32 -3
  453. package/src/ui/host.tsx +11 -0
  454. package/src/ui/toast.tsx +26 -1
  455. package/src/utils/ascii-text.ts +140 -0
  456. package/src/utils/cli-output.ts +91 -18
  457. package/src/utils/concurrency.ts +24 -0
  458. package/src/utils/exchanges.ts +6 -0
  459. package/src/utils/external-url.ts +20 -7
  460. package/src/utils/futures-generic.ts +4 -4
  461. package/src/utils/http-proxy-upstream.ts +124 -0
  462. package/src/utils/main-thread-stall.ts +75 -3
  463. package/src/utils/perf-marks.ts +36 -2
  464. package/src/utils/private-files.ts +141 -0
  465. package/src/utils/public-address.ts +99 -0
  466. package/src/account/upgrade-personalization.ts +0 -57
  467. package/src/account/upgrade-tickers.ts +0 -124
  468. package/src/api-client/upgrade-experiment.ts +0 -91
  469. package/src/plugins/builtin/cloud/browser.tsx +0 -3
  470. package/src/renderers/electrobun/launcher/preload.ts +0 -13
  471. package/src/renderers/electrobun/launcher/windows-emulation.ts +0 -50
  472. package/src/time-series/studies.ts +0 -1628
@@ -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; });
@@ -297,6 +305,9 @@ async function readPluginExport(targetDir: string): Promise<Pick<GloomPlugin, "i
297
305
  if (missingPeerPlugins(targetDir).length > 0) return null;
298
306
  const entryFile = await resolvePluginEntry(targetDir);
299
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);
300
311
  // Fresh so an update is validated against the new code, not the module Bun
301
312
  // cached when the previous version loaded.
302
313
  const plugin = pluginFromModule(await import(`${entryFile}?validate=${Date.now()}`));
@@ -412,7 +423,7 @@ function githubRepoFromUrl(url: string): string | null {
412
423
  /** `owner/repo` from the checkout's origin remote, or null for anything that is not a GitHub clone. */
413
424
  export function readPluginRemote(pluginDir: string): string | null {
414
425
  try {
415
- return githubRepoFromUrl(execFileSync("git", ["config", "--get", "remote.origin.url"], { cwd: pluginDir, stdio: ["ignore", "pipe", "ignore"] })
426
+ return githubRepoFromUrl(execFileSync(gitExecutable(), ["config", "--get", "remote.origin.url"], { cwd: pluginDir, stdio: ["ignore", "pipe", "ignore"] })
416
427
  .toString("utf-8"));
417
428
  } catch {
418
429
  // Intentional ignore: Hand-copied plugins and local repositories may have no origin remote.
@@ -450,7 +461,7 @@ export async function readPluginRemoteHead(name: string): Promise<string | null>
450
461
  if (!existsSync(join(dir, ".git"))) return null;
451
462
  if (lstatSync(dir, { throwIfNoEntry: false })?.isSymbolicLink()) return null;
452
463
  try {
453
- const { stdout } = await execFile("git", ["ls-remote", "origin", "HEAD"], {
464
+ const { stdout } = await execFile(gitExecutable(), ["ls-remote", "origin", "HEAD"], {
454
465
  cwd: dir,
455
466
  timeout: REMOTE_HEAD_TIMEOUT_MS,
456
467
  env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_ASKPASS: "echo" },
@@ -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);
@@ -0,0 +1,201 @@
1
+ import { readFileSync, realpathSync } from "fs";
2
+ import { dirname, extname, isAbsolute, join, relative } from "path";
3
+ import type { BunPlugin } from "bun";
4
+
5
+ import { installedPeerPlugins } from "./host-link";
6
+
7
+ /**
8
+ * A plugin's imports of a sibling plugin it declares as a peer (IBKR Gateway
9
+ * imports `gloom-ibkr/gateway-bridge`), read from the sibling's own folder
10
+ * instead of through the link in the plugin's `node_modules`.
11
+ *
12
+ * That link is a junction on Windows, and a process can be unable to follow
13
+ * a junction at all. On the first launch after an update the gateway failed
14
+ * with its link in place and pointing at Interactive Brokers: the loader wrote
15
+ * it again four times, never saw it resolve, and an import that looked again
16
+ * failed the same way, while Interactive Brokers itself loaded from its folder
17
+ * in the same process. The next launch worked.
18
+ *
19
+ * So the host resolves those imports itself and hands Bun the sibling's file
20
+ * by its absolute path: the file the link leads to, under the module key the
21
+ * sibling's own imports give it, so the plugin still shares the sibling's
22
+ * modules. The terminal and the desktop's Bun process rewrite the plugin's
23
+ * source as Bun loads it; the desktop's compile for the view resolves them in
24
+ * the bundler. The link stays, for editors, `tsc`, and older Gloom versions.
25
+ */
26
+
27
+ type SourceLoader = "ts" | "tsx" | "js" | "jsx";
28
+
29
+ /** What Bun loads a plugin's own source files with, by extension. */
30
+ export const SOURCE_LOADERS: Readonly<Record<string, SourceLoader>> = {
31
+ ".ts": "ts",
32
+ ".mts": "ts",
33
+ ".tsx": "tsx",
34
+ ".js": "js",
35
+ ".mjs": "js",
36
+ ".jsx": "jsx",
37
+ };
38
+
39
+ export function sourceLoader(file: string): SourceLoader {
40
+ return SOURCE_LOADERS[extname(file).toLowerCase()] ?? "js";
41
+ }
42
+
43
+ export function isInside(root: string, path: string): boolean {
44
+ const inside = relative(root, path);
45
+ return !!inside && !inside.startsWith("..") && !isAbsolute(inside);
46
+ }
47
+
48
+ function escapeRegExp(text: string): string {
49
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
50
+ }
51
+
52
+ /** Where a module is named: after `from`, `import` or `import(`, and in `require(`. */
53
+ const IMPORTED_AT = String.raw`(\bfrom\s*|\bimport\s*(?:\(\s*)?|\brequire\s*\(\s*)`;
54
+
55
+ /**
56
+ * `contents` with `specifier` replaced by `replacement` where it is imported.
57
+ * The same text in a string anywhere else stays.
58
+ */
59
+ export function replaceSpecifier(contents: string, specifier: string, replacement: string): string {
60
+ const imported = new RegExp(`${IMPORTED_AT}(["'\`])${escapeRegExp(specifier)}\\2`, "g");
61
+ return contents.replace(imported, (_match, at: string) => `${at}${JSON.stringify(replacement)}`);
62
+ }
63
+
64
+ function realFolder(path: string): string {
65
+ try {
66
+ return realpathSync(path);
67
+ } catch {
68
+ // Intentional ignore: a folder that cannot be resolved is matched as spelled.
69
+ return path;
70
+ }
71
+ }
72
+
73
+ /** Each registered plugin's real folder, with its siblings' folders by package name. */
74
+ const siblingsByPlugin = new Map<string, ReadonlyMap<string, string>>();
75
+ /** Real plugin folders whose source Bun loads through the hook below. */
76
+ const hookedFolders = new Set<string>();
77
+
78
+ /**
79
+ * The installed siblings `pluginDir` declares, by package name, each with its
80
+ * real folder. A sibling that maps its paths with `exports` is left out: only
81
+ * a lookup through `node_modules` reads that map.
82
+ */
83
+ function siblingFolders(pluginDir: string, pluginsDir: string = dirname(pluginDir)): Map<string, string> {
84
+ const folders = new Map<string, string>();
85
+ for (const { peer, directory } of installedPeerPlugins(pluginDir, pluginsDir)) {
86
+ const folder = join(pluginsDir, directory);
87
+ try {
88
+ const pkg = JSON.parse(readFileSync(join(folder, "package.json"), "utf8")) as { exports?: unknown };
89
+ if (pkg.exports !== undefined) continue;
90
+ } catch {
91
+ // Intentional ignore: a sibling without a readable package.json has no export map.
92
+ }
93
+ folders.set(peer, realFolder(folder));
94
+ }
95
+ return folders;
96
+ }
97
+
98
+ /** The file `specifier` names in the sibling `peer`, as Bun resolves it from inside its folder; null when there is none. */
99
+ function siblingModule(peer: string, peerDir: string, specifier: string): string | null {
100
+ const subpath = specifier.slice(peer.length + 1);
101
+ try {
102
+ return Bun.resolveSync(subpath ? `./${subpath}` : ".", peerDir);
103
+ } catch {
104
+ // Intentional ignore: left as written, the import fails the way it always did.
105
+ return null;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * `contents` of `file` with its imports of the siblings its plugin declares
111
+ * naming the siblings' files by absolute path. Anything else is left as
112
+ * written: a file outside a registered plugin, and an import the sibling
113
+ * cannot satisfy, which then fails the way it always did.
114
+ */
115
+ export function withSiblingImports(file: string, contents: string): string {
116
+ let siblings: ReadonlyMap<string, string> | undefined;
117
+ for (const [root, folders] of siblingsByPlugin) {
118
+ if (isInside(root, file)) siblings = folders;
119
+ }
120
+ if (!siblings) return contents;
121
+ let next = contents;
122
+ for (const [peer, peerDir] of siblings) {
123
+ if (!next.includes(peer)) continue;
124
+ const imported = new RegExp(`${IMPORTED_AT}(["'\`])(${escapeRegExp(peer)}(?:/[^"'\`\\s]*)?)\\2`, "g");
125
+ next = next.replace(imported, (match, at: string, _quote: string, specifier: string) => {
126
+ const target = siblingModule(peer, peerDir, specifier);
127
+ return target ? `${at}${JSON.stringify(target)}` : match;
128
+ });
129
+ }
130
+ return next;
131
+ }
132
+
133
+ function pathPattern(path: string): string {
134
+ return path.split(/[\\/]+/).map(escapeRegExp).join("[\\\\/]+");
135
+ }
136
+
137
+ /**
138
+ * The plugin's own source files, with an entry imported fresh (`?reload=`,
139
+ * or the installer's `?validate=`); not its dependencies, and not a retried
140
+ * import's files, which the retry's own hook loads the same way
141
+ * (peer-retry.ts).
142
+ */
143
+ function pluginSourceFilter(root: string): RegExp {
144
+ return new RegExp(
145
+ `^${pathPattern(root)}[\\\\/](?!(?:[^?]*[\\\\/])?node_modules[\\\\/])[^?]*\\.(?:tsx?|mts|jsx?|mjs)(?:\\?(?:reload|validate)=\\d+)?$`,
146
+ process.platform === "win32" ? "i" : "",
147
+ );
148
+ }
149
+
150
+ function hookPluginSource(root: string): boolean {
151
+ if (hookedFolders.has(root)) return true;
152
+ if (typeof Bun === "undefined" || typeof Bun.plugin !== "function") return false;
153
+ Bun.plugin({
154
+ name: "gloomberb-sibling-imports",
155
+ setup(build) {
156
+ build.onLoad({ filter: pluginSourceFilter(root) }, (args) => {
157
+ const file = args.path.replace(/\?.*$/, "");
158
+ return { contents: withSiblingImports(file, readFileSync(file, "utf8")), loader: sourceLoader(file) };
159
+ });
160
+ },
161
+ });
162
+ hookedFolders.add(root);
163
+ return true;
164
+ }
165
+
166
+ /**
167
+ * Makes this process import the siblings `pluginDir` declares from their own
168
+ * folders, from the next import of the plugin on. Returns whether it does: a
169
+ * plugin with no installed sibling, and a process without Bun's runtime
170
+ * plugins, go through the links as before.
171
+ */
172
+ export function importSiblingsFromTheirFolders(pluginDir: string, pluginsDir?: string): boolean {
173
+ const siblings = siblingFolders(pluginDir, pluginsDir);
174
+ const root = realFolder(pluginDir);
175
+ if (siblings.size === 0) {
176
+ siblingsByPlugin.delete(root);
177
+ return false;
178
+ }
179
+ siblingsByPlugin.set(root, siblings);
180
+ return hookPluginSource(root);
181
+ }
182
+
183
+ /**
184
+ * The same for a bundle of `pluginDir`: a bundler plugin that resolves the
185
+ * siblings it declares in their folders, or null when none is installed.
186
+ */
187
+ export function siblingImportsBundlerPlugin(pluginDir: string, pluginsDir?: string): BunPlugin | null {
188
+ const siblings = siblingFolders(pluginDir, pluginsDir);
189
+ if (siblings.size === 0) return null;
190
+ const filter = new RegExp(`^(?:${[...siblings.keys()].map(escapeRegExp).join("|")})(?:/.*)?$`);
191
+ return {
192
+ name: "gloomberb-sibling-imports",
193
+ setup(build) {
194
+ build.onResolve({ filter }, (args) => {
195
+ const peer = [...siblings.keys()].find((name) => args.path === name || args.path.startsWith(`${name}/`));
196
+ const path = peer ? siblingModule(peer, siblings.get(peer)!, args.path) : null;
197
+ return path ? { path } : undefined;
198
+ });
199
+ },
200
+ };
201
+ }
@@ -1,5 +1,8 @@
1
- import { readdirSync, readFileSync, type Dirent } from "fs";
2
- import { dirname, extname, isAbsolute, join, relative } from "path";
1
+ import { existsSync, readdirSync, readFileSync, type Dirent } from "fs";
2
+ import { dirname, extname, join } from "path";
3
+
4
+ import { isInside, replaceSpecifier, SOURCE_LOADERS, sourceLoader, withSiblingImports } from "./peer-imports";
5
+ import { pluginDirectoryNames } from "./plugin-names";
3
6
 
4
7
  /**
5
8
  * A second import of a plugin whose first one failed on a sibling plugin it
@@ -19,19 +22,15 @@ import { dirname, extname, isAbsolute, join, relative } from "path";
19
22
  * nothing from the failed attempt is reused. Imports that leave the plugin,
20
23
  * the sibling's files included, resolve to the same modules as always, so the
21
24
  * plugin shares the sibling's state as on a normal load.
25
+ *
26
+ * A plugin's imports of its siblings no longer go through the link at all
27
+ * (peer-imports.ts), so what is left for the retry is a plugin that Bun
28
+ * already failed in this process, imported before its sibling was there.
22
29
  */
23
30
 
24
31
  /** The query that gives a plugin's files their new module keys. */
25
32
  const RETRY_PARAM = "gloom-plugin-retry";
26
33
  const RETRY_QUERY = new RegExp(`[?&]${RETRY_PARAM}=(\\d+)(?:&|$)`);
27
- const SOURCE_LOADERS: Readonly<Record<string, "ts" | "tsx" | "js" | "jsx">> = {
28
- ".ts": "ts",
29
- ".mts": "ts",
30
- ".tsx": "tsx",
31
- ".js": "js",
32
- ".mjs": "js",
33
- ".jsx": "jsx",
34
- };
35
34
  /** A file name that is never there: looking it up is what clears a folder. */
36
35
  const MISSING_FILE = "./.gloom-resolver-refresh";
37
36
  const SKIPPED_FOLDERS = new Set(["node_modules", ".git", "dist"]);
@@ -49,11 +48,6 @@ const retryRoots = new Map<number, string>();
49
48
  let retries = 0;
50
49
  let installed = false;
51
50
 
52
- function isInside(root: string, path: string): boolean {
53
- const inside = relative(root, path);
54
- return !!inside && !inside.startsWith("..") && !isAbsolute(inside);
55
- }
56
-
57
51
  /** The plugin's own folders, without dependencies or build output. */
58
52
  function sourceFolders(dir: string, out: string[] = [], depth = 0): string[] {
59
53
  out.push(dir);
@@ -72,11 +66,11 @@ function sourceFolders(dir: string, out: string[] = [], depth = 0): string[] {
72
66
  }
73
67
 
74
68
  /**
75
- * Makes Bun read the plugin's folders and its links to `peers` from disk
76
- * again. Bun drops a folder from its resolver cache when a lookup in it
77
- * misses, so this looks up a file that is not there in each one. The
78
- * bundler shares the cache, which is why the desktop's compile of the same
79
- * plugin failed in the same session too.
69
+ * Makes Bun read the plugin's folders, its links to `peers` and the peers'
70
+ * own folders from disk again. Bun drops a folder from its resolver cache
71
+ * when a lookup in it misses, so this looks up a file that is not there in
72
+ * each one. The bundler shares the cache, which is why the desktop's compile
73
+ * of the same plugin failed in the same session too.
80
74
  */
81
75
  export function refreshResolverFolders(pluginDir: string, peers: readonly string[]): void {
82
76
  const modules = join(pluginDir, "node_modules");
@@ -84,6 +78,10 @@ export function refreshResolverFolders(pluginDir: string, peers: readonly string
84
78
  for (const peer of peers) {
85
79
  if (peer.startsWith("@")) folders.push(join(modules, peer.split("/")[0]!));
86
80
  folders.push(join(modules, peer));
81
+ for (const name of pluginDirectoryNames(peer)) {
82
+ const peerDir = join(dirname(pluginDir), name);
83
+ if (existsSync(peerDir)) folders.push(...sourceFolders(peerDir));
84
+ }
87
85
  }
88
86
  for (const folder of folders) {
89
87
  try {
@@ -97,15 +95,17 @@ export function refreshResolverFolders(pluginDir: string, peers: readonly string
97
95
  /**
98
96
  * The plugin's file at `path` (with the retry query), its relative imports
99
97
  * of other source files in the plugin given the same query, so they load
100
- * under new keys as well. Imports that leave the plugin keep their keys.
98
+ * under new keys as well. Imports that leave the plugin keep their keys,
99
+ * and those of a sibling plugin name its file in the sibling's folder, as
100
+ * on a first import (peer-imports.ts).
101
101
  */
102
- function rekeyedSource(path: string): { contents: string; loader: "ts" | "tsx" | "js" | "jsx" } {
102
+ function rekeyedSource(path: string): { contents: string; loader: ReturnType<typeof sourceLoader> } {
103
103
  const match = RETRY_QUERY.exec(path);
104
104
  const file = match ? path.slice(0, match.index) : path;
105
- const loader = SOURCE_LOADERS[extname(file).toLowerCase()] ?? "js";
105
+ const loader = sourceLoader(file);
106
106
  let contents = readFileSync(file, "utf8");
107
107
  const root = match ? retryRoots.get(Number(match[1])) : undefined;
108
- if (!match || !root) return { contents, loader };
108
+ if (!match || !root) return { contents: withSiblingImports(file, contents), loader };
109
109
  const retry = Number(match[1]);
110
110
  const specifiers = new Set(
111
111
  new Bun.Transpiler({ loader }).scanImports(contents)
@@ -121,12 +121,9 @@ function rekeyedSource(path: string): { contents: string; loader: "ts" | "tsx" |
121
121
  continue;
122
122
  }
123
123
  if (!isInside(root, target) || !SOURCE_LOADERS[extname(target).toLowerCase()]) continue;
124
- const rekeyed = `${specifier}?${RETRY_PARAM}=${retry}`;
125
- for (const quote of ["\"", "'", "`"]) {
126
- contents = contents.split(`${quote}${specifier}${quote}`).join(`${quote}${rekeyed}${quote}`);
127
- }
124
+ contents = replaceSpecifier(contents, specifier, `${specifier}?${RETRY_PARAM}=${retry}`);
128
125
  }
129
- return { contents, loader };
126
+ return { contents: withSiblingImports(file, contents), loader };
130
127
  }
131
128
 
132
129
  function installRekeyingLoader(): boolean {
@@ -0,0 +1,128 @@
1
+ /**
2
+ * The `process` a plugin sees in the desktop view and the web app.
3
+ *
4
+ * A plugin written for the terminal runs in Bun and reads `process.env`,
5
+ * `process.platform` and friends without a second thought. The desktop view
6
+ * and the browser are not Node: there is no `process`, so the same plugin
7
+ * died on load with `ReferenceError: process is not defined`. Defining a global
8
+ * `process` would be simpler, but the host itself looks at `globalThis.process`
9
+ * to tell a Node runtime from a browser (updater, platform labels, libc
10
+ * detection), so every plugin would change how the host behaves. The bundler
11
+ * (`bundle.ts`) gives each plugin bundle its own copy instead, as a module
12
+ * level constant that only that bundle's references to `process` read.
13
+ *
14
+ * What it holds is the harmless half of `process`: a copy of `env` with only
15
+ * `NODE_ENV` in it (never the host's real environment, which the view cannot
16
+ * read and a plugin should not receive by RPC), the platform of the window it
17
+ * runs in, empty `argv` and `versions`, and timers and listeners that work or
18
+ * do nothing. What cannot exist in a view (`exit`, `kill`, `chdir`, memory
19
+ * and CPU readings) throws an error naming the plugin, instead of "is not a
20
+ * function". Anything else is left undefined, so feature detection such as
21
+ * `process.stdin?.isTTY` keeps working.
22
+ */
23
+
24
+ /** The name the bundler gives the shim; no plugin source declares it. */
25
+ export const PLUGIN_PROCESS_IDENTIFIER = "__gloomPluginProcess";
26
+
27
+ /**
28
+ * Builds the shim. This function is serialised into the plugin bundle with
29
+ * `toString()`, so it has to stay self-contained: no imports and no module
30
+ * level names, only globals the view provides.
31
+ */
32
+ function createPluginProcess(pluginName: string): Record<string, unknown> {
33
+ const nav = (globalThis as {
34
+ navigator?: { userAgentData?: { platform?: string }; platform?: string; userAgent?: string };
35
+ }).navigator;
36
+ const hint = String(nav?.userAgentData?.platform || nav?.platform || nav?.userAgent || "").toLowerCase();
37
+ // Mac is tested before Windows because "darwin" contains "win".
38
+ const platform = /android/.test(hint)
39
+ ? "android"
40
+ : /mac|iphone|ipad|darwin/.test(hint)
41
+ ? "darwin"
42
+ : /win/.test(hint)
43
+ ? "win32"
44
+ : /linux|x11|cros/.test(hint)
45
+ ? "linux"
46
+ : "browser";
47
+
48
+ const hrtime = (previous?: readonly [number, number]): [number, number] => {
49
+ const ms = performance.now();
50
+ let seconds = Math.floor(ms / 1000);
51
+ let nanos = Math.floor((ms % 1000) * 1e6);
52
+ if (previous) {
53
+ seconds -= previous[0];
54
+ nanos -= previous[1];
55
+ if (nanos < 0) {
56
+ seconds -= 1;
57
+ nanos += 1e9;
58
+ }
59
+ }
60
+ return [seconds, nanos];
61
+ };
62
+ const stream = (log: (line: string) => void) => ({
63
+ isTTY: false,
64
+ write: (chunk: unknown) => {
65
+ log(String(chunk).replace(/\n$/, ""));
66
+ return true;
67
+ },
68
+ });
69
+
70
+ const proc: Record<string, unknown> = {
71
+ env: { NODE_ENV: "production" },
72
+ platform,
73
+ browser: true,
74
+ title: "browser",
75
+ argv: [],
76
+ execArgv: [],
77
+ execPath: "",
78
+ version: "",
79
+ versions: {},
80
+ pid: 0,
81
+ ppid: 0,
82
+ cwd: () => "/",
83
+ uptime: () => performance.now() / 1000,
84
+ hrtime: Object.assign(hrtime, { bigint: () => BigInt(Math.round(performance.now() * 1e6)) }),
85
+ nextTick: (callback: (...args: unknown[]) => void, ...args: unknown[]) => {
86
+ queueMicrotask(() => callback(...args));
87
+ },
88
+ emitWarning: (warning: unknown) => {
89
+ console.warn(String((warning as { message?: string } | null)?.message ?? warning));
90
+ },
91
+ stdout: stream((line) => console.log(line)),
92
+ stderr: stream((line) => console.error(line)),
93
+ emit: () => false,
94
+ listeners: () => [],
95
+ listenerCount: () => 0,
96
+ };
97
+ // Nothing exits or signals in a view, so a handler would never run.
98
+ for (const name of [
99
+ "on", "once", "off", "addListener", "removeListener", "removeAllListeners",
100
+ "prependListener", "prependOnceListener", "setMaxListeners",
101
+ ]) {
102
+ proc[name] = () => proc;
103
+ }
104
+ for (const name of [
105
+ "exit", "abort", "kill", "chdir", "umask", "binding", "memoryUsage", "cpuUsage", "resourceUsage",
106
+ ]) {
107
+ proc[name] = () => {
108
+ throw new Error(
109
+ `Plugin "${pluginName}" called process.${name}(), which is only available in the terminal app, not in the desktop or web app.`,
110
+ );
111
+ };
112
+ }
113
+ return proc;
114
+ }
115
+
116
+ /** The declaration the bundler puts at the top of a bundle that reads `process`. */
117
+ export function pluginProcessDeclaration(pluginName: string): string {
118
+ return `const ${PLUGIN_PROCESS_IDENTIFIER} = (${createPluginProcess.toString()})(${JSON.stringify(pluginName)});\n`;
119
+ }
120
+
121
+ /** Source of `import process from "node:process"`, for a plugin that imports it. */
122
+ export function pluginProcessModuleSource(): string {
123
+ const names = Object.keys(createPluginProcess(""));
124
+ return [
125
+ "export default process;",
126
+ ...names.map((name) => `export const ${name} = process.${name};`),
127
+ ].join("\n");
128
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Whether Windows lets this process follow a junction a standard user made,
3
+ * which is what every link to a sibling plugin is (host-link.ts).
4
+ *
5
+ * Windows 11's RedirectionGuard stops a process that runs with it from
6
+ * following such a junction at all: the junction is there, points where it
7
+ * should, and every read through it fails. That is how the gateway's link
8
+ * looked on the first launch after an update. Some Windows services run with
9
+ * it; whether that launch did is not known, so the report of a link that does
10
+ * not resolve carries the answer.
11
+ */
12
+
13
+ /** `ProcessRedirectionTrustPolicy` in Windows' PROCESS_MITIGATION_POLICY. */
14
+ const PROCESS_REDIRECTION_TRUST_POLICY = 16;
15
+ const CURRENT_PROCESS = -1;
16
+ const ENFORCE = 0x1;
17
+ const AUDIT = 0x2;
18
+
19
+ let answer: string | null | undefined;
20
+
21
+ /** `enforce`, `audit` (only logged), `off`, or `unknown` when Windows does not say. Null on other systems. */
22
+ export function redirectionGuard(): string | null {
23
+ if (process.platform !== "win32") return null;
24
+ if (answer !== undefined) return answer;
25
+ answer = "unknown";
26
+ try {
27
+ // Required when asked, not imported: the pane screenshot page bundles the
28
+ // plugin loader for a browser, which has no `bun:ffi`.
29
+ const { dlopen, ptr } = import.meta.require("bun:ffi") as typeof import("bun:ffi");
30
+ const kernel32 = dlopen("kernel32.dll", {
31
+ GetProcessMitigationPolicy: { args: ["i64", "i32", "ptr", "u64"], returns: "i32" },
32
+ });
33
+ try {
34
+ const flags = new Uint32Array(1);
35
+ if (kernel32.symbols.GetProcessMitigationPolicy(CURRENT_PROCESS, PROCESS_REDIRECTION_TRUST_POLICY, ptr(flags), 4)) {
36
+ answer = (flags[0]! & ENFORCE) ? "enforce" : (flags[0]! & AUDIT) ? "audit" : "off";
37
+ }
38
+ } finally {
39
+ kernel32.close();
40
+ }
41
+ } catch {
42
+ // Intentional ignore: a Windows without the policy leaves the answer unknown.
43
+ }
44
+ return answer;
45
+ }