gloomberb 0.9.2 → 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.
- package/README.md +23 -2
- package/bin/gloomberb +1 -1
- package/package.json +1 -1
- package/src/app.tsx +10 -7
- package/src/capabilities/registry.ts +1 -0
- package/src/capabilities/types.ts +23 -0
- package/src/cli/commands/api.ts +119 -0
- package/src/cli/commands/automation.ts +173 -0
- package/src/cli/commands/command-utils.ts +53 -0
- package/src/cli/commands/market.ts +527 -0
- package/src/cli/commands/overview.ts +222 -0
- package/src/cli/commands/search.ts +39 -0
- package/src/cli/commands/system.ts +553 -0
- package/src/cli/commands/ticker.ts +104 -12
- package/src/cli/context.ts +35 -2
- package/src/cli/entry.ts +51 -0
- package/src/cli/errors.ts +56 -5
- package/src/cli/index.ts +73 -11
- package/src/cli/options.test.ts +38 -0
- package/src/cli/options.ts +95 -0
- package/src/cli/pane-functions/index.ts +10 -1
- package/src/cli/registry.test.ts +40 -0
- package/src/cli/registry.ts +46 -5
- package/src/cli/result.test.ts +82 -0
- package/src/cli/result.ts +149 -0
- package/src/cli/types.ts +11 -0
- package/src/cli.test.ts +11 -11
- package/src/components/chart/comparison-stock-chart/interactions.ts +7 -1
- package/src/components/chart/comparison-stock-chart/legend.tsx +4 -0
- package/src/components/chart/comparison-stock-chart/render-data.ts +37 -8
- package/src/components/chart/comparison-stock-chart/selection-runtime.ts +19 -2
- package/src/components/chart/comparison-stock-chart/sources.ts +8 -4
- package/src/components/chart/comparison-stock-chart/toolbar.tsx +13 -3
- package/src/components/chart/comparison-stock-chart/view.tsx +12 -2
- package/src/components/chart/core/pane-settings.test.ts +27 -0
- package/src/components/chart/core/pane-settings.ts +60 -1
- package/src/components/chart/core/pointer.ts +2 -0
- package/src/components/chart/stock-chart/resolved/runtime.ts +1 -1
- package/src/components/chart/stock-chart/settings.ts +7 -3
- package/src/components/chart/stock-chart/viewport/runtime.ts +5 -5
- package/src/components/command-bar/pane-templates/workflow-route.ts +10 -0
- package/src/components/command-bar/surface/pane.test.tsx +76 -0
- package/src/components/data-table/view.tsx +13 -2
- package/src/components/feed-data-table/stack-view.tsx +9 -1
- package/src/components/index.ts +1 -1
- package/src/components/input-search-bar.test.tsx +103 -0
- package/src/components/input-search-bar.tsx +16 -0
- package/src/components/layout/shell/active-drag.ts +5 -1
- package/src/components/layout/shell/drag/runtime.ts +21 -3
- package/src/components/layout/shell/fullscreen.ts +10 -0
- package/src/components/layout/shell/index.test.tsx +395 -2
- package/src/components/layout/shell/index.tsx +128 -2
- package/src/components/layout/shell/native/pointer-runtime.ts +10 -4
- package/src/components/layout/shell/pane/layers.tsx +26 -14
- package/src/components/layout/shell/pane/management-shortcuts.ts +19 -0
- package/src/components/layout/shell/shortcuts.ts +4 -0
- package/src/components/layout/shell/terminal-pointer-runtime.ts +29 -19
- package/src/components/layout/shell/window-mode/index.ts +11 -3
- package/src/components/layout/shell/window-mode/overlays.tsx +8 -1
- package/src/components/layout/status-bar.test.tsx +105 -0
- package/src/components/layout/status-bar.tsx +32 -6
- package/src/components/layout/transient-layout.tsx +57 -0
- package/src/components/layout/window-edit/mode.test.ts +76 -5
- package/src/components/layout/window-edit/mode.ts +47 -2
- package/src/components/layout/window-edit/presentation.ts +2 -1
- package/src/components/price-performance.tsx +9 -1
- package/src/core/app-services.ts +7 -4
- package/src/data/config/store/index.ts +1 -2
- package/src/plugins/builtin/cloud-tweets/pane.tsx +2 -0
- package/src/plugins/builtin/cloud-tweets/search-bar.tsx +3 -0
- package/src/plugins/builtin/cloud-tweets/table.tsx +14 -2
- package/src/plugins/builtin/comparison-chart/index.test.tsx +130 -6
- package/src/plugins/builtin/news/wire/read-state.ts +9 -32
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +23 -0
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +19 -2
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +1 -1
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -7
- package/src/plugins/builtin/shared/read-state.ts +43 -0
- package/src/plugins/builtin/substack/article-detail.test.tsx +10 -0
- package/src/plugins/builtin/substack/article-detail.tsx +39 -15
- package/src/plugins/builtin/substack/article-stack.tsx +6 -2
- package/src/plugins/builtin/substack/feed-tabs.tsx +12 -9
- package/src/plugins/builtin/substack/pane.tsx +8 -3
- package/src/plugins/builtin/substack/read-state.ts +55 -0
- package/src/plugins/builtin/thirteenf/pane.test.tsx +26 -0
- package/src/plugins/builtin/thirteenf/pane.tsx +12 -1
- package/src/plugins/pane-manager/docking.ts +9 -2
- package/src/plugins/pane-manager/queries.ts +8 -2
- package/src/plugins/prediction-markets/controller/index.ts +2 -0
- package/src/plugins/prediction-markets/controller/keyboard.ts +34 -5
- package/src/plugins/prediction-markets/pane.test.tsx +32 -0
- package/src/plugins/prediction-markets/pane.tsx +21 -1
- package/src/plugins/prediction-markets/test-helpers.tsx +1 -0
- package/src/renderers/electrobun/bun/application-menu/click.test.ts +9 -0
- package/src/renderers/electrobun/bun/application-menu/click.ts +1 -0
- package/src/renderers/electrobun/bun/application-menu/index.test.ts +9 -0
- package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
- package/src/renderers/electrobun/bun/index.ts +13 -0
- package/src/renderers/electrobun/bun/window/focus.test.ts +35 -0
- package/src/renderers/electrobun/bun/window/focus.ts +5 -0
- package/src/renderers/electrobun/view/app-services.ts +6 -4
- package/src/renderers/electrobun/view/host/ascii-text.ts +50 -0
- package/src/renderers/electrobun/view/host/text.test.ts +22 -0
- package/src/renderers/electrobun/view/host/text.tsx +24 -20
- package/src/renderers/electrobun/view/ui-host.tsx +1 -1
- package/src/renderers/opentui/start.tsx +13 -5
- package/src/state/app/input-capture.ts +12 -0
- package/src/types/plugin.ts +28 -0
- package/src/utils/cli-output.ts +6 -0
- package/src/utils/search-focus-navigation.ts +24 -0
- 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 (
|
|
383
|
-
closeAndFailCommand(
|
|
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
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
}
|
package/src/cli/context.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
}
|
|
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
|
+
}
|
package/src/cli/entry.ts
ADDED
|
@@ -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
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
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();
|
package/src/cli/registry.test.ts
CHANGED
|
@@ -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
|
|