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.
Files changed (143) hide show
  1. package/README.md +27 -16
  2. package/package.json +1 -1
  3. package/src/app.tsx +29 -65
  4. package/src/cli/base-converter.ts +31 -0
  5. package/src/cli/commands/search.ts +11 -3
  6. package/src/cli/commands/ticker.ts +22 -11
  7. package/src/cli/context.ts +27 -32
  8. package/src/cli/helpers.ts +1 -4
  9. package/src/cli/index.ts +140 -123
  10. package/src/cli/registry.test.ts +226 -0
  11. package/src/cli/registry.ts +201 -0
  12. package/src/cli.test.ts +219 -0
  13. package/src/components/chart/chart-controller.ts +309 -0
  14. package/src/components/chart/chart-data.ts +5 -6
  15. package/src/components/chart/chart-pane-settings.ts +27 -0
  16. package/src/components/chart/chart-renderer.ts +7 -2
  17. package/src/components/chart/chart-resolution.test.ts +56 -0
  18. package/src/components/chart/chart-resolution.ts +362 -0
  19. package/src/components/chart/chart-types.ts +12 -4
  20. package/src/components/chart/chart-viewport.test.ts +25 -0
  21. package/src/components/chart/chart-viewport.ts +28 -0
  22. package/src/components/chart/chart.test.ts +26 -0
  23. package/src/components/chart/comparison-chart-data.ts +20 -27
  24. package/src/components/chart/comparison-stock-chart.tsx +374 -99
  25. package/src/components/chart/stock-chart-renderer-switch.test.tsx +124 -0
  26. package/src/components/chart/stock-chart.tsx +484 -112
  27. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +4 -4
  28. package/src/components/command-bar/command-bar.test.tsx +650 -5
  29. package/src/components/command-bar/command-bar.tsx +359 -63
  30. package/src/components/command-bar/command-registry.ts +8 -0
  31. package/src/components/command-bar/helpers.test.ts +23 -1
  32. package/src/components/command-bar/helpers.ts +14 -5
  33. package/src/components/command-bar/pane-template-display.ts +12 -0
  34. package/src/components/command-bar/root-shortcuts.ts +2 -1
  35. package/src/components/command-bar/workflow-ops.ts +4 -2
  36. package/src/components/detail-feed-view.test.tsx +124 -49
  37. package/src/components/detail-feed-view.tsx +396 -160
  38. package/src/components/layout/floating-pane.tsx +1 -1
  39. package/src/components/layout/pane-header.tsx +26 -2
  40. package/src/components/layout/shell.tsx +62 -50
  41. package/src/components/layout/status-bar.test.tsx +66 -0
  42. package/src/components/layout/status-bar.tsx +26 -5
  43. package/src/components/onboarding/onboarding-wizard.test.tsx +65 -0
  44. package/src/components/onboarding/onboarding-wizard.tsx +5 -6
  45. package/src/components/ticker-list-table.test.tsx +83 -0
  46. package/src/components/ticker-list-table.tsx +174 -97
  47. package/src/components/ui/data-table.tsx +27 -2
  48. package/src/components/ui/index.ts +1 -0
  49. package/src/components/ui/page-stack-view.tsx +60 -0
  50. package/src/components/use-double-click-activation.ts +31 -0
  51. package/src/index.tsx +10 -10
  52. package/src/market-data/coordinator.test.ts +92 -4
  53. package/src/market-data/coordinator.ts +32 -5
  54. package/src/market-data/hooks.test.tsx +120 -0
  55. package/src/market-data/hooks.tsx +53 -16
  56. package/src/market-data/request-types.ts +4 -2
  57. package/src/market-data/selectors.ts +24 -58
  58. package/src/plugins/builtin/chat-controller.test.ts +370 -3
  59. package/src/plugins/builtin/chat-controller.ts +310 -35
  60. package/src/plugins/builtin/chat.test.tsx +118 -6
  61. package/src/plugins/builtin/chat.tsx +175 -116
  62. package/src/plugins/builtin/comparison-chart.test.tsx +66 -5
  63. package/src/plugins/builtin/comparison-chart.tsx +21 -1
  64. package/src/plugins/builtin/news.tsx +1 -11
  65. package/src/plugins/builtin/portfolio-list/cli/commands.ts +656 -0
  66. package/src/plugins/builtin/portfolio-list/command-bar.ts +122 -0
  67. package/src/plugins/builtin/portfolio-list/header.tsx +16 -34
  68. package/src/plugins/builtin/portfolio-list/index.tsx +5 -0
  69. package/src/plugins/builtin/portfolio-list/metrics.test.ts +61 -0
  70. package/src/plugins/builtin/portfolio-list/metrics.ts +2 -2
  71. package/src/plugins/builtin/portfolio-list/mutations.test.ts +187 -0
  72. package/src/plugins/builtin/portfolio-list/mutations.ts +226 -0
  73. package/src/plugins/builtin/portfolio-list/pane.tsx +39 -17
  74. package/src/plugins/builtin/portfolio-list/table.tsx +11 -2
  75. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +163 -0
  76. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +81 -0
  77. package/src/plugins/builtin/sec.tsx +1 -11
  78. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +13 -2
  79. package/src/plugins/builtin/ticker-detail/financials-tab.tsx +21 -0
  80. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +45 -10
  81. package/src/plugins/builtin/ticker-detail/pane.tsx +35 -17
  82. package/src/plugins/builtin/ticker-detail/settings.ts +19 -1
  83. package/src/plugins/builtin/ticker-detail.test.tsx +47 -0
  84. package/src/plugins/catalog.ts +65 -0
  85. package/src/plugins/ibkr/broker-adapter.ts +28 -0
  86. package/src/plugins/ibkr/gateway-service.test.ts +75 -1
  87. package/src/plugins/ibkr/gateway-service.ts +179 -9
  88. package/src/plugins/pane-manager.test.ts +1 -1
  89. package/src/plugins/plugin-runtime.tsx +27 -7
  90. package/src/plugins/prediction-markets/chart.test.tsx +139 -0
  91. package/src/plugins/prediction-markets/chart.tsx +65 -436
  92. package/src/plugins/prediction-markets/cli.ts +28 -0
  93. package/src/plugins/prediction-markets/controller-data.ts +17 -5
  94. package/src/plugins/prediction-markets/controller.ts +141 -121
  95. package/src/plugins/prediction-markets/detail/outcomes.tsx +2 -6
  96. package/src/plugins/prediction-markets/detail/pane.tsx +4 -25
  97. package/src/plugins/prediction-markets/detail/shared.tsx +0 -1
  98. package/src/plugins/prediction-markets/detail.test.tsx +4 -0
  99. package/src/plugins/prediction-markets/index.tsx +2 -0
  100. package/src/plugins/prediction-markets/launch.ts +51 -16
  101. package/src/plugins/prediction-markets/metrics.ts +9 -5
  102. package/src/plugins/prediction-markets/pane.test.tsx +160 -58
  103. package/src/plugins/prediction-markets/pane.tsx +74 -183
  104. package/src/plugins/prediction-markets/table.tsx +6 -3
  105. package/src/plugins/prediction-markets/types.ts +0 -1
  106. package/src/sources/gloomberb-cloud.test.ts +29 -0
  107. package/src/sources/gloomberb-cloud.ts +89 -1
  108. package/src/sources/provider-router.test.ts +308 -3
  109. package/src/sources/provider-router.ts +247 -17
  110. package/src/sources/yahoo-finance.test.ts +26 -0
  111. package/src/sources/yahoo-finance.ts +54 -3
  112. package/src/state/app-context-selector.test.tsx +99 -0
  113. package/src/state/app-context.test.ts +18 -0
  114. package/src/state/app-context.tsx +170 -65
  115. package/src/state/selectors.ts +25 -0
  116. package/src/theme/colors.ts +8 -8
  117. package/src/theme/themes.test.ts +2 -0
  118. package/src/theme/themes.ts +22 -10
  119. package/src/types/broker.ts +21 -0
  120. package/src/types/config.ts +17 -1
  121. package/src/types/data-provider.ts +18 -0
  122. package/src/types/financials.ts +31 -1
  123. package/src/types/plugin.ts +70 -0
  124. package/src/utils/api-client.test.ts +57 -0
  125. package/src/utils/api-client.ts +35 -12
  126. package/src/utils/back-navigation.ts +27 -0
  127. package/src/utils/format.test.ts +14 -0
  128. package/src/utils/format.ts +3 -3
  129. package/src/utils/market-status.ts +46 -1
  130. package/src/utils/price-history.test.ts +25 -0
  131. package/src/utils/price-history.ts +29 -5
  132. package/src/utils/quote-freshness.ts +127 -0
  133. package/src/utils/quote-resolution.test.ts +277 -0
  134. package/src/utils/quote-resolution.ts +641 -0
  135. package/src/utils/slugify.ts +3 -0
  136. package/src/utils/timestamp.ts +25 -0
  137. package/src/version.ts +1 -1
  138. package/src/cli/commands/portfolio.ts +0 -210
  139. package/src/cli/commands/watchlist.ts +0 -210
  140. package/src/plugins/builtin/data-providers.ts +0 -16
  141. package/src/sources/exchange-rates.ts +0 -18
  142. package/src/utils/ascii-chart.ts +0 -172
  143. package/src/utils/ticker-tokenizer.test.ts +0 -48
