@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
|
@@ -11,7 +11,7 @@ import { applyChartComposerCapabilityOptions } from "../../plugins/builtin/chart
|
|
|
11
11
|
import { parseChartSpec } from "../../plugins/builtin/chart-composer/chart-spec";
|
|
12
12
|
import { normalizeTickerInput } from "../../tickers/search";
|
|
13
13
|
import { slugifyName } from "../../utils/slugify";
|
|
14
|
-
import { ListingArgError, listingInstrument, listingReadNotes, resolveCliListing, usListingsOnlyMessage } from "../listing-arg";
|
|
14
|
+
import { listCoinNotes, ListingArgError, listingInstrument, listingReadNotes, resolveCliListing, usListingsOnlyMessage } from "../listing-arg";
|
|
15
15
|
import { uncoveredInstrumentKind, uncoveredInstrumentMessage } from "../../tickers/instrument-coverage";
|
|
16
16
|
import { uncoveredInstrumentCommand } from "../uncovered-instrument";
|
|
17
17
|
import { listingAbroadVenue } from "../../utils/sec";
|
|
@@ -83,7 +83,7 @@ async function buildPaneInstance(
|
|
|
83
83
|
// Free text is a phrase, not a symbol. Treating it as a ticker makes the
|
|
84
84
|
// pane look for a provider for values such as "HIGH GROWTH SOFTWARE".
|
|
85
85
|
const headlessArgKind = resolved.headless?.argument.kind;
|
|
86
|
-
const fallbackSymbol = !argumentNamesTicker(resolved.template, resolved.headless)
|
|
86
|
+
const fallbackSymbol = !argumentNamesTicker(resolved.template, resolved.headless, resolved.pane)
|
|
87
87
|
? null
|
|
88
88
|
: headlessArgKind === "tickers" || headlessArgKind === "symbol-list"
|
|
89
89
|
? normalizeTickerInput(null, cleanTickerInput(target.split(/[,\s]+/)[0] ?? ""))
|
|
@@ -254,8 +254,9 @@ export async function applyListingArgument(
|
|
|
254
254
|
}
|
|
255
255
|
notes.push(...answers.map((answer) => `${answer.message} ${answer.details.replaceAll("\n", " ")}`));
|
|
256
256
|
}
|
|
257
|
-
// One listing's notes come with its choice note; a list's are said here
|
|
258
|
-
|
|
257
|
+
// One listing's notes come with its choice note; a list's are said here, with a coin's code read as a listing
|
|
258
|
+
// (CORR RIOT,BTC: BTC is the Grayscale fund) as `quote BTC` says it.
|
|
259
|
+
if (listings.length > 1) notes.unshift(...listings.flatMap(listingReadNotes), ...await listCoinNotes(listings, context));
|
|
259
260
|
return {
|
|
260
261
|
...args,
|
|
261
262
|
arg: listings.map((listing) => listing.key).join(", "),
|
|
@@ -349,8 +350,8 @@ export async function resolvePaneFunction(
|
|
|
349
350
|
if (!pane) {
|
|
350
351
|
throw new Error(`Template "${template?.id}" points at missing pane "${template?.paneId}".`);
|
|
351
352
|
}
|
|
352
|
-
const createOptions = buildCreateOptions(template, args.arg);
|
|
353
353
|
const headless = getHeadlessPaneDefinition(template, pane);
|
|
354
|
+
const createOptions = withTickerList(template, buildCreateOptions(template, args.arg), headless, args.arg, paneFunctionCode(template, pane));
|
|
354
355
|
const capability = getPaneFunctionCapability(template, pane);
|
|
355
356
|
let tableSection: string | true | undefined;
|
|
356
357
|
if (resolutionSettings.tableSection) {
|
|
@@ -400,6 +401,27 @@ export async function resolvePaneFunction(
|
|
|
400
401
|
return resolved;
|
|
401
402
|
}
|
|
402
403
|
|
|
404
|
+
/**
|
|
405
|
+
* A one-ticker template whose pane also opens on a list (`opensTickerList`:
|
|
406
|
+
* DVD's ranking): the list is passed as `symbols`, so `shot` draws the same
|
|
407
|
+
* tickers `fn` reads, in one pane, and fetches each; past the most the
|
|
408
|
+
* function's report takes, it is refused as `fn` refuses it. One ticker, and
|
|
409
|
+
* any other template, stays as it was.
|
|
410
|
+
*/
|
|
411
|
+
export function withTickerList(
|
|
412
|
+
template: PaneTemplateDef | undefined,
|
|
413
|
+
createOptions: PaneTemplateCreateOptions | undefined,
|
|
414
|
+
headless: HeadlessPaneDefinition | undefined,
|
|
415
|
+
arg: string,
|
|
416
|
+
token = template?.shortcut?.prefix ?? "",
|
|
417
|
+
): PaneTemplateCreateOptions | undefined {
|
|
418
|
+
if (!template?.opensTickerList || !createOptions || createOptions.symbols?.length) return createOptions;
|
|
419
|
+
const symbols = [...new Set(arg.split(/[,\s]+/).map((word) => cleanTickerInput(word)).filter(Boolean))];
|
|
420
|
+
if (symbols.length < 2) return createOptions;
|
|
421
|
+
if (headless) validateSymbolCount(headless, token, symbols, "shot");
|
|
422
|
+
return { ...createOptions, symbol: symbols[0]!, arg: symbols.join(","), symbols };
|
|
423
|
+
}
|
|
424
|
+
|
|
403
425
|
function validateTickerCardinality(
|
|
404
426
|
capability: PaneFunctionCapability,
|
|
405
427
|
createOptions: PaneTemplateCreateOptions | undefined,
|
|
@@ -208,6 +208,39 @@ export function filledKeyValueCount(rows: DesktopPaneShotRenderResult["visibleKe
|
|
|
208
208
|
}).length;
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
+
/**
|
|
212
|
+
* Whether a capture shows nothing that was loaded: a wall that needs an
|
|
213
|
+
* account, or an empty, loading or failed state, with no rows, labeled values
|
|
214
|
+
* or plotted chart beside it. Its footer carries no status line then, as it
|
|
215
|
+
* has no data to date: the quotes the capture loaded for its symbol are not on
|
|
216
|
+
* screen. A pane that only locks or fails part of what it shows keeps it.
|
|
217
|
+
*/
|
|
218
|
+
export function shotLoadedNothing(
|
|
219
|
+
render: Pick<DesktopPaneShotRenderResult, "accessGate" | "emptyStateDetected" | "loadingStateDetected" | "errorStateDetected" | "rows" | "visibleKeyValues" | "semanticUi">,
|
|
220
|
+
): boolean {
|
|
221
|
+
const stated = !!render.accessGate || render.emptyStateDetected || render.loadingStateDetected || render.errorStateDetected;
|
|
222
|
+
return stated
|
|
223
|
+
&& render.rows.length === 0
|
|
224
|
+
&& filledKeyValueCount(render.visibleKeyValues) === 0
|
|
225
|
+
&& !render.semanticUi.some(plotsData);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* A pane's chart evidence node that says it plotted something: a count of
|
|
230
|
+
* plotted values, or series with points. A node that is neither (a shape this
|
|
231
|
+
* does not know) counts as plotted, so its pane keeps its line.
|
|
232
|
+
*/
|
|
233
|
+
function plotsData(node: RemoteUiNodeSnapshot): boolean {
|
|
234
|
+
if (node.role !== "chart-data") return false;
|
|
235
|
+
const metadata = node.metadata;
|
|
236
|
+
if (!isRecord(metadata)) return true;
|
|
237
|
+
const counts = [metadata.plottedValueCount, metadata.projectedPointCount].filter((count): count is number => typeof count === "number");
|
|
238
|
+
if (counts.length > 0) return counts.some((count) => count > 0);
|
|
239
|
+
const lists = [metadata.series, metadata.baseSeries, metadata.resolvedSeries].filter(Array.isArray);
|
|
240
|
+
if (lists.length === 0) return true;
|
|
241
|
+
return lists.some((list) => list.some((series) => isRecord(series) && typeof series.pointCount === "number" && series.pointCount > 0));
|
|
242
|
+
}
|
|
243
|
+
|
|
211
244
|
/**
|
|
212
245
|
* A 3D surface that is not the real drawing: the frame has no mesh, or the 2D
|
|
213
246
|
* grid stands in for it. Both are fine for a person using the pane and wrong
|
|
@@ -1,14 +1,20 @@
|
|
|
1
|
+
import { lookup } from "dns/promises";
|
|
2
|
+
import { isIP } from "net";
|
|
1
3
|
import { fetchTape } from "../../plugins/builtin/time-sales/client";
|
|
2
4
|
import {
|
|
5
|
+
encodeResponseBody,
|
|
3
6
|
readRequestInit,
|
|
4
|
-
|
|
7
|
+
toResponseHead,
|
|
5
8
|
type HttpProxyRequestEnvelope,
|
|
6
9
|
type HttpProxyResponseEnvelope,
|
|
7
10
|
} from "../../utils/http-proxy-response";
|
|
11
|
+
import { fetchCheckingRedirects, readBytesWithin } from "../../utils/http-proxy-upstream";
|
|
12
|
+
import { isPublicAddress } from "../../utils/public-address";
|
|
8
13
|
import type { DesktopPaneShotApiProxy, DesktopPaneShotBridge } from "../desktop-pane-shot";
|
|
9
14
|
import type { MarketContext } from "../types";
|
|
10
15
|
import { resolvePersistedCloudSessionToken } from "./cloud-session";
|
|
11
16
|
import { getCloudApiBaseUrl } from "../../api-client/request";
|
|
17
|
+
import { fetchShotHttpAddress } from "./screenshot-http";
|
|
12
18
|
|
|
13
19
|
const CREDENTIAL_FIELD_NAMES = new Set([
|
|
14
20
|
"accesstoken",
|
|
@@ -98,10 +104,27 @@ const SHOT_BRIDGE_MARKET_OPERATIONS = new Set([
|
|
|
98
104
|
]);
|
|
99
105
|
|
|
100
106
|
const SHOT_BRIDGE_HTTP_TIMEOUT_MS = 20_000;
|
|
107
|
+
/** Feeds and API answers are far smaller; the answer goes to the page whole, as JSON. */
|
|
108
|
+
const SHOT_BRIDGE_HTTP_MAX_BYTES = 25 * 1024 * 1024;
|
|
109
|
+
/** What panes send through `httpFetch` while they draw: reads, and the POST a few APIs take a query as. */
|
|
110
|
+
const SHOT_BRIDGE_HTTP_METHODS = new Set(["GET", "HEAD", "POST"]);
|
|
111
|
+
|
|
112
|
+
/** The addresses a host name resolves to. */
|
|
113
|
+
type ResolveHost = (hostname: string) => Promise<string[]>;
|
|
114
|
+
|
|
115
|
+
const resolveHost: ResolveHost = async (hostname) => (
|
|
116
|
+
await lookup(hostname, { all: true, verbatim: true })
|
|
117
|
+
).map((entry) => entry.address);
|
|
118
|
+
|
|
119
|
+
interface ShotHttpOptions {
|
|
120
|
+
resolveHost?: ResolveHost;
|
|
121
|
+
fetch?: typeof fetch;
|
|
122
|
+
}
|
|
101
123
|
|
|
102
124
|
export function createDesktopShotBridge(
|
|
103
125
|
context: Pick<MarketContext, "dataProvider">,
|
|
104
126
|
tapeBootstrap: typeof fetchTape = fetchTape,
|
|
127
|
+
httpOptions: ShotHttpOptions = {},
|
|
105
128
|
): DesktopPaneShotBridge {
|
|
106
129
|
return {
|
|
107
130
|
async marketData(operation, args) {
|
|
@@ -123,30 +146,88 @@ export function createDesktopShotBridge(
|
|
|
123
146
|
args,
|
|
124
147
|
);
|
|
125
148
|
},
|
|
126
|
-
httpFetch: (request) => runDesktopShotHttpFetch(request),
|
|
149
|
+
httpFetch: (request) => runDesktopShotHttpFetch(request, httpOptions),
|
|
127
150
|
};
|
|
128
151
|
}
|
|
129
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Whether the page may reach `raw`: http or https on a host whose every
|
|
155
|
+
* address is on the public internet. Panes read public APIs and news feeds;
|
|
156
|
+
* nothing they draw lives on this machine or its network, and a URL that
|
|
157
|
+
* names one (or a host that resolves to one) is refused.
|
|
158
|
+
*/
|
|
159
|
+
async function checkShotHttpTarget(
|
|
160
|
+
raw: string,
|
|
161
|
+
resolve: ResolveHost,
|
|
162
|
+
): Promise<{ url: URL; address: string } | { error: string; status: number }> {
|
|
163
|
+
let url: URL;
|
|
164
|
+
try {
|
|
165
|
+
url = new URL(raw);
|
|
166
|
+
} catch {
|
|
167
|
+
// Intentional ignore: a URL that does not parse is answered as an invalid target, never fetched.
|
|
168
|
+
return { error: "The target URL is not valid.", status: 400 };
|
|
169
|
+
}
|
|
170
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
171
|
+
return { error: `Screenshot HTTP bridge refuses ${url.protocol} requests.`, status: 400 };
|
|
172
|
+
}
|
|
173
|
+
if (url.username || url.password) return { error: "Credentials in the URL are not allowed.", status: 400 };
|
|
174
|
+
const host = url.hostname.replace(/^\[|\]$/g, "").replace(/\.$/, "").toLowerCase();
|
|
175
|
+
const refused = { error: `Screenshot HTTP bridge refuses ${url.hostname}: it is not on the public internet.`, status: 403 };
|
|
176
|
+
if (host === "localhost" || host.endsWith(".localhost")) return refused;
|
|
177
|
+
let addresses: string[];
|
|
178
|
+
try {
|
|
179
|
+
addresses = isIP(host) ? [host] : await resolve(host);
|
|
180
|
+
} catch {
|
|
181
|
+
return { error: `Could not resolve ${url.hostname}.`, status: 502 };
|
|
182
|
+
}
|
|
183
|
+
if (addresses.length === 0) return { error: `Could not resolve ${url.hostname}.`, status: 502 };
|
|
184
|
+
if (!addresses.every(isPublicAddress)) return refused;
|
|
185
|
+
const address = addresses.find((candidate) => isIP(candidate) === 4) ?? addresses[0]!;
|
|
186
|
+
return { url, address };
|
|
187
|
+
}
|
|
188
|
+
|
|
130
189
|
/**
|
|
131
190
|
* Panes such as DVD fetch a third-party API directly. A browser cannot: the
|
|
132
191
|
* request is cross-origin and dies in CORS, so the pane reported that the
|
|
133
192
|
* ticker pays no dividend. The desktop renderer already proxies these through
|
|
134
|
-
* its native half; the screenshot page gets the same treatment
|
|
193
|
+
* its native half; the screenshot page gets the same treatment, limited to
|
|
194
|
+
* public hosts, each redirect checked like the first URL.
|
|
135
195
|
*/
|
|
136
196
|
async function runDesktopShotHttpFetch(
|
|
137
197
|
request: HttpProxyRequestEnvelope,
|
|
198
|
+
options: ShotHttpOptions,
|
|
138
199
|
): Promise<HttpProxyResponseEnvelope> {
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
200
|
+
const resolve = options.resolveHost ?? resolveHost;
|
|
201
|
+
const addresses = new Map<string, string>();
|
|
202
|
+
const checkTarget = async (raw: string) => {
|
|
203
|
+
const checked = await checkShotHttpTarget(raw, resolve);
|
|
204
|
+
if ("url" in checked) addresses.set(checked.url.href, checked.address);
|
|
205
|
+
return checked;
|
|
206
|
+
};
|
|
207
|
+
const target = await checkTarget(typeof request?.url === "string" ? request.url : "");
|
|
208
|
+
if ("error" in target) throw new Error(target.error);
|
|
209
|
+
const init = readRequestInit(request.init);
|
|
210
|
+
if (!SHOT_BRIDGE_HTTP_METHODS.has(init.method)) {
|
|
211
|
+
throw new Error(`Screenshot HTTP bridge refuses ${init.method} requests.`);
|
|
142
212
|
}
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
213
|
+
const result = await fetchCheckingRedirects(target.url, init, {
|
|
214
|
+
fetch: options.fetch ?? (async (input, init) => {
|
|
215
|
+
const url = new URL(String(input));
|
|
216
|
+
const address = addresses.get(url.href);
|
|
217
|
+
if (!address) throw new Error("The screenshot target has not been checked.");
|
|
218
|
+
return fetchShotHttpAddress(url, init ?? {}, address);
|
|
219
|
+
}) as typeof fetch,
|
|
220
|
+
check: checkTarget,
|
|
149
221
|
signal: AbortSignal.timeout(SHOT_BRIDGE_HTTP_TIMEOUT_MS),
|
|
150
222
|
});
|
|
151
|
-
|
|
223
|
+
if ("error" in result) throw new Error(result.error);
|
|
224
|
+
const { response } = result;
|
|
225
|
+
const tooLarge = `The response from ${target.url.hostname} is larger than ${SHOT_BRIDGE_HTTP_MAX_BYTES / (1024 * 1024)} MB.`;
|
|
226
|
+
if (init.method !== "HEAD" && Number(response.headers.get("content-length") ?? 0) > SHOT_BRIDGE_HTTP_MAX_BYTES) {
|
|
227
|
+
await response.body?.cancel().catch(() => { /* Intentional ignore: an answer over the cap is dropped unread. */ });
|
|
228
|
+
throw new Error(tooLarge);
|
|
229
|
+
}
|
|
230
|
+
const body = await readBytesWithin(response.body, SHOT_BRIDGE_HTTP_MAX_BYTES);
|
|
231
|
+
if (body === null) throw new Error(tooLarge);
|
|
232
|
+
return { ...toResponseHead(response), ...encodeResponseBody(body) };
|
|
152
233
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { request as httpRequest } from "node:http";
|
|
2
|
+
import { request as httpsRequest } from "node:https";
|
|
3
|
+
import { isIP } from "node:net";
|
|
4
|
+
import { Readable } from "node:stream";
|
|
5
|
+
import { checkServerIdentity } from "node:tls";
|
|
6
|
+
import { createBrotliDecompress, createGunzip, createInflate } from "node:zlib";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A single HTTP hop to an address already checked by the screenshot bridge.
|
|
10
|
+
* The socket uses that address directly, while Host and TLS keep the URL's
|
|
11
|
+
* name. A fresh connection keeps another request's DNS result out of the hop.
|
|
12
|
+
*/
|
|
13
|
+
export function fetchShotHttpAddress(url: URL, init: RequestInit, address: string): Promise<Response> {
|
|
14
|
+
if (!isIP(address)) return Promise.reject(new Error("The screenshot address must be an IP address."));
|
|
15
|
+
const headers = new Headers(init.headers);
|
|
16
|
+
headers.set("host", url.host);
|
|
17
|
+
headers.set("accept-encoding", "gzip, deflate, br");
|
|
18
|
+
if (!headers.has("accept")) headers.set("accept", "*/*");
|
|
19
|
+
if (!headers.has("user-agent")) headers.set("user-agent", `Bun/${Bun.version}`);
|
|
20
|
+
if (typeof init.body === "string" && !headers.has("content-type")) headers.set("content-type", "text/plain;charset=UTF-8");
|
|
21
|
+
const requestHeaders: Record<string, string> = {};
|
|
22
|
+
headers.forEach((value, name) => { requestHeaders[name] = value; });
|
|
23
|
+
const hostname = url.hostname.replace(/^\[|\]$/g, "");
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const request = (url.protocol === "https:" ? httpsRequest : httpRequest)(url, {
|
|
26
|
+
hostname: address,
|
|
27
|
+
method: init.method,
|
|
28
|
+
headers: requestHeaders,
|
|
29
|
+
agent: false,
|
|
30
|
+
signal: init.signal ?? undefined,
|
|
31
|
+
...(url.protocol === "https:" ? {
|
|
32
|
+
servername: isIP(hostname) ? "" : hostname,
|
|
33
|
+
checkServerIdentity: (_name: string, certificate: Parameters<typeof checkServerIdentity>[1]) => checkServerIdentity(hostname, certificate),
|
|
34
|
+
} : {}),
|
|
35
|
+
}, (incoming) => {
|
|
36
|
+
try {
|
|
37
|
+
const responseHeaders = new Headers();
|
|
38
|
+
for (let index = 0; index < incoming.rawHeaders.length; index += 2) {
|
|
39
|
+
responseHeaders.append(incoming.rawHeaders[index]!, incoming.rawHeaders[index + 1]!);
|
|
40
|
+
}
|
|
41
|
+
const status = incoming.statusCode ?? 502;
|
|
42
|
+
if (init.method === "HEAD" || status === 204 || status === 205 || status === 304) {
|
|
43
|
+
incoming.resume();
|
|
44
|
+
resolve(new Response(null, { status, statusText: incoming.statusMessage, headers: responseHeaders }));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const encoding = responseHeaders.get("content-encoding")?.toLowerCase();
|
|
48
|
+
const decoder = encoding === "gzip" ? createGunzip()
|
|
49
|
+
: encoding === "deflate" ? createInflate()
|
|
50
|
+
: encoding === "br" ? createBrotliDecompress() : null;
|
|
51
|
+
if (decoder) {
|
|
52
|
+
responseHeaders.delete("content-encoding");
|
|
53
|
+
responseHeaders.delete("content-length");
|
|
54
|
+
incoming.on("error", (error) => decoder.destroy(error));
|
|
55
|
+
decoder.once("close", () => incoming.destroy());
|
|
56
|
+
}
|
|
57
|
+
const stream = decoder ? incoming.pipe(decoder) : incoming;
|
|
58
|
+
resolve(new Response(Readable.toWeb(stream) as unknown as ReadableStream<Uint8Array>, {
|
|
59
|
+
status, statusText: incoming.statusMessage, headers: responseHeaders,
|
|
60
|
+
}));
|
|
61
|
+
} catch (error) {
|
|
62
|
+
incoming.destroy();
|
|
63
|
+
reject(error);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
request.once("error", reject);
|
|
67
|
+
if (init.body !== undefined && init.body !== null) request.write(init.body);
|
|
68
|
+
request.end();
|
|
69
|
+
});
|
|
70
|
+
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { logFailure } from "../../utils/log-failure";
|
|
2
2
|
import { apiClient } from "../../api-client";
|
|
3
3
|
import { loadResolvedHeadlessPaneModel, reportCoverage, reportGaps, type LoadedHeadlessPaneModel } from "./headless";
|
|
4
|
-
import {
|
|
5
|
-
import { mkdir } from "fs/promises";
|
|
4
|
+
import { createOutputFolder } from "../output-file";
|
|
6
5
|
import type { Quote } from "../../types/financials";
|
|
7
6
|
import { isRecord } from "../../utils/guards";
|
|
8
7
|
import {
|
|
@@ -28,7 +27,18 @@ import { quotesFreshness } from "../freshness";
|
|
|
28
27
|
import { paneFitsRows, shotMaxHeightCells, buildDesktopShotPayload } from "./screenshot-payload";
|
|
29
28
|
import { resolveDesktopShotApiProxy, createDesktopShotBridge } from "./screenshot-bridge";
|
|
30
29
|
import type { PaneScreenshotResult } from "./screenshot-types";
|
|
31
|
-
import { assessPaneScreenshot } from "./screenshot-assessment";
|
|
30
|
+
import { assessPaneScreenshot, shotLoadedNothing } from "./screenshot-assessment";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Whether a capture's footer keeps a row for the dated status line. A pane of
|
|
34
|
+
* the user's own state (notes, connections, documentation) declares `local`:
|
|
35
|
+
* it has no data to date, and "not a live feed" would be said of its own
|
|
36
|
+
* state. A capture that loaded nothing gives the row back later
|
|
37
|
+
* (`shotLoadedNothing`).
|
|
38
|
+
*/
|
|
39
|
+
export function shotHasStatusLine(resolved: Pick<ResolvedPaneFunction, "pane" | "template">): boolean {
|
|
40
|
+
return !declaredReportFreshness(resolved).local;
|
|
41
|
+
}
|
|
32
42
|
|
|
33
43
|
/** The bridge, keeping every quote it serves the page: what a pane that loads its own quotes (WEI) shows. */
|
|
34
44
|
function recordBridgedQuotes(bridge: DesktopPaneShotBridge): { bridge: DesktopPaneShotBridge; quotes: Quote[] } {
|
|
@@ -241,7 +251,7 @@ export async function renderDesktopShot({
|
|
|
241
251
|
*/
|
|
242
252
|
onStalledReport?: () => void;
|
|
243
253
|
}): Promise<PaneScreenshotResult> {
|
|
244
|
-
if (captureImage) await
|
|
254
|
+
if (captureImage) await createOutputFolder(outputPath);
|
|
245
255
|
const apiProxy = resolveDesktopShotApiProxy(context);
|
|
246
256
|
const previousSessionToken = apiClient.getSessionToken();
|
|
247
257
|
let payload: DesktopPaneShotPayload;
|
|
@@ -277,7 +287,7 @@ export async function renderDesktopShot({
|
|
|
277
287
|
watermark ?? null,
|
|
278
288
|
loadModel,
|
|
279
289
|
);
|
|
280
|
-
if (statusLine && captureImage) payload.statusLine = true;
|
|
290
|
+
if (statusLine && captureImage && shotHasStatusLine(resolved)) payload.statusLine = true;
|
|
281
291
|
const fitRoles = [
|
|
282
292
|
...(fitRows && paneFitsRows(resolved) ? ["data-table-body-scroll"] : []),
|
|
283
293
|
...(fitReader && resolved.pane.shotFitsReader ? ["detail-scroll-body"] : []),
|
|
@@ -296,6 +306,10 @@ export async function renderDesktopShot({
|
|
|
296
306
|
captureImage,
|
|
297
307
|
bridge: recorded.bridge,
|
|
298
308
|
statusLine: async (rendered) => {
|
|
309
|
+
if (shotLoadedNothing(rendered)) {
|
|
310
|
+
abandonReports();
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
299
313
|
const tab = report && shownReportTab(resolved, rendered.semanticUi);
|
|
300
314
|
const shown = tab ? reportShotFreshness(resolved, () => loadReport({ ...resolved, options: { ...resolved.options, tab } })) : report;
|
|
301
315
|
const line = shotFreshness(resolved, shotPayload, rendered, recorded.quotes, shown && await settledWithin(shown, SHOT_REPORT_WAIT_MS));
|
package/src/cli/quote-answer.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { QuoteBatchResult } from "../types/data-provider";
|
|
|
2
2
|
import type { Quote } from "../types/financials";
|
|
3
3
|
import { bareListingNote } from "../tickers/listing-fate";
|
|
4
4
|
import { notATickerSymbol } from "../sources/provider-errors";
|
|
5
|
+
import { listingSuffixCandidate } from "../sources/listing-symbols";
|
|
5
6
|
import { looksLikeCurrencyPair } from "../utils/currency-pair";
|
|
6
7
|
import { canonicalExchange } from "../utils/exchanges";
|
|
7
8
|
import type { CliCommandDef } from "../types/plugin";
|
|
@@ -102,6 +103,9 @@ export async function answerQuote(
|
|
|
102
103
|
const notTraded = await notTradedMessage(listing, market);
|
|
103
104
|
if (notTraded) return { result, listing, failure: { message: notTraded, oneLine: notTraded }, notes: [] };
|
|
104
105
|
if (!isNoProviderError(result.error)) return { result, listing, notes: [] };
|
|
106
|
+
// HCI.L is HCI on LSE written with a vendor's suffix: one that does not trade there says so as HCI:LSE does.
|
|
107
|
+
const suffixed = await notTradedOnSuffixVenue(listing, market);
|
|
108
|
+
if (suffixed) return { result, listing, failure: { message: suffixed, oneLine: suffixed }, notes: [] };
|
|
105
109
|
// A word the service marks as a company's name is that company, before the symbol is read on its listings abroad.
|
|
106
110
|
const named = !listing.exchange && listing.typed ? await serviceNamedCompany(listing.typed, market) : null;
|
|
107
111
|
const meant = named ? await answerNamedListing(named, market, request, refresh, datesChangeBasis) : null;
|
|
@@ -137,6 +141,22 @@ export async function answerQuote(
|
|
|
137
141
|
return { result, listing, failure: { message, details, oneLine }, notes: [] };
|
|
138
142
|
}
|
|
139
143
|
|
|
144
|
+
/**
|
|
145
|
+
* "HCI does not trade on LSE. HCI trades on: NYSE, IEX, JSE." for a symbol
|
|
146
|
+
* written with a venue's suffix (HCI.L) that no source quotes, when its base
|
|
147
|
+
* symbol trades elsewhere and not on the venue the suffix names: the answer
|
|
148
|
+
* `HCI:LSE` gets. Only for London's `.L` and suffixes of two letters or more
|
|
149
|
+
* (`.IS`, `.JO`): another one-letter suffix is as likely a US share class
|
|
150
|
+
* (MKC.V, BRK.B) and keeps its own answer. Null otherwise.
|
|
151
|
+
*/
|
|
152
|
+
async function notTradedOnSuffixVenue(listing: CliListing, market: MarketContext): Promise<string | null> {
|
|
153
|
+
if (listing.exchange) return null;
|
|
154
|
+
const suffix = listing.symbol.slice(listing.symbol.lastIndexOf(".") + 1);
|
|
155
|
+
if (!listing.symbol.includes(".") || (suffix.length < 2 && suffix.toUpperCase() !== "L")) return null;
|
|
156
|
+
const venue = listingSuffixCandidate(listing.symbol);
|
|
157
|
+
return venue ? notTradedMessage(venue, market) : null;
|
|
158
|
+
}
|
|
159
|
+
|
|
140
160
|
/**
|
|
141
161
|
* A company's name typed as one word (`Apple`) that the source says is not a
|
|
142
162
|
* ticker, answered as the listing search names for it with the line saying so
|
package/src/cli/registry.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type {
|
|
|
6
6
|
GloomPlugin,
|
|
7
7
|
} from "../types/plugin";
|
|
8
8
|
import type { LoadedExternalPlugin } from "../plugins/loader";
|
|
9
|
-
import {
|
|
9
|
+
import { getBuiltinCliCommands } from "../plugins/catalog-cli";
|
|
10
10
|
import { initCliServices, initConfigData, initMarketData } from "./context";
|
|
11
11
|
import { closeAndFail, fail } from "./errors";
|
|
12
12
|
import { DEFAULT_CLI_OPTIONS, type CliGlobalOptions } from "./options";
|
|
@@ -38,11 +38,14 @@ export interface CliCommandRegistry {
|
|
|
38
38
|
config: AppConfig | null;
|
|
39
39
|
plugins: GloomPlugin[];
|
|
40
40
|
externalPlugins: LoadedExternalPlugin[];
|
|
41
|
+
/** Loads installed contributions when dispatch or application services need them. */
|
|
42
|
+
ensureExternalPlugins?: () => Promise<void>;
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
interface BuildCliCommandRegistryOptions {
|
|
44
46
|
coreCommands: CliCommandDef[];
|
|
45
47
|
externalPlugins?: LoadedExternalPlugin[];
|
|
48
|
+
loadExternalPlugins?: () => Promise<LoadedExternalPlugin[]>;
|
|
46
49
|
config?: AppConfig | null;
|
|
47
50
|
}
|
|
48
51
|
|
|
@@ -72,11 +75,11 @@ export function normalizeCliDispatchResult(result: void | CliDispatchResult): Cl
|
|
|
72
75
|
export function buildCliCommandRegistry({
|
|
73
76
|
coreCommands,
|
|
74
77
|
externalPlugins = [],
|
|
78
|
+
loadExternalPlugins,
|
|
75
79
|
config = null,
|
|
76
80
|
}: BuildCliCommandRegistryOptions): CliCommandRegistry {
|
|
77
81
|
const allCommands: RegisteredCliCommand[] = [];
|
|
78
82
|
const allTokens = new Map<string, RegisteredCliCommand>();
|
|
79
|
-
const catalog = getPluginCatalog(externalPlugins);
|
|
80
83
|
const disabledPlugins = new Set(config?.disabledPlugins ?? []);
|
|
81
84
|
|
|
82
85
|
const registerCommand = (
|
|
@@ -110,8 +113,10 @@ export function buildCliCommandRegistry({
|
|
|
110
113
|
registerCommand(command, "core", "core", true);
|
|
111
114
|
}
|
|
112
115
|
|
|
113
|
-
const
|
|
114
|
-
|
|
116
|
+
for (const { pluginId, commands } of getBuiltinCliCommands()) {
|
|
117
|
+
for (const command of commands) registerCommand(command, pluginId, "plugin", true);
|
|
118
|
+
}
|
|
119
|
+
for (const entry of externalPlugins) {
|
|
115
120
|
if (entry.error) {
|
|
116
121
|
registryLog.warn(`Skipping external plugin "${entry.plugin.id}" for CLI registration.`, {
|
|
117
122
|
path: entry.path,
|
|
@@ -119,9 +124,8 @@ export function buildCliCommandRegistry({
|
|
|
119
124
|
});
|
|
120
125
|
continue;
|
|
121
126
|
}
|
|
122
|
-
loadablePlugins.push(entry.plugin);
|
|
123
127
|
for (const command of entry.plugin.cliCommands ?? []) {
|
|
124
|
-
registerCommand(command, entry.plugin.id, "plugin",
|
|
128
|
+
registerCommand(command, entry.plugin.id, "plugin", false);
|
|
125
129
|
}
|
|
126
130
|
}
|
|
127
131
|
|
|
@@ -137,30 +141,54 @@ export function buildCliCommandRegistry({
|
|
|
137
141
|
}
|
|
138
142
|
}
|
|
139
143
|
|
|
140
|
-
|
|
144
|
+
let plugins: GloomPlugin[] | undefined;
|
|
145
|
+
let loading: Promise<void> | undefined;
|
|
146
|
+
const registry: CliCommandRegistry = {
|
|
141
147
|
commands,
|
|
142
148
|
lookup,
|
|
143
149
|
config,
|
|
144
|
-
plugins
|
|
150
|
+
get plugins() {
|
|
151
|
+
const { getPluginCatalog } = require("../plugins/catalog") as typeof import("../plugins/catalog");
|
|
152
|
+
return plugins ??= getPluginCatalog(registry.externalPlugins)
|
|
153
|
+
.filter((entry) => !entry.error && !disabledPlugins.has(entry.plugin.id))
|
|
154
|
+
.map((entry) => entry.plugin);
|
|
155
|
+
},
|
|
145
156
|
externalPlugins,
|
|
157
|
+
...(loadExternalPlugins ? { ensureExternalPlugins: () => loading ??= (async () => {
|
|
158
|
+
const loaded = await loadExternalPlugins();
|
|
159
|
+
const expanded = buildCliCommandRegistry({ coreCommands, externalPlugins: loaded, config });
|
|
160
|
+
registry.commands = expanded.commands;
|
|
161
|
+
registry.lookup = expanded.lookup;
|
|
162
|
+
registry.externalPlugins = loaded;
|
|
163
|
+
// A command can retain ctx.plugins before opening market data (search does).
|
|
164
|
+
// Keep that reference current without materializing panes for command-only work.
|
|
165
|
+
if (plugins) plugins.splice(0, plugins.length, ...expanded.plugins);
|
|
166
|
+
})() } : {}),
|
|
146
167
|
};
|
|
168
|
+
return registry;
|
|
147
169
|
}
|
|
148
170
|
|
|
149
171
|
export function createCliCommandContext(
|
|
150
172
|
ownerId: string,
|
|
151
|
-
registryOrPlugins: Pick<CliCommandRegistry, "plugins" | "externalPlugins"> | GloomPlugin[],
|
|
173
|
+
registryOrPlugins: Pick<CliCommandRegistry, "plugins" | "externalPlugins" | "ensureExternalPlugins"> | GloomPlugin[],
|
|
152
174
|
cliOptions: CliGlobalOptions = DEFAULT_CLI_OPTIONS,
|
|
153
175
|
args: readonly string[] = [],
|
|
154
176
|
): CliCommandContext {
|
|
155
|
-
const registry = Array.isArray(registryOrPlugins)
|
|
177
|
+
const registry: Pick<CliCommandRegistry, "plugins" | "externalPlugins" | "ensureExternalPlugins"> = Array.isArray(registryOrPlugins)
|
|
156
178
|
? { plugins: registryOrPlugins, externalPlugins: [] }
|
|
157
179
|
: registryOrPlugins;
|
|
158
180
|
return {
|
|
159
181
|
initConfigData,
|
|
160
|
-
initMarketData: () =>
|
|
161
|
-
|
|
182
|
+
initMarketData: async () => {
|
|
183
|
+
await registry.ensureExternalPlugins?.();
|
|
184
|
+
return initMarketData({ plugins: registry.plugins });
|
|
185
|
+
},
|
|
186
|
+
initServices: async () => {
|
|
187
|
+
await registry.ensureExternalPlugins?.();
|
|
188
|
+
return initCliServices({ externalPlugins: registry.externalPlugins });
|
|
189
|
+
},
|
|
162
190
|
cliOptions,
|
|
163
|
-
plugins
|
|
191
|
+
get plugins() { return registry.plugins; },
|
|
164
192
|
fail,
|
|
165
193
|
closeAndFail,
|
|
166
194
|
output: {
|
package/src/cli/result.ts
CHANGED
|
@@ -93,6 +93,8 @@ export interface CliResultRenderOptions<T = unknown, Row = Record<string, unknow
|
|
|
93
93
|
heading?: string;
|
|
94
94
|
/** Text-mode line printed under the rows, such as where a listing stops. Not printed with no rows: say it in `empty`. */
|
|
95
95
|
footnote?: string;
|
|
96
|
+
/** Text-mode line under each table row, indented and muted, cut to the width: a story's summary under its headline. */
|
|
97
|
+
rowNote?: (row: Row) => string | null | undefined;
|
|
96
98
|
/** Text-mode block printed under the rows and footnote, for figures about a row that are not columns. */
|
|
97
99
|
details?: (data: T) => string;
|
|
98
100
|
/**
|
|
@@ -446,6 +448,7 @@ function formatTextCell<Row extends Record<string, unknown>>(
|
|
|
446
448
|
function renderTextTable<Row extends Record<string, unknown>>(
|
|
447
449
|
rows: Row[],
|
|
448
450
|
columns?: CliResultColumn<Row>[],
|
|
451
|
+
rowNote?: (row: Row) => string | null | undefined,
|
|
449
452
|
): string {
|
|
450
453
|
const resolvedColumns = textColumns(rows, columns);
|
|
451
454
|
const cells = rows.map((row) => resolvedColumns.map((column) => formatTextCell(column, row, "table")));
|
|
@@ -469,6 +472,7 @@ function renderTextTable<Row extends Record<string, unknown>>(
|
|
|
469
472
|
stackRows: column.stackRows,
|
|
470
473
|
})),
|
|
471
474
|
cells.map((row) => shown.map(({ index }) => row[index]!)),
|
|
475
|
+
rowNote ? { below: rows.map((row) => { const note = rowNote(row); return note ? [note] : []; }), cutBelow: true } : {},
|
|
472
476
|
);
|
|
473
477
|
}
|
|
474
478
|
|
|
@@ -581,15 +585,16 @@ function serializeCliRows<T, Row extends Record<string, unknown>>(
|
|
|
581
585
|
if (renderOptions.text) {
|
|
582
586
|
return renderOptions.text(result.data);
|
|
583
587
|
}
|
|
584
|
-
|
|
585
|
-
const
|
|
588
|
+
// The heading's lines, the summary and the notes are prose: each wraps to a narrow --width, as the source line does.
|
|
589
|
+
const summary = wrapToTerminal(renderOptions.summary?.(result.data) ?? "");
|
|
590
|
+
const withHeading = (text: string) => renderOptions.heading ? `${wrapToTerminal(renderOptions.heading)}\n${text}` : text;
|
|
586
591
|
if (rows.length === 0) {
|
|
587
|
-
return withHeading(summary || cliStyles.muted(renderOptions.empty ?? "No results."));
|
|
592
|
+
return withHeading(summary || cliStyles.muted(wrapToTerminal(renderOptions.empty ?? "No results.")));
|
|
588
593
|
}
|
|
589
594
|
const table = renderTextRows(rows as Row[], result.data, renderOptions);
|
|
590
595
|
const notes = [window.note, renderOptions.footnote].filter((note): note is string => !!note);
|
|
591
596
|
const details = renderOptions.details?.(result.data) ?? "";
|
|
592
|
-
const body = [table, ...notes.map((note) => cliStyles.muted(note)), ...details ? ["", details] : []].join("\n");
|
|
597
|
+
const body = [table, ...notes.map((note) => cliStyles.muted(wrapToTerminal(note))), ...details ? ["", details] : []].join("\n");
|
|
593
598
|
return withHeading(summary ? `${summary}\n\n${body}` : body);
|
|
594
599
|
}
|
|
595
600
|
|
|
@@ -607,7 +612,7 @@ function renderTextRows<T, Row extends Record<string, unknown>>(
|
|
|
607
612
|
}
|
|
608
613
|
return layout === "record"
|
|
609
614
|
? renderTextRecords(rows as Row[], columns)
|
|
610
|
-
: renderTextTable(rows as Row[], columns);
|
|
615
|
+
: renderTextTable(rows as Row[], columns, renderOptions.rowNote);
|
|
611
616
|
}
|
|
612
617
|
|
|
613
618
|
/** Settles once the result `printCliResult` last wrote has left the process: a pipe can hold it back. */
|
|
@@ -640,8 +645,9 @@ export function printCliResult<T, Row extends Record<string, unknown> = Record<s
|
|
|
640
645
|
// JSON carries warnings in its envelope. Text, CSV and NDJSON are only the
|
|
641
646
|
// rows, so a warning goes to stderr, out of a file the rows are piped into.
|
|
642
647
|
if (options.format !== "json" && !options.quiet) {
|
|
648
|
+
// Each wraps to a narrow --width, as a note does.
|
|
643
649
|
for (const warning of result.warnings ?? []) {
|
|
644
|
-
console.error(`${cliStyles.warning("warning:")} ${hinted(warning)}`);
|
|
650
|
+
console.error(wrapToTerminal(`${cliStyles.warning("warning:")} ${hinted(warning)}`));
|
|
645
651
|
}
|
|
646
652
|
}
|
|
647
653
|
// CSV says what its rows do not in closing `# note:` lines; NDJSON has no place for one among its rows,
|
|
@@ -139,7 +139,8 @@ export interface CliTickerOptions {
|
|
|
139
139
|
*/
|
|
140
140
|
export class CoinListingError extends ListingArgError {
|
|
141
141
|
constructor(message: string, details: string, readonly hint: string) {
|
|
142
|
-
|
|
142
|
+
// A listing message that already ends in the coin note (`describeOtherListings`) keeps it once.
|
|
143
|
+
super(message, details.endsWith(hint) ? details : `${details}\n${hint}`);
|
|
143
144
|
this.name = "CoinListingError";
|
|
144
145
|
}
|
|
145
146
|
}
|
package/src/cli/types.ts
CHANGED
|
@@ -21,6 +21,8 @@ export type MarketContext = ConfigContext & {
|
|
|
21
21
|
seriesCapabilities?: CapabilityInvoker;
|
|
22
22
|
/** The catalog series charted as an expression (`FRED:DFII10`), in a report run without the plugin runtime. */
|
|
23
23
|
findCatalogSeries?: (expression: string) => import("../series/types").SeriesDefinition | undefined;
|
|
24
|
+
/** The catalog series a bare word is an alias of (`hashrate`), in a report run without the plugin runtime. */
|
|
25
|
+
findCatalogSeriesNamed?: (word: string) => import("../series/types").SeriesDefinition | undefined;
|
|
24
26
|
/** `--refresh`: prefetched data bypasses the local cache. */
|
|
25
27
|
refresh?: boolean;
|
|
26
28
|
/** Filings behind the statements and a bank's figures, read from Gloom Cloud; absent where the data service is a stand-in. */
|
|
@@ -49,6 +49,7 @@ function formatFullCurrencyValue(
|
|
|
49
49
|
options: MarketFormatOptions = {},
|
|
50
50
|
): string | null {
|
|
51
51
|
if (unit.trim() === INDEX_POINTS_UNIT) return formatMarketPrice(value, { ...options, assetCategory, priceBasis: "points" });
|
|
52
|
+
if (unit.trim() === "32nds") return formatMarketPrice(value, { ...options, assetCategory, priceBasis: "thirty-seconds" });
|
|
52
53
|
const currency = unitCurrencyCode(unit);
|
|
53
54
|
return currency ? formatMarketPriceWithCurrency(value, currency, { ...options, assetCategory }) : null;
|
|
54
55
|
}
|
|
@@ -362,6 +363,8 @@ export function formatCompositeAxisValue(value: number, domain: CompositeAxisDom
|
|
|
362
363
|
// Derived dimensions are included in the legend value. Axis labels stay
|
|
363
364
|
// numeric so a narrow gutter cannot truncate USD/JPY into a false USD label.
|
|
364
365
|
if (group.startsWith("derived-unit:")) return compact;
|
|
366
|
+
// Treasury prices keep their whole points and 32nds at every zoom level.
|
|
367
|
+
if (domain.unit.trim() === "32nds") return formatAxisPriceValue(value, domain);
|
|
365
368
|
// Compact labels suit a wide view, but a zoomed one can hold several ticks
|
|
366
369
|
// inside a single rounding step and print one repeated price down the gutter.
|
|
367
370
|
// Where that happens the axis spends the digits needed to tell them apart,
|