@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.
- package/README.md +403 -0
- package/bin/hoox.js +12 -0
- package/package.json +60 -0
- package/src/commands/check/check-command.test.ts +468 -0
- package/src/commands/check/check-command.ts +1144 -0
- package/src/commands/check/index.ts +10 -0
- package/src/commands/check/prerequisites-command.test.ts +19 -0
- package/src/commands/check/prerequisites-command.ts +92 -0
- package/src/commands/check/types.ts +103 -0
- package/src/commands/clone/clone-command.test.ts +442 -0
- package/src/commands/clone/clone-command.ts +440 -0
- package/src/commands/clone/index.ts +1 -0
- package/src/commands/config/config-command.test.ts +583 -0
- package/src/commands/config/config-command.ts +901 -0
- package/src/commands/config/env-command.test.ts +43 -0
- package/src/commands/config/env-command.ts +314 -0
- package/src/commands/config/index.ts +3 -0
- package/src/commands/config/kv-command.test.ts +14 -0
- package/src/commands/config/kv-command.ts +329 -0
- package/src/commands/dashboard/dashboard-command.test.ts +47 -0
- package/src/commands/dashboard/dashboard-command.ts +127 -0
- package/src/commands/dashboard/index.ts +1 -0
- package/src/commands/db/db-command.test.ts +21 -0
- package/src/commands/db/db-command.ts +314 -0
- package/src/commands/db/index.ts +1 -0
- package/src/commands/deploy/deploy-command.test.ts +304 -0
- package/src/commands/deploy/deploy-command.ts +1053 -0
- package/src/commands/deploy/index.ts +2 -0
- package/src/commands/deploy/telegram-service.ts +61 -0
- package/src/commands/deploy/types.ts +34 -0
- package/src/commands/dev/dev-command.test.ts +383 -0
- package/src/commands/dev/dev-command.ts +407 -0
- package/src/commands/dev/index.ts +1 -0
- package/src/commands/infra/index.ts +5 -0
- package/src/commands/infra/infra-command.test.ts +719 -0
- package/src/commands/infra/infra-command.ts +940 -0
- package/src/commands/infra/types.ts +23 -0
- package/src/commands/init/index.ts +1 -0
- package/src/commands/init/init-command.test.ts +827 -0
- package/src/commands/init/init-command.ts +627 -0
- package/src/commands/init/types.ts +185 -0
- package/src/commands/monitor/index.ts +2 -0
- package/src/commands/monitor/monitor-command.test.ts +235 -0
- package/src/commands/monitor/monitor-command.ts +245 -0
- package/src/commands/monitor/monitor-service.ts +50 -0
- package/src/commands/monitor/types.ts +13 -0
- package/src/commands/repair/index.ts +2 -0
- package/src/commands/repair/repair-command.test.ts +204 -0
- package/src/commands/repair/repair-command.ts +199 -0
- package/src/commands/repair/repair-service.ts +102 -0
- package/src/commands/repair/types.ts +13 -0
- package/src/commands/test/index.ts +2 -0
- package/src/commands/test/test-command.test.ts +319 -0
- package/src/commands/test/test-command.ts +412 -0
- package/src/commands/waf/index.ts +2 -0
- package/src/commands/waf/types.ts +48 -0
- package/src/commands/waf/waf-command.test.ts +506 -0
- package/src/commands/waf/waf-command.ts +548 -0
- package/src/index.ts +198 -0
- package/src/services/cloudflare/cloudflare-service.test.ts +654 -0
- package/src/services/cloudflare/cloudflare-service.ts +435 -0
- package/src/services/cloudflare/index.ts +2 -0
- package/src/services/cloudflare/types.ts +29 -0
- package/src/services/config/config-service.test.ts +395 -0
- package/src/services/config/config-service.ts +207 -0
- package/src/services/config/index.ts +9 -0
- package/src/services/config/types.ts +66 -0
- package/src/services/db/db-service.test.ts +51 -0
- package/src/services/db/db-service.ts +140 -0
- package/src/services/db/index.ts +1 -0
- package/src/services/docker/docker-service.ts +155 -0
- package/src/services/docker/index.ts +1 -0
- package/src/services/env/env-service.test.ts +210 -0
- package/src/services/env/env-service.ts +156 -0
- package/src/services/env/index.ts +1 -0
- package/src/services/kv/index.ts +1 -0
- package/src/services/kv/kv-sync-service.test.ts +38 -0
- package/src/services/kv/kv-sync-service.ts +151 -0
- package/src/services/prerequisites/index.ts +1 -0
- package/src/services/prerequisites/prerequisites-service.test.ts +89 -0
- package/src/services/prerequisites/prerequisites-service.ts +269 -0
- package/src/services/prerequisites/types.ts +48 -0
- package/src/services/secrets/index.ts +12 -0
- package/src/services/secrets/secrets-service.test.ts +486 -0
- package/src/services/secrets/secrets-service.ts +293 -0
- package/src/services/secrets/types.ts +57 -0
- package/src/ui/banner.ts +52 -0
- package/src/ui/index.ts +8 -0
- package/src/ui/menu.ts +473 -0
- package/src/utils/errors.test.ts +69 -0
- package/src/utils/errors.ts +23 -0
- package/src/utils/formatters.test.ts +180 -0
- package/src/utils/formatters.ts +252 -0
- 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,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
|
+
}
|