@gloom-sh/terminal 0.19.1 → 0.19.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (554) hide show
  1. package/README.md +7 -1
  2. package/package.json +2 -1
  3. package/patches/electrobun@1.18.1.patch +108 -1
  4. package/src/account/upgrade-dialog.tsx +4 -23
  5. package/src/api-client/coalesce.ts +118 -0
  6. package/src/api-client/credit-boards.ts +13 -0
  7. package/src/api-client/crypto-etf-flows.ts +6 -0
  8. package/src/api-client/crypto-markets.ts +40 -0
  9. package/src/api-client/data.ts +201 -14
  10. package/src/api-client/equity-screener.ts +10 -0
  11. package/src/api-client/errors.ts +58 -0
  12. package/src/api-client/gpu.ts +29 -0
  13. package/src/api-client/index.ts +5 -2
  14. package/src/api-client/plan-access.ts +10 -1
  15. package/src/api-client/plan-rules.ts +35 -0
  16. package/src/api-client/research-activity.ts +1 -32
  17. package/src/api-client/types.ts +21 -1
  18. package/src/app/attention-notice.tsx +94 -0
  19. package/src/app/runtime/startup.ts +3 -1
  20. package/src/app.tsx +23 -1
  21. package/src/architecture/core-builtin-allowlist.json +2 -1
  22. package/src/capabilities/chart-series.ts +2 -1
  23. package/src/capabilities/factories.ts +1 -0
  24. package/src/cli/ascii-output.ts +42 -0
  25. package/src/cli/base-converter.ts +10 -2
  26. package/src/cli/command-options.ts +16 -1
  27. package/src/cli/commands/crypto-hints.ts +23 -4
  28. package/src/cli/commands/market.ts +101 -27
  29. package/src/cli/commands/news-search.ts +24 -5
  30. package/src/cli/commands/overview.ts +14 -10
  31. package/src/cli/commands/search.ts +84 -25
  32. package/src/cli/commands/system.ts +22 -30
  33. package/src/cli/commands/ticker.ts +49 -9
  34. package/src/cli/commands/version.ts +33 -0
  35. package/src/cli/context.ts +6 -9
  36. package/src/cli/data-dir.ts +7 -0
  37. package/src/cli/desktop-pane-shot-api-policy.ts +85 -0
  38. package/src/cli/desktop-pane-shot-browser.ts +320 -0
  39. package/src/cli/desktop-pane-shot-routes.ts +26 -1
  40. package/src/cli/desktop-pane-shot.ts +196 -256
  41. package/src/cli/early-command.ts +42 -0
  42. package/src/cli/earnings-calendar.ts +9 -0
  43. package/src/cli/entry.ts +19 -13
  44. package/src/cli/errors.ts +12 -2
  45. package/src/cli/glossary.ts +2 -1
  46. package/src/cli/help.ts +1 -1
  47. package/src/cli/history-export.ts +22 -2
  48. package/src/cli/history-rows.ts +189 -11
  49. package/src/cli/index.ts +18 -1
  50. package/src/cli/known-gaps.ts +51 -5
  51. package/src/cli/listing-arg.ts +63 -10
  52. package/src/cli/options.ts +1 -1
  53. package/src/cli/output-file.ts +60 -13
  54. package/src/cli/pane-functions/catalog.ts +14 -3
  55. package/src/cli/pane-functions/data.ts +1 -1
  56. package/src/cli/pane-functions/dom.ts +45 -0
  57. package/src/cli/pane-functions/freshness.ts +46 -9
  58. package/src/cli/pane-functions/function-help.ts +17 -9
  59. package/src/cli/pane-functions/headless.ts +22 -14
  60. package/src/cli/pane-functions/index.ts +6 -2
  61. package/src/cli/pane-functions/options.ts +6 -2
  62. package/src/cli/pane-functions/resolver.ts +27 -5
  63. package/src/cli/pane-functions/screenshot-assessment.ts +33 -0
  64. package/src/cli/pane-functions/screenshot-bridge.ts +94 -13
  65. package/src/cli/pane-functions/screenshot-http.ts +70 -0
  66. package/src/cli/pane-functions/screenshot-render.ts +19 -5
  67. package/src/cli/quote-answer.ts +49 -6
  68. package/src/cli/registry.ts +41 -13
  69. package/src/cli/result.ts +12 -6
  70. package/src/cli/ticker-resolution.ts +2 -1
  71. package/src/cli/types.ts +2 -0
  72. package/src/components/chart/composite/format.ts +3 -0
  73. package/src/components/chart-table/header.tsx +1 -1
  74. package/src/components/chart-table/layout.ts +28 -11
  75. package/src/components/command-bar/action-runtime.ts +4 -0
  76. package/src/components/command-bar/commands/direct/index.ts +18 -33
  77. package/src/components/command-bar/commands/direct/runtime.ts +5 -0
  78. package/src/components/command-bar/commands/registry.ts +10 -1
  79. package/src/components/command-bar/routes/root/command-items.ts +9 -4
  80. package/src/components/command-bar/routes/root/runtime.ts +51 -6
  81. package/src/components/command-bar/selection-runtime.ts +12 -29
  82. package/src/components/command-bar/surface/index.tsx +9 -3
  83. package/src/components/company-logo.tsx +2 -1
  84. package/src/components/layout/detached-pane-shell.tsx +2 -2
  85. package/src/components/layout/header.tsx +10 -5
  86. package/src/components/layout/market-summary.ts +25 -7
  87. package/src/components/layout/pane/footer/index.tsx +8 -2
  88. package/src/components/layout/pane/footer/model.ts +73 -6
  89. package/src/components/layout/pane/header.tsx +2 -2
  90. package/src/components/layout/pane/status-footer.ts +2 -1
  91. package/src/components/market-board/index.tsx +3 -2
  92. package/src/components/onboarding/desks-step.tsx +12 -4
  93. package/src/components/onboarding/first-run-workspace.ts +81 -4
  94. package/src/components/onboarding/onboarding-frame.tsx +4 -2
  95. package/src/components/onboarding/onboarding-keyboard.ts +8 -5
  96. package/src/components/onboarding/onboarding-wizard.tsx +60 -26
  97. package/src/components/onboarding/portfolio-step/positions-panel.tsx +7 -2
  98. package/src/components/onboarding/run-setup-again.ts +24 -0
  99. package/src/components/onboarding/use-onboarding-progress.ts +33 -15
  100. package/src/components/onboarding/use-onboarding-upgrade.ts +2 -6
  101. package/src/components/onboarding/wizard-model.ts +57 -3
  102. package/src/components/onboarding/wizard-positions.ts +1 -1
  103. package/src/components/ui/delayed-mark.tsx +56 -0
  104. package/src/components/ui/display.tsx +16 -5
  105. package/src/components/ui/external-link.tsx +3 -6
  106. package/src/components/ui/icon.tsx +16 -3
  107. package/src/components/ui/logo-mark.tsx +8 -5
  108. package/src/components/ui/stat-grid.tsx +263 -19
  109. package/src/core/app-services.ts +11 -2
  110. package/src/data/app-persistence.ts +9 -7
  111. package/src/data/config/store/node.ts +11 -5
  112. package/src/data/config/store/normalize.ts +16 -3
  113. package/src/data/plugin-cache.ts +15 -0
  114. package/src/data/plugin-state-store.ts +81 -24
  115. package/src/data/profile-permissions.ts +47 -0
  116. package/src/data/resource-store.ts +165 -90
  117. package/src/data/session-store.ts +31 -8
  118. package/src/data/sqlite/access.ts +324 -0
  119. package/src/data/sqlite/checkpointer.ts +285 -0
  120. package/src/data/sqlite/database.ts +80 -4
  121. package/src/data/sqlite/retry.ts +50 -9
  122. package/src/data/ticker-store.ts +57 -23
  123. package/src/docs/snapshot.json +191 -78
  124. package/src/i18n/es.ts +18 -2
  125. package/src/i18n/ja.ts +18 -2
  126. package/src/i18n/ko.ts +18 -2
  127. package/src/i18n/zh-cn.ts +18 -2
  128. package/src/i18n/zh-tw.ts +18 -2
  129. package/src/layout/desk-keys.ts +4 -1
  130. package/src/layout/desks.ts +38 -5
  131. package/src/layout-marketplace/gallery-desktop.tsx +45 -16
  132. package/src/market-data/batched-reads.ts +16 -0
  133. package/src/market-data/coordinator/auxiliary.ts +4 -2
  134. package/src/market-data/coordinator/index.ts +11 -2
  135. package/src/market-data/futures-board.ts +1 -1
  136. package/src/market-data/futures-contract.ts +3 -1
  137. package/src/market-data/market/index-level.ts +6 -4
  138. package/src/market-data/published-apac-sessions.ts +6 -0
  139. package/src/market-data/realized-volatility.ts +4 -2
  140. package/src/news/aggregator.ts +7 -4
  141. package/src/news/news-model.ts +11 -0
  142. package/src/news/story-id.ts +22 -0
  143. package/src/news/ticker-symbols.ts +17 -1
  144. package/src/notifications/app-notifier.ts +35 -14
  145. package/src/plugins/auto-update-node.ts +10 -6
  146. package/src/plugins/builtin/account-management/form-components.tsx +1 -1
  147. package/src/plugins/builtin/account-management/index.tsx +2 -3
  148. package/src/plugins/builtin/account-management/model.ts +1 -1
  149. package/src/plugins/builtin/alerts/cli-forms.ts +15 -1
  150. package/src/plugins/builtin/alerts/events-pane.tsx +7 -5
  151. package/src/plugins/builtin/alerts/events.ts +10 -2
  152. package/src/plugins/builtin/alerts/index.tsx +5 -5
  153. package/src/plugins/builtin/alerts/research-builder.ts +5 -1
  154. package/src/plugins/builtin/alerts/research-rules.ts +18 -0
  155. package/src/plugins/builtin/alerts/targets.ts +4 -1
  156. package/src/plugins/builtin/attention/headless.ts +1 -1
  157. package/src/plugins/builtin/attention/index.tsx +2 -4
  158. package/src/plugins/builtin/attention/listing.ts +1 -1
  159. package/src/plugins/builtin/attention/series.ts +1 -1
  160. package/src/plugins/builtin/awards/index.tsx +1 -2
  161. package/src/plugins/builtin/backtest/client.ts +1 -0
  162. package/src/plugins/builtin/backtest/index.tsx +1 -2
  163. package/src/plugins/builtin/backtest/rules.ts +3 -1
  164. package/src/plugins/builtin/backtest/window.ts +8 -7
  165. package/src/plugins/builtin/broker-manager/detail.tsx +16 -3
  166. package/src/plugins/builtin/broker-manager/index.tsx +1 -1
  167. package/src/plugins/builtin/buildout/cells.tsx +5 -0
  168. package/src/plugins/builtin/buildout/company/flow.tsx +367 -0
  169. package/src/plugins/builtin/buildout/company/id.ts +2 -0
  170. package/src/plugins/builtin/buildout/company/model.ts +247 -0
  171. package/src/plugins/builtin/buildout/company/pane.tsx +317 -0
  172. package/src/plugins/builtin/buildout/data-runtime.ts +31 -10
  173. package/src/plugins/builtin/buildout/facilities.ts +309 -0
  174. package/src/plugins/builtin/buildout/format.ts +9 -0
  175. package/src/plugins/builtin/buildout/index.tsx +51 -5
  176. package/src/plugins/builtin/buildout/model/index.ts +62 -10
  177. package/src/plugins/builtin/buildout/model/normalizers.ts +5 -0
  178. package/src/plugins/builtin/buildout/model/types.ts +11 -2
  179. package/src/plugins/builtin/buildout/pane/index.tsx +91 -20
  180. package/src/plugins/builtin/buildout/pane/status.tsx +1 -1
  181. package/src/plugins/builtin/buildout/pane/themes.tsx +110 -0
  182. package/src/plugins/builtin/buildout/table-model.ts +26 -2
  183. package/src/plugins/builtin/builtin-plugin-meta.ts +16 -0
  184. package/src/plugins/builtin/cds/headless.ts +10 -2
  185. package/src/plugins/builtin/cds/model.ts +7 -3
  186. package/src/plugins/builtin/central-bank-rates/index.tsx +1 -2
  187. package/src/plugins/builtin/central-bank-rates/pane.tsx +2 -1
  188. package/src/plugins/builtin/central-bank-rates/series.ts +2 -3
  189. package/src/plugins/builtin/changelog/index.tsx +3 -2
  190. package/src/plugins/builtin/chart-composer/cli-options.ts +16 -0
  191. package/src/plugins/builtin/chart-composer/headless-schema.ts +10 -0
  192. package/src/plugins/builtin/chart-composer/headless.ts +88 -21
  193. package/src/plugins/builtin/chart-composer/index.tsx +2 -3
  194. package/src/plugins/builtin/chart-composer/pane.tsx +30 -2
  195. package/src/plugins/builtin/chart-composer/series-expression.ts +3 -2
  196. package/src/plugins/builtin/chart-composer/session-range.ts +5 -2
  197. package/src/plugins/builtin/chart-composer/studies.ts +1 -1
  198. package/src/plugins/builtin/chat/controller/index.ts +1 -1
  199. package/src/plugins/builtin/chat/controller/messages.ts +15 -4
  200. package/src/plugins/builtin/chat/ghost-user.ts +8 -0
  201. package/src/plugins/builtin/chat/index.tsx +5 -5
  202. package/src/plugins/builtin/clinical-trials/index.tsx +1 -2
  203. package/src/plugins/builtin/cloud/company-picker.tsx +1 -1
  204. package/src/plugins/builtin/cloud/index.tsx +1 -2
  205. package/src/plugins/builtin/cloud/plugin.tsx +6 -4
  206. package/src/plugins/builtin/cloud/team/module.tsx +1 -1
  207. package/src/plugins/builtin/cloud/thesis/module.tsx +1 -1
  208. package/src/plugins/builtin/cloud/trial-offer-status-widget.tsx +24 -1
  209. package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +39 -15
  210. package/src/plugins/builtin/comment-letters/index.tsx +1 -2
  211. package/src/plugins/builtin/commodity-balances/client.ts +4 -1
  212. package/src/plugins/builtin/commodity-balances/index.tsx +3 -3
  213. package/src/plugins/builtin/commodity-balances/pane.tsx +2 -2
  214. package/src/plugins/builtin/congress-trades/aggregates.ts +13 -2
  215. package/src/plugins/builtin/congress-trades/detail.tsx +20 -4
  216. package/src/plugins/builtin/congress-trades/headless.ts +2 -0
  217. package/src/plugins/builtin/congress-trades/model.ts +92 -64
  218. package/src/plugins/builtin/congress-trades/table.tsx +14 -2
  219. package/src/plugins/builtin/connections/index.ts +2 -3
  220. package/src/plugins/builtin/cot/client.ts +28 -2
  221. package/src/plugins/builtin/cot/headless.ts +4 -1
  222. package/src/plugins/builtin/cot/index.tsx +5 -4
  223. package/src/plugins/builtin/cot/model.ts +58 -7
  224. package/src/plugins/builtin/cot/pane.tsx +6 -2
  225. package/src/plugins/builtin/cpi/client.ts +6 -5
  226. package/src/plugins/builtin/cpi/index.tsx +1 -2
  227. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  228. package/src/plugins/builtin/credit-boards/client.ts +7 -2
  229. package/src/plugins/builtin/credit-boards/headless.ts +25 -12
  230. package/src/plugins/builtin/credit-boards/model.ts +29 -0
  231. package/src/plugins/builtin/credit-boards/series.ts +3 -4
  232. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +9 -4
  233. package/src/plugins/builtin/credit-conditions/client.ts +22 -14
  234. package/src/plugins/builtin/crypto-board/bitcoin-network.ts +143 -0
  235. package/src/plugins/builtin/crypto-board/client.ts +5 -5
  236. package/src/plugins/builtin/crypto-board/headless.ts +4 -3
  237. package/src/plugins/builtin/crypto-board/index.tsx +4 -2
  238. package/src/plugins/builtin/crypto-board/series.ts +6 -4
  239. package/src/plugins/builtin/crypto-etf-flows/client.ts +9 -2
  240. package/src/plugins/builtin/crypto-etf-flows/headless.ts +61 -6
  241. package/src/plugins/builtin/crypto-etf-flows/index.tsx +1 -2
  242. package/src/plugins/builtin/crypto-etf-flows/model.ts +14 -1
  243. package/src/plugins/builtin/crypto-etf-flows/pane.tsx +8 -5
  244. package/src/plugins/builtin/debt-maturities/headless.ts +4 -3
  245. package/src/plugins/builtin/debt-maturities/model.ts +17 -1
  246. package/src/plugins/builtin/debt-maturities/pane.tsx +18 -4
  247. package/src/plugins/builtin/debt-maturities/series.ts +3 -3
  248. package/src/plugins/builtin/debug/index.tsx +1 -1
  249. package/src/plugins/builtin/dividend-yield/client.ts +32 -2
  250. package/src/plugins/builtin/dividend-yield/headless.ts +58 -33
  251. package/src/plugins/builtin/dividend-yield/index.tsx +37 -2
  252. package/src/plugins/builtin/dividend-yield/pane.tsx +32 -13
  253. package/src/plugins/builtin/dividend-yield/ranking-reads.ts +73 -0
  254. package/src/plugins/builtin/dividend-yield/ranking-view.tsx +163 -0
  255. package/src/plugins/builtin/dividend-yield/ranking.ts +1 -1
  256. package/src/plugins/builtin/dividend-yield/view.ts +21 -0
  257. package/src/plugins/builtin/docs/index.tsx +2 -3
  258. package/src/plugins/builtin/doe/client.ts +6 -5
  259. package/src/plugins/builtin/doe/headless.ts +16 -3
  260. package/src/plugins/builtin/doe/index.tsx +5 -4
  261. package/src/plugins/builtin/doe/prices.ts +2 -3
  262. package/src/plugins/builtin/doe/series.ts +4 -4
  263. package/src/plugins/builtin/earnings/board.tsx +2 -1
  264. package/src/plugins/builtin/earnings/history.tsx +2 -1
  265. package/src/plugins/builtin/earnings-calls/calendar.tsx +10 -2
  266. package/src/plugins/builtin/earnings-calls/data.ts +19 -11
  267. package/src/plugins/builtin/earnings-calls/pane.tsx +27 -14
  268. package/src/plugins/builtin/earnings-ripple/client.ts +32 -7
  269. package/src/plugins/builtin/earnings-ripple/index.tsx +1 -2
  270. package/src/plugins/builtin/econ/calendar-model.ts +29 -6
  271. package/src/plugins/builtin/econ/detail-view.tsx +16 -2
  272. package/src/plugins/builtin/econ-statistics/index.tsx +1 -2
  273. package/src/plugins/builtin/equity-screener/client.ts +1 -1
  274. package/src/plugins/builtin/equity-screener/headless.ts +1 -0
  275. package/src/plugins/builtin/equity-screener/ideas.ts +30 -1
  276. package/src/plugins/builtin/equity-screener/index.tsx +1 -2
  277. package/src/plugins/builtin/equity-screener/model.ts +28 -4
  278. package/src/plugins/builtin/equity-screener/pane.tsx +5 -0
  279. package/src/plugins/builtin/equity-screener/screen-check.ts +23 -1
  280. package/src/plugins/builtin/estimate-revisions/index.tsx +1 -2
  281. package/src/plugins/builtin/executives/data.ts +6 -1
  282. package/src/plugins/builtin/executives/pane.tsx +9 -21
  283. package/src/plugins/builtin/exposure/holdings.ts +35 -13
  284. package/src/plugins/builtin/exposure/index.tsx +1 -2
  285. package/src/plugins/builtin/fear-greed/cache.ts +4 -0
  286. package/src/plugins/builtin/fear-greed/cli.ts +44 -0
  287. package/src/plugins/builtin/fear-greed/index.tsx +5 -53
  288. package/src/plugins/builtin/fear-greed/pane.tsx +2 -1
  289. package/src/plugins/builtin/fear-greed/series.ts +6 -4
  290. package/src/plugins/builtin/fixed-income/index.tsx +1 -2
  291. package/src/plugins/builtin/futures/board-reads.ts +25 -0
  292. package/src/plugins/builtin/futures/coalesced-bump.ts +44 -0
  293. package/src/plugins/builtin/futures/index.tsx +15 -0
  294. package/src/plugins/builtin/futures/model.ts +14 -0
  295. package/src/plugins/builtin/futures/use-board-terms.ts +22 -15
  296. package/src/plugins/builtin/futures/use-front-returns.ts +20 -14
  297. package/src/plugins/builtin/futures-curve/headless.ts +6 -1
  298. package/src/plugins/builtin/futures-curve/index.tsx +1 -2
  299. package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
  300. package/src/plugins/builtin/gpu/board.tsx +51 -12
  301. package/src/plugins/builtin/gpu/client.ts +19 -3
  302. package/src/plugins/builtin/gpu/equities.tsx +5 -2
  303. package/src/plugins/builtin/gpu/headless.ts +14 -2
  304. package/src/plugins/builtin/gpu/index.tsx +1 -2
  305. package/src/plugins/builtin/gpu/model.ts +20 -2
  306. package/src/plugins/builtin/gpu/pane.tsx +1 -1
  307. package/src/plugins/builtin/gpu/series.ts +2 -3
  308. package/src/plugins/builtin/help/index.tsx +1 -1
  309. package/src/plugins/builtin/holders/beneficial-view.tsx +1 -2
  310. package/src/plugins/builtin/holders/index-holders.ts +24 -12
  311. package/src/plugins/builtin/holders/index.tsx +2 -3
  312. package/src/plugins/builtin/ipo-calendar/index.tsx +1 -2
  313. package/src/plugins/builtin/iv-history/index.tsx +1 -2
  314. package/src/plugins/builtin/jodi/client.ts +4 -1
  315. package/src/plugins/builtin/jodi/index.tsx +1 -2
  316. package/src/plugins/builtin/kelly-sizer/index.tsx +1 -2
  317. package/src/plugins/builtin/layout-manager/index.tsx +9 -8
  318. package/src/plugins/builtin/macro-day/pane.tsx +4 -2
  319. package/src/plugins/builtin/market-halts/index.tsx +1 -2
  320. package/src/plugins/builtin/market-heatmap/index.tsx +11 -2
  321. package/src/plugins/builtin/market-movers/client.ts +1 -0
  322. package/src/plugins/builtin/market-movers/index.tsx +16 -5
  323. package/src/plugins/builtin/market-movers/screener.ts +23 -6
  324. package/src/plugins/builtin/market-movers/session-body.tsx +6 -2
  325. package/src/plugins/builtin/market-valuation/index.tsx +1 -2
  326. package/src/plugins/builtin/members/index.tsx +1 -2
  327. package/src/plugins/builtin/money-markets/index.tsx +1 -2
  328. package/src/plugins/builtin/money-markets/pane.tsx +2 -1
  329. package/src/plugins/builtin/money-markets/series.ts +3 -5
  330. package/src/plugins/builtin/news/headless.ts +7 -0
  331. package/src/plugins/builtin/news/wire/index.tsx +1 -1
  332. package/src/plugins/builtin/news/wire/news/pop-out.tsx +9 -3
  333. package/src/plugins/builtin/notes/index.tsx +3 -6
  334. package/src/plugins/builtin/openfda/index.tsx +1 -2
  335. package/src/plugins/builtin/options/view.tsx +6 -5
  336. package/src/plugins/builtin/options-calculator/index.tsx +1 -2
  337. package/src/plugins/builtin/options-positioning/index.tsx +1 -2
  338. package/src/plugins/builtin/options-scenario/index.tsx +2 -4
  339. package/src/plugins/builtin/perps/index.tsx +1 -2
  340. package/src/plugins/builtin/plugin-marketplace/index.tsx +2 -3
  341. package/src/plugins/builtin/plugin-marketplace/pane.tsx +2 -1
  342. package/src/plugins/builtin/plugin-module.ts +16 -2
  343. package/src/plugins/builtin/portfolio-list/cli/render.ts +53 -3
  344. package/src/plugins/builtin/portfolio-list/index.tsx +1 -2
  345. package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -0
  346. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -2
  347. package/src/plugins/builtin/power/gas-client.ts +2 -1
  348. package/src/plugins/builtin/power/index.tsx +1 -2
  349. package/src/plugins/builtin/power/prices-client.ts +3 -2
  350. package/src/plugins/builtin/rate-path/index.tsx +1 -2
  351. package/src/plugins/builtin/realized-vol/headless.ts +2 -0
  352. package/src/plugins/builtin/realized-vol/index.tsx +1 -2
  353. package/src/plugins/builtin/realized-vol/model.ts +16 -2
  354. package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
  355. package/src/plugins/builtin/relative-rotation/index.tsx +1 -2
  356. package/src/plugins/builtin/research/analyst-headless.ts +33 -6
  357. package/src/plugins/builtin/research/analyst-model.ts +37 -2
  358. package/src/plugins/builtin/research/analyst-pane.tsx +25 -5
  359. package/src/plugins/builtin/research/index.tsx +5 -8
  360. package/src/plugins/builtin/research-search/index.tsx +1 -2
  361. package/src/plugins/builtin/revenue-breakdown/headless.ts +22 -8
  362. package/src/plugins/builtin/revenue-breakdown/model.ts +23 -0
  363. package/src/plugins/builtin/reverse-dcf/pane.tsx +4 -2
  364. package/src/plugins/builtin/risk-factors/data.ts +8 -4
  365. package/src/plugins/builtin/risk-factors/index.tsx +3 -1
  366. package/src/plugins/builtin/risk-factors/pane.tsx +5 -8
  367. package/src/plugins/builtin/roadmap/index.tsx +2 -3
  368. package/src/plugins/builtin/roadmap/pane.tsx +49 -2
  369. package/src/plugins/builtin/scanner/hilo-model.ts +18 -1
  370. package/src/plugins/builtin/scanner/hilo-pane.tsx +12 -2
  371. package/src/plugins/builtin/scanner/live-pane.tsx +7 -4
  372. package/src/plugins/builtin/scanner/lock-note.ts +26 -3
  373. package/src/plugins/builtin/sec/filing-content.ts +38 -6
  374. package/src/plugins/builtin/sec/headless.ts +5 -1
  375. package/src/plugins/builtin/sec/index.tsx +4 -2
  376. package/src/plugins/builtin/sec/model.ts +2 -0
  377. package/src/plugins/builtin/sectors/index.tsx +11 -5
  378. package/src/plugins/builtin/series-inventory.ts +2 -0
  379. package/src/plugins/builtin/shared/cloud-resource.ts +73 -8
  380. package/src/plugins/builtin/shared/cloud-upgrade.ts +24 -1
  381. package/src/plugins/builtin/shared/forming-bars.ts +29 -0
  382. package/src/plugins/builtin/shared/fred-points.ts +6 -1
  383. package/src/plugins/builtin/shared/fx-rate-freshness.ts +17 -0
  384. package/src/plugins/builtin/shared/pro-lock-note.ts +23 -8
  385. package/src/plugins/builtin/shared/series-detail-chart.tsx +4 -2
  386. package/src/plugins/builtin/shared/series-warning.ts +13 -4
  387. package/src/plugins/builtin/shared/use-quote-board.ts +5 -0
  388. package/src/plugins/builtin/short-interest/publications-pane.tsx +2 -1
  389. package/src/plugins/builtin/short-volume/pane.tsx +2 -1
  390. package/src/plugins/builtin/short-volume/series.ts +2 -3
  391. package/src/plugins/builtin/social-mentions/client.ts +15 -10
  392. package/src/plugins/builtin/social-mentions/index.tsx +1 -2
  393. package/src/plugins/builtin/social-mentions/pane.tsx +2 -1
  394. package/src/plugins/builtin/social-mentions/series.ts +2 -3
  395. package/src/plugins/builtin/supply-chain/client.ts +22 -1
  396. package/src/plugins/builtin/supply-chain/graph-client.ts +22 -1
  397. package/src/plugins/builtin/supply-chain/index.tsx +1 -2
  398. package/src/plugins/builtin/thirteenf/api.ts +127 -29
  399. package/src/plugins/builtin/thirteenf/data.ts +165 -35
  400. package/src/plugins/builtin/thirteenf/index.tsx +2 -4
  401. package/src/plugins/builtin/thirteenf/model.ts +2 -0
  402. package/src/plugins/builtin/thirteenf/overlap-pane.tsx +5 -1
  403. package/src/plugins/builtin/thirteenf/pane.tsx +41 -16
  404. package/src/plugins/builtin/thirteenf/types.ts +14 -0
  405. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +22 -6
  406. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +81 -11
  407. package/src/plugins/builtin/ticker-detail/financials/model.ts +35 -2
  408. package/src/plugins/builtin/ticker-detail/financials/ratios.ts +53 -18
  409. package/src/plugins/builtin/ticker-detail/headless.ts +70 -11
  410. package/src/plugins/builtin/ticker-detail/index.tsx +5 -10
  411. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +16 -6
  412. package/src/plugins/builtin/time-sales/index.tsx +1 -2
  413. package/src/plugins/builtin/time-sales/pane.tsx +3 -1
  414. package/src/plugins/builtin/trace-bonds/headless.ts +11 -2
  415. package/src/plugins/builtin/trace-bonds/model.ts +44 -10
  416. package/src/plugins/builtin/trace-bonds/pane.tsx +12 -7
  417. package/src/plugins/builtin/treasury-auctions/cache.ts +20 -8
  418. package/src/plugins/builtin/treasury-auctions/client.ts +3 -3
  419. package/src/plugins/builtin/treasury-auctions/headless.ts +3 -1
  420. package/src/plugins/builtin/treasury-auctions/pane.tsx +10 -4
  421. package/src/plugins/builtin/vol-surface/client.ts +18 -3
  422. package/src/plugins/builtin/vol-surface/index.tsx +1 -2
  423. package/src/plugins/builtin/volatility/client.ts +43 -16
  424. package/src/plugins/builtin/volatility/headless.ts +5 -2
  425. package/src/plugins/builtin/volatility/index.tsx +1 -2
  426. package/src/plugins/builtin/volatility/model.ts +17 -6
  427. package/src/plugins/builtin/volatility/pane.tsx +6 -2
  428. package/src/plugins/builtin/volatility/series.ts +2 -1
  429. package/src/plugins/builtin/world-indices/footer.ts +6 -1
  430. package/src/plugins/builtin/world-venue-map/client.ts +10 -1
  431. package/src/plugins/builtin/world-venue-map/index.ts +6 -2
  432. package/src/plugins/builtin/world-venue-map/layered-pane.tsx +48 -7
  433. package/src/plugins/builtin/world-venue-map/layers.ts +17 -0
  434. package/src/plugins/builtin/world-venue-map/local-layers.ts +141 -0
  435. package/src/plugins/builtin/world-venue-map/map-symbols.ts +18 -0
  436. package/src/plugins/builtin/world-venue-map/map.tsx +4 -2
  437. package/src/plugins/builtin/world-venue-map/scope.ts +63 -0
  438. package/src/plugins/builtin/yield-curve/client.ts +4 -6
  439. package/src/plugins/builtin/yield-curve/curves.ts +16 -2
  440. package/src/plugins/builtin/yield-curve/headless.ts +3 -3
  441. package/src/plugins/bundle.ts +52 -2
  442. package/src/plugins/catalog-browser.ts +2 -2
  443. package/src/plugins/catalog-cli.ts +20 -0
  444. package/src/plugins/host-link.ts +13 -1
  445. package/src/plugins/installer.ts +64 -42
  446. package/src/plugins/loader.ts +43 -11
  447. package/src/plugins/peer-imports.ts +201 -0
  448. package/src/plugins/peer-retry.ts +26 -29
  449. package/src/plugins/plugin-process.ts +128 -0
  450. package/src/plugins/redirection-guard.ts +45 -0
  451. package/src/remote/endpoint.ts +16 -3
  452. package/src/renderers/browser/main.tsx +3 -2
  453. package/src/renderers/cloudflare/http-proxy.ts +7 -123
  454. package/src/renderers/dom/desktop/icons.tsx +5 -2
  455. package/src/renderers/dom/host/image-surface.tsx +46 -21
  456. package/src/renderers/dom/styles.css +7 -0
  457. package/src/renderers/dom/toast-host.tsx +2 -0
  458. package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
  459. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +23 -1
  460. package/src/renderers/electrobun/bun/desktop/initialization.ts +12 -3
  461. package/src/renderers/electrobun/bun/desktop/rpc-timing.ts +88 -0
  462. package/src/renderers/electrobun/bun/desktop/update.ts +20 -4
  463. package/src/renderers/electrobun/bun/desktop/view-ids.ts +8 -4
  464. package/src/renderers/electrobun/bun/external-plugins.ts +4 -0
  465. package/src/renderers/electrobun/bun/window/attention.ts +54 -0
  466. package/src/renderers/electrobun/bun/window/create.ts +6 -2
  467. package/src/renderers/electrobun/bun/window/native-timing.ts +48 -0
  468. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +82 -4
  469. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +13 -5
  470. package/src/renderers/electrobun/view/desktop/window/snapshot.ts +3 -0
  471. package/src/renderers/electrobun/view/main.tsx +7 -2
  472. package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
  473. package/src/renderers/electrobun/view/ui-host.tsx +9 -2
  474. package/src/renderers/opentui/browser-open.ts +10 -4
  475. package/src/renderers/opentui/image/surface.tsx +1 -1
  476. package/src/renderers/opentui/start.tsx +12 -6
  477. package/src/renderers/opentui/toast-host.tsx +2 -1
  478. package/src/series/registry.ts +18 -0
  479. package/src/series/resolve.ts +2 -1
  480. package/src/series/standalone.ts +10 -1
  481. package/src/series/types.ts +12 -0
  482. package/src/series/validate.ts +11 -2
  483. package/src/sources/gloomberb-cloud/fred-series.ts +5 -3
  484. package/src/sources/gloomberb-cloud/index.ts +28 -5
  485. package/src/sources/gloomberb-cloud/news.ts +24 -3
  486. package/src/sources/gloomberb-cloud/normalizers.ts +17 -2
  487. package/src/sources/provider-errors.ts +26 -3
  488. package/src/sources/provider-router/history.ts +5 -0
  489. package/src/sources/provider-router/index.ts +13 -0
  490. package/src/sync/core-contributors.ts +32 -0
  491. package/src/telemetry/attention-counts.ts +64 -6
  492. package/src/telemetry/attention-setting.ts +33 -0
  493. package/src/telemetry/crash-reports-node.ts +3 -1
  494. package/src/telemetry/stall-clocks.ts +93 -0
  495. package/src/telemetry/stall-reports-node.ts +25 -9
  496. package/src/telemetry/stall-reports.ts +218 -13
  497. package/src/test-support/outage.ts +8 -0
  498. package/src/test-support/preload.ts +9 -0
  499. package/src/tickers/company-name-listing.ts +59 -29
  500. package/src/tickers/company-name.ts +84 -1
  501. package/src/tickers/listing-fate.ts +14 -3
  502. package/src/tickers/saved-instrument.ts +17 -0
  503. package/src/tickers/search/home-listing.ts +104 -8
  504. package/src/tickers/search/index.ts +5 -2
  505. package/src/tickers/search/types.ts +6 -1
  506. package/src/time-series/bar-aggregation.ts +7 -1
  507. package/src/time-series/base-series.ts +3 -3
  508. package/src/time-series/chart-bounds.ts +1 -1
  509. package/src/time-series/chart-data.ts +5 -2
  510. package/src/time-series/chart-seed.ts +1 -1
  511. package/src/time-series/coin-session-close.ts +211 -0
  512. package/src/time-series/live-bars.ts +13 -2
  513. package/src/time-series/live-quotes.ts +1 -1
  514. package/src/time-series/market.ts +5 -3
  515. package/src/time-series/official-close.ts +159 -0
  516. package/src/time-series/price-comparison.ts +75 -8
  517. package/src/time-series/resolve.ts +32 -4
  518. package/src/time-series/series-presentation.ts +1 -1
  519. package/src/time-series/studies/formula.ts +205 -0
  520. package/src/time-series/studies/indicators.ts +299 -0
  521. package/src/time-series/studies/leverage.ts +75 -0
  522. package/src/time-series/studies/pair.ts +138 -0
  523. package/src/time-series/studies/resolve.ts +333 -0
  524. package/src/time-series/studies/shared.ts +141 -0
  525. package/src/time-series/studies/units.ts +243 -0
  526. package/src/time-series/studies/vwap.ts +274 -0
  527. package/src/time-series/types.ts +7 -0
  528. package/src/types/config.ts +18 -2
  529. package/src/types/data-provider.ts +23 -0
  530. package/src/types/financials.ts +40 -3
  531. package/src/types/headless.ts +42 -3
  532. package/src/types/plugin.ts +32 -3
  533. package/src/ui/host.tsx +17 -1
  534. package/src/ui/toast.tsx +26 -1
  535. package/src/utils/ascii-text.ts +140 -0
  536. package/src/utils/cli-output.ts +91 -18
  537. package/src/utils/concurrency.ts +24 -0
  538. package/src/utils/exchanges.ts +6 -0
  539. package/src/utils/external-url.ts +20 -7
  540. package/src/utils/financial-statements.ts +73 -16
  541. package/src/utils/fundamentals.ts +17 -5
  542. package/src/utils/futures-generic.ts +4 -4
  543. package/src/utils/http-proxy-upstream.ts +124 -0
  544. package/src/utils/lease-liabilities.ts +54 -0
  545. package/src/utils/main-thread-stall.ts +122 -5
  546. package/src/utils/perf-marks.ts +58 -12
  547. package/src/utils/price-history-integrity.ts +47 -1
  548. package/src/utils/private-files.ts +141 -0
  549. package/src/utils/public-address.ts +99 -0
  550. package/src/account/upgrade-personalization.ts +0 -57
  551. package/src/account/upgrade-tickers.ts +0 -124
  552. package/src/api-client/upgrade-experiment.ts +0 -91
  553. package/src/plugins/builtin/cloud/browser.tsx +0 -3
  554. package/src/time-series/studies.ts +0 -1628
