@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
package/package.json CHANGED
@@ -1,16 +1,11 @@
1
1
  {
2
2
  "name": "@jango-blockchained/hoox-cli",
3
- "version": "0.5.2",
3
+ "version": "0.5.7",
4
4
  "description": "Hoox CLI — manage Cloudflare Workers, infrastructure, secrets, and deployments (@jango-blockchained/hoox-cli)",
5
5
  "type": "module",
6
- "main": "./bin/hoox.js",
7
- "types": "./src/index.ts",
8
- "exports": {
9
- ".": "./bin/hoox.js"
10
- },
11
6
  "bin": {
12
- "hoox": "./bin/hoox.js",
13
- "hx": "./bin/hoox.js"
7
+ "hoox": "bin/hoox.js",
8
+ "hx": "bin/hoox.js"
14
9
  },
15
10
  "scripts": {
16
11
  "test": "bun test",
@@ -22,23 +17,23 @@
22
17
  },
23
18
  "dependencies": {
24
19
  "@clack/prompts": "^1.3.0",
25
- "@jango-blockchained/hoox-shared": "workspace:*",
26
20
  "ansis": "^4.2.0",
27
21
  "commander": "^14.0.3",
28
22
  "jsonc-parser": "^3.3.1",
29
- "wrangler": "^4.92.0"
23
+ "wrangler": "^4.97.0"
30
24
  },
31
25
  "devDependencies": {
32
- "@types/node": "^25.6.0",
33
- "bun-types": "^1.3.13",
34
- "typescript": "^5.9.3"
26
+ "@jango-blockchained/hoox-shared": "^1.0.8",
27
+ "@types/node": "^25.8.0",
28
+ "bun-types": "^1.3.14",
29
+ "typescript": "^6.0.3"
35
30
  },
36
31
  "publishConfig": {
37
32
  "access": "public"
38
33
  },
39
34
  "repository": {
40
35
  "type": "git",
41
- "url": "https://github.com/jango-blockchained/hoox-setup.git"
36
+ "url": "git+https://github.com/jango-blockchained/hoox-setup.git"
42
37
  },
43
38
  "keywords": [
44
39
  "cloudflare",
@@ -56,7 +51,7 @@
56
51
  },
57
52
  "files": [
58
53
  "bin",
59
- "src",
54
+ "dist",
60
55
  "README.md"
61
56
  ]
62
57
  }
