@jango-blockchained/hoox-cli 0.3.4

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 (94) hide show
  1. package/README.md +403 -0
  2. package/bin/hoox.js +12 -0
  3. package/package.json +60 -0
  4. package/src/commands/check/check-command.test.ts +468 -0
  5. package/src/commands/check/check-command.ts +1144 -0
  6. package/src/commands/check/index.ts +10 -0
  7. package/src/commands/check/prerequisites-command.test.ts +19 -0
  8. package/src/commands/check/prerequisites-command.ts +92 -0
  9. package/src/commands/check/types.ts +103 -0
  10. package/src/commands/clone/clone-command.test.ts +442 -0
  11. package/src/commands/clone/clone-command.ts +440 -0
  12. package/src/commands/clone/index.ts +1 -0
  13. package/src/commands/config/config-command.test.ts +583 -0
  14. package/src/commands/config/config-command.ts +901 -0
  15. package/src/commands/config/env-command.test.ts +43 -0
  16. package/src/commands/config/env-command.ts +314 -0
  17. package/src/commands/config/index.ts +3 -0
  18. package/src/commands/config/kv-command.test.ts +14 -0
  19. package/src/commands/config/kv-command.ts +329 -0
  20. package/src/commands/dashboard/dashboard-command.test.ts +47 -0
  21. package/src/commands/dashboard/dashboard-command.ts +127 -0
  22. package/src/commands/dashboard/index.ts +1 -0
  23. package/src/commands/db/db-command.test.ts +21 -0
  24. package/src/commands/db/db-command.ts +314 -0
  25. package/src/commands/db/index.ts +1 -0
  26. package/src/commands/deploy/deploy-command.test.ts +304 -0
  27. package/src/commands/deploy/deploy-command.ts +1053 -0
  28. package/src/commands/deploy/index.ts +2 -0
  29. package/src/commands/deploy/telegram-service.ts +61 -0
  30. package/src/commands/deploy/types.ts +34 -0
  31. package/src/commands/dev/dev-command.test.ts +383 -0
  32. package/src/commands/dev/dev-command.ts +407 -0
  33. package/src/commands/dev/index.ts +1 -0
  34. package/src/commands/infra/index.ts +5 -0
  35. package/src/commands/infra/infra-command.test.ts +719 -0
  36. package/src/commands/infra/infra-command.ts +940 -0
  37. package/src/commands/infra/types.ts +23 -0
  38. package/src/commands/init/index.ts +1 -0
  39. package/src/commands/init/init-command.test.ts +827 -0
  40. package/src/commands/init/init-command.ts +627 -0
  41. package/src/commands/init/types.ts +185 -0
  42. package/src/commands/monitor/index.ts +2 -0
  43. package/src/commands/monitor/monitor-command.test.ts +235 -0
  44. package/src/commands/monitor/monitor-command.ts +245 -0
  45. package/src/commands/monitor/monitor-service.ts +50 -0
  46. package/src/commands/monitor/types.ts +13 -0
  47. package/src/commands/repair/index.ts +2 -0
  48. package/src/commands/repair/repair-command.test.ts +204 -0
  49. package/src/commands/repair/repair-command.ts +199 -0
  50. package/src/commands/repair/repair-service.ts +102 -0
  51. package/src/commands/repair/types.ts +13 -0
  52. package/src/commands/test/index.ts +2 -0
  53. package/src/commands/test/test-command.test.ts +319 -0
  54. package/src/commands/test/test-command.ts +412 -0
  55. package/src/commands/waf/index.ts +2 -0
  56. package/src/commands/waf/types.ts +48 -0
  57. package/src/commands/waf/waf-command.test.ts +506 -0
  58. package/src/commands/waf/waf-command.ts +548 -0
  59. package/src/index.ts +198 -0
  60. package/src/services/cloudflare/cloudflare-service.test.ts +654 -0
  61. package/src/services/cloudflare/cloudflare-service.ts +435 -0
  62. package/src/services/cloudflare/index.ts +2 -0
  63. package/src/services/cloudflare/types.ts +29 -0
  64. package/src/services/config/config-service.test.ts +395 -0
  65. package/src/services/config/config-service.ts +207 -0
  66. package/src/services/config/index.ts +9 -0
  67. package/src/services/config/types.ts +66 -0
  68. package/src/services/db/db-service.test.ts +51 -0
  69. package/src/services/db/db-service.ts +140 -0
  70. package/src/services/db/index.ts +1 -0
  71. package/src/services/docker/docker-service.ts +155 -0
  72. package/src/services/docker/index.ts +1 -0
  73. package/src/services/env/env-service.test.ts +210 -0
  74. package/src/services/env/env-service.ts +156 -0
  75. package/src/services/env/index.ts +1 -0
  76. package/src/services/kv/index.ts +1 -0
  77. package/src/services/kv/kv-sync-service.test.ts +38 -0
  78. package/src/services/kv/kv-sync-service.ts +151 -0
  79. package/src/services/prerequisites/index.ts +1 -0
  80. package/src/services/prerequisites/prerequisites-service.test.ts +89 -0
  81. package/src/services/prerequisites/prerequisites-service.ts +269 -0
  82. package/src/services/prerequisites/types.ts +48 -0
  83. package/src/services/secrets/index.ts +12 -0
  84. package/src/services/secrets/secrets-service.test.ts +486 -0
  85. package/src/services/secrets/secrets-service.ts +293 -0
  86. package/src/services/secrets/types.ts +57 -0
  87. package/src/ui/banner.ts +52 -0
  88. package/src/ui/index.ts +8 -0
  89. package/src/ui/menu.ts +473 -0
  90. package/src/utils/errors.test.ts +69 -0
  91. package/src/utils/errors.ts +23 -0
  92. package/src/utils/formatters.test.ts +180 -0
  93. package/src/utils/formatters.ts +252 -0
  94. package/src/utils/theme.ts +94 -0
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Types for the `hoox init` interactive setup wizard.
3
+ */
4
+
5
+ /** CLI flags for non-interactive mode. */
6
+ export interface InitOptions {
7
+ /** --token: Cloudflare API token (non-interactive mode) */
8
+ token?: string;
9
+ /** --account: Cloudflare account ID (non-interactive mode) */
10
+ account?: string;
11
+ /** --secret-store: Secret Store ID (non-interactive mode) */
12
+ secretStore?: string;
13
+ /** --prefix: Subdomain prefix (non-interactive mode) */
14
+ prefix?: string;
15
+ /** --accept-risk: Skip the risk acknowledgment confirmation */
16
+ acceptRisk?: boolean;
17
+ }
18
+
19
+ /** Definition of an integration that can be enabled during setup. */
20
+ export interface IntegrationConfig {
21
+ /** Unique key used for the multiselect value. */
22
+ key: string;
23
+ /** Display label shown in the multiselect prompt. */
24
+ label: string;
25
+ /** Which worker this integration maps to in wrangler.jsonc. */
26
+ workerName: string;
27
+ /** Secrets that must be collected for this integration (name → prompt label). */
28
+ secrets: Record<string, string>;
29
+ /** Extra non-secret environment variables for the worker. */
30
+ vars?: Record<string, string>;
31
+ }
32
+
33
+ /** Shape of a single worker entry in wrangler.jsonc. */
34
+ export interface WorkerConfig {
35
+ enabled: boolean;
36
+ path: string;
37
+ vars: Record<string, string>;
38
+ secrets: string[];
39
+ }
40
+
41
+ /** Shape of the wrangler.jsonc file. */
42
+ export interface WorkersJsonConfig {
43
+ global: {
44
+ cloudflare_api_token: string;
45
+ cloudflare_account_id: string;
46
+ cloudflare_secret_store_id: string;
47
+ subdomain_prefix: string;
48
+ };
49
+ workers: Record<string, WorkerConfig>;
50
+ }
51
+
52
+ /**
53
+ * All supported integrations.
54
+ * Exchanges (Binance, MEXC, Bybit) map to trade-worker.
55
+ * Wallet maps to web3-wallet-worker, email to email-worker,
56
+ * telegram to telegram-worker.
57
+ */
58
+ export const INTEGRATIONS: IntegrationConfig[] = [
59
+ {
60
+ key: "binance",
61
+ label: "Binance Exchange",
62
+ workerName: "trade-worker",
63
+ secrets: {
64
+ BINANCE_API_KEY: "Binance API Key",
65
+ BINANCE_API_SECRET: "Binance API Secret",
66
+ },
67
+ },
68
+ {
69
+ key: "mexc",
70
+ label: "MEXC Exchange",
71
+ workerName: "trade-worker",
72
+ secrets: {
73
+ MEXC_API_KEY: "MEXC API Key",
74
+ MEXC_API_SECRET: "MEXC API Secret",
75
+ },
76
+ },
77
+ {
78
+ key: "bybit",
79
+ label: "Bybit Exchange",
80
+ workerName: "trade-worker",
81
+ secrets: {
82
+ BYBIT_API_KEY: "Bybit API Key",
83
+ BYBIT_API_SECRET: "Bybit API Secret",
84
+ },
85
+ },
86
+ {
87
+ key: "wallet",
88
+ label: "Web3 Wallet (on-chain execution)",
89
+ workerName: "web3-wallet-worker",
90
+ secrets: {
91
+ WALLET_MNEMONIC_SECRET: "Wallet Mnemonic Phrase",
92
+ WALLET_PK_SECRET: "Wallet Private Key",
93
+ },
94
+ },
95
+ {
96
+ key: "email",
97
+ label: "Email Signal Parsing",
98
+ workerName: "email-worker",
99
+ secrets: {
100
+ EMAIL_HOST: "Email Host (IMAP server)",
101
+ EMAIL_USER: "Email Username",
102
+ EMAIL_PASS: "Email Password",
103
+ INTERNAL_KEY: "Internal Auth Key",
104
+ },
105
+ vars: {
106
+ USE_IMAP: "false",
107
+ },
108
+ },
109
+ {
110
+ key: "telegram",
111
+ label: "Telegram Notifications",
112
+ workerName: "telegram-worker",
113
+ secrets: {
114
+ TELEGRAM_BOT_TOKEN: "Telegram Bot Token",
115
+ },
116
+ },
117
+ {
118
+ key: "openai",
119
+ label: "OpenAI (AI Agent)",
120
+ workerName: "agent-worker",
121
+ secrets: {
122
+ AGENT_OPENAI_KEY: "OpenAI API Key",
123
+ },
124
+ },
125
+ {
126
+ key: "anthropic",
127
+ label: "Anthropic (AI Agent)",
128
+ workerName: "agent-worker",
129
+ secrets: {
130
+ AGENT_ANTHROPIC_KEY: "Anthropic API Key",
131
+ },
132
+ },
133
+ {
134
+ key: "google-ai",
135
+ label: "Google AI (AI Agent)",
136
+ workerName: "agent-worker",
137
+ secrets: {
138
+ AGENT_GOOGLE_KEY: "Google AI API Key",
139
+ },
140
+ },
141
+ {
142
+ key: "home-assistant",
143
+ label: "Home Assistant (Smart Home)",
144
+ workerName: "hoox",
145
+ secrets: {
146
+ HA_TOKEN_BINDING: "Home Assistant Token",
147
+ },
148
+ },
149
+ ];
150
+
151
+ /**
152
+ * Base workers that are always enabled regardless of integration selection.
153
+ */
154
+ export const BASE_WORKERS: Record<string, Omit<WorkerConfig, "secrets">> = {
155
+ "d1-worker": {
156
+ enabled: true,
157
+ path: "workers/d1-worker",
158
+ vars: { database_name: "my-database" },
159
+ },
160
+ hoox: {
161
+ enabled: true,
162
+ path: "workers/hoox",
163
+ vars: {},
164
+ },
165
+ "agent-worker": {
166
+ enabled: true,
167
+ path: "workers/agent-worker",
168
+ vars: {},
169
+ },
170
+ "analytics-worker": {
171
+ enabled: true,
172
+ path: "workers/analytics-worker",
173
+ vars: {},
174
+ },
175
+ };
176
+
177
+ /**
178
+ * Base secrets for base workers (not integration-driven).
179
+ */
180
+ export const BASE_SECRETS: Record<string, string[]> = {
181
+ hoox: ["WEBHOOK_API_KEY_BINDING"],
182
+ "agent-worker": ["AGENT_INTERNAL_KEY"],
183
+ "analytics-worker": ["CLOUDFLARE_API_TOKEN"],
184
+ "trade-worker": ["API_SERVICE_KEY"],
185
+ };
@@ -0,0 +1,2 @@
1
+ export { registerMonitorCommand } from "./monitor-command.js";
2
+ export type { WorkerHealthResult, MonitorStatusResult } from "./types.js";
@@ -0,0 +1,235 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Unit tests for the monitor command.
4
+ *
5
+ * Stubs MonitorService, DbService, and KvSyncService prototypes to verify
6
+ * the monitor command logic in isolation. Uses Commander's exitOverride to
7
+ * suppress process exits during test runs.
8
+ */
9
+ import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test";
10
+ import { Command } from "commander";
11
+ import { MonitorService } from "./monitor-service.js";
12
+ import { DbService } from "../../services/db/db-service.js";
13
+ import { KvSyncService } from "../../services/kv/kv-sync-service.js";
14
+
15
+ // Stub variables
16
+ let checkAllWorkerHealthMock: ReturnType<typeof mock>;
17
+ let resolveDbNameMock: ReturnType<typeof mock>;
18
+ let queryMock: ReturnType<typeof mock>;
19
+ let exportMock: ReturnType<typeof mock>;
20
+ let resolveNamespaceIdMock: ReturnType<typeof mock>;
21
+ let getMock: ReturnType<typeof mock>;
22
+ let setMock: ReturnType<typeof mock>;
23
+
24
+ // Preserve originals
25
+ const origCheckAll = MonitorService.prototype.checkAllWorkerHealth;
26
+ const origResolveDbName = DbService.prototype.resolveDbName;
27
+ const origQuery = DbService.prototype.query;
28
+ const origExport = DbService.prototype.export;
29
+ const origResolveNs = KvSyncService.prototype.resolveNamespaceId;
30
+ const origGet = KvSyncService.prototype.get;
31
+ const origSet = KvSyncService.prototype.set;
32
+
33
+ beforeEach(() => {
34
+ mock.restore();
35
+ process.exitCode = 0;
36
+
37
+ // Restore originals
38
+ (MonitorService.prototype as Record<string, unknown>).checkAllWorkerHealth = origCheckAll;
39
+ (DbService.prototype as Record<string, unknown>).resolveDbName = origResolveDbName;
40
+ (DbService.prototype as Record<string, unknown>).query = origQuery;
41
+ (DbService.prototype as Record<string, unknown>).export = origExport;
42
+ (KvSyncService.prototype as Record<string, unknown>).resolveNamespaceId = origResolveNs;
43
+ (KvSyncService.prototype as Record<string, unknown>).get = origGet;
44
+ (KvSyncService.prototype as Record<string, unknown>).set = origSet;
45
+
46
+ // Fresh mocks
47
+ checkAllWorkerHealthMock = mock(async () => ({
48
+ workers: [
49
+ { worker: "hoox", status: "healthy", statusCode: 200 },
50
+ { worker: "trade-worker", status: "healthy", statusCode: 200 },
51
+ ],
52
+ healthyCount: 2,
53
+ degradedCount: 0,
54
+ unreachableCount: 0,
55
+ }));
56
+
57
+ resolveDbNameMock = mock(async () => "trade-data-db");
58
+ queryMock = mock(async (_dbName: string, _sql: string, _remote: boolean) => "[mock query result]");
59
+ exportMock = mock(async (_dbName: string) => "backup-2026-05-13.sql");
60
+ resolveNamespaceIdMock = mock(async () => "ns-id-123");
61
+ getMock = mock(async (_nsId: string, _key: string) => "false");
62
+ setMock = mock(async (_nsId: string, _key: string, _value: string) => {});
63
+
64
+ // Install mocks on prototypes
65
+ (MonitorService.prototype as Record<string, unknown>).checkAllWorkerHealth = checkAllWorkerHealthMock;
66
+ (DbService.prototype as Record<string, unknown>).resolveDbName = resolveDbNameMock;
67
+ (DbService.prototype as Record<string, unknown>).query = queryMock;
68
+ (DbService.prototype as Record<string, unknown>).export = exportMock;
69
+ (KvSyncService.prototype as Record<string, unknown>).resolveNamespaceId = resolveNamespaceIdMock;
70
+ (KvSyncService.prototype as Record<string, unknown>).get = getMock;
71
+ (KvSyncService.prototype as Record<string, unknown>).set = setMock;
72
+ });
73
+
74
+ afterEach(() => {
75
+ mock.restore();
76
+ (MonitorService.prototype as Record<string, unknown>).checkAllWorkerHealth = origCheckAll;
77
+ (DbService.prototype as Record<string, unknown>).resolveDbName = origResolveDbName;
78
+ (DbService.prototype as Record<string, unknown>).query = origQuery;
79
+ (DbService.prototype as Record<string, unknown>).export = origExport;
80
+ (KvSyncService.prototype as Record<string, unknown>).resolveNamespaceId = origResolveNs;
81
+ (KvSyncService.prototype as Record<string, unknown>).get = origGet;
82
+ (KvSyncService.prototype as Record<string, unknown>).set = origSet;
83
+ });
84
+
85
+ async function importMonitorCommand(): Promise<{
86
+ registerMonitorCommand: typeof import("./monitor-command.js").registerMonitorCommand;
87
+ }> {
88
+ return import("./monitor-command.js");
89
+ }
90
+
91
+ async function createProgram(): Promise<Command> {
92
+ const { registerMonitorCommand } = await importMonitorCommand();
93
+ const program = new Command().name("hoox-test").exitOverride(() => {});
94
+ registerMonitorCommand(program);
95
+ return program;
96
+ }
97
+
98
+ describe("registerMonitorCommand", () => {
99
+ // -- Command registration -------------------------------------------------
100
+
101
+ it("registers 'monitor' as a command on the program", async () => {
102
+ const program = await createProgram();
103
+ const cmd = program.commands.find((c) => c.name() === "monitor");
104
+ expect(cmd).toBeDefined();
105
+ });
106
+
107
+ it("registers 'monitor status' subcommand", async () => {
108
+ const program = await createProgram();
109
+ const monitorCmd = program.commands.find((c) => c.name() === "monitor")!;
110
+ const statusCmd = monitorCmd.commands.find((c) => c.name() === "status");
111
+ expect(statusCmd).toBeDefined();
112
+ });
113
+
114
+ it("registers 'monitor trades' subcommand with argument", async () => {
115
+ const program = await createProgram();
116
+ const monitorCmd = program.commands.find((c) => c.name() === "monitor")!;
117
+ const tradesCmd = monitorCmd.commands.find((c) => c.name() === "trades");
118
+ expect(tradesCmd).toBeDefined();
119
+ const args = tradesCmd!.registeredArguments;
120
+ expect(args.some((a) => a.name() === "limit")).toBe(true);
121
+ });
122
+
123
+ it("registers 'monitor logs' subcommand with optional argument", async () => {
124
+ const program = await createProgram();
125
+ const monitorCmd = program.commands.find((c) => c.name() === "monitor")!;
126
+ const logsCmd = monitorCmd.commands.find((c) => c.name() === "logs");
127
+ expect(logsCmd).toBeDefined();
128
+ });
129
+
130
+ it("registers 'monitor kill-switch' with show/on/off subcommands", async () => {
131
+ const program = await createProgram();
132
+ const monitorCmd = program.commands.find((c) => c.name() === "monitor")!;
133
+ const ksCmd = monitorCmd.commands.find((c) => c.name() === "kill-switch");
134
+ expect(ksCmd).toBeDefined();
135
+ expect(ksCmd!.commands.find((c) => c.name() === "show")).toBeDefined();
136
+ expect(ksCmd!.commands.find((c) => c.name() === "on")).toBeDefined();
137
+ expect(ksCmd!.commands.find((c) => c.name() === "off")).toBeDefined();
138
+ });
139
+
140
+ it("registers 'monitor queue-depth' subcommand", async () => {
141
+ const program = await createProgram();
142
+ const monitorCmd = program.commands.find((c) => c.name() === "monitor")!;
143
+ const qCmd = monitorCmd.commands.find((c) => c.name() === "queue-depth");
144
+ expect(qCmd).toBeDefined();
145
+ });
146
+
147
+ it("registers 'monitor backup' subcommand", async () => {
148
+ const program = await createProgram();
149
+ const monitorCmd = program.commands.find((c) => c.name() === "monitor")!;
150
+ const backupCmd = monitorCmd.commands.find((c) => c.name() === "backup");
151
+ expect(backupCmd).toBeDefined();
152
+ });
153
+
154
+ // -- monitor status -------------------------------------------------------
155
+
156
+ describe("monitor status", () => {
157
+ it("calls checkAllWorkerHealth", async () => {
158
+ const program = await createProgram();
159
+ await program.parseAsync(["monitor", "status"], { from: "user" });
160
+ expect(checkAllWorkerHealthMock).toHaveBeenCalled();
161
+ });
162
+
163
+ it("exits cleanly on success", async () => {
164
+ process.exitCode = 0;
165
+ const program = await createProgram();
166
+ process.exitCode = 0;
167
+ await program.parseAsync(["monitor", "status"], { from: "user" });
168
+ expect(process.exitCode).toBe(0);
169
+ });
170
+
171
+ it("sets exitCode on service failure", async () => {
172
+ checkAllWorkerHealthMock = mock(async () => {
173
+ throw new Error("Connection failed");
174
+ });
175
+ (MonitorService.prototype as Record<string, unknown>).checkAllWorkerHealth = checkAllWorkerHealthMock;
176
+
177
+ const program = await createProgram();
178
+ await program.parseAsync(["monitor", "status"], { from: "user" });
179
+ expect(process.exitCode).toBe(1);
180
+ });
181
+ });
182
+
183
+ // -- monitor trades -------------------------------------------------------
184
+
185
+ describe("monitor trades", () => {
186
+ it("calls db.query with trades table", async () => {
187
+ const program = await createProgram();
188
+ await program.parseAsync(["monitor", "trades"], { from: "user" });
189
+ expect(resolveDbNameMock).toHaveBeenCalled();
190
+ expect(queryMock).toHaveBeenCalled();
191
+ const sql = queryMock.mock.calls[0][1] as string;
192
+ expect(sql).toContain("trades");
193
+ expect(sql).toContain("ORDER BY timestamp DESC");
194
+ });
195
+
196
+ it("accepts custom limit", async () => {
197
+ const program = await createProgram();
198
+ await program.parseAsync(["monitor", "trades", "25"], { from: "user" });
199
+ const sql = queryMock.mock.calls[0][1] as string;
200
+ expect(sql).toContain("LIMIT 25");
201
+ });
202
+ });
203
+
204
+ // -- monitor kill-switch --------------------------------------------------
205
+
206
+ describe("monitor kill-switch", () => {
207
+ it("shows kill switch status", async () => {
208
+ const program = await createProgram();
209
+ await program.parseAsync(["monitor", "kill-switch", "show"], { from: "user" });
210
+ expect(getMock).toHaveBeenCalledWith("ns-id-123", "trade:kill_switch");
211
+ });
212
+
213
+ it("turns kill switch on", async () => {
214
+ const program = await createProgram();
215
+ await program.parseAsync(["monitor", "kill-switch", "on"], { from: "user" });
216
+ expect(setMock).toHaveBeenCalledWith("ns-id-123", "trade:kill_switch", "true");
217
+ });
218
+
219
+ it("turns kill switch off", async () => {
220
+ const program = await createProgram();
221
+ await program.parseAsync(["monitor", "kill-switch", "off"], { from: "user" });
222
+ expect(setMock).toHaveBeenCalledWith("ns-id-123", "trade:kill_switch", "false");
223
+ });
224
+ });
225
+
226
+ // -- monitor backup -------------------------------------------------------
227
+
228
+ describe("monitor backup", () => {
229
+ it("calls db.export", async () => {
230
+ const program = await createProgram();
231
+ await program.parseAsync(["monitor", "backup"], { from: "user" });
232
+ expect(exportMock).toHaveBeenCalled();
233
+ });
234
+ });
235
+ });
@@ -0,0 +1,245 @@
1
+ import { Command } from "commander";
2
+ import { DbService } from "../../services/db/index.js";
3
+ import { MonitorService } from "./monitor-service.js";
4
+ import { KvSyncService } from "../../services/kv/kv-sync-service.js";
5
+ import { formatSuccess, formatError, formatTable } from "../../utils/formatters.js";
6
+ import type { FormatOptions } from "../../utils/formatters.js";
7
+ import { ExitCode } from "../../utils/errors.js";
8
+ import { theme } from "../../utils/theme.js";
9
+
10
+ function getFormatOptions(cmd: Command): FormatOptions {
11
+ const opts = cmd.optsWithGlobals();
12
+ return { json: Boolean(opts.json), quiet: Boolean(opts.quiet) };
13
+ }
14
+
15
+ async function doMonitorStatus(fmt: FormatOptions): Promise<void> {
16
+ try {
17
+ const monitor = new MonitorService();
18
+ const result = await monitor.checkAllWorkerHealth();
19
+
20
+ if (fmt.json) {
21
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
22
+ return;
23
+ }
24
+
25
+ const rows = result.workers.map((w) => ({
26
+ Worker: w.worker,
27
+ Status: w.status,
28
+ "Status Code": String(w.statusCode ?? "-"),
29
+ Error: w.error ?? "-",
30
+ }));
31
+ formatTable(rows, fmt);
32
+
33
+ process.stdout.write(
34
+ `\n${theme.heading("Summary:")} ${result.healthyCount} healthy, ${result.degradedCount} degraded, ${result.unreachableCount} unreachable\n`
35
+ );
36
+ } catch (err) {
37
+ formatError(err instanceof Error ? err.message : String(err), fmt);
38
+ process.exitCode = ExitCode.ERROR;
39
+ }
40
+ }
41
+
42
+ async function doMonitorTrades(limit: number, fmt: FormatOptions): Promise<void> {
43
+ try {
44
+ const db = new DbService();
45
+ const dbName = await db.resolveDbName();
46
+ const sql = `SELECT * FROM trades ORDER BY timestamp DESC LIMIT ${Math.min(limit, 100)}`;
47
+ const output = await db.query(dbName, sql, true);
48
+ process.stdout.write(output + "\n");
49
+ } catch (err) {
50
+ formatError(err instanceof Error ? err.message : String(err), fmt);
51
+ process.exitCode = ExitCode.ERROR;
52
+ }
53
+ }
54
+
55
+ async function doMonitorLogs(workerName: string | undefined, fmt: FormatOptions): Promise<void> {
56
+ try {
57
+ const db = new DbService();
58
+ const dbName = await db.resolveDbName();
59
+ let sql: string;
60
+ if (workerName) {
61
+ sql = `SELECT * FROM system_logs WHERE worker = '${workerName.replace(/'/g, "''")}' ORDER BY timestamp DESC LIMIT 20`;
62
+ } else {
63
+ sql = "SELECT * FROM system_logs ORDER BY timestamp DESC LIMIT 20";
64
+ }
65
+ const output = await db.query(dbName, sql, true);
66
+ process.stdout.write(output + "\n");
67
+ } catch (err) {
68
+ formatError(err instanceof Error ? err.message : String(err), fmt);
69
+ process.exitCode = ExitCode.ERROR;
70
+ }
71
+ }
72
+
73
+ async function doMonitorKillSwitch(
74
+ action: "show" | "on" | "off",
75
+ fmt: FormatOptions,
76
+ ): Promise<void> {
77
+ try {
78
+ const kv = new KvSyncService();
79
+ const namespaceId = await kv.resolveNamespaceId();
80
+ const key = "trade:kill_switch";
81
+
82
+ if (action === "show") {
83
+ const value = await kv.get(namespaceId, key);
84
+ const status = value === "true"
85
+ ? "KILL SWITCH IS ON (trading halted)"
86
+ : value === "false"
87
+ ? "Kill switch is off (trading active)"
88
+ : `Kill switch value: ${value ?? "(not set)"}`;
89
+ process.stdout.write(`${theme.info(status)}\n`);
90
+ } else {
91
+ const newValue = action === "on" ? "true" : "false";
92
+ await kv.set(namespaceId, key, newValue);
93
+ formatSuccess(`Kill switch turned ${action === "on" ? "ON" : "OFF"}`, fmt);
94
+ }
95
+ } catch (err) {
96
+ formatError(err instanceof Error ? err.message : String(err), fmt);
97
+ process.exitCode = ExitCode.ERROR;
98
+ }
99
+ }
100
+
101
+ async function doMonitorQueueDepth(fmt: FormatOptions): Promise<void> {
102
+ try {
103
+ const proc = Bun.spawn(["wrangler", "queues", "list"], {
104
+ stdout: "pipe",
105
+ stderr: "pipe",
106
+ });
107
+ const stdout = await new Response(proc.stdout).text();
108
+ const stderr = await new Response(proc.stderr).text();
109
+ const exitCode = await proc.exited;
110
+
111
+ if (exitCode !== 0) {
112
+ throw new Error(stderr.trim() || `wrangler exited with code ${exitCode}`);
113
+ }
114
+
115
+ process.stdout.write(stdout + "\n");
116
+ } catch (err) {
117
+ formatError(err instanceof Error ? err.message : String(err), fmt);
118
+ process.exitCode = ExitCode.ERROR;
119
+ }
120
+ }
121
+
122
+ async function doMonitorBackup(fmt: FormatOptions): Promise<void> {
123
+ try {
124
+ const db = new DbService();
125
+ const dbName = await db.resolveDbName();
126
+ const outputPath = await db.export(dbName);
127
+ formatSuccess(`Database exported to ${outputPath}`, fmt);
128
+ } catch (err) {
129
+ formatError(err instanceof Error ? err.message : String(err), fmt);
130
+ process.exitCode = ExitCode.ERROR;
131
+ }
132
+ }
133
+
134
+ export function registerMonitorCommand(program: Command): void {
135
+ const monitorCmd = program
136
+ .command("monitor")
137
+ .summary("Monitor and operate the trading system")
138
+ .description(
139
+ `Monitor worker health, trades, logs, and perform operational tasks.
140
+
141
+ SUBCOMMANDS:
142
+ status Check health of all workers
143
+ trades [N] Show N most recent trades (default: 10)
144
+ logs [worker] Show recent system logs from D1
145
+ kill-switch Emergency stop/resume trading
146
+ queue-depth List queues and pending messages
147
+ backup Export D1 database to timestamped .sql file
148
+
149
+ EXAMPLES:
150
+ hoox monitor status Check all workers health
151
+ hoox monitor trades 20 Show 20 recent trades
152
+ hoox monitor logs hoox Show logs for hoox worker
153
+ hoox monitor kill-switch show Check kill switch status
154
+ hoox monitor kill-switch on Halt all trading
155
+ hoox monitor kill-switch off Resume trading
156
+ hoox monitor queue-depth Show queue info
157
+ hoox monitor backup Export D1 database`
158
+ );
159
+
160
+ monitorCmd
161
+ .command("status")
162
+ .summary("Check health of all workers")
163
+ .description("Probe each worker's /health endpoint and report status.")
164
+ .action(async function (this: Command) {
165
+ const fmt = getFormatOptions(this);
166
+ await doMonitorStatus(fmt);
167
+ });
168
+
169
+ monitorCmd
170
+ .command("trades")
171
+ .summary("Show recent trades from D1")
172
+ .description("Query the trades table for the most recent entries.")
173
+ .argument("[limit]", "Number of trades to show (default: 10, max: 100)", "10")
174
+ .action(async function (this: Command, limit: string) {
175
+ const fmt = getFormatOptions(this);
176
+ await doMonitorTrades(parseInt(limit, 10) || 10, fmt);
177
+ });
178
+
179
+ monitorCmd
180
+ .command("logs")
181
+ .summary("Show recent system logs from D1")
182
+ .description("Query the system_logs table. Optionally filter by worker name.")
183
+ .argument("[worker]", "Worker name to filter (optional)")
184
+ .action(async function (this: Command, worker?: string) {
185
+ const fmt = getFormatOptions(this);
186
+ await doMonitorLogs(worker, fmt);
187
+ });
188
+
189
+ const ksCmd = monitorCmd
190
+ .command("kill-switch")
191
+ .summary("Emergency stop or resume trading")
192
+ .description(
193
+ `Control the trade:kill_switch KV key.
194
+
195
+ Commands:
196
+ show Display current kill switch status
197
+ on Halt all trading (set kill_switch=true)
198
+ off Resume trading (set kill_switch=false)`
199
+ );
200
+
201
+ ksCmd
202
+ .command("show")
203
+ .summary("Show kill switch status")
204
+ .description("Display whether trading is halted or active.")
205
+ .action(async function (this: Command) {
206
+ const fmt = getFormatOptions(this);
207
+ await doMonitorKillSwitch("show", fmt);
208
+ });
209
+
210
+ ksCmd
211
+ .command("on")
212
+ .summary("Halt all trading")
213
+ .description("Set trade:kill_switch=true to stop all trading operations. WARNING: This halts ALL trading activity immediately.")
214
+ .action(async function (this: Command) {
215
+ const fmt = getFormatOptions(this);
216
+ await doMonitorKillSwitch("on", fmt);
217
+ });
218
+
219
+ ksCmd
220
+ .command("off")
221
+ .summary("Resume trading")
222
+ .description("Set trade:kill_switch=false to resume normal trading operations.")
223
+ .action(async function (this: Command) {
224
+ const fmt = getFormatOptions(this);
225
+ await doMonitorKillSwitch("off", fmt);
226
+ });
227
+
228
+ monitorCmd
229
+ .command("queue-depth")
230
+ .summary("Show queue details")
231
+ .description("List queues via wrangler queues list to show configured queues.")
232
+ .action(async function (this: Command) {
233
+ const fmt = getFormatOptions(this);
234
+ await doMonitorQueueDepth(fmt);
235
+ });
236
+
237
+ monitorCmd
238
+ .command("backup")
239
+ .summary("Export D1 database to .sql file")
240
+ .description("Export the D1 database to a timestamped .sql backup file via wrangler d1 export.")
241
+ .action(async function (this: Command) {
242
+ const fmt = getFormatOptions(this);
243
+ await doMonitorBackup(fmt);
244
+ });
245
+ }