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.
- 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/helpers.ts +0 -32
- package/src/components/chart/comparison-stock-chart/interactions.ts +7 -1
- package/src/components/chart/comparison-stock-chart/layout-metrics.ts +6 -6
- package/src/components/chart/comparison-stock-chart/legend.tsx +45 -44
- 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 +56 -6
- 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/detached-pane-shell.tsx +5 -2
- package/src/components/layout/header.tsx +13 -5
- 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-controls.tsx +19 -10
- 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 +183 -0
- package/src/core/app-services.ts +7 -4
- package/src/data/config/store/index.ts +1 -2
- package/src/market-data/performance.test.ts +54 -0
- package/src/market-data/performance.ts +109 -0
- 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 +218 -15
- package/src/plugins/builtin/congress-trades/detail.tsx +204 -68
- package/src/plugins/builtin/congress-trades/footer.ts +14 -8
- package/src/plugins/builtin/congress-trades/keyboard.ts +4 -23
- package/src/plugins/builtin/congress-trades/model.ts +29 -0
- package/src/plugins/builtin/congress-trades/pane.tsx +8 -13
- package/src/plugins/builtin/congress-trades/table.tsx +10 -1
- 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/builtin/ticker-detail/overview/model.ts +0 -14
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +25 -0
- 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/desktop/detached-windows.ts +9 -2
- package/src/renderers/electrobun/bun/desktop/initialization.ts +1 -0
- package/src/renderers/electrobun/bun/desktop/window-controls.ts +11 -2
- package/src/renderers/electrobun/bun/desktop/window-style.ts +27 -1
- package/src/renderers/electrobun/bun/desktop/windows-custom-chrome.ts +58 -0
- package/src/renderers/electrobun/bun/desktop/windows-icons.ts +11 -64
- package/src/renderers/electrobun/bun/desktop/windows-native.ts +75 -0
- package/src/renderers/electrobun/bun/index.ts +22 -2
- 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/shared/protocol.ts +1 -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/main.tsx +2 -1
- package/src/renderers/electrobun/view/styles.css +9 -8
- package/src/renderers/electrobun/view/ui-host.tsx +102 -77
- 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
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { VERSION } from "../../version";
|
|
4
|
+
import { saveConfig } from "../../data/config/store";
|
|
5
|
+
import { NotesFiles } from "../../plugins/builtin/notes/files";
|
|
6
|
+
import { createAlert, deserializeAlerts, serializeAlerts } from "../../plugins/builtin/alerts/alert-engine";
|
|
7
|
+
import type { AlertCondition } from "../../plugins/builtin/alerts/types";
|
|
8
|
+
import type { CliCommandDef } from "../../types/plugin";
|
|
9
|
+
import { debugLog, type LogLevel } from "../../utils/debug-log";
|
|
10
|
+
import { parsePositiveInt, requireArg, takeOption } from "./command-utils";
|
|
11
|
+
|
|
12
|
+
const ALERTS_PLUGIN_ID = "alerts";
|
|
13
|
+
const ALERTS_KEY = "alerts";
|
|
14
|
+
const LOG_LEVELS = new Set<LogLevel>(["debug", "info", "warn", "error"]);
|
|
15
|
+
|
|
16
|
+
function commandRows(commands: CliCommandDef[]) {
|
|
17
|
+
return commands.map((command) => ({
|
|
18
|
+
name: command.name,
|
|
19
|
+
aliases: command.aliases?.join(",") ?? "",
|
|
20
|
+
description: command.description,
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parseLogLevel(value: string | undefined): LogLevel | undefined {
|
|
25
|
+
return value && LOG_LEVELS.has(value as LogLevel) ? value as LogLevel : undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resourceCacheStats(services: Awaited<ReturnType<Parameters<CliCommandDef["execute"]>[1]["initServices"]>>) {
|
|
29
|
+
const row = services.persistence.database.connection
|
|
30
|
+
.query<{ count: number; size: number; expired: number; stale: number }, []>(
|
|
31
|
+
`SELECT COUNT(*) as count,
|
|
32
|
+
COALESCE(SUM(size_bytes), 0) as size,
|
|
33
|
+
SUM(CASE WHEN expires_at < strftime('%s','now') * 1000 THEN 1 ELSE 0 END) as expired,
|
|
34
|
+
SUM(CASE WHEN stale_at < strftime('%s','now') * 1000 THEN 1 ELSE 0 END) as stale
|
|
35
|
+
FROM resource_cache`,
|
|
36
|
+
)
|
|
37
|
+
.get();
|
|
38
|
+
return {
|
|
39
|
+
entries: row?.count ?? 0,
|
|
40
|
+
sizeBytes: row?.size ?? 0,
|
|
41
|
+
staleEntries: row?.stale ?? 0,
|
|
42
|
+
expiredEntries: row?.expired ?? 0,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function createSystemCliCommands(allCommands: () => CliCommandDef[]): CliCommandDef[] {
|
|
47
|
+
const versionCommand: CliCommandDef = {
|
|
48
|
+
name: "version",
|
|
49
|
+
aliases: ["--version", "-v"],
|
|
50
|
+
description: "Print version and runtime information",
|
|
51
|
+
execute: async (_args, ctx) => {
|
|
52
|
+
let dataDir: string | null = null;
|
|
53
|
+
try {
|
|
54
|
+
const config = await ctx.initConfigData();
|
|
55
|
+
dataDir = config.dataDir;
|
|
56
|
+
config.persistence.close();
|
|
57
|
+
} catch {
|
|
58
|
+
dataDir = null;
|
|
59
|
+
}
|
|
60
|
+
ctx.printResult({
|
|
61
|
+
data: [{
|
|
62
|
+
version: VERSION,
|
|
63
|
+
bun: typeof Bun === "undefined" ? null : Bun.version,
|
|
64
|
+
platform: process.platform,
|
|
65
|
+
arch: process.arch,
|
|
66
|
+
dataDir,
|
|
67
|
+
}],
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const doctorCommand: CliCommandDef = {
|
|
73
|
+
name: "doctor",
|
|
74
|
+
description: "Check local CLI runtime, config, plugins, and capability health",
|
|
75
|
+
execute: async (_args, ctx) => {
|
|
76
|
+
const checks: Array<{ check: string; status: string; detail: string }> = [];
|
|
77
|
+
let services: Awaited<ReturnType<typeof ctx.initServices>> | null = null;
|
|
78
|
+
try {
|
|
79
|
+
services = await ctx.initServices();
|
|
80
|
+
checks.push({ check: "config", status: "ok", detail: services.dataDir });
|
|
81
|
+
checks.push({ check: "database", status: "ok", detail: join(services.dataDir, ".gloomberb-cache.db") });
|
|
82
|
+
checks.push({ check: "plugins", status: "ok", detail: String(services.services.pluginRegistry.allPlugins.size) });
|
|
83
|
+
checks.push({ check: "capabilities", status: "ok", detail: String(services.services.pluginRegistry.capabilities.manifests().length) });
|
|
84
|
+
} catch (error) {
|
|
85
|
+
checks.push({ check: "runtime", status: "error", detail: error instanceof Error ? error.message : String(error) });
|
|
86
|
+
} finally {
|
|
87
|
+
services?.destroy();
|
|
88
|
+
}
|
|
89
|
+
ctx.printResult({ data: checks }, {
|
|
90
|
+
columns: [
|
|
91
|
+
{ key: "check", header: "Check" },
|
|
92
|
+
{ key: "status", header: "Status" },
|
|
93
|
+
{ key: "detail", header: "Detail" },
|
|
94
|
+
],
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const configCommand: CliCommandDef = {
|
|
100
|
+
name: "config",
|
|
101
|
+
description: "Inspect and update local configuration",
|
|
102
|
+
help: { usage: ["config list", "config get <key>", "config set <key> <value>"] },
|
|
103
|
+
execute: async (args, ctx) => {
|
|
104
|
+
const action = args[0] ?? "list";
|
|
105
|
+
const context = await ctx.initConfigData();
|
|
106
|
+
try {
|
|
107
|
+
const safeConfig: Record<string, unknown> = {
|
|
108
|
+
dataDir: context.config.dataDir,
|
|
109
|
+
baseCurrency: context.config.baseCurrency,
|
|
110
|
+
refreshIntervalMinutes: context.config.refreshIntervalMinutes,
|
|
111
|
+
theme: context.config.theme,
|
|
112
|
+
disabledPlugins: context.config.disabledPlugins,
|
|
113
|
+
disabledSources: context.config.disabledSources,
|
|
114
|
+
portfolios: context.config.portfolios.length,
|
|
115
|
+
watchlists: context.config.watchlists.length,
|
|
116
|
+
brokerInstances: context.config.brokerInstances.length,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
if (action === "list") {
|
|
120
|
+
ctx.printResult({ data: safeConfig });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (action === "get") {
|
|
124
|
+
const key = requireArg(args[1], "Usage: gloomberb config get <key>", ctx);
|
|
125
|
+
ctx.printResult({
|
|
126
|
+
data: {
|
|
127
|
+
key,
|
|
128
|
+
value: Object.prototype.hasOwnProperty.call(safeConfig, key) ? safeConfig[key] : null,
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (action === "set") {
|
|
134
|
+
const key = requireArg(args[1], "Usage: gloomberb config set <key> <value>", ctx);
|
|
135
|
+
const value = requireArg(args[2], "Usage: gloomberb config set <key> <value>", ctx);
|
|
136
|
+
const editable = new Set(["baseCurrency", "refreshIntervalMinutes", "theme", "valueFlashingEnabled"]);
|
|
137
|
+
if (!editable.has(key)) ctx.fail(`Config key "${key}" is not editable from the CLI.`);
|
|
138
|
+
const parsedValue = key === "refreshIntervalMinutes"
|
|
139
|
+
? Number(value)
|
|
140
|
+
: key === "valueFlashingEnabled"
|
|
141
|
+
? value === "true"
|
|
142
|
+
: value;
|
|
143
|
+
const nextConfig = { ...context.config, [key]: parsedValue };
|
|
144
|
+
if (!ctx.cliOptions.dryRun) await saveConfig(nextConfig);
|
|
145
|
+
ctx.printResult({ data: { changed: !ctx.cliOptions.dryRun, dryRun: ctx.cliOptions.dryRun, key, value: parsedValue } });
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
ctx.fail("Usage: gloomberb config list|get|set");
|
|
149
|
+
} finally {
|
|
150
|
+
context.persistence.close();
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const cacheCommand: CliCommandDef = {
|
|
156
|
+
name: "cache",
|
|
157
|
+
description: "Inspect or clear the resource cache",
|
|
158
|
+
help: { usage: ["cache status", "cache clear [namespace]"] },
|
|
159
|
+
execute: async (args, ctx) => {
|
|
160
|
+
const action = args[0] ?? "status";
|
|
161
|
+
const services = await ctx.initServices();
|
|
162
|
+
try {
|
|
163
|
+
if (action === "status") {
|
|
164
|
+
ctx.printResult({ data: [resourceCacheStats(services)] });
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (action === "clear") {
|
|
168
|
+
const namespace = args[1];
|
|
169
|
+
const before = resourceCacheStats(services);
|
|
170
|
+
if (!ctx.cliOptions.dryRun) services.persistence.resources.clear(namespace);
|
|
171
|
+
const after = ctx.cliOptions.dryRun ? before : resourceCacheStats(services);
|
|
172
|
+
ctx.printResult({ data: [{ changed: !ctx.cliOptions.dryRun, dryRun: ctx.cliOptions.dryRun, namespace: namespace ?? "all", before: before.entries, after: after.entries }] });
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
ctx.fail("Usage: gloomberb cache status|clear");
|
|
176
|
+
} finally {
|
|
177
|
+
services.destroy();
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const providerCommand: CliCommandDef = {
|
|
183
|
+
name: "provider",
|
|
184
|
+
aliases: ["providers"],
|
|
185
|
+
description: "Inspect provider/source capability status",
|
|
186
|
+
help: { usage: ["provider status"] },
|
|
187
|
+
execute: async (_args, ctx) => {
|
|
188
|
+
const services = await ctx.initServices();
|
|
189
|
+
try {
|
|
190
|
+
const disabledSources = new Set(services.config.disabledSources ?? []);
|
|
191
|
+
const rows = services.services.pluginRegistry.capabilities.manifests().map((manifest) => ({
|
|
192
|
+
id: manifest.sourceId ?? manifest.id,
|
|
193
|
+
capability: manifest.id,
|
|
194
|
+
kind: manifest.kind,
|
|
195
|
+
enabled: !disabledSources.has(manifest.sourceId ?? manifest.id),
|
|
196
|
+
operations: manifest.operations.map((operation) => operation.id).join(","),
|
|
197
|
+
}));
|
|
198
|
+
ctx.printResult({ data: rows }, {
|
|
199
|
+
columns: [
|
|
200
|
+
{ key: "id", header: "Source" },
|
|
201
|
+
{ key: "kind", header: "Kind" },
|
|
202
|
+
{ key: "enabled", header: "Enabled" },
|
|
203
|
+
{ key: "operations", header: "Operations" },
|
|
204
|
+
],
|
|
205
|
+
});
|
|
206
|
+
} finally {
|
|
207
|
+
services.destroy();
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const pluginCommand: CliCommandDef = {
|
|
213
|
+
name: "plugin",
|
|
214
|
+
description: "Inspect, enable, disable, or doctor plugins",
|
|
215
|
+
help: { usage: ["plugin list", "plugin info <id>", "plugin enable <id>", "plugin disable <id>", "plugin doctor"] },
|
|
216
|
+
execute: async (args, ctx) => {
|
|
217
|
+
const action = args[0] ?? "list";
|
|
218
|
+
const services = await ctx.initServices();
|
|
219
|
+
try {
|
|
220
|
+
const pluginRows = () => [...services.services.pluginRegistry.allPlugins.values()].map((plugin) => ({
|
|
221
|
+
id: plugin.id,
|
|
222
|
+
name: plugin.name,
|
|
223
|
+
version: plugin.version,
|
|
224
|
+
enabled: !services.config.disabledPlugins.includes(plugin.id),
|
|
225
|
+
panes: services.services.pluginRegistry.getPluginPaneIds(plugin.id).length,
|
|
226
|
+
templates: services.services.pluginRegistry.getPluginPaneTemplateIds(plugin.id).length,
|
|
227
|
+
capabilities: services.services.pluginRegistry.capabilities.manifests().filter((manifest) => (
|
|
228
|
+
services.services.pluginRegistry.getCapabilityPluginId(manifest.id) === plugin.id
|
|
229
|
+
)).length,
|
|
230
|
+
}));
|
|
231
|
+
|
|
232
|
+
if (action === "list" || action === "doctor") {
|
|
233
|
+
ctx.printResult({ data: pluginRows() });
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (action === "info") {
|
|
237
|
+
const id = requireArg(args[1], "Usage: gloomberb plugin info <id>", ctx);
|
|
238
|
+
const row = pluginRows().find((plugin) => plugin.id === id);
|
|
239
|
+
if (!row) ctx.fail(`Plugin "${id}" is not available.`);
|
|
240
|
+
ctx.printResult({ data: row });
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (action === "enable" || action === "disable") {
|
|
244
|
+
const id = requireArg(args[1], `Usage: gloomberb plugin ${action} <id>`, ctx);
|
|
245
|
+
const disabled = new Set(services.config.disabledPlugins ?? []);
|
|
246
|
+
const before = disabled.has(id);
|
|
247
|
+
if (action === "enable") disabled.delete(id);
|
|
248
|
+
else disabled.add(id);
|
|
249
|
+
const nextConfig = { ...services.config, disabledPlugins: [...disabled] };
|
|
250
|
+
if (!ctx.cliOptions.dryRun) await saveConfig(nextConfig);
|
|
251
|
+
ctx.printResult({ data: { changed: before !== disabled.has(id) && !ctx.cliOptions.dryRun, dryRun: ctx.cliOptions.dryRun, id, enabled: !disabled.has(id) } });
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
ctx.fail("Usage: gloomberb plugin list|info|enable|disable|doctor");
|
|
255
|
+
} finally {
|
|
256
|
+
services.destroy();
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const layoutCommand: CliCommandDef = {
|
|
262
|
+
name: "layout",
|
|
263
|
+
description: "Inspect saved layouts",
|
|
264
|
+
execute: async (_args, ctx) => {
|
|
265
|
+
const config = await ctx.initConfigData();
|
|
266
|
+
try {
|
|
267
|
+
const rows = config.config.layouts.map((layout, index) => ({
|
|
268
|
+
index,
|
|
269
|
+
active: index === config.config.activeLayoutIndex,
|
|
270
|
+
name: layout.name,
|
|
271
|
+
panes: layout.layout.instances.length,
|
|
272
|
+
floating: layout.layout.floating.length,
|
|
273
|
+
detached: layout.layout.detached.length,
|
|
274
|
+
}));
|
|
275
|
+
ctx.printResult({ data: rows });
|
|
276
|
+
} finally {
|
|
277
|
+
config.persistence.close();
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const paneCommand: CliCommandDef = {
|
|
283
|
+
name: "pane",
|
|
284
|
+
description: "Inspect pane and pane-template inventory",
|
|
285
|
+
help: { usage: ["pane list"] },
|
|
286
|
+
execute: async (_args, ctx) => {
|
|
287
|
+
const services = await ctx.initServices();
|
|
288
|
+
try {
|
|
289
|
+
const rows = [
|
|
290
|
+
...[...services.services.pluginRegistry.panes.entries()].map(([id, pane]) => ({
|
|
291
|
+
kind: "pane",
|
|
292
|
+
id,
|
|
293
|
+
name: pane.name,
|
|
294
|
+
owner: "",
|
|
295
|
+
})),
|
|
296
|
+
...[...services.services.pluginRegistry.paneTemplates.entries()].map(([id, template]) => ({
|
|
297
|
+
kind: "template",
|
|
298
|
+
id,
|
|
299
|
+
name: template.label,
|
|
300
|
+
owner: services.services.pluginRegistry.getPaneTemplatePluginId(id) ?? "",
|
|
301
|
+
})),
|
|
302
|
+
];
|
|
303
|
+
ctx.printResult({ data: rows }, {
|
|
304
|
+
columns: [
|
|
305
|
+
{ key: "kind", header: "Kind" },
|
|
306
|
+
{ key: "id", header: "ID" },
|
|
307
|
+
{ key: "name", header: "Name" },
|
|
308
|
+
{ key: "owner", header: "Owner" },
|
|
309
|
+
],
|
|
310
|
+
});
|
|
311
|
+
} finally {
|
|
312
|
+
services.destroy();
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const notesCommand: CliCommandDef = {
|
|
318
|
+
name: "notes",
|
|
319
|
+
description: "Read and mutate ticker or quick notes",
|
|
320
|
+
help: { usage: ["notes show <symbol>", "notes set <symbol> TEXT", "notes delete <symbol>", "notes quick list"] },
|
|
321
|
+
execute: async (args, ctx) => {
|
|
322
|
+
const config = await ctx.initConfigData();
|
|
323
|
+
const notes = new NotesFiles(config.dataDir);
|
|
324
|
+
try {
|
|
325
|
+
const action = args[0] ?? "list";
|
|
326
|
+
if (action === "quick") {
|
|
327
|
+
const subaction = args[1] ?? "list";
|
|
328
|
+
if (subaction !== "list") ctx.fail("Usage: gloomberb notes quick list");
|
|
329
|
+
const entries = await notes.loadQuickNotesIndex();
|
|
330
|
+
ctx.printResult({ data: entries });
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
if (action === "show") {
|
|
334
|
+
const symbol = requireArg(args[1]?.toUpperCase(), "Usage: gloomberb notes show <symbol>", ctx);
|
|
335
|
+
ctx.printResult({ data: { symbol, text: await notes.load(symbol) } });
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (action === "set") {
|
|
339
|
+
const symbol = requireArg(args[1]?.toUpperCase(), "Usage: gloomberb notes set <symbol> TEXT", ctx);
|
|
340
|
+
const text = args.slice(2).join(" ");
|
|
341
|
+
if (!ctx.cliOptions.dryRun) await notes.save(symbol, text);
|
|
342
|
+
ctx.printResult({ data: { changed: !ctx.cliOptions.dryRun, dryRun: ctx.cliOptions.dryRun, symbol, bytes: text.length } });
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (action === "delete" || action === "rm") {
|
|
346
|
+
const symbol = requireArg(args[1]?.toUpperCase(), "Usage: gloomberb notes delete <symbol>", ctx);
|
|
347
|
+
if (!ctx.cliOptions.dryRun) await notes.delete(symbol);
|
|
348
|
+
ctx.printResult({ data: { changed: !ctx.cliOptions.dryRun, dryRun: ctx.cliOptions.dryRun, symbol } });
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
ctx.fail("Usage: gloomberb notes show|set|delete|quick");
|
|
352
|
+
} finally {
|
|
353
|
+
config.persistence.close();
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const alertsCommand: CliCommandDef = {
|
|
359
|
+
name: "alerts",
|
|
360
|
+
aliases: ["alert"],
|
|
361
|
+
description: "List and manage price alerts",
|
|
362
|
+
help: { usage: ["alerts list", "alerts add <symbol> <above|below|crosses> <price>", "alerts delete <id>", "alerts rearm <id>"] },
|
|
363
|
+
execute: async (args, ctx) => {
|
|
364
|
+
const action = args[0] ?? "list";
|
|
365
|
+
const config = await ctx.initConfigData();
|
|
366
|
+
try {
|
|
367
|
+
const raw = config.config.pluginConfig[ALERTS_PLUGIN_ID]?.[ALERTS_KEY];
|
|
368
|
+
const alerts = deserializeAlerts(typeof raw === "string" ? raw : "[]");
|
|
369
|
+
const saveAlerts = async (nextAlerts: typeof alerts) => {
|
|
370
|
+
const nextConfig = {
|
|
371
|
+
...config.config,
|
|
372
|
+
pluginConfig: {
|
|
373
|
+
...config.config.pluginConfig,
|
|
374
|
+
[ALERTS_PLUGIN_ID]: {
|
|
375
|
+
...(config.config.pluginConfig[ALERTS_PLUGIN_ID] ?? {}),
|
|
376
|
+
[ALERTS_KEY]: serializeAlerts(nextAlerts),
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
};
|
|
380
|
+
if (!ctx.cliOptions.dryRun) await saveConfig(nextConfig);
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
if (action === "list") {
|
|
384
|
+
ctx.printResult({ data: alerts });
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (action === "add") {
|
|
388
|
+
const symbol = requireArg(args[1]?.toUpperCase(), "Usage: gloomberb alerts add <symbol> <above|below|crosses> <price>", ctx);
|
|
389
|
+
const condition = requireArg(args[2], "Usage: gloomberb alerts add <symbol> <above|below|crosses> <price>", ctx) as AlertCondition;
|
|
390
|
+
if (!["above", "below", "crosses"].includes(condition)) ctx.fail("Condition must be above, below, or crosses.");
|
|
391
|
+
const price = Number(requireArg(args[3], "Usage: gloomberb alerts add <symbol> <above|below|crosses> <price>", ctx));
|
|
392
|
+
if (!Number.isFinite(price)) ctx.fail("Alert price must be a finite number.");
|
|
393
|
+
const alert = createAlert(symbol, condition, price);
|
|
394
|
+
await saveAlerts([...alerts, alert]);
|
|
395
|
+
ctx.printResult({ data: { changed: !ctx.cliOptions.dryRun, dryRun: ctx.cliOptions.dryRun, alert } });
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
if (action === "delete" || action === "rm") {
|
|
399
|
+
const id = requireArg(args[1], "Usage: gloomberb alerts delete <id>", ctx);
|
|
400
|
+
const next = alerts.filter((alert) => alert.id !== id);
|
|
401
|
+
await saveAlerts(next);
|
|
402
|
+
ctx.printResult({ data: { changed: !ctx.cliOptions.dryRun && next.length !== alerts.length, dryRun: ctx.cliOptions.dryRun, id } });
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
if (action === "rearm") {
|
|
406
|
+
const id = requireArg(args[1], "Usage: gloomberb alerts rearm <id>", ctx);
|
|
407
|
+
const next = alerts.map((alert) => alert.id === id ? { ...alert, status: "active" as const, triggeredAt: undefined } : alert);
|
|
408
|
+
await saveAlerts(next);
|
|
409
|
+
ctx.printResult({ data: { changed: !ctx.cliOptions.dryRun, dryRun: ctx.cliOptions.dryRun, id } });
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
ctx.fail("Usage: gloomberb alerts list|add|delete|rearm");
|
|
413
|
+
} finally {
|
|
414
|
+
config.persistence.close();
|
|
415
|
+
}
|
|
416
|
+
},
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
const debugCommand: CliCommandDef = {
|
|
420
|
+
name: "debug",
|
|
421
|
+
description: "Inspect, export, clear, or tail in-memory debug logs",
|
|
422
|
+
help: { usage: ["debug logs", "debug export [--output path]", "debug clear", "debug tail [--limit n]"] },
|
|
423
|
+
execute: async (rawArgs, ctx) => {
|
|
424
|
+
const args = [...rawArgs];
|
|
425
|
+
const action = args[0] ?? "logs";
|
|
426
|
+
const source = takeOption(args, "--source");
|
|
427
|
+
const level = parseLogLevel(takeOption(args, "--level"));
|
|
428
|
+
if (action === "clear") {
|
|
429
|
+
debugLog.clear();
|
|
430
|
+
ctx.printResult({ data: { changed: true } });
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (action === "export") {
|
|
434
|
+
const output = takeOption(args, "--output");
|
|
435
|
+
const text = debugLog.exportAsText({ source, level });
|
|
436
|
+
if (output) writeFileSync(output, text);
|
|
437
|
+
ctx.printResult({ data: { output: output ?? null, bytes: text.length, text: output ? undefined : text } });
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const entries = debugLog.getEntries({ source, level }).slice(-(ctx.cliOptions.limit ?? 50));
|
|
441
|
+
ctx.printResult({ data: entries }, {
|
|
442
|
+
columns: [
|
|
443
|
+
{ key: "id", header: "ID", align: "right" },
|
|
444
|
+
{ key: "timestamp", header: "Time", value: (row) => new Date(Number(row.timestamp)).toISOString() },
|
|
445
|
+
{ key: "level", header: "Level" },
|
|
446
|
+
{ key: "source", header: "Source" },
|
|
447
|
+
{ key: "message", header: "Message" },
|
|
448
|
+
],
|
|
449
|
+
});
|
|
450
|
+
},
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
const changelogCommand: CliCommandDef = {
|
|
454
|
+
name: "changelog",
|
|
455
|
+
description: "Print local changelog or release notes when available",
|
|
456
|
+
execute: async (args, ctx) => {
|
|
457
|
+
const limit = parsePositiveInt(args[0], ctx.cliOptions.limit ?? 80, "Line count", ctx);
|
|
458
|
+
const candidates = ["CHANGELOG.md", "CHANGELOG", "RELEASE_NOTES.md", "README.md"];
|
|
459
|
+
const found = candidates.find((candidate) => existsSync(candidate));
|
|
460
|
+
const text = found ? readFileSync(found, "utf8").split("\n").slice(0, limit).join("\n") : "";
|
|
461
|
+
ctx.printResult({ data: { path: found ?? null, text, version: VERSION } });
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
const commandCatalogCommand: CliCommandDef = {
|
|
466
|
+
name: "command",
|
|
467
|
+
aliases: ["commands"],
|
|
468
|
+
description: "List registered first-class CLI commands",
|
|
469
|
+
execute: (_args, ctx) => {
|
|
470
|
+
ctx.printResult({ data: commandRows(allCommands()) }, {
|
|
471
|
+
columns: [
|
|
472
|
+
{ key: "name", header: "Command" },
|
|
473
|
+
{ key: "aliases", header: "Aliases" },
|
|
474
|
+
{ key: "description", header: "Description" },
|
|
475
|
+
],
|
|
476
|
+
});
|
|
477
|
+
},
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
const coverageCommand: CliCommandDef = {
|
|
481
|
+
name: "coverage",
|
|
482
|
+
description: "Show CLI coverage for panes, templates, capabilities, and deferred exceptions",
|
|
483
|
+
execute: async (_args, ctx) => {
|
|
484
|
+
const commandNames = new Set(allCommands().map((command) => command.name));
|
|
485
|
+
const services = await ctx.initServices();
|
|
486
|
+
try {
|
|
487
|
+
const rows = [
|
|
488
|
+
...[...services.services.pluginRegistry.panes.entries()].map(([id, pane]) => ({
|
|
489
|
+
surface: "pane",
|
|
490
|
+
id,
|
|
491
|
+
label: pane.name,
|
|
492
|
+
coverage: commandNames.has(id) ? "first-class" : "visual-only",
|
|
493
|
+
command: commandNames.has(id) ? id : "fn/shot",
|
|
494
|
+
})),
|
|
495
|
+
...[...services.services.pluginRegistry.paneTemplates.entries()].map(([id, template]) => {
|
|
496
|
+
const direct = [id, template.paneId, template.shortcut?.prefix?.toLowerCase()]
|
|
497
|
+
.filter((value): value is string => !!value)
|
|
498
|
+
.find((value) => commandNames.has(value));
|
|
499
|
+
return {
|
|
500
|
+
surface: "template",
|
|
501
|
+
id,
|
|
502
|
+
label: template.label,
|
|
503
|
+
coverage: direct ? "first-class" : "visual-only",
|
|
504
|
+
command: direct ?? "fn/shot",
|
|
505
|
+
};
|
|
506
|
+
}),
|
|
507
|
+
...services.services.pluginRegistry.capabilities.manifests().map((manifest) => ({
|
|
508
|
+
surface: "capability",
|
|
509
|
+
id: manifest.id,
|
|
510
|
+
label: manifest.name,
|
|
511
|
+
coverage: "api",
|
|
512
|
+
command: "api list|get|invoke|subscribe",
|
|
513
|
+
})),
|
|
514
|
+
...["auth", "account-management", "chat"].map((id) => ({
|
|
515
|
+
surface: "deferred",
|
|
516
|
+
id,
|
|
517
|
+
label: id,
|
|
518
|
+
coverage: "deferred",
|
|
519
|
+
command: "",
|
|
520
|
+
})),
|
|
521
|
+
];
|
|
522
|
+
ctx.printResult({ data: rows }, {
|
|
523
|
+
columns: [
|
|
524
|
+
{ key: "surface", header: "Surface" },
|
|
525
|
+
{ key: "id", header: "ID" },
|
|
526
|
+
{ key: "coverage", header: "Coverage" },
|
|
527
|
+
{ key: "command", header: "Command" },
|
|
528
|
+
{ key: "label", header: "Label" },
|
|
529
|
+
],
|
|
530
|
+
});
|
|
531
|
+
} finally {
|
|
532
|
+
services.destroy();
|
|
533
|
+
}
|
|
534
|
+
},
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
return [
|
|
538
|
+
versionCommand,
|
|
539
|
+
doctorCommand,
|
|
540
|
+
configCommand,
|
|
541
|
+
cacheCommand,
|
|
542
|
+
providerCommand,
|
|
543
|
+
pluginCommand,
|
|
544
|
+
layoutCommand,
|
|
545
|
+
paneCommand,
|
|
546
|
+
notesCommand,
|
|
547
|
+
alertsCommand,
|
|
548
|
+
debugCommand,
|
|
549
|
+
changelogCommand,
|
|
550
|
+
commandCatalogCommand,
|
|
551
|
+
coverageCommand,
|
|
552
|
+
];
|
|
553
|
+
}
|