gloomberb 0.3.7 → 0.4.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.
Files changed (143) hide show
  1. package/README.md +19 -8
  2. package/package.json +1 -1
  3. package/src/app.tsx +45 -69
  4. package/src/cli/base-converter.ts +31 -0
  5. package/src/cli/commands/search.ts +11 -3
  6. package/src/cli/commands/ticker.ts +22 -11
  7. package/src/cli/context.ts +27 -32
  8. package/src/cli/helpers.ts +1 -4
  9. package/src/cli/index.ts +140 -123
  10. package/src/cli/registry.test.ts +226 -0
  11. package/src/cli/registry.ts +201 -0
  12. package/src/cli.test.ts +219 -0
  13. package/src/components/chart/chart-controller.ts +309 -0
  14. package/src/components/chart/chart-data.ts +5 -6
  15. package/src/components/chart/chart-pane-settings.ts +27 -0
  16. package/src/components/chart/chart-resolution.test.ts +56 -0
  17. package/src/components/chart/chart-resolution.ts +362 -0
  18. package/src/components/chart/chart-types.ts +12 -4
  19. package/src/components/chart/chart-viewport.test.ts +25 -0
  20. package/src/components/chart/chart-viewport.ts +28 -0
  21. package/src/components/chart/comparison-chart-data.ts +20 -27
  22. package/src/components/chart/comparison-stock-chart.tsx +374 -99
  23. package/src/components/chart/stock-chart-renderer-switch.test.tsx +124 -0
  24. package/src/components/chart/stock-chart.tsx +455 -106
  25. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +4 -4
  26. package/src/components/command-bar/command-bar.test.tsx +650 -5
  27. package/src/components/command-bar/command-bar.tsx +363 -63
  28. package/src/components/command-bar/command-registry.ts +8 -0
  29. package/src/components/command-bar/helpers.test.ts +23 -1
  30. package/src/components/command-bar/helpers.ts +14 -5
  31. package/src/components/command-bar/pane-template-display.ts +12 -0
  32. package/src/components/command-bar/root-shortcuts.ts +2 -1
  33. package/src/components/command-bar/workflow-ops.ts +4 -2
  34. package/src/components/detail-feed-view.test.tsx +124 -49
  35. package/src/components/detail-feed-view.tsx +396 -160
  36. package/src/components/layout/floating-pane.tsx +4 -2
  37. package/src/components/layout/header.tsx +3 -3
  38. package/src/components/layout/pane-header.tsx +26 -2
  39. package/src/components/layout/pane-sizing.ts +6 -0
  40. package/src/components/layout/pane.tsx +5 -1
  41. package/src/components/layout/shell.test.tsx +77 -5
  42. package/src/components/layout/shell.tsx +67 -52
  43. package/src/components/layout/status-bar.test.tsx +66 -0
  44. package/src/components/layout/status-bar.tsx +26 -5
  45. package/src/components/onboarding/onboarding-wizard.test.tsx +65 -0
  46. package/src/components/onboarding/onboarding-wizard.tsx +5 -6
  47. package/src/components/ticker-list-table.test.tsx +83 -0
  48. package/src/components/ticker-list-table.tsx +181 -97
  49. package/src/components/ui/data-table.tsx +50 -5
  50. package/src/components/ui/index.ts +1 -0
  51. package/src/components/ui/page-stack-view.tsx +60 -0
  52. package/src/components/use-double-click-activation.ts +31 -0
  53. package/src/index.tsx +10 -10
  54. package/src/market-data/coordinator.test.ts +92 -4
  55. package/src/market-data/coordinator.ts +32 -5
  56. package/src/market-data/hooks.test.tsx +120 -0
  57. package/src/market-data/hooks.tsx +53 -16
  58. package/src/market-data/request-types.ts +4 -2
  59. package/src/market-data/selectors.ts +24 -58
  60. package/src/plugins/builtin/chat-controller.test.ts +400 -3
  61. package/src/plugins/builtin/chat-controller.ts +328 -35
  62. package/src/plugins/builtin/chat.test.tsx +118 -6
  63. package/src/plugins/builtin/chat.tsx +180 -116
  64. package/src/plugins/builtin/comparison-chart.test.tsx +66 -5
  65. package/src/plugins/builtin/comparison-chart.tsx +21 -1
  66. package/src/plugins/builtin/layout-manager.tsx +9 -0
  67. package/src/plugins/builtin/news.tsx +1 -11
  68. package/src/plugins/builtin/portfolio-list/cli/commands.ts +656 -0
  69. package/src/plugins/builtin/portfolio-list/command-bar.ts +122 -0
  70. package/src/plugins/builtin/portfolio-list/header.tsx +16 -34
  71. package/src/plugins/builtin/portfolio-list/index.tsx +5 -0
  72. package/src/plugins/builtin/portfolio-list/metrics.test.ts +61 -0
  73. package/src/plugins/builtin/portfolio-list/metrics.ts +2 -2
  74. package/src/plugins/builtin/portfolio-list/mutations.test.ts +187 -0
  75. package/src/plugins/builtin/portfolio-list/mutations.ts +226 -0
  76. package/src/plugins/builtin/portfolio-list/pane.tsx +39 -17
  77. package/src/plugins/builtin/portfolio-list/table.tsx +11 -2
  78. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +163 -0
  79. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +81 -0
  80. package/src/plugins/builtin/sec.tsx +1 -11
  81. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +13 -2
  82. package/src/plugins/builtin/ticker-detail/financials-tab.tsx +21 -0
  83. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +45 -10
  84. package/src/plugins/builtin/ticker-detail/pane.tsx +35 -17
  85. package/src/plugins/builtin/ticker-detail/settings.ts +19 -1
  86. package/src/plugins/builtin/ticker-detail.test.tsx +47 -0
  87. package/src/plugins/catalog.ts +65 -0
  88. package/src/plugins/ibkr/broker-adapter.ts +28 -0
  89. package/src/plugins/ibkr/gateway-service.test.ts +67 -1
  90. package/src/plugins/ibkr/gateway-service.ts +147 -7
  91. package/src/plugins/ibkr/index.tsx +7 -1
  92. package/src/plugins/pane-manager.test.ts +1 -1
  93. package/src/plugins/plugin-runtime.tsx +27 -7
  94. package/src/plugins/prediction-markets/cli.ts +28 -0
  95. package/src/plugins/prediction-markets/controller-data.ts +132 -17
  96. package/src/plugins/prediction-markets/controller.ts +142 -122
  97. package/src/plugins/prediction-markets/index.tsx +7 -1
  98. package/src/plugins/prediction-markets/launch.ts +51 -16
  99. package/src/plugins/prediction-markets/metrics.ts +9 -5
  100. package/src/plugins/prediction-markets/pane.test.tsx +197 -57
  101. package/src/plugins/prediction-markets/pane.tsx +80 -155
  102. package/src/plugins/prediction-markets/services/fetch.ts +4 -0
  103. package/src/plugins/prediction-markets/table.tsx +6 -3
  104. package/src/plugins/prediction-markets/test-helpers.tsx +20 -5
  105. package/src/plugins/prediction-markets/types.ts +0 -1
  106. package/src/plugins/registry.ts +23 -0
  107. package/src/sources/gloomberb-cloud.test.ts +29 -0
  108. package/src/sources/gloomberb-cloud.ts +89 -1
  109. package/src/sources/provider-router.test.ts +265 -3
  110. package/src/sources/provider-router.ts +239 -11
  111. package/src/sources/yahoo-finance.test.ts +26 -0
  112. package/src/sources/yahoo-finance.ts +54 -3
  113. package/src/state/app-context-selector.test.tsx +99 -0
  114. package/src/state/app-context.test.ts +18 -0
  115. package/src/state/app-context.tsx +170 -65
  116. package/src/state/selectors.ts +25 -0
  117. package/src/theme/colors.ts +8 -8
  118. package/src/theme/themes.test.ts +2 -0
  119. package/src/theme/themes.ts +22 -10
  120. package/src/types/broker.ts +21 -0
  121. package/src/types/config.ts +17 -1
  122. package/src/types/data-provider.ts +18 -0
  123. package/src/types/financials.ts +31 -1
  124. package/src/types/plugin.ts +70 -0
  125. package/src/updater.test.ts +2 -0
  126. package/src/utils/api-client.test.ts +57 -0
  127. package/src/utils/api-client.ts +47 -12
  128. package/src/utils/back-navigation.ts +27 -0
  129. package/src/utils/format.test.ts +14 -0
  130. package/src/utils/format.ts +3 -3
  131. package/src/utils/market-status.ts +46 -1
  132. package/src/utils/quote-freshness.ts +127 -0
  133. package/src/utils/quote-resolution.test.ts +277 -0
  134. package/src/utils/quote-resolution.ts +641 -0
  135. package/src/utils/slugify.ts +3 -0
  136. package/src/utils/timestamp.ts +25 -0
  137. package/src/version.ts +1 -1
  138. package/src/cli/commands/portfolio.ts +0 -210
  139. package/src/cli/commands/watchlist.ts +0 -210
  140. package/src/plugins/builtin/data-providers.ts +0 -16
  141. package/src/sources/exchange-rates.ts +0 -18
  142. package/src/utils/ascii-chart.ts +0 -172
  143. package/src/utils/ticker-tokenizer.test.ts +0 -48
