@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,440 @@
1
+ /**
2
+ * `hoox clone` command — clone worker repos as git submodules.
3
+ *
4
+ * Modes:
5
+ * hoox clone — List workers with clone status (cloned / not cloned)
6
+ * hoox clone --all — Clone all workers from wrangler.jsonc
7
+ * hoox clone <name> — Clone a specific worker by name
8
+ *
9
+ * Repo URLs are derived from: https://github.com/<org>/<worker-name>.git
10
+ * The org is auto-detected from `git remote get-url origin` or set via --org.
11
+ *
12
+ * Uses @clack/prompts spinner for progress feedback.
13
+ */
14
+ import { Command } from "commander";
15
+ import { spinner } from "@clack/prompts";
16
+ import { ConfigService } from "../../services/config/index.js";
17
+ import { icons } from "../../utils/theme.js";
18
+ import {
19
+ formatSuccess,
20
+ formatError,
21
+ formatTable,
22
+ formatJson,
23
+ } from "../../utils/formatters.js";
24
+ import { CLIError, ExitCode } from "../../utils/errors.js";
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Types
28
+ // ---------------------------------------------------------------------------
29
+
30
+ /** Per-worker clone status entry. */
31
+ interface CloneStatus {
32
+ worker: string;
33
+ path: string;
34
+ cloned: boolean;
35
+ repo?: string;
36
+ error?: string;
37
+ }
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Helpers
41
+ // ---------------------------------------------------------------------------
42
+
43
+ /** Extract --json / --quiet from the global program options. */
44
+ function getFormatOptions(cmd: Command) {
45
+ const opts = cmd.optsWithGlobals();
46
+ return { json: Boolean(opts.json), quiet: Boolean(opts.quiet) };
47
+ }
48
+
49
+ /**
50
+ * Resolve the base GitHub URL for worker repos.
51
+ * Tries `git remote get-url origin` first, falls back to a sensible default.
52
+ *
53
+ * Examples:
54
+ * https://github.com/org/repo.git → https://github.com/org
55
+ * git@github.com:org/repo.git → https://github.com/org
56
+ */
57
+ async function resolveRepoBase(): Promise<string> {
58
+ try {
59
+ const proc = Bun.spawn(["git", "remote", "get-url", "origin"], {
60
+ stdout: "pipe",
61
+ stderr: "pipe",
62
+ });
63
+ const exitCode = await proc.exited;
64
+ if (exitCode === 0) {
65
+ const url = (await new Response(proc.stdout).text()).trim();
66
+ const match = url.match(/github\.com[:/]([^/]+)\/.+?(?:\.git)?$/);
67
+ if (match) {
68
+ return `https://github.com/${match[1]}`;
69
+ }
70
+ }
71
+ } catch {
72
+ // Fall through to default
73
+ }
74
+ return "https://github.com/jango-blockchained";
75
+ }
76
+
77
+ /** Build a repo URL from the org base and worker name. */
78
+ function getRepoUrl(orgBase: string, workerName: string): string {
79
+ return `${orgBase}/${workerName}.git`;
80
+ }
81
+
82
+ /**
83
+ * Check whether a worker is already cloned (has a `.git` entry).
84
+ * For git submodules this is a file (not a directory) pointing to the
85
+ * superproject's modules directory.
86
+ */
87
+ async function isWorkerCloned(workerPath: string): Promise<boolean> {
88
+ const dotGit = Bun.file(`${workerPath}/.git`);
89
+ return dotGit.exists();
90
+ }
91
+
92
+ /**
93
+ * Run `git submodule add <repo> <path>` to clone a single worker.
94
+ * Returns a CloneStatus with the outcome.
95
+ */
96
+ async function cloneWorker(
97
+ repoUrl: string,
98
+ targetPath: string,
99
+ name: string,
100
+ cwd: string
101
+ ): Promise<CloneStatus> {
102
+ const proc = Bun.spawn(["git", "submodule", "add", repoUrl, targetPath], {
103
+ cwd,
104
+ stdout: "pipe",
105
+ stderr: "pipe",
106
+ });
107
+
108
+ const exitCode = await proc.exited;
109
+
110
+ if (exitCode !== 0) {
111
+ const stderr = await new Response(proc.stderr).text();
112
+ return {
113
+ worker: name,
114
+ path: targetPath,
115
+ cloned: false,
116
+ repo: repoUrl,
117
+ error:
118
+ stderr.trim() || `git submodule add failed with exit code ${exitCode}`,
119
+ };
120
+ }
121
+
122
+ return {
123
+ worker: name,
124
+ path: targetPath,
125
+ cloned: true,
126
+ repo: repoUrl,
127
+ };
128
+ }
129
+
130
+ /** Run `git submodule update --init --recursive`. */
131
+ async function updateSubmodules(cwd: string): Promise<void> {
132
+ const proc = Bun.spawn(
133
+ ["git", "submodule", "update", "--init", "--recursive"],
134
+ {
135
+ cwd,
136
+ stdout: "pipe",
137
+ stderr: "pipe",
138
+ }
139
+ );
140
+
141
+ const exitCode = await proc.exited;
142
+
143
+ if (exitCode !== 0) {
144
+ const stderr = await new Response(proc.stderr).text();
145
+ throw new Error(
146
+ stderr.trim() || `git submodule update failed with exit code ${exitCode}`
147
+ );
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Build the list of worker clone statuses, checking the filesystem for
153
+ * whether each worker has already been cloned.
154
+ */
155
+ async function buildStatusList(
156
+ configService: ConfigService,
157
+ repoBase: string
158
+ ): Promise<CloneStatus[]> {
159
+ const workers = configService.listWorkers();
160
+ const statuses: CloneStatus[] = [];
161
+
162
+ for (const name of workers) {
163
+ const workerConfig = configService.getWorker(name);
164
+ if (!workerConfig) continue;
165
+
166
+ const cloned = await isWorkerCloned(workerConfig.path);
167
+ statuses.push({
168
+ worker: name,
169
+ path: workerConfig.path,
170
+ cloned,
171
+ repo: getRepoUrl(repoBase, name),
172
+ });
173
+ }
174
+
175
+ return statuses;
176
+ }
177
+
178
+ // ---------------------------------------------------------------------------
179
+ // Command registration
180
+ // ---------------------------------------------------------------------------
181
+
182
+ /**
183
+ * Register the `hoox clone` command on the given commander program.
184
+ *
185
+ * @example
186
+ * ```ts
187
+ * import { registerCloneCommand } from "./commands/clone/index.js";
188
+ * registerCloneCommand(program);
189
+ * ```
190
+ */
191
+ export function registerCloneCommand(program: Command): void {
192
+ program
193
+ .command("clone")
194
+ .summary("Clone worker repositories as git submodules")
195
+ .description(
196
+ `Clone worker repositories from GitHub as git submodules.
197
+
198
+ Workers in the Hoox monorepo are stored as git submodules. This command helps you clone them.
199
+
200
+ MODES:
201
+ hoox clone List clone status of all workers
202
+ hoox clone --all Clone all workers
203
+ hoox clone <name> Clone a specific worker
204
+
205
+ OPTIONS:
206
+ --all Clone all worker repositories
207
+ --org <org> GitHub organization (auto-detected from git remote by default)
208
+
209
+ ARGUMENTS:
210
+ name Optional worker name to clone (e.g., trade-worker, agent-worker)
211
+
212
+ EXAMPLES:
213
+ hoox clone List all workers' clone status
214
+ hoox clone --all Clone all workers
215
+ hoox clone trade-worker Clone specific worker
216
+ hoox clone --org my-org Clone from specific org`
217
+ )
218
+ .option("--all", "Clone all worker repositories")
219
+ .option(
220
+ "--org <org>",
221
+ "GitHub organization (derived from git remote by default)"
222
+ )
223
+ .argument("[name]", "Worker name to clone (omit to list status)")
224
+ .action(
225
+ async (
226
+ name: string | undefined,
227
+ options: { all?: boolean; org?: string }
228
+ ) => {
229
+ const fmt = getFormatOptions(program);
230
+
231
+ try {
232
+ const configService = new ConfigService();
233
+ await configService.load();
234
+
235
+ const workers = configService.listWorkers();
236
+
237
+ // -------------------------------------------------------------------
238
+ // Mode 1: No name, no --all → list clone status
239
+ // -------------------------------------------------------------------
240
+ if (!options.all && !name) {
241
+ const repoBase = options.org
242
+ ? `https://github.com/${options.org}`
243
+ : await resolveRepoBase();
244
+
245
+ const statuses = await buildStatusList(configService, repoBase);
246
+
247
+ if (fmt.json) {
248
+ formatJson(statuses, fmt);
249
+ return;
250
+ }
251
+
252
+ if (fmt.quiet) {
253
+ for (const s of statuses) {
254
+ process.stdout.write(
255
+ `${s.worker}: ${s.cloned ? "cloned" : "not cloned"}\n`
256
+ );
257
+ }
258
+ return;
259
+ }
260
+
261
+ const rows = statuses.map((s) => ({
262
+ Worker: s.worker,
263
+ Status: s.cloned ? "cloned" : "not cloned",
264
+ Repo: s.repo ?? "-",
265
+ }));
266
+ formatTable(rows, fmt);
267
+ return;
268
+ }
269
+
270
+ // -------------------------------------------------------------------
271
+ // Mode 2: --all
272
+ // -------------------------------------------------------------------
273
+ if (options.all) {
274
+ if (workers.length === 0) {
275
+ formatSuccess("No workers defined in wrangler.jsonc", fmt);
276
+ return;
277
+ }
278
+
279
+ const repoBase = options.org
280
+ ? `https://github.com/${options.org}`
281
+ : await resolveRepoBase();
282
+
283
+ const s = spinner();
284
+ s.start(`Cloning ${workers.length} worker(s)...`);
285
+
286
+ const results: CloneStatus[] = [];
287
+
288
+ for (let i = 0; i < workers.length; i++) {
289
+ const name = workers[i];
290
+ const workerConfig = configService.getWorker(name);
291
+ if (!workerConfig) continue;
292
+
293
+ // Skip already cloned workers
294
+ const alreadyCloned = await isWorkerCloned(workerConfig.path);
295
+ if (alreadyCloned) {
296
+ s.message(
297
+ `[${i + 1}/${workers.length}] ${icons.info} ${name} already cloned — skipping`
298
+ );
299
+ results.push({
300
+ worker: name,
301
+ path: workerConfig.path,
302
+ cloned: true,
303
+ repo: getRepoUrl(repoBase, name),
304
+ });
305
+ continue;
306
+ }
307
+
308
+ s.message(`[${i + 1}/${workers.length}] Cloning ${name}...`);
309
+
310
+ const result = await cloneWorker(
311
+ getRepoUrl(repoBase, name),
312
+ workerConfig.path,
313
+ name,
314
+ process.cwd()
315
+ );
316
+ results.push(result);
317
+
318
+ if (result.cloned) {
319
+ s.message(
320
+ `[${i + 1}/${workers.length}] ${icons.success} ${name} cloned`
321
+ );
322
+ } else {
323
+ s.message(
324
+ `[${i + 1}/${workers.length}] ${icons.error} ${name} failed: ${result.error}`
325
+ );
326
+ }
327
+ }
328
+
329
+ // Run submodule update after all clones
330
+ try {
331
+ s.message("Updating submodules...");
332
+ await updateSubmodules(process.cwd());
333
+ } catch (updateErr) {
334
+ const msg =
335
+ updateErr instanceof Error
336
+ ? updateErr.message
337
+ : String(updateErr);
338
+ s.message(`Warning: submodule update failed: ${msg}`);
339
+ }
340
+
341
+ const succeeded = results.filter((r) => r.cloned).length;
342
+ const failed = results.filter((r) => !r.cloned).length;
343
+
344
+ if (failed > 0) {
345
+ s.stop(
346
+ `Clone complete: ${succeeded} succeeded, ${failed} failed`
347
+ );
348
+ process.exitCode = ExitCode.ERROR;
349
+ } else {
350
+ s.stop(`All ${succeeded} worker(s) cloned successfully`);
351
+ }
352
+
353
+ // Print summary table (unless quiet)
354
+ if (!fmt.quiet) {
355
+ const rows = results.map((r) => ({
356
+ Worker: r.worker,
357
+ Status: r.cloned ? "cloned" : "failed",
358
+ Repo: r.repo ?? "-",
359
+ }));
360
+ formatTable(rows, { json: fmt.json, quiet: false });
361
+ }
362
+
363
+ return;
364
+ }
365
+
366
+ // -------------------------------------------------------------------
367
+ // Mode 3: <name> — clone a single worker
368
+ // -------------------------------------------------------------------
369
+ if (name) {
370
+ const workerConfig = configService.getWorker(name);
371
+
372
+ if (!workerConfig) {
373
+ formatError(
374
+ new CLIError(
375
+ `Worker "${name}" not found in wrangler.jsonc`,
376
+ ExitCode.ERROR
377
+ ),
378
+ fmt
379
+ );
380
+ process.exitCode = ExitCode.ERROR;
381
+ return;
382
+ }
383
+
384
+ // Already cloned → early exit with success
385
+ const alreadyCloned = await isWorkerCloned(workerConfig.path);
386
+ if (alreadyCloned) {
387
+ formatSuccess(
388
+ `Worker "${name}" is already cloned at ${workerConfig.path}`,
389
+ fmt
390
+ );
391
+ return;
392
+ }
393
+
394
+ const repoBase = options.org
395
+ ? `https://github.com/${options.org}`
396
+ : await resolveRepoBase();
397
+
398
+ const s = spinner();
399
+ s.start(`Cloning ${name}...`);
400
+
401
+ const result = await cloneWorker(
402
+ getRepoUrl(repoBase, name),
403
+ workerConfig.path,
404
+ name,
405
+ process.cwd()
406
+ );
407
+
408
+ if (result.cloned) {
409
+ // Update submodules after single clone too
410
+ try {
411
+ s.message("Updating submodules...");
412
+ await updateSubmodules(process.cwd());
413
+ } catch {
414
+ // Non-fatal — the clone itself succeeded
415
+ }
416
+
417
+ s.stop(`Successfully cloned ${name}`);
418
+ formatSuccess(`Cloned ${name} from ${result.repo}`, fmt);
419
+ } else {
420
+ s.stop(`Failed to clone ${name}`);
421
+ formatError(
422
+ new CLIError(
423
+ `Failed to clone "${name}": ${result.error}`,
424
+ ExitCode.ERROR
425
+ ),
426
+ fmt
427
+ );
428
+ process.exitCode = ExitCode.ERROR;
429
+ }
430
+
431
+ return;
432
+ }
433
+ } catch (err) {
434
+ const message = err instanceof Error ? err.message : String(err);
435
+ formatError(message, fmt);
436
+ process.exitCode = ExitCode.ERROR;
437
+ }
438
+ }
439
+ );
440
+ }
@@ -0,0 +1 @@
1
+ export { registerCloneCommand } from "./clone-command.js";