@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
package/src/ui/menu.ts ADDED
@@ -0,0 +1,473 @@
1
+ /**
2
+ * Interactive TUI loop for the Hoox CLI.
3
+ *
4
+ * When `hoox` is called with no arguments, this module takes over:
5
+ * shows the ASCII banner, an interactive clack menu, dispatches to
6
+ * the appropriate Commander action, and returns to the menu.
7
+ */
8
+
9
+ import { Command } from "commander";
10
+ import {
11
+ intro,
12
+ outro,
13
+ select,
14
+ text,
15
+ confirm,
16
+ log,
17
+ isCancel,
18
+ cancel,
19
+ } from "@clack/prompts";
20
+ import { renderBanner } from "./banner.js";
21
+ import { CLIError } from "../utils/errors.js";
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Main entry
25
+ // ---------------------------------------------------------------------------
26
+
27
+ /**
28
+ * Run the interactive TUI loop.
29
+ * - Shows the banner once
30
+ * - Shows a category-selection menu
31
+ * - Each category leads to sub-actions
32
+ * - After each action, returns to the menu
33
+ * - Exit or Ctrl+C to quit
34
+ */
35
+ export async function runInteractiveTUI(program: Command): Promise<void> {
36
+ // Print banner directly, then start clack prompt session
37
+ process.stdout.write(renderBanner() + "\n");
38
+ intro("hoox");
39
+
40
+ while (true) {
41
+ const category = await showMainMenu();
42
+ if (category === "__exit") break;
43
+
44
+ const result = await handleCategory(category, program);
45
+ if (result === "exit") break;
46
+ // "back" or "continue" → loop back to main menu
47
+ }
48
+
49
+ outro("See you later!");
50
+ }
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Main menu
54
+ // ---------------------------------------------------------------------------
55
+
56
+ const MAIN_CATEGORIES = [
57
+ { value: "deploy", label: "Deploy", hint: "workers, dashboard" },
58
+ { value: "develop", label: "Develop", hint: "dev server, init project" },
59
+ {
60
+ value: "manage",
61
+ label: "Manage",
62
+ hint: "infrastructure, config, secrets",
63
+ },
64
+ {
65
+ value: "monitor",
66
+ label: "Monitor",
67
+ hint: "diagnostics, health, logs, tests",
68
+ },
69
+ { value: "tools", label: "Tools", hint: "WAF, clone worker, dashboard UI" },
70
+ { value: "__exit", label: "Exit" },
71
+ ] as const;
72
+
73
+ async function showMainMenu(): Promise<string> {
74
+ const choice = await select({
75
+ message: "What would you like to do?",
76
+ options: [...MAIN_CATEGORIES],
77
+ });
78
+
79
+ if (isCancel(choice)) {
80
+ cancel("Operation cancelled.");
81
+ return "__exit";
82
+ }
83
+
84
+ return choice;
85
+ }
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // Category handlers
89
+ // ---------------------------------------------------------------------------
90
+
91
+ /**
92
+ * Handle a selected category. Returns "back", "exit", or "continue".
93
+ */
94
+ async function handleCategory(
95
+ category: string,
96
+ program: Command
97
+ ): Promise<"back" | "exit" | "continue"> {
98
+ switch (category) {
99
+ case "deploy":
100
+ return showDeployMenu(program);
101
+ case "develop":
102
+ return showDevelopMenu(program);
103
+ case "manage":
104
+ return showManageMenu(program);
105
+ case "monitor":
106
+ return showMonitorMenu(program);
107
+ case "tools":
108
+ return showToolsMenu(program);
109
+ default:
110
+ return "back";
111
+ }
112
+ }
113
+
114
+ // ---------------------------------------------------------------------------
115
+ // Deploy sub-menu
116
+ // ---------------------------------------------------------------------------
117
+
118
+ async function showDeployMenu(
119
+ program: Command
120
+ ): Promise<"back" | "exit" | "continue"> {
121
+ while (true) {
122
+ const choice = await select({
123
+ message: "Deploy",
124
+ options: [
125
+ { value: "all", label: "All workers + dashboard", hint: "recommended" },
126
+ { value: "workers", label: "Workers only" },
127
+ { value: "single", label: "Single worker..." },
128
+ { value: "dashboard", label: "Dashboard only" },
129
+ { value: "__back", label: "◀ Back to main menu" },
130
+ ],
131
+ });
132
+
133
+ if (isCancel(choice)) return "back";
134
+ if (choice === "__back") return "continue";
135
+
136
+ if (choice === "single") {
137
+ const name = await text({
138
+ message: "Which worker?",
139
+ placeholder: "e.g. hoox, trade-worker, d1-worker",
140
+ validate: (v) =>
141
+ v === undefined || v.length === 0
142
+ ? "Worker name is required"
143
+ : undefined,
144
+ });
145
+ if (isCancel(name)) continue;
146
+ if (!name) continue;
147
+ await runCommand(program, `deploy worker ${name}`);
148
+ } else {
149
+ const cmd = choice === "all" ? "deploy all" : `deploy ${choice}`;
150
+ await runCommand(program, cmd);
151
+ }
152
+ }
153
+ }
154
+
155
+ // ---------------------------------------------------------------------------
156
+ // Develop sub-menu
157
+ // ---------------------------------------------------------------------------
158
+
159
+ async function showDevelopMenu(
160
+ program: Command
161
+ ): Promise<"back" | "exit" | "continue"> {
162
+ while (true) {
163
+ const choice = await select({
164
+ message: "Develop",
165
+ options: [
166
+ {
167
+ value: "dev start",
168
+ label: "Start dev server",
169
+ hint: "runs all workers locally",
170
+ },
171
+ { value: "init", label: "Init project", hint: "bootstrap new project" },
172
+ { value: "__back", label: "◀ Back to main menu" },
173
+ ],
174
+ });
175
+
176
+ if (isCancel(choice)) return "back";
177
+ if (choice === "__back") return "continue";
178
+
179
+ await runCommand(program, choice);
180
+ }
181
+ }
182
+
183
+ // ---------------------------------------------------------------------------
184
+ // Manage sub-menu
185
+ // ---------------------------------------------------------------------------
186
+
187
+ async function showManageMenu(
188
+ program: Command
189
+ ): Promise<"back" | "exit" | "continue"> {
190
+ while (true) {
191
+ const choice = await select({
192
+ message: "Manage",
193
+ options: [
194
+ { value: "infra", label: "Infrastructure", hint: "D1, R2, KV, Queues" },
195
+ {
196
+ value: "config",
197
+ label: "Configuration",
198
+ hint: "show / set wrangler.jsonc",
199
+ },
200
+ { value: "secrets", label: "Secrets", hint: "list, set, sync" },
201
+ { value: "keys", label: "Auth keys", hint: "generate, list" },
202
+ { value: "waf", label: "WAF rules" },
203
+ { value: "__back", label: "◀ Back to main menu" },
204
+ ],
205
+ });
206
+
207
+ if (isCancel(choice)) return "back";
208
+ if (choice === "__back") return "continue";
209
+
210
+ switch (choice) {
211
+ case "infra":
212
+ await runCommand(program, "infra");
213
+ break;
214
+ case "config":
215
+ await showConfigSubMenu(program);
216
+ break;
217
+ case "secrets":
218
+ await showSecretsSubMenu(program);
219
+ break;
220
+ case "keys":
221
+ await showKeysSubMenu(program);
222
+ break;
223
+ case "waf":
224
+ await runCommand(program, "waf");
225
+ break;
226
+ }
227
+ }
228
+ }
229
+
230
+ // ---------------------------------------------------------------------------
231
+ // Config sub-sub-menu
232
+ // ---------------------------------------------------------------------------
233
+
234
+ async function showConfigSubMenu(program: Command): Promise<void> {
235
+ const choice = await select({
236
+ message: "Configuration",
237
+ options: [
238
+ { value: "show", label: "Show config", hint: "display wrangler.jsonc" },
239
+ { value: "set", label: "Set a value..." },
240
+ { value: "__back", label: "◀ Back to Manage" },
241
+ ],
242
+ });
243
+
244
+ if (isCancel(choice) || choice === "__back") return;
245
+
246
+ if (choice === "set") {
247
+ const key = await text({
248
+ message: "Config key (dot-separated path)",
249
+ placeholder: "e.g. global.cloudflare_account_id",
250
+ validate: (v) =>
251
+ v === undefined || v.length === 0 ? "Key is required" : undefined,
252
+ });
253
+ if (isCancel(key) || !key) return;
254
+
255
+ const value = await text({
256
+ message: `Value for "${key}"`,
257
+ placeholder: "enter value",
258
+ });
259
+ if (isCancel(value) || value === undefined) return;
260
+
261
+ await runCommand(program, `config set ${key} ${value ?? ""}`);
262
+ } else {
263
+ await runCommand(program, "config show");
264
+ }
265
+ }
266
+
267
+ // ---------------------------------------------------------------------------
268
+ // Secrets sub-sub-menu
269
+ // ---------------------------------------------------------------------------
270
+
271
+ async function showSecretsSubMenu(program: Command): Promise<void> {
272
+ const choice = await select({
273
+ message: "Secrets",
274
+ options: [
275
+ { value: "list", label: "List secrets" },
276
+ { value: "set", label: "Set a secret..." },
277
+ { value: "sync", label: "Sync to Cloudflare" },
278
+ { value: "__back", label: "◀ Back to Manage" },
279
+ ],
280
+ });
281
+
282
+ if (isCancel(choice) || choice === "__back") return;
283
+
284
+ switch (choice) {
285
+ case "list":
286
+ await runCommand(program, "config secrets list");
287
+ break;
288
+ case "set": {
289
+ const worker = await text({
290
+ message: "Worker name",
291
+ placeholder: "e.g. hoox, trade-worker",
292
+ validate: (v) =>
293
+ v === undefined || v.length === 0
294
+ ? "Worker name is required"
295
+ : undefined,
296
+ });
297
+ if (isCancel(worker) || !worker) return;
298
+
299
+ const secretName = await text({
300
+ message: "Secret name",
301
+ placeholder: "e.g. API_KEY, DATABASE_URL",
302
+ validate: (v) =>
303
+ v === undefined || v.length === 0
304
+ ? "Secret name is required"
305
+ : undefined,
306
+ });
307
+ if (isCancel(secretName) || !secretName) return;
308
+
309
+ await runCommand(program, `config secrets set ${worker} ${secretName}`);
310
+ break;
311
+ }
312
+ case "sync":
313
+ await runCommand(program, "config secrets sync");
314
+ break;
315
+ }
316
+ }
317
+
318
+ // ---------------------------------------------------------------------------
319
+ // Keys sub-sub-menu
320
+ // ---------------------------------------------------------------------------
321
+
322
+ async function showKeysSubMenu(program: Command): Promise<void> {
323
+ const choice = await select({
324
+ message: "Auth Keys",
325
+ options: [
326
+ { value: "generate", label: "Generate new keys" },
327
+ { value: "list", label: "List existing keys" },
328
+ { value: "__back", label: "◀ Back to Manage" },
329
+ ],
330
+ });
331
+
332
+ if (isCancel(choice) || choice === "__back") return;
333
+
334
+ if (choice === "generate") {
335
+ const proceed = await confirm({
336
+ message:
337
+ "Generate new internal auth keys? This will create .keys/*.env files.",
338
+ initialValue: false,
339
+ });
340
+ if (isCancel(proceed) || !proceed) return;
341
+ }
342
+
343
+ await runCommand(program, `config keys ${choice}`);
344
+ }
345
+
346
+ // ---------------------------------------------------------------------------
347
+ // Monitor sub-menu
348
+ // ---------------------------------------------------------------------------
349
+
350
+ async function showMonitorMenu(
351
+ program: Command
352
+ ): Promise<"back" | "exit" | "continue"> {
353
+ while (true) {
354
+ const choice = await select({
355
+ message: "Monitor",
356
+ options: [
357
+ {
358
+ value: "check setup",
359
+ label: "Run diagnostics",
360
+ hint: "full system validation",
361
+ },
362
+ {
363
+ value: "check health",
364
+ label: "Health check",
365
+ hint: "worker connectivity",
366
+ },
367
+ { value: "check fix", label: "Auto-repair", hint: "fix common issues" },
368
+ { value: "__logs", label: "View logs", hint: "real-time log tailing" },
369
+ { value: "test", label: "Run tests" },
370
+ { value: "__back", label: "◀ Back to main menu" },
371
+ ],
372
+ });
373
+
374
+ if (isCancel(choice)) return "back";
375
+ if (choice === "__back") return "continue";
376
+
377
+ if (choice === "__logs") {
378
+ const target = await select({
379
+ message: "Tail logs for",
380
+ options: [
381
+ { value: "all", label: "All workers" },
382
+ { value: "single", label: "A specific worker..." },
383
+ { value: "__back", label: "◀ Back" },
384
+ ],
385
+ });
386
+ if (isCancel(target) || target === "__back") continue;
387
+ if (target === "single") {
388
+ const name = await text({
389
+ message: "Worker name",
390
+ placeholder: "e.g. hoox, trade-worker, d1-worker",
391
+ });
392
+ if (isCancel(name) || !name) continue;
393
+ await runCommand(program, `logs worker ${name}`);
394
+ } else {
395
+ await runCommand(program, "logs all");
396
+ }
397
+ continue;
398
+ }
399
+
400
+ await runCommand(program, choice);
401
+ }
402
+ }
403
+
404
+ // ---------------------------------------------------------------------------
405
+ // Tools sub-menu
406
+ // ---------------------------------------------------------------------------
407
+
408
+ async function showToolsMenu(
409
+ program: Command
410
+ ): Promise<"back" | "exit" | "continue"> {
411
+ while (true) {
412
+ const choice = await select({
413
+ message: "Tools",
414
+ options: [
415
+ {
416
+ value: "clone",
417
+ label: "Clone worker",
418
+ hint: "scaffold from template",
419
+ },
420
+ {
421
+ value: "dashboard",
422
+ label: "Dashboard UI",
423
+ hint: "manage dashboard URLs",
424
+ },
425
+ { value: "__back", label: "◀ Back to main menu" },
426
+ ],
427
+ });
428
+
429
+ if (isCancel(choice)) return "back";
430
+ if (choice === "__back") return "continue";
431
+
432
+ await runCommand(program, choice);
433
+ }
434
+ }
435
+
436
+ // ---------------------------------------------------------------------------
437
+ // Command execution
438
+ // ---------------------------------------------------------------------------
439
+
440
+ /**
441
+ * Run a Commander command string inside the TUI loop.
442
+ *
443
+ * Delegates to `program.parseAsync()` with the given arguments.
444
+ * Wraps in try-catch so errors don't terminate the loop.
445
+ */
446
+ async function runCommand(program: Command, commandStr: string): Promise<void> {
447
+ const args = commandStr.split(" ");
448
+
449
+ try {
450
+ // from: "user" means Commander expects raw user arguments only —
451
+ // NOT [node, script, ...args] format. So just pass the args directly.
452
+ await program.parseAsync(args, {
453
+ from: "user",
454
+ });
455
+ } catch (err) {
456
+ // Check if it's a Commander informational exit (help/version)
457
+ if (
458
+ err instanceof Error &&
459
+ "code" in err &&
460
+ (err as { code: string }).code === "commander.helpDisplayed"
461
+ ) {
462
+ return;
463
+ }
464
+
465
+ const message =
466
+ err instanceof CLIError
467
+ ? err.message
468
+ : err instanceof Error
469
+ ? err.message
470
+ : String(err);
471
+ log.error(`Command failed: ${message}`);
472
+ }
473
+ }
@@ -0,0 +1,69 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { CLIError, ExitCode } from "./errors.js";
3
+
4
+ // CommandItem is defined in errors.js and re-exported; avoid redeclare
5
+
6
+ describe("ExitCode", () => {
7
+ it("has correct numeric values", () => {
8
+ expect(ExitCode.SUCCESS).toBe(0);
9
+ expect(ExitCode.ERROR).toBe(1);
10
+ expect(ExitCode.INVALID_USAGE).toBe(2);
11
+ expect(ExitCode.INFRA_UNAVAILABLE).toBe(3);
12
+ });
13
+
14
+ it("is a numeric enum with expected values", () => {
15
+ expect(ExitCode.SUCCESS).toBe(0);
16
+ expect(ExitCode.ERROR).toBe(1);
17
+ expect(ExitCode.INVALID_USAGE).toBe(2);
18
+ expect(ExitCode.INFRA_UNAVAILABLE).toBe(3);
19
+ // All values are distinct
20
+ const values = [
21
+ ExitCode.SUCCESS,
22
+ ExitCode.ERROR,
23
+ ExitCode.INVALID_USAGE,
24
+ ExitCode.INFRA_UNAVAILABLE,
25
+ ];
26
+ expect(new Set(values).size).toBe(4);
27
+ });
28
+ });
29
+
30
+ describe("CLIError", () => {
31
+ it("constructs with message and defaults", () => {
32
+ const err = new CLIError("something went wrong");
33
+ expect(err).toBeInstanceOf(Error);
34
+ expect(err).toBeInstanceOf(CLIError);
35
+ expect(err.name).toBe("CLIError");
36
+ expect(err.message).toBe("something went wrong");
37
+ expect(err.code).toBe(ExitCode.ERROR);
38
+ expect(err.details).toBeUndefined();
39
+ expect(err.recoverable).toBe(false);
40
+ });
41
+
42
+ it("constructs with all fields", () => {
43
+ const err = new CLIError(
44
+ "invalid input",
45
+ ExitCode.INVALID_USAGE,
46
+ "Expected --name to be a non-empty string",
47
+ true
48
+ );
49
+ expect(err.message).toBe("invalid input");
50
+ expect(err.code).toBe(ExitCode.INVALID_USAGE);
51
+ expect(err.details).toBe("Expected --name to be a non-empty string");
52
+ expect(err.recoverable).toBe(true);
53
+ });
54
+
55
+ it("supports INFRA_UNAVAILABLE code", () => {
56
+ const err = new CLIError(
57
+ "Cloudflare API unreachable",
58
+ ExitCode.INFRA_UNAVAILABLE,
59
+ "Check your network or try again later"
60
+ );
61
+ expect(err.code).toBe(3);
62
+ expect(err.recoverable).toBe(false);
63
+ });
64
+
65
+ it("has a string representation that includes the name", () => {
66
+ const err = new CLIError("fail");
67
+ expect(err.toString()).toBe("CLIError: fail");
68
+ });
69
+ });
@@ -0,0 +1,23 @@
1
+ /**
2
+ * CLIError — structured error class for the Hoox CLI.
3
+ * Provides exit codes, optional details, and recoverable flag.
4
+ */
5
+
6
+ export const enum ExitCode {
7
+ SUCCESS = 0,
8
+ ERROR = 1,
9
+ INVALID_USAGE = 2,
10
+ INFRA_UNAVAILABLE = 3,
11
+ }
12
+
13
+ export class CLIError extends Error {
14
+ constructor(
15
+ message: string,
16
+ public readonly code: ExitCode = ExitCode.ERROR,
17
+ public readonly details?: string,
18
+ public readonly recoverable = false
19
+ ) {
20
+ super(message);
21
+ this.name = "CLIError";
22
+ }
23
+ }