@@ -1,476 +0,0 @@
1
- /**
2
- * Unit tests for the check command.
3
- *
4
- * Uses prototype mocking on ConfigService, CloudflareService, and SecretsService
5
- * to verify the command logic in isolation. @clack/prompts spinner is not
6
- * mocked — it degrades gracefully in non-TTY test environments.
7
- */
8
- import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test";
9
-
10
- // ---------------------------------------------------------------------------
11
- // Stub service prototypes
12
- // ---------------------------------------------------------------------------
13
-
14
- import { ConfigService } from "../../services/config/config-service.js";
15
- import { CloudflareService } from "../../services/cloudflare/cloudflare-service.js";
16
- import { SecretsService } from "../../services/secrets/secrets-service.js";
17
-
18
- // ---------------------------------------------------------------------------
19
- // Stub variables — reassigned in beforeEach
20
- // ---------------------------------------------------------------------------
21
-
22
- let loadMock: ReturnType<typeof mock>;
23
- let validateMock: ReturnType<typeof mock>;
24
- let getGlobalMock: ReturnType<typeof mock>;
25
- let listWorkersMock: ReturnType<typeof mock>;
26
- let listEnabledWorkersMock: ReturnType<typeof mock>;
27
- let getWorkerMock: ReturnType<typeof mock>;
28
-
29
- let d1ListMock: ReturnType<typeof mock>;
30
- let kvListMock: ReturnType<typeof mock>;
31
- let r2ListMock: ReturnType<typeof mock>;
32
- let queueListMock: ReturnType<typeof mock>;
33
- let tailMock: ReturnType<typeof mock>;
34
- let secretListMock: ReturnType<typeof mock>;
35
-
36
- let secretsCreateMock: ReturnType<typeof mock>;
37
- let checkLocalSecretsMock: ReturnType<typeof mock>;
38
- let listSecretsMock: ReturnType<typeof mock>;
39
-
40
- // Preserve originals for cleanup
41
- const origLoad = ConfigService.prototype.load;
42
- const origValidate = ConfigService.prototype.validate;
43
- const origGetGlobal = ConfigService.prototype.getGlobal;
44
- const origListWorkers = ConfigService.prototype.listWorkers;
45
- const origListEnabled = ConfigService.prototype.listEnabledWorkers;
46
- const origGetWorker = ConfigService.prototype.getWorker;
47
-
48
- const origD1List = CloudflareService.prototype.d1List;
49
- const origKvList = CloudflareService.prototype.kvList;
50
- const origR2List = CloudflareService.prototype.r2List;
51
- const origQueueList = CloudflareService.prototype.queueList;
52
- const origTail = CloudflareService.prototype.tail;
53
- const origSecretList = CloudflareService.prototype.secretList;
54
-
55
- const origCreate = SecretsService.create;
56
- const origCheckLocal = SecretsService.prototype.checkLocalSecrets;
57
- const origListSecrets = SecretsService.prototype.listSecrets;
58
-
59
- beforeEach(() => {
60
- mock.restore();
61
-
62
- // Reset process.exitCode between tests
63
- process.exitCode = undefined;
64
-
65
- // Reset to originals on prototypes
66
- ConfigService.prototype.load = origLoad;
67
- ConfigService.prototype.validate = origValidate;
68
- ConfigService.prototype.getGlobal = origGetGlobal;
69
- ConfigService.prototype.listWorkers = origListWorkers;
70
- ConfigService.prototype.listEnabledWorkers = origListEnabled;
71
- ConfigService.prototype.getWorker = origGetWorker;
72
-
73
- CloudflareService.prototype.d1List = origD1List;
74
- CloudflareService.prototype.kvList = origKvList;
75
- CloudflareService.prototype.r2List = origR2List;
76
- CloudflareService.prototype.queueList = origQueueList;
77
- CloudflareService.prototype.tail = origTail;
78
- CloudflareService.prototype.secretList = origSecretList;
79
-
80
- // Fresh mocks
81
- loadMock = mock(async () => ({}));
82
- validateMock = mock(() => ({ valid: true, errors: [] }));
83
- getGlobalMock = mock(() => ({
84
- cloudflare_account_id: "abc123",
85
- }));
86
- listWorkersMock = mock(() => ["d1-worker", "hoox", "trade-worker"]);
87
- listEnabledWorkersMock = mock(() => ["d1-worker", "hoox", "trade-worker"]);
88
- getWorkerMock = mock((_name: string) => ({
89
- enabled: true,
90
- path: "workers/test-worker",
91
- }));
92
-
93
- d1ListMock = mock(async () => ({
94
- ok: true as const,
95
- data: "my-database (abc-123)",
96
- }));
97
- kvListMock = mock(async () => ({
98
- ok: true as const,
99
- data: "namespace-1 (id1)\nnamespace-2 (id2)",
100
- }));
101
- r2ListMock = mock(async () => ({
102
- ok: true as const,
103
- data: "bucket-1\nbucket-2",
104
- }));
105
- queueListMock = mock(async () => ({
106
- ok: true as const,
107
- data: "queue-1\nqueue-2",
108
- }));
109
- tailMock = mock(async () => ({
110
- ok: true as const,
111
- data: "Connected to worker",
112
- }));
113
- secretListMock = mock(async () => ({
114
- ok: true as const,
115
- data: "Secret names: API_KEY, DB_PASSWORD",
116
- }));
117
-
118
- secretsCreateMock = mock(async () =>
119
- Object.create(SecretsService.prototype, {
120
- checkLocalSecrets: {
121
- value: mock(async () => ({ allSet: true, missing: [], secrets: [] })),
122
- },
123
- listSecrets: { value: mock(() => []) },
124
- })
125
- );
126
-
127
- checkLocalSecretsMock = mock(async () => ({
128
- worker: "test-worker",
129
- secrets: [],
130
- allSet: true,
131
- missing: [],
132
- }));
133
- listSecretsMock = mock(() => ["API_KEY"]);
134
-
135
- // Apply stubs
136
- ConfigService.prototype.load = loadMock;
137
- ConfigService.prototype.validate = validateMock;
138
- ConfigService.prototype.getGlobal = getGlobalMock;
139
- ConfigService.prototype.listWorkers = listWorkersMock;
140
- ConfigService.prototype.listEnabledWorkers = listEnabledWorkersMock;
141
- ConfigService.prototype.getWorker = getWorkerMock;
142
-
143
- CloudflareService.prototype.d1List = d1ListMock;
144
- CloudflareService.prototype.kvList = kvListMock;
145
- CloudflareService.prototype.r2List = r2ListMock;
146
- CloudflareService.prototype.queueList = queueListMock;
147
- CloudflareService.prototype.tail = tailMock;
148
- CloudflareService.prototype.secretList = secretListMock;
149
-
150
- // Replace static create on SecretsService
151
- (SecretsService as unknown as Record<string, unknown>).create =
152
- secretsCreateMock;
153
- });
154
-
155
- afterEach(() => {
156
- mock.restore();
157
-
158
- // Restore originals
159
- ConfigService.prototype.load = origLoad;
160
- ConfigService.prototype.validate = origValidate;
161
- ConfigService.prototype.getGlobal = origGetGlobal;
162
- ConfigService.prototype.listWorkers = origListWorkers;
163
- ConfigService.prototype.listEnabledWorkers = origListEnabled;
164
- ConfigService.prototype.getWorker = origGetWorker;
165
-
166
- CloudflareService.prototype.d1List = origD1List;
167
- CloudflareService.prototype.kvList = origKvList;
168
- CloudflareService.prototype.r2List = origR2List;
169
- CloudflareService.prototype.queueList = origQueueList;
170
- CloudflareService.prototype.tail = origTail;
171
- CloudflareService.prototype.secretList = origSecretList;
172
-
173
- (SecretsService as unknown as Record<string, unknown>).create = origCreate;
174
- SecretsService.prototype.checkLocalSecrets = origCheckLocal;
175
- SecretsService.prototype.listSecrets = origListSecrets;
176
- });
177
-
178
- // ---------------------------------------------------------------------------
179
- // Helpers
180
- // ---------------------------------------------------------------------------
181
-
182
- /**
183
- * Dynamically import the check command after mocks are in place.
184
- */
185
- async function importCheckCommand(): Promise<{
186
- registerCheckCommand: typeof import("./check-command.js").registerCheckCommand;
187
- }> {
188
- return import("./check-command.js");
189
- }
190
-
191
- import { Command } from "commander";
192
- import { ExitCode } from "../../utils/errors.js";
193
-
194
- async function createProgram() {
195
- const { registerCheckCommand } = await importCheckCommand();
196
- const program = new Command()
197
- .name("hoox-test")
198
- .option("--json", "Output in JSON format")
199
- .option("--quiet", "Minimal output")
200
- .exitOverride(() => {
201
- // Suppress Commander's own exit during tests
202
- });
203
- registerCheckCommand(program);
204
- return program;
205
- }
206
-
207
- // ---------------------------------------------------------------------------
208
- // Tests
209
- // ---------------------------------------------------------------------------
210
-
211
- describe("registerCheckCommand", () => {
212
- // -- Command registration -------------------------------------------------
213
-
214
- it("registers 'check' as a command on the program", async () => {
215
- const program = await createProgram();
216
- const checkCmd = program.commands.find((c) => c.name() === "check");
217
- expect(checkCmd).toBeDefined();
218
- });
219
-
220
- it("registers 'check setup' subcommand", async () => {
221
- const program = await createProgram();
222
- const checkCmd = program.commands.find((c) => c.name() === "check")!;
223
- const setupCmd = checkCmd.commands.find((c) => c.name() === "setup");
224
- expect(setupCmd).toBeDefined();
225
- expect(setupCmd!.description()).toContain("validation");
226
- });
227
-
228
- it("registers 'check health' subcommand", async () => {
229
- const program = await createProgram();
230
- const checkCmd = program.commands.find((c) => c.name() === "check")!;
231
- const healthCmd = checkCmd.commands.find((c) => c.name() === "health");
232
- expect(healthCmd).toBeDefined();
233
- expect(healthCmd!.description()).toContain("health");
234
- });
235
-
236
- it("registers 'check fix' subcommand", async () => {
237
- const program = await createProgram();
238
- const checkCmd = program.commands.find((c) => c.name() === "check")!;
239
- const fixCmd = checkCmd.commands.find((c) => c.name() === "fix");
240
- expect(fixCmd).toBeDefined();
241
- expect(fixCmd!.description()).toContain("Repair");
242
- });
243
-
244
- // -- check setup ----------------------------------------------------------
245
-
246
- describe("check setup", () => {
247
- it(
248
- "loads config and runs all 4 categories",
249
- { timeout: 30000 },
250
- async () => {
251
- const program = await createProgram();
252
- await program.parseAsync(["check", "setup"], { from: "user" });
253
-
254
- expect(loadMock).toHaveBeenCalled();
255
- expect(d1ListMock).toHaveBeenCalled();
256
- expect(kvListMock).toHaveBeenCalled();
257
- expect(r2ListMock).toHaveBeenCalled();
258
- expect(queueListMock).toHaveBeenCalled();
259
- }
260
- );
261
-
262
- it("outputs JSON when --json flag is set", { timeout: 30000 }, async () => {
263
- const program = await createProgram();
264
- const logSpy = mock((..._args: unknown[]) => {});
265
- const origWrite = process.stdout.write;
266
- (process.stdout as unknown as Record<string, unknown>).write = logSpy;
267
-
268
- try {
269
- await program.parseAsync(["--json", "check", "setup"], {
270
- from: "user",
271
- });
272
-
273
- // Should have called stdout.write with JSON
274
- const jsonCalls = (
275
- logSpy as unknown as { mock: { calls: Array<unknown[]> } }
276
- ).mock.calls.filter((call: unknown[]) => {
277
- const str = String(call[0] ?? "");
278
- try {
279
- JSON.parse(str);
280
- return str.includes("categories") && str.includes("summary");
281
- } catch {
282
- return false;
283
- }
284
- });
285
- expect(jsonCalls.length).toBeGreaterThan(0);
286
- } finally {
287
- (process.stdout as unknown as Record<string, unknown>).write =
288
- origWrite;
289
- }
290
- });
291
-
292
- it(
293
- "sets exitCode to ERROR when validation fails",
294
- { timeout: 30000 },
295
- async () => {
296
- validateMock = mock(() => ({
297
- valid: false,
298
- errors: ["global.cloudflare_account_id is required"],
299
- }));
300
- ConfigService.prototype.validate = validateMock;
301
-
302
- const program = await createProgram();
303
- await program.parseAsync(["check", "setup"], { from: "user" });
304
-
305
- expect(process.exitCode).toBe(ExitCode.ERROR);
306
- }
307
- );
308
-
309
- it("sets exitCode to ERROR when infra check fails", async () => {
310
- d1ListMock = mock(async () => ({
311
- ok: false as const,
312
- error: "Authentication failed",
313
- }));
314
- CloudflareService.prototype.d1List = d1ListMock;
315
-
316
- const program = await createProgram();
317
- await program.parseAsync(["check", "setup"], { from: "user" });
318
-
319
- expect(process.exitCode).toBe(ExitCode.ERROR);
320
- });
321
-
322
- it("handles config load failure gracefully", async () => {
323
- loadMock = mock(async () => {
324
- throw new Error("Config file not found");
325
- });
326
- ConfigService.prototype.load = loadMock;
327
-
328
- const program = await createProgram();
329
- await program.parseAsync(["check", "setup"], { from: "user" });
330
-
331
- expect(process.exitCode).toBe(ExitCode.ERROR);
332
- });
333
- });
334
-
335
- // -- check health ---------------------------------------------------------
336
-
337
- describe("check health", () => {
338
- it("checks connectivity for all enabled workers", async () => {
339
- const program = await createProgram();
340
- await program.parseAsync(["check", "health"], { from: "user" });
341
-
342
- expect(loadMock).toHaveBeenCalled();
343
- // tailMock should have been called for each enabled worker
344
- expect(tailMock).toHaveBeenCalled();
345
- });
346
-
347
- it("outputs JSON when --json flag is set", async () => {
348
- const program = await createProgram();
349
- const logSpy = mock((..._args: unknown[]) => {});
350
- const origWrite = process.stdout.write;
351
- (process.stdout as unknown as Record<string, unknown>).write = logSpy;
352
-
353
- try {
354
- await program.parseAsync(["--json", "check", "health"], {
355
- from: "user",
356
- });
357
-
358
- const jsonCalls = (
359
- logSpy as unknown as { mock: { calls: Array<unknown[]> } }
360
- ).mock.calls.filter((call: unknown[]) => {
361
- const str = String(call[0] ?? "");
362
- try {
363
- const parsed = JSON.parse(str);
364
- return Array.isArray(parsed) && parsed.length > 0;
365
- } catch {
366
- return false;
367
- }
368
- });
369
- expect(jsonCalls.length).toBeGreaterThan(0);
370
- } finally {
371
- (process.stdout as unknown as Record<string, unknown>).write =
372
- origWrite;
373
- }
374
- });
375
-
376
- it("sets exitCode to ERROR when any worker is unhealthy", async () => {
377
- tailMock = mock(async () => ({
378
- ok: false as const,
379
- error: "Worker not reachable",
380
- }));
381
- CloudflareService.prototype.tail = tailMock;
382
-
383
- const program = await createProgram();
384
- await program.parseAsync(["check", "health"], { from: "user" });
385
-
386
- expect(process.exitCode).toBe(ExitCode.ERROR);
387
- });
388
-
389
- it("handles empty worker list gracefully", async () => {
390
- listEnabledWorkersMock = mock(() => []);
391
- ConfigService.prototype.listEnabledWorkers = listEnabledWorkersMock;
392
-
393
- const program = await createProgram();
394
- await program.parseAsync(["check", "health"], { from: "user" });
395
-
396
- // Should exit cleanly — the handler outputs success message
397
- expect(process.exitCode).toBe(1);
398
- });
399
- });
400
-
401
- // -- check fix ------------------------------------------------------------
402
-
403
- describe("check fix", () => {
404
- it("scans for issues and reports when dry-run", async () => {
405
- const program = await createProgram();
406
-
407
- // Mock Bun.file so .dev.vars doesn't exist (triggers fix action)
408
- const origBunFile = Bun.file;
409
- (Bun as Record<string, unknown>).file = mock((_path: string) => ({
410
- exists: mock(async () => false),
411
- text: mock(async () => ""),
412
- name: _path,
413
- }));
414
-
415
- try {
416
- await program.parseAsync(["check", "fix", "--dry-run"], {
417
- from: "user",
418
- });
419
-
420
- // Should complete without errors
421
- // In dry-run mode, exitCode should not be ERROR (no actual failures to apply)
422
- } finally {
423
- (Bun as Record<string, unknown>).file = origBunFile;
424
- }
425
- });
426
-
427
- it("outputs JSON when --json flag is set", async () => {
428
- const program = await createProgram();
429
- const logSpy = mock((..._args: unknown[]) => {});
430
- const origWrite = process.stdout.write;
431
- (process.stdout as unknown as Record<string, unknown>).write = logSpy;
432
-
433
- const origBunFile = Bun.file;
434
- (Bun as Record<string, unknown>).file = mock((_path: string) => ({
435
- exists: mock(async () => false),
436
- text: mock(async () => ""),
437
- name: _path,
438
- }));
439
-
440
- try {
441
- await program.parseAsync(["--json", "check", "fix", "--dry-run"], {
442
- from: "user",
443
- });
444
-
445
- const jsonCalls = (
446
- logSpy as unknown as { mock: { calls: Array<unknown[]> } }
447
- ).mock.calls.filter((call: unknown[]) => {
448
- const str = String(call[0] ?? "");
449
- try {
450
- const parsed = JSON.parse(str);
451
- return parsed.actions !== undefined && parsed.summary !== undefined;
452
- } catch {
453
- return false;
454
- }
455
- });
456
- expect(jsonCalls.length).toBeGreaterThan(0);
457
- } finally {
458
- (process.stdout as unknown as Record<string, unknown>).write =
459
- origWrite;
460
- (Bun as Record<string, unknown>).file = origBunFile;
461
- }
462
- });
463
-
464
- it("handles config load failure gracefully", async () => {
465
- loadMock = mock(async () => {
466
- throw new Error("Config file not found");
467
- });
468
- ConfigService.prototype.load = loadMock;
469
-
470
- const program = await createProgram();
471
- await program.parseAsync(["check", "fix"], { from: "user" });
472
-
473
- expect(process.exitCode).toBe(ExitCode.ERROR);
474
- });
475
- });
476
- });