@@ -1,7 +1,7 @@
1
1
  import { PluginStateStore } from "./plugin-state-store";
2
2
  import { ResourceStore } from "./resource-store";
3
3
  import { SessionStore } from "./session-store";
4
- import { SqliteDatabase } from "./sqlite/database";
4
+ import { SqliteDatabase, type SqliteDatabaseOptions } from "./sqlite/database";
5
5
  import { TickerStore } from "./ticker-store";
6
6
 
7
7
  export class AppPersistence {
@@ -11,15 +11,17 @@ export class AppPersistence {
11
11
  readonly pluginState: PluginStateStore;
12
12
  readonly sessions: SessionStore;
13
13
 
14
- constructor(dbPath: string) {
15
- this.database = new SqliteDatabase(dbPath);
16
- this.tickers = new TickerStore(this.database.connection);
17
- this.resources = new ResourceStore(this.database.connection);
18
- this.pluginState = new PluginStateStore(this.database.connection);
19
- this.sessions = new SessionStore(this.database.connection);
14
+ constructor(dbPath: string, options: SqliteDatabaseOptions = {}) {
15
+ this.database = new SqliteDatabase(dbPath, options);
16
+ const { connection, access } = this.database;
17
+ this.tickers = new TickerStore(connection, access);
18
+ this.resources = new ResourceStore(connection, access);
19
+ this.pluginState = new PluginStateStore(connection, access);
20
+ this.sessions = new SessionStore(connection, access);
20
21
  }
21
22
 
22
23
  close(): void {
24
+ this.resources.close();
23
25
  this.database.close();
24
26
  }
25
27
  }
@@ -1,6 +1,6 @@
1
1
  import { logFailure } from "../../../utils/log-failure";
2
2
  import { existsSync, readdirSync, type Dirent } from "fs";
3
- import { mkdir, readFile, rename, rm, writeFile } from "fs/promises";
3
+ import { readFile, rename, rm } from "fs/promises";
4
4
  import { dirname, join, resolve } from "path";
5
5
  import { expandUserHome, getGloomberbDirs } from "../home";
6
6
  import type { AppConfig } from "../../../types/config";
@@ -8,6 +8,8 @@ import { createDefaultConfig } from "../../../types/config";
8
8
  import { debugLog } from "../../../utils/debug-log";
9
9
  import { findAbsorbedCheckout, isDirectoryOrLink, isPluginDirectory } from "../../../plugins/loader";
10
10
  import { EXTRACTED_PLUGINS } from "../../../plugins/extracted-plugins";
11
+ import { makePrivateDir, writePrivateFile } from "../../../utils/private-files";
12
+ import { tightenProfilePermissions } from "../../profile-permissions";
11
13
  import type { ConfigMigrationHost } from "./migrations";
12
14
  import {
13
15
  normalizeConfigForSave,
@@ -113,11 +115,12 @@ const saveQueues = new Map<string, Promise<void>>();
113
115
  let tempFileCounter = 0;
114
116
 
115
117
  async function writeConfigFile(configPath: string, contents: string): Promise<void> {
116
- await mkdir(dirname(configPath), { recursive: true });
118
+ await makePrivateDir(dirname(configPath));
117
119
  // Unique per call: a clock tick is not, and two saves can land in one.
118
120
  const tempPath = `${configPath}.${process.pid}.${Date.now()}.${tempFileCounter++}.tmp`;
119
121
  try {
120
- await writeFile(tempPath, contents, "utf-8");
122
+ // Readable by the user alone: config.json holds broker credentials.
123
+ await writePrivateFile(tempPath, contents);
121
124
  await rename(tempPath, configPath);
122
125
  } catch (error) {
123
126
  await rm(tempPath, { force: true }).catch(() => { /* Intentional ignore: Remove a partial temporary file while preserving the write error rethrown below. */ });
@@ -149,11 +152,12 @@ export async function saveConfig(config: AppConfig): Promise<void> {
149
152
 
150
153
  export async function initDataDir(dataDir: string): Promise<AppConfig> {
151
154
  configLog.info(`Initializing data directory: ${dataDir}`);
152
- await mkdir(dataDir, { recursive: true });
155
+ await makePrivateDir(dataDir);
153
156
  const { config, needsSave } = await loadConfigState(dataDir);
154
157
  if (needsSave) {
155
158
  await saveConfig(config);
156
159
  }
160
+ await tightenProfilePermissions(dataDir, [configFileFor(dataDir), getGloomberbDirs().configFile]);
157
161
  return config;
158
162
  }
159
163
 
@@ -166,7 +170,9 @@ export async function resetAllData(dataDir: string): Promise<void> {
166
170
 
167
171
  export async function exportConfig(config: AppConfig, destPath: string): Promise<void> {
168
172
  const { dataDir, ...rest } = config;
169
- await writeFile(expandHomePath(destPath), JSON.stringify(rest, null, 2), "utf-8");
173
+ // The export carries broker credentials as config.json does: readable by the
174
+ // user alone, an existing file included, and never written through a link.
175
+ await writePrivateFile(expandHomePath(destPath), JSON.stringify(rest, null, 2));
170
176
  }
171
177
 
172
178
  export async function importConfig(dataDir: string, srcPath: string): Promise<AppConfig> {
@@ -30,6 +30,7 @@ import { DEFAULT_THEME, themes } from "../../../theme/themes";
30
30
  import { sanitizeStarPrompt } from "../../../app/star-prompt/model";
31
31
  import { sanitizeCommandBarOpens } from "../../../app/command-bar-intro";
32
32
  import { sanitizeUpdatesConfig } from "../../../updater/channel";
33
+ import { ATTENTION_NOTICE_ACCOUNTS } from "../../../telemetry/attention-setting";
33
34
 
34
35
  const configLog = debugLog.createLogger("config");
35
36
  const reportedUnknownThemes = new Set<string>();
@@ -201,17 +202,28 @@ function withUpdates(updates: AppConfig["updates"]): Pick<AppConfig, "updates">
201
202
  return updates ? { updates } : {};
202
203
  }
203
204
 
204
- /** Only the switches that are set survive; an empty object is the same as none. */
205
+ /**
206
+ * Only the switches that are set survive; an empty object is the same as none.
207
+ * Attention Counts is on by default, so a value that cannot be read turns it
208
+ * off instead: a hand-edited "false" or a broken block never counts as on.
209
+ */
205
210
  function sanitizeTelemetry(value: unknown): TelemetryConfig | undefined {
206
- if (!isRecord(value)) return undefined;
211
+ if (value === undefined) return undefined;
212
+ if (!isRecord(value)) return { attention: false };
207
213
  const telemetry: TelemetryConfig = {
208
214
  ...(typeof value.crashReports === "boolean" ? { crashReports: value.crashReports } : {}),
209
215
  ...(typeof value.usage === "boolean" ? { usage: value.usage } : {}),
210
- ...(typeof value.attention === "boolean" ? { attention: value.attention } : {}),
216
+ ...(value.attention !== undefined ? { attention: value.attention === true } : {}),
217
+ ...sanitizeAttentionNotice(value.attentionNoticeShownTo),
211
218
  };
212
219
  return Object.keys(telemetry).length > 0 ? telemetry : undefined;
213
220
  }
214
221
 
222
+ function sanitizeAttentionNotice(value: unknown): Pick<TelemetryConfig, "attentionNoticeShownTo"> {
223
+ const accounts = sanitizeUniqueStringList(value).slice(-ATTENTION_NOTICE_ACCOUNTS);
224
+ return accounts.length > 0 ? { attentionNoticeShownTo: accounts } : {};
225
+ }
226
+
215
227
  /**
216
228
  * Keeps the shape honest without judging the contents: a chord that does not
217
229
  * parse stays in the file and is reported when the table resolves, so a typo
@@ -286,6 +298,7 @@ function sanitizeOnboardingProgress(value: unknown): OnboardingProgress | undefi
286
298
  accountStatus,
287
299
  checkoutOpenedAt: typeof value.checkoutOpenedAt === "string" ? value.checkoutOpenedAt : undefined,
288
300
  desks: Array.isArray(value.desks) ? sanitizeUniqueStringList(value.desks) : undefined,
301
+ rerun: value.rerun === true ? true : undefined,
289
302
  };
290
303
  }
291
304
 
@@ -2,6 +2,7 @@ import { logFailure } from "../utils/log-failure";
2
2
  import type { CachePolicy } from "../types/persistence";
3
3
  import type { PluginPersistence } from "../types/plugin";
4
4
  import { CachedQuery, type CachedValue } from "./cached-query";
5
+ import { isTransientFailure } from "../api-client/errors";
5
6
 
6
7
  export interface PluginCacheResult<T> {
7
8
  data: T;
@@ -12,6 +13,20 @@ export interface PluginCacheResult<T> {
12
13
  source: "cache" | "network" | "stale-fallback";
13
14
  }
14
15
 
16
+ /**
17
+ * `result`, or the failure of the refresh it stands in for when a saved copy
18
+ * may not answer for it. A saved copy covers only a failure asking again may
19
+ * get past (`isTransientFailure`: no connection, a timeout, a rate-limited or
20
+ * overloaded server, a reply whose source did not answer this time); a
21
+ * refusal (another 4xx, such as a series no longer served or a signed-out
22
+ * session) and an answer that failed validation are thrown, so they show as
23
+ * the errors they are instead of as old data.
24
+ */
25
+ export function savedCopyForTransientFailure<R extends { source?: PluginCacheResult<unknown>["source"]; error?: unknown }>(result: R): R {
26
+ if (result.source === "stale-fallback" && !isTransientFailure(result.error)) throw result.error;
27
+ return result;
28
+ }
29
+
15
30
  /** Shared persistence/request lifecycle; keys, codecs, validation and policies stay with the plugin. */
16
31
  export function createPluginCache<T, Stored = T>(options: {
17
32
  kind: string;
@@ -1,6 +1,6 @@
1
1
  import type { Database } from "bun:sqlite";
2
+ import { createSqliteAccess, type SqliteAccess } from "./sqlite/access";
2
3
  import { safeParseJson, serializeJson } from "./sqlite/json";
3
- import { withSqliteBusyRetry } from "./sqlite/retry";
4
4
 
5
5
  const DEFAULT_PLUGIN_STATE_SCHEMA_VERSION = 1;
6
6
 
@@ -24,14 +24,39 @@ interface CachedPluginStateRecord {
24
24
  rawValue: string;
25
25
  }
26
26
 
27
+ /** A queued write's record (null for a delete), one object per write. */
28
+ interface PendingPluginState {
29
+ pluginId: string;
30
+ key: string;
31
+ record: CachedPluginStateRecord | null;
32
+ }
33
+
34
+ function stateKey(pluginId: string, key: string): string {
35
+ return `${pluginId}:${key}`;
36
+ }
37
+
38
+ /** A queued write's row, unambiguous where ids and keys hold colons. */
39
+ function rowKey(pluginId: string, key: string): string {
40
+ return `${pluginId}\u0000${key}`;
41
+ }
42
+
27
43
  export class PluginStateStore {
28
44
  private readonly cache = new Map<string, CachedPluginStateRecord>();
45
+ /** Writes queued behind another program's lock, read back until they land. */
46
+ private readonly pending = new Map<string, PendingPluginState>();
29
47
 
30
- constructor(private readonly db: Database) {}
48
+ constructor(
49
+ private readonly db: Database,
50
+ private readonly access: SqliteAccess = createSqliteAccess(db),
51
+ ) {}
31
52
 
32
53
  get<T>(pluginId: string, key: string, schemaVersion = DEFAULT_PLUGIN_STATE_SCHEMA_VERSION): PluginStateRecord<T> | null {
33
- const cacheKey = `${pluginId}:${key}`;
34
- const row = withSqliteBusyRetry("load plugin state", () => (
54
+ const cacheKey = stateKey(pluginId, key);
55
+ const pending = this.pending.get(rowKey(pluginId, key));
56
+ if (pending) {
57
+ return pending.record?.schemaVersion === schemaVersion ? pending.record as PluginStateRecord<T> : null;
58
+ }
59
+ const row = this.access.run("load plugin state", () => (
35
60
  this.db
36
61
  .query<{ schema_version: number; value: string; updated_at: number }, [string, string]>(
37
62
  "SELECT schema_version, value, updated_at FROM plugin_state WHERE plugin_id = ? AND key = ?",
@@ -77,46 +102,42 @@ export class PluginStateStore {
77
102
  setMany(entries: PluginStateSetEntry[]): void {
78
103
  if (entries.length === 0) return;
79
104
  const updatedAt = Date.now();
80
- const records = entries.map((entry) => ({
105
+ const writes: PendingPluginState[] = entries.map((entry) => ({
81
106
  pluginId: entry.pluginId,
82
107
  key: entry.key,
83
- value: entry.value,
84
- schemaVersion: entry.schemaVersion ?? DEFAULT_PLUGIN_STATE_SCHEMA_VERSION,
85
- rawValue: serializeJson(entry.value),
108
+ record: {
109
+ value: entry.value,
110
+ schemaVersion: entry.schemaVersion ?? DEFAULT_PLUGIN_STATE_SCHEMA_VERSION,
111
+ updatedAt,
112
+ rawValue: serializeJson(entry.value),
113
+ },
86
114
  }));
87
115
 
88
- withSqliteBusyRetry("save plugin state", () => {
116
+ this.write("save plugin state", writes, writes.length === 1, () => {
89
117
  const insert = this.db.query(
90
118
  `INSERT OR REPLACE INTO plugin_state (plugin_id, key, schema_version, value, updated_at)
91
119
  VALUES (?, ?, ?, ?, ?)`,
92
120
  );
93
121
  const tx = this.db.transaction(() => {
94
- for (const record of records) {
95
- insert.run(record.pluginId, record.key, record.schemaVersion, record.rawValue, updatedAt);
122
+ for (const { pluginId, key, record } of writes) {
123
+ insert.run(pluginId, key, record!.schemaVersion, record!.rawValue, updatedAt);
96
124
  }
97
125
  });
98
126
  tx();
99
127
  });
100
128
 
101
- for (const record of records) {
102
- this.cache.set(`${record.pluginId}:${record.key}`, {
103
- value: record.value,
104
- schemaVersion: record.schemaVersion,
105
- updatedAt,
106
- rawValue: record.rawValue,
107
- });
108
- }
129
+ for (const { pluginId, key, record } of writes) this.cache.set(stateKey(pluginId, key), record!);
109
130
  }
110
131
 
111
132
  delete(pluginId: string, key: string): void {
112
- withSqliteBusyRetry("delete plugin state", () => {
133
+ this.write("delete plugin state", [{ pluginId, key, record: null }], true, () => {
113
134
  this.db.query("DELETE FROM plugin_state WHERE plugin_id = ? AND key = ?").run(pluginId, key);
114
135
  });
115
- this.cache.delete(`${pluginId}:${key}`);
136
+ this.cache.delete(stateKey(pluginId, key));
116
137
  }
117
138
 
118
139
  keys(pluginId: string): string[] {
119
- return withSqliteBusyRetry("list plugin state keys", () => (
140
+ const stored = this.access.run("list plugin state keys", () => (
120
141
  this.db
121
142
  .query<{ key: string }, [string]>(
122
143
  "SELECT key FROM plugin_state WHERE plugin_id = ? ORDER BY key",
@@ -124,10 +145,18 @@ export class PluginStateStore {
124
145
  .all(pluginId)
125
146
  .map((row) => row.key)
126
147
  ));
148
+ if (this.pending.size === 0) return stored;
149
+ const keys = new Set(stored);
150
+ for (const entry of this.pending.values()) {
151
+ if (entry.pluginId !== pluginId) continue;
152
+ if (entry.record) keys.add(entry.key);
153
+ else keys.delete(entry.key);
154
+ }
155
+ return [...keys].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
127
156
  }
128
157
 
129
158
  pluginIds(): string[] {
130
- return withSqliteBusyRetry("list plugin state ids", () => (
159
+ const stored = this.access.run("list plugin state ids", () => (
131
160
  this.db
132
161
  .query<{ plugin_id: string }, []>(
133
162
  "SELECT DISTINCT plugin_id FROM plugin_state ORDER BY plugin_id",
@@ -135,10 +164,19 @@ export class PluginStateStore {
135
164
  .all()
136
165
  .map((row) => row.plugin_id)
137
166
  ));
167
+ if (this.pending.size === 0) return stored;
168
+ const ids = new Set(stored);
169
+ for (const { pluginId } of this.pending.values()) {
170
+ if (this.keys(pluginId).length > 0) ids.add(pluginId);
171
+ else ids.delete(pluginId);
172
+ }
173
+ return [...ids].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
138
174
  }
139
175
 
140
176
  clear(pluginId: string): void {
141
- withSqliteBusyRetry("clear plugin state", () => {
177
+ const cleared = this.keys(pluginId).map((key): PendingPluginState => ({ pluginId, key, record: null }));
178
+ // Deletes rows this store may not know of: never one row's write.
179
+ this.write("clear plugin state", cleared, false, () => {
142
180
  this.db.query("DELETE FROM plugin_state WHERE plugin_id = ?").run(pluginId);
143
181
  });
144
182
  for (const cacheKey of this.cache.keys()) {
@@ -147,4 +185,23 @@ export class PluginStateStore {
147
185
  }
148
186
  }
149
187
  }
188
+
189
+ /**
190
+ * Runs `statement` through the write queue, reading `entries` back until it
191
+ * lands. A write of exactly one row (`oneRow`) replaces a queued write of
192
+ * the same row.
193
+ */
194
+ private write(operationName: string, entries: PendingPluginState[], oneRow: boolean, statement: () => void): void {
195
+ for (const entry of entries) this.pending.set(rowKey(entry.pluginId, entry.key), entry);
196
+ const only = oneRow && entries.length === 1 ? entries[0] : undefined;
197
+ this.access.write(operationName, statement, {
198
+ ...(only ? { key: `plugin_state\u0000${rowKey(only.pluginId, only.key)}` } : {}),
199
+ settled: () => {
200
+ for (const entry of entries) {
201
+ const pendingKey = rowKey(entry.pluginId, entry.key);
202
+ if (this.pending.get(pendingKey) === entry) this.pending.delete(pendingKey);
203
+ }
204
+ },
205
+ });
206
+ }
150
207
  }
@@ -0,0 +1,47 @@
1
+ import { readdir } from "fs/promises";
2
+ import { dirname, join } from "path";
3
+ import { logFailure } from "../utils/log-failure";
4
+ import { restrictDir, restrictFile } from "../utils/private-files";
5
+
6
+ /** The data folder's files that hold the Cloud session and cache, or logs of what the user did. */
7
+ const PRIVATE_DATA_FILES = [
8
+ ".gloomberb-cache.db",
9
+ ".gloomberb-cache.db-wal",
10
+ ".gloomberb-cache.db-shm",
11
+ "stall-log.txt",
12
+ "stall-reports.json",
13
+ "crash-reports.json",
14
+ "desktop-view-ids.json",
15
+ ];
16
+ /** `remote-control.json` and one per app (`remote-control.tui.json`): each holds the remote-control token. */
17
+ const REMOTE_ENDPOINT_FILE = /^remote-control(?:\.[\w-]+)?\.json$/;
18
+
19
+ /**
20
+ * Tightens a profile made before its folders and files were created private,
21
+ * or under a umask that let others read them: the data folder and each
22
+ * config.json's folder open to the user alone, config.json (broker
23
+ * credentials) and the files above readable by the user alone. A step that
24
+ * fails (a folder another account owns, a disk without POSIX modes) is logged
25
+ * and the others still run; startup goes on either way. Nothing to do on
26
+ * Windows (see private-files.ts).
27
+ */
28
+ export async function tightenProfilePermissions(dataDir: string, configFiles: readonly string[]): Promise<void> {
29
+ if (process.platform === "win32") return;
30
+ const steps: Array<() => Promise<void>> = [
31
+ ...[...new Set([dataDir, ...configFiles.map((file) => dirname(file))])].map((dir) => () => restrictDir(dir)),
32
+ ...[...new Set(configFiles)].map((file) => () => restrictFile(file)),
33
+ ...PRIVATE_DATA_FILES.map((name) => () => restrictFile(join(dataDir, name))),
34
+ async () => {
35
+ for (const name of await readdir(dataDir)) {
36
+ if (REMOTE_ENDPOINT_FILE.test(name)) await restrictFile(join(dataDir, name));
37
+ }
38
+ },
39
+ ];
40
+ for (const step of steps) {
41
+ try {
42
+ await step();
43
+ } catch (error) {
44
+ logFailure("data.profile-permissions", "Restricting a profile file to its user failed")(error);
45
+ }
46
+ }
47
+ }