@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.
- package/README.md +7 -1
- package/package.json +1 -1
- package/patches/electrobun@1.18.1.patch +108 -1
- package/src/account/upgrade-dialog.tsx +4 -23
- package/src/api-client/coalesce.ts +118 -0
- package/src/api-client/credit-boards.ts +13 -0
- package/src/api-client/crypto-etf-flows.ts +6 -0
- package/src/api-client/crypto-markets.ts +40 -0
- package/src/api-client/data.ts +201 -14
- package/src/api-client/equity-screener.ts +10 -0
- package/src/api-client/errors.ts +20 -0
- package/src/api-client/gpu.ts +29 -0
- package/src/api-client/index.ts +5 -2
- package/src/api-client/plan-access.ts +10 -1
- package/src/api-client/plan-rules.ts +35 -0
- package/src/api-client/research-activity.ts +1 -32
- package/src/api-client/types.ts +21 -1
- package/src/app/attention-notice.tsx +94 -0
- package/src/app.tsx +23 -1
- package/src/architecture/core-builtin-allowlist.json +2 -1
- package/src/capabilities/factories.ts +1 -0
- package/src/cli/ascii-output.ts +42 -0
- package/src/cli/base-converter.ts +10 -2
- package/src/cli/command-options.ts +16 -1
- package/src/cli/commands/crypto-hints.ts +23 -4
- package/src/cli/commands/market.ts +92 -23
- package/src/cli/commands/news-search.ts +24 -5
- package/src/cli/commands/overview.ts +9 -4
- package/src/cli/commands/search.ts +71 -12
- package/src/cli/commands/system.ts +22 -30
- package/src/cli/commands/ticker.ts +49 -9
- package/src/cli/commands/version.ts +33 -0
- package/src/cli/context.ts +5 -9
- package/src/cli/data-dir.ts +7 -0
- package/src/cli/desktop-pane-shot-api-policy.ts +85 -0
- package/src/cli/desktop-pane-shot-browser.ts +320 -0
- package/src/cli/desktop-pane-shot-routes.ts +26 -1
- package/src/cli/desktop-pane-shot.ts +196 -256
- package/src/cli/early-command.ts +42 -0
- package/src/cli/earnings-calendar.ts +9 -0
- package/src/cli/entry.ts +19 -13
- package/src/cli/errors.ts +12 -2
- package/src/cli/help.ts +1 -1
- package/src/cli/history-export.ts +22 -2
- package/src/cli/history-rows.ts +154 -4
- package/src/cli/index.ts +18 -1
- package/src/cli/known-gaps.ts +40 -5
- package/src/cli/listing-arg.ts +46 -9
- package/src/cli/options.ts +1 -1
- package/src/cli/output-file.ts +60 -13
- package/src/cli/pane-functions/catalog.ts +14 -3
- package/src/cli/pane-functions/data.ts +1 -1
- package/src/cli/pane-functions/dom.ts +45 -0
- package/src/cli/pane-functions/freshness.ts +46 -9
- package/src/cli/pane-functions/function-help.ts +17 -9
- package/src/cli/pane-functions/headless.ts +22 -14
- package/src/cli/pane-functions/index.ts +6 -2
- package/src/cli/pane-functions/options.ts +6 -2
- package/src/cli/pane-functions/resolver.ts +27 -5
- package/src/cli/pane-functions/screenshot-assessment.ts +33 -0
- package/src/cli/pane-functions/screenshot-bridge.ts +94 -13
- package/src/cli/pane-functions/screenshot-http.ts +70 -0
- package/src/cli/pane-functions/screenshot-render.ts +19 -5
- package/src/cli/quote-answer.ts +20 -0
- package/src/cli/registry.ts +41 -13
- package/src/cli/result.ts +12 -6
- package/src/cli/ticker-resolution.ts +2 -1
- package/src/cli/types.ts +2 -0
- package/src/components/chart/composite/format.ts +3 -0
- package/src/components/chart-table/header.tsx +1 -1
- package/src/components/chart-table/layout.ts +28 -11
- package/src/components/command-bar/action-runtime.ts +4 -0
- package/src/components/command-bar/commands/direct/index.ts +18 -33
- package/src/components/command-bar/commands/direct/runtime.ts +5 -0
- package/src/components/command-bar/commands/registry.ts +10 -1
- package/src/components/command-bar/routes/root/command-items.ts +9 -4
- package/src/components/command-bar/routes/root/runtime.ts +24 -1
- package/src/components/command-bar/surface/index.tsx +9 -3
- package/src/components/company-logo.tsx +2 -1
- package/src/components/layout/header.tsx +8 -3
- package/src/components/layout/market-summary.ts +25 -7
- package/src/components/layout/pane/footer/index.tsx +7 -2
- package/src/components/layout/pane/footer/model.ts +40 -5
- package/src/components/market-board/index.tsx +3 -2
- package/src/components/onboarding/desks-step.tsx +12 -4
- package/src/components/onboarding/first-run-workspace.ts +81 -4
- package/src/components/onboarding/onboarding-frame.tsx +2 -1
- package/src/components/onboarding/onboarding-keyboard.ts +8 -5
- package/src/components/onboarding/onboarding-wizard.tsx +54 -23
- package/src/components/onboarding/portfolio-step/positions-panel.tsx +7 -2
- package/src/components/onboarding/run-setup-again.ts +24 -0
- package/src/components/onboarding/use-onboarding-progress.ts +33 -15
- package/src/components/onboarding/use-onboarding-upgrade.ts +2 -6
- package/src/components/onboarding/wizard-model.ts +57 -3
- package/src/components/onboarding/wizard-positions.ts +1 -1
- package/src/components/ui/delayed-mark.tsx +56 -0
- package/src/components/ui/external-link.tsx +3 -6
- package/src/components/ui/icon.tsx +8 -1
- package/src/components/ui/logo-mark.tsx +8 -5
- package/src/components/ui/stat-grid.tsx +263 -19
- package/src/data/config/store/node.ts +11 -5
- package/src/data/config/store/normalize.ts +16 -3
- package/src/data/profile-permissions.ts +47 -0
- package/src/data/resource-store.ts +34 -31
- package/src/data/sqlite/access.ts +19 -8
- package/src/data/sqlite/checkpointer.ts +95 -8
- package/src/data/sqlite/database.ts +40 -7
- package/src/docs/snapshot.json +191 -78
- package/src/i18n/es.ts +18 -2
- package/src/i18n/ja.ts +18 -2
- package/src/i18n/ko.ts +18 -2
- package/src/i18n/zh-cn.ts +18 -2
- package/src/i18n/zh-tw.ts +18 -2
- package/src/layout/desk-keys.ts +4 -1
- package/src/layout/desks.ts +38 -5
- package/src/layout-marketplace/gallery-desktop.tsx +45 -16
- package/src/market-data/batched-reads.ts +16 -0
- package/src/market-data/coordinator/auxiliary.ts +4 -2
- package/src/market-data/coordinator/index.ts +11 -2
- package/src/market-data/futures-board.ts +1 -1
- package/src/market-data/futures-contract.ts +3 -1
- package/src/market-data/market/index-level.ts +6 -4
- package/src/market-data/published-apac-sessions.ts +6 -0
- package/src/news/aggregator.ts +7 -4
- package/src/news/news-model.ts +11 -0
- package/src/news/story-id.ts +22 -0
- package/src/news/ticker-symbols.ts +17 -1
- package/src/notifications/app-notifier.ts +35 -14
- package/src/plugins/builtin/account-management/form-components.tsx +1 -1
- package/src/plugins/builtin/account-management/index.tsx +2 -3
- package/src/plugins/builtin/account-management/model.ts +1 -1
- package/src/plugins/builtin/alerts/cli-forms.ts +15 -1
- package/src/plugins/builtin/alerts/events-pane.tsx +7 -5
- package/src/plugins/builtin/alerts/events.ts +10 -2
- package/src/plugins/builtin/alerts/index.tsx +5 -5
- package/src/plugins/builtin/alerts/research-builder.ts +5 -1
- package/src/plugins/builtin/alerts/research-rules.ts +18 -0
- package/src/plugins/builtin/alerts/targets.ts +4 -1
- package/src/plugins/builtin/attention/headless.ts +1 -1
- package/src/plugins/builtin/attention/index.tsx +2 -4
- package/src/plugins/builtin/attention/listing.ts +1 -1
- package/src/plugins/builtin/attention/series.ts +1 -1
- package/src/plugins/builtin/awards/index.tsx +1 -2
- package/src/plugins/builtin/backtest/client.ts +1 -0
- package/src/plugins/builtin/backtest/index.tsx +1 -2
- package/src/plugins/builtin/backtest/rules.ts +3 -1
- package/src/plugins/builtin/backtest/window.ts +8 -7
- package/src/plugins/builtin/broker-manager/detail.tsx +16 -3
- package/src/plugins/builtin/broker-manager/index.tsx +1 -1
- package/src/plugins/builtin/buildout/cells.tsx +5 -0
- package/src/plugins/builtin/buildout/company/flow.tsx +367 -0
- package/src/plugins/builtin/buildout/company/id.ts +2 -0
- package/src/plugins/builtin/buildout/company/model.ts +247 -0
- package/src/plugins/builtin/buildout/company/pane.tsx +317 -0
- package/src/plugins/builtin/buildout/data-runtime.ts +31 -10
- package/src/plugins/builtin/buildout/facilities.ts +309 -0
- package/src/plugins/builtin/buildout/format.ts +9 -0
- package/src/plugins/builtin/buildout/index.tsx +51 -5
- package/src/plugins/builtin/buildout/model/index.ts +62 -10
- package/src/plugins/builtin/buildout/model/normalizers.ts +5 -0
- package/src/plugins/builtin/buildout/model/types.ts +11 -2
- package/src/plugins/builtin/buildout/pane/index.tsx +91 -20
- package/src/plugins/builtin/buildout/pane/status.tsx +1 -1
- package/src/plugins/builtin/buildout/pane/themes.tsx +110 -0
- package/src/plugins/builtin/buildout/table-model.ts +26 -2
- package/src/plugins/builtin/builtin-plugin-meta.ts +16 -0
- package/src/plugins/builtin/cds/headless.ts +10 -2
- package/src/plugins/builtin/cds/model.ts +7 -3
- package/src/plugins/builtin/central-bank-rates/index.tsx +1 -2
- package/src/plugins/builtin/central-bank-rates/pane.tsx +2 -1
- package/src/plugins/builtin/changelog/index.tsx +3 -2
- package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -2
- package/src/plugins/builtin/chart-composer/headless.ts +88 -21
- package/src/plugins/builtin/chart-composer/index.tsx +2 -3
- package/src/plugins/builtin/chart-composer/pane.tsx +26 -2
- package/src/plugins/builtin/chart-composer/series-expression.ts +3 -2
- package/src/plugins/builtin/chart-composer/studies.ts +1 -1
- package/src/plugins/builtin/chat/controller/index.ts +1 -1
- package/src/plugins/builtin/chat/controller/messages.ts +15 -4
- package/src/plugins/builtin/chat/ghost-user.ts +8 -0
- package/src/plugins/builtin/chat/index.tsx +5 -5
- package/src/plugins/builtin/clinical-trials/index.tsx +1 -2
- package/src/plugins/builtin/cloud/company-picker.tsx +1 -1
- package/src/plugins/builtin/cloud/index.tsx +1 -2
- package/src/plugins/builtin/cloud/plugin.tsx +6 -4
- package/src/plugins/builtin/cloud/team/module.tsx +1 -1
- package/src/plugins/builtin/cloud/thesis/module.tsx +1 -1
- package/src/plugins/builtin/cloud/trial-offer-status-widget.tsx +24 -1
- package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +39 -15
- package/src/plugins/builtin/comment-letters/index.tsx +1 -2
- package/src/plugins/builtin/commodity-balances/index.tsx +3 -3
- package/src/plugins/builtin/commodity-balances/pane.tsx +2 -2
- package/src/plugins/builtin/congress-trades/aggregates.ts +13 -2
- package/src/plugins/builtin/congress-trades/detail.tsx +20 -4
- package/src/plugins/builtin/congress-trades/headless.ts +2 -0
- package/src/plugins/builtin/congress-trades/model.ts +92 -64
- package/src/plugins/builtin/congress-trades/table.tsx +14 -2
- package/src/plugins/builtin/connections/index.ts +2 -3
- package/src/plugins/builtin/cot/index.tsx +1 -2
- package/src/plugins/builtin/cpi/index.tsx +1 -2
- package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
- package/src/plugins/builtin/credit-boards/client.ts +7 -2
- package/src/plugins/builtin/credit-boards/headless.ts +25 -12
- package/src/plugins/builtin/credit-boards/model.ts +29 -0
- package/src/plugins/builtin/credit-boards/sovr-pane.tsx +9 -4
- package/src/plugins/builtin/crypto-board/bitcoin-network.ts +143 -0
- package/src/plugins/builtin/crypto-board/headless.ts +4 -3
- package/src/plugins/builtin/crypto-board/index.tsx +4 -2
- package/src/plugins/builtin/crypto-board/series.ts +2 -0
- package/src/plugins/builtin/crypto-etf-flows/client.ts +9 -2
- package/src/plugins/builtin/crypto-etf-flows/headless.ts +61 -6
- package/src/plugins/builtin/crypto-etf-flows/index.tsx +1 -2
- package/src/plugins/builtin/crypto-etf-flows/model.ts +14 -1
- package/src/plugins/builtin/crypto-etf-flows/pane.tsx +8 -5
- package/src/plugins/builtin/debt-maturities/headless.ts +4 -3
- package/src/plugins/builtin/debt-maturities/model.ts +17 -1
- package/src/plugins/builtin/debt-maturities/pane.tsx +18 -4
- package/src/plugins/builtin/debug/index.tsx +1 -1
- package/src/plugins/builtin/dividend-yield/client.ts +32 -2
- package/src/plugins/builtin/dividend-yield/headless.ts +58 -33
- package/src/plugins/builtin/dividend-yield/index.tsx +37 -2
- package/src/plugins/builtin/dividend-yield/pane.tsx +32 -13
- package/src/plugins/builtin/dividend-yield/ranking-reads.ts +73 -0
- package/src/plugins/builtin/dividend-yield/ranking-view.tsx +163 -0
- package/src/plugins/builtin/dividend-yield/ranking.ts +1 -1
- package/src/plugins/builtin/dividend-yield/view.ts +21 -0
- package/src/plugins/builtin/docs/index.tsx +2 -3
- package/src/plugins/builtin/doe/headless.ts +16 -3
- package/src/plugins/builtin/doe/index.tsx +5 -4
- package/src/plugins/builtin/earnings/board.tsx +2 -1
- package/src/plugins/builtin/earnings/history.tsx +2 -1
- package/src/plugins/builtin/earnings-ripple/client.ts +32 -7
- package/src/plugins/builtin/earnings-ripple/index.tsx +1 -2
- package/src/plugins/builtin/econ/calendar-model.ts +29 -6
- package/src/plugins/builtin/econ-statistics/index.tsx +1 -2
- package/src/plugins/builtin/equity-screener/client.ts +1 -1
- package/src/plugins/builtin/equity-screener/headless.ts +1 -0
- package/src/plugins/builtin/equity-screener/ideas.ts +30 -1
- package/src/plugins/builtin/equity-screener/index.tsx +1 -2
- package/src/plugins/builtin/equity-screener/model.ts +28 -4
- package/src/plugins/builtin/equity-screener/pane.tsx +5 -0
- package/src/plugins/builtin/equity-screener/screen-check.ts +23 -1
- package/src/plugins/builtin/estimate-revisions/index.tsx +1 -2
- package/src/plugins/builtin/exposure/holdings.ts +35 -13
- package/src/plugins/builtin/exposure/index.tsx +1 -2
- package/src/plugins/builtin/fear-greed/cli.ts +44 -0
- package/src/plugins/builtin/fear-greed/index.tsx +5 -53
- package/src/plugins/builtin/fear-greed/pane.tsx +2 -1
- package/src/plugins/builtin/fixed-income/index.tsx +1 -2
- package/src/plugins/builtin/futures/board-reads.ts +25 -0
- package/src/plugins/builtin/futures/coalesced-bump.ts +44 -0
- package/src/plugins/builtin/futures/index.tsx +15 -0
- package/src/plugins/builtin/futures/model.ts +14 -0
- package/src/plugins/builtin/futures/use-board-terms.ts +22 -15
- package/src/plugins/builtin/futures/use-front-returns.ts +20 -14
- package/src/plugins/builtin/futures-curve/headless.ts +6 -1
- package/src/plugins/builtin/futures-curve/index.tsx +1 -2
- package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
- package/src/plugins/builtin/gpu/board.tsx +51 -12
- package/src/plugins/builtin/gpu/client.ts +15 -2
- package/src/plugins/builtin/gpu/equities.tsx +5 -2
- package/src/plugins/builtin/gpu/headless.ts +14 -2
- package/src/plugins/builtin/gpu/index.tsx +1 -2
- package/src/plugins/builtin/gpu/model.ts +20 -2
- package/src/plugins/builtin/gpu/pane.tsx +1 -1
- package/src/plugins/builtin/help/index.tsx +1 -1
- package/src/plugins/builtin/holders/index-holders.ts +24 -12
- package/src/plugins/builtin/holders/index.tsx +2 -3
- package/src/plugins/builtin/ipo-calendar/index.tsx +1 -2
- package/src/plugins/builtin/iv-history/index.tsx +1 -2
- package/src/plugins/builtin/jodi/index.tsx +1 -2
- package/src/plugins/builtin/kelly-sizer/index.tsx +1 -2
- package/src/plugins/builtin/layout-manager/index.tsx +9 -8
- package/src/plugins/builtin/macro-day/pane.tsx +4 -2
- package/src/plugins/builtin/market-halts/index.tsx +1 -2
- package/src/plugins/builtin/market-heatmap/index.tsx +11 -2
- package/src/plugins/builtin/market-movers/index.tsx +7 -1
- package/src/plugins/builtin/market-movers/session-body.tsx +4 -1
- package/src/plugins/builtin/market-valuation/index.tsx +1 -2
- package/src/plugins/builtin/members/index.tsx +1 -2
- package/src/plugins/builtin/money-markets/index.tsx +1 -2
- package/src/plugins/builtin/money-markets/pane.tsx +2 -1
- package/src/plugins/builtin/news/headless.ts +7 -0
- package/src/plugins/builtin/news/wire/index.tsx +1 -1
- package/src/plugins/builtin/news/wire/news/pop-out.tsx +9 -3
- package/src/plugins/builtin/notes/index.tsx +3 -6
- package/src/plugins/builtin/openfda/index.tsx +1 -2
- package/src/plugins/builtin/options/view.tsx +6 -5
- package/src/plugins/builtin/options-calculator/index.tsx +1 -2
- package/src/plugins/builtin/options-positioning/index.tsx +1 -2
- package/src/plugins/builtin/options-scenario/index.tsx +2 -4
- package/src/plugins/builtin/perps/index.tsx +1 -2
- package/src/plugins/builtin/plugin-marketplace/index.tsx +2 -3
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +2 -1
- package/src/plugins/builtin/plugin-module.ts +16 -2
- package/src/plugins/builtin/portfolio-list/cli/render.ts +53 -3
- package/src/plugins/builtin/portfolio-list/index.tsx +1 -2
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -0
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -2
- package/src/plugins/builtin/power/index.tsx +1 -2
- package/src/plugins/builtin/rate-path/index.tsx +1 -2
- package/src/plugins/builtin/realized-vol/headless.ts +2 -0
- package/src/plugins/builtin/realized-vol/index.tsx +1 -2
- package/src/plugins/builtin/realized-vol/model.ts +16 -2
- package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
- package/src/plugins/builtin/relative-rotation/index.tsx +1 -2
- package/src/plugins/builtin/research/analyst-headless.ts +33 -6
- package/src/plugins/builtin/research/analyst-model.ts +37 -2
- package/src/plugins/builtin/research/analyst-pane.tsx +25 -5
- package/src/plugins/builtin/research/index.tsx +5 -8
- package/src/plugins/builtin/research-search/index.tsx +1 -2
- package/src/plugins/builtin/revenue-breakdown/headless.ts +22 -8
- package/src/plugins/builtin/revenue-breakdown/model.ts +23 -0
- package/src/plugins/builtin/reverse-dcf/pane.tsx +4 -2
- package/src/plugins/builtin/roadmap/index.tsx +2 -3
- package/src/plugins/builtin/roadmap/pane.tsx +49 -2
- package/src/plugins/builtin/scanner/hilo-model.ts +18 -1
- package/src/plugins/builtin/scanner/hilo-pane.tsx +12 -2
- package/src/plugins/builtin/scanner/live-pane.tsx +7 -4
- package/src/plugins/builtin/sec/filing-content.ts +38 -6
- package/src/plugins/builtin/sec/headless.ts +5 -1
- package/src/plugins/builtin/sec/index.tsx +4 -2
- package/src/plugins/builtin/sec/model.ts +2 -0
- package/src/plugins/builtin/sectors/index.tsx +11 -5
- package/src/plugins/builtin/series-inventory.ts +2 -0
- package/src/plugins/builtin/shared/cloud-resource.ts +60 -0
- package/src/plugins/builtin/shared/cloud-upgrade.ts +24 -1
- package/src/plugins/builtin/shared/forming-bars.ts +29 -0
- package/src/plugins/builtin/shared/fx-rate-freshness.ts +17 -0
- package/src/plugins/builtin/shared/series-detail-chart.tsx +4 -2
- package/src/plugins/builtin/shared/use-quote-board.ts +5 -0
- package/src/plugins/builtin/short-interest/publications-pane.tsx +2 -1
- package/src/plugins/builtin/short-volume/pane.tsx +2 -1
- package/src/plugins/builtin/social-mentions/index.tsx +1 -2
- package/src/plugins/builtin/social-mentions/pane.tsx +2 -1
- package/src/plugins/builtin/supply-chain/client.ts +22 -1
- package/src/plugins/builtin/supply-chain/graph-client.ts +22 -1
- package/src/plugins/builtin/supply-chain/index.tsx +1 -2
- package/src/plugins/builtin/thirteenf/api.ts +116 -13
- package/src/plugins/builtin/thirteenf/data.ts +132 -30
- package/src/plugins/builtin/thirteenf/index.tsx +2 -4
- package/src/plugins/builtin/thirteenf/model.ts +2 -0
- package/src/plugins/builtin/thirteenf/pane.tsx +20 -11
- package/src/plugins/builtin/thirteenf/types.ts +12 -0
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +17 -4
- package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +81 -11
- package/src/plugins/builtin/ticker-detail/headless.ts +11 -3
- package/src/plugins/builtin/ticker-detail/index.tsx +5 -10
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +16 -6
- package/src/plugins/builtin/time-sales/index.tsx +1 -2
- package/src/plugins/builtin/time-sales/pane.tsx +3 -1
- package/src/plugins/builtin/trace-bonds/headless.ts +11 -2
- package/src/plugins/builtin/trace-bonds/model.ts +44 -10
- package/src/plugins/builtin/trace-bonds/pane.tsx +12 -7
- package/src/plugins/builtin/treasury-auctions/pane.tsx +2 -2
- package/src/plugins/builtin/vol-surface/client.ts +18 -3
- package/src/plugins/builtin/vol-surface/index.tsx +1 -2
- package/src/plugins/builtin/volatility/index.tsx +1 -2
- package/src/plugins/builtin/world-indices/footer.ts +6 -1
- package/src/plugins/builtin/world-venue-map/client.ts +10 -1
- package/src/plugins/builtin/world-venue-map/index.ts +6 -2
- package/src/plugins/builtin/world-venue-map/layered-pane.tsx +48 -7
- package/src/plugins/builtin/world-venue-map/layers.ts +17 -0
- package/src/plugins/builtin/world-venue-map/local-layers.ts +141 -0
- package/src/plugins/builtin/world-venue-map/map-symbols.ts +18 -0
- package/src/plugins/builtin/world-venue-map/map.tsx +4 -2
- package/src/plugins/builtin/world-venue-map/scope.ts +63 -0
- package/src/plugins/builtin/yield-curve/curves.ts +16 -2
- package/src/plugins/builtin/yield-curve/headless.ts +3 -3
- package/src/plugins/bundle.ts +52 -2
- package/src/plugins/catalog-browser.ts +2 -2
- package/src/plugins/catalog-cli.ts +20 -0
- package/src/plugins/host-link.ts +13 -1
- package/src/plugins/installer.ts +15 -4
- package/src/plugins/loader.ts +43 -11
- package/src/plugins/peer-imports.ts +201 -0
- package/src/plugins/peer-retry.ts +26 -29
- package/src/plugins/plugin-process.ts +128 -0
- package/src/plugins/redirection-guard.ts +45 -0
- package/src/remote/endpoint.ts +16 -3
- package/src/renderers/browser/main.tsx +3 -2
- package/src/renderers/cloudflare/http-proxy.ts +7 -123
- package/src/renderers/dom/desktop/icons.tsx +2 -0
- package/src/renderers/dom/host/image-surface.tsx +46 -21
- package/src/renderers/dom/styles.css +7 -0
- package/src/renderers/dom/toast-host.tsx +2 -0
- package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
- package/src/renderers/electrobun/bun/desktop/initialization.ts +3 -2
- package/src/renderers/electrobun/bun/desktop/rpc-timing.ts +88 -0
- package/src/renderers/electrobun/bun/desktop/update.ts +20 -4
- package/src/renderers/electrobun/bun/desktop/view-ids.ts +8 -4
- package/src/renderers/electrobun/bun/external-plugins.ts +4 -0
- package/src/renderers/electrobun/bun/window/create.ts +4 -2
- package/src/renderers/electrobun/bun/window/native-timing.ts +48 -0
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +82 -4
- package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +13 -5
- package/src/renderers/electrobun/view/desktop/window/snapshot.ts +3 -0
- package/src/renderers/electrobun/view/main.tsx +7 -2
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
- package/src/renderers/opentui/browser-open.ts +10 -4
- package/src/renderers/opentui/image/surface.tsx +1 -1
- package/src/renderers/opentui/start.tsx +12 -6
- package/src/renderers/opentui/toast-host.tsx +2 -1
- package/src/series/registry.ts +18 -0
- package/src/series/standalone.ts +10 -1
- package/src/series/types.ts +7 -0
- package/src/series/validate.ts +11 -2
- package/src/sources/gloomberb-cloud/index.ts +28 -5
- package/src/sources/gloomberb-cloud/news.ts +24 -3
- package/src/sources/gloomberb-cloud/normalizers.ts +17 -2
- package/src/sources/provider-errors.ts +26 -3
- package/src/sources/provider-router/history.ts +5 -0
- package/src/sources/provider-router/index.ts +13 -0
- package/src/sync/core-contributors.ts +32 -0
- package/src/telemetry/attention-counts.ts +64 -6
- package/src/telemetry/attention-setting.ts +33 -0
- package/src/telemetry/crash-reports-node.ts +3 -1
- package/src/telemetry/stall-clocks.ts +93 -0
- package/src/telemetry/stall-reports-node.ts +12 -7
- package/src/telemetry/stall-reports.ts +34 -5
- package/src/test-support/preload.ts +9 -0
- package/src/tickers/company-name.ts +19 -0
- package/src/tickers/listing-fate.ts +14 -3
- package/src/tickers/saved-instrument.ts +17 -0
- package/src/tickers/search/home-listing.ts +13 -2
- package/src/tickers/search/index.ts +4 -1
- package/src/tickers/search/types.ts +6 -1
- package/src/time-series/bar-aggregation.ts +7 -1
- package/src/time-series/base-series.ts +3 -3
- package/src/time-series/chart-bounds.ts +1 -1
- package/src/time-series/chart-data.ts +5 -2
- package/src/time-series/chart-seed.ts +1 -1
- package/src/time-series/coin-session-close.ts +211 -0
- package/src/time-series/live-bars.ts +13 -2
- package/src/time-series/live-quotes.ts +1 -1
- package/src/time-series/official-close.ts +159 -0
- package/src/time-series/price-comparison.ts +55 -6
- package/src/time-series/resolve.ts +12 -3
- package/src/time-series/series-presentation.ts +1 -1
- package/src/time-series/studies/formula.ts +205 -0
- package/src/time-series/studies/indicators.ts +299 -0
- package/src/time-series/studies/leverage.ts +75 -0
- package/src/time-series/studies/pair.ts +138 -0
- package/src/time-series/studies/resolve.ts +333 -0
- package/src/time-series/studies/shared.ts +141 -0
- package/src/time-series/studies/units.ts +243 -0
- package/src/time-series/studies/vwap.ts +274 -0
- package/src/types/config.ts +18 -2
- package/src/types/data-provider.ts +23 -0
- package/src/types/financials.ts +22 -0
- package/src/types/headless.ts +42 -3
- package/src/types/plugin.ts +32 -3
- package/src/ui/host.tsx +11 -0
- package/src/ui/toast.tsx +26 -1
- package/src/utils/ascii-text.ts +140 -0
- package/src/utils/cli-output.ts +91 -18
- package/src/utils/concurrency.ts +24 -0
- package/src/utils/exchanges.ts +6 -0
- package/src/utils/external-url.ts +20 -7
- package/src/utils/futures-generic.ts +4 -4
- package/src/utils/http-proxy-upstream.ts +124 -0
- package/src/utils/main-thread-stall.ts +75 -3
- package/src/utils/perf-marks.ts +36 -2
- package/src/utils/private-files.ts +141 -0
- package/src/utils/public-address.ts +99 -0
- package/src/account/upgrade-personalization.ts +0 -57
- package/src/account/upgrade-tickers.ts +0 -124
- package/src/api-client/upgrade-experiment.ts +0 -91
- package/src/plugins/builtin/cloud/browser.tsx +0 -3
- package/src/renderers/electrobun/launcher/preload.ts +0 -13
- package/src/renderers/electrobun/launcher/windows-emulation.ts +0 -50
- package/src/time-series/studies.ts +0 -1628
package/src/plugins/installer.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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" },
|
package/src/plugins/loader.ts
CHANGED
|
@@ -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,
|
|
303
|
-
* process wrote it before, which `bun
|
|
304
|
-
* many loads ran at once,
|
|
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
|
|
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
|
-
|
|
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
|
-
`
|
|
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:
|
|
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:
|
|
388
|
+
return { module, detail: describePeerLinkState(pluginDir, peer, "retry=loaded", relinked) };
|
|
362
389
|
} catch (error) {
|
|
363
|
-
return { error, detail:
|
|
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,
|
|
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
|
|
76
|
-
* again. Bun drops a folder from its resolver cache
|
|
77
|
-
* misses, so this looks up a file that is not there in
|
|
78
|
-
* bundler shares the cache, which is why the desktop's compile
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
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
|
+
}
|