package/README.md CHANGED
@@ -36,7 +36,7 @@ Then run `gloomberb`.
36
36
 
37
37
  Everything, from the portfolio list to broker integrations, is a plugin.
38
38
 
39
- Plugins can add tabs, columns, commands, status bar widgets, and more.
39
+ Plugins can add tabs, columns, command bar commands, CLI commands, status bar widgets, and more.
40
40
 
41
41
  See **[PLUGINS.md](PLUGINS.md)** for the plugin API and the shared UI surface available through `gloomberb/components`.
42
42
 
@@ -56,12 +56,6 @@ See **[PLUGINS.md](PLUGINS.md)** for the plugin API and the shared UI surface av
56
56
  | **Alerts** | WIP |
57
57
  | **Scanner** | WIP |
58
58
 
59
- #### AI plugin
60
-
61
- - The ticker detail pane keeps its `Ask AI` tab for per-company questions.
62
- - The `AI Screener` pane lets you create multiple prompt-based screening tabs, refresh them, force reruns, edit the prompt, and review the last run time.
63
- - Use the `AI <prompt>` shortcut from the command bar to open a new screener pane seeded with a prompt.
64
-
65
59
  ### Data providers
66
60
 
67
61
  | Provider | Description |
@@ -78,6 +72,23 @@ See **[PLUGINS.md](PLUGINS.md)** for the plugin API and the shared UI surface av
78
72
 
