@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,548 @@
1
+ /**
2
+ * `hoox waf` command — Cloudflare WAF (Web Application Firewall) management.
3
+ *
4
+ * Subcommands:
5
+ * waf status Show WAF status for the zone
6
+ * waf rules list List all active firewall rules
7
+ * waf rules add <type> <value> Add a WAF rule
8
+ * waf rules remove <id> Remove a WAF rule by ID
9
+ * waf mode enable Enable WAF protection
10
+ * waf mode disable Disable WAF protection
11
+ */
12
+
13
+ import type { Command } from "commander";
14
+ import { CloudflareService } from "../../services/cloudflare/cloudflare-service.js";
15
+ import type { WranglerResult } from "../../services/cloudflare/types.js";
16
+ import { CLIError, ExitCode } from "../../utils/errors.js";
17
+ import {
18
+ formatSuccess,
19
+ formatError,
20
+ formatTable,
21
+ } from "../../utils/formatters.js";
22
+ import { theme, icons } from "../../utils/theme.js";
23
+ import type { FormatOptions } from "../../utils/formatters.js";
24
+ import type { WafStatus, WafRule, WafRuleInput } from "./types.js";
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Cloudflare API helpers
28
+ // ---------------------------------------------------------------------------
29
+
30
+ const CF_API_BASE = "https://api.cloudflare.com/client/v4";
31
+
32
+ /**
33
+ * Reads the Cloudflare API token and account ID from environment variables.
34
+ * Required for direct REST API calls (wrangler doesn't have native WAF commands).
35
+ */
36
+ function getCredentials(): { token: string; accountId: string } {
37
+ const token = process.env.CLOUDFLARE_API_TOKEN;
38
+ const accountId = process.env.CLOUDFLARE_ACCOUNT_ID;
39
+ if (!token) {
40
+ throw new CLIError(
41
+ "CLOUDFLARE_API_TOKEN environment variable is not set. Set it or run `wrangler login`.",
42
+ ExitCode.ERROR
43
+ );
44
+ }
45
+ if (!accountId) {
46
+ throw new CLIError(
47
+ "CLOUDFLARE_ACCOUNT_ID environment variable is not set.",
48
+ ExitCode.ERROR
49
+ );
50
+ }
51
+ return { token, accountId };
52
+ }
53
+
54
+ /**
55
+ * Makes an authenticated request to the Cloudflare REST API.
56
+ * Returns a WranglerResult-style discriminated union for consistent error handling.
57
+ */
58
+ async function cfApi<T>(
59
+ method: "GET" | "POST" | "PATCH" | "DELETE",
60
+ path: string,
61
+ body?: unknown
62
+ ): Promise<WranglerResult<T>> {
63
+ const { token } = getCredentials();
64
+ const url = `${CF_API_BASE}${path}`;
65
+
66
+ try {
67
+ const response = await fetch(url, {
68
+ method,
69
+ headers: {
70
+ Authorization: `Bearer ${token}`,
71
+ "Content-Type": "application/json",
72
+ },
73
+ body: body !== undefined ? JSON.stringify(body) : undefined,
74
+ });
75
+
76
+ const json = (await response.json()) as {
77
+ success: boolean;
78
+ result: T;
79
+ errors: Array<{ message: string }>;
80
+ };
81
+
82
+ if (!response.ok || !json.success) {
83
+ const errorMsg =
84
+ json.errors?.map((e) => e.message).join("; ") ||
85
+ `HTTP ${response.status}`;
86
+ return { ok: false, error: errorMsg };
87
+ }
88
+
89
+ return { ok: true, data: json.result };
90
+ } catch (err) {
91
+ const message = err instanceof Error ? err.message : String(err);
92
+ return { ok: false, error: `Cloudflare API request failed: ${message}` };
93
+ }
94
+ }
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // WAF API operations
98
+ // ---------------------------------------------------------------------------
99
+
100
+ interface CfWafSetting {
101
+ id: string;
102
+ value: string;
103
+ }
104
+
105
+ interface CfFirewallRule {
106
+ id: string;
107
+ description: string;
108
+ action: string;
109
+ filter: { id: string; expression: string };
110
+ created_on: string;
111
+ modified_on: string;
112
+ }
113
+
114
+ interface CfAnalyticsOverview {
115
+ totals: {
116
+ threats: number;
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Builds expression from rule type and value.
122
+ * ip-allowlist → (ip.src eq {value})
123
+ * ip-blocklist → (ip.src eq {value})
124
+ * rate-limit → handled as custom with rate-limit expression
125
+ * custom → uses value as raw expression
126
+ */
127
+ function buildExpression(type: WafRuleInput["type"], value: string): string {
128
+ switch (type) {
129
+ case "ip-allowlist":
130
+ return `(ip.src eq ${value})`;
131
+ case "ip-blocklist":
132
+ return `(ip.src eq ${value})`;
133
+ case "rate-limit":
134
+ // Rate limiting is configured via a separate API; here we build a
135
+ // basic rate-limit expression that can be extended.
136
+ return `(http.request.uri.path contains "/")`;
137
+ case "custom":
138
+ return value;
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Maps rule type and value to a Cloudflare firewall rule action.
144
+ */
145
+ function mapAction(type: WafRuleInput["type"]): string {
146
+ switch (type) {
147
+ case "ip-allowlist":
148
+ return "allow";
149
+ case "ip-blocklist":
150
+ return "block";
151
+ case "rate-limit":
152
+ return "managed_challenge";
153
+ case "custom":
154
+ return "block";
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Fetches the Cloudflare zone to operate on. Uses CloudflareService.zonesList()
160
+ * and picks the first zone. Falls back to env CLOUDFLARE_ZONE_ID.
161
+ */
162
+ async function resolveZone(
163
+ cf: CloudflareService
164
+ ): Promise<{ id: string; name: string }> {
165
+ // Try explicit env var first
166
+ if (process.env.CLOUDFLARE_ZONE_ID) {
167
+ return {
168
+ id: process.env.CLOUDFLARE_ZONE_ID,
169
+ name: process.env.CLOUDFLARE_ZONE_ID,
170
+ };
171
+ }
172
+
173
+ const result = await cf.zonesList();
174
+ if (!result.ok) {
175
+ throw new CLIError(`Failed to list zones: ${result.error}`, ExitCode.ERROR);
176
+ }
177
+
178
+ // wrangler zones list outputs lines like "zone-name (zone-id)"
179
+ const lines = result.data.split("\n").filter(Boolean);
180
+ if (lines.length === 0) {
181
+ throw new CLIError(
182
+ "No zones found. Set CLOUDFLARE_ZONE_ID environment variable.",
183
+ ExitCode.ERROR
184
+ );
185
+ }
186
+
187
+ // Parse first zone: "domain.com (abc123...)"
188
+ const match = lines[0].match(/^(.+?)\s+\((.+?)\)\s*$/);
189
+ if (match) {
190
+ return { name: match[1], id: match[2] };
191
+ }
192
+
193
+ // Fallback: use the first line as ID
194
+ return { name: lines[0], id: lines[0] };
195
+ }
196
+
197
+ // ---------------------------------------------------------------------------
198
+ // Command action: waf status
199
+ // ---------------------------------------------------------------------------
200
+
201
+ async function handleStatus(opts: FormatOptions): Promise<void> {
202
+ const cf = new CloudflareService();
203
+
204
+ const zone = await resolveZone(cf);
205
+
206
+ // Fetch WAF setting
207
+ const wafResult = await cfApi<CfWafSetting>(
208
+ "GET",
209
+ `/zones/${zone.id}/settings/waf`
210
+ );
211
+ if (!wafResult.ok) {
212
+ throw new CLIError(
213
+ `Failed to get WAF status: ${wafResult.error}`,
214
+ ExitCode.ERROR
215
+ );
216
+ }
217
+
218
+ // Fetch firewall rules count
219
+ const rulesResult = await cfApi<CfFirewallRule[]>(
220
+ "GET",
221
+ `/zones/${zone.id}/firewall/rules?per_page=50`
222
+ );
223
+ const activeRulesCount = rulesResult.ok ? rulesResult.data.length : 0;
224
+
225
+ // Fetch recent blocks (last 24h analytics overview)
226
+ let recentBlocks = 0;
227
+ const since = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
228
+ const analyticsResult = await cfApi<CfAnalyticsOverview>(
229
+ "GET",
230
+ `/zones/${zone.id}/analytics/dashboard?since=${encodeURIComponent(since)}&continuous=true`
231
+ );
232
+ if (analyticsResult.ok) {
233
+ recentBlocks = analyticsResult.data.totals?.threats ?? 0;
234
+ }
235
+
236
+ const status: WafStatus = {
237
+ enabled: wafResult.data.value === "on",
238
+ mode: wafResult.data.value,
239
+ activeRulesCount,
240
+ recentBlocks,
241
+ zoneId: zone.id,
242
+ zoneName: zone.name,
243
+ };
244
+
245
+ if (opts.json) {
246
+ process.stdout.write(JSON.stringify(status, null, 2) + "\n");
247
+ return;
248
+ }
249
+
250
+ process.stdout.write(theme.heading(`\nWAF status for ${zone.name}\n\n`));
251
+
252
+ const colorFn = status.enabled ? theme.success : theme.error;
253
+ process.stdout.write(
254
+ ` Status: ${colorFn(status.enabled ? `${icons.success} ENABLED` : `${icons.error} DISABLED`)}\n`
255
+ );
256
+ process.stdout.write(` Mode: ${status.mode}\n`);
257
+ process.stdout.write(` Rules: ${status.activeRulesCount} active\n`);
258
+ process.stdout.write(` Blocks: ${status.recentBlocks} (last 24h)\n`);
259
+ process.stdout.write(` Zone ID: ${theme.dim(zone.id)}\n\n`);
260
+ }
261
+
262
+ // ---------------------------------------------------------------------------
263
+ // Command action: waf rules list
264
+ // ---------------------------------------------------------------------------
265
+
266
+ async function handleRulesList(opts: FormatOptions): Promise<void> {
267
+ const cf = new CloudflareService();
268
+ const zone = await resolveZone(cf);
269
+
270
+ const result = await cfApi<CfFirewallRule[]>(
271
+ "GET",
272
+ `/zones/${zone.id}/firewall/rules?per_page=50`
273
+ );
274
+ if (!result.ok) {
275
+ throw new CLIError(
276
+ `Failed to list firewall rules: ${result.error}`,
277
+ ExitCode.ERROR
278
+ );
279
+ }
280
+
281
+ const rules: WafRule[] = result.data.map((r) => ({
282
+ id: r.id,
283
+ description: r.description,
284
+ mode: r.action,
285
+ expression: r.filter?.expression ?? "(unknown)",
286
+ created_at: r.created_on,
287
+ updated_at: r.modified_on,
288
+ }));
289
+
290
+ if (opts.json) {
291
+ process.stdout.write(JSON.stringify(rules, null, 2) + "\n");
292
+ return;
293
+ }
294
+
295
+ if (rules.length === 0) {
296
+ process.stdout.write(
297
+ `${theme.dim(`${icons.info} No firewall rules configured.\n`)}`
298
+ );
299
+ return;
300
+ }
301
+
302
+ process.stdout.write(theme.heading(`\nWAF rules for ${zone.name}\n\n`));
303
+
304
+ const rows = rules.map((r) => ({
305
+ ID: r.id.substring(0, 8) + "...",
306
+ Description: r.description || "(no description)",
307
+ Mode: r.mode,
308
+ Expression:
309
+ r.expression.length > 40
310
+ ? r.expression.substring(0, 37) + "..."
311
+ : r.expression,
312
+ }));
313
+
314
+ formatTable(rows, opts);
315
+ }
316
+
317
+ // ---------------------------------------------------------------------------
318
+ // Command action: waf rules add
319
+ // ---------------------------------------------------------------------------
320
+
321
+ async function handleRulesAdd(
322
+ type: string,
323
+ value: string,
324
+ opts: FormatOptions
325
+ ): Promise<void> {
326
+ const validTypes = ["ip-allowlist", "ip-blocklist", "rate-limit", "custom"];
327
+ if (!validTypes.includes(type)) {
328
+ throw new CLIError(
329
+ `Invalid rule type "${type}". Must be one of: ${validTypes.join(", ")}`,
330
+ ExitCode.INVALID_USAGE
331
+ );
332
+ }
333
+
334
+ const ruleInput: WafRuleInput = {
335
+ type: type as WafRuleInput["type"],
336
+ value,
337
+ };
338
+
339
+ const cf = new CloudflareService();
340
+ const zone = await resolveZone(cf);
341
+
342
+ const expression = buildExpression(ruleInput.type, value);
343
+ const action = mapAction(ruleInput.type);
344
+
345
+ const body = {
346
+ description: ruleInput.description ?? `${ruleInput.type} rule for ${value}`,
347
+ action,
348
+ filter: { expression },
349
+ };
350
+
351
+ // Create filter first, then rule referencing the filter
352
+ const filterResult = await cfApi<{ id: string }>(
353
+ "POST",
354
+ `/zones/${zone.id}/filters`,
355
+ { expression }
356
+ );
357
+ if (!filterResult.ok) {
358
+ throw new CLIError(
359
+ `Failed to create filter: ${filterResult.error}`,
360
+ ExitCode.ERROR
361
+ );
362
+ }
363
+
364
+ const ruleBody = {
365
+ description: body.description,
366
+ action,
367
+ filter: { id: filterResult.data.id },
368
+ };
369
+
370
+ const ruleResult = await cfApi<CfFirewallRule>(
371
+ "POST",
372
+ `/zones/${zone.id}/firewall/rules`,
373
+ [ruleBody] // Cloudflare expects an array of rules
374
+ );
375
+ if (!ruleResult.ok) {
376
+ throw new CLIError(
377
+ `Failed to create firewall rule: ${ruleResult.error}`,
378
+ ExitCode.ERROR
379
+ );
380
+ }
381
+
382
+ // Cloudflare returns an array of created rules
383
+ const created = Array.isArray(ruleResult.data)
384
+ ? (ruleResult.data[0] as unknown as CfFirewallRule)
385
+ : (ruleResult.data as unknown as CfFirewallRule);
386
+
387
+ formatSuccess(
388
+ `WAF rule added (${created.id}) — ${created.action} for "${value}"`,
389
+ opts
390
+ );
391
+ }
392
+
393
+ // ---------------------------------------------------------------------------
394
+ // Command action: waf rules remove
395
+ // ---------------------------------------------------------------------------
396
+
397
+ async function handleRulesRemove(
398
+ ruleId: string,
399
+ opts: FormatOptions
400
+ ): Promise<void> {
401
+ const cf = new CloudflareService();
402
+ const zone = await resolveZone(cf);
403
+
404
+ const result = await cfApi<{ id: string }>(
405
+ "DELETE",
406
+ `/zones/${zone.id}/firewall/rules/${ruleId}`
407
+ );
408
+ if (!result.ok) {
409
+ throw new CLIError(
410
+ `Failed to remove firewall rule: ${result.error}`,
411
+ ExitCode.ERROR
412
+ );
413
+ }
414
+
415
+ formatSuccess(`WAF rule ${ruleId} removed`, opts);
416
+ }
417
+
418
+ // ---------------------------------------------------------------------------
419
+ // Command action: waf mode enable / disable
420
+ // ---------------------------------------------------------------------------
421
+
422
+ async function handleMode(
423
+ mode: "on" | "off",
424
+ opts: FormatOptions
425
+ ): Promise<void> {
426
+ const cf = new CloudflareService();
427
+ const zone = await resolveZone(cf);
428
+
429
+ const result = await cfApi<CfWafSetting>(
430
+ "PATCH",
431
+ `/zones/${zone.id}/settings/waf`,
432
+ { value: mode }
433
+ );
434
+ if (!result.ok) {
435
+ throw new CLIError(
436
+ `Failed to ${mode === "on" ? "enable" : "disable"} WAF: ${result.error}`,
437
+ ExitCode.ERROR
438
+ );
439
+ }
440
+
441
+ const label = mode === "on" ? "enabled" : "disabled";
442
+ formatSuccess(`WAF ${label} on zone ${zone.name}`, opts);
443
+ }
444
+
445
+ // ---------------------------------------------------------------------------
446
+ // Command registration
447
+ // ---------------------------------------------------------------------------
448
+
449
+ /**
450
+ * Registers the `hoox waf` command and all subcommands on the given
451
+ * Commander.js program instance.
452
+ */
453
+ export function registerWafCommand(program: Command): void {
454
+ const waf = program
455
+ .command("waf")
456
+ .description("Manage Cloudflare WAF (Web Application Firewall)");
457
+
458
+ // -- waf status ------------------------------------------------------------
459
+ waf
460
+ .command("status")
461
+ .description(
462
+ "Show WAF status (enabled/disabled, active rules, recent blocks)"
463
+ )
464
+ .action(async (_, cmd: Command) => {
465
+ const opts: FormatOptions = cmd.parent!.parent!.opts();
466
+ try {
467
+ await handleStatus(opts);
468
+ } catch (err) {
469
+ formatError(err instanceof Error ? err : String(err), opts);
470
+ process.exit(err instanceof CLIError ? err.code : ExitCode.ERROR);
471
+ }
472
+ });
473
+
474
+ // -- waf rules -------------------------------------------------------------
475
+ const rules = waf.command("rules").description("Manage WAF firewall rules");
476
+
477
+ rules
478
+ .command("list")
479
+ .description("List all WAF firewall rules")
480
+ .action(async (_, cmd: Command) => {
481
+ const opts: FormatOptions = cmd.parent!.parent!.parent!.opts();
482
+ try {
483
+ await handleRulesList(opts);
484
+ } catch (err) {
485
+ formatError(err instanceof Error ? err : String(err), opts);
486
+ process.exit(err instanceof CLIError ? err.code : ExitCode.ERROR);
487
+ }
488
+ });
489
+
490
+ rules
491
+ .command("add <type> <value>")
492
+ .description(
493
+ "Add a WAF rule. Types: ip-allowlist, ip-blocklist, rate-limit, custom"
494
+ )
495
+ .action(async (type: string, value: string, _, cmd: Command) => {
496
+ const opts: FormatOptions = cmd.parent!.parent!.parent!.opts();
497
+ try {
498
+ await handleRulesAdd(type, value, opts);
499
+ } catch (err) {
500
+ formatError(err instanceof Error ? err : String(err), opts);
501
+ process.exit(err instanceof CLIError ? err.code : ExitCode.ERROR);
502
+ }
503
+ });
504
+
505
+ rules
506
+ .command("remove <ruleId>")
507
+ .description("Remove a WAF rule by ID")
508
+ .action(async (ruleId: string, _, cmd: Command) => {
509
+ const opts: FormatOptions = cmd.parent!.parent!.parent!.opts();
510
+ try {
511
+ await handleRulesRemove(ruleId, opts);
512
+ } catch (err) {
513
+ formatError(err instanceof Error ? err : String(err), opts);
514
+ process.exit(err instanceof CLIError ? err.code : ExitCode.ERROR);
515
+ }
516
+ });
517
+
518
+ // -- waf mode --------------------------------------------------------------
519
+ const mode = waf
520
+ .command("mode")
521
+ .description("Enable or disable WAF protection");
522
+
523
+ mode
524
+ .command("enable")
525
+ .description("Enable WAF protection on the zone")
526
+ .action(async (_, cmd: Command) => {
527
+ const opts: FormatOptions = cmd.parent!.parent!.parent!.opts();
528
+ try {
529
+ await handleMode("on", opts);
530
+ } catch (err) {
531
+ formatError(err instanceof Error ? err : String(err), opts);
532
+ process.exit(err instanceof CLIError ? err.code : ExitCode.ERROR);
533
+ }
534
+ });
535
+
536
+ mode
537
+ .command("disable")
538
+ .description("Disable WAF protection on the zone")
539
+ .action(async (_, cmd: Command) => {
540
+ const opts: FormatOptions = cmd.parent!.parent!.parent!.opts();
541
+ try {
542
+ await handleMode("off", opts);
543
+ } catch (err) {
544
+ formatError(err instanceof Error ? err : String(err), opts);
545
+ process.exit(err instanceof CLIError ? err.code : ExitCode.ERROR);
546
+ }
547
+ });
548
+ }