@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,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service for interacting with the Telegram Bot API.
|
|
3
|
+
* Used by `hoox deploy telegram-webhook`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class TelegramService {
|
|
7
|
+
/**
|
|
8
|
+
* Set the webhook for a Telegram bot.
|
|
9
|
+
* POST https://api.telegram.org/bot{TOKEN}/setWebhook
|
|
10
|
+
*/
|
|
11
|
+
async setWebhook(
|
|
12
|
+
botToken: string,
|
|
13
|
+
webhookUrl: string,
|
|
14
|
+
secretToken: string,
|
|
15
|
+
): Promise<{ ok: boolean; description?: string; error?: string }> {
|
|
16
|
+
try {
|
|
17
|
+
const url = `https://api.telegram.org/bot${botToken}/setWebhook`;
|
|
18
|
+
const response = await fetch(url, {
|
|
19
|
+
method: "POST",
|
|
20
|
+
headers: { "Content-Type": "application/json" },
|
|
21
|
+
body: JSON.stringify({ url: webhookUrl, secret_token: secretToken }),
|
|
22
|
+
});
|
|
23
|
+
const data = (await response.json()) as { ok: boolean; description?: string };
|
|
24
|
+
return { ok: data.ok, description: data.description };
|
|
25
|
+
} catch (err) {
|
|
26
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get the current webhook status.
|
|
32
|
+
* GET https://api.telegram.org/bot{TOKEN}/getWebhookInfo
|
|
33
|
+
*/
|
|
34
|
+
async getWebhookInfo(botToken: string): Promise<{
|
|
35
|
+
ok: boolean;
|
|
36
|
+
url?: string;
|
|
37
|
+
has_custom_certificate?: boolean;
|
|
38
|
+
pending_update_count?: number;
|
|
39
|
+
error?: string;
|
|
40
|
+
}> {
|
|
41
|
+
try {
|
|
42
|
+
const response = await fetch(`https://api.telegram.org/bot${botToken}/getWebhookInfo`);
|
|
43
|
+
const data = (await response.json()) as {
|
|
44
|
+
ok: boolean;
|
|
45
|
+
result?: { url: string; has_custom_certificate: boolean; pending_update_count: number };
|
|
46
|
+
description?: string;
|
|
47
|
+
};
|
|
48
|
+
if (data.ok && data.result) {
|
|
49
|
+
return {
|
|
50
|
+
ok: true,
|
|
51
|
+
url: data.result.url,
|
|
52
|
+
has_custom_certificate: data.result.has_custom_certificate,
|
|
53
|
+
pending_update_count: data.result.pending_update_count,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return { ok: false, error: data.description };
|
|
57
|
+
} catch (err) {
|
|
58
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deploy command types — options and result interfaces for the deploy
|
|
3
|
+
* command group (all, workers, worker, dashboard).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Options for a single worker deployment.
|
|
8
|
+
*/
|
|
9
|
+
export interface DeployOptions {
|
|
10
|
+
/** Cloudflare environment (e.g. "production", "staging"). */
|
|
11
|
+
env?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Result of deploying a single worker.
|
|
16
|
+
*/
|
|
17
|
+
export interface DeployResult {
|
|
18
|
+
/** Worker name as defined in wrangler.jsonc. */
|
|
19
|
+
worker: string;
|
|
20
|
+
/** Deployed URL extracted from wrangler output, if available. */
|
|
21
|
+
url?: string;
|
|
22
|
+
/** Whether the deployment succeeded. */
|
|
23
|
+
success: boolean;
|
|
24
|
+
/** Error message when success is false. */
|
|
25
|
+
error?: string;
|
|
26
|
+
/** Bundle size (e.g., "7102.32 KiB") */
|
|
27
|
+
size?: string;
|
|
28
|
+
/** Worker startup time (e.g., "37 ms") */
|
|
29
|
+
startupTime?: string;
|
|
30
|
+
/** Version ID */
|
|
31
|
+
versionId?: string;
|
|
32
|
+
/** Raw wrangler output (first non-empty line, for debugging) */
|
|
33
|
+
rawOutput?: string;
|
|
34
|
+
}
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Unit tests for the dev command.
|
|
4
|
+
*
|
|
5
|
+
* Stubs ConfigService and CloudflareService prototypes to verify the dev
|
|
6
|
+
* command logic in isolation. Uses Commander's exitOverride to suppress
|
|
7
|
+
* process exits during test runs.
|
|
8
|
+
*
|
|
9
|
+
* Subcommand actions are async — we use parseAsync() and then inspect mocks
|
|
10
|
+
* to confirm correct service calls.
|
|
11
|
+
*/
|
|
12
|
+
import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test";
|
|
13
|
+
import { Command } from "commander";
|
|
14
|
+
import { ConfigService } from "../../services/config/config-service.js";
|
|
15
|
+
import { CloudflareService } from "../../services/cloudflare/cloudflare-service.js";
|
|
16
|
+
import { PrerequisitesService } from "../../services/prerequisites/index.js";
|
|
17
|
+
import { DockerService } from "../../services/docker/index.js";
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Stub variables — reassigned in beforeEach
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
let devMock: ReturnType<typeof mock>;
|
|
24
|
+
let loadMock: ReturnType<typeof mock>;
|
|
25
|
+
let listWorkersMock: ReturnType<typeof mock>;
|
|
26
|
+
let listEnabledWorkersMock: ReturnType<typeof mock>;
|
|
27
|
+
let getWorkerMock: ReturnType<typeof mock>;
|
|
28
|
+
let validateMock: ReturnType<typeof mock>;
|
|
29
|
+
let getDevRuntimeMock: ReturnType<typeof mock>;
|
|
30
|
+
let checkWranglerMock: ReturnType<typeof mock>;
|
|
31
|
+
let dockerCheckMock: ReturnType<typeof mock>;
|
|
32
|
+
let composeExistsMock: ReturnType<typeof mock>;
|
|
33
|
+
|
|
34
|
+
// Preserve originals so we can restore them after tests
|
|
35
|
+
const origLoad = ConfigService.prototype
|
|
36
|
+
.load as typeof ConfigService.prototype.load;
|
|
37
|
+
const origListWorkers = ConfigService.prototype
|
|
38
|
+
.listWorkers as typeof ConfigService.prototype.listWorkers;
|
|
39
|
+
const origListEnabled = ConfigService.prototype
|
|
40
|
+
.listEnabledWorkers as typeof ConfigService.prototype.listEnabledWorkers;
|
|
41
|
+
const origGetWorker = ConfigService.prototype
|
|
42
|
+
.getWorker as typeof ConfigService.prototype.getWorker;
|
|
43
|
+
const origValidate = ConfigService.prototype
|
|
44
|
+
.validate as typeof ConfigService.prototype.validate;
|
|
45
|
+
const origGetDevRuntime = ConfigService.prototype
|
|
46
|
+
.getDevRuntime as typeof ConfigService.prototype.getDevRuntime;
|
|
47
|
+
const origDev = CloudflareService.prototype
|
|
48
|
+
.dev as typeof CloudflareService.prototype.dev;
|
|
49
|
+
const origCheckWrangler = PrerequisitesService.prototype
|
|
50
|
+
.checkWranglerVersion as typeof PrerequisitesService.prototype.checkWranglerVersion;
|
|
51
|
+
const origDockerCheck = DockerService.prototype
|
|
52
|
+
.checkAvailability as typeof DockerService.prototype.checkAvailability;
|
|
53
|
+
const origComposeExists = DockerService.prototype
|
|
54
|
+
.composeFileExists as typeof DockerService.prototype.composeFileExists;
|
|
55
|
+
|
|
56
|
+
// Preserve original Bun globals
|
|
57
|
+
const origBunSpawn = Bun.spawn;
|
|
58
|
+
const origBunFile = Bun.file;
|
|
59
|
+
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
mock.restore();
|
|
62
|
+
|
|
63
|
+
// Reset process.exitCode between tests
|
|
64
|
+
process.exitCode = undefined;
|
|
65
|
+
|
|
66
|
+
// Restore prototypes to originals
|
|
67
|
+
(ConfigService.prototype as Record<string, unknown>).load = origLoad;
|
|
68
|
+
(ConfigService.prototype as Record<string, unknown>).listWorkers =
|
|
69
|
+
origListWorkers;
|
|
70
|
+
(ConfigService.prototype as Record<string, unknown>).listEnabledWorkers =
|
|
71
|
+
origListEnabled;
|
|
72
|
+
(ConfigService.prototype as Record<string, unknown>).getWorker =
|
|
73
|
+
origGetWorker;
|
|
74
|
+
(ConfigService.prototype as Record<string, unknown>).validate = origValidate;
|
|
75
|
+
(CloudflareService.prototype as Record<string, unknown>).dev = origDev;
|
|
76
|
+
|
|
77
|
+
// Stub Bun.spawn to prevent actual process spawning during tests.
|
|
78
|
+
// Uses individual property assignment (not globalThis.Bun = {...}) to
|
|
79
|
+
// avoid "readonly property" errors in Bun 1.3.x.
|
|
80
|
+
(Bun as unknown as Record<string, unknown>).spawn = mock(() => ({
|
|
81
|
+
stdout: new Blob([]),
|
|
82
|
+
stderr: new Blob([]),
|
|
83
|
+
exited: Promise.resolve(0),
|
|
84
|
+
stdin: { write: mock(() => {}), end: mock(() => {}) },
|
|
85
|
+
kill: mock(() => {}),
|
|
86
|
+
}));
|
|
87
|
+
(Bun as unknown as Record<string, unknown>).file = mock((_path: string) => ({
|
|
88
|
+
exists: mock(async () => true),
|
|
89
|
+
}));
|
|
90
|
+
|
|
91
|
+
// Fresh mocks
|
|
92
|
+
devMock = mock(async (_path: string, _port?: number) => ({
|
|
93
|
+
ok: true as const,
|
|
94
|
+
data: { port: _port ?? 8787 },
|
|
95
|
+
}));
|
|
96
|
+
|
|
97
|
+
loadMock = mock(async function () {
|
|
98
|
+
// Use a regular function so `this` refers to the ConfigService instance
|
|
99
|
+
(this as Record<string, unknown>).config = {};
|
|
100
|
+
return {} as Record<string, unknown>;
|
|
101
|
+
});
|
|
102
|
+
listWorkersMock = mock(() => ["hoox", "trade-worker", "d1-worker"]);
|
|
103
|
+
listEnabledWorkersMock = mock(() => ["hoox", "trade-worker", "d1-worker"]);
|
|
104
|
+
getWorkerMock = mock((_name: string) => ({
|
|
105
|
+
enabled: true,
|
|
106
|
+
path: "workers/test-worker",
|
|
107
|
+
}));
|
|
108
|
+
validateMock = mock(() => ({ valid: true, errors: [] }));
|
|
109
|
+
getDevRuntimeMock = mock(() => "native");
|
|
110
|
+
checkWranglerMock = mock(async () => ({
|
|
111
|
+
outdated: false,
|
|
112
|
+
}));
|
|
113
|
+
dockerCheckMock = mock(async () => ({ docker: false, compose: false }));
|
|
114
|
+
composeExistsMock = mock(async () => false);
|
|
115
|
+
|
|
116
|
+
// Install mocks on prototypes
|
|
117
|
+
(ConfigService.prototype as Record<string, unknown>).load = loadMock;
|
|
118
|
+
(ConfigService.prototype as Record<string, unknown>).listWorkers =
|
|
119
|
+
listWorkersMock;
|
|
120
|
+
(ConfigService.prototype as Record<string, unknown>).listEnabledWorkers =
|
|
121
|
+
listEnabledWorkersMock;
|
|
122
|
+
(ConfigService.prototype as Record<string, unknown>).getWorker =
|
|
123
|
+
getWorkerMock;
|
|
124
|
+
(ConfigService.prototype as Record<string, unknown>).validate = validateMock;
|
|
125
|
+
(ConfigService.prototype as Record<string, unknown>).getDevRuntime =
|
|
126
|
+
getDevRuntimeMock;
|
|
127
|
+
(CloudflareService.prototype as Record<string, unknown>).dev = devMock;
|
|
128
|
+
(
|
|
129
|
+
PrerequisitesService.prototype as Record<string, unknown>
|
|
130
|
+
).checkWranglerVersion = checkWranglerMock;
|
|
131
|
+
(DockerService.prototype as Record<string, unknown>).checkAvailability =
|
|
132
|
+
dockerCheckMock;
|
|
133
|
+
(DockerService.prototype as Record<string, unknown>).composeFileExists =
|
|
134
|
+
composeExistsMock;
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
afterEach(() => {
|
|
138
|
+
mock.restore();
|
|
139
|
+
|
|
140
|
+
// Restore originals
|
|
141
|
+
(ConfigService.prototype as Record<string, unknown>).load = origLoad;
|
|
142
|
+
(ConfigService.prototype as Record<string, unknown>).listWorkers =
|
|
143
|
+
origListWorkers;
|
|
144
|
+
(ConfigService.prototype as Record<string, unknown>).listEnabledWorkers =
|
|
145
|
+
origListEnabled;
|
|
146
|
+
(ConfigService.prototype as Record<string, unknown>).getWorker =
|
|
147
|
+
origGetWorker;
|
|
148
|
+
(ConfigService.prototype as Record<string, unknown>).validate = origValidate;
|
|
149
|
+
(ConfigService.prototype as Record<string, unknown>).getDevRuntime =
|
|
150
|
+
origGetDevRuntime;
|
|
151
|
+
(CloudflareService.prototype as Record<string, unknown>).dev = origDev;
|
|
152
|
+
(
|
|
153
|
+
PrerequisitesService.prototype as Record<string, unknown>
|
|
154
|
+
).checkWranglerVersion = origCheckWrangler;
|
|
155
|
+
(DockerService.prototype as Record<string, unknown>).checkAvailability =
|
|
156
|
+
origDockerCheck;
|
|
157
|
+
(DockerService.prototype as Record<string, unknown>).composeFileExists =
|
|
158
|
+
origComposeExists;
|
|
159
|
+
|
|
160
|
+
// Restore Bun globals (individual property restore)
|
|
161
|
+
(Bun as unknown as Record<string, unknown>).spawn = origBunSpawn;
|
|
162
|
+
(Bun as unknown as Record<string, unknown>).file = origBunFile;
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// Dynamic import — load the dev command after stubs are in place
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
async function importDevCommand(): Promise<{
|
|
170
|
+
registerDevCommand: typeof import("./dev-command.js").registerDevCommand;
|
|
171
|
+
}> {
|
|
172
|
+
return import("./dev-command.js");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Create a fresh Commander program with the dev command registered.
|
|
177
|
+
*/
|
|
178
|
+
async function createProgram(): Promise<Command> {
|
|
179
|
+
const { registerDevCommand } = await importDevCommand();
|
|
180
|
+
const program = new Command().name("hoox-test").exitOverride(() => {
|
|
181
|
+
// Suppress Commander's own exit during tests
|
|
182
|
+
});
|
|
183
|
+
registerDevCommand(program);
|
|
184
|
+
return program;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Make devMock return a failure for all subsequent calls. */
|
|
188
|
+
function makeDevFail(error: string): void {
|
|
189
|
+
devMock = mock(async () => ({ ok: false as const, error }));
|
|
190
|
+
(CloudflareService.prototype as Record<string, unknown>).dev = devMock;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
// Tests
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
describe("registerDevCommand", () => {
|
|
198
|
+
// -- Command registration --------------------------------------------------
|
|
199
|
+
|
|
200
|
+
it("registers 'dev' as a command on the program", async () => {
|
|
201
|
+
const program = await createProgram();
|
|
202
|
+
const devCmd = program.commands.find((c) => c.name() === "dev");
|
|
203
|
+
expect(devCmd).toBeDefined();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("registers 'dev start' subcommand", async () => {
|
|
207
|
+
const program = await createProgram();
|
|
208
|
+
const devCmd = program.commands.find((c) => c.name() === "dev")!;
|
|
209
|
+
const startCmd = devCmd.commands.find((c) => c.name() === "start");
|
|
210
|
+
expect(startCmd).toBeDefined();
|
|
211
|
+
expect(startCmd!.description()).toContain("wrangler dev");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("registers 'dev worker <name>' subcommand with argument", async () => {
|
|
215
|
+
const program = await createProgram();
|
|
216
|
+
const devCmd = program.commands.find((c) => c.name() === "dev")!;
|
|
217
|
+
const workerCmd = devCmd.commands.find((c) => c.name() === "worker");
|
|
218
|
+
expect(workerCmd).toBeDefined();
|
|
219
|
+
expect(workerCmd!.description()).toContain("wrangler dev");
|
|
220
|
+
const args = workerCmd!.registeredArguments;
|
|
221
|
+
expect(args.some((a) => a.name() === "name")).toBe(true);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("registers 'dev dashboard' subcommand", async () => {
|
|
225
|
+
const program = await createProgram();
|
|
226
|
+
const devCmd = program.commands.find((c) => c.name() === "dev")!;
|
|
227
|
+
const dashboardCmd = devCmd.commands.find((c) => c.name() === "dashboard");
|
|
228
|
+
expect(dashboardCmd).toBeDefined();
|
|
229
|
+
expect(dashboardCmd!.description()).toContain("dashboard");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// -- dev start -------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
describe("dev start", () => {
|
|
235
|
+
it("handles config validation failure gracefully", async () => {
|
|
236
|
+
validateMock = mock(() => ({
|
|
237
|
+
valid: false,
|
|
238
|
+
errors: ["global.cloudflare_account_id is required"],
|
|
239
|
+
}));
|
|
240
|
+
(ConfigService.prototype as Record<string, unknown>).validate =
|
|
241
|
+
validateMock;
|
|
242
|
+
|
|
243
|
+
const program = await createProgram();
|
|
244
|
+
await program.parseAsync(["dev", "start"], { from: "user" });
|
|
245
|
+
|
|
246
|
+
expect(process.exitCode).toBe(2); // INVALID_USAGE
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("handles no enabled workers gracefully", async () => {
|
|
250
|
+
listEnabledWorkersMock = mock(() => []);
|
|
251
|
+
(ConfigService.prototype as Record<string, unknown>).listEnabledWorkers =
|
|
252
|
+
listEnabledWorkersMock;
|
|
253
|
+
|
|
254
|
+
const program = await createProgram();
|
|
255
|
+
await program.parseAsync(["dev", "start"], { from: "user" });
|
|
256
|
+
|
|
257
|
+
expect(process.exitCode).toBe(2); // INVALID_USAGE
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("starts all enabled workers on assigned ports", async () => {
|
|
261
|
+
listEnabledWorkersMock = mock(() => ["hoox", "trade-worker"]);
|
|
262
|
+
(ConfigService.prototype as Record<string, unknown>).listEnabledWorkers =
|
|
263
|
+
listEnabledWorkersMock;
|
|
264
|
+
|
|
265
|
+
const program = await createProgram();
|
|
266
|
+
await program.parseAsync(["dev", "start"], { from: "user" });
|
|
267
|
+
|
|
268
|
+
// Both workers should be started via CloudflareService.dev()
|
|
269
|
+
expect(devMock).toHaveBeenCalledTimes(2);
|
|
270
|
+
const calls = (
|
|
271
|
+
devMock as unknown as { mock: { calls: Array<unknown[]> } }
|
|
272
|
+
).mock.calls;
|
|
273
|
+
// hoox → port 8787, trade-worker → port 8788
|
|
274
|
+
expect(calls[0][1]).toBe(8787);
|
|
275
|
+
expect(calls[1][1]).toBe(8788);
|
|
276
|
+
// exitCode remains unset since the action ran successfully
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// -- dev worker <name> -----------------------------------------------------
|
|
281
|
+
|
|
282
|
+
describe("dev worker <name>", () => {
|
|
283
|
+
it("starts the specified worker with default port", async () => {
|
|
284
|
+
const program = await createProgram();
|
|
285
|
+
await program.parseAsync(["dev", "worker", "hoox"], { from: "user" });
|
|
286
|
+
|
|
287
|
+
expect(devMock).toHaveBeenCalledTimes(1);
|
|
288
|
+
const calls = (
|
|
289
|
+
devMock as unknown as { mock: { calls: Array<unknown[]> } }
|
|
290
|
+
).mock.calls;
|
|
291
|
+
expect(calls[0][0]).toContain("test-worker");
|
|
292
|
+
expect(calls[0][1]).toBe(8787);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("passes --port to dev service", async () => {
|
|
296
|
+
const program = await createProgram();
|
|
297
|
+
await program.parseAsync(["dev", "worker", "hoox", "--port", "3000"], {
|
|
298
|
+
from: "user",
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const calls = (
|
|
302
|
+
devMock as unknown as { mock: { calls: Array<unknown[]> } }
|
|
303
|
+
).mock.calls;
|
|
304
|
+
expect(calls[0][1]).toBe(3000);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("handles unknown worker name", async () => {
|
|
308
|
+
getWorkerMock = mock(() => undefined);
|
|
309
|
+
(ConfigService.prototype as Record<string, unknown>).getWorker =
|
|
310
|
+
getWorkerMock;
|
|
311
|
+
|
|
312
|
+
const program = await createProgram();
|
|
313
|
+
await program.parseAsync(["dev", "worker", "nonexistent"], {
|
|
314
|
+
from: "user",
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
expect(devMock).toHaveBeenCalledTimes(0);
|
|
318
|
+
expect(process.exitCode).toBe(2); // INVALID_USAGE
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("handles disabled worker", async () => {
|
|
322
|
+
getWorkerMock = mock(() => ({
|
|
323
|
+
enabled: false,
|
|
324
|
+
path: "workers/disabled-worker",
|
|
325
|
+
}));
|
|
326
|
+
(ConfigService.prototype as Record<string, unknown>).getWorker =
|
|
327
|
+
getWorkerMock;
|
|
328
|
+
|
|
329
|
+
const program = await createProgram();
|
|
330
|
+
await program.parseAsync(["dev", "worker", "disabled-worker"], {
|
|
331
|
+
from: "user",
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
expect(devMock).toHaveBeenCalledTimes(0);
|
|
335
|
+
expect(process.exitCode).toBe(2); // INVALID_USAGE
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("handles dev failure (sets exitCode to 1)", async () => {
|
|
339
|
+
makeDevFail("wrangler not found");
|
|
340
|
+
getWorkerMock = mock(() => ({
|
|
341
|
+
enabled: true,
|
|
342
|
+
path: "workers/hoox",
|
|
343
|
+
}));
|
|
344
|
+
(ConfigService.prototype as Record<string, unknown>).getWorker =
|
|
345
|
+
getWorkerMock;
|
|
346
|
+
|
|
347
|
+
const program = await createProgram();
|
|
348
|
+
await program.parseAsync(["dev", "worker", "hoox"], { from: "user" });
|
|
349
|
+
|
|
350
|
+
expect(process.exitCode).toBe(1); // ERROR
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// -- dev dashboard ---------------------------------------------------------
|
|
355
|
+
|
|
356
|
+
describe("dev dashboard", () => {
|
|
357
|
+
it("handles missing dashboard directory", async () => {
|
|
358
|
+
// Override Bun.file for this test to say dashboard doesn't exist
|
|
359
|
+
(Bun as unknown as Record<string, unknown>).file = mock((_p: string) => ({
|
|
360
|
+
exists: mock(async () => false),
|
|
361
|
+
}));
|
|
362
|
+
|
|
363
|
+
const program = await createProgram();
|
|
364
|
+
await program.parseAsync(["dev", "dashboard"], { from: "user" });
|
|
365
|
+
|
|
366
|
+
expect(process.exitCode).toBe(2); // INVALID_USAGE
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// -- config loading errors ------------------------------------------------
|
|
371
|
+
|
|
372
|
+
it("handles config load failure gracefully", async () => {
|
|
373
|
+
loadMock = mock(async () => {
|
|
374
|
+
throw new Error("Config file not found");
|
|
375
|
+
});
|
|
376
|
+
(ConfigService.prototype as Record<string, unknown>).load = loadMock;
|
|
377
|
+
|
|
378
|
+
const program = await createProgram();
|
|
379
|
+
await program.parseAsync(["dev", "start"], { from: "user" });
|
|
380
|
+
|
|
381
|
+
expect(process.exitCode).toBe(1);
|
|
382
|
+
});
|
|
383
|
+
});
|