@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.
- package/README.md +7 -1
- package/package.json +2 -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 +58 -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/runtime/startup.ts +3 -1
- package/src/app.tsx +23 -1
- package/src/architecture/core-builtin-allowlist.json +2 -1
- package/src/capabilities/chart-series.ts +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 +101 -27
- package/src/cli/commands/news-search.ts +24 -5
- package/src/cli/commands/overview.ts +14 -10
- package/src/cli/commands/search.ts +84 -25
- 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 +6 -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/glossary.ts +2 -1
- package/src/cli/help.ts +1 -1
- package/src/cli/history-export.ts +22 -2
- package/src/cli/history-rows.ts +189 -11
- package/src/cli/index.ts +18 -1
- package/src/cli/known-gaps.ts +51 -5
- package/src/cli/listing-arg.ts +63 -10
- 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 +49 -6
- 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 +51 -6
- package/src/components/command-bar/selection-runtime.ts +12 -29
- package/src/components/command-bar/surface/index.tsx +9 -3
- package/src/components/company-logo.tsx +2 -1
- package/src/components/layout/detached-pane-shell.tsx +2 -2
- package/src/components/layout/header.tsx +10 -5
- package/src/components/layout/market-summary.ts +25 -7
- package/src/components/layout/pane/footer/index.tsx +8 -2
- package/src/components/layout/pane/footer/model.ts +73 -6
- package/src/components/layout/pane/header.tsx +2 -2
- package/src/components/layout/pane/status-footer.ts +2 -1
- 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 +4 -2
- package/src/components/onboarding/onboarding-keyboard.ts +8 -5
- package/src/components/onboarding/onboarding-wizard.tsx +60 -26
- 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/display.tsx +16 -5
- package/src/components/ui/external-link.tsx +3 -6
- package/src/components/ui/icon.tsx +16 -3
- package/src/components/ui/logo-mark.tsx +8 -5
- package/src/components/ui/stat-grid.tsx +263 -19
- package/src/core/app-services.ts +11 -2
- package/src/data/app-persistence.ts +9 -7
- package/src/data/config/store/node.ts +11 -5
- package/src/data/config/store/normalize.ts +16 -3
- package/src/data/plugin-cache.ts +15 -0
- package/src/data/plugin-state-store.ts +81 -24
- package/src/data/profile-permissions.ts +47 -0
- package/src/data/resource-store.ts +165 -90
- package/src/data/session-store.ts +31 -8
- package/src/data/sqlite/access.ts +324 -0
- package/src/data/sqlite/checkpointer.ts +285 -0
- package/src/data/sqlite/database.ts +80 -4
- package/src/data/sqlite/retry.ts +50 -9
- package/src/data/ticker-store.ts +57 -23
- 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/market-data/realized-volatility.ts +4 -2
- 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/auto-update-node.ts +10 -6
- 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/central-bank-rates/series.ts +2 -3
- package/src/plugins/builtin/changelog/index.tsx +3 -2
- package/src/plugins/builtin/chart-composer/cli-options.ts +16 -0
- package/src/plugins/builtin/chart-composer/headless-schema.ts +10 -0
- 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 +30 -2
- package/src/plugins/builtin/chart-composer/series-expression.ts +3 -2
- package/src/plugins/builtin/chart-composer/session-range.ts +5 -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/client.ts +4 -1
- 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/client.ts +28 -2
- package/src/plugins/builtin/cot/headless.ts +4 -1
- package/src/plugins/builtin/cot/index.tsx +5 -4
- package/src/plugins/builtin/cot/model.ts +58 -7
- package/src/plugins/builtin/cot/pane.tsx +6 -2
- package/src/plugins/builtin/cpi/client.ts +6 -5
- 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/series.ts +3 -4
- package/src/plugins/builtin/credit-boards/sovr-pane.tsx +9 -4
- package/src/plugins/builtin/credit-conditions/client.ts +22 -14
- package/src/plugins/builtin/crypto-board/bitcoin-network.ts +143 -0
- package/src/plugins/builtin/crypto-board/client.ts +5 -5
- 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 +6 -4
- 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/debt-maturities/series.ts +3 -3
- 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/client.ts +6 -5
- package/src/plugins/builtin/doe/headless.ts +16 -3
- package/src/plugins/builtin/doe/index.tsx +5 -4
- package/src/plugins/builtin/doe/prices.ts +2 -3
- package/src/plugins/builtin/doe/series.ts +4 -4
- package/src/plugins/builtin/earnings/board.tsx +2 -1
- package/src/plugins/builtin/earnings/history.tsx +2 -1
- package/src/plugins/builtin/earnings-calls/calendar.tsx +10 -2
- package/src/plugins/builtin/earnings-calls/data.ts +19 -11
- package/src/plugins/builtin/earnings-calls/pane.tsx +27 -14
- 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/detail-view.tsx +16 -2
- 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/executives/data.ts +6 -1
- package/src/plugins/builtin/executives/pane.tsx +9 -21
- package/src/plugins/builtin/exposure/holdings.ts +35 -13
- package/src/plugins/builtin/exposure/index.tsx +1 -2
- package/src/plugins/builtin/fear-greed/cache.ts +4 -0
- 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/fear-greed/series.ts +6 -4
- 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 +19 -3
- 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/gpu/series.ts +2 -3
- package/src/plugins/builtin/help/index.tsx +1 -1
- package/src/plugins/builtin/holders/beneficial-view.tsx +1 -2
- 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/client.ts +4 -1
- 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/client.ts +1 -0
- package/src/plugins/builtin/market-movers/index.tsx +16 -5
- package/src/plugins/builtin/market-movers/screener.ts +23 -6
- package/src/plugins/builtin/market-movers/session-body.tsx +6 -2
- 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/money-markets/series.ts +3 -5
- 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/gas-client.ts +2 -1
- package/src/plugins/builtin/power/index.tsx +1 -2
- package/src/plugins/builtin/power/prices-client.ts +3 -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/risk-factors/data.ts +8 -4
- package/src/plugins/builtin/risk-factors/index.tsx +3 -1
- package/src/plugins/builtin/risk-factors/pane.tsx +5 -8
- 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/scanner/lock-note.ts +26 -3
- 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 +73 -8
- 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/fred-points.ts +6 -1
- package/src/plugins/builtin/shared/fx-rate-freshness.ts +17 -0
- package/src/plugins/builtin/shared/pro-lock-note.ts +23 -8
- package/src/plugins/builtin/shared/series-detail-chart.tsx +4 -2
- package/src/plugins/builtin/shared/series-warning.ts +13 -4
- 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/short-volume/series.ts +2 -3
- package/src/plugins/builtin/social-mentions/client.ts +15 -10
- package/src/plugins/builtin/social-mentions/index.tsx +1 -2
- package/src/plugins/builtin/social-mentions/pane.tsx +2 -1
- package/src/plugins/builtin/social-mentions/series.ts +2 -3
- 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 +127 -29
- package/src/plugins/builtin/thirteenf/data.ts +165 -35
- package/src/plugins/builtin/thirteenf/index.tsx +2 -4
- package/src/plugins/builtin/thirteenf/model.ts +2 -0
- package/src/plugins/builtin/thirteenf/overlap-pane.tsx +5 -1
- package/src/plugins/builtin/thirteenf/pane.tsx +41 -16
- package/src/plugins/builtin/thirteenf/types.ts +14 -0
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +22 -6
- package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +81 -11
- package/src/plugins/builtin/ticker-detail/financials/model.ts +35 -2
- package/src/plugins/builtin/ticker-detail/financials/ratios.ts +53 -18
- package/src/plugins/builtin/ticker-detail/headless.ts +70 -11
- 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/cache.ts +20 -8
- package/src/plugins/builtin/treasury-auctions/client.ts +3 -3
- package/src/plugins/builtin/treasury-auctions/headless.ts +3 -1
- package/src/plugins/builtin/treasury-auctions/pane.tsx +10 -4
- 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/client.ts +43 -16
- package/src/plugins/builtin/volatility/headless.ts +5 -2
- package/src/plugins/builtin/volatility/index.tsx +1 -2
- package/src/plugins/builtin/volatility/model.ts +17 -6
- package/src/plugins/builtin/volatility/pane.tsx +6 -2
- package/src/plugins/builtin/volatility/series.ts +2 -1
- 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/client.ts +4 -6
- 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 +64 -42
- 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 +5 -2
- 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/detached-windows.ts +23 -1
- package/src/renderers/electrobun/bun/desktop/initialization.ts +12 -3
- 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/attention.ts +54 -0
- package/src/renderers/electrobun/bun/window/create.ts +6 -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/electrobun/view/ui-host.tsx +9 -2
- 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/resolve.ts +2 -1
- package/src/series/standalone.ts +10 -1
- package/src/series/types.ts +12 -0
- package/src/series/validate.ts +11 -2
- package/src/sources/gloomberb-cloud/fred-series.ts +5 -3
- 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 +25 -9
- package/src/telemetry/stall-reports.ts +218 -13
- package/src/test-support/outage.ts +8 -0
- package/src/test-support/preload.ts +9 -0
- package/src/tickers/company-name-listing.ts +59 -29
- package/src/tickers/company-name.ts +84 -1
- package/src/tickers/listing-fate.ts +14 -3
- package/src/tickers/saved-instrument.ts +17 -0
- package/src/tickers/search/home-listing.ts +104 -8
- package/src/tickers/search/index.ts +5 -2
- 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/market.ts +5 -3
- package/src/time-series/official-close.ts +159 -0
- package/src/time-series/price-comparison.ts +75 -8
- package/src/time-series/resolve.ts +32 -4
- 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/time-series/types.ts +7 -0
- package/src/types/config.ts +18 -2
- package/src/types/data-provider.ts +23 -0
- package/src/types/financials.ts +40 -3
- package/src/types/headless.ts +42 -3
- package/src/types/plugin.ts +32 -3
- package/src/ui/host.tsx +17 -1
- 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/financial-statements.ts +73 -16
- package/src/utils/fundamentals.ts +17 -5
- package/src/utils/futures-generic.ts +4 -4
- package/src/utils/http-proxy-upstream.ts +124 -0
- package/src/utils/lease-liabilities.ts +54 -0
- package/src/utils/main-thread-stall.ts +122 -5
- package/src/utils/perf-marks.ts +58 -12
- package/src/utils/price-history-integrity.ts +47 -1
- 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/time-series/studies.ts +0 -1628
package/src/utils/cli-output.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { asciiText, localeReadsUtf8 } from "./ascii-text";
|
|
1
2
|
type CliAlign = "left" | "right" | "center";
|
|
2
3
|
|
|
3
4
|
export interface CliTableColumn {
|
|
@@ -45,6 +46,8 @@ export interface CliTableOptions {
|
|
|
45
46
|
indent?: number;
|
|
46
47
|
/** Lines printed under each row, by row index, indented and muted: a footnote that belongs to the row. */
|
|
47
48
|
below?: ReadonlyArray<readonly string[] | undefined>;
|
|
49
|
+
/** Each line under a row stays one line, cut to the table's width, rather than wrapping: a story's summary. */
|
|
50
|
+
cutBelow?: boolean;
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
export type CliStatEntry = readonly [label: string, value: string];
|
|
@@ -73,7 +76,12 @@ const NUMBER_JOINER = /^(?:\/|·|→|↑|↓|[-–]|to|up|down)$/;
|
|
|
73
76
|
const PLAIN_EMPTY_CELL = "—";
|
|
74
77
|
let colorEnabledOverride: boolean | null = null;
|
|
75
78
|
let widthOverride: number | null = null;
|
|
79
|
+
/** The widths `COLUMNS` is taken at for piped output, as `--width` takes them; anything else is ignored. */
|
|
80
|
+
const MIN_COLUMNS_VARIABLE = 20;
|
|
81
|
+
const MAX_COLUMNS_VARIABLE = 1000;
|
|
76
82
|
let plainOverride: boolean | null = null;
|
|
83
|
+
/** Whether the terminal reads only ASCII (`localeReadsUtf8`); undefined until first asked. */
|
|
84
|
+
let asciiLocale: boolean | undefined;
|
|
77
85
|
/** Output is being rendered for a file (`renderForFile`), which has no terminal width. */
|
|
78
86
|
let renderingForFile = false;
|
|
79
87
|
|
|
@@ -143,6 +151,23 @@ export function cliPlainEnabled(): boolean {
|
|
|
143
151
|
return plainOverride === true;
|
|
144
152
|
}
|
|
145
153
|
|
|
154
|
+
/** Tests say whether the terminal reads only ASCII; null asks the environment again. */
|
|
155
|
+
export function setCliAsciiLocaleOverride(value: boolean | null): void {
|
|
156
|
+
asciiLocale = value ?? undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Plain output to a terminal that cannot show UTF-8 (a C locale, a Windows
|
|
161
|
+
* code page such as 437): it is written in ASCII (`asciiText`), so a dash, a
|
|
162
|
+
* euro or a cent sign does not print as `—`. Output rendered for a file
|
|
163
|
+
* keeps its UTF-8.
|
|
164
|
+
*/
|
|
165
|
+
export function cliAsciiEnabled(): boolean {
|
|
166
|
+
if (plainOverride !== true || renderingForFile) return false;
|
|
167
|
+
asciiLocale ??= !localeReadsUtf8();
|
|
168
|
+
return asciiLocale;
|
|
169
|
+
}
|
|
170
|
+
|
|
146
171
|
/**
|
|
147
172
|
* Whether output is plain without `--plain` or `--no-plain`: NO_COLOR is set
|
|
148
173
|
* to a value (an empty one is unset, as the convention says), or stdout is
|
|
@@ -185,14 +210,20 @@ export function plainWords(text: string): string {
|
|
|
185
210
|
}
|
|
186
211
|
|
|
187
212
|
/** A table header's sort mark in words: `MKT VAL ▼` is "MKT VAL (sorted descending)". */
|
|
188
|
-
function plainHeader(header: string): string {
|
|
213
|
+
function plainHeader(header: string, ascii: boolean): string {
|
|
189
214
|
const sorted = header.replace(/\s*([▲▼])\s*$/, (_match, mark: string) => ` (sorted ${mark === "▲" ? "ascending" : "descending"})`);
|
|
190
|
-
|
|
215
|
+
const worded = plainWords(sorted.trimStart());
|
|
216
|
+
return ascii ? asciiText(worded) : worded;
|
|
191
217
|
}
|
|
192
218
|
|
|
193
|
-
/**
|
|
194
|
-
|
|
195
|
-
|
|
219
|
+
/**
|
|
220
|
+
* A plain table's cell: arrows in words, and "—" for no value. In ASCII
|
|
221
|
+
* (`cliAsciiEnabled`) before the columns are measured, so `EUR211.25` lines
|
|
222
|
+
* up as `€211.25` did.
|
|
223
|
+
*/
|
|
224
|
+
function plainCell(cell: string, ascii: boolean): string {
|
|
225
|
+
const plain = EMPTY_CELL.test(stripAnsi(cell).trim()) ? PLAIN_EMPTY_CELL : plainWords(cell);
|
|
226
|
+
return ascii ? asciiText(plain) : plain;
|
|
196
227
|
}
|
|
197
228
|
|
|
198
229
|
function applyAnsi(text: string, code: string): string {
|
|
@@ -217,11 +248,24 @@ export function setCliWidthOverride(value: number | null): void {
|
|
|
217
248
|
/** Columns the output is fitted to: `--width`, else the terminal's, or null when output is piped or redirected. */
|
|
218
249
|
export function cliTerminalWidth(): number | null {
|
|
219
250
|
if (widthOverride != null) return widthOverride;
|
|
220
|
-
if (renderingForFile
|
|
251
|
+
if (renderingForFile) return null;
|
|
252
|
+
if (!process.stdout.isTTY) return columnsVariable();
|
|
221
253
|
const columns = process.stdout.columns;
|
|
222
254
|
return typeof columns === "number" && columns > 0 ? columns : null;
|
|
223
255
|
}
|
|
224
256
|
|
|
257
|
+
/**
|
|
258
|
+
* `COLUMNS=60 gloom ... | less`: piped output is fitted to the width the
|
|
259
|
+
* variable sets, as `--width` does, so a screen reader or a pager gets the
|
|
260
|
+
* same narrow layout a terminal does. Unset or not a width, output stays whole.
|
|
261
|
+
*/
|
|
262
|
+
function columnsVariable(): number | null {
|
|
263
|
+
const value = process.env.COLUMNS?.trim();
|
|
264
|
+
if (!value || !/^\d+$/.test(value)) return null;
|
|
265
|
+
const columns = Number(value);
|
|
266
|
+
return columns >= MIN_COLUMNS_VARIABLE && columns <= MAX_COLUMNS_VARIABLE ? columns : null;
|
|
267
|
+
}
|
|
268
|
+
|
|
225
269
|
function padDisplay(text: string, width: number, align: CliAlign = "left"): string {
|
|
226
270
|
const padding = Math.max(0, width - visibleLength(text));
|
|
227
271
|
if (padding === 0) return text;
|
|
@@ -263,7 +307,8 @@ export function truncateDisplay(text: string, width: number): string {
|
|
|
263
307
|
|
|
264
308
|
/**
|
|
265
309
|
* Word-wraps text to `width` cells. Lines that already fit are kept verbatim, so aligned
|
|
266
|
-
* columns survive; continuation lines keep the paragraph's leading indent
|
|
310
|
+
* columns survive; continuation lines keep the paragraph's leading indent, and words on one
|
|
311
|
+
* line keep the spaces between them (a heading's ` · `).
|
|
267
312
|
*/
|
|
268
313
|
export function wrapText(text: string, width: number): string[] {
|
|
269
314
|
const lines: string[] = [];
|
|
@@ -273,7 +318,13 @@ export function wrapText(text: string, width: number): string[] {
|
|
|
273
318
|
continue;
|
|
274
319
|
}
|
|
275
320
|
const indent = /^ */.exec(paragraph)![0];
|
|
276
|
-
|
|
321
|
+
// Each word with the spaces before it, which a line break drops. A separator (`·`) ends its line, never starts one.
|
|
322
|
+
const words: { gap: string; word: string }[] = [];
|
|
323
|
+
for (const [, gap, word] of paragraph.slice(indent.length).matchAll(/( *)([^ ]+)/g)) {
|
|
324
|
+
const last = words.at(-1);
|
|
325
|
+
if (last && word === "·") last.word += `${gap}${word}`;
|
|
326
|
+
else words.push({ gap: gap!, word: word! });
|
|
327
|
+
}
|
|
277
328
|
if (words.length === 0) {
|
|
278
329
|
lines.push("");
|
|
279
330
|
continue;
|
|
@@ -281,16 +332,16 @@ export function wrapText(text: string, width: number): string[] {
|
|
|
281
332
|
let line = indent;
|
|
282
333
|
let lineWidth = indent.length;
|
|
283
334
|
let lineHasWord = false;
|
|
284
|
-
for (const word of words) {
|
|
335
|
+
for (const { gap, word } of words) {
|
|
285
336
|
const wordWidth = visibleLength(word);
|
|
286
|
-
if (lineHasWord && lineWidth +
|
|
337
|
+
if (lineHasWord && lineWidth + gap.length + wordWidth > width) {
|
|
287
338
|
lines.push(line);
|
|
288
339
|
line = indent;
|
|
289
340
|
lineWidth = indent.length;
|
|
290
341
|
lineHasWord = false;
|
|
291
342
|
}
|
|
292
|
-
line += lineHasWord ?
|
|
293
|
-
lineWidth += (lineHasWord ?
|
|
343
|
+
line += lineHasWord ? `${gap}${word}` : word;
|
|
344
|
+
lineWidth += (lineHasWord ? gap.length : 0) + wordWidth;
|
|
294
345
|
lineHasWord = true;
|
|
295
346
|
}
|
|
296
347
|
lines.push(line);
|
|
@@ -512,6 +563,22 @@ function wrapCell(text: string, width: number): string[] {
|
|
|
512
563
|
});
|
|
513
564
|
}
|
|
514
565
|
|
|
566
|
+
/**
|
|
567
|
+
* The columns that say what a row is: the leading ones up to the first figure
|
|
568
|
+
* after a word (`#`, Coin and Name before Price; Symbol and Name before Last).
|
|
569
|
+
*/
|
|
570
|
+
function rowIdentityColumns(columns: readonly CliTableColumn[]): Set<number> {
|
|
571
|
+
const identity = new Set<number>();
|
|
572
|
+
let named = false;
|
|
573
|
+
for (const [index, column] of columns.entries()) {
|
|
574
|
+
const figure = column.align === "right";
|
|
575
|
+
if (figure && named) break;
|
|
576
|
+
identity.add(index);
|
|
577
|
+
named ||= !figure;
|
|
578
|
+
}
|
|
579
|
+
return identity;
|
|
580
|
+
}
|
|
581
|
+
|
|
515
582
|
interface TableLayout {
|
|
516
583
|
kept: TableEntry[];
|
|
517
584
|
stacked: TableEntry[];
|
|
@@ -532,13 +599,18 @@ function movesSooner(left: readonly number[], right: readonly number[]): boolean
|
|
|
532
599
|
* (a name, a title) before dates and figures, then rightmost first. The
|
|
533
600
|
* first column stays. The list ends at the first layout that keeps every
|
|
534
601
|
* column it shows whole on one line: moving more only adds lines.
|
|
602
|
+
*
|
|
603
|
+
* Plain output (`--plain`, read aloud by a screen reader) keeps the columns
|
|
604
|
+
* that say what a row is on its first line, after only the ones too wide to
|
|
605
|
+
* sit there: a row is heard as "BTC Bitcoin" before its figures.
|
|
535
606
|
*/
|
|
536
|
-
function tableLayouts(entries: TableEntry[], available: number): TableLayout[] {
|
|
607
|
+
function tableLayouts(entries: TableEntry[], available: number, identity: ReadonlySet<number> = new Set()): TableLayout[] {
|
|
537
608
|
const fit = (kept: TableEntry[]) => fitColumnWidths(kept.map((entry) => entry.width), kept.map((entry) => entry.floor), available);
|
|
538
609
|
const first = entries[0]!;
|
|
539
610
|
const room = available - first.floor - COLUMN_GAP;
|
|
540
611
|
const order = (entry: TableEntry) => [
|
|
541
612
|
entry.floor > room ? 1 : 0,
|
|
613
|
+
identity.has(entry.index) ? 0 : 1,
|
|
542
614
|
entry.column.stackPriority ?? 0,
|
|
543
615
|
entry.text && entry.width > SOFT_MIN_COLUMN_WIDTH ? 1 : 0,
|
|
544
616
|
entry.index,
|
|
@@ -601,7 +673,7 @@ function renderTableLayout(
|
|
|
601
673
|
const belowIndent = `${indent}${" ".repeat(COLUMN_GAP)}`;
|
|
602
674
|
const belowWidth = Math.max(1, (available ?? 0) - COLUMN_GAP);
|
|
603
675
|
const renderBelow = (index: number) => (options.below?.[index] ?? []).flatMap((line) => (
|
|
604
|
-
available == null ? [line] : wrapText(line, belowWidth)
|
|
676
|
+
available == null ? [line] : options.cutBelow ? [truncateDisplay(line, belowWidth)] : wrapText(line, belowWidth)
|
|
605
677
|
)).map((line) => `${belowIndent}${cliStyles.muted(line)}`.trimEnd());
|
|
606
678
|
// Columns moved under the rows print there as `Header value`, as many to a line as fit.
|
|
607
679
|
const renderStacked = (cells: string[], rowIndex: number): string[] => {
|
|
@@ -645,9 +717,10 @@ function renderTableLayout(
|
|
|
645
717
|
*/
|
|
646
718
|
export function renderTable(columns: CliTableColumn[], rows: string[][], options: CliTableOptions = {}): string {
|
|
647
719
|
if (cliPlainEnabled()) {
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
720
|
+
const ascii = cliAsciiEnabled();
|
|
721
|
+
columns = columns.map((column) => ({ ...column, header: plainHeader(column.header, ascii) }));
|
|
722
|
+
rows = rows.map((row) => columns.map((_, index) => plainCell(row[index] ?? "", ascii)));
|
|
723
|
+
if (options.below) options = { ...options, below: options.below.map((lines) => lines?.map((line) => ascii ? asciiText(plainWords(line)) : plainWords(line))) };
|
|
651
724
|
}
|
|
652
725
|
const maxWidth = options.maxWidth === undefined ? cliTerminalWidth() : options.maxWidth;
|
|
653
726
|
const available = maxWidth == null ? null : maxWidth - (options.indent ?? 0);
|
|
@@ -674,7 +747,7 @@ export function renderTable(columns: CliTableColumn[], rows: string[][], options
|
|
|
674
747
|
});
|
|
675
748
|
let best = "";
|
|
676
749
|
let bestLines = Infinity;
|
|
677
|
-
for (const layout of tableLayouts(entries, available)) {
|
|
750
|
+
for (const layout of tableLayouts(entries, available, cliPlainEnabled() ? rowIdentityColumns(columns) : undefined)) {
|
|
678
751
|
const stacked = layout.stacked.filter((entry) => entry.column.stackRows !== 0).sort((left, right) => left.index - right.index);
|
|
679
752
|
const text = renderTableLayout(columns, rows, options, layout.kept, stacked, available, true);
|
|
680
753
|
const lines = text.split("\n").length;
|
package/src/utils/concurrency.ts
CHANGED
|
@@ -23,3 +23,27 @@ export function chunked<T>(items: readonly T[], size: number): T[][] {
|
|
|
23
23
|
for (let index = 0; index < items.length; index += size) chunks.push(items.slice(index, index + size));
|
|
24
24
|
return chunks;
|
|
25
25
|
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* One gate that every caller shares: at most `limit` tasks run at once, and
|
|
29
|
+
* the rest wait in the order they came. Unlike mapWithConcurrency, separate
|
|
30
|
+
* callers (two panes, a pane and a report) count against the same limit. A
|
|
31
|
+
* finished task hands its slot straight to the next waiting one, so a caller
|
|
32
|
+
* arriving in between never takes it.
|
|
33
|
+
*/
|
|
34
|
+
export function createConcurrencyLimiter(limit: number): <T>(task: () => Promise<T>) => Promise<T> {
|
|
35
|
+
const slots = Math.max(1, Math.floor(limit));
|
|
36
|
+
let active = 0;
|
|
37
|
+
const waiting: Array<() => void> = [];
|
|
38
|
+
return async (task) => {
|
|
39
|
+
if (active < slots) active += 1;
|
|
40
|
+
else await new Promise<void>((resolve) => waiting.push(resolve));
|
|
41
|
+
try {
|
|
42
|
+
return await task();
|
|
43
|
+
} finally {
|
|
44
|
+
const next = waiting.shift();
|
|
45
|
+
if (next) next();
|
|
46
|
+
else active -= 1;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
package/src/utils/exchanges.ts
CHANGED
|
@@ -93,6 +93,12 @@ export function isKnownExchangeCode(value?: string): boolean {
|
|
|
93
93
|
return !!normalized && KNOWN_EXCHANGE_CODES.has(normalized);
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/** "Warsaw Stock Exchange" for a venue with a name, the canonical code otherwise (NASDAQ, whose name is its code). */
|
|
97
|
+
export function exchangeName(value?: string): string {
|
|
98
|
+
const code = canonicalExchange(value);
|
|
99
|
+
return EXCHANGE_NAMES[code] ?? code;
|
|
100
|
+
}
|
|
101
|
+
|
|
96
102
|
/** "Euronext Paris (EPA)" for a venue with a name, the canonical code otherwise. */
|
|
97
103
|
export function exchangeLabel(value?: string): string {
|
|
98
104
|
const code = canonicalExchange(value);
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Scheme validation for links handed to the operating system.
|
|
3
3
|
*
|
|
4
|
-
* Opening a link natively means spawning `open` / `xdg-open` / `
|
|
5
|
-
*
|
|
6
|
-
* markets, news, filings, Substack.
|
|
7
|
-
*
|
|
8
|
-
* argument through cmd's parser.
|
|
4
|
+
* Opening a link natively means spawning `open` / `xdg-open` / `rundll32` with
|
|
5
|
+
* a string that usually came from an external feed: RSS items, prediction
|
|
6
|
+
* markets, news, filings, Substack. `file://` and the other local schemes
|
|
7
|
+
* launch local content, so only http(s) goes through.
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Returns the normalised URL so callers spawn the parsed form rather than the
|
|
10
|
+
* raw input.
|
|
12
11
|
*/
|
|
13
12
|
export function safeExternalUrl(value: string): string | null {
|
|
14
13
|
if (!value.trim()) return null;
|
|
@@ -22,3 +21,17 @@ export function safeExternalUrl(value: string): string | null {
|
|
|
22
21
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
|
|
23
22
|
return parsed.toString();
|
|
24
23
|
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The command that hands an http(s) link to the system's own opener, with the
|
|
27
|
+
* link as the last argument, or null for any other link. Windows opens it
|
|
28
|
+
* through `url.dll` rather than `cmd /c start`: `&`, `|`, `^` and `%` are part
|
|
29
|
+
* of a link, and a command line would read them as its own.
|
|
30
|
+
*/
|
|
31
|
+
export function systemOpenCommand(rawUrl: string, platform: NodeJS.Platform): string[] | null {
|
|
32
|
+
const url = safeExternalUrl(rawUrl);
|
|
33
|
+
if (!url) return null;
|
|
34
|
+
if (platform === "darwin") return ["open", url];
|
|
35
|
+
if (platform === "win32") return ["rundll32.exe", "url.dll,FileProtocolHandler", url];
|
|
36
|
+
return ["xdg-open", url];
|
|
37
|
+
}
|
|
@@ -1,31 +1,88 @@
|
|
|
1
|
-
import type { FinancialStatement, StatementFill, StatementFillField } from "../types/financials";
|
|
1
|
+
import type { FinancialStatement, StatementFill, StatementFillField, TickerFinancials } from "../types/financials";
|
|
2
|
+
import { settleWithin } from "./async-deadline";
|
|
3
|
+
import { logFailure } from "./log-failure";
|
|
2
4
|
import { copyIncomeField, incomeFieldOwner, INCOME_STATEMENT_FIELDS, isIncomeStatementField } from "./income-statement";
|
|
3
5
|
import { mergeStatementGaps } from "./statement-gaps";
|
|
4
6
|
|
|
5
7
|
export const FINANCIAL_VINTAGE_NOTICE = "Latest available statements may include restatements. Historical as-of values are not reconstructed.";
|
|
6
8
|
|
|
9
|
+
/** The oldest dated period of a report's columns (a TTM column has no date), or null when none has one. */
|
|
10
|
+
function oldestPeriod(dates: readonly string[]): string | null {
|
|
11
|
+
return dates.filter((date) => /^\d{4}-\d{2}-\d{2}/.test(date)).map((date) => date.slice(0, 10)).sort()[0] ?? null;
|
|
12
|
+
}
|
|
13
|
+
|
|
7
14
|
/**
|
|
8
15
|
* The closing lines of a statements report that say how far back it goes, from
|
|
9
|
-
* the oldest period it served: the report holds no older ones
|
|
10
|
-
*
|
|
11
|
-
*
|
|
16
|
+
* the oldest period it served: the report holds no older ones. `older` says
|
|
17
|
+
* whether `GF`, which reads one metric from the listing's extended history
|
|
18
|
+
* (`hasOlderStatements`), goes further back: it is pointed to only then, and
|
|
19
|
+
* when it does not, the listing has no older ones at all (IAG on LSE: its
|
|
20
|
+
* statements start in 2022, in GF too). Null when that could not be told in
|
|
21
|
+
* time: the report then says nothing of older periods. Empty too when no
|
|
22
|
+
* period has a date, so a report with nothing in it says nothing.
|
|
12
23
|
*/
|
|
13
24
|
export function statementHistoryNotes(
|
|
14
25
|
period: "annual" | "quarterly",
|
|
15
26
|
dates: readonly string[],
|
|
16
27
|
symbol: string,
|
|
28
|
+
older: boolean | null,
|
|
17
29
|
): string[] {
|
|
18
|
-
const oldest = dates
|
|
19
|
-
if (!oldest) return [];
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
]
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
const oldest = oldestPeriod(dates);
|
|
31
|
+
if (!oldest || older === null) return [];
|
|
32
|
+
const [from, unit] = period === "annual" ? [oldest.slice(0, 4), "years"] : [oldest, "quarters"];
|
|
33
|
+
if (older === false) return [`${period} history from ${from}; older ${unit} not available for this listing`];
|
|
34
|
+
return [
|
|
35
|
+
`${period} history from ${from}; older ${unit} not served`,
|
|
36
|
+
...older ? [`earlier ${unit}, one metric at a time: gloom fn GF ${symbol} --period ${period} --periods 40`] : [],
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* How long a statements report waits on the extended history for its
|
|
42
|
+
* older-periods note, as a lock note waits on its routes' checks: a read still
|
|
43
|
+
* out then leaves the report on time, without the note. The read goes on and
|
|
44
|
+
* warms the cache for the next report.
|
|
45
|
+
*/
|
|
46
|
+
const OLDER_STATEMENTS_WAIT_MS = 3_000;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Whether the listing's extended statement history, which `GF` reads, has a
|
|
50
|
+
* period older than the oldest of `dates`, a report's columns: a US filer's
|
|
51
|
+
* filings reach years back; a listing abroad has nothing past what the report
|
|
52
|
+
* served. Null when the history could not be read within `waitMs`.
|
|
53
|
+
*/
|
|
54
|
+
export async function hasOlderStatements(
|
|
55
|
+
loadExtended: () => Promise<Pick<TickerFinancials, "annualStatements" | "quarterlyStatements">>,
|
|
56
|
+
period: "annual" | "quarterly",
|
|
57
|
+
dates: readonly string[],
|
|
58
|
+
waitMs = OLDER_STATEMENTS_WAIT_MS,
|
|
59
|
+
): Promise<boolean | null> {
|
|
60
|
+
const oldest = oldestPeriod(dates);
|
|
61
|
+
if (!oldest) return null;
|
|
62
|
+
const read = Promise.resolve().then(loadExtended).catch((error: unknown) => {
|
|
63
|
+
logFailure("utils.financial-statements", "Reading the extended statement history failed")(error);
|
|
64
|
+
throw error;
|
|
65
|
+
});
|
|
66
|
+
const extended = await settleWithin(read, waitMs);
|
|
67
|
+
if (!extended) return null;
|
|
68
|
+
const rows = period === "annual" ? extended.annualStatements : extended.quarterlyStatements;
|
|
69
|
+
return rows.some((row) => /^\d{4}-\d{2}-\d{2}/.test(row.date) && row.date.slice(0, 10) < oldest);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Venues whose companies report half-yearly and yearly, with no quarterly statements: Australia's (ASX, and Cboe Australia, which trades ASX companies) and New Zealand's. */
|
|
73
|
+
const HALF_YEARLY_VENUES = new Set(["ASX", "CXA", "NZX"]);
|
|
74
|
+
const DAY_MS = 86_400_000;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Whether a listing's company reports half-yearly rather than quarterly: it
|
|
78
|
+
* lists on a venue whose companies do (Qantas on ASX), or the periods of its
|
|
79
|
+
* "quarterly" statements are half a year apart.
|
|
80
|
+
*/
|
|
81
|
+
export function reportsHalfYearly(exchange: string | null | undefined, quarterly: readonly Pick<FinancialStatement, "date">[]): boolean {
|
|
82
|
+
if (exchange && HALF_YEARLY_VENUES.has(exchange.toUpperCase())) return true;
|
|
83
|
+
const times = quarterly.map(({ date }) => Date.parse(`${date.slice(0, 10)}T00:00:00Z`)).filter(Number.isFinite).sort((a, b) => a - b);
|
|
84
|
+
const gaps = times.slice(1).map((time, index) => (time - times[index]!) / DAY_MS);
|
|
85
|
+
return gaps.length > 0 && gaps.every((days) => days > 150 && days < 215);
|
|
29
86
|
}
|
|
30
87
|
|
|
31
88
|
/** How old a statement's latest period may be before a report says how old it is. */
|
|
@@ -106,7 +163,7 @@ function retainedFill(merged: FinancialStatement, rows: Array<FinancialStatement
|
|
|
106
163
|
|
|
107
164
|
export const SEC_EPS_BASIS_NOTICE = "SEC EPS uses corroborated split-adjusted share bases. Unverified bases are unavailable.";
|
|
108
165
|
|
|
109
|
-
const STATEMENT_METADATA_KEYS = new Set(["date", "dateSource", "providerDate", "dateEvidence", "currency", "availableAt", "fieldAvailability", "epsBasis", "fieldSources", "unavailableFields", "withdrawnObservations", "operatingResult", "operatingResultAggregation", "earningsResult", "unavailableEarnings", "filledFrom"]);
|
|
166
|
+
const STATEMENT_METADATA_KEYS = new Set(["date", "dateSource", "accountingStandard", "providerDate", "dateEvidence", "currency", "availableAt", "fieldAvailability", "epsBasis", "fieldSources", "unavailableFields", "withdrawnObservations", "operatingResult", "operatingResultAggregation", "earningsResult", "unavailableEarnings", "filledFrom"]);
|
|
110
167
|
const NEARBY_PERIOD_END_MS = 7 * 24 * 60 * 60 * 1_000;
|
|
111
168
|
|
|
112
169
|
/** An explicit field map is authoritative: omitted fields have unknown availability. */
|
|
@@ -45,7 +45,10 @@ export function enterpriseValueCheck(
|
|
|
45
45
|
// Not meaningful needs no figure of its own: a source EV below zero may come with no cap or currency.
|
|
46
46
|
if (check.status === "not-meaningful") return withdrawn ? check : undefined;
|
|
47
47
|
if (!check.currency) return undefined;
|
|
48
|
-
|
|
48
|
+
// A withdrawn EV comes with the figure it was checked against, or with why no balance sheet checked it.
|
|
49
|
+
if (check.status === "withdrawn") {
|
|
50
|
+
return withdrawn && ((finiteNumber(check.computed) && finiteNumber(check.source)) || !!check.reason?.trim()) ? check : undefined;
|
|
51
|
+
}
|
|
49
52
|
if (check.status === "computed") {
|
|
50
53
|
return finiteNumber(check.computed) && fundamentals?.enterpriseValue === check.computed ? check : undefined;
|
|
51
54
|
}
|
|
@@ -87,8 +90,9 @@ export function enterpriseValueLabel(fundamentals: Parameters<typeof enterpriseV
|
|
|
87
90
|
/**
|
|
88
91
|
* The reconciliation line under a labelled, withdrawn or not meaningful EV: "EV 61.75B USD (provider) vs 63.75B
|
|
89
92
|
* computed from market cap + total debt - cash (balance sheet 2026-06-30).", "EV N/M: cash and short-term investments
|
|
90
|
-
* exceed market cap + total debt (balance sheet 2026-06-30)."
|
|
91
|
-
*
|
|
93
|
+
* exceed market cap + total debt (balance sheet 2026-06-30)." A balance sheet in another currency names the rate it
|
|
94
|
+
* was converted at ("balance sheet 2026-06-30, EUR at 0.8614 GBP"). A withdrawn EV no balance sheet checked gives
|
|
95
|
+
* Gloom Cloud's reason; one withdrawn without a check, that the source failed validation. Undefined when the EV matches, is unchecked or computed, or has no check. Read it from the
|
|
92
96
|
* fundamentals the rows show (atRegularSessionPrice moves the check with the EV and the market cap), so its figures
|
|
93
97
|
* are theirs.
|
|
94
98
|
*/
|
|
@@ -97,14 +101,22 @@ export function enterpriseValueReconciliationNote(fundamentals: Parameters<typeo
|
|
|
97
101
|
if (notMeaningful) return `EV N/M: ${notMeaningful.charAt(0).toLowerCase()}${notMeaningful.slice(1)}.`;
|
|
98
102
|
const check = enterpriseValueCheck(fundamentals);
|
|
99
103
|
if (!check && enterpriseValueWithdrawn(fundamentals)) return "EV unavailable: the provider's figure failed validation.";
|
|
104
|
+
const reason = check?.reason?.trim().replace(/\.$/, "");
|
|
105
|
+
if (check?.status === "withdrawn" && !finiteNumber(check.computed) && reason) {
|
|
106
|
+
return `EV unavailable: ${reason.charAt(0).toLowerCase()}${reason.slice(1)}.`;
|
|
107
|
+
}
|
|
100
108
|
if (!check || !finiteNumber(check.computed) || !finiteNumber(check.source)
|
|
101
109
|
|| (check.status !== "differs" && check.status !== "withdrawn")) return undefined;
|
|
102
110
|
const source = `${formatCompact(check.source)} ${check.currency}`;
|
|
103
111
|
const computed = formatCompact(check.computed);
|
|
104
|
-
const
|
|
112
|
+
const leases = finiteNumber(check.leases) && check.leases > 0 ? " + leases" : "";
|
|
113
|
+
const fx = check.fx && finiteNumber(check.fx.rate) ? `, ${check.fx.from} at ${check.fx.rate.toPrecision(4)} ${check.currency}` : "";
|
|
114
|
+
const sheet = check.balanceSheetDate ? ` (balance sheet ${check.balanceSheetDate}${fx})` : "";
|
|
115
|
+
const basis = `market cap + total debt${leases} - cash${sheet}`;
|
|
105
116
|
if (check.status === "differs") return `EV ${source} (provider) vs ${computed} computed from ${basis}.`;
|
|
106
117
|
const ratio = check.computed > 0 ? ` is ${(check.source / check.computed).toFixed(1)}x` : " is far from";
|
|
107
|
-
|
|
118
|
+
const twice = check.convertedTwice && check.convertedFrom ? `; it converts ${check.convertedFrom} to ${check.currency} twice` : "";
|
|
119
|
+
return `EV unavailable: the provider's ${source}${ratio} the ${computed} computed from ${basis}${twice}.`;
|
|
108
120
|
}
|
|
109
121
|
|
|
110
122
|
/**
|
|
@@ -170,9 +170,9 @@ export function futuresGenericPriceBasis(generic: Pick<FuturesGeneric, "root">):
|
|
|
170
170
|
|
|
171
171
|
/**
|
|
172
172
|
* How a futures listing's price reads, whichever form it takes: a generic by
|
|
173
|
-
* its root (VX1 points, TY1 32nds), and a continuous (`ES=F`, `
|
|
174
|
-
* listed contract (`
|
|
175
|
-
*
|
|
173
|
+
* its root (VX1 points, TY1 32nds), and a continuous (`ES=F`, `ZN=F`) or
|
|
174
|
+
* listed contract (`ZNZ26.CBT`) in the same units as its root's generic,
|
|
175
|
+
* on the root's own venue or with none. The quote keeps its currency
|
|
176
176
|
* for what a tick is worth. Null for every other listing, a security that
|
|
177
177
|
* shares a root's code on another venue (XAU on Toronto, the XAR ETF) included.
|
|
178
178
|
*/
|
|
@@ -180,7 +180,7 @@ export function futuresListingPriceBasis(symbol: string, exchange?: string | nul
|
|
|
180
180
|
const generic = futuresGenericListing(symbol, exchange);
|
|
181
181
|
if (generic) return futuresGenericPriceBasis(generic);
|
|
182
182
|
const listing = futuresListingVenue(symbol, exchange);
|
|
183
|
-
return listing?.root && ROOT_VENUES[listing.root] === listing.venue
|
|
183
|
+
return listing?.root && ROOT_VENUES[listing.root] === listing.venue ? futuresGenericPriceBasis({ root: listing.root }) : null;
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
/** Whether a root's date rules count back from first notice or from last trade. */
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { ProxiedRequestInit } from "./http-proxy-response";
|
|
2
|
+
|
|
3
|
+
/** Set by the caller's browser or meaningful only to the hop it came from. */
|
|
4
|
+
const STRIPPED_REQUEST_HEADERS = new Set([
|
|
5
|
+
"connection",
|
|
6
|
+
"content-length",
|
|
7
|
+
"host",
|
|
8
|
+
"keep-alive",
|
|
9
|
+
"proxy-authorization",
|
|
10
|
+
"te",
|
|
11
|
+
"trailer",
|
|
12
|
+
"transfer-encoding",
|
|
13
|
+
"upgrade",
|
|
14
|
+
]);
|
|
15
|
+
/** Dropped when a redirect leaves the origin the plugin set them for. */
|
|
16
|
+
const CROSS_ORIGIN_STRIPPED_HEADERS = ["authorization", "cookie", "proxy-authorization"];
|
|
17
|
+
/** Dropped when a redirect turns the request into a bodiless GET. */
|
|
18
|
+
const REQUEST_BODY_HEADERS = ["content-encoding", "content-language", "content-location", "content-type"];
|
|
19
|
+
const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
20
|
+
const MAX_REDIRECTS = 5;
|
|
21
|
+
|
|
22
|
+
/** Whether a proxy may reach a URL: the URL to fetch, or why not and the status to answer with. */
|
|
23
|
+
export type ProxyTargetCheck = (url: string) => ProxyTargetVerdict | Promise<ProxyTargetVerdict>;
|
|
24
|
+
type ProxyTargetVerdict = { url: URL } | { error: string; status: number };
|
|
25
|
+
|
|
26
|
+
export type ProxyUpstreamResult = { response: Response } | { error: string; status: number };
|
|
27
|
+
|
|
28
|
+
function upstreamHeaders(raw: Record<string, string>): Headers {
|
|
29
|
+
const headers = new Headers();
|
|
30
|
+
for (const [name, value] of Object.entries(raw)) {
|
|
31
|
+
if (STRIPPED_REQUEST_HEADERS.has(name.toLowerCase())) continue;
|
|
32
|
+
try {
|
|
33
|
+
headers.set(name, value);
|
|
34
|
+
} catch {
|
|
35
|
+
// Intentional ignore: a header name the runtime refuses is dropped, as a browser drops an unsettable header.
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return headers;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Fetches a proxied plugin request, following redirects one hop at a time
|
|
43
|
+
* instead of letting `fetch` do it, so every `Location` passes the same
|
|
44
|
+
* `check` as the first URL. A host that redirects elsewhere (an open
|
|
45
|
+
* redirect, a moved API) would otherwise carry the request anywhere.
|
|
46
|
+
*
|
|
47
|
+
* Method and header handling mirror the Fetch spec: 303, and 301/302 after a
|
|
48
|
+
* POST, become a bodiless GET; credentials the plugin set are dropped when the
|
|
49
|
+
* redirect leaves the origin they were meant for.
|
|
50
|
+
*/
|
|
51
|
+
export async function fetchCheckingRedirects(
|
|
52
|
+
first: URL,
|
|
53
|
+
init: ProxiedRequestInit,
|
|
54
|
+
options: { fetch: typeof fetch; check: ProxyTargetCheck; signal: AbortSignal },
|
|
55
|
+
): Promise<ProxyUpstreamResult> {
|
|
56
|
+
const headers = upstreamHeaders(init.headers);
|
|
57
|
+
const mode = init.redirect ?? "follow";
|
|
58
|
+
let url = first;
|
|
59
|
+
let method = init.method;
|
|
60
|
+
let body = init.body;
|
|
61
|
+
|
|
62
|
+
for (let followed = 0; ; followed += 1) {
|
|
63
|
+
const response = await options.fetch(url, { method, headers, body, redirect: "manual", signal: options.signal });
|
|
64
|
+
const location = REDIRECT_STATUSES.has(response.status) ? response.headers.get("location") : null;
|
|
65
|
+
// "manual" hands the redirect back to the plugin. If it follows it, that
|
|
66
|
+
// comes back through here as a new request and is checked like one.
|
|
67
|
+
if (location === null || mode === "manual") return { response };
|
|
68
|
+
|
|
69
|
+
await response.body?.cancel().catch(() => { /* Intentional ignore: the redirect's own body is never read, only dropped. */ });
|
|
70
|
+
if (mode === "error") return { error: "The upstream response was a redirect.", status: 502 };
|
|
71
|
+
if (followed >= MAX_REDIRECTS) return { error: "The upstream redirected too many times.", status: 502 };
|
|
72
|
+
|
|
73
|
+
let next: string;
|
|
74
|
+
try {
|
|
75
|
+
next = new URL(location, url).href;
|
|
76
|
+
} catch {
|
|
77
|
+
// Intentional ignore: a Location that does not parse is reported as an invalid redirect, never followed.
|
|
78
|
+
return { error: "The upstream sent an invalid redirect.", status: 502 };
|
|
79
|
+
}
|
|
80
|
+
const target = await options.check(next);
|
|
81
|
+
if ("error" in target) return { error: `The upstream redirect was refused. ${target.error}`, status: 502 };
|
|
82
|
+
|
|
83
|
+
if (target.url.origin !== url.origin) {
|
|
84
|
+
for (const name of CROSS_ORIGIN_STRIPPED_HEADERS) headers.delete(name);
|
|
85
|
+
}
|
|
86
|
+
if (
|
|
87
|
+
(response.status === 303 && method !== "GET" && method !== "HEAD")
|
|
88
|
+
|| ((response.status === 301 || response.status === 302) && method === "POST")
|
|
89
|
+
) {
|
|
90
|
+
method = "GET";
|
|
91
|
+
body = undefined;
|
|
92
|
+
for (const name of REQUEST_BODY_HEADERS) headers.delete(name);
|
|
93
|
+
}
|
|
94
|
+
url = target.url;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Reads a body, giving up once it passes `maxBytes`. Returns null when it was
|
|
100
|
+
* too large, after cancelling the stream so the rest is never pulled.
|
|
101
|
+
*/
|
|
102
|
+
export async function readBytesWithin(body: ReadableStream<Uint8Array> | null, maxBytes: number): Promise<Uint8Array | null> {
|
|
103
|
+
if (!body) return new Uint8Array(0);
|
|
104
|
+
const reader = body.getReader();
|
|
105
|
+
const chunks: Uint8Array[] = [];
|
|
106
|
+
let size = 0;
|
|
107
|
+
for (;;) {
|
|
108
|
+
const { done, value } = await reader.read();
|
|
109
|
+
if (done) break;
|
|
110
|
+
size += value.byteLength;
|
|
111
|
+
if (size > maxBytes) {
|
|
112
|
+
await reader.cancel().catch(() => { /* Intentional ignore: the rest of a body past the cap is dropped unread. */ });
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
chunks.push(value);
|
|
116
|
+
}
|
|
117
|
+
const bytes = new Uint8Array(size);
|
|
118
|
+
let offset = 0;
|
|
119
|
+
for (const chunk of chunks) {
|
|
120
|
+
bytes.set(chunk, offset);
|
|
121
|
+
offset += chunk.byteLength;
|
|
122
|
+
}
|
|
123
|
+
return bytes;
|
|
124
|
+
}
|