gloomberb 0.9.1 → 0.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/README.md +23 -2
  2. package/bin/gloomberb +1 -1
  3. package/package.json +1 -1
  4. package/src/app.tsx +10 -7
  5. package/src/capabilities/registry.ts +1 -0
  6. package/src/capabilities/types.ts +23 -0
  7. package/src/cli/commands/api.ts +119 -0
  8. package/src/cli/commands/automation.ts +173 -0
  9. package/src/cli/commands/command-utils.ts +53 -0
  10. package/src/cli/commands/market.ts +527 -0
  11. package/src/cli/commands/overview.ts +222 -0
  12. package/src/cli/commands/search.ts +39 -0
  13. package/src/cli/commands/system.ts +553 -0
  14. package/src/cli/commands/ticker.ts +104 -12
  15. package/src/cli/context.ts +35 -2
  16. package/src/cli/entry.ts +51 -0
  17. package/src/cli/errors.ts +56 -5
  18. package/src/cli/index.ts +73 -11
  19. package/src/cli/options.test.ts +38 -0
  20. package/src/cli/options.ts +95 -0
  21. package/src/cli/pane-functions/index.ts +10 -1
  22. package/src/cli/registry.test.ts +40 -0
  23. package/src/cli/registry.ts +46 -5
  24. package/src/cli/result.test.ts +82 -0
  25. package/src/cli/result.ts +149 -0
  26. package/src/cli/types.ts +11 -0
  27. package/src/cli.test.ts +11 -11
  28. package/src/components/chart/comparison-stock-chart/helpers.ts +0 -32
  29. package/src/components/chart/comparison-stock-chart/interactions.ts +7 -1
  30. package/src/components/chart/comparison-stock-chart/layout-metrics.ts +6 -6
  31. package/src/components/chart/comparison-stock-chart/legend.tsx +45 -44
  32. package/src/components/chart/comparison-stock-chart/render-data.ts +37 -8
  33. package/src/components/chart/comparison-stock-chart/selection-runtime.ts +19 -2
  34. package/src/components/chart/comparison-stock-chart/sources.ts +8 -4
  35. package/src/components/chart/comparison-stock-chart/toolbar.tsx +13 -3
  36. package/src/components/chart/comparison-stock-chart/view.tsx +56 -6
  37. package/src/components/chart/core/pane-settings.test.ts +27 -0
  38. package/src/components/chart/core/pane-settings.ts +60 -1
  39. package/src/components/chart/core/pointer.ts +2 -0
  40. package/src/components/chart/stock-chart/resolved/runtime.ts +1 -1
  41. package/src/components/chart/stock-chart/settings.ts +7 -3
  42. package/src/components/chart/stock-chart/viewport/runtime.ts +5 -5
  43. package/src/components/command-bar/pane-templates/workflow-route.ts +10 -0
  44. package/src/components/command-bar/surface/pane.test.tsx +76 -0
  45. package/src/components/data-table/view.tsx +13 -2
  46. package/src/components/feed-data-table/stack-view.tsx +9 -1
  47. package/src/components/index.ts +1 -1
  48. package/src/components/input-search-bar.test.tsx +103 -0
  49. package/src/components/input-search-bar.tsx +16 -0
  50. package/src/components/layout/detached-pane-shell.tsx +5 -2
  51. package/src/components/layout/header.tsx +13 -5
  52. package/src/components/layout/shell/active-drag.ts +5 -1
  53. package/src/components/layout/shell/drag/runtime.ts +21 -3
  54. package/src/components/layout/shell/fullscreen.ts +10 -0
  55. package/src/components/layout/shell/index.test.tsx +395 -2
  56. package/src/components/layout/shell/index.tsx +128 -2
  57. package/src/components/layout/shell/native/pointer-runtime.ts +10 -4
  58. package/src/components/layout/shell/pane/layers.tsx +26 -14
  59. package/src/components/layout/shell/pane/management-shortcuts.ts +19 -0
  60. package/src/components/layout/shell/shortcuts.ts +4 -0
  61. package/src/components/layout/shell/terminal-pointer-runtime.ts +29 -19
  62. package/src/components/layout/shell/window-mode/index.ts +11 -3
  63. package/src/components/layout/shell/window-mode/overlays.tsx +8 -1
  64. package/src/components/layout/status-bar.test.tsx +105 -0
  65. package/src/components/layout/status-bar.tsx +32 -6
  66. package/src/components/layout/transient-layout.tsx +57 -0
  67. package/src/components/layout/window-controls.tsx +19 -10
  68. package/src/components/layout/window-edit/mode.test.ts +76 -5
  69. package/src/components/layout/window-edit/mode.ts +47 -2
  70. package/src/components/layout/window-edit/presentation.ts +2 -1
  71. package/src/components/price-performance.tsx +183 -0
  72. package/src/core/app-services.ts +7 -4
  73. package/src/data/config/store/index.ts +1 -2
  74. package/src/market-data/performance.test.ts +54 -0
  75. package/src/market-data/performance.ts +109 -0
  76. package/src/plugins/builtin/cloud-tweets/pane.tsx +2 -0
  77. package/src/plugins/builtin/cloud-tweets/search-bar.tsx +3 -0
  78. package/src/plugins/builtin/cloud-tweets/table.tsx +14 -2
  79. package/src/plugins/builtin/comparison-chart/index.test.tsx +218 -15
  80. package/src/plugins/builtin/congress-trades/detail.tsx +204 -68
  81. package/src/plugins/builtin/congress-trades/footer.ts +14 -8
  82. package/src/plugins/builtin/congress-trades/keyboard.ts +4 -23
  83. package/src/plugins/builtin/congress-trades/model.ts +29 -0
  84. package/src/plugins/builtin/congress-trades/pane.tsx +8 -13
  85. package/src/plugins/builtin/congress-trades/table.tsx +10 -1
  86. package/src/plugins/builtin/news/wire/read-state.ts +9 -32
  87. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +23 -0
  88. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +19 -2
  89. package/src/plugins/builtin/portfolio-list/pane/index.tsx +1 -1
  90. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -7
  91. package/src/plugins/builtin/shared/read-state.ts +43 -0
  92. package/src/plugins/builtin/substack/article-detail.test.tsx +10 -0
  93. package/src/plugins/builtin/substack/article-detail.tsx +39 -15
  94. package/src/plugins/builtin/substack/article-stack.tsx +6 -2
  95. package/src/plugins/builtin/substack/feed-tabs.tsx +12 -9
  96. package/src/plugins/builtin/substack/pane.tsx +8 -3
  97. package/src/plugins/builtin/substack/read-state.ts +55 -0
  98. package/src/plugins/builtin/thirteenf/pane.test.tsx +26 -0
  99. package/src/plugins/builtin/thirteenf/pane.tsx +12 -1
  100. package/src/plugins/builtin/ticker-detail/overview/model.ts +0 -14
  101. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +25 -0
  102. package/src/plugins/pane-manager/docking.ts +9 -2
  103. package/src/plugins/pane-manager/queries.ts +8 -2
  104. package/src/plugins/prediction-markets/controller/index.ts +2 -0
  105. package/src/plugins/prediction-markets/controller/keyboard.ts +34 -5
  106. package/src/plugins/prediction-markets/pane.test.tsx +32 -0
  107. package/src/plugins/prediction-markets/pane.tsx +21 -1
  108. package/src/plugins/prediction-markets/test-helpers.tsx +1 -0
  109. package/src/renderers/electrobun/bun/application-menu/click.test.ts +9 -0
  110. package/src/renderers/electrobun/bun/application-menu/click.ts +1 -0
  111. package/src/renderers/electrobun/bun/application-menu/index.test.ts +9 -0
  112. package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
  113. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +9 -2
  114. package/src/renderers/electrobun/bun/desktop/initialization.ts +1 -0
  115. package/src/renderers/electrobun/bun/desktop/window-controls.ts +11 -2
  116. package/src/renderers/electrobun/bun/desktop/window-style.ts +27 -1
  117. package/src/renderers/electrobun/bun/desktop/windows-custom-chrome.ts +58 -0
  118. package/src/renderers/electrobun/bun/desktop/windows-icons.ts +11 -64
  119. package/src/renderers/electrobun/bun/desktop/windows-native.ts +75 -0
  120. package/src/renderers/electrobun/bun/index.ts +22 -2
  121. package/src/renderers/electrobun/bun/window/focus.test.ts +35 -0
  122. package/src/renderers/electrobun/bun/window/focus.ts +5 -0
  123. package/src/renderers/electrobun/shared/protocol.ts +1 -0
  124. package/src/renderers/electrobun/view/app-services.ts +6 -4
  125. package/src/renderers/electrobun/view/host/ascii-text.ts +50 -0
  126. package/src/renderers/electrobun/view/host/text.test.ts +22 -0
  127. package/src/renderers/electrobun/view/host/text.tsx +24 -20
  128. package/src/renderers/electrobun/view/main.tsx +2 -1
  129. package/src/renderers/electrobun/view/styles.css +9 -8
  130. package/src/renderers/electrobun/view/ui-host.tsx +102 -77
  131. package/src/renderers/opentui/start.tsx +13 -5
  132. package/src/state/app/input-capture.ts +12 -0
  133. package/src/types/plugin.ts +28 -0
  134. package/src/utils/cli-output.ts +6 -0
  135. package/src/utils/search-focus-navigation.ts +24 -0
  136. package/src/version.ts +1 -1
