@jango-blockchained/hoox-cli 0.5.2 → 0.5.7

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