@gloom-sh/terminal 0.18.5 → 0.19.1
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 +1 -1
- package/README.zh-CN.md +1 -1
- package/package.json +1 -1
- package/patches/electrobun@1.18.1.patch +163 -8
- package/src/api-client/app-attention.ts +3 -0
- package/src/api-client/beneficial-owners.ts +3 -0
- package/src/api-client/company-kpis.ts +3 -0
- package/src/api-client/cot.ts +6 -2
- package/src/api-client/credit-documents.ts +6 -0
- package/src/api-client/data.ts +19 -7
- package/src/api-client/equity-screener.ts +24 -0
- package/src/api-client/fund-facts.ts +45 -4
- package/src/api-client/hiring.ts +3 -0
- package/src/api-client/index.ts +15 -0
- package/src/api-client/ipo.ts +24 -1
- package/src/api-client/market-discovery.ts +10 -0
- package/src/api-client/market-movers.ts +7 -0
- package/src/api-client/members.ts +1 -1
- package/src/api-client/mna.ts +3 -0
- package/src/api-client/paths.ts +9 -1
- package/src/api-client/perps.ts +43 -0
- package/src/api-client/research-activity.ts +3 -2
- package/src/api-client/roadmap.ts +85 -0
- package/src/api-client/types.ts +82 -2
- package/src/app/pane-runtime/plugin-bindings.ts +9 -5
- package/src/app/runtime/broker-import.ts +0 -2
- package/src/app/runtime/plugin-bindings.ts +73 -56
- package/src/architecture/core-builtin-allowlist.json +2 -2
- package/src/architecture/pane-tabs-allowlist.json +3 -0
- package/src/architecture/ttl-caches-allowlist.json +19 -0
- package/src/brokers/sync-broker-instance.ts +2 -8
- package/src/capabilities/chart-series.ts +1 -1
- package/src/cli/commands/crypto-hints.ts +40 -24
- package/src/cli/commands/market.ts +107 -38
- package/src/cli/commands/news-search.ts +22 -0
- package/src/cli/commands/options-chain.ts +48 -7
- package/src/cli/commands/options-leaps.ts +9 -3
- package/src/cli/commands/overview.ts +147 -32
- package/src/cli/commands/search.ts +21 -1
- package/src/cli/commands/themes.ts +11 -2
- package/src/cli/commands/ticker.ts +87 -36
- package/src/cli/company-name-hint.ts +41 -0
- package/src/cli/earnings-calendar.ts +1 -1
- package/src/cli/errors.ts +25 -2
- package/src/cli/glossary.ts +31 -2
- package/src/cli/history-rows.ts +57 -9
- package/src/cli/index-roots.ts +0 -7
- package/src/cli/index.ts +2 -0
- package/src/cli/listing-arg.ts +235 -60
- package/src/cli/output-file.ts +68 -0
- package/src/cli/pane-functions/capabilities.ts +4 -0
- package/src/cli/pane-functions/catalog.ts +80 -7
- package/src/cli/pane-functions/discovery.ts +21 -1
- package/src/cli/pane-functions/dom.ts +13 -3
- package/src/cli/pane-functions/freshness.ts +93 -4
- package/src/cli/pane-functions/function-help.ts +20 -10
- package/src/cli/pane-functions/headless.ts +17 -7
- package/src/cli/pane-functions/index.ts +54 -16
- package/src/cli/pane-functions/market-session.ts +66 -4
- package/src/cli/pane-functions/options.ts +19 -1
- package/src/cli/pane-functions/resolver.ts +19 -8
- package/src/cli/pane-functions/screenshot-assessment.ts +6 -4
- package/src/cli/pane-functions/screenshot-types.ts +5 -0
- package/src/cli/quote-answer.ts +22 -11
- package/src/cli/saved-copy-persistence.ts +88 -0
- package/src/cli/ticker-resolution.ts +3 -2
- package/src/components/chart/composite/composite-chart.tsx +4 -0
- package/src/components/chart/composite/interactions.ts +6 -0
- package/src/components/chart/composite/legend.tsx +45 -7
- package/src/components/chart/composite/panel-gestures.ts +33 -0
- package/src/components/chart/composite/panel-surface.tsx +52 -3
- package/src/components/chart/composite/rasterizer.ts +24 -13
- package/src/components/chart/composite/scene.ts +39 -4
- package/src/components/chart/composite/series-cues.ts +14 -0
- package/src/components/chart/composite/text-renderer.ts +9 -2
- package/src/components/chart/composite/tool-catalog.ts +36 -0
- package/src/components/chart/composite/tools.ts +203 -44
- package/src/components/chart/composite/use-composite-chart-keyboard.ts +11 -4
- package/src/components/command-bar/assist/inventory.ts +2 -1
- package/src/components/command-bar/commands/direct/index.ts +28 -13
- package/src/components/command-bar/list/model.ts +5 -0
- package/src/components/command-bar/pane-templates/items.ts +3 -2
- package/src/components/command-bar/routes/root/runtime.ts +6 -7
- package/src/components/command-bar/selection-runtime.ts +52 -3
- package/src/components/command-bar/surface/index.tsx +2 -0
- package/src/components/command-bar/workflow/collection-actions.ts +3 -3
- package/src/components/layout/pane/capture-stamp.ts +2 -2
- package/src/components/layout/pane/footer/index.tsx +22 -6
- package/src/components/layout/pane/footer/model.ts +83 -15
- package/src/components/layout/pane/status-footer.ts +35 -2
- package/src/components/layout/status-widget-space.ts +9 -0
- package/src/components/onboarding/onboarding-wizard.tsx +4 -1
- package/src/components/onboarding/portfolio-step/positions-panel.tsx +12 -7
- package/src/components/onboarding/wizard-broker-sync.ts +0 -1
- package/src/components/onboarding/wizard-positions.ts +3 -3
- package/src/components/ticker/list-table-view.tsx +8 -21
- package/src/components/ui/choice-dialog.tsx +23 -18
- package/src/components/ui/choice-groups.ts +18 -0
- package/src/components/ui/data-table/opentui/index.tsx +9 -7
- package/src/components/ui/data-table/opentui/model.ts +26 -1
- package/src/components/ui/data-table/types.ts +2 -0
- package/src/components/ui/display.tsx +18 -3
- package/src/components/ui/icon.tsx +4 -1
- package/src/components/ui/list-view.tsx +8 -4
- package/src/components/ui/query-bar-keys.tsx +2 -0
- package/src/components/ui/query-bar.tsx +1 -0
- package/src/components/ui/select-button.tsx +5 -1
- package/src/components/ui/select-field.tsx +1 -0
- package/src/components/ui/table-layout.ts +8 -5
- package/src/core/app-runtime.ts +2 -0
- package/src/core/app-services.ts +2 -2
- package/src/data/config/chart-settings.ts +1 -1
- package/src/data/json-ticker-repository.ts +17 -8
- package/src/data/plugin-cache.ts +5 -3
- package/src/data/saved-copies.ts +8 -0
- package/src/data/ticker-repository.ts +16 -9
- package/src/data/ticker-writes.ts +81 -0
- package/src/data/ttl-cache.ts +165 -0
- package/src/docs/snapshot.json +103 -142
- package/src/i18n/es.ts +20 -6
- package/src/i18n/ja.ts +20 -6
- package/src/i18n/ko.ts +20 -6
- package/src/i18n/zh-cn.ts +20 -6
- package/src/i18n/zh-tw.ts +20 -6
- package/src/market-data/depositary-pair.ts +27 -3
- package/src/market-data/fund-facts.ts +50 -0
- package/src/market-data/market/change-basis.ts +16 -3
- package/src/market-data/market/format.ts +25 -0
- package/src/market-data/market/freshness.ts +2 -1
- package/src/market-data/options-alternatives.ts +81 -4
- package/src/market-data/quotes/metadata.ts +7 -0
- package/src/news/aggregator.ts +119 -9
- package/src/news/cloud-session.ts +21 -0
- package/src/news/hooks.ts +1 -0
- package/src/news/news-model.ts +1 -0
- package/src/news/types.ts +17 -0
- package/src/plugins/builtin/alerts/alert-engine.ts +20 -1
- package/src/plugins/builtin/alerts/alert-log.ts +33 -0
- package/src/plugins/builtin/alerts/calendar-events.ts +10 -4
- package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
- package/src/plugins/builtin/alerts/cli.ts +362 -80
- package/src/plugins/builtin/alerts/command.ts +25 -1
- package/src/plugins/builtin/alerts/event-rows.ts +24 -3
- package/src/plugins/builtin/alerts/events-pane.tsx +34 -10
- package/src/plugins/builtin/alerts/fired-log.ts +73 -9
- package/src/plugins/builtin/alerts/format.ts +75 -5
- package/src/plugins/builtin/alerts/headless.ts +34 -12
- package/src/plugins/builtin/alerts/history-rows.ts +3 -2
- package/src/plugins/builtin/alerts/history.ts +1 -1
- package/src/plugins/builtin/alerts/index.tsx +79 -16
- package/src/plugins/builtin/alerts/metric-command.ts +77 -4
- package/src/plugins/builtin/alerts/metric-data.ts +54 -13
- package/src/plugins/builtin/alerts/metric-model.ts +163 -14
- package/src/plugins/builtin/alerts/metric-parity-fixture.json +1 -1
- package/src/plugins/builtin/alerts/pane.tsx +61 -23
- package/src/plugins/builtin/alerts/quotes.ts +6 -0
- package/src/plugins/builtin/alerts/storage.ts +12 -8
- package/src/plugins/builtin/alerts/types.ts +4 -0
- package/src/plugins/builtin/analytics/index.tsx +2 -1
- package/src/plugins/builtin/analytics/risk-client.ts +16 -5
- package/src/plugins/builtin/analytics/risk-fx.ts +70 -15
- package/src/plugins/builtin/analytics/risk-headless.ts +27 -0
- package/src/plugins/builtin/analytics/risk-model.ts +55 -9
- package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
- package/src/plugins/builtin/attention/index.tsx +2 -1
- package/src/plugins/builtin/attention/listing.ts +14 -0
- package/src/plugins/builtin/broker-manager/index.tsx +7 -3
- package/src/plugins/builtin/buildout/cells.tsx +12 -4
- package/src/plugins/builtin/buildout/format.ts +38 -0
- package/src/plugins/builtin/buildout/model/normalizers.ts +36 -1
- package/src/plugins/builtin/buildout/pane/index.tsx +4 -1
- package/src/plugins/builtin/buildout/table-model.ts +23 -2
- package/src/plugins/builtin/builtin-plugin-meta.ts +6 -2
- package/src/plugins/builtin/cds/client.ts +142 -30
- package/src/plugins/builtin/cds/headless.ts +35 -8
- package/src/plugins/builtin/cds/model.ts +145 -18
- package/src/plugins/builtin/cds/pane.tsx +16 -5
- package/src/plugins/builtin/changelog/index.tsx +9 -6
- package/src/plugins/builtin/chart-composer/catalog-inventory.ts +3 -0
- package/src/plugins/builtin/chart-composer/cli-options.ts +4 -0
- package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +7 -0
- package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -2
- package/src/plugins/builtin/chart-composer/follow-binding.ts +14 -2
- package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -1
- package/src/plugins/builtin/chart-composer/headless.ts +6 -1
- package/src/plugins/builtin/chart-composer/index.tsx +5 -6
- package/src/plugins/builtin/chart-composer/pane.tsx +37 -8
- package/src/plugins/builtin/chart-composer/settings.ts +27 -0
- package/src/plugins/builtin/chat/content/command-suggestions.ts +1 -0
- package/src/plugins/builtin/chat/controller/index.ts +5 -1
- package/src/plugins/builtin/chat/controller/notifications.ts +33 -3
- package/src/plugins/builtin/chat/controller/state.ts +25 -2
- package/src/plugins/builtin/chat/controller/unread-people.ts +89 -23
- package/src/plugins/builtin/chat/controller/utils.ts +7 -2
- package/src/plugins/builtin/chat/pane-links/commands.ts +7 -1
- package/src/plugins/builtin/chat/status-preview.ts +79 -0
- package/src/plugins/builtin/chat/status-widget.tsx +147 -10
- package/src/plugins/builtin/chat/unread-badge.ts +51 -10
- package/src/plugins/builtin/clinical-trials/client.ts +49 -4
- package/src/plugins/builtin/clinical-trials/model.ts +86 -31
- package/src/plugins/builtin/clinical-trials/pane.tsx +61 -49
- package/src/plugins/builtin/clinical-trials/sponsor-listing.ts +90 -0
- package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +95 -15
- package/src/plugins/builtin/clinical-trials/trial-pages.ts +50 -0
- package/src/plugins/builtin/cloud/askg/manifest.ts +2 -0
- package/src/plugins/builtin/company-attention/building.tsx +5 -1
- package/src/plugins/builtin/company-attention/client.ts +8 -9
- package/src/plugins/builtin/company-attention/evidence.ts +4 -1
- package/src/plugins/builtin/company-attention/headless.ts +2 -1
- package/src/plugins/builtin/company-attention/moves.tsx +5 -1
- package/src/plugins/builtin/company-attention/pane.tsx +12 -2
- package/src/plugins/builtin/company-attention/signals.tsx +5 -1
- package/src/plugins/builtin/company-kpis/headless.ts +8 -3
- package/src/plugins/builtin/company-kpis/model.ts +73 -5
- package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
- package/src/plugins/builtin/composite-plugins.ts +2 -1
- package/src/plugins/builtin/correlation/headless.ts +13 -5
- package/src/plugins/builtin/correlation/history.ts +22 -0
- package/src/plugins/builtin/cot/client.ts +22 -18
- package/src/plugins/builtin/cot/headless.ts +15 -10
- package/src/plugins/builtin/cot/model.ts +14 -1
- package/src/plugins/builtin/cot/pane.tsx +22 -14
- package/src/plugins/builtin/credit-boards/client.ts +6 -10
- package/src/plugins/builtin/credit-conditions/headless.ts +8 -2
- package/src/plugins/builtin/credit-conditions/index.tsx +5 -1
- package/src/plugins/builtin/credit-conditions/model.ts +4 -0
- package/src/plugins/builtin/credit-conditions/series.ts +3 -1
- package/src/plugins/builtin/credit-documents/detail.tsx +2 -2
- package/src/plugins/builtin/credit-documents/export.ts +173 -0
- package/src/plugins/builtin/credit-documents/headless.ts +10 -10
- package/src/plugins/builtin/credit-documents/model.ts +104 -14
- package/src/plugins/builtin/credit-documents/pane.tsx +12 -5
- package/src/plugins/builtin/custom-view/sources.ts +2 -0
- package/src/plugins/builtin/dividend-yield/client.ts +131 -23
- package/src/plugins/builtin/dividend-yield/headless.ts +44 -16
- package/src/plugins/builtin/dividend-yield/model.ts +8 -5
- package/src/plugins/builtin/dividend-yield/pane.tsx +38 -13
- package/src/plugins/builtin/dividend-yield/ranking.ts +37 -14
- package/src/plugins/builtin/dividend-yield/types.ts +20 -2
- package/src/plugins/builtin/dividend-yield/view.ts +30 -2
- package/src/plugins/builtin/docs/targets.ts +5 -1
- package/src/plugins/builtin/doe/headless.ts +3 -2
- package/src/plugins/builtin/doe/pane.tsx +13 -3
- package/src/plugins/builtin/earnings/board-model.ts +16 -15
- package/src/plugins/builtin/earnings/board.tsx +62 -71
- package/src/plugins/builtin/earnings/headless.ts +40 -4
- package/src/plugins/builtin/earnings/scope.ts +31 -43
- package/src/plugins/builtin/earnings-calls/calendar.tsx +2 -2
- package/src/plugins/builtin/earnings-calls/data.ts +18 -6
- package/src/plugins/builtin/earnings-calls/format.ts +31 -2
- package/src/plugins/builtin/earnings-calls/headless.ts +20 -5
- package/src/plugins/builtin/earnings-calls/model.ts +6 -1
- package/src/plugins/builtin/earnings-calls/pane.tsx +12 -4
- package/src/plugins/builtin/econ/calendar-model.ts +48 -1
- package/src/plugins/builtin/econ/calendar-source.ts +4 -2
- package/src/plugins/builtin/econ/detail-view.tsx +2 -2
- package/src/plugins/builtin/econ/figures.ts +48 -0
- package/src/plugins/builtin/econ/index.tsx +14 -5
- package/src/plugins/builtin/econ/print-watch.ts +3 -1
- package/src/plugins/builtin/econ/types.ts +2 -0
- package/src/plugins/builtin/econ-statistics/client.ts +11 -0
- package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
- package/src/plugins/builtin/econ-statistics/headless.ts +16 -6
- package/src/plugins/builtin/econ-statistics/pane.tsx +8 -4
- package/src/plugins/builtin/econ-statistics/stats.ts +39 -9
- package/src/plugins/builtin/equity-screener/categories.ts +179 -0
- package/src/plugins/builtin/equity-screener/client.ts +4 -2
- package/src/plugins/builtin/equity-screener/headless.ts +144 -13
- package/src/plugins/builtin/equity-screener/ideas.ts +49 -0
- package/src/plugins/builtin/equity-screener/model.ts +99 -10
- package/src/plugins/builtin/equity-screener/pane.tsx +68 -13
- package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
- package/src/plugins/builtin/futures/use-board-terms.ts +42 -28
- package/src/plugins/builtin/futures/use-front-returns.ts +45 -28
- package/src/plugins/builtin/fx-matrix/client.ts +6 -3
- package/src/plugins/builtin/fx-matrix/headless.ts +1 -1
- package/src/plugins/builtin/fx-matrix/index.tsx +4 -2
- package/src/plugins/builtin/fx-matrix/live-legs.ts +42 -3
- package/src/plugins/builtin/gpu/headless.ts +8 -2
- package/src/plugins/builtin/help/function-index.ts +4 -1
- package/src/plugins/builtin/help/index.tsx +62 -59
- package/src/plugins/builtin/help/shortcut-model.ts +1 -1
- package/src/plugins/builtin/help/shortcut-table.tsx +5 -6
- package/src/plugins/builtin/help/table-cells.tsx +13 -2
- package/src/plugins/builtin/holders/beneficial-client.ts +5 -1
- package/src/plugins/builtin/holders/beneficial-view.tsx +3 -0
- package/src/plugins/builtin/holders/fund-holders.ts +51 -0
- package/src/plugins/builtin/holders/headless.ts +22 -3
- package/src/plugins/builtin/holders/pane.tsx +3 -0
- package/src/plugins/builtin/holders/report-rows.ts +4 -1
- package/src/plugins/builtin/insider/headless.ts +2 -2
- package/src/plugins/builtin/ipo-calendar/client.ts +18 -2
- package/src/plugins/builtin/ipo-calendar/headless.ts +75 -11
- package/src/plugins/builtin/ipo-calendar/listing-ipo.ts +160 -0
- package/src/plugins/builtin/ipo-calendar/model.ts +98 -2
- package/src/plugins/builtin/iv-history/client.ts +52 -11
- package/src/plugins/builtin/iv-history/format.ts +36 -23
- package/src/plugins/builtin/iv-history/headless.ts +33 -18
- package/src/plugins/builtin/iv-history/index.tsx +2 -2
- package/src/plugins/builtin/iv-history/model.ts +163 -27
- package/src/plugins/builtin/iv-history/pane.tsx +10 -6
- package/src/plugins/builtin/iv-history/screen-pane.tsx +22 -15
- package/src/plugins/builtin/iv-history/universe.ts +5 -1
- package/src/plugins/builtin/jobs/client.ts +25 -29
- package/src/plugins/builtin/kelly-sizer/model.ts +12 -8
- package/src/plugins/builtin/kelly-sizer/pane.tsx +9 -9
- package/src/plugins/builtin/kelly-sizer/sections.tsx +19 -10
- package/src/plugins/builtin/kelly-sizer/types.ts +10 -4
- package/src/plugins/builtin/macro-day/headless.ts +12 -5
- package/src/plugins/builtin/macro-day/index.tsx +1 -1
- package/src/plugins/builtin/macro-day/model.ts +39 -5
- package/src/plugins/builtin/macro-day/pane.tsx +27 -19
- package/src/plugins/builtin/macro-day/release-table.ts +2 -2
- package/src/plugins/builtin/market-heatmap/data.ts +8 -33
- package/src/plugins/builtin/market-movers/client.ts +27 -7
- package/src/plugins/builtin/market-movers/headless.ts +34 -12
- package/src/plugins/builtin/market-movers/index.tsx +26 -14
- package/src/plugins/builtin/market-movers/min-float.ts +74 -0
- package/src/plugins/builtin/market-movers/model.ts +4 -1
- package/src/plugins/builtin/market-movers/screener.ts +22 -4
- package/src/plugins/builtin/market-movers/session-body.tsx +13 -8
- package/src/plugins/builtin/market-movers/session-table.ts +4 -3
- package/src/plugins/builtin/market-movers/table.ts +17 -5
- package/src/plugins/builtin/members/breadth-pane.tsx +2 -1
- package/src/plugins/builtin/mna/headless.ts +2 -0
- package/src/plugins/builtin/mna/pane.tsx +3 -1
- package/src/plugins/builtin/news/headless.ts +3 -1
- package/src/plugins/builtin/news/index.tsx +4 -0
- package/src/plugins/builtin/news/wire/categories.ts +19 -3
- package/src/plugins/builtin/news/wire/industry-pane.tsx +1 -0
- package/src/plugins/builtin/news/wire/news/footer.ts +31 -5
- package/src/plugins/builtin/news/wire/news/preset-pane.tsx +6 -1
- package/src/plugins/builtin/news/wire/news/table.tsx +2 -2
- package/src/plugins/builtin/openfda/client.ts +14 -2
- package/src/plugins/builtin/openfda/index.tsx +3 -2
- package/src/plugins/builtin/openfda/model.ts +9 -0
- package/src/plugins/builtin/openfda/pane.tsx +42 -6
- package/src/plugins/builtin/options/analytics.ts +29 -5
- package/src/plugins/builtin/options/enrichment.ts +17 -5
- package/src/plugins/builtin/options/table.ts +5 -2
- package/src/plugins/builtin/options/types.ts +2 -0
- package/src/plugins/builtin/options/view.tsx +17 -4
- package/src/plugins/builtin/options-positioning/client.ts +11 -0
- package/src/plugins/builtin/options-positioning/headless.ts +21 -8
- package/src/plugins/builtin/options-positioning/index.tsx +1 -1
- package/src/plugins/builtin/options-positioning/model.ts +78 -9
- package/src/plugins/builtin/options-positioning/pane.tsx +21 -6
- package/src/plugins/builtin/options-scenario/client.ts +116 -47
- package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +142 -10
- package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +206 -55
- package/src/plugins/builtin/options-scenario/csp-screen.ts +63 -23
- package/src/plugins/builtin/options-scenario/csp.ts +8 -2
- package/src/plugins/builtin/options-scenario/index.tsx +9 -7
- package/src/plugins/builtin/options-scenario/strategy-screen.ts +532 -0
- package/src/plugins/builtin/pe-band/model.ts +12 -49
- package/src/plugins/builtin/perps/cascade-tabs.tsx +161 -0
- package/src/plugins/builtin/perps/cascade.ts +236 -0
- package/src/plugins/builtin/perps/client.ts +56 -2
- package/src/plugins/builtin/perps/headless.ts +65 -9
- package/src/plugins/builtin/perps/index.tsx +7 -5
- package/src/plugins/builtin/perps/model.ts +18 -4
- package/src/plugins/builtin/perps/pane.tsx +19 -22
- package/src/plugins/builtin/perps/status-line.ts +25 -0
- package/src/plugins/builtin/plugin-module.ts +5 -1
- package/src/plugins/builtin/portfolio-list/cached-account.ts +3 -1
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +2 -8
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -4
- package/src/plugins/builtin/portfolio-list/cli/render.ts +7 -5
- package/src/plugins/builtin/portfolio-list/pane/futures-front.ts +40 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +6 -7
- package/src/plugins/builtin/portfolio-list/pane/streaming.ts +5 -2
- package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +3 -2
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +0 -14
- package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +5 -2
- package/src/plugins/builtin/portfolio-list/table.tsx +15 -9
- package/src/plugins/builtin/primary-dealers/client.ts +13 -38
- package/src/plugins/builtin/realized-vol/client.ts +13 -5
- package/src/plugins/builtin/realized-vol/evidence.ts +29 -4
- package/src/plugins/builtin/realized-vol/headless.ts +8 -4
- package/src/plugins/builtin/realized-vol/index.tsx +2 -2
- package/src/plugins/builtin/realized-vol/model.ts +37 -6
- package/src/plugins/builtin/realized-vol/pane.tsx +11 -9
- package/src/plugins/builtin/relative-rotation/client.ts +18 -4
- package/src/plugins/builtin/relative-rotation/headless.ts +3 -3
- package/src/plugins/builtin/relative-rotation/index.tsx +2 -1
- package/src/plugins/builtin/relative-rotation/model.ts +16 -40
- package/src/plugins/builtin/relative-rotation/pane.tsx +3 -2
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -2
- package/src/plugins/builtin/research/event-model.ts +40 -12
- package/src/plugins/builtin/research/headless.ts +1 -1
- package/src/plugins/builtin/research/index.tsx +7 -3
- package/src/plugins/builtin/research/relative-valuation-headless.ts +13 -26
- package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +59 -20
- package/src/plugins/builtin/research/relative-valuation-peers.ts +217 -20
- package/src/plugins/builtin/research/relative-valuation-settings.ts +44 -0
- package/src/plugins/builtin/reverse-dcf/headless.ts +5 -3
- package/src/plugins/builtin/reverse-dcf/model.ts +61 -6
- package/src/plugins/builtin/reverse-dcf/pane.tsx +7 -4
- package/src/plugins/builtin/risk-factors/data.ts +4 -2
- package/src/plugins/builtin/risk-factors/headless.ts +5 -2
- package/src/plugins/builtin/roadmap/index.tsx +28 -0
- package/src/plugins/builtin/roadmap/model.ts +67 -0
- package/src/plugins/builtin/roadmap/pane.tsx +244 -0
- package/src/plugins/builtin/scanner/flow-model.ts +50 -4
- package/src/plugins/builtin/scanner/flow-pane.tsx +367 -65
- package/src/plugins/builtin/scanner/flow-session.ts +146 -0
- package/src/plugins/builtin/scanner/headless.ts +204 -35
- package/src/plugins/builtin/scanner/index.ts +16 -5
- package/src/plugins/builtin/scanner/live-headless.ts +48 -5
- package/src/plugins/builtin/scanner/live-model.ts +11 -0
- package/src/plugins/builtin/scanner/live-pane.tsx +3 -3
- package/src/plugins/builtin/scanner/lock-note.ts +3 -2
- package/src/plugins/builtin/seasonality/headless.ts +10 -1
- package/src/plugins/builtin/sec/fund-facts-view.tsx +5 -2
- package/src/plugins/builtin/sec/fund-facts.ts +116 -34
- package/src/plugins/builtin/sec/headless.ts +4 -2
- package/src/plugins/builtin/sec/index.tsx +3 -0
- package/src/plugins/builtin/series-inventory.ts +2 -0
- package/src/plugins/builtin/shared/cloud-resource.ts +16 -7
- package/src/plugins/builtin/shared/cloud-upgrade.ts +4 -3
- package/src/plugins/builtin/shared/earnings-calendar.ts +6 -2
- package/src/plugins/builtin/shared/earnings-report.ts +2 -2
- package/src/plugins/builtin/shared/headless-market-data.ts +2 -2
- package/src/plugins/builtin/shared/job-posting.ts +1 -0
- package/src/plugins/builtin/shared/members-menu.tsx +3 -2
- package/src/plugins/builtin/shared/pro-lock-note.ts +12 -12
- package/src/plugins/builtin/shared/report-freshness.ts +11 -2
- package/src/plugins/builtin/shared/research-cells.tsx +1 -1
- package/src/plugins/builtin/shared/series-cache.ts +4 -4
- package/src/plugins/builtin/shared/ticker-choice.tsx +2 -1
- package/src/plugins/builtin/shared/ticker-company.ts +77 -0
- package/src/plugins/builtin/shared/use-quote-board.ts +2 -1
- package/src/plugins/builtin/shared/volatility/live-session.ts +34 -8
- package/src/plugins/builtin/shared/volatility/pricing.ts +19 -0
- package/src/plugins/builtin/shared/wall-view-lifecycle.ts +3 -1
- package/src/plugins/builtin/themes/model.ts +16 -7
- package/src/plugins/builtin/themes/pane.tsx +5 -5
- package/src/plugins/builtin/thirteenf/api.ts +12 -1
- package/src/plugins/builtin/thirteenf/data.ts +7 -7
- package/src/plugins/builtin/thirteenf/headless.ts +31 -4
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +43 -18
- package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +3 -2
- package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +62 -0
- package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +83 -46
- package/src/plugins/builtin/ticker-detail/headless.ts +31 -16
- package/src/plugins/builtin/ticker-detail/overview/model.ts +11 -3
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +2 -1
- package/src/plugins/builtin/ticker-detail/pane.tsx +14 -8
- package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -1
- package/src/plugins/builtin/ticker-detail/settings.ts +1 -0
- package/src/plugins/builtin/time-sales/pane.tsx +31 -19
- package/src/plugins/builtin/treasury-auctions/headless.ts +21 -12
- package/src/plugins/builtin/treasury-auctions/pane.tsx +9 -3
- package/src/plugins/builtin/treasury-daily/client.ts +41 -5
- package/src/plugins/builtin/treasury-daily/headless.ts +2 -2
- package/src/plugins/builtin/treasury-daily/index.tsx +1 -1
- package/src/plugins/builtin/treasury-daily/model.ts +50 -3
- package/src/plugins/builtin/treasury-daily/pane.tsx +81 -2
- package/src/plugins/builtin/vol-surface/client.ts +3 -13
- package/src/plugins/builtin/vol-surface/headless.ts +3 -3
- package/src/plugins/builtin/vol-surface/index.tsx +1 -1
- package/src/plugins/builtin/vol-surface/pane.tsx +7 -5
- package/src/plugins/builtin/volatility/index.tsx +2 -2
- package/src/plugins/builtin/volatility/pane.tsx +7 -4
- package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
- package/src/plugins/builtin/yield-curve/client.ts +61 -1
- package/src/plugins/builtin/yield-curve/headless.ts +9 -6
- package/src/plugins/builtin/yield-curve/index.tsx +34 -10
- package/src/plugins/builtin/yield-curve/swaps.tsx +9 -5
- package/src/plugins/catalog-browser.ts +2 -1
- package/src/plugins/catalog-manifest.ts +2 -1
- package/src/plugins/host-link.ts +1 -1
- package/src/plugins/marketplace/model.ts +18 -5
- package/src/portfolio/column-values.ts +11 -7
- package/src/portfolio/futures-positions.ts +6 -3
- package/src/portfolio/position-quantity.ts +15 -9
- package/src/remote/resources.ts +2 -1
- package/src/renderers/dom/data-table/row.tsx +26 -0
- package/src/renderers/dom/desktop/icons.tsx +7 -0
- package/src/renderers/dom/desktop/list-view.tsx +13 -10
- package/src/renderers/dom/desktop/query-bar.tsx +4 -2
- package/src/renderers/dom/desktop/select-field.tsx +4 -2
- package/src/renderers/dom/host/chart-surface.tsx +21 -0
- package/src/renderers/electrobun/bun/desktop/app-name.ts +3 -3
- package/src/renderers/electrobun/bun/desktop/backend.ts +21 -10
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +15 -0
- package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
- package/src/renderers/electrobun/bun/desktop/installer-folder.ts +731 -0
- package/src/renderers/electrobun/bun/desktop/relaunch.ts +102 -10
- package/src/renderers/electrobun/bun/desktop/update.ts +115 -14
- package/src/renderers/electrobun/bun/desktop/view-ids.ts +52 -0
- package/src/renderers/electrobun/bun/desktop/windows-script.ts +92 -0
- package/src/renderers/electrobun/bun/index.ts +26 -2
- package/src/renderers/electrobun/bun/window/create.ts +43 -4
- package/src/renderers/electrobun/bun/window/events.ts +20 -0
- package/src/renderers/electrobun/bun/window/windows-custom-chrome.ts +9 -2
- package/src/renderers/electrobun/bun/window/windows-native.ts +78 -0
- package/src/renderers/electrobun/bun/window/windows-quit.ts +57 -0
- package/src/renderers/electrobun/shared/app-name.ts +13 -0
- package/src/renderers/electrobun/shared/protocol.ts +12 -0
- package/src/renderers/electrobun/view/backend-rpc.ts +6 -0
- package/src/renderers/electrobun/view/main.tsx +24 -1
- package/src/renderers/electrobun/view/news-source.ts +5 -1
- package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
- package/src/renderers/electrobun/view/ui-host.tsx +8 -0
- package/src/renderers/electrobun/view/view-ids.ts +85 -0
- package/src/renderers/opentui/start.tsx +5 -0
- package/src/series/registry.ts +1 -0
- package/src/series/types.ts +6 -0
- package/src/series/validate.ts +1 -0
- package/src/sources/gloomberb-cloud/fred-series.ts +3 -1
- package/src/sources/gloomberb-cloud/index.ts +43 -8
- package/src/sources/gloomberb-cloud/news.ts +47 -7
- package/src/sources/provider-router/financials.ts +15 -3
- package/src/sources/provider-router/index.ts +8 -5
- package/src/state/ticker-cleanup.ts +50 -0
- package/src/sync/controller.ts +4 -3
- package/src/sync/core-contributors.ts +90 -31
- package/src/telemetry/stall-reports-dom.ts +38 -0
- package/src/telemetry/stall-reports-node.ts +75 -0
- package/src/telemetry/stall-reports.ts +214 -0
- package/src/test-support/preload.ts +14 -0
- package/src/theme/themes.ts +10 -0
- package/src/tickers/company-name-listing.ts +53 -13
- package/src/tickers/company-name.ts +50 -4
- package/src/tickers/index-roots.ts +7 -0
- package/src/tickers/listing-fate.ts +2 -2
- package/src/tickers/listing-venues.ts +15 -3
- package/src/tickers/search/index.ts +6 -2
- package/src/tickers/search/ranking.ts +5 -1
- package/src/tickers/search/upsert.ts +13 -0
- package/src/tickers/terminal-codes.ts +19 -0
- package/src/tickers/ticker-changes.ts +8 -4
- package/src/time-series/alignment.ts +1 -1
- package/src/time-series/economic.ts +18 -4
- package/src/time-series/field-catalog.ts +1 -1
- package/src/time-series/filed-trailing-eps.ts +197 -0
- package/src/time-series/fundamentals.ts +44 -1
- package/src/time-series/ice-credit.ts +3 -0
- package/src/time-series/price-comparison.ts +30 -2
- package/src/time-series/resolve-context.ts +3 -1
- package/src/time-series/resolve.ts +56 -4
- package/src/time-series/session-bar.ts +11 -0
- package/src/time-series/spec.ts +3 -2
- package/src/time-series/studies.ts +8 -2
- package/src/time-series/total-return.ts +160 -0
- package/src/time-series/transforms.ts +2 -3
- package/src/time-series/types.ts +22 -1
- package/src/types/company-from.ts +40 -0
- package/src/types/data-provider.ts +7 -0
- package/src/types/financials.ts +59 -1
- package/src/types/headless.ts +27 -3
- package/src/types/news-source.ts +1 -0
- package/src/types/plugin.ts +9 -0
- package/src/ui/host.tsx +3 -0
- package/src/updater/failure.ts +4 -1
- package/src/utils/cli-output.ts +42 -1
- package/src/utils/crypto-pair.ts +165 -7
- package/src/utils/declared-dividends.ts +1 -1
- package/src/utils/forward-dividend.ts +2 -0
- package/src/utils/fundamentals.ts +60 -8
- package/src/utils/fx-market-hours.ts +26 -3
- package/src/utils/main-thread-stall.ts +35 -2
- package/src/utils/market-capitalization.ts +24 -1
- package/src/utils/price-history.ts +7 -1
- package/src/utils/shell-word.ts +8 -0
- package/src/utils/trailing-dividend.ts +53 -4
- package/src/version.ts +2 -2
- package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +0 -17
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
import { lstat, mkdir, readdir, readFile, rm, writeFile } from "fs/promises";
|
|
2
|
+
import { tmpdir } from "os";
|
|
3
|
+
import { win32 } from "path";
|
|
4
|
+
import { logFailure } from "../../../../utils/log-failure";
|
|
5
|
+
import { DESKTOP_APP_IDENTIFIER, DESKTOP_APP_IDENTIFIERS } from "../../shared/app-name";
|
|
6
|
+
import { psString, runScriptAfterExit, runScriptInBackground, writePowerShellScript } from "./windows-script";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The Windows installer (scripts/windows-installer.iss) puts the app in its
|
|
10
|
+
* own folder, `Programs\Gloom` unless the user picks another, and its Start
|
|
11
|
+
* menu and Desktop shortcuts, the `gloom` command on PATH and its uninstall
|
|
12
|
+
* entry lead there. Electrobun's updater knows nothing of it: it installs
|
|
13
|
+
* every update into its own folder, `<app data>\<identifier>\<channel>\app`,
|
|
14
|
+
* and opens it from there. The next start from a shortcut opened the build
|
|
15
|
+
* from before the update, which offered the same update again, and
|
|
16
|
+
* uninstalling left the updated copy behind.
|
|
17
|
+
*
|
|
18
|
+
* So a build that finds itself in the updater's folder while the installer's
|
|
19
|
+
* install is recorded quits before any window shows, and a script moves it
|
|
20
|
+
* into the installer's folder and opens it from there. A build running from
|
|
21
|
+
* the installer's folder removes a copy an earlier update left in the
|
|
22
|
+
* updater's. Either way the shortcuts and uninstall entry Electrobun's own
|
|
23
|
+
* setup made for its folder, if any, follow to the installer's.
|
|
24
|
+
*
|
|
25
|
+
* The app quits for the move only once the script has checked it can make
|
|
26
|
+
* it, and tries a build at most three times, ten minutes apart: a move that
|
|
27
|
+
* cannot happen never makes it quit and open again at every start.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** The installer's AppId, which names its uninstall entry (windows-installer.iss). */
|
|
31
|
+
export const WINDOWS_INSTALLER_APP_ID = "{D7B2304C-840B-4E5F-956A-3D1C66E67B35}";
|
|
32
|
+
const INSTALLER_ENTRY = `${WINDOWS_INSTALLER_APP_ID}_is1`;
|
|
33
|
+
|
|
34
|
+
export interface InstallerFolderContext {
|
|
35
|
+
platform: string;
|
|
36
|
+
execPath: string;
|
|
37
|
+
homeDir: string;
|
|
38
|
+
/** `%LOCALAPPDATA%`, where Electrobun keeps the updater's folder. */
|
|
39
|
+
localAppData?: string;
|
|
40
|
+
/** The folder the installer's uninstall entry records, or null; read from the registry when left out. */
|
|
41
|
+
readInstallerFolder?: () => Promise<string | null>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type InstallerFolderMove =
|
|
45
|
+
/** Not an installer install, or already in its folder with nothing left elsewhere. */
|
|
46
|
+
| { kind: "none" }
|
|
47
|
+
/** The app runs from the updater's folder, and quits for the script to move it and open it again. */
|
|
48
|
+
| { kind: "relaunching" }
|
|
49
|
+
/** A copy left in the updater's folder goes in the background. */
|
|
50
|
+
| { kind: "removing" }
|
|
51
|
+
/** The app runs from where it is: a try failed a moment ago, tries failed too often for this build, or the installer's folder is in use. */
|
|
52
|
+
| { kind: "skipped" }
|
|
53
|
+
| { kind: "failed" };
|
|
54
|
+
|
|
55
|
+
/** How long a try at moving the app keeps it from trying again, so a move that cannot happen never makes it quit in a loop. */
|
|
56
|
+
const RETRY_AFTER_MS = 10 * 60_000;
|
|
57
|
+
/** Tries at moving one build before it stays in the updater's folder until the next update. */
|
|
58
|
+
const MAX_MOVE_TRIES = 3;
|
|
59
|
+
/** How long after the start a copy left in the updater's folder goes, out of the launch's way. */
|
|
60
|
+
const REMOVAL_DELAY_MS = 10_000;
|
|
61
|
+
const LAUNCHER = "bin\\launcher.exe";
|
|
62
|
+
|
|
63
|
+
function samePath(left: string, right: string): boolean {
|
|
64
|
+
return win32.resolve(left).toLowerCase() === win32.resolve(right).toLowerCase();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function exists(path: string): Promise<boolean> {
|
|
68
|
+
return lstat(path).then(() => true, () => false);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isDesktopIdentifierName(name: string): boolean {
|
|
72
|
+
return DESKTOP_APP_IDENTIFIERS.some((identifier) => identifier.toLowerCase() === name.toLowerCase());
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The updater's folder the app runs from, from its `bin\bun.exe`:
|
|
77
|
+
* `<local app data>\<identifier>\<channel>\app`, under either identifier the
|
|
78
|
+
* app has carried. Null anywhere else.
|
|
79
|
+
*/
|
|
80
|
+
export function updaterAppFolder(execPath: string, localAppData: string): { folder: string; channel: string } | null {
|
|
81
|
+
const relative = win32.relative(localAppData, execPath);
|
|
82
|
+
if (!relative || relative.startsWith("..") || win32.isAbsolute(relative)) return null;
|
|
83
|
+
const [identifier, channel, app, bin, ...rest] = relative.split("\\");
|
|
84
|
+
if (!identifier || !channel || rest.length !== 1 || !isDesktopIdentifierName(identifier)) return null;
|
|
85
|
+
if (app?.toLowerCase() !== "app" || bin?.toLowerCase() !== "bin") return null;
|
|
86
|
+
return { folder: win32.join(localAppData, identifier, channel, "app"), channel };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The updater's folders for `channel` under either identifier, and what a
|
|
91
|
+
* removal that did not finish left of one (`app.removing-*`).
|
|
92
|
+
*/
|
|
93
|
+
async function updaterFolders(localAppData: string, channel: string): Promise<string[]> {
|
|
94
|
+
const folders: string[] = [];
|
|
95
|
+
for (const identifier of DESKTOP_APP_IDENTIFIERS) {
|
|
96
|
+
const parent = win32.join(localAppData, identifier, channel);
|
|
97
|
+
for (const entry of await readdir(parent).catch(() => [] as string[])) {
|
|
98
|
+
const name = entry.toLowerCase();
|
|
99
|
+
if (name === "app" || name.startsWith("app.removing-")) folders.push(win32.join(parent, entry));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return folders;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The value `name` holds in a `.reg` export, unescaped; null when it holds none or no string. */
|
|
106
|
+
export function regExportValue(text: string, name: string): string | null {
|
|
107
|
+
const prefix = `${JSON.stringify(name)}=`;
|
|
108
|
+
for (const line of text.split(/\r?\n/)) {
|
|
109
|
+
if (!line.startsWith(prefix)) continue;
|
|
110
|
+
const value = line.slice(prefix.length);
|
|
111
|
+
if (value.length < 2 || !value.startsWith("\"") || !value.endsWith("\"")) return null;
|
|
112
|
+
return value.slice(1, -1).replace(/\\(.)/g, "$1");
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The folder the installer's uninstall entry records. `reg export` writes
|
|
119
|
+
* UTF-16, where `reg query` would print a path with an accent in the console's
|
|
120
|
+
* code page.
|
|
121
|
+
*/
|
|
122
|
+
async function readInstallerFolderFromRegistry(): Promise<string | null> {
|
|
123
|
+
const file = win32.join(tmpdir(), `gloom-installer-entry-${process.pid}.reg`);
|
|
124
|
+
try {
|
|
125
|
+
const key = `HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${INSTALLER_ENTRY}`;
|
|
126
|
+
const result = Bun.spawnSync(["reg.exe", "export", key, file, "/y"], { stdio: ["ignore", "ignore", "ignore"], windowsHide: true });
|
|
127
|
+
if (!result.success) return null;
|
|
128
|
+
return regExportValue(new TextDecoder("utf-16le").decode(await readFile(file)), "Inno Setup: App Path");
|
|
129
|
+
} finally {
|
|
130
|
+
await rm(file, { force: true });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The installer's folder, when it holds the app. A move cut short (a power
|
|
136
|
+
* cut between its renames) can leave its launcher in what the move set aside
|
|
137
|
+
* there: the next move puts that back before anything else.
|
|
138
|
+
*/
|
|
139
|
+
async function installerFolder(read: () => Promise<string | null>): Promise<string | null> {
|
|
140
|
+
const folder = await read().catch(() => null);
|
|
141
|
+
if (!folder) return null;
|
|
142
|
+
if (await exists(win32.join(folder, LAUNCHER))) return win32.resolve(folder);
|
|
143
|
+
for (const entry of await readdir(folder).catch(() => [] as string[])) {
|
|
144
|
+
if (/^\.gloom-previous-/i.test(entry) && await exists(win32.join(folder, entry, LAUNCHER))) return win32.resolve(folder);
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function buildChannel(appFolder: string): Promise<string> {
|
|
150
|
+
const info = await readFile(win32.join(appFolder, "Resources", "version.json"), "utf8").then((text) => JSON.parse(text) as { channel?: unknown }, () => null);
|
|
151
|
+
return typeof info?.channel === "string" && info.channel ? info.channel : "stable";
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
interface InstallerFolderScript {
|
|
155
|
+
/** The installer's folder. */
|
|
156
|
+
target: string;
|
|
157
|
+
/** The updater's folders to empty into it or remove; with `afterExit`, the first holds the running app. */
|
|
158
|
+
sources: string[];
|
|
159
|
+
/** This build's app data folder: the script's log goes there, and a folder it removes is moved there first. */
|
|
160
|
+
folder: string;
|
|
161
|
+
log: string;
|
|
162
|
+
/**
|
|
163
|
+
* The app runs from `sources[0]`: the script waits for everything running
|
|
164
|
+
* from there to quit, moves it into `target` and opens it again. `cancel`
|
|
165
|
+
* is the file the app writes when it stops waiting for the script.
|
|
166
|
+
*/
|
|
167
|
+
afterExit?: { cancel: string; taskName: string };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// PowerShell 5.1. The build goes into the installer's folder by renames only:
|
|
171
|
+
// from another drive it is copied beside the installed build first. A folder
|
|
172
|
+
// of the build (bin, Resources) replaces the installed one whole when that
|
|
173
|
+
// holds nothing but the app's files; in one that also holds other files (an
|
|
174
|
+
// install in C:\Tools, whose bin was already there) only the app's files are
|
|
175
|
+
// replaced. What it replaces waits beside it, in the installer's folder,
|
|
176
|
+
// until all of the build is in, and goes back if it is not. Then the app
|
|
177
|
+
// opens again, and the rest (shortcuts, uninstall entries, the copy it came
|
|
178
|
+
// from) follows.
|
|
179
|
+
const SCRIPT_BODY = String.raw`
|
|
180
|
+
$Launcher = 'bin\launcher.exe'
|
|
181
|
+
$FileList = 'Resources\update-files.txt'
|
|
182
|
+
$UninstallPath = 'Software\Microsoft\Windows\CurrentVersion\Uninstall'
|
|
183
|
+
$Journal = New-Object System.Collections.Generic.List[object]
|
|
184
|
+
$Exited = $false
|
|
185
|
+
$Opened = $false
|
|
186
|
+
|
|
187
|
+
# UTF-8 without a byte order mark: the app reads the answer from the first
|
|
188
|
+
# line, and a path with an accent in it reads back as it is.
|
|
189
|
+
$LogEncoding = New-Object System.Text.UTF8Encoding $false
|
|
190
|
+
|
|
191
|
+
function Write-Log([string]$Message) {
|
|
192
|
+
for ($Attempt = 0; $Attempt -lt 10; $Attempt += 1) {
|
|
193
|
+
try { [IO.File]::AppendAllText($Log, (Get-Date).ToString('o') + ' ' + $Message + [Environment]::NewLine, $LogEncoding); return } catch { Start-Sleep -Milliseconds 100 }
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function Test-Under([string]$Path, [string]$Root) {
|
|
198
|
+
if (-not $Path) { return $false }
|
|
199
|
+
return $Path.Equals($Root, [StringComparison]::OrdinalIgnoreCase) -or $Path.StartsWith($Root + '\', [StringComparison]::OrdinalIgnoreCase)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
# The processes whose program is in $Root, whatever started them.
|
|
203
|
+
function Get-RunningUnder([string]$Root) {
|
|
204
|
+
@(Get-Process -ErrorAction SilentlyContinue | Where-Object { try { Test-Under $_.Path $Root } catch { $false } })
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
# The processes running one of the build's programs from the installer's
|
|
208
|
+
# folder, such as the gloom command: they keep its files from being replaced.
|
|
209
|
+
# Another program the user keeps in that folder does not.
|
|
210
|
+
function Get-AppRunningInTarget {
|
|
211
|
+
@(Get-RunningUnder $Target | Where-Object { $BuildFiles.Contains($_.Path.Substring($Target.Length + 1)) })
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function Wait-Until([int]$Seconds, [scriptblock]$Condition) {
|
|
215
|
+
for ($Wait = 0; $Wait -lt ($Seconds * 2); $Wait += 1) {
|
|
216
|
+
if (& $Condition) { return $true }
|
|
217
|
+
Start-Sleep -Milliseconds 500
|
|
218
|
+
}
|
|
219
|
+
return [bool](& $Condition)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function Invoke-Retried([scriptblock]$Action, [string]$What) {
|
|
223
|
+
for ($Attempt = 1; $true; $Attempt += 1) {
|
|
224
|
+
try { & $Action | Out-Null; return } catch {
|
|
225
|
+
if ($Attempt -ge 20) { throw ('could not ' + $What + ': ' + $_.Exception.Message) }
|
|
226
|
+
Start-Sleep -Milliseconds 500
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function Get-Stamp { (Get-Date).ToUniversalTime().ToString('yyyyMMddHHmmss') + '-' + [guid]::NewGuid().ToString('N').Substring(0, 8) }
|
|
232
|
+
|
|
233
|
+
function Test-Folder([string]$Path) {
|
|
234
|
+
$Item = Get-Item -LiteralPath $Path -Force -ErrorAction SilentlyContinue
|
|
235
|
+
return ($null -ne $Item) -and $Item.PSIsContainer -and -not ($Item.Attributes -band [IO.FileAttributes]::ReparsePoint)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
# The files in $Root, as paths after $Prefix. A link is a file here: it is not followed.
|
|
239
|
+
function Get-Files([string]$Root, [string]$Prefix) {
|
|
240
|
+
foreach ($Item in @(Get-ChildItem -LiteralPath $Root -Force)) {
|
|
241
|
+
$Relative = if ($Prefix) { $Prefix + '\' + $Item.Name } else { $Item.Name }
|
|
242
|
+
if ($Item.PSIsContainer -and -not ($Item.Attributes -band [IO.FileAttributes]::ReparsePoint)) { Get-Files $Item.FullName $Relative } else { $Relative }
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function Remove-Tree([string]$Path) {
|
|
247
|
+
if (Test-Path -LiteralPath $Path) { Invoke-Retried { Remove-Item -LiteralPath $Path -Recurse -Force } ('remove ' + $Path) }
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
# Makes the missing folders up to $Path, each noted to take back.
|
|
251
|
+
function New-Folder([string]$Path) {
|
|
252
|
+
if (Test-Path -LiteralPath $Path) { return }
|
|
253
|
+
New-Folder (Split-Path -Parent $Path)
|
|
254
|
+
New-Item -ItemType Directory -Path $Path | Out-Null
|
|
255
|
+
$Journal.Add(@($null, $Path))
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
# A rename, noted to take back. Nothing lands inside a folder in its way.
|
|
259
|
+
function Move-Noted([string]$From, [string]$To) {
|
|
260
|
+
if (Test-Path -LiteralPath $To) { throw ($To + ' is in the way') }
|
|
261
|
+
New-Folder (Split-Path -Parent $To)
|
|
262
|
+
Invoke-Retried { Move-Item -LiteralPath $From -Destination $To } ('move ' + $From)
|
|
263
|
+
$Journal.Add(@($From, $To))
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function Undo-Noted {
|
|
267
|
+
for ($Index = $Journal.Count - 1; $Index -ge 0; $Index -= 1) {
|
|
268
|
+
$Step = $Journal[$Index]
|
|
269
|
+
try {
|
|
270
|
+
if ($null -eq $Step[0]) {
|
|
271
|
+
[IO.Directory]::Delete($Step[1], $false)
|
|
272
|
+
} else {
|
|
273
|
+
if (Test-Path -LiteralPath $Step[0]) { throw ($Step[0] + ' is in the way') }
|
|
274
|
+
Invoke-Retried { Move-Item -LiteralPath $Step[1] -Destination $Step[0] } ('put back ' + $Step[0])
|
|
275
|
+
}
|
|
276
|
+
} catch { Write-Log ('could not take back a step: ' + $_.Exception.Message) }
|
|
277
|
+
}
|
|
278
|
+
$Journal.Clear()
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
# The files earlier updates put in the installer's folder, as they listed them.
|
|
282
|
+
function Read-FileList {
|
|
283
|
+
$Path = Join-Path $Target $FileList
|
|
284
|
+
if (-not (Test-Path -LiteralPath $Path)) { return }
|
|
285
|
+
foreach ($Line in [IO.File]::ReadAllLines($Path)) {
|
|
286
|
+
$Line = $Line.Trim()
|
|
287
|
+
if ($Line -and $Line -notmatch '^[\\/]|:|\.\.') { $Line }
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function Install-Build([string]$Source) {
|
|
292
|
+
$Stamp = Get-Stamp
|
|
293
|
+
$From = $Source
|
|
294
|
+
$Next = $null
|
|
295
|
+
if (-not [IO.Path]::GetPathRoot($Source).Equals([IO.Path]::GetPathRoot($Target), [StringComparison]::OrdinalIgnoreCase)) {
|
|
296
|
+
$Next = Join-Path $Target ('.gloom-next-' + $Stamp)
|
|
297
|
+
try {
|
|
298
|
+
New-Item -ItemType Directory -Path $Next | Out-Null
|
|
299
|
+
foreach ($Entry in @(Get-ChildItem -LiteralPath $Source -Force)) {
|
|
300
|
+
Copy-Item -LiteralPath $Entry.FullName -Destination (Join-Path $Next $Entry.Name) -Recurse -Force
|
|
301
|
+
}
|
|
302
|
+
} catch {
|
|
303
|
+
$Reason = $_.Exception.Message
|
|
304
|
+
try { Remove-Tree $Next } catch { Write-Log ('could not remove ' + $Next + ': ' + $_.Exception.Message) }
|
|
305
|
+
throw ('could not copy the build into ' + $Target + ': ' + $Reason)
|
|
306
|
+
}
|
|
307
|
+
$From = $Next
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
$NewFiles = @(Get-Files $From '')
|
|
311
|
+
$Listed = @(Read-FileList)
|
|
312
|
+
$Own = New-Object 'System.Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase)
|
|
313
|
+
foreach ($File in ($NewFiles + $Listed + @($FileList))) { [void]$Own.Add($File) }
|
|
314
|
+
$Aside = Join-Path $Target ('.gloom-previous-' + $Stamp)
|
|
315
|
+
$Journal.Clear()
|
|
316
|
+
try {
|
|
317
|
+
foreach ($Entry in @(Get-ChildItem -LiteralPath $From -Force)) {
|
|
318
|
+
$Name = $Entry.Name
|
|
319
|
+
$Old = Join-Path $Target $Name
|
|
320
|
+
if (-not (Test-Path -LiteralPath $Old)) {
|
|
321
|
+
Move-Noted $Entry.FullName $Old
|
|
322
|
+
} elseif (-not $Entry.PSIsContainer) {
|
|
323
|
+
if (Test-Folder $Old) { throw ($Old + ' is a folder') }
|
|
324
|
+
Move-Noted $Old (Join-Path $Aside $Name)
|
|
325
|
+
Move-Noted $Entry.FullName $Old
|
|
326
|
+
} else {
|
|
327
|
+
if (-not (Test-Folder $Old)) { throw ($Old + ' is not a folder') }
|
|
328
|
+
$Others = @(Get-Files $Old $Name | Where-Object { -not $Own.Contains($_) })
|
|
329
|
+
if ($Others.Count -eq 0) {
|
|
330
|
+
Move-Noted $Old (Join-Path $Aside $Name)
|
|
331
|
+
Move-Noted $Entry.FullName $Old
|
|
332
|
+
Write-Log ('replaced ' + $Old)
|
|
333
|
+
} else {
|
|
334
|
+
Write-Log ('kept ' + $Others.Count + ' file(s) in ' + $Old + ' that are not the app''s, such as ' + $Others[0])
|
|
335
|
+
foreach ($Relative in @(Get-Files $Entry.FullName $Name)) {
|
|
336
|
+
$Place = Join-Path $Target $Relative
|
|
337
|
+
if (Test-Path -LiteralPath $Place) {
|
|
338
|
+
if (Test-Folder $Place) { throw ($Place + ' is a folder') }
|
|
339
|
+
Move-Noted $Place (Join-Path $Aside $Relative)
|
|
340
|
+
}
|
|
341
|
+
Move-Noted (Join-Path $From $Relative) $Place
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
} catch {
|
|
347
|
+
$Reason = $_.Exception.Message
|
|
348
|
+
Undo-Noted
|
|
349
|
+
if ($Next) { try { Remove-Tree $Next } catch { Write-Log ('could not remove ' + $Next + ': ' + $_.Exception.Message) } }
|
|
350
|
+
throw $Reason
|
|
351
|
+
}
|
|
352
|
+
$Journal.Clear()
|
|
353
|
+
|
|
354
|
+
# For the uninstaller, which removes what it installed itself: what updates
|
|
355
|
+
# put here, the files of this build and those of earlier ones still here.
|
|
356
|
+
try {
|
|
357
|
+
$Files = New-Object 'System.Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase)
|
|
358
|
+
foreach ($File in $NewFiles) { [void]$Files.Add($File) }
|
|
359
|
+
foreach ($File in $Listed) { if (Test-Path -LiteralPath (Join-Path $Target $File)) { [void]$Files.Add($File) } }
|
|
360
|
+
[IO.File]::WriteAllLines((Join-Path $Target $FileList), [string[]]@($Files | Sort-Object))
|
|
361
|
+
} catch { Write-Log ('could not list the files for the uninstaller: ' + $_.Exception.Message) }
|
|
362
|
+
foreach ($Leftover in @($Aside, $Next)) {
|
|
363
|
+
if ($Leftover) {
|
|
364
|
+
try { Remove-Tree $Leftover } catch { Write-Log ($_.Exception.Message + '; the app removes it at its next start') }
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
# What a move that did not finish left in the installer's folder.
|
|
370
|
+
# A move cut short (a power cut between the two renames of bin, or an undo
|
|
371
|
+
# that failed) can leave the installed build without some of its files, in
|
|
372
|
+
# what it set aside. A folder or file of the build missing from the
|
|
373
|
+
# installer's folder goes back; file by file only while the launcher is
|
|
374
|
+
# missing, so a file a newer build dropped is not brought back beside it.
|
|
375
|
+
function Restore-SetAside([string]$Aside) {
|
|
376
|
+
try {
|
|
377
|
+
foreach ($Entry in @(Get-ChildItem -LiteralPath $Aside -Force)) {
|
|
378
|
+
$Place = Join-Path $Target $Entry.Name
|
|
379
|
+
if (-not (Test-Path -LiteralPath $Place)) {
|
|
380
|
+
Invoke-Retried { Move-Item -LiteralPath $Entry.FullName -Destination $Place } ('put back ' + $Place)
|
|
381
|
+
Write-Log ('put back ' + $Place)
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (Test-Path -LiteralPath (Join-Path $Target $Launcher)) { return }
|
|
385
|
+
foreach ($Relative in @(Get-Files $Aside '')) {
|
|
386
|
+
$Place = Join-Path $Target $Relative
|
|
387
|
+
if (Test-Path -LiteralPath $Place) { continue }
|
|
388
|
+
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $Place) | Out-Null
|
|
389
|
+
Invoke-Retried { Move-Item -LiteralPath (Join-Path $Aside $Relative) -Destination $Place } ('put back ' + $Place)
|
|
390
|
+
Write-Log ('put back ' + $Place)
|
|
391
|
+
}
|
|
392
|
+
} catch { Write-Log ('could not put back what ' + $Aside + ' holds: ' + $_.Exception.Message) }
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
# What a move that did not finish left in the installer's folder: a copy of
|
|
396
|
+
# a build on its way in, and what it set aside, newest first. Nothing set
|
|
397
|
+
# aside goes while the installed build has no launcher: it may be the only
|
|
398
|
+
# copy of it.
|
|
399
|
+
function Remove-Staging {
|
|
400
|
+
$Leftovers = @(Get-ChildItem -LiteralPath $Target -Directory -Force -ErrorAction SilentlyContinue |
|
|
401
|
+
Where-Object { $_.Name -like '.gloom-next-*' -or $_.Name -like '.gloom-previous-*' } | Sort-Object Name -Descending)
|
|
402
|
+
foreach ($Leftover in $Leftovers) {
|
|
403
|
+
if ((Get-RunningUnder $Leftover.FullName).Count -gt 0) { continue }
|
|
404
|
+
if ($Leftover.Name -like '.gloom-previous-*') {
|
|
405
|
+
Restore-SetAside $Leftover.FullName
|
|
406
|
+
if (-not (Test-Path -LiteralPath (Join-Path $Target $Launcher))) {
|
|
407
|
+
Write-Log ('kept ' + $Leftover.FullName + ': the installed build has no ' + $Launcher)
|
|
408
|
+
continue
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
try { Remove-Tree $Leftover.FullName; Write-Log ('removed ' + $Leftover.FullName) } catch { Write-Log $_.Exception.Message }
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function Open-App([string]$Path) {
|
|
416
|
+
$script:Opened = $true
|
|
417
|
+
try {
|
|
418
|
+
Start-Process -FilePath $Path -WorkingDirectory (Split-Path -Parent $Path)
|
|
419
|
+
Write-Log ('opened ' + $Path)
|
|
420
|
+
} catch { Write-Log ('could not open ' + $Path + ': ' + $_.Exception.Message) }
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
# A value naming a path in $From, naming $To instead; null for any other.
|
|
424
|
+
function Get-Retargeted([string]$Value, [string]$From, [string]$To) {
|
|
425
|
+
if (-not $Value) { return $null }
|
|
426
|
+
$Pattern = [regex]::Escape($From) + '(?=[\\/",;]|$)'
|
|
427
|
+
$Evaluator = [System.Text.RegularExpressions.MatchEvaluator] ({ param($Found) $To }.GetNewClosure())
|
|
428
|
+
$Next = [regex]::Replace($Value, $Pattern, $Evaluator, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
|
|
429
|
+
if ($Next -ceq $Value) { return $null }
|
|
430
|
+
return $Next
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
# Electrobun's setup makes Desktop and Start menu shortcuts into its folder.
|
|
434
|
+
function Update-Shortcuts([string]$From, [string]$To) {
|
|
435
|
+
$Shell = New-Object -ComObject WScript.Shell
|
|
436
|
+
$Folders = @(
|
|
437
|
+
[Environment]::GetFolderPath('Desktop'),
|
|
438
|
+
[Environment]::GetFolderPath('Programs'),
|
|
439
|
+
(Join-Path $env:APPDATA 'Microsoft\Internet Explorer\Quick Launch')
|
|
440
|
+
) | Where-Object { $_ -and (Test-Path -LiteralPath $_) } | Sort-Object -Unique
|
|
441
|
+
foreach ($Folder in $Folders) {
|
|
442
|
+
foreach ($Link in @(Get-ChildItem -LiteralPath $Folder -Filter '*.lnk' -Recurse -File -Force -ErrorAction SilentlyContinue)) {
|
|
443
|
+
try {
|
|
444
|
+
$Shortcut = $Shell.CreateShortcut($Link.FullName)
|
|
445
|
+
$Changed = $false
|
|
446
|
+
foreach ($Field in @('TargetPath', 'WorkingDirectory', 'IconLocation', 'Arguments')) {
|
|
447
|
+
$Next = Get-Retargeted ([string]$Shortcut.$Field) $From $To
|
|
448
|
+
if ($Next) { $Shortcut.$Field = $Next; $Changed = $true }
|
|
449
|
+
}
|
|
450
|
+
if ($Changed) { $Shortcut.Save(); Write-Log ('pointed ' + $Link.FullName + ' at ' + $To) }
|
|
451
|
+
} catch { Write-Log ('could not update ' + $Link.FullName + ': ' + $_.Exception.Message) }
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
# The uninstall entry Electrobun's setup made for its folder goes with the
|
|
457
|
+
# folder; the installer's own entry is the one that removes the app.
|
|
458
|
+
function Remove-UninstallEntries([string]$Folder) {
|
|
459
|
+
$Uninstall = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey($UninstallPath, $true)
|
|
460
|
+
if (-not $Uninstall) { return }
|
|
461
|
+
try {
|
|
462
|
+
foreach ($Name in @($Uninstall.GetSubKeyNames())) {
|
|
463
|
+
if ($Name -eq $InstallerEntry) { continue }
|
|
464
|
+
$Key = $Uninstall.OpenSubKey($Name)
|
|
465
|
+
if (-not $Key) { continue }
|
|
466
|
+
$Location = [string]$Key.GetValue('InstallLocation', '')
|
|
467
|
+
$Key.Close()
|
|
468
|
+
if (Test-Under $Location.TrimEnd('\') $Folder) {
|
|
469
|
+
try { $Uninstall.DeleteSubKeyTree($Name); Write-Log ('removed the uninstall entry ' + $Name) } catch { Write-Log ('could not remove the uninstall entry ' + $Name + ': ' + $_.Exception.Message) }
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
} finally { $Uninstall.Close() }
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
# Apps & features shows the version the installer's entry names.
|
|
476
|
+
function Set-InstalledVersion {
|
|
477
|
+
try {
|
|
478
|
+
$Version = [string](Get-Content -LiteralPath (Join-Path $Target 'Resources\version.json') -Raw | ConvertFrom-Json).version
|
|
479
|
+
$Key = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey($UninstallPath + '\' + $InstallerEntry, $true)
|
|
480
|
+
if ($Key) {
|
|
481
|
+
if ($Version) { $Key.SetValue('DisplayVersion', $Version); Write-Log ('the uninstall entry names version ' + $Version) }
|
|
482
|
+
$Key.Close()
|
|
483
|
+
}
|
|
484
|
+
} catch { Write-Log ('could not update the uninstall entry: ' + $_.Exception.Message) }
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
# Moved into this build's app data folder first, out of the way of an update
|
|
488
|
+
# installing into the updater's folder meanwhile, and of anything else that
|
|
489
|
+
# moves the folders under an identifier.
|
|
490
|
+
function Remove-Source([string]$Source) {
|
|
491
|
+
if (-not (Test-Path -LiteralPath $Source)) { return }
|
|
492
|
+
if ((Get-RunningUnder $Source).Count -gt 0) { Write-Log ('left ' + $Source + ': the app runs from it'); return }
|
|
493
|
+
$Doomed = $Source
|
|
494
|
+
if ((Split-Path -Leaf $Source) -notlike 'app.removing-*') {
|
|
495
|
+
New-Item -ItemType Directory -Force -Path $AppData | Out-Null
|
|
496
|
+
$Doomed = Join-Path $AppData ('app.removing-' + (Get-Stamp))
|
|
497
|
+
Invoke-Retried { Move-Item -LiteralPath $Source -Destination $Doomed } ('set aside ' + $Source)
|
|
498
|
+
}
|
|
499
|
+
Invoke-Retried { Remove-Item -LiteralPath $Doomed -Recurse -Force } ('remove ' + $Doomed)
|
|
500
|
+
Write-Log ('removed ' + $Source)
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
# One script at a time: the app opened in the installer's folder removes the
|
|
504
|
+
# copy it came from too, and waits for this one.
|
|
505
|
+
$Mutex = New-Object System.Threading.Mutex($false, 'Local\GloomInstallerFolder')
|
|
506
|
+
$Locked = $false
|
|
507
|
+
try {
|
|
508
|
+
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $Log) | Out-Null
|
|
509
|
+
try { $Locked = $Mutex.WaitOne($(if ($AfterExit) { 5000 } else { 120000 })) } catch { $Locked = ($_.Exception.GetBaseException() -is [System.Threading.AbandonedMutexException]) }
|
|
510
|
+
if ($AfterExit) {
|
|
511
|
+
# The app quits only on "started": anything that keeps the move from
|
|
512
|
+
# happening is said before, and the app runs on.
|
|
513
|
+
if (Test-Path -LiteralPath $Cancel) { Write-Log 'the app stopped waiting before this started'; return }
|
|
514
|
+
if (-not $Locked) { Write-Log 'busy: another move of the app runs'; return }
|
|
515
|
+
if (-not (Test-Path -LiteralPath (Join-Path $Sources[0] $Launcher))) { Write-Log ('declined: no ' + $Launcher + ' in ' + $Sources[0]); return }
|
|
516
|
+
$BuildFiles = New-Object 'System.Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase)
|
|
517
|
+
foreach ($File in @(Get-Files $Sources[0] '')) { [void]$BuildFiles.Add($File) }
|
|
518
|
+
# A build that just quit there for an update can take a moment to go.
|
|
519
|
+
if (-not (Wait-Until 8 { (Get-AppRunningInTarget).Count -eq 0 })) {
|
|
520
|
+
Write-Log ('busy: ' + @(Get-AppRunningInTarget)[0].Path + ' runs'); return
|
|
521
|
+
}
|
|
522
|
+
try {
|
|
523
|
+
$Probe = Join-Path $Target ('.gloom-next-' + (Get-Stamp))
|
|
524
|
+
New-Item -ItemType Directory -Path $Probe | Out-Null
|
|
525
|
+
[IO.Directory]::Delete($Probe, $false)
|
|
526
|
+
} catch { Write-Log ('declined: cannot write in ' + $Target + ': ' + $_.Exception.Message); return }
|
|
527
|
+
Write-Log ('started ' + $PID)
|
|
528
|
+
# The app and its WebView helpers, by where they run from. Having seen
|
|
529
|
+
# "started", the app quits, however long that takes: a slow quit still
|
|
530
|
+
# ends with it open again. Only one that stopped waiting for this, and
|
|
531
|
+
# wrote $Cancel, runs on, and is left so.
|
|
532
|
+
$Deadline = (Get-Date).AddMinutes(30)
|
|
533
|
+
while ((Get-RunningUnder $Sources[0]).Count -gt 0 -and -not (Test-Path -LiteralPath $Cancel) -and (Get-Date) -lt $Deadline) {
|
|
534
|
+
Start-Sleep -Milliseconds 500
|
|
535
|
+
}
|
|
536
|
+
if (Test-Path -LiteralPath $Cancel) { Write-Log 'the app stopped waiting; it stays where it is'; return }
|
|
537
|
+
if ((Get-RunningUnder $Sources[0]).Count -gt 0) { throw 'the app did not quit' }
|
|
538
|
+
$Exited = $true
|
|
539
|
+
if (-not (Wait-Until 30 { (Get-AppRunningInTarget).Count -eq 0 })) { throw ('the app runs from ' + $Target) }
|
|
540
|
+
Remove-Staging
|
|
541
|
+
Install-Build $Sources[0]
|
|
542
|
+
Write-Log ('moved ' + $Sources[0] + ' into ' + $Target)
|
|
543
|
+
Open-App (Join-Path $Target $Launcher)
|
|
544
|
+
Set-InstalledVersion
|
|
545
|
+
} else {
|
|
546
|
+
if (-not $Locked) { throw 'another move of the app runs' }
|
|
547
|
+
Write-Log ('started ' + $PID)
|
|
548
|
+
}
|
|
549
|
+
foreach ($Source in $Sources) {
|
|
550
|
+
try {
|
|
551
|
+
Update-Shortcuts $Source $Target
|
|
552
|
+
Remove-UninstallEntries $Source
|
|
553
|
+
Remove-Source $Source
|
|
554
|
+
} catch { Write-Log ('could not remove ' + $Source + ': ' + $_.Exception.Message) }
|
|
555
|
+
}
|
|
556
|
+
Remove-Staging
|
|
557
|
+
Write-Log 'done'
|
|
558
|
+
} catch {
|
|
559
|
+
if ($AfterExit) { Write-Log ('move failed: ' + $_.Exception.Message) } else { Write-Log ('removal failed: ' + $_.Exception.Message) }
|
|
560
|
+
# The app opens again only if it quit for this, from where it was.
|
|
561
|
+
if ($Exited -and -not $Opened) {
|
|
562
|
+
$Open = Join-Path $Sources[0] $Launcher
|
|
563
|
+
if (-not (Test-Path -LiteralPath $Open)) { $Open = Join-Path $Target $Launcher }
|
|
564
|
+
Open-App $Open
|
|
565
|
+
}
|
|
566
|
+
} finally {
|
|
567
|
+
$ErrorActionPreference = 'Continue'
|
|
568
|
+
if ($Locked) { try { $Mutex.ReleaseMutex() } catch { } }
|
|
569
|
+
$Mutex.Dispose()
|
|
570
|
+
if ($TaskName) { schtasks.exe /delete /tn $TaskName /f | Out-Null }
|
|
571
|
+
Remove-Item -LiteralPath $PSCommandPath -Force -ErrorAction SilentlyContinue
|
|
572
|
+
}
|
|
573
|
+
`;
|
|
574
|
+
|
|
575
|
+
/** The script that moves the app into the installer's folder or removes the updater's copy, with its paths written in. */
|
|
576
|
+
export function installerFolderScript({ target, sources, folder, log, afterExit }: InstallerFolderScript): string {
|
|
577
|
+
const header = [
|
|
578
|
+
"$ErrorActionPreference = 'Stop'",
|
|
579
|
+
`$Target = ${psString(target)}`,
|
|
580
|
+
`$Sources = @(${sources.map(psString).join(", ")})`,
|
|
581
|
+
`$AppData = ${psString(folder)}`,
|
|
582
|
+
`$Log = ${psString(log)}`,
|
|
583
|
+
`$InstallerEntry = ${psString(INSTALLER_ENTRY)}`,
|
|
584
|
+
`$AfterExit = ${afterExit ? "$true" : "$false"}`,
|
|
585
|
+
`$Cancel = ${psString(afterExit?.cancel ?? "")}`,
|
|
586
|
+
`$TaskName = ${psString(afterExit?.taskName ?? "")}`,
|
|
587
|
+
];
|
|
588
|
+
return `${header.join("\n")}\n${SCRIPT_BODY}`;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/** The tries at moving a build into the installer's folder, kept across starts. */
|
|
592
|
+
export interface MoveTries {
|
|
593
|
+
/** The build they were for: its version and hash. */
|
|
594
|
+
build: string;
|
|
595
|
+
/** Tries that failed, or would have: each one the app may have quit for. */
|
|
596
|
+
failed: number;
|
|
597
|
+
/** When the last one started, in ms. */
|
|
598
|
+
last: number;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/** Whether the app may try to move `build` now, after `tries`. */
|
|
602
|
+
export function mayTryMove(tries: MoveTries | null, build: string, now: number): boolean {
|
|
603
|
+
if (!tries || tries.build !== build) return true;
|
|
604
|
+
return tries.failed < MAX_MOVE_TRIES && now - tries.last >= RETRY_AFTER_MS;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* `tries` with one more for `build` at `now`. Every try counts as failed when
|
|
609
|
+
* it starts: one that works leaves this build's updater folder for good. One
|
|
610
|
+
* the script put off because the installer's folder was in use does not.
|
|
611
|
+
*/
|
|
612
|
+
export function noteMoveTry(tries: MoveTries | null, build: string, now: number, counts = true): MoveTries {
|
|
613
|
+
const failed = tries?.build === build ? tries.failed : 0;
|
|
614
|
+
return { build, failed: failed + (counts ? 1 : 0), last: now };
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
async function readMoveTries(path: string): Promise<MoveTries | null> {
|
|
618
|
+
const value = await readFile(path, "utf8").then((text) => JSON.parse(text) as Partial<MoveTries>, () => null);
|
|
619
|
+
if (!value || typeof value.build !== "string" || typeof value.failed !== "number" || typeof value.last !== "number") return null;
|
|
620
|
+
return { build: value.build, failed: value.failed, last: value.last };
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
async function writeMoveTries(path: string, tries: MoveTries): Promise<void> {
|
|
624
|
+
await writeFile(path, JSON.stringify(tries), "utf8");
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/** The build in `appFolder`, by its version and hash. */
|
|
628
|
+
async function buildId(appFolder: string): Promise<string> {
|
|
629
|
+
const info = await readFile(win32.join(appFolder, "Resources", "version.json"), "utf8").then((text) => JSON.parse(text) as { version?: unknown; hash?: unknown }, () => null);
|
|
630
|
+
return `${String(info?.version ?? "")}+${String(info?.hash ?? "")}`;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/** Where the script, its log and the tries go: the app data folder of this build's identifier. */
|
|
634
|
+
function scriptFolder(localAppData: string, channel: string): string {
|
|
635
|
+
return win32.join(localAppData, DESKTOP_APP_IDENTIFIER, channel);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
async function moveIntoInstallerFolder(
|
|
639
|
+
localAppData: string,
|
|
640
|
+
running: { folder: string; channel: string },
|
|
641
|
+
target: string,
|
|
642
|
+
): Promise<InstallerFolderMove> {
|
|
643
|
+
const folder = scriptFolder(localAppData, running.channel);
|
|
644
|
+
const triesPath = win32.join(folder, "installer-folder-tries.json");
|
|
645
|
+
const build = await buildId(running.folder);
|
|
646
|
+
const tries = await readMoveTries(triesPath);
|
|
647
|
+
const now = Date.now();
|
|
648
|
+
if (!mayTryMove(tries, build, now)) return { kind: "skipped" };
|
|
649
|
+
// Noted before the app can quit for it: a try that cannot be noted is not made.
|
|
650
|
+
try {
|
|
651
|
+
await mkdir(folder, { recursive: true });
|
|
652
|
+
await writeMoveTries(triesPath, noteMoveTry(tries, build, now));
|
|
653
|
+
} catch (error) {
|
|
654
|
+
logFailure("desktop-installer-folder", "Could not note a try at moving the app into the installer's folder")(error);
|
|
655
|
+
return { kind: "failed" };
|
|
656
|
+
}
|
|
657
|
+
const log = win32.join(folder, "installer-folder.log");
|
|
658
|
+
const cancel = win32.join(folder, "installer-folder.cancel");
|
|
659
|
+
await rm(log, { force: true });
|
|
660
|
+
await rm(cancel, { force: true });
|
|
661
|
+
const others = (await updaterFolders(localAppData, running.channel)).filter((path) => !samePath(path, running.folder));
|
|
662
|
+
const taskName = `GloomInstallerFolder-${now}`;
|
|
663
|
+
const script = win32.join(folder, "installer-folder-move.ps1");
|
|
664
|
+
try {
|
|
665
|
+
await writePowerShellScript(script, installerFolderScript({
|
|
666
|
+
target,
|
|
667
|
+
sources: [running.folder, ...others],
|
|
668
|
+
folder,
|
|
669
|
+
log,
|
|
670
|
+
afterExit: { cancel, taskName },
|
|
671
|
+
}));
|
|
672
|
+
const answer = await runScriptAfterExit(script, log, cancel, taskName);
|
|
673
|
+
if (answer.kind === "started") return { kind: "relaunching" };
|
|
674
|
+
if (answer.kind === "busy") await writeMoveTries(triesPath, noteMoveTry(tries, build, now, false)).catch(() => {});
|
|
675
|
+
console.error(`[desktop] the app stays where it is, for now: ${answer.reason}`);
|
|
676
|
+
return { kind: "skipped" };
|
|
677
|
+
} catch (error) {
|
|
678
|
+
logFailure("desktop-installer-folder", "Could not run the script that moves the app into the installer's folder")(error);
|
|
679
|
+
return { kind: "failed" };
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function removeUpdaterCopies(localAppData: string, channel: string, target: string, sources: string[]): InstallerFolderMove {
|
|
684
|
+
const timer = setTimeout(() => {
|
|
685
|
+
const folder = scriptFolder(localAppData, channel);
|
|
686
|
+
const script = win32.join(folder, "installer-folder-cleanup.ps1");
|
|
687
|
+
const log = win32.join(folder, "installer-folder-cleanup.log");
|
|
688
|
+
void mkdir(folder, { recursive: true })
|
|
689
|
+
.then(() => rm(log, { force: true }))
|
|
690
|
+
.then(() => writePowerShellScript(script, installerFolderScript({ target, sources, folder, log })))
|
|
691
|
+
.then(() => runScriptInBackground(script))
|
|
692
|
+
.then((code) => {
|
|
693
|
+
if (code !== 0) throw new Error(`the script exited with ${code}`);
|
|
694
|
+
})
|
|
695
|
+
.catch(logFailure("desktop-installer-folder", "Could not remove the copy of the app an update left"));
|
|
696
|
+
}, REMOVAL_DELAY_MS);
|
|
697
|
+
timer.unref?.();
|
|
698
|
+
return { kind: "removing" };
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Puts an installer install's app in the installer's folder. Anywhere else,
|
|
703
|
+
* and with nothing left in the updater's folder, it returns without writing
|
|
704
|
+
* anything. On `relaunching` the caller quits at once, before any window
|
|
705
|
+
* opens.
|
|
706
|
+
*/
|
|
707
|
+
export async function settleInstallerFolder(context: InstallerFolderContext): Promise<InstallerFolderMove> {
|
|
708
|
+
if (context.platform !== "win32") return { kind: "none" };
|
|
709
|
+
const localAppData = context.localAppData || win32.join(context.homeDir, "AppData", "Local");
|
|
710
|
+
const read = context.readInstallerFolder ?? readInstallerFolderFromRegistry;
|
|
711
|
+
|
|
712
|
+
const running = updaterAppFolder(context.execPath, localAppData);
|
|
713
|
+
if (running) {
|
|
714
|
+
const target = await installerFolder(read);
|
|
715
|
+
if (!target || samePath(target, running.folder)) return { kind: "none" };
|
|
716
|
+
return moveIntoInstallerFolder(localAppData, running, target);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const bin = win32.dirname(context.execPath);
|
|
720
|
+
if (win32.basename(bin).toLowerCase() !== "bin") return { kind: "none" };
|
|
721
|
+
const app = win32.dirname(bin);
|
|
722
|
+
const channel = await buildChannel(app);
|
|
723
|
+
const copies = await updaterFolders(localAppData, channel);
|
|
724
|
+
const leftovers = (await readdir(app).catch(() => [] as string[])).filter((name) => /^\.gloom-(next|previous)-/i.test(name));
|
|
725
|
+
if (copies.length === 0 && leftovers.length === 0) return { kind: "none" };
|
|
726
|
+
// Only from the installer's folder: a copy run from anywhere else (a
|
|
727
|
+
// portable one) leaves an install from Electrobun's own setup alone.
|
|
728
|
+
const target = await installerFolder(read);
|
|
729
|
+
if (!target || !samePath(target, app)) return { kind: "none" };
|
|
730
|
+
return removeUpdaterCopies(localAppData, channel, target, copies);
|
|
731
|
+
}
|