@jango-blockchained/hoox-cli 0.5.1 → 0.5.6

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 (106) hide show
  1. package/bin/hoox.js +3 -4
  2. package/dist/index.js +32545 -0
  3. package/package.json +10 -15
  4. package/src/commands/check/check-command.test.ts +0 -476
  5. package/src/commands/check/check-command.ts +0 -1144
  6. package/src/commands/check/index.ts +0 -10
  7. package/src/commands/check/prerequisites-command.test.ts +0 -19
  8. package/src/commands/check/prerequisites-command.ts +0 -96
  9. package/src/commands/check/types.ts +0 -103
  10. package/src/commands/clone/clone-command.test.ts +0 -442
  11. package/src/commands/clone/clone-command.ts +0 -431
  12. package/src/commands/clone/index.ts +0 -1
  13. package/src/commands/config/config-command.test.ts +0 -583
  14. package/src/commands/config/config-command.ts +0 -897
  15. package/src/commands/config/env-command.test.ts +0 -43
  16. package/src/commands/config/env-command.ts +0 -295
  17. package/src/commands/config/index.ts +0 -3
  18. package/src/commands/config/kv-command.test.ts +0 -14
  19. package/src/commands/config/kv-command.ts +0 -290
  20. package/src/commands/dashboard/dashboard-command.test.ts +0 -47
  21. package/src/commands/dashboard/dashboard-command.ts +0 -128
  22. package/src/commands/dashboard/index.ts +0 -1
  23. package/src/commands/db/db-command.test.ts +0 -21
  24. package/src/commands/db/db-command.ts +0 -312
  25. package/src/commands/db/index.ts +0 -1
  26. package/src/commands/deploy/deploy-command.test.ts +0 -304
  27. package/src/commands/deploy/deploy-command.ts +0 -1135
  28. package/src/commands/deploy/index.ts +0 -2
  29. package/src/commands/deploy/telegram-service.ts +0 -76
  30. package/src/commands/deploy/types.ts +0 -34
  31. package/src/commands/dev/dev-command.test.ts +0 -383
  32. package/src/commands/dev/dev-command.ts +0 -398
  33. package/src/commands/dev/index.ts +0 -1
  34. package/src/commands/infra/index.ts +0 -5
  35. package/src/commands/infra/infra-command.test.ts +0 -719
  36. package/src/commands/infra/infra-command.ts +0 -974
  37. package/src/commands/infra/types.ts +0 -23
  38. package/src/commands/init/__tests__/cli-provisioner.test.ts +0 -17
  39. package/src/commands/init/cli-provisioner.ts +0 -71
  40. package/src/commands/init/index.ts +0 -1
  41. package/src/commands/init/init-command.test.ts +0 -876
  42. package/src/commands/init/init-command.ts +0 -593
  43. package/src/commands/init/types.ts +0 -23
  44. package/src/commands/logs/index.ts +0 -1
  45. package/src/commands/logs/logs-command.test.ts +0 -294
  46. package/src/commands/logs/logs-command.ts +0 -452
  47. package/src/commands/monitor/index.ts +0 -2
  48. package/src/commands/monitor/monitor-command.test.ts +0 -263
  49. package/src/commands/monitor/monitor-command.ts +0 -269
  50. package/src/commands/monitor/monitor-service.ts +0 -65
  51. package/src/commands/monitor/types.ts +0 -13
  52. package/src/commands/repair/index.ts +0 -2
  53. package/src/commands/repair/repair-command.test.ts +0 -224
  54. package/src/commands/repair/repair-command.ts +0 -220
  55. package/src/commands/repair/repair-service.ts +0 -138
  56. package/src/commands/repair/types.ts +0 -13
  57. package/src/commands/test/index.ts +0 -2
  58. package/src/commands/test/test-command.test.ts +0 -319
  59. package/src/commands/test/test-command.ts +0 -406
  60. package/src/commands/tui/index.ts +0 -1
  61. package/src/commands/tui/tui-command.ts +0 -87
  62. package/src/commands/update/index.ts +0 -1
  63. package/src/commands/update/update-command.ts +0 -168
  64. package/src/commands/waf/index.ts +0 -2
  65. package/src/commands/waf/types.ts +0 -48
  66. package/src/commands/waf/waf-command.test.ts +0 -506
  67. package/src/commands/waf/waf-command.ts +0 -548
  68. package/src/index.ts +0 -269
  69. package/src/services/cloudflare/cloudflare-service.test.ts +0 -658
  70. package/src/services/cloudflare/cloudflare-service.ts +0 -433
  71. package/src/services/cloudflare/index.ts +0 -2
  72. package/src/services/cloudflare/types.ts +0 -30
  73. package/src/services/config/config-service.test.ts +0 -395
  74. package/src/services/config/config-service.ts +0 -207
  75. package/src/services/config/index.ts +0 -9
  76. package/src/services/config/types.ts +0 -66
  77. package/src/services/db/db-service.test.ts +0 -51
  78. package/src/services/db/db-service.ts +0 -131
  79. package/src/services/db/index.ts +0 -1
  80. package/src/services/docker/docker-service.ts +0 -155
  81. package/src/services/docker/index.ts +0 -1
  82. package/src/services/env/env-service.test.ts +0 -236
  83. package/src/services/env/env-service.ts +0 -386
  84. package/src/services/env/index.ts +0 -1
  85. package/src/services/kv/index.ts +0 -1
  86. package/src/services/kv/kv-sync-service.test.ts +0 -38
  87. package/src/services/kv/kv-sync-service.ts +0 -234
  88. package/src/services/prerequisites/index.ts +0 -1
  89. package/src/services/prerequisites/prerequisites-service.test.ts +0 -89
  90. package/src/services/prerequisites/prerequisites-service.ts +0 -377
  91. package/src/services/prerequisites/types.ts +0 -48
  92. package/src/services/secrets/index.ts +0 -11
  93. package/src/services/secrets/secrets-service.test.ts +0 -495
  94. package/src/services/secrets/secrets-service.ts +0 -287
  95. package/src/services/secrets/types.ts +0 -50
  96. package/src/services/update/index.ts +0 -2
  97. package/src/services/update/update-service.test.ts +0 -76
  98. package/src/services/update/update-service.ts +0 -193
  99. package/src/ui/banner.ts +0 -141
  100. package/src/ui/index.ts +0 -8
  101. package/src/ui/menu.ts +0 -490
  102. package/src/utils/errors.test.ts +0 -69
  103. package/src/utils/errors.ts +0 -23
  104. package/src/utils/formatters.test.ts +0 -180
  105. package/src/utils/formatters.ts +0 -269
  106. package/src/utils/theme.ts +0 -94