package/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
- cliStyles,
4
- renderSection,
5
- renderTable,
6
- } from "../utils/cli-output";
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 help() {
20
- console.log(`${cliStyles.bold(`gloomberb v${VERSION}`)}\n${cliStyles.muted("Bloomberg-style portfolio tracker for the terminal")}`);
21
- console.log("");
22
- console.log(renderSection("Usage"));
23
- console.log("gloomberb [command]");
24
- console.log("");
25
- console.log(renderSection("Commands"));
26
- console.log(renderTable(
27
- [
28
- { header: "Command" },
29
- { header: "Description" },
30
- ],
31
- [
32
- ["(no command)", "Launch the terminal UI"],
33
- ["predictions [...]", "Launch the UI with Prediction Markets focused"],
34
- ["help", "Show this help message"],
35
- ["portfolio [name]", "List collections or show a portfolio/watchlist"],
36
- ["watchlist [action]", "List, create, delete, add, or remove watchlists"],
37
- ["search <query>", "Search tickers and company names"],
38
- ["ticker <symbol>", "Show quote, ownership, and detailed financials"],
39
- ["install <user/repo>", "Install a plugin from GitHub"],
40
- ["remove <name>", "Remove an installed plugin"],
41
- ["update [name]", "Update plugins"],
42
- ["plugins", "List installed plugins"],
43
- ],
44
- ));
45
- console.log("");
46
- console.log(renderSection("Prediction Launch"));
47
- console.log(renderTable(
48
- [
49
- { header: "Input" },
50
- { header: "Example" },
51
- ],
52
- [
53
- ["gloomberb predictions [venue] [category] [browse-tab] [search...]", "gloomberb predictions world"],
54
- ["venue", "all | polymarket | kalshi"],
55
- ["category", "all | politics | world | macro | crypto | science | sports | entertainment | climate | social"],
56
- ["browse-tab", "top | ending | new | watchlist"],
57
- ],
58
- ));
59
- console.log("");
60
- console.log(renderSection("Watchlist Actions"));
61
- console.log(renderTable(
62
- [
63
- { header: "Action" },
64
- { header: "Example" },
65
- ],
66
- [
67
- ["list", "gloomberb watchlist list"],
68
- ["show", "gloomberb watchlist show Growth"],
69
- ["create", "gloomberb watchlist create Growth"],
70
- ["delete", "gloomberb watchlist delete Growth"],
71
- ["add", "gloomberb watchlist add Growth NVDA"],
72
- ["remove", "gloomberb watchlist remove Growth NVDA"],
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 runCli(args: string[]): Promise<boolean> {
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
- switch (command) {
83
- case "help":
84
- case "--help":
85
- case "-h": {
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
+ });