@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.
- package/README.md +403 -0
- package/bin/hoox.js +12 -0
- package/package.json +60 -0
- package/src/commands/check/check-command.test.ts +468 -0
- package/src/commands/check/check-command.ts +1144 -0
- package/src/commands/check/index.ts +10 -0
- package/src/commands/check/prerequisites-command.test.ts +19 -0
- package/src/commands/check/prerequisites-command.ts +92 -0
- package/src/commands/check/types.ts +103 -0
- package/src/commands/clone/clone-command.test.ts +442 -0
- package/src/commands/clone/clone-command.ts +440 -0
- package/src/commands/clone/index.ts +1 -0
- package/src/commands/config/config-command.test.ts +583 -0
- package/src/commands/config/config-command.ts +901 -0
- package/src/commands/config/env-command.test.ts +43 -0
- package/src/commands/config/env-command.ts +314 -0
- package/src/commands/config/index.ts +3 -0
- package/src/commands/config/kv-command.test.ts +14 -0
- package/src/commands/config/kv-command.ts +329 -0
- package/src/commands/dashboard/dashboard-command.test.ts +47 -0
- package/src/commands/dashboard/dashboard-command.ts +127 -0
- package/src/commands/dashboard/index.ts +1 -0
- package/src/commands/db/db-command.test.ts +21 -0
- package/src/commands/db/db-command.ts +314 -0
- package/src/commands/db/index.ts +1 -0
- package/src/commands/deploy/deploy-command.test.ts +304 -0
- package/src/commands/deploy/deploy-command.ts +1053 -0
- package/src/commands/deploy/index.ts +2 -0
- package/src/commands/deploy/telegram-service.ts +61 -0
- package/src/commands/deploy/types.ts +34 -0
- package/src/commands/dev/dev-command.test.ts +383 -0
- package/src/commands/dev/dev-command.ts +407 -0
- package/src/commands/dev/index.ts +1 -0
- package/src/commands/infra/index.ts +5 -0
- package/src/commands/infra/infra-command.test.ts +719 -0
- package/src/commands/infra/infra-command.ts +940 -0
- package/src/commands/infra/types.ts +23 -0
- package/src/commands/init/index.ts +1 -0
- package/src/commands/init/init-command.test.ts +827 -0
- package/src/commands/init/init-command.ts +627 -0
- package/src/commands/init/types.ts +185 -0
- package/src/commands/monitor/index.ts +2 -0
- package/src/commands/monitor/monitor-command.test.ts +235 -0
- package/src/commands/monitor/monitor-command.ts +245 -0
- package/src/commands/monitor/monitor-service.ts +50 -0
- package/src/commands/monitor/types.ts +13 -0
- package/src/commands/repair/index.ts +2 -0
- package/src/commands/repair/repair-command.test.ts +204 -0
- package/src/commands/repair/repair-command.ts +199 -0
- package/src/commands/repair/repair-service.ts +102 -0
- package/src/commands/repair/types.ts +13 -0
- package/src/commands/test/index.ts +2 -0
- package/src/commands/test/test-command.test.ts +319 -0
- package/src/commands/test/test-command.ts +412 -0
- package/src/commands/waf/index.ts +2 -0
- package/src/commands/waf/types.ts +48 -0
- package/src/commands/waf/waf-command.test.ts +506 -0
- package/src/commands/waf/waf-command.ts +548 -0
- package/src/index.ts +198 -0
- package/src/services/cloudflare/cloudflare-service.test.ts +654 -0
- package/src/services/cloudflare/cloudflare-service.ts +435 -0
- package/src/services/cloudflare/index.ts +2 -0
- package/src/services/cloudflare/types.ts +29 -0
- package/src/services/config/config-service.test.ts +395 -0
- package/src/services/config/config-service.ts +207 -0
- package/src/services/config/index.ts +9 -0
- package/src/services/config/types.ts +66 -0
- package/src/services/db/db-service.test.ts +51 -0
- package/src/services/db/db-service.ts +140 -0
- package/src/services/db/index.ts +1 -0
- package/src/services/docker/docker-service.ts +155 -0
- package/src/services/docker/index.ts +1 -0
- package/src/services/env/env-service.test.ts +210 -0
- package/src/services/env/env-service.ts +156 -0
- package/src/services/env/index.ts +1 -0
- package/src/services/kv/index.ts +1 -0
- package/src/services/kv/kv-sync-service.test.ts +38 -0
- package/src/services/kv/kv-sync-service.ts +151 -0
- package/src/services/prerequisites/index.ts +1 -0
- package/src/services/prerequisites/prerequisites-service.test.ts +89 -0
- package/src/services/prerequisites/prerequisites-service.ts +269 -0
- package/src/services/prerequisites/types.ts +48 -0
- package/src/services/secrets/index.ts +12 -0
- package/src/services/secrets/secrets-service.test.ts +486 -0
- package/src/services/secrets/secrets-service.ts +293 -0
- package/src/services/secrets/types.ts +57 -0
- package/src/ui/banner.ts +52 -0
- package/src/ui/index.ts +8 -0
- package/src/ui/menu.ts +473 -0
- package/src/utils/errors.test.ts +69 -0
- package/src/utils/errors.ts +23 -0
- package/src/utils/formatters.test.ts +180 -0
- package/src/utils/formatters.ts +252 -0
- 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
|
+
}
|