gloomberb 0.3.3 → 0.4.0
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 +27 -16
- package/package.json +1 -1
- package/src/app.tsx +29 -65
- package/src/cli/base-converter.ts +31 -0
- package/src/cli/commands/search.ts +11 -3
- package/src/cli/commands/ticker.ts +22 -11
- package/src/cli/context.ts +27 -32
- package/src/cli/helpers.ts +1 -4
- package/src/cli/index.ts +140 -123
- package/src/cli/registry.test.ts +226 -0
- package/src/cli/registry.ts +201 -0
- package/src/cli.test.ts +219 -0
- package/src/components/chart/chart-controller.ts +309 -0
- package/src/components/chart/chart-data.ts +5 -6
- package/src/components/chart/chart-pane-settings.ts +27 -0
- package/src/components/chart/chart-renderer.ts +7 -2
- package/src/components/chart/chart-resolution.test.ts +56 -0
- package/src/components/chart/chart-resolution.ts +362 -0
- package/src/components/chart/chart-types.ts +12 -4
- package/src/components/chart/chart-viewport.test.ts +25 -0
- package/src/components/chart/chart-viewport.ts +28 -0
- package/src/components/chart/chart.test.ts +26 -0
- package/src/components/chart/comparison-chart-data.ts +20 -27
- package/src/components/chart/comparison-stock-chart.tsx +374 -99
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +124 -0
- package/src/components/chart/stock-chart.tsx +484 -112
- package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +4 -4
- package/src/components/command-bar/command-bar.test.tsx +650 -5
- package/src/components/command-bar/command-bar.tsx +359 -63
- package/src/components/command-bar/command-registry.ts +8 -0
- package/src/components/command-bar/helpers.test.ts +23 -1
- package/src/components/command-bar/helpers.ts +14 -5
- package/src/components/command-bar/pane-template-display.ts +12 -0
- package/src/components/command-bar/root-shortcuts.ts +2 -1
- package/src/components/command-bar/workflow-ops.ts +4 -2
- package/src/components/detail-feed-view.test.tsx +124 -49
- package/src/components/detail-feed-view.tsx +396 -160
- package/src/components/layout/floating-pane.tsx +1 -1
- package/src/components/layout/pane-header.tsx +26 -2
- package/src/components/layout/shell.tsx +62 -50
- package/src/components/layout/status-bar.test.tsx +66 -0
- package/src/components/layout/status-bar.tsx +26 -5
- package/src/components/onboarding/onboarding-wizard.test.tsx +65 -0
- package/src/components/onboarding/onboarding-wizard.tsx +5 -6
- package/src/components/ticker-list-table.test.tsx +83 -0
- package/src/components/ticker-list-table.tsx +174 -97
- package/src/components/ui/data-table.tsx +27 -2
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/page-stack-view.tsx +60 -0
- package/src/components/use-double-click-activation.ts +31 -0
- package/src/index.tsx +10 -10
- package/src/market-data/coordinator.test.ts +92 -4
- package/src/market-data/coordinator.ts +32 -5
- package/src/market-data/hooks.test.tsx +120 -0
- package/src/market-data/hooks.tsx +53 -16
- package/src/market-data/request-types.ts +4 -2
- package/src/market-data/selectors.ts +24 -58
- package/src/plugins/builtin/chat-controller.test.ts +370 -3
- package/src/plugins/builtin/chat-controller.ts +310 -35
- package/src/plugins/builtin/chat.test.tsx +118 -6
- package/src/plugins/builtin/chat.tsx +175 -116
- package/src/plugins/builtin/comparison-chart.test.tsx +66 -5
- package/src/plugins/builtin/comparison-chart.tsx +21 -1
- package/src/plugins/builtin/news.tsx +1 -11
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +656 -0
- package/src/plugins/builtin/portfolio-list/command-bar.ts +122 -0
- package/src/plugins/builtin/portfolio-list/header.tsx +16 -34
- package/src/plugins/builtin/portfolio-list/index.tsx +5 -0
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +61 -0
- package/src/plugins/builtin/portfolio-list/metrics.ts +2 -2
- package/src/plugins/builtin/portfolio-list/mutations.test.ts +187 -0
- package/src/plugins/builtin/portfolio-list/mutations.ts +226 -0
- package/src/plugins/builtin/portfolio-list/pane.tsx +39 -17
- package/src/plugins/builtin/portfolio-list/table.tsx +11 -2
- package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +163 -0
- package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +81 -0
- package/src/plugins/builtin/sec.tsx +1 -11
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +13 -2
- package/src/plugins/builtin/ticker-detail/financials-tab.tsx +21 -0
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +45 -10
- package/src/plugins/builtin/ticker-detail/pane.tsx +35 -17
- package/src/plugins/builtin/ticker-detail/settings.ts +19 -1
- package/src/plugins/builtin/ticker-detail.test.tsx +47 -0
- package/src/plugins/catalog.ts +65 -0
- package/src/plugins/ibkr/broker-adapter.ts +28 -0
- package/src/plugins/ibkr/gateway-service.test.ts +75 -1
- package/src/plugins/ibkr/gateway-service.ts +179 -9
- package/src/plugins/pane-manager.test.ts +1 -1
- package/src/plugins/plugin-runtime.tsx +27 -7
- package/src/plugins/prediction-markets/chart.test.tsx +139 -0
- package/src/plugins/prediction-markets/chart.tsx +65 -436
- package/src/plugins/prediction-markets/cli.ts +28 -0
- package/src/plugins/prediction-markets/controller-data.ts +17 -5
- package/src/plugins/prediction-markets/controller.ts +141 -121
- package/src/plugins/prediction-markets/detail/outcomes.tsx +2 -6
- package/src/plugins/prediction-markets/detail/pane.tsx +4 -25
- package/src/plugins/prediction-markets/detail/shared.tsx +0 -1
- package/src/plugins/prediction-markets/detail.test.tsx +4 -0
- package/src/plugins/prediction-markets/index.tsx +2 -0
- package/src/plugins/prediction-markets/launch.ts +51 -16
- package/src/plugins/prediction-markets/metrics.ts +9 -5
- package/src/plugins/prediction-markets/pane.test.tsx +160 -58
- package/src/plugins/prediction-markets/pane.tsx +74 -183
- package/src/plugins/prediction-markets/table.tsx +6 -3
- package/src/plugins/prediction-markets/types.ts +0 -1
- package/src/sources/gloomberb-cloud.test.ts +29 -0
- package/src/sources/gloomberb-cloud.ts +89 -1
- package/src/sources/provider-router.test.ts +308 -3
- package/src/sources/provider-router.ts +247 -17
- package/src/sources/yahoo-finance.test.ts +26 -0
- package/src/sources/yahoo-finance.ts +54 -3
- package/src/state/app-context-selector.test.tsx +99 -0
- package/src/state/app-context.test.ts +18 -0
- package/src/state/app-context.tsx +170 -65
- package/src/state/selectors.ts +25 -0
- package/src/theme/colors.ts +8 -8
- package/src/theme/themes.test.ts +2 -0
- package/src/theme/themes.ts +22 -10
- package/src/types/broker.ts +21 -0
- package/src/types/config.ts +17 -1
- package/src/types/data-provider.ts +18 -0
- package/src/types/financials.ts +31 -1
- package/src/types/plugin.ts +70 -0
- package/src/utils/api-client.test.ts +57 -0
- package/src/utils/api-client.ts +35 -12
- package/src/utils/back-navigation.ts +27 -0
- package/src/utils/format.test.ts +14 -0
- package/src/utils/format.ts +3 -3
- package/src/utils/market-status.ts +46 -1
- package/src/utils/price-history.test.ts +25 -0
- package/src/utils/price-history.ts +29 -5
- package/src/utils/quote-freshness.ts +127 -0
- package/src/utils/quote-resolution.test.ts +277 -0
- package/src/utils/quote-resolution.ts +641 -0
- package/src/utils/slugify.ts +3 -0
- package/src/utils/timestamp.ts +25 -0
- package/src/version.ts +1 -1
- package/src/cli/commands/portfolio.ts +0 -210
- package/src/cli/commands/watchlist.ts +0 -210
- package/src/plugins/builtin/data-providers.ts +0 -16
- package/src/sources/exchange-rates.ts +0 -18
- package/src/utils/ascii-chart.ts +0 -172
- package/src/utils/ticker-tokenizer.test.ts +0 -48
package/src/cli/index.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { VERSION } from "../version";
|
|
2
|
+
import type { CliCommandDef, CliDispatchResult } from "../types/plugin";
|
|
3
|
+
import type { LoadedExternalPlugin } from "../plugins/loader";
|
|
4
|
+
import { loadCliConfigIfAvailable } from "./context";
|
|
2
5
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
buildCliCommandRegistry,
|
|
7
|
+
createCliCommandContext,
|
|
8
|
+
normalizeCliCommandToken,
|
|
9
|
+
normalizeCliDispatchResult,
|
|
10
|
+
renderCliHelp,
|
|
11
|
+
type CliCommandRegistry,
|
|
12
|
+
} from "./registry";
|
|
7
13
|
import { fail } from "./errors";
|
|
8
|
-
import { portfolio } from "./commands/portfolio";
|
|
9
|
-
import { watchlist } from "./commands/watchlist";
|
|
10
14
|
import { search, searchCandidatesForCli, buildSearchReport } from "./commands/search";
|
|
11
15
|
import { ticker, buildTickerReport } from "./commands/ticker";
|
|
12
16
|
import {
|
|
@@ -16,128 +20,141 @@ import {
|
|
|
16
20
|
updatePlugins,
|
|
17
21
|
} from "./commands/plugins";
|
|
18
22
|
|
|
19
|
-
function
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
23
|
+
function createCoreCliCommands(renderHelp: () => string): CliCommandDef[] {
|
|
24
|
+
return [
|
|
25
|
+
{
|
|
26
|
+
name: "help",
|
|
27
|
+
aliases: ["--help", "-h"],
|
|
28
|
+
description: "Show this help message",
|
|
29
|
+
help: {
|
|
30
|
+
usage: ["help"],
|
|
31
|
+
},
|
|
32
|
+
execute: () => {
|
|
33
|
+
console.log(renderHelp());
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: "search",
|
|
38
|
+
description: "Search tickers and company names",
|
|
39
|
+
help: {
|
|
40
|
+
usage: ["search <query>"],
|
|
41
|
+
},
|
|
42
|
+
execute: async (args, ctx) => {
|
|
43
|
+
const query = args.join(" ");
|
|
44
|
+
await search(query, {
|
|
45
|
+
initMarketData: ctx.initMarketData,
|
|
46
|
+
fail: ctx.fail,
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "ticker",
|
|
52
|
+
description: "Show quote, ownership, and detailed financials",
|
|
53
|
+
help: {
|
|
54
|
+
usage: ["ticker <symbol>"],
|
|
55
|
+
},
|
|
56
|
+
execute: async (args, ctx) => {
|
|
57
|
+
const symbol = args[0];
|
|
58
|
+
if (!symbol) {
|
|
59
|
+
ctx.fail("Usage: gloomberb ticker <symbol>");
|
|
60
|
+
}
|
|
61
|
+
await ticker(symbol!, {
|
|
62
|
+
initMarketData: ctx.initMarketData,
|
|
63
|
+
closeAndFail: ctx.closeAndFail,
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "install",
|
|
69
|
+
description: "Install a plugin from GitHub",
|
|
70
|
+
help: {
|
|
71
|
+
usage: ["install <user/repo>"],
|
|
72
|
+
},
|
|
73
|
+
execute: async (args) => {
|
|
74
|
+
const ref = args[0];
|
|
75
|
+
if (!ref) {
|
|
76
|
+
fail("Usage: gloomberb install <github-user/repo>");
|
|
77
|
+
}
|
|
78
|
+
await installPlugin(ref);
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "remove",
|
|
83
|
+
aliases: ["uninstall"],
|
|
84
|
+
description: "Remove an installed plugin",
|
|
85
|
+
help: {
|
|
86
|
+
usage: ["remove <name>"],
|
|
87
|
+
},
|
|
88
|
+
execute: async (args) => {
|
|
89
|
+
const name = args[0];
|
|
90
|
+
if (!name) {
|
|
91
|
+
fail("Usage: gloomberb remove <plugin-name>");
|
|
92
|
+
}
|
|
93
|
+
await removePlugin(name);
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: "update",
|
|
98
|
+
description: "Update plugins",
|
|
99
|
+
help: {
|
|
100
|
+
usage: ["update [name]"],
|
|
101
|
+
},
|
|
102
|
+
execute: async (args) => {
|
|
103
|
+
await updatePlugins(args[0]);
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: "plugins",
|
|
108
|
+
aliases: ["list"],
|
|
109
|
+
description: "List installed plugins",
|
|
110
|
+
help: {
|
|
111
|
+
usage: ["plugins"],
|
|
112
|
+
},
|
|
113
|
+
execute: () => {
|
|
114
|
+
listPlugins();
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface DispatchCliOptions {
|
|
121
|
+
externalPlugins?: LoadedExternalPlugin[];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function createRegistry(options: DispatchCliOptions = {}): Promise<CliCommandRegistry> {
|
|
125
|
+
const config = await loadCliConfigIfAvailable();
|
|
126
|
+
let registry: CliCommandRegistry | null = null;
|
|
127
|
+
const coreCommands = createCoreCliCommands(() => renderCliHelp(registry!, VERSION));
|
|
128
|
+
registry = buildCliCommandRegistry({
|
|
129
|
+
coreCommands,
|
|
130
|
+
externalPlugins: options.externalPlugins ?? [],
|
|
131
|
+
config,
|
|
132
|
+
});
|
|
133
|
+
return registry;
|
|
75
134
|
}
|
|
76
135
|
|
|
77
136
|
export { buildSearchReport, buildTickerReport, searchCandidatesForCli };
|
|
78
137
|
|
|
79
|
-
export async function
|
|
138
|
+
export async function dispatchCli(args: string[], options: DispatchCliOptions = {}): Promise<CliDispatchResult> {
|
|
80
139
|
const command = args[0];
|
|
140
|
+
if (!command) {
|
|
141
|
+
return { kind: "unhandled" };
|
|
142
|
+
}
|
|
81
143
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
help();
|
|
87
|
-
return true;
|
|
88
|
-
}
|
|
89
|
-
case "portfolio": {
|
|
90
|
-
await portfolio(args.slice(1).join(" ") || undefined);
|
|
91
|
-
return true;
|
|
92
|
-
}
|
|
93
|
-
case "watchlist":
|
|
94
|
-
case "watchlists": {
|
|
95
|
-
await watchlist(args.slice(1));
|
|
96
|
-
return true;
|
|
97
|
-
}
|
|
98
|
-
case "search": {
|
|
99
|
-
const query = args.slice(1).join(" ");
|
|
100
|
-
if (!query) {
|
|
101
|
-
fail("Usage: gloomberb search <query>");
|
|
102
|
-
}
|
|
103
|
-
await search(query);
|
|
104
|
-
return true;
|
|
105
|
-
}
|
|
106
|
-
case "ticker": {
|
|
107
|
-
const symbol = args[1];
|
|
108
|
-
if (!symbol) {
|
|
109
|
-
fail("Usage: gloomberb ticker <symbol>");
|
|
110
|
-
}
|
|
111
|
-
await ticker(symbol);
|
|
112
|
-
return true;
|
|
113
|
-
}
|
|
114
|
-
case "install": {
|
|
115
|
-
const ref = args[1];
|
|
116
|
-
if (!ref) {
|
|
117
|
-
fail("Usage: gloomberb install <github-user/repo>");
|
|
118
|
-
}
|
|
119
|
-
await installPlugin(ref);
|
|
120
|
-
return true;
|
|
121
|
-
}
|
|
122
|
-
case "remove":
|
|
123
|
-
case "uninstall": {
|
|
124
|
-
const name = args[1];
|
|
125
|
-
if (!name) {
|
|
126
|
-
fail("Usage: gloomberb remove <plugin-name>");
|
|
127
|
-
}
|
|
128
|
-
await removePlugin(name);
|
|
129
|
-
return true;
|
|
130
|
-
}
|
|
131
|
-
case "update": {
|
|
132
|
-
await updatePlugins(args[1]);
|
|
133
|
-
return true;
|
|
134
|
-
}
|
|
135
|
-
case "plugins":
|
|
136
|
-
case "list": {
|
|
137
|
-
listPlugins();
|
|
138
|
-
return true;
|
|
139
|
-
}
|
|
140
|
-
default:
|
|
141
|
-
return false;
|
|
144
|
+
const registry = await createRegistry(options);
|
|
145
|
+
const resolved = registry.lookup.get(normalizeCliCommandToken(command));
|
|
146
|
+
if (!resolved) {
|
|
147
|
+
return { kind: "unhandled" };
|
|
142
148
|
}
|
|
149
|
+
|
|
150
|
+
const result = await resolved.command.execute(
|
|
151
|
+
args.slice(1),
|
|
152
|
+
createCliCommandContext(resolved.ownerId, registry.plugins),
|
|
153
|
+
);
|
|
154
|
+
return normalizeCliDispatchResult(result);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function runCli(args: string[], options: DispatchCliOptions = {}): Promise<boolean> {
|
|
158
|
+
const result = await dispatchCli(args, options);
|
|
159
|
+
return result.kind === "handled";
|
|
143
160
|
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtemp, rm } from "fs/promises";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { tmpdir } from "os";
|
|
5
|
+
import { createDefaultConfig } from "../types/config";
|
|
6
|
+
import type { GloomPlugin } from "../types/plugin";
|
|
7
|
+
import type { LoadedExternalPlugin } from "../plugins/loader";
|
|
8
|
+
import {
|
|
9
|
+
buildCliCommandRegistry,
|
|
10
|
+
createCliCommandContext,
|
|
11
|
+
normalizeCliCommandToken,
|
|
12
|
+
renderCliHelp,
|
|
13
|
+
} from "./registry";
|
|
14
|
+
import { dispatchCli } from "./index";
|
|
15
|
+
import { applyPredictionLaunchIntentToConfig, applyPredictionLaunchIntentToSessionSnapshot, parsePredictionCommandArgs } from "../plugins/prediction-markets/launch";
|
|
16
|
+
|
|
17
|
+
const tempDirs: string[] = [];
|
|
18
|
+
const originalHome = process.env.HOME;
|
|
19
|
+
|
|
20
|
+
afterEach(async () => {
|
|
21
|
+
process.env.HOME = originalHome;
|
|
22
|
+
await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
async function createTempHome(prefix: string): Promise<string> {
|
|
26
|
+
const dir = await mkdtemp(join(tmpdir(), prefix));
|
|
27
|
+
tempDirs.push(dir);
|
|
28
|
+
return dir;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function captureConsole<T>(fn: () => Promise<T> | T): Promise<{ result: T; stdout: string; stderr: string }> {
|
|
32
|
+
const logs: string[] = [];
|
|
33
|
+
const errors: string[] = [];
|
|
34
|
+
const originalLog = console.log;
|
|
35
|
+
const originalError = console.error;
|
|
36
|
+
|
|
37
|
+
console.log = (...args: unknown[]) => {
|
|
38
|
+
logs.push(args.map(String).join(" "));
|
|
39
|
+
};
|
|
40
|
+
console.error = (...args: unknown[]) => {
|
|
41
|
+
errors.push(args.map(String).join(" "));
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const result = await fn();
|
|
46
|
+
return {
|
|
47
|
+
result,
|
|
48
|
+
stdout: logs.join("\n"),
|
|
49
|
+
stderr: errors.join("\n"),
|
|
50
|
+
};
|
|
51
|
+
} finally {
|
|
52
|
+
console.log = originalLog;
|
|
53
|
+
console.error = originalError;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function createSyntheticPlugin(commandName = "example"): GloomPlugin {
|
|
58
|
+
return {
|
|
59
|
+
id: "synthetic-cli",
|
|
60
|
+
name: "Synthetic CLI",
|
|
61
|
+
version: "1.0.0",
|
|
62
|
+
cliCommands: [{
|
|
63
|
+
name: commandName,
|
|
64
|
+
aliases: ["alias-example"],
|
|
65
|
+
description: "Synthetic plugin command",
|
|
66
|
+
help: {
|
|
67
|
+
usage: [`${commandName} [value]`],
|
|
68
|
+
sections: [{
|
|
69
|
+
title: "Synthetic Help",
|
|
70
|
+
lines: ["Hello from the synthetic plugin."],
|
|
71
|
+
}],
|
|
72
|
+
},
|
|
73
|
+
execute: async (args, ctx) => {
|
|
74
|
+
console.log(`${ctx.log ? "ok" : "missing"}:${args.join(" ")}`);
|
|
75
|
+
},
|
|
76
|
+
}],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
describe("CLI registry", () => {
|
|
81
|
+
test("indexes plugin commands and aliases from the registry", () => {
|
|
82
|
+
const registry = buildCliCommandRegistry({
|
|
83
|
+
coreCommands: [],
|
|
84
|
+
externalPlugins: [{
|
|
85
|
+
plugin: createSyntheticPlugin(),
|
|
86
|
+
path: "/tmp/synthetic-cli",
|
|
87
|
+
}],
|
|
88
|
+
config: null,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(registry.lookup.get(normalizeCliCommandToken("example"))?.ownerId).toBe("synthetic-cli");
|
|
92
|
+
expect(registry.lookup.get(normalizeCliCommandToken("alias-example"))?.ownerId).toBe("synthetic-cli");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("rejects duplicate command names and aliases", () => {
|
|
96
|
+
expect(() => buildCliCommandRegistry({
|
|
97
|
+
coreCommands: [{
|
|
98
|
+
name: "duplicate",
|
|
99
|
+
description: "Core command",
|
|
100
|
+
execute: async () => {},
|
|
101
|
+
}],
|
|
102
|
+
externalPlugins: [{
|
|
103
|
+
plugin: {
|
|
104
|
+
id: "dup-plugin",
|
|
105
|
+
name: "Duplicate",
|
|
106
|
+
version: "1.0.0",
|
|
107
|
+
cliCommands: [{
|
|
108
|
+
name: "other",
|
|
109
|
+
aliases: ["duplicate"],
|
|
110
|
+
description: "Plugin command",
|
|
111
|
+
execute: async () => {},
|
|
112
|
+
}],
|
|
113
|
+
},
|
|
114
|
+
path: "/tmp/dup-plugin",
|
|
115
|
+
}],
|
|
116
|
+
config: null,
|
|
117
|
+
})).toThrow(/duplicate/i);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("omits disabled plugin commands from help and dispatch lookup", () => {
|
|
121
|
+
const config = createDefaultConfig("/tmp/gloomberb-cli-disabled");
|
|
122
|
+
config.disabledPlugins = ["synthetic-cli"];
|
|
123
|
+
|
|
124
|
+
const registry = buildCliCommandRegistry({
|
|
125
|
+
coreCommands: [],
|
|
126
|
+
externalPlugins: [{
|
|
127
|
+
plugin: createSyntheticPlugin(),
|
|
128
|
+
path: "/tmp/synthetic-cli",
|
|
129
|
+
}],
|
|
130
|
+
config,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
expect(registry.lookup.get("example")).toBeUndefined();
|
|
134
|
+
expect(renderCliHelp(registry, "0.0.0")).not.toContain("Synthetic plugin command");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("ignores broken external plugins while keeping other commands available", () => {
|
|
138
|
+
const registry = buildCliCommandRegistry({
|
|
139
|
+
coreCommands: [{
|
|
140
|
+
name: "core-only",
|
|
141
|
+
description: "Core command",
|
|
142
|
+
execute: async () => {},
|
|
143
|
+
}],
|
|
144
|
+
externalPlugins: [{
|
|
145
|
+
plugin: {
|
|
146
|
+
id: "broken",
|
|
147
|
+
name: "Broken Plugin",
|
|
148
|
+
version: "0.0.0",
|
|
149
|
+
},
|
|
150
|
+
path: "/tmp/broken",
|
|
151
|
+
error: "Failed to load plugin",
|
|
152
|
+
}],
|
|
153
|
+
config: null,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
expect(registry.lookup.get("core-only")?.ownerId).toBe("core");
|
|
157
|
+
expect(registry.lookup.get("broken")).toBeUndefined();
|
|
158
|
+
expect(registry.commands.some((entry) => entry.ownerId === "core")).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("creates plugin-scoped command contexts", async () => {
|
|
162
|
+
const context = createCliCommandContext("synthetic-cli", [createSyntheticPlugin()]);
|
|
163
|
+
expect(context.log).toBeDefined();
|
|
164
|
+
expect(context.output.renderSection("Test")).toContain("Test");
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe("CLI dispatch", () => {
|
|
169
|
+
test("dispatches a synthetic plugin command without main CLI changes", async () => {
|
|
170
|
+
process.env.HOME = await createTempHome("gloomberb-cli-registry-home-");
|
|
171
|
+
|
|
172
|
+
const { result, stdout } = await captureConsole(() => dispatchCli(
|
|
173
|
+
["example", "hello", "world"],
|
|
174
|
+
{
|
|
175
|
+
externalPlugins: [{
|
|
176
|
+
plugin: createSyntheticPlugin(),
|
|
177
|
+
path: "/tmp/synthetic-cli",
|
|
178
|
+
}],
|
|
179
|
+
},
|
|
180
|
+
));
|
|
181
|
+
|
|
182
|
+
expect(result).toEqual({ kind: "handled" });
|
|
183
|
+
expect(stdout).toContain("ok:hello world");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("routes prediction market commands and aliases through launch-ui requests", async () => {
|
|
187
|
+
process.env.HOME = await createTempHome("gloomberb-cli-prediction-home-");
|
|
188
|
+
|
|
189
|
+
for (const rootCommand of ["predictions", "prediction-markets", "pm"]) {
|
|
190
|
+
const dispatchResult = await dispatchCli([rootCommand, "world"]);
|
|
191
|
+
expect(dispatchResult.kind).toBe("launch-ui");
|
|
192
|
+
if (dispatchResult.kind !== "launch-ui") continue;
|
|
193
|
+
|
|
194
|
+
const config = createDefaultConfig("/tmp/gloomberb-cli-prediction");
|
|
195
|
+
const intent = parsePredictionCommandArgs(["world"]);
|
|
196
|
+
const expectedConfigResult = applyPredictionLaunchIntentToConfig(config, intent, {
|
|
197
|
+
width: 132,
|
|
198
|
+
height: 40,
|
|
199
|
+
});
|
|
200
|
+
const actualConfigResult = dispatchResult.request.applyConfig(config, {
|
|
201
|
+
terminalWidth: 132,
|
|
202
|
+
terminalHeight: 40,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
expect(actualConfigResult.config).toEqual(expectedConfigResult.config);
|
|
206
|
+
expect(actualConfigResult.launchState).toEqual({
|
|
207
|
+
paneInstanceId: expectedConfigResult.paneInstanceId,
|
|
208
|
+
intent,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const expectedSession = applyPredictionLaunchIntentToSessionSnapshot(
|
|
212
|
+
actualConfigResult.config,
|
|
213
|
+
null,
|
|
214
|
+
expectedConfigResult.paneInstanceId,
|
|
215
|
+
intent,
|
|
216
|
+
);
|
|
217
|
+
const actualSession = dispatchResult.request.applySessionSnapshot?.(
|
|
218
|
+
actualConfigResult.config,
|
|
219
|
+
null,
|
|
220
|
+
actualConfigResult.launchState,
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
expect(actualSession).toEqual(expectedSession);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
});
|