@@ -17,6 +17,7 @@ import type { FinancialStatement, TickerFinancials } from "../../types/financial
17
17
  import type { SecFilingItem } from "../../types/data-provider";
18
18
  import type { NewsArticle } from "../../news/types";
19
19
  import type { TickerRecord } from "../../types/ticker";
20
+ import type { CliCommandContext } from "../../types/plugin";
20
21
  import { createBaseConverter } from "../base-converter";
21
22
  import { initMarketData } from "../context";
22
23
  import { closeAndFail } from "../errors";
@@ -40,6 +41,7 @@ const SEC_FILING_LIMIT = 5;
40
41
  interface TickerCommandDependencies {
41
42
  initMarketData?: () => Promise<MarketContext>;
42
43
  closeAndFail?: typeof closeAndFail;
44
+ printResult?: CliCommandContext["printResult"];
43
45
  }
44
46
 
45
47
  function appendMetricSection(lines: string[], title: string, metrics: Array<[string, string]>) {
@@ -367,6 +369,78 @@ export async function buildTickerReport({
367
369
  return lines.join("\n");
368
370
  }
369
371
 
372
+ function buildTickerStructuredData({
373
+ symbol,
374
+ tickerFile,
375
+ financials,
376
+ config,
377
+ notes,
378
+ recentNews,
379
+ recentSecFilings,
380
+ }: {
381
+ symbol: string;
382
+ tickerFile: TickerRecord | null;
383
+ financials: TickerFinancials;
384
+ config: AppConfig;
385
+ notes: string;
386
+ recentNews: NewsArticle[];
387
+ recentSecFilings: SecFilingItem[];
388
+ }) {
389
+ const quote = financials.quote;
390
+ return {
391
+ symbol,
392
+ quote: quote ? {
393
+ symbol: quote.symbol,
394
+ name: quote.name,
395
+ price: quote.price,
396
+ change: quote.change,
397
+ changePercent: quote.changePercent,
398
+ currency: quote.currency,
399
+ marketCap: quote.marketCap ?? null,
400
+ volume: quote.volume ?? null,
401
+ exchangeName: quote.exchangeName ?? "",
402
+ fullExchangeName: quote.fullExchangeName ?? "",
403
+ marketState: quote.marketState ?? "",
404
+ dataSource: quote.dataSource ?? "",
405
+ providerId: quote.providerId ?? "",
406
+ lastUpdated: quote.lastUpdated ? new Date(quote.lastUpdated).toISOString() : "",
407
+ } : null,
408
+ ticker: tickerFile ? {
409
+ ticker: tickerFile.metadata.ticker,
410
+ name: tickerFile.metadata.name ?? "",
411
+ exchange: tickerFile.metadata.exchange ?? "",
412
+ assetCategory: tickerFile.metadata.assetCategory ?? "",
413
+ sector: tickerFile.metadata.sector ?? "",
414
+ industry: tickerFile.metadata.industry ?? "",
415
+ portfolios: formatPortfolioNames(config, tickerFile.metadata.portfolios),
416
+ watchlists: formatWatchlistNames(config, tickerFile.metadata.watchlists),
417
+ positions: tickerFile.metadata.positions,
418
+ } : null,
419
+ fundamentals: financials.fundamentals,
420
+ profile: financials.profile,
421
+ latestAnnual: financials.annualStatements.at(-1) ?? null,
422
+ latestQuarter: financials.quarterlyStatements.at(-1) ?? null,
423
+ annualStatementCount: financials.annualStatements.length,
424
+ quarterlyStatementCount: financials.quarterlyStatements.length,
425
+ notes,
426
+ recentNews: recentNews.map((item) => ({
427
+ title: item.title,
428
+ source: item.source,
429
+ publishedAt: item.publishedAt instanceof Date ? item.publishedAt.toISOString() : item.publishedAt,
430
+ url: item.url,
431
+ summary: item.summary ?? "",
432
+ })),
433
+ recentSecFilings: recentSecFilings.map((filing) => ({
434
+ form: filing.form,
435
+ filingDate: filing.filingDate instanceof Date ? filing.filingDate.toISOString() : filing.filingDate,
436
+ accessionNumber: filing.accessionNumber,
437
+ filingUrl: filing.filingUrl,
438
+ primaryDocumentUrl: filing.primaryDocumentUrl ?? "",
439
+ description: getFilingDescription(filing) ?? "",
440
+ })),
441
+ };
442
+ }
443
+
370
444
  export async function ticker(symbol: string, dependencies: TickerCommandDependencies = {}) {
371
445
  const initMarketDataFn = dependencies.initMarketData ?? initMarketData;
372
446
  const closeAndFailCommand = dependencies.closeAndFail ?? closeAndFail;
@@ -379,8 +453,12 @@ export async function ticker(symbol: string, dependencies: TickerCommandDependen
379
453
  let financials: TickerFinancials | null = null;
380
454
  try {
381
455
  financials = await dataProvider.getTickerFinancials(normalized, exchange);
382
- } catch (err: any) {
383
- closeAndFailCommand(persistence, `Failed to fetch data for ${normalized}.`, err?.message);
456
+ } catch (error) {
457
+ closeAndFailCommand(
458
+ persistence,
459
+ `Failed to fetch data for ${normalized}.`,
460
+ error instanceof Error ? error.message : String(error),
461
+ );
384
462
  }
385
463
 
386
464
  if (!financials?.quote) {
@@ -409,16 +487,30 @@ export async function ticker(symbol: string, dependencies: TickerCommandDependen
409
487
  const recentNews = newsResult.status === "fulfilled" ? newsResult.value : [];
410
488
  const recentSecFilings = secFilingsResult.status === "fulfilled" ? secFilingsResult.value : [];
411
489
 
412
- console.log(await buildTickerReport({
413
- symbol: normalized,
414
- tickerFile,
415
- financials: resolvedFinancials,
416
- config,
417
- toBase,
418
- notes,
419
- recentNews,
420
- recentSecFilings,
421
- }));
490
+ if (dependencies.printResult) {
491
+ dependencies.printResult({
492
+ data: buildTickerStructuredData({
493
+ symbol: normalized,
494
+ tickerFile,
495
+ financials: resolvedFinancials,
496
+ config,
497
+ notes,
498
+ recentNews,
499
+ recentSecFilings,
500
+ }),
501
+ });
502
+ } else {
503
+ console.log(await buildTickerReport({
504
+ symbol: normalized,
505
+ tickerFile,
506
+ financials: resolvedFinancials,
507
+ config,
508
+ toBase,
509
+ notes,
510
+ recentNews,
511
+ recentSecFilings,
512
+ }));
513
+ }
422
514
 
423
515
  persistence.close();
424
516
  }
@@ -4,10 +4,13 @@ import { getDataDir, loadConfig } from "../data/config/store";
4
4
  import { AppPersistence } from "../data/app-persistence";
5
5
  import { TickerRepository } from "../data/ticker-repository";
6
6
  import { AssetDataRouter } from "../sources/provider-router";
7
+ import { createAppServices } from "../core/app-services";
7
8
  import type { PluginCapability } from "../capabilities";
8
9
  import type { AppConfig } from "../types/config";
9
10
  import type { GloomPlugin } from "../types/plugin";
10
11
  import { getLoadablePlugins } from "../plugins/catalog";
12
+ import type { PluginRegistry } from "../plugins/registry";
13
+ import type { LoadedExternalPlugin } from "../plugins/loader";
11
14
  import { fail } from "./errors";
12
15
  import type { ConfigContext, MarketContext } from "./types";
13
16
 
@@ -15,6 +18,10 @@ interface CliContextOptions {
15
18
  plugins?: GloomPlugin[];
16
19
  }
17
20
 
21
+ interface CliServicesOptions {
22
+ externalPlugins?: LoadedExternalPlugin[];
23
+ }
24
+
18
25
  function resolveCliCapabilities(config: AppConfig, plugins: GloomPlugin[]): PluginCapability[] {
19
26
  const disabledPlugins = new Set(config.disabledPlugins ?? []);
20
27
  const disabledSources = new Set(config.disabledSources ?? []);
@@ -55,9 +62,35 @@ export async function initMarketData(options: CliContextOptions = {}): Promise<M
55
62
  const plugins = options.plugins ?? getLoadablePlugins();
56
63
  const capabilities = resolveCliCapabilities(context.config, plugins);
57
64
  const dataProvider = new AssetDataRouter(null, [], context.persistence.resources);
58
- dataProvider.attachRegistry({
65
+ const registryAdapter: Pick<PluginRegistry, "brokers" | "getEnabledCapabilities"> = {
59
66
  brokers: new Map(),
60
67
  getEnabledCapabilities: (kind?: string) => capabilities.filter((capability) => !kind || capability.kind === kind),
61
- } as any);
68
+ };
69
+ dataProvider.attachRegistry(registryAdapter as PluginRegistry);
70
+ dataProvider.setConfigAccessor(() => context.config);
62
71
  return { ...context, dataProvider };
63
72
  }
73
+
74
+ export async function initCliServices(options: CliServicesOptions = {}) {
75
+ const dataDir = await getDataDir();
76
+ if (!dataDir || !existsSync(dataDir)) {
77
+ fail("No data directory configured.", "Run gloomberb once to initialize your local data.");
78
+ }
79
+
80
+ const config = await loadConfig(dataDir);
81
+ const services = createAppServices({
82
+ config,
83
+ externalPlugins: options.externalPlugins ?? [],
84
+ });
85
+ services.providerRouter.setConfigAccessor(() => config);
86
+ await services.ready;
87
+ return {
88
+ config,
89
+ dataDir,
90
+ services,
91
+ dataProvider: services.providerRouter,
92
+ persistence: services.persistence,
93
+ store: services.tickerRepository,
94
+ destroy: () => services.destroy(),
95
+ };
96
+ }
@@ -0,0 +1,51 @@
1
+ import { dispatchCli } from "./index";
2
+ import { fail, inferCliErrorOptions, printCliError } from "./errors";
3
+ import { loadExternalPlugins } from "../plugins/loader";
4
+ import type { CliLaunchRequest } from "../types/plugin";
5
+
6
+ async function launchOpenTuiApp(options: {
7
+ externalPlugins: Awaited<ReturnType<typeof loadExternalPlugins>>;
8
+ cliLaunchRequest?: CliLaunchRequest | null;
9
+ cliArgs?: string[];
10
+ }): Promise<void> {
11
+ const { startOpenTuiApp } = await import("../renderers/opentui/start");
12
+ await startOpenTuiApp({
13
+ externalPlugins: options.externalPlugins,
14
+ cliArgs: options.cliArgs ?? [],
15
+ skipCliDispatch: true,
16
+ cliLaunchRequest: options.cliLaunchRequest ?? null,
17
+ });
18
+ }
19
+
20
+ export async function runCliEntrypoint(rawArgs = process.argv.slice(2)): Promise<void> {
21
+ const externalPlugins = await loadExternalPlugins();
22
+ const command = rawArgs[0];
23
+
24
+ if (!command) {
25
+ await launchOpenTuiApp({ externalPlugins });
26
+ return;
27
+ }
28
+
29
+ if (command === "launch-ui" || command === "ui") {
30
+ await launchOpenTuiApp({ externalPlugins, cliArgs: rawArgs.slice(1) });
31
+ return;
32
+ }
33
+
34
+ const dispatchResult = await dispatchCli(rawArgs, { externalPlugins });
35
+ if (dispatchResult.kind === "handled") return;
36
+ if (dispatchResult.kind === "launch-ui") {
37
+ await launchOpenTuiApp({
38
+ externalPlugins,
39
+ cliLaunchRequest: dispatchResult.request,
40
+ cliArgs: [],
41
+ });
42
+ return;
43
+ }
44
+
45
+ fail(`Unknown command "${command}".`, "Run gloomberb help to list available commands.");
46
+ }
47
+
48
+ runCliEntrypoint().catch((error) => {
49
+ printCliError(error, inferCliErrorOptions(process.argv.slice(2)));
50
+ process.exitCode = 1;
51
+ });
package/src/cli/errors.ts CHANGED
@@ -1,13 +1,64 @@
1
- import { cliStyles } from "../utils/cli-output";
2
1
  import type { AppPersistence } from "../data/app-persistence";
2
+ import { DEFAULT_CLI_OPTIONS, type CliGlobalOptions } from "./options";
3
+ import { serializeCliError, type CliErrorObject } from "./result";
3
4
 
4
- export function fail(message: string, details?: string): never {
5
- console.error(cliStyles.danger(message));
6
- if (details) console.error(cliStyles.muted(details));
7
- process.exit(1);
5
+ export class CliFailure extends Error {
6
+ readonly code: string;
7
+ readonly details?: unknown;
8
+ readonly retryable?: boolean;
9
+
10
+ constructor(message: string, details?: unknown, code = "cli_error", retryable?: boolean) {
11
+ super(message);
12
+ this.name = "CliFailure";
13
+ this.code = code;
14
+ this.details = details;
15
+ this.retryable = retryable;
16
+ }
17
+ }
18
+
19
+ export function fail(message: string, details?: unknown, code?: string): never {
20
+ throw new CliFailure(message, details, code);
8
21
  }
9
22
 
10
23
  export function closeAndFail(persistence: AppPersistence, message: string, details?: string): never {
11
24
  persistence.close();
12
25
  fail(message, details);
13
26
  }
27
+
28
+ export function isCliFailure(error: unknown): error is CliFailure {
29
+ return error instanceof CliFailure;
30
+ }
31
+
32
+ export function cliErrorObject(error: unknown): CliErrorObject {
33
+ if (isCliFailure(error)) {
34
+ return {
35
+ code: error.code,
36
+ message: error.message,
37
+ details: error.details,
38
+ retryable: error.retryable,
39
+ };
40
+ }
41
+ return {
42
+ code: "unexpected_error",
43
+ message: error instanceof Error ? error.message : String(error),
44
+ };
45
+ }
46
+
47
+ export function inferCliErrorOptions(rawArgs: string[]): CliGlobalOptions {
48
+ const options: CliGlobalOptions = { ...DEFAULT_CLI_OPTIONS };
49
+ for (const arg of rawArgs) {
50
+ if (arg === "--") break;
51
+ if (arg === "--json") options.format = "json";
52
+ if (arg === "--csv") options.format = "csv";
53
+ if (arg === "--ndjson") options.format = "ndjson";
54
+ if (arg === "--quiet" || arg === "-q") options.quiet = true;
55
+ if (arg === "--no-color") options.color = false;
56
+ if (arg === "--color") options.color = true;
57
+ }
58
+ return options;
59
+ }
60
+
61
+ export function printCliError(error: unknown, options: CliGlobalOptions): void {
62
+ if (options.quiet && options.format === "text") return;
63
+ console.error(serializeCliError(cliErrorObject(error), options));
64
+ }
package/src/cli/index.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { VERSION } from "../version";
2
- import type { CliCommandDef, CliDispatchResult } from "../types/plugin";
2
+ import type { CliCommandDef, CliDispatchResult, CliLaunchRequest } from "../types/plugin";
3
3
  import type { LoadedExternalPlugin } from "../plugins/loader";
4
4
  import { loadCliConfigIfAvailable } from "./context";
5
+ import { parseCliGlobalArgs } from "./options";
5
6
  import {
6
7
  buildCliCommandRegistry,
7
8
  createCliCommandContext,
@@ -10,9 +11,21 @@ import {
10
11
  renderCliHelp,
11
12
  type CliCommandRegistry,
12
13
  } from "./registry";
13
- import { fail } from "./errors";
14
+ import { fail, inferCliErrorOptions, printCliError } from "./errors";
15
+ import { setCliColorEnabledOverride } from "../utils/cli-output";
14
16
  import { search, searchCandidatesForCli, buildSearchReport } from "./commands/search";
15
17
  import { ticker, buildTickerReport } from "./commands/ticker";
18
+ import { apiCliCommand } from "./commands/api";
19
+ import { marketDataCliCommands } from "./commands/market";
20
+ import { overviewCliCommands } from "./commands/overview";
21
+ import { createSystemCliCommands } from "./commands/system";
22
+ import {
23
+ aiCliCommand,
24
+ brokerCliCommand,
25
+ cloudCliCommands,
26
+ ibkrCliCommand,
27
+ rssCliCommand,
28
+ } from "./commands/automation";
16
29
  import {
17
30
  installPlugin,
18
31
  listPlugins,
@@ -22,7 +35,11 @@ import {
22
35
  import { runPaneCatalog, runPaneFunction, runPaneScreenshot } from "./pane-functions";
23
36
 
24
37
  function createCoreCliCommands(renderHelp: () => string): CliCommandDef[] {
25
- return [
38
+ let commands: CliCommandDef[] = [];
39
+ const launchUiRequest: CliLaunchRequest = {
40
+ applyConfig: (config) => ({ config }),
41
+ };
42
+ commands = [
26
43
  {
27
44
  name: "help",
28
45
  aliases: ["--help", "-h"],
@@ -30,10 +47,28 @@ function createCoreCliCommands(renderHelp: () => string): CliCommandDef[] {
30
47
  help: {
31
48
  usage: ["help"],
32
49
  },
33
- execute: () => {
34
- console.log(renderHelp());
50
+ execute: (_args, ctx) => {
51
+ if (ctx.cliOptions.format === "text") {
52
+ console.log(renderHelp());
53
+ return;
54
+ }
55
+ ctx.printResult({ data: commands.map((command) => ({
56
+ name: command.name,
57
+ aliases: command.aliases ?? [],
58
+ description: command.description,
59
+ usage: command.help?.usage ?? [],
60
+ })) });
35
61
  },
36
62
  },
63
+ {
64
+ name: "launch-ui",
65
+ aliases: ["ui"],
66
+ description: "Launch the terminal UI explicitly",
67
+ help: {
68
+ usage: ["launch-ui"],
69
+ },
70
+ execute: () => ({ kind: "launch-ui", request: launchUiRequest }),
71
+ },
37
72
  {
38
73
  name: "search",
39
74
  description: "Search tickers and company names",
@@ -45,6 +80,7 @@ function createCoreCliCommands(renderHelp: () => string): CliCommandDef[] {
45
80
  await search(query, {
46
81
  initMarketData: ctx.initMarketData,
47
82
  fail: ctx.fail,
83
+ ...(ctx.cliOptions.format === "text" ? {} : { printResult: ctx.printResult }),
48
84
  });
49
85
  },
50
86
  },
@@ -62,6 +98,7 @@ function createCoreCliCommands(renderHelp: () => string): CliCommandDef[] {
62
98
  await ticker(symbol!, {
63
99
  initMarketData: ctx.initMarketData,
64
100
  closeAndFail: ctx.closeAndFail,
101
+ ...(ctx.cliOptions.format === "text" ? {} : { printResult: ctx.printResult }),
65
102
  });
66
103
  },
67
104
  },
@@ -148,7 +185,17 @@ function createCoreCliCommands(renderHelp: () => string): CliCommandDef[] {
148
185
  listPlugins();
149
186
  },
150
187
  },
188
+ apiCliCommand,
189
+ ...marketDataCliCommands,
190
+ ...overviewCliCommands,
191
+ ...createSystemCliCommands(() => commands),
192
+ brokerCliCommand,
193
+ ibkrCliCommand,
194
+ aiCliCommand,
195
+ rssCliCommand,
196
+ ...cloudCliCommands,
151
197
  ];
198
+ return commands;
152
199
  }
153
200
 
154
201
  export interface DispatchCliOptions {
@@ -170,7 +217,16 @@ async function createRegistry(options: DispatchCliOptions = {}): Promise<CliComm
170
217
  export { buildSearchReport, buildTickerReport, searchCandidatesForCli };
171
218
 
172
219
  export async function dispatchCli(args: string[], options: DispatchCliOptions = {}): Promise<CliDispatchResult> {
173
- const command = args[0];
220
+ let parsed;
221
+ try {
222
+ parsed = parseCliGlobalArgs(args);
223
+ } catch (error) {
224
+ printCliError(error, inferCliErrorOptions(args));
225
+ process.exitCode = 1;
226
+ return { kind: "handled" };
227
+ }
228
+ setCliColorEnabledOverride(parsed.options.color);
229
+ const command = parsed.args[0];
174
230
  if (!command) {
175
231
  return { kind: "unhandled" };
176
232
  }
@@ -181,11 +237,17 @@ export async function dispatchCli(args: string[], options: DispatchCliOptions =
181
237
  return { kind: "unhandled" };
182
238
  }
183
239
 
184
- const result = await resolved.command.execute(
185
- args.slice(1),
186
- createCliCommandContext(resolved.ownerId, registry.plugins),
187
- );
188
- return normalizeCliDispatchResult(result);
240
+ try {
241
+ const result = await resolved.command.execute(
242
+ parsed.args.slice(1),
243
+ createCliCommandContext(resolved.ownerId, registry, parsed.options),
244
+ );
245
+ return normalizeCliDispatchResult(result);
246
+ } catch (error) {
247
+ printCliError(error, parsed.options);
248
+ process.exitCode = 1;
249
+ return { kind: "handled" };
250
+ }
189
251
  }
190
252
 
191
253
  export async function runCli(args: string[], options: DispatchCliOptions = {}): Promise<boolean> {
@@ -0,0 +1,38 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { parseCliGlobalArgs } from "./options";
3
+
4
+ describe("parseCliGlobalArgs", () => {
5
+ test("extracts global output and execution flags anywhere before --", () => {
6
+ const parsed = parseCliGlobalArgs([
7
+ "quote",
8
+ "--json",
9
+ "AAPL",
10
+ "--limit",
11
+ "2",
12
+ "--refresh",
13
+ "--dry-run",
14
+ "--yes",
15
+ "--no-color",
16
+ ]);
17
+
18
+ expect(parsed.args).toEqual(["quote", "AAPL"]);
19
+ expect(parsed.options).toMatchObject({
20
+ format: "json",
21
+ limit: 2,
22
+ refresh: true,
23
+ dryRun: true,
24
+ yes: true,
25
+ color: false,
26
+ });
27
+ });
28
+
29
+ test("leaves arguments after -- untouched", () => {
30
+ const parsed = parseCliGlobalArgs(["ai", "ask", "--", "--json"]);
31
+ expect(parsed.args).toEqual(["ai", "ask", "--json"]);
32
+ expect(parsed.options.format).toBe("text");
33
+ });
34
+
35
+ test("rejects invalid limits", () => {
36
+ expect(() => parseCliGlobalArgs(["quote", "AAPL", "--limit=0"])).toThrow("--limit");
37
+ });
38
+ });
@@ -0,0 +1,95 @@
1
+ export type CliOutputFormat = "text" | "json" | "csv" | "ndjson";
2
+
3
+ export interface CliGlobalOptions {
4
+ format: CliOutputFormat;
5
+ quiet: boolean;
6
+ color: boolean | null;
7
+ limit?: number;
8
+ refresh: boolean;
9
+ dryRun: boolean;
10
+ yes: boolean;
11
+ }
12
+
13
+ export interface ParsedCliArgs {
14
+ args: string[];
15
+ options: CliGlobalOptions;
16
+ }
17
+
18
+ export const DEFAULT_CLI_OPTIONS: CliGlobalOptions = {
19
+ format: "text",
20
+ quiet: false,
21
+ color: null,
22
+ refresh: false,
23
+ dryRun: false,
24
+ yes: false,
25
+ };
26
+
27
+ function parseLimit(value: string | undefined): number {
28
+ if (!value) throw new Error("Missing value for --limit.");
29
+ const parsed = Number(value);
30
+ if (!Number.isInteger(parsed) || parsed < 1) {
31
+ throw new Error("--limit must be a positive integer.");
32
+ }
33
+ return parsed;
34
+ }
35
+
36
+ export function parseCliGlobalArgs(rawArgs: string[]): ParsedCliArgs {
37
+ const options: CliGlobalOptions = { ...DEFAULT_CLI_OPTIONS };
38
+ const args: string[] = [];
39
+
40
+ for (let index = 0; index < rawArgs.length; index += 1) {
41
+ const arg = rawArgs[index]!;
42
+ if (arg === "--") {
43
+ args.push(...rawArgs.slice(index + 1));
44
+ break;
45
+ }
46
+ if (arg === "--json") {
47
+ options.format = "json";
48
+ continue;
49
+ }
50
+ if (arg === "--csv") {
51
+ options.format = "csv";
52
+ continue;
53
+ }
54
+ if (arg === "--ndjson") {
55
+ options.format = "ndjson";
56
+ continue;
57
+ }
58
+ if (arg === "--quiet" || arg === "-q") {
59
+ options.quiet = true;
60
+ continue;
61
+ }
62
+ if (arg === "--no-color") {
63
+ options.color = false;
64
+ continue;
65
+ }
66
+ if (arg === "--color") {
67
+ options.color = true;
68
+ continue;
69
+ }
70
+ if (arg === "--refresh") {
71
+ options.refresh = true;
72
+ continue;
73
+ }
74
+ if (arg === "--dry-run") {
75
+ options.dryRun = true;
76
+ continue;
77
+ }
78
+ if (arg === "--yes" || arg === "-y") {
79
+ options.yes = true;
80
+ continue;
81
+ }
82
+ if (arg === "--limit") {
83
+ index += 1;
84
+ options.limit = parseLimit(rawArgs[index]);
85
+ continue;
86
+ }
87
+ if (arg.startsWith("--limit=")) {
88
+ options.limit = parseLimit(arg.slice("--limit=".length));
89
+ continue;
90
+ }
91
+ args.push(arg);
92
+ }
93
+
94
+ return { args, options };
95
+ }
@@ -76,11 +76,20 @@ export async function runPaneScreenshot(args: string[], ctx: CliCommandContext)
76
76
  export async function runPaneCatalog(args: string[], ctx: CliCommandContext) {
77
77
  await runPaneCliCommand(ctx, async () => {
78
78
  const parsed = parsePaneCatalogArgs(args);
79
+ const effectiveParsed = {
80
+ ...parsed,
81
+ limit: ctx.cliOptions.limit ?? parsed.limit,
82
+ };
79
83
  const context = await ctx.initMarketData();
80
84
  const registry = await createPaneCatalog(context, ctx.plugins);
81
85
  try {
82
86
  const entries = await buildPaneCatalogEntries(registry, context);
83
- console.log(renderPaneCatalogReport(filterPaneCatalogEntries(entries, parsed.query), parsed));
87
+ const filtered = filterPaneCatalogEntries(entries, parsed.query);
88
+ if (ctx.cliOptions.format === "text") {
89
+ console.log(renderPaneCatalogReport(filtered, effectiveParsed));
90
+ } else {
91
+ ctx.printResult({ data: filtered.slice(0, effectiveParsed.limit) });
92
+ }
84
93
  } finally {
85
94
  registry.destroy();
86
95
  context.persistence.close();
@@ -18,6 +18,7 @@ const originalHome = process.env.HOME;
18
18
 
19
19
  afterEach(async () => {
20
20
  process.env.HOME = originalHome;
21
+ process.exitCode = 0;
21
22
  await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
22
23
  });
23
24
 
@@ -182,6 +183,45 @@ describe("CLI dispatch", () => {
182
183
  expect(stdout).toContain("ok:hello world");
183
184
  });
184
185
 
186
+ test("renders plugin command failures through structured output", async () => {
187
+ process.env.HOME = await createTempHome("gloomberb-cli-registry-failure-home-");
188
+ const originalExitCode = process.exitCode;
189
+ process.exitCode = undefined;
190
+ try {
191
+ const { result, stderr } = await captureConsole(() => dispatchCli(
192
+ ["fail-example", "--json"],
193
+ {
194
+ externalPlugins: [{
195
+ plugin: {
196
+ id: "failing-cli",
197
+ name: "Failing CLI",
198
+ version: "1.0.0",
199
+ cliCommands: [{
200
+ name: "fail-example",
201
+ description: "Fail on purpose",
202
+ execute: (_args, ctx) => ctx.fail("Synthetic failure.", "Synthetic details."),
203
+ }],
204
+ },
205
+ path: "/tmp/failing-cli",
206
+ }],
207
+ },
208
+ ));
209
+
210
+ expect(result).toEqual({ kind: "handled" });
211
+ expect(process.exitCode).toBe(1);
212
+ expect(JSON.parse(stderr)).toEqual({
213
+ ok: false,
214
+ error: {
215
+ code: "cli_error",
216
+ message: "Synthetic failure.",
217
+ details: "Synthetic details.",
218
+ },
219
+ });
220
+ } finally {
221
+ process.exitCode = originalExitCode ?? 0;
222
+ }
223
+ });
224
+
185
225
  test("routes prediction market commands and aliases through launch-ui requests", async () => {
186
226
  process.env.HOME = await createTempHome("gloomberb-cli-prediction-home-");
187
227