@@ -1,876 +0,0 @@
1
- /**
2
- * Unit tests for `hoox init` command.
3
- *
4
- * Mocks:
5
- * - @clack/prompts → controllable prompt responses
6
- * - CloudflareService → simulated wrangler output
7
- * - Bun.write → captured file writes
8
- * - process.exit → captured exit codes
9
- */
10
-
11
- // @ts-nocheck — spyOn on module namespace requires type bypasses for strict signatures
12
- import {
13
- afterEach,
14
- beforeEach,
15
- describe,
16
- expect,
17
- it,
18
- mock,
19
- spyOn,
20
- } from "bun:test";
21
- import { runInitCommand } from "./init-command.js";
22
- import type { InitOptions } from "./types.js";
23
- import { ExitCode } from "../../utils/errors.js";
24
- import { CloudflareService } from "../../services/cloudflare/cloudflare-service.js";
25
-
26
- // ---------------------------------------------------------------------------
27
- // Mock types / helpers
28
- // ---------------------------------------------------------------------------
29
-
30
- /** Store captured calls so tests can assert on behaviour. */
31
- interface CapturedCalls {
32
- intro: string[];
33
- outro: string[];
34
- note: { title: string; content: string }[];
35
- passwordMessages: string[];
36
- textMessages: string[];
37
- multiselectMessages: string[];
38
- selectMessages: string[];
39
- confirmMessages: string[];
40
- cancelMessages: string[];
41
- logInfo: string[];
42
- logStep: string[];
43
- logSuccess: string[];
44
- logWarn: string[];
45
- logError: string[];
46
- /** Written files: path → content */
47
- writes: Record<string, string>;
48
- }
49
-
50
- function makeCapture(): CapturedCalls {
51
- return {
52
- intro: [],
53
- outro: [],
54
- note: [],
55
- passwordMessages: [],
56
- textMessages: [],
57
- multiselectMessages: [],
58
- selectMessages: [],
59
- confirmMessages: [],
60
- cancelMessages: [],
61
- logInfo: [],
62
- logStep: [],
63
- logSuccess: [],
64
- logWarn: [],
65
- logError: [],
66
- writes: {},
67
- };
68
- }
69
-
70
- let captured: CapturedCalls;
71
-
72
- /** Configurable responses for each prompt type. */
73
- interface PromptResponses {
74
- password?: string | symbol;
75
- text?: string | symbol;
76
- multiselect?: string[] | symbol;
77
- select?: string | symbol;
78
- /** Confirm responses returned in order (call-by-call). Defaults are `[true, false, false]`. */
79
- confirmSequence?: boolean[];
80
- /** Group responses keyed by field name */
81
- group?: Record<string, string>;
82
- }
83
-
84
- const defaultResponses: Required<PromptResponses> = {
85
- password: "test-token",
86
- text: "test-account",
87
- multiselect: [],
88
- select: "minimal",
89
- confirmSequence: [true, false, false], // risk → provisioning → deploy
90
- group: {},
91
- };
92
-
93
- let responses: PromptResponses = { ...defaultResponses };
94
-
95
- /** Control how many times each prompt type has been called. */
96
- interface CallCounters {
97
- password: number;
98
- text: number;
99
- multiselect: number;
100
- select: number;
101
- confirm: number;
102
- }
103
-
104
- let counters: CallCounters = {
105
- password: 0,
106
- text: 0,
107
- multiselect: 0,
108
- select: 0,
109
- confirm: 0,
110
- };
111
-
112
- function resetCounters(): void {
113
- counters = { password: 0, text: 0, multiselect: 0, select: 0, confirm: 0 };
114
- }
115
-
116
- /** Whether to simulate cancellation on next check. */
117
- let simulateCancel = false;
118
-
119
- /** Custom password responder — set per-test for sequenced token responses. */
120
- let customPasswordResponder: ((msg: string) => string | symbol) | null = null;
121
-
122
- // ---------------------------------------------------------------------------
123
- // Mock: CloudflareService (prototype based — no mock.module, no leakage)
124
- // ---------------------------------------------------------------------------
125
-
126
- const origWhoami = CloudflareService.prototype.whoami;
127
-
128
- const mockWhoami = mock(
129
- async (): Promise<
130
- { ok: true; value: string } | { ok: false; error: string }
131
- > => ({
132
- ok: true,
133
- value: "user@example.com",
134
- })
135
- );
136
-
137
- // ---------------------------------------------------------------------------
138
- // Mock: @clack/prompts (import namespace + spyOn, not mock.module)
139
- // ---------------------------------------------------------------------------
140
-
141
- import * as clack from "@clack/prompts";
142
-
143
- function installClackSpies(): void {
144
- spyOn(clack, "intro").mockImplementation((msg: string) => {
145
- captured.intro.push(msg);
146
- });
147
- spyOn(clack, "outro").mockImplementation((msg: string) => {
148
- captured.outro.push(msg);
149
- });
150
- spyOn(clack, "note").mockImplementation((content: string, title?: string) => {
151
- captured.note.push({ title: title ?? "", content });
152
- });
153
- spyOn(clack, "password").mockImplementation(
154
- async (opts: {
155
- message: string;
156
- validate?: (v: string) => string | void;
157
- }) => {
158
- captured.passwordMessages.push(opts.message);
159
- counters.password++;
160
- if (simulateCancel) return Symbol.for("clack.cancel");
161
- if (customPasswordResponder) return customPasswordResponder(opts.message);
162
- return responses.password ?? defaultResponses.password;
163
- }
164
- );
165
- spyOn(clack, "text").mockImplementation(
166
- async (opts: {
167
- message: string;
168
- placeholder?: string;
169
- defaultValue?: string;
170
- validate?: (v: string) => string | void;
171
- }) => {
172
- captured.textMessages.push(opts.message);
173
- counters.text++;
174
- return simulateCancel
175
- ? Symbol.for("clack.cancel")
176
- : (responses.text ?? defaultResponses.text);
177
- }
178
- );
179
- spyOn(clack, "multiselect").mockImplementation(
180
- async (opts: {
181
- message: string;
182
- options: { value: string; label: string; hint?: string }[];
183
- required?: boolean;
184
- }) => {
185
- captured.multiselectMessages.push(opts.message);
186
- counters.multiselect++;
187
- return simulateCancel
188
- ? Symbol.for("clack.cancel")
189
- : (responses.multiselect ?? defaultResponses.multiselect);
190
- }
191
- );
192
- spyOn(clack, "select").mockImplementation(
193
- async (opts: {
194
- message: string;
195
- options: { value: string; label: string; hint?: string }[];
196
- }) => {
197
- captured.selectMessages.push(opts.message);
198
- counters.select++;
199
- return simulateCancel
200
- ? Symbol.for("clack.cancel")
201
- : (responses.select ?? defaultResponses.select);
202
- }
203
- );
204
- spyOn(clack, "confirm").mockImplementation(
205
- async (opts: { message: string; initialValue?: boolean }) => {
206
- captured.confirmMessages.push(opts.message);
207
- const idx = counters.confirm++;
208
- const seq = responses.confirmSequence ?? defaultResponses.confirmSequence;
209
- const val = idx < seq.length ? seq[idx] : seq[seq.length - 1];
210
- if (simulateCancel) return Symbol.for("clack.cancel");
211
- return val;
212
- }
213
- );
214
- spyOn(clack, "group").mockImplementation(
215
- async (
216
- fields: Record<string, () => Promise<string | symbol>>,
217
- groupOpts?: { onCancel?: () => void }
218
- ) => {
219
- const results: Record<string, string> = {};
220
- for (const [key, fn] of Object.entries(fields)) {
221
- if (simulateCancel) {
222
- if (groupOpts?.onCancel) groupOpts.onCancel();
223
- return Symbol.for("clack.cancel");
224
- }
225
- const val = await fn();
226
- results[key] =
227
- responses.group?.[key] !== undefined
228
- ? responses.group[key]
229
- : typeof val === "string"
230
- ? val
231
- : "";
232
- }
233
- return results;
234
- }
235
- );
236
- spyOn(clack, "isCancel").mockImplementation((value: unknown) => {
237
- return (
238
- simulateCancel ||
239
- (typeof value === "symbol" &&
240
- Symbol.keyFor(value as symbol) === "clack.cancel")
241
- );
242
- });
243
- spyOn(clack, "cancel").mockImplementation((msg: string) => {
244
- captured.cancelMessages.push(msg);
245
- });
246
- spyOn(clack.log, "info").mockImplementation((msg: string) =>
247
- captured.logInfo.push(msg)
248
- );
249
- spyOn(clack.log, "step").mockImplementation((msg: string) =>
250
- captured.logStep.push(msg)
251
- );
252
- spyOn(clack.log, "success").mockImplementation((msg: string) =>
253
- captured.logSuccess.push(msg)
254
- );
255
- spyOn(clack.log, "warn").mockImplementation((msg: string) =>
256
- captured.logWarn.push(msg)
257
- );
258
- spyOn(clack.log, "error").mockImplementation((msg: string) =>
259
- captured.logError.push(msg)
260
- );
261
- }
262
-
263
- // ---------------------------------------------------------------------------
264
- // Mock: Bun.write & Bun.file
265
- // ---------------------------------------------------------------------------
266
-
267
- const origWhoamiPrototype = CloudflareService.prototype.whoami;
268
-
269
- beforeEach(() => {
270
- captured = makeCapture();
271
- responses = { ...defaultResponses };
272
- resetCounters();
273
- simulateCancel = false;
274
- customPasswordResponder = null;
275
- mockWhoami.mockImplementation(async () => ({
276
- ok: true,
277
- data: "user@example.com",
278
- }));
279
-
280
- // Mock: CloudflareService prototype
281
- CloudflareService.prototype.whoami = mockWhoami;
282
-
283
- // Mock: @clack/prompts via spyOn (scoped, restorable)
284
- installClackSpies();
285
-
286
- // Mock Bun.write to capture all file writes
287
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
288
- spyOn(Bun, "write" as any).mockImplementation(
289
- async (path: string | URL, content: string | Uint8Array) => {
290
- captured.writes[String(path)] =
291
- typeof content === "string"
292
- ? content
293
- : new TextDecoder().decode(content);
294
- return typeof content === "string" ? content.length : content.byteLength;
295
- }
296
- );
297
-
298
- // Mock Bun.file to simulate filesystem (no existing wrangler.jsonc by default)
299
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
300
- spyOn(Bun, "file" as any).mockImplementation(
301
- (_path: string | URL) =>
302
- ({
303
- exists: async () => false,
304
- text: async () => "",
305
- arrayBuffer: async () => new ArrayBuffer(0),
306
- json: async () => ({}),
307
- size: 0,
308
- name: String(_path),
309
- lastModified: 0,
310
- slice: () => new Blob(),
311
- stream: () => new ReadableStream(),
312
- type: "",
313
- }) as unknown as Bun.BunFile
314
- );
315
-
316
- // Mock process.exit — no-op to prevent actual test termination
317
- spyOn(process, "exit").mockImplementation(((_code?: number) => {
318
- // no-op: prevent actual exit
319
- }) as never);
320
- });
321
-
322
- afterEach(() => {
323
- mock.restore();
324
- CloudflareService.prototype.whoami = origWhoamiPrototype;
325
- });
326
-
327
- // ---------------------------------------------------------------------------
328
- // Tests
329
- // ---------------------------------------------------------------------------
330
-
331
- describe("init command", () => {
332
- // ------------------------------------------------------------------
333
- // Interactive flow
334
- // ------------------------------------------------------------------
335
-
336
- describe("interactive flow", () => {
337
- it("shows intro and outro messages", async () => {
338
- responses = {
339
- password: "valid-token",
340
- text: "test-account-id",
341
- multiselect: [],
342
- select: "minimal",
343
- confirmSequence: [true, false, false],
344
- };
345
-
346
- await runInitCommand({}, { json: false, quiet: true }, false);
347
-
348
- expect(captured.intro.length).toBeGreaterThan(0);
349
- expect(captured.intro.some((m) => m.includes("Hoox Setup Wizard"))).toBe(
350
- true
351
- );
352
- expect(captured.outro.some((m) => m.includes("Setup complete"))).toBe(
353
- true
354
- );
355
- });
356
-
357
- it("collects Cloudflare API token with validation", async () => {
358
- responses = {
359
- password: "valid-token",
360
- text: "test-account-id",
361
- multiselect: [],
362
- select: "minimal",
363
- confirmSequence: [true, false, false],
364
- };
365
-
366
- await runInitCommand({}, { json: false, quiet: true }, false);
367
-
368
- expect(captured.passwordMessages).toContain("Cloudflare API token:");
369
- expect(mockWhoami).toHaveBeenCalled();
370
- });
371
-
372
- it("retries on invalid token (validation error)", async () => {
373
- let callCount = 0;
374
- mockWhoami.mockImplementation(async () => {
375
- callCount++;
376
- if (callCount === 1) {
377
- return { ok: false as const, error: "Invalid credentials" };
378
- }
379
- return { ok: true as const, data: "user@example.com" };
380
- });
381
-
382
- // First password call returns bad token, second returns good token
383
- let pwdCount = 0;
384
- customPasswordResponder = (_msg: string) => {
385
- pwdCount++;
386
- return pwdCount === 1 ? "bad-token" : "good-token";
387
- };
388
-
389
- responses = {
390
- password: "",
391
- text: "test-account-id",
392
- multiselect: [],
393
- select: "minimal",
394
- confirmSequence: [true, false, false],
395
- };
396
-
397
- await runInitCommand({}, { json: false, quiet: true }, false);
398
-
399
- // Should have called password at least twice (first failed, retry)
400
- expect(pwdCount).toBeGreaterThanOrEqual(2);
401
- expect(captured.logError.length).toBeGreaterThan(0);
402
- });
403
-
404
- it("collects account ID with default from existing wrangler.jsonc", async () => {
405
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
406
- const fileSpy = spyOn(Bun, "file" as any).mockImplementation(
407
- (path: string) => {
408
- if (path === "wrangler.jsonc") {
409
- return {
410
- exists: async () => true,
411
- text: async () =>
412
- '{"global":{"cloudflare_account_id":"existing-account-id-123"}}',
413
- arrayBuffer: async () => new ArrayBuffer(0),
414
- json: async () => ({}),
415
- size: 0,
416
- name: "",
417
- lastModified: 0,
418
- slice: () => new Blob(),
419
- stream: () => new ReadableStream(),
420
- type: "",
421
- } as unknown as Bun.BunFile;
422
- }
423
- return {
424
- exists: async () => false,
425
- text: async () => "",
426
- arrayBuffer: async () => new ArrayBuffer(0),
427
- json: async () => ({}),
428
- size: 0,
429
- name: "",
430
- lastModified: 0,
431
- slice: () => new Blob(),
432
- stream: () => new ReadableStream(),
433
- type: "",
434
- } as unknown as Bun.BunFile;
435
- }
436
- );
437
-
438
- responses = {
439
- password: "valid-token",
440
- text: "existing-account-id-123",
441
- multiselect: [],
442
- select: "minimal",
443
- confirmSequence: [true, false, false],
444
- };
445
-
446
- await runInitCommand({}, { json: false, quiet: true }, false);
447
-
448
- expect(captured.textMessages.some((m) => m.includes("Account ID"))).toBe(
449
- true
450
- );
451
-
452
- fileSpy.mockRestore();
453
- });
454
-
455
- it("collects integrations via multiselect", async () => {
456
- responses = {
457
- password: "valid-token",
458
- text: "test-account-id",
459
- multiselect: ["binance", "telegram"],
460
- select: "full", // full preset includes integrations
461
- confirmSequence: [true, false, false],
462
- group: {
463
- BINANCE_API_KEY: "binance-key-123",
464
- BINANCE_API_SECRET: "binance-secret-123",
465
- TELEGRAM_BOT_TOKEN: "tg-bot-token-123",
466
- },
467
- };
468
-
469
- await runInitCommand({}, { json: false, quiet: true }, false);
470
-
471
- expect(
472
- captured.selectMessages.some((m) =>
473
- m.includes("Select a worker preset")
474
- )
475
- ).toBe(true);
476
- });
477
-
478
- it("collects per-integration secrets for selected integrations", async () => {
479
- responses = {
480
- password: "valid-token",
481
- text: "test-account-id",
482
- multiselect: ["telegram"],
483
- select: "full",
484
- confirmSequence: [true, false, false],
485
- group: {
486
- TELEGRAM_BOT_TOKEN: "tg-token-abc",
487
- },
488
- };
489
-
490
- await runInitCommand({}, { json: false, quiet: true }, false);
491
-
492
- // Password prompts should include Telegram Bot Token
493
- expect(
494
- captured.passwordMessages.some((m) => m.includes("Telegram Bot Token"))
495
- ).toBe(true);
496
- });
497
- });
498
-
499
- // ------------------------------------------------------------------
500
- // Non-interactive flow
501
- // ------------------------------------------------------------------
502
-
503
- describe("non-interactive flow", () => {
504
- it("skips prompts when --token and --account are provided", async () => {
505
- const options: InitOptions = {
506
- token: "cf-token-non-interactive",
507
- account: "cf-account-non-interactive",
508
- secretStore: "ss-id-123",
509
- prefix: "myprefix",
510
- };
511
-
512
- await runInitCommand(options, { json: false, quiet: true }, true);
513
-
514
- // No interactive prompts should have been shown
515
- expect(captured.passwordMessages.length).toBe(0);
516
- expect(captured.textMessages.length).toBe(0);
517
- expect(captured.multiselectMessages.length).toBe(0);
518
-
519
- // Token should have been validated
520
- expect(mockWhoami).toHaveBeenCalled();
521
- });
522
-
523
- it("validates token in non-interactive mode", async () => {
524
- mockWhoami.mockImplementation(async () => ({
525
- ok: false as const,
526
- error: "Bad token",
527
- }));
528
-
529
- // Override process.exit to capture and throw (halts execution)
530
- let exitCode = -1;
531
- process.exit = mock((code?: number) => {
532
- exitCode = code ?? -1;
533
- throw new Error("EXIT");
534
- }) as unknown as typeof process.exit;
535
-
536
- const options: InitOptions = {
537
- token: "bad-token",
538
- account: "abc123def456abc123def456abc123de",
539
- };
540
-
541
- const promise = runInitCommand(
542
- options,
543
- { json: false, quiet: true },
544
- true
545
- );
546
- await promise.catch(() => {
547
- /* expected */
548
- });
549
-
550
- expect(exitCode).toBe(ExitCode.ERROR);
551
- });
552
-
553
- it("writes wrangler.jsonc in non-interactive mode", async () => {
554
- const options: InitOptions = {
555
- token: "cf-token-ni",
556
- account: "cf-account-ni",
557
- secretStore: "ss-ni",
558
- prefix: "ni-prefix",
559
- };
560
-
561
- await runInitCommand(options, { json: false, quiet: true }, true);
562
-
563
- // Verify wrangler.jsonc was written
564
- const workersJsonc = captured.writes["wrangler.jsonc"];
565
- expect(workersJsonc).toBeDefined();
566
- expect(workersJsonc).toContain("cf-token-ni");
567
- expect(workersJsonc).toContain("cf-account-ni");
568
- expect(workersJsonc).toContain("ss-ni");
569
- expect(workersJsonc).toContain("ni-prefix");
570
- expect(workersJsonc).toContain("d1-worker");
571
- expect(workersJsonc).toContain("hoox");
572
- });
573
-
574
- it("uses default prefix 'cryptolinx' when --prefix not provided", async () => {
575
- const options: InitOptions = {
576
- token: "cf-token-def",
577
- account: "cf-account-def",
578
- };
579
-
580
- await runInitCommand(options, { json: false, quiet: true }, true);
581
-
582
- const workersJsonc = captured.writes["wrangler.jsonc"];
583
- expect(workersJsonc).toContain("cryptolinx");
584
- });
585
- });
586
-
587
- // ------------------------------------------------------------------
588
- // wrangler.jsonc content verification
589
- // ------------------------------------------------------------------
590
-
591
- describe("wrangler.jsonc output", () => {
592
- it("includes base workers (d1-worker, hoox, agent-worker, analytics-worker)", async () => {
593
- responses = {
594
- password: "valid-token",
595
- text: "test-account",
596
- multiselect: [],
597
- select: "minimal",
598
- confirmSequence: [true, false, false],
599
- };
600
-
601
- await runInitCommand({}, { json: false, quiet: true }, false);
602
-
603
- const workersJsonc = captured.writes["wrangler.jsonc"];
604
- expect(workersJsonc).toBeDefined();
605
- expect(workersJsonc).toContain('"d1-worker"');
606
- expect(workersJsonc).toContain('"hoox"');
607
- expect(workersJsonc).toContain('"agent-worker"');
608
- expect(workersJsonc).toContain('"analytics-worker"');
609
- });
610
-
611
- it("includes integration workers when selected", async () => {
612
- responses = {
613
- password: "valid-token",
614
- text: "test-account",
615
- multiselect: [],
616
- select: "full",
617
- confirmSequence: [true, false, false],
618
- group: {
619
- BINANCE_API_KEY: "bk",
620
- BINANCE_API_SECRET: "bs",
621
- TELEGRAM_BOT_TOKEN: "tgt",
622
- },
623
- };
624
-
625
- await runInitCommand({}, { json: false, quiet: true }, false);
626
-
627
- const workersJsonc = captured.writes["wrangler.jsonc"];
628
- expect(workersJsonc).toBeDefined();
629
- expect(workersJsonc).toContain('"trade-worker"');
630
- expect(workersJsonc).toContain('"BINANCE_API_KEY"');
631
- expect(workersJsonc).toContain('"BINANCE_API_SECRET"');
632
- expect(workersJsonc).toContain('"telegram-worker"');
633
- expect(workersJsonc).toContain('"TELEGRAM_BOT_TOKEN"');
634
- });
635
-
636
- it("includes AI provider (OpenAI) integration from full preset", async () => {
637
- responses = {
638
- password: "valid-token",
639
- text: "test-account",
640
- multiselect: [],
641
- select: "full",
642
- confirmSequence: [true, false, false],
643
- group: {
644
- AGENT_OPENAI_KEY: "sk-openai-123",
645
- },
646
- };
647
-
648
- await runInitCommand({}, { json: false, quiet: true }, false);
649
-
650
- const workersJsonc = captured.writes["wrangler.jsonc"];
651
- expect(workersJsonc).toBeDefined();
652
- expect(workersJsonc).toContain('"agent-worker"');
653
- expect(workersJsonc).toContain('"AGENT_OPENAI_KEY"');
654
- });
655
-
656
- it("includes wallet integration from full preset", async () => {
657
- responses = {
658
- password: "valid-token",
659
- text: "test-account",
660
- multiselect: [],
661
- select: "full",
662
- confirmSequence: [true, false, false],
663
- group: {
664
- WALLET_MNEMONIC_SECRET: "seed-phrase-789",
665
- },
666
- };
667
-
668
- await runInitCommand({}, { json: false, quiet: true }, false);
669
-
670
- const workersJsonc = captured.writes["wrangler.jsonc"];
671
- expect(workersJsonc).toBeDefined();
672
- expect(workersJsonc).toContain('"web3-wallet-worker"');
673
- expect(workersJsonc).toContain('"WALLET_MNEMONIC_SECRET"');
674
- });
675
-
676
- it("merges exchange secrets into single trade-worker", async () => {
677
- responses = {
678
- password: "valid-token",
679
- text: "test-account",
680
- multiselect: [],
681
- select: "full",
682
- confirmSequence: [true, false, false],
683
- group: {
684
- BINANCE_API_KEY: "bk",
685
- BINANCE_API_SECRET: "bs",
686
- MEXC_API_KEY: "mk",
687
- MEXC_API_SECRET: "ms",
688
- },
689
- };
690
-
691
- await runInitCommand({}, { json: false, quiet: true }, false);
692
-
693
- const workersJsonc = captured.writes["wrangler.jsonc"];
694
- expect(workersJsonc).toBeDefined();
695
- expect(workersJsonc).toContain('"trade-worker"');
696
- expect(workersJsonc).toContain('"BINANCE_API_KEY"');
697
- expect(workersJsonc).toContain('"MEXC_API_KEY"');
698
- // Should only appear once
699
- const tradeWorkerCount =
700
- workersJsonc.match(/"trade-worker"/g)?.length ?? 0;
701
- expect(tradeWorkerCount).toBe(1);
702
- });
703
-
704
- it("includes global config section", async () => {
705
- responses = {
706
- password: "my-cf-token",
707
- text: "my-account-id",
708
- multiselect: [],
709
- select: "minimal",
710
- confirmSequence: [true, false, false],
711
- };
712
-
713
- await runInitCommand({}, { json: false, quiet: true }, false);
714
-
715
- const workersJsonc = captured.writes["wrangler.jsonc"];
716
- expect(workersJsonc).toBeDefined();
717
- expect(workersJsonc).toContain('"global"');
718
- expect(workersJsonc).toContain('"cloudflare_api_token"');
719
- expect(workersJsonc).toContain('"cloudflare_account_id"');
720
- expect(workersJsonc).toContain('"my-account-id"');
721
- });
722
-
723
- it("has JSONC header comment", async () => {
724
- responses = {
725
- password: "valid-token",
726
- text: "test-account",
727
- multiselect: [],
728
- select: "minimal",
729
- confirmSequence: [true, false, false],
730
- };
731
-
732
- await runInitCommand({}, { json: false, quiet: true }, false);
733
-
734
- const workersJsonc = captured.writes["wrangler.jsonc"];
735
- expect(workersJsonc).toBeDefined();
736
- expect(workersJsonc).toContain("// Hoox Workspace Configuration");
737
- expect(workersJsonc).toContain("// Generated by `hoox init`");
738
- });
739
- });
740
-
741
- // ------------------------------------------------------------------
742
- // .dev.vars creation
743
- // ------------------------------------------------------------------
744
-
745
- describe(".dev.vars creation", () => {
746
- it("creates .dev.vars for integration workers", async () => {
747
- responses = {
748
- password: "valid-token",
749
- text: "test-account",
750
- multiselect: [],
751
- select: "full",
752
- confirmSequence: [true, false, false],
753
- group: {
754
- TELEGRAM_BOT_TOKEN: "tg-secret-value",
755
- },
756
- };
757
-
758
- await runInitCommand({}, { json: false, quiet: true }, false);
759
-
760
- const devVarsPath = "workers/telegram-worker/.dev.vars";
761
- const devVars = captured.writes[devVarsPath];
762
- expect(devVars).toBeDefined();
763
- expect(devVars).toContain("TELEGRAM_BOT_TOKEN=tg-secret-value");
764
- expect(devVars).toContain("NEVER commit this file");
765
- });
766
-
767
- it("does not create .dev.vars for workers without secrets", async () => {
768
- responses = {
769
- password: "valid-token",
770
- text: "test-account",
771
- multiselect: [],
772
- select: "minimal",
773
- confirmSequence: [true, false, false],
774
- };
775
-
776
- await runInitCommand({}, { json: false, quiet: true }, false);
777
-
778
- // d1-worker has no user-collected secrets
779
- const d1DevVars = captured.writes["workers/d1-worker/.dev.vars"];
780
- expect(d1DevVars).toBeUndefined();
781
- });
782
-
783
- it("creates .dev.vars for agent-worker when an AI provider is selected", async () => {
784
- responses = {
785
- password: "valid-token",
786
- text: "test-account",
787
- multiselect: [],
788
- select: "full",
789
- confirmSequence: [true, false, false],
790
- group: {
791
- AGENT_OPENAI_KEY: "sk-openai-real-value",
792
- },
793
- };
794
-
795
- await runInitCommand({}, { json: false, quiet: true }, false);
796
-
797
- const devVarsPath = "workers/agent-worker/.dev.vars";
798
- const devVars = captured.writes[devVarsPath];
799
- expect(devVars).toBeDefined();
800
- expect(devVars).toContain("AGENT_OPENAI_KEY=sk-openai-real-value");
801
- expect(devVars).toContain("NEVER commit this file");
802
- });
803
-
804
- it("creates .dev.vars for web3-wallet-worker when wallet integration selected", async () => {
805
- responses = {
806
- password: "valid-token",
807
- text: "test-account",
808
- multiselect: [],
809
- select: "full",
810
- confirmSequence: [true, false, false],
811
- group: {
812
- WALLET_MNEMONIC_SECRET: "mnemonic-real-value",
813
- },
814
- };
815
-
816
- await runInitCommand({}, { json: false, quiet: true }, false);
817
-
818
- const devVarsPath = "workers/web3-wallet-worker/.dev.vars";
819
- const devVars = captured.writes[devVarsPath];
820
- expect(devVars).toBeDefined();
821
- expect(devVars).toContain("WALLET_MNEMONIC_SECRET=mnemonic-real-value");
822
- expect(devVars).toContain("NEVER commit this file");
823
- });
824
- });
825
-
826
- // ------------------------------------------------------------------
827
- // Cancellation handling
828
- // ------------------------------------------------------------------
829
-
830
- describe("cancellation handling", () => {
831
- it("exits on cancel during risk acknowledgment", async () => {
832
- let exitCalled = false;
833
- let exitCode = -1;
834
- const exitMock = mock((code?: number) => {
835
- exitCalled = true;
836
- exitCode = code ?? -1;
837
- throw new Error("EXIT"); // Must throw to stop execution flow
838
- });
839
- process.exit = exitMock as unknown as typeof process.exit;
840
-
841
- // First confirm (risk) is canceled
842
- simulateCancel = true;
843
-
844
- try {
845
- await runInitCommand({}, { json: false, quiet: true }, false);
846
- } catch {
847
- // expected — process.exit throws
848
- }
849
-
850
- expect(exitCalled).toBe(true);
851
- expect(exitCode).toBe(0);
852
- });
853
- });
854
-
855
- // ------------------------------------------------------------------
856
- // Global options (--json, --quiet)
857
- // ------------------------------------------------------------------
858
-
859
- describe("global options", () => {
860
- it("suppresses output in quiet mode", async () => {
861
- responses = {
862
- password: "valid-token",
863
- text: "test-account",
864
- multiselect: [],
865
- select: "minimal",
866
- confirmSequence: [true, false, false],
867
- };
868
-
869
- await runInitCommand({}, { json: false, quiet: true }, false);
870
-
871
- // In quiet mode, formatSuccess/formatter functions respect quiet mode.
872
- // The flow should complete without throwing.
873
- expect(captured.writes["wrangler.jsonc"]).toBeDefined();
874
- });
875
- });
876
- });