@jango-blockchained/hoox-cli 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +403 -0
  2. package/bin/hoox.js +12 -0
  3. package/package.json +60 -0
  4. package/src/commands/check/check-command.test.ts +468 -0
  5. package/src/commands/check/check-command.ts +1144 -0
  6. package/src/commands/check/index.ts +10 -0
  7. package/src/commands/check/prerequisites-command.test.ts +19 -0
  8. package/src/commands/check/prerequisites-command.ts +92 -0
  9. package/src/commands/check/types.ts +103 -0
  10. package/src/commands/clone/clone-command.test.ts +442 -0
  11. package/src/commands/clone/clone-command.ts +440 -0
  12. package/src/commands/clone/index.ts +1 -0
  13. package/src/commands/config/config-command.test.ts +583 -0
  14. package/src/commands/config/config-command.ts +901 -0
  15. package/src/commands/config/env-command.test.ts +43 -0
  16. package/src/commands/config/env-command.ts +314 -0
  17. package/src/commands/config/index.ts +3 -0
  18. package/src/commands/config/kv-command.test.ts +14 -0
  19. package/src/commands/config/kv-command.ts +329 -0
  20. package/src/commands/dashboard/dashboard-command.test.ts +47 -0
  21. package/src/commands/dashboard/dashboard-command.ts +127 -0
  22. package/src/commands/dashboard/index.ts +1 -0
  23. package/src/commands/db/db-command.test.ts +21 -0
  24. package/src/commands/db/db-command.ts +314 -0
  25. package/src/commands/db/index.ts +1 -0
  26. package/src/commands/deploy/deploy-command.test.ts +304 -0
  27. package/src/commands/deploy/deploy-command.ts +1053 -0
  28. package/src/commands/deploy/index.ts +2 -0
  29. package/src/commands/deploy/telegram-service.ts +61 -0
  30. package/src/commands/deploy/types.ts +34 -0
  31. package/src/commands/dev/dev-command.test.ts +383 -0
  32. package/src/commands/dev/dev-command.ts +407 -0
  33. package/src/commands/dev/index.ts +1 -0
  34. package/src/commands/infra/index.ts +5 -0
  35. package/src/commands/infra/infra-command.test.ts +719 -0
  36. package/src/commands/infra/infra-command.ts +940 -0
  37. package/src/commands/infra/types.ts +23 -0
  38. package/src/commands/init/index.ts +1 -0
  39. package/src/commands/init/init-command.test.ts +827 -0
  40. package/src/commands/init/init-command.ts +627 -0
  41. package/src/commands/init/types.ts +185 -0
  42. package/src/commands/monitor/index.ts +2 -0
  43. package/src/commands/monitor/monitor-command.test.ts +235 -0
  44. package/src/commands/monitor/monitor-command.ts +245 -0
  45. package/src/commands/monitor/monitor-service.ts +50 -0
  46. package/src/commands/monitor/types.ts +13 -0
  47. package/src/commands/repair/index.ts +2 -0
  48. package/src/commands/repair/repair-command.test.ts +204 -0
  49. package/src/commands/repair/repair-command.ts +199 -0
  50. package/src/commands/repair/repair-service.ts +102 -0
  51. package/src/commands/repair/types.ts +13 -0
  52. package/src/commands/test/index.ts +2 -0
  53. package/src/commands/test/test-command.test.ts +319 -0
  54. package/src/commands/test/test-command.ts +412 -0
  55. package/src/commands/waf/index.ts +2 -0
  56. package/src/commands/waf/types.ts +48 -0
  57. package/src/commands/waf/waf-command.test.ts +506 -0
  58. package/src/commands/waf/waf-command.ts +548 -0
  59. package/src/index.ts +198 -0
  60. package/src/services/cloudflare/cloudflare-service.test.ts +654 -0
  61. package/src/services/cloudflare/cloudflare-service.ts +435 -0
  62. package/src/services/cloudflare/index.ts +2 -0
  63. package/src/services/cloudflare/types.ts +29 -0
  64. package/src/services/config/config-service.test.ts +395 -0
  65. package/src/services/config/config-service.ts +207 -0
  66. package/src/services/config/index.ts +9 -0
  67. package/src/services/config/types.ts +66 -0
  68. package/src/services/db/db-service.test.ts +51 -0
  69. package/src/services/db/db-service.ts +140 -0
  70. package/src/services/db/index.ts +1 -0
  71. package/src/services/docker/docker-service.ts +155 -0
  72. package/src/services/docker/index.ts +1 -0
  73. package/src/services/env/env-service.test.ts +210 -0
  74. package/src/services/env/env-service.ts +156 -0
  75. package/src/services/env/index.ts +1 -0
  76. package/src/services/kv/index.ts +1 -0
  77. package/src/services/kv/kv-sync-service.test.ts +38 -0
  78. package/src/services/kv/kv-sync-service.ts +151 -0
  79. package/src/services/prerequisites/index.ts +1 -0
  80. package/src/services/prerequisites/prerequisites-service.test.ts +89 -0
  81. package/src/services/prerequisites/prerequisites-service.ts +269 -0
  82. package/src/services/prerequisites/types.ts +48 -0
  83. package/src/services/secrets/index.ts +12 -0
  84. package/src/services/secrets/secrets-service.test.ts +486 -0
  85. package/src/services/secrets/secrets-service.ts +293 -0
  86. package/src/services/secrets/types.ts +57 -0
  87. package/src/ui/banner.ts +52 -0
  88. package/src/ui/index.ts +8 -0
  89. package/src/ui/menu.ts +473 -0
  90. package/src/utils/errors.test.ts +69 -0
  91. package/src/utils/errors.ts +23 -0
  92. package/src/utils/formatters.test.ts +180 -0
  93. package/src/utils/formatters.ts +252 -0
  94. package/src/utils/theme.ts +94 -0