79
73
  Toggleable plugins can be enabled/disabled from the command bar screen (`Ctrl+p`).
80
74
 
75
+ ## 💻 CLI
76
+
77
+ Running `gloomberb` with no arguments launches the terminal UI. Use `gloomberb help` to see the full command list.
78
+ Feature-owned root commands such as `portfolio`, `watchlist`, and `predictions` are registered by their plugins rather than hardcoded in the main CLI switch.
79
+
80
+ ```bash
81
+ gloomberb
82
+ gloomberb help
83
+ gloomberb search NVDA
84
+ gloomberb ticker AAPL
85
+ gloomberb portfolio create Research
86
+ gloomberb portfolio position set Research NVDA 10 400
87
+ gloomberb watchlist list
88
+ gloomberb plugins
89
+ gloomberb predictions world
90
+ ```
91
+
81
92
  ## ⌨️ Keyboard Shortcuts
82
93
 
83
94
  | Key | Action |
@@ -85,7 +96,7 @@ Toggleable plugins can be enabled/disabled from the command bar screen (`Ctrl+p`
85
96
  | `Ctrl+P` or `` ` `` | Open command bar |
86
97
  | `Ctrl+x` | Close current window |
87
98
  | `Tab` | Switch between panels |
88
- `j` / `k` | Navigate ticker list |
99
+ | `j` / `k` | Navigate ticker list |
89
100
  | `h` / `l` | Switch tabs |
90
101
  | `Ctrl+,` | Open settings |
91
102
  | `m` | Cycle chart mode in the chart tab |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gloomberb",
3
- "version": "0.3.7",
3
+ "version": "0.4.1",
4
4
  "description": "Bloomberg-style stock portfolio tracker for the terminal",
5
5
  "module": "src/index.tsx",
6
6
  "type": "module",
package/src/app.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { useState, useEffect, useCallback, useRef, useMemo } from "react";
1
+ import { useState, useEffect, useCallback, useRef, useMemo, useLayoutEffect } from "react";
2
2
  import { useKeyboard, useRenderer } from "@opentui/react";
3
3
  import type { CliRenderer } from "@opentui/core";
4
4
  import {
@@ -32,7 +32,7 @@ import {
32
32
  type PaneBinding,
33
33
  type PaneInstanceConfig,
34
34
  } from "./types/config";
35
- import type { PaneTemplateCreateOptions, PaneTemplateInstanceConfig, WizardStep } from "./types/plugin";
35
+ import type { CliLaunchRequest, PaneTemplateCreateOptions, PaneTemplateInstanceConfig, WizardStep } from "./types/plugin";
36
36
  import type { PaneSettingField } from "./types/plugin";
37
37
  import type { TickerRecord } from "./types/ticker";
38
38
  import type { DataProvider } from "./types/data-provider";
@@ -41,36 +41,17 @@ import type { BrokerAccount } from "./types/trading";
41
41
  import { resolveTickerSearch, upsertTickerFromSearchResult } from "./utils/ticker-search";
42
42
 
43
43
  // Built-in plugins
44
- import { portfolioListPlugin } from "./plugins/builtin/portfolio-list";
45
- import { tickerDetailPlugin } from "./plugins/builtin/ticker-detail";
46
- import { manualEntryPlugin } from "./plugins/builtin/manual-entry";
47
- import { ibkrPlugin } from "./plugins/ibkr";
48
44
  import {
49
45
  clearPersistedIbkrAccounts,
50
46
  loadPersistedIbkrAccountMap,
51
47
  } from "./plugins/ibkr/account-cache";
52
48
  import { getIbkrConfigIdentity } from "./plugins/ibkr/config";
53
- import { newsPlugin } from "./plugins/builtin/news";
54
- import { secPlugin } from "./plugins/builtin/sec";
55
- import { optionsPlugin } from "./plugins/builtin/options";
56
- import { notesPlugin } from "./plugins/builtin/notes";
57
- import { aiPlugin } from "./plugins/builtin/ai/index";
58
- import { gloomberbCloudPlugin } from "./plugins/builtin/chat";
59
49
  import { chatController } from "./plugins/builtin/chat-controller";
60
- import { helpPlugin } from "./plugins/builtin/help";
61
- import { comparisonChartPlugin } from "./plugins/builtin/comparison-chart";
62
- import { debugPlugin } from "./plugins/builtin/debug";
63
- import { layoutManagerPlugin, setLayoutManagerDispatch } from "./plugins/builtin/layout-manager";
64
- import { yahooPlugin } from "./plugins/builtin/yahoo";
65
- import { predictionMarketsPlugin } from "./plugins/prediction-markets";
66
- import {
67
- applyPredictionLaunchIntentToConfig,
68
- applyPredictionLaunchIntentToSessionSnapshot,
69
- type PredictionLaunchIntent,
70
- } from "./plugins/prediction-markets/launch";
50
+ import { setLayoutManagerDispatch } from "./plugins/builtin/layout-manager";
51
+ import { getLoadablePlugins } from "./plugins/catalog";
71
52
  import { saveConfig } from "./data/config-store";
72
53
  import { Toaster, toast } from "@opentui-ui/toast/react";
73
- import { canSelfUpdate, checkForUpdateDetailed, performUpdate } from "./updater";
54
+ import { canSelfUpdate, checkForUpdateDetailed, performUpdate, type ReleaseInfo } from "./updater";
74
55
  import { VERSION } from "./version";
75
56
  import { join } from "path";
76
57
  import {
@@ -107,6 +88,7 @@ import {
107
88
  applyPaneSettingFieldValue as applyPaneSettingFieldValueShared,
108
89
  createPaneTemplateOrThrow,
109
90
  } from "./components/command-bar/workflow-ops";
91
+ import { getPaneTemplateDisplayLabel } from "./components/command-bar/pane-template-display";
110
92
  import { debugLog } from "./utils/debug-log";
111
93
  import { MarketDataCoordinator, setSharedMarketDataCoordinator } from "./market-data/coordinator";
112
94
  import { instrumentFromTicker } from "./market-data/request-types";
@@ -543,6 +525,13 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
543
525
  }
544
526
  }, [state.config.brokerInstances, importBrokerPositions]);
545
527
 
528
+ const startUpdate = useCallback((release: ReleaseInfo) => {
529
+ dispatch({ type: "SET_UPDATE_PROGRESS", progress: { phase: "downloading", percent: 0 } });
530
+ void performUpdate(release, (progress) => {
531
+ dispatch({ type: "SET_UPDATE_PROGRESS", progress });
532
+ });
533
+ }, [dispatch]);
534
+
546
535
  const runUpdateCheck = useCallback(async (manual = false) => {
547
536
  if (manual) {
548
537
  dispatch({ type: "SET_UPDATE_CHECK_IN_PROGRESS", checking: true });
@@ -584,6 +573,12 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
584
573
  void runUpdateCheck(false);
585
574
  }, [runUpdateCheck]);
586
575
 
576
+ useEffect(() => {
577
+ if (!state.updateAvailable || state.updateProgress || state.updateCheckInProgress) return;
578
+ if (!canSelfUpdate(state.updateAvailable)) return;
579
+ startUpdate(state.updateAvailable);
580
+ }, [startUpdate, state.updateAvailable, state.updateCheckInProgress, state.updateProgress]);
581
+
587
582
  // Load tickers on mount
588
583
  useEffect(() => {
589
584
  if (state.initialized || (globalThis as any).__gloomInitStarted) return;
@@ -966,7 +961,7 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
966
961
  });
967
962
  } catch (error) {
968
963
  pluginRegistry.showToastFn(
969
- error instanceof Error ? error.message : `Could not create ${template.label.toLowerCase()}.`,
964
+ error instanceof Error ? error.message : `Could not create ${getPaneTemplateDisplayLabel(template).toLowerCase()}.`,
970
965
  { type: "info" },
971
966
  );
972
967
  }
@@ -1190,9 +1185,7 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
1190
1185
  dispatch({ type: "SET_COMMAND_BAR", open: true, query: "" });
1191
1186
  }
1192
1187
  } else if (event.name === "u" && state.updateAvailable && !state.updateProgress && !state.updateCheckInProgress && canSelfUpdate(state.updateAvailable)) {
1193
- performUpdate(state.updateAvailable, (progress) => {
1194
- dispatch({ type: "SET_UPDATE_PROGRESS", progress });
1195
- });
1188
+ startUpdate(state.updateAvailable);
1196
1189
  } else {
1197
1190
  // Plugin keyboard shortcuts (built-ins take priority)
1198
1191
  const disabledPlugins = new Set(state.config.disabledPlugins || []);
@@ -1232,32 +1225,36 @@ interface AppProps {
1232
1225
  config: AppConfig;
1233
1226
  renderer: CliRenderer;
1234
1227
  externalPlugins?: import("./plugins/loader").LoadedExternalPlugin[];
1235
- predictionLaunchIntent?: PredictionLaunchIntent | null;
1228
+ cliLaunchRequest?: CliLaunchRequest | null;
1236
1229
  }
1237
1230
 
1238
1231
  export function App({
1239
1232
  config: initialConfig,
1240
1233
  renderer,
1241
1234
  externalPlugins = [],
1242
- predictionLaunchIntent = null,
1235
+ cliLaunchRequest = null,
1243
1236
  }: AppProps) {
1244
- const applyPredictionLaunch = useCallback((configValue: AppConfig) => {
1245
- if (!predictionLaunchIntent) return configValue;
1246
- return applyPredictionLaunchIntentToConfig(configValue, predictionLaunchIntent, {
1247
- width: Math.max(renderer.terminalWidth, 120),
1248
- height: Math.max(renderer.terminalHeight, 40),
1249
- }).config;
1250
- }, [predictionLaunchIntent, renderer.terminalHeight, renderer.terminalWidth]);
1237
+ const initialCliLaunch = useMemo(() => {
1238
+ if (!cliLaunchRequest) {
1239
+ return { config: initialConfig, launchState: undefined };
1240
+ }
1241
+ return cliLaunchRequest.applyConfig(initialConfig, {
1242
+ terminalWidth: renderer.terminalWidth,
1243
+ terminalHeight: renderer.terminalHeight,
1244
+ });
1245
+ }, [cliLaunchRequest, initialConfig, renderer.terminalHeight, renderer.terminalWidth]);
1246
+ const cliLaunchStateRef = useRef(initialCliLaunch.launchState);
1251
1247
 
1252
1248
  const [config, setConfig] = useState(() => {
1253
- return applyPredictionLaunch(initialConfig);
1249
+ return initialCliLaunch.config;
1254
1250
  });
1255
1251
  const [showOnboarding, setShowOnboarding] = useState(!initialConfig.onboardingComplete);
1256
1252
 
1257
1253
  useEffect(() => bindAppActivity(renderer), [renderer]);
1258
1254
 
1259
- // Apply saved theme before first render
1260
- if (config.theme) applyTheme(config.theme);
1255
+ useLayoutEffect(() => {
1256
+ if (config.theme) applyTheme(config.theme);
1257
+ }, [config.theme]);
1261
1258
 
1262
1259
  const services = useMemo(() => {
1263
1260
  const dbPath = join(config.dataDir, ".gloomberb-cache.db");
@@ -1271,25 +1268,8 @@ export function App({
1271
1268
  pluginRegistry.getConfigFn = () => config;
1272
1269
  pluginRegistry.getLayoutFn = () => config.layout;
1273
1270
 
1274
- pluginRegistry.register(yahooPlugin);
1275
- pluginRegistry.register(gloomberbCloudPlugin);
1276
- pluginRegistry.register(portfolioListPlugin);
1277
- pluginRegistry.register(tickerDetailPlugin);
1278
- pluginRegistry.register(manualEntryPlugin);
1279
- pluginRegistry.register(ibkrPlugin);
1280
- pluginRegistry.register(layoutManagerPlugin);
1281
- pluginRegistry.register(newsPlugin);
1282
- pluginRegistry.register(secPlugin);
1283
- pluginRegistry.register(optionsPlugin);
1284
- pluginRegistry.register(notesPlugin);
1285
- pluginRegistry.register(aiPlugin);
1286
- pluginRegistry.register(helpPlugin);
1287
- pluginRegistry.register(comparisonChartPlugin);
1288
- pluginRegistry.register(predictionMarketsPlugin);
1289
- pluginRegistry.register(debugPlugin);
1290
-
1291
- for (const { plugin, error } of externalPlugins) {
1292
- if (!error) pluginRegistry.register(plugin);
1271
+ for (const plugin of getLoadablePlugins(externalPlugins)) {
1272
+ pluginRegistry.register(plugin);
1293
1273
  }
1294
1274
 
1295
1275
  return {
@@ -1309,6 +1289,7 @@ export function App({
1309
1289
  useEffect(() => {
1310
1290
  return () => {
1311
1291
  setSharedMarketDataCoordinator(null);
1292
+ services.pluginRegistry.destroy();
1312
1293
  services.persistence.close();
1313
1294
  };
1314
1295
  }, [services]);
@@ -1316,20 +1297,15 @@ export function App({
1316
1297
  const sessionSnapshot = useMemo(() => {
1317
1298
  const persisted = services.persistence.sessions.get<AppSessionSnapshot>(APP_SESSION_ID, APP_SESSION_SCHEMA_VERSION)?.value ?? null;
1318
1299
  const reconciled = reconcileAppSessionSnapshot(config, persisted);
1319
- if (!predictionLaunchIntent) {
1320
- return reconciled;
1321
- }
1322
- const paneInstanceId = config.layout.instances.find((instance) => instance.paneId === "prediction-markets")?.instanceId;
1323
- if (!paneInstanceId) {
1300
+ if (!cliLaunchRequest?.applySessionSnapshot) {
1324
1301
  return reconciled;
1325
1302
  }
1326
- return applyPredictionLaunchIntentToSessionSnapshot(
1303
+ return cliLaunchRequest.applySessionSnapshot(
1327
1304
  config,
1328
1305
  reconciled,
1329
- paneInstanceId,
1330
- predictionLaunchIntent,
1306
+ cliLaunchStateRef.current,
1331
1307
  );
1332
- }, [config, predictionLaunchIntent, services.persistence.sessions]);
1308
+ }, [cliLaunchRequest, config, services.persistence.sessions]);
1333
1309
 
1334
1310
  if (showOnboarding) {
1335
1311
  return (
@@ -1337,7 +1313,7 @@ export function App({
1337
1313
  config={config}
1338
1314
  pluginRegistry={services.pluginRegistry}
1339
1315
  onComplete={(updatedConfig) => {
1340
- setConfig(applyPredictionLaunch(updatedConfig));
1316
+ setConfig(updatedConfig);
1341
1317
  setShowOnboarding(false);
1342
1318
  }}
1343
1319
  />
@@ -0,0 +1,31 @@
1
+ import type { DataProvider } from "../types/data-provider";
2
+
3
+ export function createBaseConverter(dataProvider: Pick<DataProvider, "getExchangeRate">, baseCurrency: string) {
4
+ const rateCache = new Map<string, number>([["USD", 1]]);
5
+
6
+ const getRate = async (currency: string): Promise<number> => {
7
+ const normalizedCurrency = currency.toUpperCase();
8
+ const cached = rateCache.get(normalizedCurrency);
9
+ if (cached != null) return cached;
10
+ try {
11
+ const rate = await dataProvider.getExchangeRate(normalizedCurrency);
12
+ rateCache.set(normalizedCurrency, rate);
13
+ return rate;
14
+ } catch {
15
+ return 1;
16
+ }
17
+ };
18
+
19
+ return async (value: number, fromCurrency: string): Promise<number> => {
20
+ const normalizedFrom = fromCurrency.toUpperCase();
21
+ const normalizedBase = baseCurrency.toUpperCase();
22
+ if (normalizedFrom === normalizedBase) return value;
23
+
24
+ const [fromRate, baseRate] = await Promise.all([
25
+ getRate(normalizedFrom),
26
+ getRate(normalizedBase),
27
+ ]);
28
+ if (baseRate === 0) return value;
29
+ return (value * fromRate) / baseRate;
30
+ };
31
+ }
@@ -11,6 +11,12 @@ import {
11
11
  } from "../../utils/ticker-search";
12
12
  import { initMarketData } from "../context";
13
13
  import { fail } from "../errors";
14
+ import type { MarketContext } from "../types";
15
+
16
+ interface SearchCommandDependencies {
17
+ initMarketData?: () => Promise<MarketContext>;
18
+ fail?: (message: string, details?: string) => never;
19
+ }
14
20
 
15
21
  export async function searchCandidatesForCli({
16
22
  query,
@@ -97,13 +103,15 @@ export function buildSearchReport({
97
103
  return lines.join("\n");
98
104
  }
99
105
 
100
- export async function search(query: string) {
106
+ export async function search(query: string, dependencies: SearchCommandDependencies = {}) {
101
107
  const trimmedQuery = query.trim();
108
+ const failCommand = dependencies.fail ?? fail;
109
+ const initMarketDataFn = dependencies.initMarketData ?? initMarketData;
102
110
  if (!trimmedQuery) {
103
- fail("Usage: gloomberb search <query>");
111
+ failCommand("Usage: gloomberb search <query>");
104
112
  }
105
113
 
106
- const { store, dataProvider, persistence } = await initMarketData();
114
+ const { store, dataProvider, persistence } = await initMarketDataFn();
107
115
  const tickers = await store.loadAllTickers();
108
116
  const candidates = await searchCandidatesForCli({
109
117
  query: trimmedQuery,
@@ -15,8 +15,10 @@ import type { AppConfig } from "../../types/config";
15
15
  import type { FinancialStatement, TickerFinancials } from "../../types/financials";
16
16
  import type { NewsItem, SecFilingItem } from "../../types/data-provider";
17
17
  import type { TickerRecord } from "../../types/ticker";
18
- import { createBaseConverter, initMarketData } from "../context";
18
+ import { createBaseConverter } from "../base-converter";
19
+ import { initMarketData } from "../context";
19
20
  import { closeAndFail } from "../errors";
21
+ import type { MarketContext } from "../types";
20
22
  import {
21
23
  formatBidAsk,
22
24
  formatNullableCompact,
@@ -33,6 +35,11 @@ import { isUsEquityTicker } from "../../utils/sec";
33
35
  const NEWS_ITEM_LIMIT = 5;
34
36
  const SEC_FILING_LIMIT = 5;
35
37
 
38
+ interface TickerCommandDependencies {
39
+ initMarketData?: () => Promise<MarketContext>;
40
+ closeAndFail?: typeof closeAndFail;
41
+ }
42
+
36
43
  function appendMetricSection(lines: string[], title: string, metrics: Array<[string, string]>) {
37
44
  const populated = metrics.filter(([, value]) => value !== "—");
38
45
  if (populated.length === 0) return;
@@ -329,30 +336,34 @@ export async function buildTickerReport({
329
336
  return lines.join("\n");
330
337
  }
331
338
 
332
- export async function ticker(symbol: string) {
333
- const { config, store, dataProvider, persistence, dataDir } = await initMarketData();
339
+ export async function ticker(symbol: string, dependencies: TickerCommandDependencies = {}) {
340
+ const initMarketDataFn = dependencies.initMarketData ?? initMarketData;
341
+ const closeAndFailCommand = dependencies.closeAndFail ?? closeAndFail;
342
+ const { config, store, dataProvider, persistence, dataDir } = await initMarketDataFn();
334
343
  const normalized = symbol.trim().toUpperCase();
335
344
  const tickerFile = await store.loadTicker(normalized);
336
345
  const exchange = tickerFile?.metadata.exchange ?? "";
337
346
  const toBase = createBaseConverter(dataProvider, config.baseCurrency);
338
347
 
339
- let financials: TickerFinancials;
348
+ let financials: TickerFinancials | null = null;
340
349
  try {
341
350
  financials = await dataProvider.getTickerFinancials(normalized, exchange);
342
351
  } catch (err: any) {
343
- closeAndFail(persistence, `Failed to fetch data for ${normalized}.`, err?.message);
352
+ closeAndFailCommand(persistence, `Failed to fetch data for ${normalized}.`, err?.message);
344
353
  }
345
354
 
346
- if (!financials.quote) {
347
- closeAndFail(persistence, `No quote data available for ${normalized}.`);
355
+ if (!financials?.quote) {
356
+ closeAndFailCommand(persistence, `No quote data available for ${normalized}.`);
348
357
  }
358
+ const resolvedFinancials = financials as TickerFinancials;
359
+ const quote = resolvedFinancials.quote!;
349
360
 
350
361
  const notesFiles = new NotesFiles(dataDir);
351
362
  const [notesResult, newsResult, secFilingsResult] = await Promise.allSettled([
352
363
  notesFiles.load(normalized),
353
- dataProvider.getNews(normalized, NEWS_ITEM_LIMIT, exchange || financials.quote.exchangeName || ""),
354
- shouldFetchSecFilings(tickerFile, financials) && dataProvider.getSecFilings
355
- ? dataProvider.getSecFilings(normalized, SEC_FILING_LIMIT, exchange || financials.quote.exchangeName || "")
364
+ dataProvider.getNews(normalized, NEWS_ITEM_LIMIT, exchange || quote.exchangeName || ""),
365
+ shouldFetchSecFilings(tickerFile, resolvedFinancials) && dataProvider.getSecFilings
366
+ ? dataProvider.getSecFilings(normalized, SEC_FILING_LIMIT, exchange || quote.exchangeName || "")
356
367
  : Promise.resolve([]),
357
368
  ]);
358
369
 
@@ -363,7 +374,7 @@ export async function ticker(symbol: string) {
363
374
  console.log(await buildTickerReport({
364
375
  symbol: normalized,
365
376
  tickerFile,
366
- financials,
377
+ financials: resolvedFinancials,
367
378
  config,
368
379
  toBase,
369
380
  notes,
@@ -5,10 +5,32 @@ import { AppPersistence } from "../data/app-persistence";
5
5
  import { TickerRepository } from "../data/ticker-repository";
6
6
  import { ProviderRouter } from "../sources/provider-router";
7
7
  import type { DataProvider } from "../types/data-provider";
8
- import { createBuiltinDataProviders } from "../plugins/builtin/data-providers";
8
+ import type { AppConfig } from "../types/config";
9
+ import type { GloomPlugin } from "../types/plugin";
10
+ import { getLoadablePlugins } from "../plugins/catalog";
11
+ import { createBaseConverter } from "./base-converter";
9
12
  import { fail } from "./errors";
10
13
  import type { ConfigContext, MarketContext } from "./types";
11
14
 
15
+ interface CliContextOptions {
16
+ plugins?: GloomPlugin[];
17
+ }
18
+
19
+ function resolveCliDataProviders(config: AppConfig, plugins: GloomPlugin[]): DataProvider[] {
20
+ const disabledPlugins = new Set(config.disabledPlugins ?? []);
21
+ return plugins
22
+ .filter((plugin) => !disabledPlugins.has(plugin.id) && !!plugin.dataProvider)
23
+ .map((plugin) => plugin.dataProvider as DataProvider);
24
+ }
25
+
26
+ export async function loadCliConfigIfAvailable(): Promise<AppConfig | null> {
27
+ const dataDir = await getDataDir();
28
+ if (!dataDir || !existsSync(dataDir)) {
29
+ return null;
30
+ }
31
+ return loadConfig(dataDir);
32
+ }
33
+
12
34
  export async function initConfigData(): Promise<ConfigContext> {
13
35
  const dataDir = await getDataDir();
14
36
  if (!dataDir || !existsSync(dataDir)) {
@@ -21,42 +43,15 @@ export async function initConfigData(): Promise<ConfigContext> {
21
43
  return { config, persistence, store, dataDir };
22
44
  }
23
45
 
24
- export async function initMarketData(): Promise<MarketContext> {
46
+ export async function initMarketData(options: CliContextOptions = {}): Promise<MarketContext> {
25
47
  const context = await initConfigData();
48
+ const plugins = options.plugins ?? getLoadablePlugins();
26
49
  const dataProvider = new ProviderRouter(
27
50
  null,
28
- createBuiltinDataProviders(context.config),
51
+ resolveCliDataProviders(context.config, plugins),
29
52
  context.persistence.resources,
30
53
  );
31
54
  return { ...context, dataProvider };
32
55
  }
33
56
 
34
- export function createBaseConverter(dataProvider: Pick<DataProvider, "getExchangeRate">, baseCurrency: string) {
35
- const rateCache = new Map<string, number>([["USD", 1]]);
36
-
37
- const getRate = async (currency: string): Promise<number> => {
38
- const normalizedCurrency = currency.toUpperCase();
39
- const cached = rateCache.get(normalizedCurrency);
40
- if (cached != null) return cached;
41
- try {
42
- const rate = await dataProvider.getExchangeRate(normalizedCurrency);
43
- rateCache.set(normalizedCurrency, rate);
44
- return rate;
45
- } catch {
46
- return 1;
47
- }
48
- };
49
-
50
- return async (value: number, fromCurrency: string): Promise<number> => {
51
- const normalizedFrom = fromCurrency.toUpperCase();
52
- const normalizedBase = baseCurrency.toUpperCase();
53
- if (normalizedFrom === normalizedBase) return value;
54
-
55
- const [fromRate, baseRate] = await Promise.all([
56
- getRate(normalizedFrom),
57
- getRate(normalizedBase),
58
- ]);
59
- if (baseRate === 0) return value;
60
- return (value * fromRate) / baseRate;
61
- };
62
- }
57
+ export { createBaseConverter };
@@ -4,13 +4,10 @@ import {
4
4
  formatNumber,
5
5
  formatPercentRaw,
6
6
  } from "../utils/format";
7
+ export { slugifyName } from "../utils/slugify";
7
8
  import type { AppConfig } from "../types/config";
8
9
  import type { Watchlist, TickerRecord } from "../types/ticker";
9
10
 
10
- export function slugifyName(name: string, fallbackPrefix: string): string {
11
- return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || `${fallbackPrefix}-${Date.now()}`;
12
- }
13
-
14
11
  export function formatSignedCurrency(value: number, currency: string): string {
15
12
  return value > 0 ? `+${formatCurrency(value, currency)}` : formatCurrency(value, currency);
16
13
  }