@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
package/package.json CHANGED
@@ -1,16 +1,11 @@
1
1
  {
2
2
  "name": "@jango-blockchained/hoox-cli",
3
- "version": "0.5.1",
3
+ "version": "0.5.6",
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.95.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
- });