@@ -0,0 +1,412 @@
1
+ /**
2
+ * `hoox test` command group — run tests and CI pipeline.
3
+ *
4
+ * Subcommands:
5
+ * all — Run full CI pipeline (lint → typecheck → unit → integration)
6
+ * unit — Run bun test (unit tests)
7
+ * integration — Run vitest integration tests
8
+ * worker — Run tests for a specific worker directory
9
+ * live — Run live Cloudflare service tests (no mocks)
10
+ */
11
+ import { Command } from "commander";
12
+ import { spinner } from "@clack/prompts";
13
+ import { ConfigService } from "../../services/config/index.js";
14
+ import { theme, icons } from "../../utils/theme.js";
15
+ import {
16
+ formatSuccess,
17
+ formatError,
18
+ formatTable,
19
+ formatJson,
20
+ } from "../../utils/formatters.js";
21
+ import { CLIError, ExitCode } from "../../utils/errors.js";
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Types
25
+ // ---------------------------------------------------------------------------
26
+
27
+ /** Result of a single pipeline step or test run. */
28
+ export interface TestStepResult {
29
+ /** Human-readable step name (e.g. "Lint", "Unit Tests"). */
30
+ step: string;
31
+ /** The raw command string that was executed. */
32
+ command: string;
33
+ /** Whether the step exited with code 0. */
34
+ success: boolean;
35
+ /** Process exit code (null if the process failed to spawn). */
36
+ exitCode: number | null;
37
+ /** Wall-clock duration in milliseconds. */
38
+ duration: number;
39
+ /** Captured stdout (trimmed). */
40
+ output?: string;
41
+ /** Captured stderr or exception message. */
42
+ error?: string;
43
+ }
44
+
45
+ /** Aggregated summary of a test pipeline run. */
46
+ export interface TestSummary {
47
+ total: number;
48
+ passed: number;
49
+ failed: number;
50
+ results: TestStepResult[];
51
+ }
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // Helpers
55
+ // ---------------------------------------------------------------------------
56
+
57
+ function getFormatOptions(cmd: Command) {
58
+ const opts = cmd.optsWithGlobals();
59
+ return { json: Boolean(opts.json), quiet: Boolean(opts.quiet) };
60
+ }
61
+
62
+ /**
63
+ * Spawn a command with stdout/stderr piped, capture output, and return a
64
+ * structured result. Does NOT print anything — the caller decides display.
65
+ */
66
+ export async function runStep(
67
+ args: string[],
68
+ cwd?: string
69
+ ): Promise<TestStepResult> {
70
+ const command = args.join(" ");
71
+ const start = Date.now();
72
+
73
+ try {
74
+ const proc = Bun.spawn(args, {
75
+ cwd: cwd ?? process.cwd(),
76
+ stdout: "pipe",
77
+ stderr: "pipe",
78
+ });
79
+
80
+ const exitCode = await proc.exited;
81
+ const stdout = await new Response(proc.stdout).text();
82
+ const stderr = await new Response(proc.stderr).text();
83
+ const duration = Date.now() - start;
84
+
85
+ return {
86
+ step: command,
87
+ command,
88
+ success: exitCode === 0,
89
+ exitCode,
90
+ duration,
91
+ output: stdout.trim() || undefined,
92
+ error: stderr.trim() ? stderr.trim() : undefined,
93
+ };
94
+ } catch (err) {
95
+ return {
96
+ step: command,
97
+ command,
98
+ success: false,
99
+ exitCode: null,
100
+ duration: Date.now() - start,
101
+ error: err instanceof Error ? err.message : String(err),
102
+ };
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Spawn a command with stdout/stderr inherited so the user sees live output.
108
+ * Returns a minimal result with exit code.
109
+ */
110
+ async function runWithInherit(
111
+ args: string[],
112
+ cwd?: string
113
+ ): Promise<TestStepResult> {
114
+ const command = args.join(" ");
115
+ const start = Date.now();
116
+
117
+ try {
118
+ const proc = Bun.spawn(args, {
119
+ cwd: cwd ?? process.cwd(),
120
+ stdout: "inherit",
121
+ stderr: "inherit",
122
+ });
123
+ const exitCode = await proc.exited;
124
+ const duration = Date.now() - start;
125
+
126
+ return {
127
+ step: command,
128
+ command,
129
+ success: exitCode === 0,
130
+ exitCode,
131
+ duration,
132
+ };
133
+ } catch (err) {
134
+ return {
135
+ step: command,
136
+ command,
137
+ success: false,
138
+ exitCode: null,
139
+ duration: Date.now() - start,
140
+ error: err instanceof Error ? err.message : String(err),
141
+ };
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Print a pipeline summary table (or JSON if --json is set).
147
+ * Quiet mode suppresses all output.
148
+ */
149
+ export function printSummary(
150
+ summary: TestSummary,
151
+ opts: { json?: boolean; quiet?: boolean }
152
+ ): void {
153
+ if (opts.quiet) return;
154
+
155
+ if (opts.json) {
156
+ formatJson(summary, opts);
157
+ return;
158
+ }
159
+
160
+ const rows = summary.results.map((r) => ({
161
+ Step: r.step,
162
+ Status: r.success ? "passed" : "failed",
163
+ Time: `${r.duration}ms`,
164
+ }));
165
+
166
+ formatTable(rows, opts);
167
+
168
+ // Final status line
169
+ if (summary.failed > 0) {
170
+ formatError(
171
+ new CLIError(
172
+ `${summary.passed}/${summary.total} passed — ${summary.failed} failed`,
173
+ ExitCode.ERROR
174
+ ),
175
+ opts
176
+ );
177
+ } else {
178
+ formatSuccess(`All ${summary.total} steps passed`, opts);
179
+ }
180
+ }
181
+
182
+ // ---------------------------------------------------------------------------
183
+ // Pipeline step definitions for `all` subcommand
184
+ // ---------------------------------------------------------------------------
185
+
186
+ const PIPELINE_STEPS: { label: string; args: string[] }[] = [
187
+ { label: "Lint", args: ["bun", "run", "lint"] },
188
+ { label: "TypeCheck", args: ["bun", "run", "typecheck"] },
189
+ { label: "Unit Tests", args: ["bun", "test", "--coverage"] },
190
+ {
191
+ label: "Integration Tests",
192
+ args: ["vitest", "run", "--config", "vitest.config.ts"],
193
+ },
194
+ ];
195
+
196
+ // ---------------------------------------------------------------------------
197
+ // Command registration
198
+ // ---------------------------------------------------------------------------
199
+
200
+ /**
201
+ * Register the `hoox test` command group with subcommands:
202
+ * all, unit, integration, worker, live.
203
+ */
204
+ export function registerTestCommand(program: Command): void {
205
+ const testCmd = program
206
+ .command("test")
207
+ .description("Run tests and CI pipeline");
208
+
209
+ // -- test all ----------------------------------------------------------
210
+ testCmd
211
+ .command("all")
212
+ .description(
213
+ "Run full CI pipeline: lint → typecheck → unit tests → integration tests"
214
+ )
215
+ .option("--json", "Output results as JSON")
216
+ .action(async (options: { json?: boolean }) => {
217
+ const fmt = getFormatOptions(program);
218
+ // Allow local --json to override global
219
+ const useJson = options.json || fmt.json;
220
+ const opts = { json: useJson, quiet: fmt.quiet };
221
+
222
+ const results: TestStepResult[] = [];
223
+ const s = spinner();
224
+ s.start("Running CI pipeline...");
225
+
226
+ try {
227
+ for (const step of PIPELINE_STEPS) {
228
+ s.message(`${theme.info(icons.info)} ${step.label}...`);
229
+ const result = await runStep(step.args, process.cwd());
230
+ results.push({ ...result, step: step.label });
231
+
232
+ if (result.success) {
233
+ s.message(
234
+ ` ${theme.success(icons.success)} ${step.label} passed (${result.duration}ms)`
235
+ );
236
+ } else {
237
+ s.message(
238
+ ` ${theme.error(icons.error)} ${step.label} failed (${result.duration}ms)`
239
+ );
240
+ if (result.error) {
241
+ s.message(theme.dim(result.error.slice(0, 500)));
242
+ }
243
+ // Stop on first failure
244
+ break;
245
+ }
246
+ }
247
+ } catch (err) {
248
+ const message = err instanceof Error ? err.message : String(err);
249
+ formatError(message, opts);
250
+ process.exitCode = ExitCode.ERROR;
251
+ s.stop("Pipeline aborted with unexpected error");
252
+ return;
253
+ }
254
+
255
+ const summary: TestSummary = {
256
+ total: results.length,
257
+ passed: results.filter((r) => r.success).length,
258
+ failed: results.filter((r) => !r.success).length,
259
+ results,
260
+ };
261
+
262
+ s.stop(
263
+ summary.failed > 0
264
+ ? `Pipeline complete: ${summary.passed} passed, ${summary.failed} failed`
265
+ : "Pipeline complete"
266
+ );
267
+
268
+ printSummary(summary, opts);
269
+
270
+ if (summary.failed > 0) {
271
+ process.exitCode = ExitCode.ERROR;
272
+ }
273
+ });
274
+
275
+ // -- test unit ---------------------------------------------------------
276
+ testCmd
277
+ .command("unit")
278
+ .description("Run unit tests with bun test")
279
+ .option("--coverage", "Run with coverage reporting")
280
+ .action(async (options: { coverage?: boolean }) => {
281
+ const fmt = getFormatOptions(program);
282
+ const args = ["bun", "test"];
283
+ if (options.coverage) args.push("--coverage");
284
+
285
+ const result = await runWithInherit(args, process.cwd());
286
+
287
+ if (result.success) {
288
+ formatSuccess("Unit tests passed", fmt);
289
+ } else {
290
+ formatError(
291
+ new CLIError(
292
+ `Unit tests failed (exit code ${result.exitCode})`,
293
+ ExitCode.ERROR
294
+ ),
295
+ fmt
296
+ );
297
+ process.exitCode = ExitCode.ERROR;
298
+ }
299
+ });
300
+
301
+ // -- test integration --------------------------------------------------
302
+ testCmd
303
+ .command("integration")
304
+ .description("Run integration tests with vitest")
305
+ .option("--coverage", "Run with coverage reporting")
306
+ .action(async (options: { coverage?: boolean }) => {
307
+ const fmt = getFormatOptions(program);
308
+ const args = ["vitest", "run", "--config", "vitest.config.ts"];
309
+ if (options.coverage) args.push("--coverage");
310
+
311
+ const result = await runWithInherit(args, process.cwd());
312
+
313
+ if (result.success) {
314
+ formatSuccess("Integration tests passed", fmt);
315
+ } else {
316
+ formatError(
317
+ new CLIError(
318
+ `Integration tests failed (exit code ${result.exitCode})`,
319
+ ExitCode.ERROR
320
+ ),
321
+ fmt
322
+ );
323
+ process.exitCode = ExitCode.ERROR;
324
+ }
325
+ });
326
+
327
+ // -- test worker <name> ------------------------------------------------
328
+ testCmd
329
+ .command("worker <name>")
330
+ .description("Run tests for a specific worker by name")
331
+ .option("--coverage", "Run with coverage reporting")
332
+ .action(async (name: string, options: { coverage?: boolean }) => {
333
+ const fmt = getFormatOptions(program);
334
+
335
+ try {
336
+ const configService = new ConfigService();
337
+ await configService.load();
338
+ const workerConfig = configService.getWorker(name);
339
+
340
+ if (!workerConfig) {
341
+ formatError(
342
+ new CLIError(
343
+ `Worker "${name}" not found in wrangler.jsonc`,
344
+ ExitCode.INVALID_USAGE
345
+ ),
346
+ fmt
347
+ );
348
+ process.exitCode = ExitCode.INVALID_USAGE;
349
+ return;
350
+ }
351
+
352
+ const workerDir = `${process.cwd()}/${workerConfig.path}`;
353
+ const args = ["bun", "test"];
354
+ if (options.coverage) args.push("--coverage");
355
+
356
+ const result = await runWithInherit(args, workerDir);
357
+
358
+ if (result.success) {
359
+ formatSuccess(`Worker "${name}" tests passed`, fmt);
360
+ } else {
361
+ formatError(
362
+ new CLIError(
363
+ `Worker "${name}" tests failed (exit code ${result.exitCode})`,
364
+ ExitCode.ERROR
365
+ ),
366
+ fmt
367
+ );
368
+ process.exitCode = ExitCode.ERROR;
369
+ }
370
+ } catch (err) {
371
+ const message = err instanceof Error ? err.message : String(err);
372
+ formatError(message, fmt);
373
+ process.exitCode = ExitCode.ERROR;
374
+ }
375
+ });
376
+
377
+ // -- test live ---------------------------------------------------------
378
+ testCmd
379
+ .command("live")
380
+ .description("Run live Cloudflare service integration tests (no mocks)")
381
+ .option(
382
+ "--service <name>",
383
+ "Test a specific service: d1, kv, r2, queues, ai, api, secrets, durable-objects"
384
+ )
385
+ .action(async (options: { service?: string }) => {
386
+ const fmt = getFormatOptions(program);
387
+ const s = spinner();
388
+
389
+ try {
390
+ let filePattern = options.service
391
+ ? `tests/live/${options.service}.test.ts`
392
+ : "tests/live/";
393
+
394
+ s.start(`Running live tests${options.service ? ` for ${options.service}` : ""}...`);
395
+
396
+ const args = ["bun", "test", filePattern, "--jobs", "1"];
397
+ const result = await runWithInherit(args, process.cwd());
398
+
399
+ if (result.success) {
400
+ s.stop("Live tests complete");
401
+ } else {
402
+ s.stop(`Live tests: some failures (exit code ${result.exitCode})`);
403
+ process.exitCode = ExitCode.ERROR;
404
+ }
405
+ } catch (err) {
406
+ s.stop("Live tests aborted");
407
+ const message = err instanceof Error ? err.message : String(err);
408
+ formatError(message, fmt);
409
+ process.exitCode = ExitCode.ERROR;
410
+ }
411
+ });
412
+ }
@@ -0,0 +1,2 @@
1
+ export { registerWafCommand } from "./waf-command.js";
2
+ export type { WafStatus, WafRule, WafMode, WafRuleInput } from "./types.js";
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Waf command type definitions.
3
+ */
4
+
5
+ /** WAF status summary returned by `hoox waf status`. */
6
+ export interface WafStatus {
7
+ /** Whether WAF protection is enabled on the zone. */
8
+ enabled: boolean;
9
+ /** WAF mode (e.g. "on", "off", "simulate"). */
10
+ mode: string;
11
+ /** Number of active firewall rules on the zone. */
12
+ activeRulesCount: number;
13
+ /** Approximate count of recent blocked requests (last 24h). */
14
+ recentBlocks: number;
15
+ /** Cloudflare zone ID. */
16
+ zoneId: string;
17
+ /** Cloudflare zone name (domain). */
18
+ zoneName: string;
19
+ }
20
+
21
+ /** A Cloudflare WAF / firewall rule. */
22
+ export interface WafRule {
23
+ /** Unique rule identifier. */
24
+ id: string;
25
+ /** Human-readable description of the rule. */
26
+ description: string;
27
+ /** Rule mode: "block", "challenge", "js_challenge", "allow", "log". */
28
+ mode: string;
29
+ /** Cloudflare Filters expression that triggers the rule. */
30
+ expression: string;
31
+ /** ISO 8601 creation timestamp. */
32
+ created_at: string;
33
+ /** ISO 8601 last-modified timestamp. */
34
+ updated_at: string;
35
+ }
36
+
37
+ /** WAF toggle mode. */
38
+ export type WafMode = "on" | "off";
39
+
40
+ /** Input for creating a new WAF rule. */
41
+ export interface WafRuleInput {
42
+ /** Rule type: "ip-allowlist", "ip-blocklist", "rate-limit", "custom". */
43
+ type: "ip-allowlist" | "ip-blocklist" | "rate-limit" | "custom";
44
+ /** Value associated with the rule type (IP address, requests/period, etc.). */
45
+ value: string;
46
+ /** Optional human-readable description. */
47
+ description?: string;
48
+ }