@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,940 @@
1
+ /**
2
+ * `hoox infra` command group — Cloudflare infrastructure management.
3
+ *
4
+ * Subcommands:
5
+ * provision — Auto-provision D1/KV/R2/Queues from wrangler.jsonc
6
+ * d1 [list|create|delete] — D1 database management
7
+ * kv [list|create|delete] — KV namespace management
8
+ * r2 [list|create|delete] — R2 bucket management
9
+ * queues [list|create|delete] — Queue management
10
+ *
11
+ * All operations delegate to CloudflareService. Output respects --json / --quiet.
12
+ */
13
+
14
+ import { Command } from "commander";
15
+ import { spinner } from "@clack/prompts";
16
+ import { parse } from "jsonc-parser";
17
+ import { resolve } from "node:path";
18
+ import { CloudflareService } from "../../services/cloudflare/index.js";
19
+ import { ConfigService } from "../../services/config/index.js";
20
+ import {
21
+ formatTable,
22
+ formatSuccess,
23
+ formatError,
24
+ formatJson,
25
+ } from "../../utils/formatters.js";
26
+ import { CLIError, ExitCode } from "../../utils/errors.js";
27
+ import { theme } from "../../utils/theme.js";
28
+ import type { InfraOptions, ProvisionItem, ProvisionResult } from "./types.js";
29
+ import type { WranglerResult } from "../../services/cloudflare/types.js";
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Helpers
33
+ // ---------------------------------------------------------------------------
34
+
35
+ /** Extract global options (--json, --quiet) from a commander Command. */
36
+ function getOptions(cmd: Command): InfraOptions {
37
+ const opts = cmd.optsWithGlobals();
38
+ return { json: opts.json, quiet: opts.quiet };
39
+ }
40
+
41
+ /**
42
+ * Try to parse a wrangler JSON list output and display as a table.
43
+ * Falls back to raw text output if parsing fails.
44
+ */
45
+ function displayListResult(
46
+ result: WranglerResult<string>,
47
+ opts: InfraOptions,
48
+ columns?: string[]
49
+ ): void {
50
+ if (!result.ok) {
51
+ formatError(new CLIError(result.error, ExitCode.ERROR), opts);
52
+ return;
53
+ }
54
+
55
+ const data = result.data;
56
+
57
+ // Try JSON parse for structured output
58
+ try {
59
+ const parsed = JSON.parse(data);
60
+ if (Array.isArray(parsed) && parsed.length > 0) {
61
+ if (opts.json) {
62
+ formatJson(parsed, opts);
63
+ return;
64
+ }
65
+ const rows = parsed.map((item: Record<string, unknown>) => {
66
+ if (columns) {
67
+ const row: Record<string, string> = {};
68
+ for (const col of columns) {
69
+ row[col] = String(item[col] ?? "");
70
+ }
71
+ return row;
72
+ }
73
+ // Show all top-level keys
74
+ const row: Record<string, string> = {};
75
+ for (const [k, v] of Object.entries(item)) {
76
+ if (typeof v !== "object" || v === null) {
77
+ row[k] = String(v);
78
+ }
79
+ }
80
+ return row;
81
+ });
82
+ formatTable(rows, opts);
83
+ return;
84
+ }
85
+ if (
86
+ typeof parsed === "object" &&
87
+ parsed !== null &&
88
+ !Array.isArray(parsed)
89
+ ) {
90
+ if (opts.json) {
91
+ formatJson(parsed, opts);
92
+ return;
93
+ }
94
+ }
95
+ } catch {
96
+ // Not JSON — fall through to raw output
97
+ }
98
+
99
+ // Raw text output
100
+ if (opts.json) {
101
+ formatJson({ output: data }, opts);
102
+ } else {
103
+ process.stdout.write(data + "\n");
104
+ }
105
+ }
106
+
107
+ /** Handle a create operation: run the service call, show success/error. */
108
+ async function handleCreate(
109
+ name: string,
110
+ label: string,
111
+ fn: (name: string) => Promise<WranglerResult<string>>,
112
+ opts: InfraOptions
113
+ ): Promise<void> {
114
+ if (!opts.quiet) {
115
+ process.stdout.write(`${theme.label(`Creating ${label}:`)} ${name}\n`);
116
+ }
117
+
118
+ const result = await fn(name);
119
+ if (result.ok) {
120
+ formatSuccess(`${label} "${name}" created`, opts);
121
+ } else {
122
+ formatError(new CLIError(result.error, ExitCode.ERROR), opts);
123
+ }
124
+ }
125
+
126
+ /** Handle a delete operation: run the service call, show success/error. */
127
+ async function handleDelete(
128
+ name: string,
129
+ label: string,
130
+ fn: (name: string) => Promise<WranglerResult<string>>,
131
+ opts: InfraOptions
132
+ ): Promise<void> {
133
+ if (!opts.quiet) {
134
+ process.stdout.write(`${theme.label(`Deleting ${label}:`)} ${name}\n`);
135
+ }
136
+
137
+ const result = await fn(name);
138
+ if (result.ok) {
139
+ formatSuccess(`${label} "${name}" deleted`, opts);
140
+ } else {
141
+ formatError(new CLIError(result.error, ExitCode.ERROR), opts);
142
+ }
143
+ }
144
+
145
+ // ---------------------------------------------------------------------------
146
+ // D1 handlers
147
+ // ---------------------------------------------------------------------------
148
+
149
+ async function doD1List(
150
+ opts: InfraOptions,
151
+ cf?: CloudflareService
152
+ ): Promise<void> {
153
+ const cloudflare = cf ?? new CloudflareService();
154
+ const result = await cloudflare.d1List();
155
+ displayListResult(result, opts, ["name", "uuid", "version", "num_tables"]);
156
+ }
157
+
158
+ async function doD1Create(
159
+ name: string,
160
+ opts: InfraOptions,
161
+ cf?: CloudflareService
162
+ ): Promise<void> {
163
+ const cloudflare = cf ?? new CloudflareService();
164
+ await handleCreate(name, "D1 database", (n) => cloudflare.d1Create(n), opts);
165
+ }
166
+
167
+ async function doD1Delete(
168
+ name: string,
169
+ opts: InfraOptions,
170
+ cf?: CloudflareService
171
+ ): Promise<void> {
172
+ const cloudflare = cf ?? new CloudflareService();
173
+ await handleDelete(name, "D1 database", (n) => cloudflare.d1Delete(n), opts);
174
+ }
175
+
176
+ // ---------------------------------------------------------------------------
177
+ // KV handlers
178
+ // ---------------------------------------------------------------------------
179
+
180
+ async function doKvList(
181
+ opts: InfraOptions,
182
+ cf?: CloudflareService
183
+ ): Promise<void> {
184
+ const cloudflare = cf ?? new CloudflareService();
185
+ const result = await cloudflare.kvList();
186
+ displayListResult(result, opts);
187
+ }
188
+
189
+ async function doKvCreate(
190
+ name: string,
191
+ opts: InfraOptions,
192
+ cf?: CloudflareService
193
+ ): Promise<void> {
194
+ const cloudflare = cf ?? new CloudflareService();
195
+ await handleCreate(name, "KV namespace", (n) => cloudflare.kvCreate(n), opts);
196
+ }
197
+
198
+ async function doKvDelete(
199
+ id: string,
200
+ opts: InfraOptions,
201
+ cf?: CloudflareService
202
+ ): Promise<void> {
203
+ const cloudflare = cf ?? new CloudflareService();
204
+ await handleDelete(id, "KV namespace", (n) => cloudflare.kvDelete(n), opts);
205
+ }
206
+
207
+ // ---------------------------------------------------------------------------
208
+ // R2 handlers
209
+ // ---------------------------------------------------------------------------
210
+
211
+ async function doR2List(
212
+ opts: InfraOptions,
213
+ cf?: CloudflareService
214
+ ): Promise<void> {
215
+ const cloudflare = cf ?? new CloudflareService();
216
+ const result = await cloudflare.r2List();
217
+ displayListResult(result, opts);
218
+ }
219
+
220
+ async function doR2Create(
221
+ name: string,
222
+ opts: InfraOptions,
223
+ cf?: CloudflareService
224
+ ): Promise<void> {
225
+ const cloudflare = cf ?? new CloudflareService();
226
+ await handleCreate(name, "R2 bucket", (n) => cloudflare.r2Create(n), opts);
227
+ }
228
+
229
+ async function doR2Delete(
230
+ name: string,
231
+ opts: InfraOptions,
232
+ cf?: CloudflareService
233
+ ): Promise<void> {
234
+ const cloudflare = cf ?? new CloudflareService();
235
+ await handleDelete(name, "R2 bucket", (n) => cloudflare.r2Delete(n), opts);
236
+ }
237
+
238
+ // ---------------------------------------------------------------------------
239
+ // Queues handlers
240
+ // ---------------------------------------------------------------------------
241
+
242
+ async function doQueueList(
243
+ opts: InfraOptions,
244
+ cf?: CloudflareService
245
+ ): Promise<void> {
246
+ const cloudflare = cf ?? new CloudflareService();
247
+ const result = await cloudflare.queueList();
248
+ displayListResult(result, opts);
249
+ }
250
+
251
+ async function doQueueCreate(
252
+ name: string,
253
+ opts: InfraOptions,
254
+ cf?: CloudflareService
255
+ ): Promise<void> {
256
+ const cloudflare = cf ?? new CloudflareService();
257
+ await handleCreate(name, "Queue", (n) => cloudflare.queueCreate(n), opts);
258
+ }
259
+
260
+ async function doQueueDelete(
261
+ name: string,
262
+ opts: InfraOptions,
263
+ cf?: CloudflareService
264
+ ): Promise<void> {
265
+ const cloudflare = cf ?? new CloudflareService();
266
+ await handleDelete(name, "Queue", (n) => cloudflare.queueDelete(n), opts);
267
+ }
268
+
269
+ // ---------------------------------------------------------------------------
270
+ // Provision handler
271
+ // ---------------------------------------------------------------------------
272
+
273
+ /**
274
+ * Provision all infrastructure by scanning each enabled worker's wrangler.jsonc
275
+ * for D1 databases, KV namespaces, R2 buckets, and Queue bindings, then
276
+ * auto-creating any referenced resources via CloudflareService.
277
+ */
278
+ async function doProvision(
279
+ opts: InfraOptions,
280
+ cf?: CloudflareService,
281
+ config?: ConfigService
282
+ ): Promise<ProvisionResult> {
283
+ const cloudflare = cf ?? new CloudflareService();
284
+ const configService = config ?? new ConfigService();
285
+ const items: ProvisionItem[] = [];
286
+ let created = 0;
287
+ let errors = 0;
288
+ let exists = 0;
289
+
290
+ try {
291
+ await configService.load();
292
+ } catch (err) {
293
+ const message = err instanceof Error ? err.message : String(err);
294
+ formatError(new CLIError(message, ExitCode.ERROR), opts);
295
+ return {
296
+ items: [],
297
+ summary: { total: 0, created: 0, errors: 0, exists: 0 },
298
+ };
299
+ }
300
+
301
+ const enabledWorkers = configService.listEnabledWorkers();
302
+
303
+ if (enabledWorkers.length === 0) {
304
+ if (!opts.quiet) {
305
+ process.stdout.write(
306
+ theme.dim("No enabled workers found in wrangler.jsonc.\n")
307
+ );
308
+ }
309
+ return {
310
+ items: [],
311
+ summary: { total: 0, created: 0, errors: 0, exists: 0 },
312
+ };
313
+ }
314
+
315
+ const s = spinner();
316
+
317
+ for (const workerName of enabledWorkers) {
318
+ const workerConfig = configService.getWorker(workerName);
319
+ if (!workerConfig?.path) continue;
320
+
321
+ const wranglerPath = resolve(workerConfig.path, "wrangler.jsonc");
322
+ const file = Bun.file(wranglerPath);
323
+
324
+ if (!(await file.exists())) continue;
325
+
326
+ let wranglerConfig: Record<string, unknown>;
327
+ try {
328
+ const content = await file.text();
329
+ wranglerConfig = (parse(content) ?? {}) as Record<string, unknown>;
330
+ } catch {
331
+ // Skip workers with invalid wrangler.jsonc
332
+ continue;
333
+ }
334
+
335
+ // -- D1 databases --
336
+ const d1Databases = Array.isArray(wranglerConfig.d1_databases)
337
+ ? (wranglerConfig.d1_databases as Array<Record<string, unknown>>)
338
+ : [];
339
+ for (const db of d1Databases) {
340
+ const dbName = (db.database_name as string) ?? (db.binding as string);
341
+ if (!dbName) continue;
342
+
343
+ s.start(`Provisioning D1 database: ${dbName}`);
344
+ const result = await cloudflare.d1Create(dbName);
345
+ if (result.ok) {
346
+ items.push({ name: dbName, type: "d1", status: "created" });
347
+ created++;
348
+ s.stop(`D1 database "${dbName}" created`);
349
+ } else if (result.error.toLowerCase().includes("already exists")) {
350
+ items.push({ name: dbName, type: "d1", status: "exists" });
351
+ exists++;
352
+ s.stop(`D1 database "${dbName}" already exists`);
353
+ } else {
354
+ items.push({
355
+ name: dbName,
356
+ type: "d1",
357
+ status: "error",
358
+ error: result.error,
359
+ });
360
+ errors++;
361
+ s.stop(`D1 database "${dbName}" failed: ${result.error}`);
362
+ }
363
+ }
364
+
365
+ // -- KV namespaces --
366
+ const kvNamespaces = Array.isArray(wranglerConfig.kv_namespaces)
367
+ ? (wranglerConfig.kv_namespaces as Array<Record<string, unknown>>)
368
+ : [];
369
+ for (const kv of kvNamespaces) {
370
+ const kvBinding = (kv.binding as string) ?? `kv-${workerName}`;
371
+ s.start(`Provisioning KV namespace: ${kvBinding}`);
372
+ const result = await cloudflare.kvCreate(kvBinding);
373
+ if (result.ok) {
374
+ items.push({ name: kvBinding, type: "kv", status: "created" });
375
+ created++;
376
+ s.stop(`KV namespace "${kvBinding}" created`);
377
+ } else if (result.error.toLowerCase().includes("already exists")) {
378
+ items.push({ name: kvBinding, type: "kv", status: "exists" });
379
+ exists++;
380
+ s.stop(`KV namespace "${kvBinding}" already exists`);
381
+ } else {
382
+ items.push({
383
+ name: kvBinding,
384
+ type: "kv",
385
+ status: "error",
386
+ error: result.error,
387
+ });
388
+ errors++;
389
+ s.stop(`KV namespace "${kvBinding}" failed: ${result.error}`);
390
+ }
391
+ }
392
+
393
+ // -- R2 buckets --
394
+ const r2Buckets = Array.isArray(wranglerConfig.r2_buckets)
395
+ ? (wranglerConfig.r2_buckets as Array<Record<string, unknown>>)
396
+ : [];
397
+ for (const bucket of r2Buckets) {
398
+ const bucketName =
399
+ (bucket.bucket_name as string) ?? (bucket.binding as string);
400
+ if (!bucketName) continue;
401
+
402
+ s.start(`Provisioning R2 bucket: ${bucketName}`);
403
+ const result = await cloudflare.r2Create(bucketName);
404
+ if (result.ok) {
405
+ items.push({ name: bucketName, type: "r2", status: "created" });
406
+ created++;
407
+ s.stop(`R2 bucket "${bucketName}" created`);
408
+ } else if (result.error.toLowerCase().includes("already exists")) {
409
+ items.push({ name: bucketName, type: "r2", status: "exists" });
410
+ exists++;
411
+ s.stop(`R2 bucket "${bucketName}" already exists`);
412
+ } else {
413
+ items.push({
414
+ name: bucketName,
415
+ type: "r2",
416
+ status: "error",
417
+ error: result.error,
418
+ });
419
+ errors++;
420
+ s.stop(`R2 bucket "${bucketName}" failed: ${result.error}`);
421
+ }
422
+ }
423
+
424
+ // -- Queues --
425
+ let queueNames: string[] = [];
426
+ if (wranglerConfig.queues && typeof wranglerConfig.queues === "object") {
427
+ const queues = wranglerConfig.queues as Record<string, unknown>;
428
+ const producers = Array.isArray(queues.producers)
429
+ ? (queues.producers as Array<Record<string, unknown>>)
430
+ : [];
431
+ const consumers = Array.isArray(queues.consumers)
432
+ ? (queues.consumers as Array<Record<string, unknown>>)
433
+ : [];
434
+ queueNames = [
435
+ ...producers.map((q) => q.queue as string),
436
+ ...consumers.map((q) => q.queue as string),
437
+ ].filter(Boolean);
438
+ }
439
+ for (const qName of [...new Set(queueNames)]) {
440
+ s.start(`Provisioning Queue: ${qName}`);
441
+ const result = await cloudflare.queueCreate(qName);
442
+ if (result.ok) {
443
+ items.push({ name: qName, type: "queue", status: "created" });
444
+ created++;
445
+ s.stop(`Queue "${qName}" created`);
446
+ } else if (result.error.toLowerCase().includes("already exists")) {
447
+ items.push({ name: qName, type: "queue", status: "exists" });
448
+ exists++;
449
+ s.stop(`Queue "${qName}" already exists`);
450
+ } else {
451
+ items.push({
452
+ name: qName,
453
+ type: "queue",
454
+ status: "error",
455
+ error: result.error,
456
+ });
457
+ errors++;
458
+ s.stop(`Queue "${qName}" failed: ${result.error}`);
459
+ }
460
+ }
461
+ }
462
+
463
+ const summary = { total: items.length, created, errors, exists };
464
+ const result: ProvisionResult = { items, summary };
465
+
466
+ // Show summary table
467
+ if (!opts.quiet) {
468
+ const summaryRows = [
469
+ { Metric: "Total resources", Value: String(summary.total) },
470
+ { Metric: "Created", Value: String(summary.created) },
471
+ { Metric: "Already existed", Value: String(summary.exists) },
472
+ { Metric: "Errors", Value: String(summary.errors) },
473
+ ];
474
+ if (opts.json) {
475
+ formatJson(result, opts);
476
+ } else {
477
+ process.stdout.write("\n");
478
+ formatTable(summaryRows, opts);
479
+ }
480
+ }
481
+
482
+ return result;
483
+ }
484
+
485
+ // ---------------------------------------------------------------------------
486
+ // Vectorize handlers
487
+ // ---------------------------------------------------------------------------
488
+
489
+ async function doVectorizeList(opts: InfraOptions, cf?: CloudflareService): Promise<void> {
490
+ const cloudflare = cf ?? new CloudflareService();
491
+ const result = await cloudflare.vectorizeList();
492
+ displayListResult(result, opts, ["name", "id", "dimensions", "metric"]);
493
+ }
494
+
495
+ async function doVectorizeCreate(name: string, opts: InfraOptions, cf?: CloudflareService): Promise<void> {
496
+ const cloudflare = cf ?? new CloudflareService();
497
+ await handleCreate(name, "Vectorize index", (n) => cloudflare.vectorizeCreate(n), opts);
498
+ }
499
+
500
+ async function doVectorizeDelete(name: string, opts: InfraOptions, cf?: CloudflareService): Promise<void> {
501
+ const cloudflare = cf ?? new CloudflareService();
502
+ await handleDelete(name, "Vectorize index", (n) => cloudflare.vectorizeDelete(n), opts);
503
+ }
504
+
505
+ // ---------------------------------------------------------------------------
506
+ // Analytics handlers
507
+ // ---------------------------------------------------------------------------
508
+
509
+ async function doAnalyticsList(opts: InfraOptions, cf?: CloudflareService): Promise<void> {
510
+ const cloudflare = cf ?? new CloudflareService();
511
+ const result = await cloudflare.analyticsList();
512
+ displayListResult(result, opts);
513
+ }
514
+
515
+ async function doAnalyticsCreate(name: string, opts: InfraOptions, cf?: CloudflareService): Promise<void> {
516
+ const cloudflare = cf ?? new CloudflareService();
517
+ const result = await cloudflare.analyticsCreate(name);
518
+ if (result.ok) {
519
+ formatSuccess(`Analytics dataset "${name}" created`, opts);
520
+ } else {
521
+ process.stdout.write(`${result.error}\n`);
522
+ }
523
+ }
524
+
525
+ // ---------------------------------------------------------------------------
526
+ // Registration
527
+ // ---------------------------------------------------------------------------
528
+
529
+ /**
530
+ * Register the `hoox infra` command group on the given commander program.
531
+ *
532
+ * @example
533
+ * ```ts
534
+ * import { Command } from "commander";
535
+ * import { registerInfraCommand } from "./commands/infra/index.js";
536
+ * const program = new Command();
537
+ * registerInfraCommand(program);
538
+ * program.parse();
539
+ * ```
540
+ */
541
+ export function registerInfraCommand(program: Command): void {
542
+ const infraCmd = program
543
+ .command("infra")
544
+ .summary("Manage Cloudflare infrastructure (D1, KV, R2, Queues, Vectorize, Analytics)")
545
+ .description(
546
+ `Provision and manage Cloudflare infrastructure resources.
547
+
548
+ INFRASTRUCTURE TYPES:
549
+ D1 - SQL databases for persistent storage
550
+ KV - Key-value stores for configuration and caching
551
+ R2 - S3-compatible object storage
552
+ Queues - Message queues for async processing
553
+ Vectorize - Vector database for AI-powered search
554
+ Analytics - Time-series analytics engine
555
+
556
+ EXAMPLES:
557
+ hoox infra provision Auto-provision from wrangler.jsonc
558
+ hoox infra d1 list List D1 databases
559
+ hoox infra d1 create my-db Create a D1 database
560
+ hoox infra kv list List KV namespaces
561
+ hoox infra r2 list List R2 buckets
562
+ hoox infra queues list List queues
563
+ hoox infra vectorize list List Vectorize indexes
564
+ hoox infra vectorize create my-rag-index Create a Vectorize index
565
+ hoox infra analytics list List Analytics datasets`
566
+ );
567
+
568
+ // -- provision ---------------------------------------------------------
569
+
570
+ infraCmd
571
+ .command("provision")
572
+ .summary("Auto-provision infrastructure from wrangler.jsonc")
573
+ .description(
574
+ `Automatically provision all required infrastructure based on worker wrangler.jsonc files.
575
+
576
+ This reads each worker's wrangler.jsonc and creates:
577
+ - D1 databases (from d1 binding)
578
+ - KV namespaces (from kv_namespaces)
579
+ - R2 buckets (from r2_buckets)
580
+ - Queues (from queues)
581
+
582
+ EXAMPLES:
583
+ hoox infra provision`
584
+ )
585
+ .action(async function (this: Command) {
586
+ const opts = getOptions(this);
587
+ await doProvision(opts);
588
+ });
589
+
590
+ // -- d1 -----------------------------------------------------------------
591
+
592
+ const d1Cmd = infraCmd
593
+ .command("d1")
594
+ .summary("Manage D1 SQL databases")
595
+ .description(
596
+ `D1 is Cloudflare's serverless SQL database.
597
+
598
+ EXAMPLES:
599
+ hoox infra d1 list
600
+ hoox infra d1 create my-database
601
+ hoox infra d1 delete my-database`
602
+ );
603
+
604
+ d1Cmd
605
+ .command("list")
606
+ .summary("List all D1 databases")
607
+ .description(
608
+ `List all D1 databases in your Cloudflare account.
609
+
610
+ EXAMPLES:
611
+ hoox infra d1 list`
612
+ )
613
+ .action(async function (this: Command) {
614
+ const opts = getOptions(this);
615
+ await doD1List(opts);
616
+ });
617
+
618
+ d1Cmd
619
+ .command("create <name>")
620
+ .summary("Create a new D1 database")
621
+ .description(
622
+ `Create a new D1 database.
623
+
624
+ ARGUMENTS:
625
+ name Database name
626
+
627
+ EXAMPLES:
628
+ hoox infra d1 create trade-data-db`
629
+ )
630
+ .action(async function (this: Command, name: string) {
631
+ const opts = getOptions(this);
632
+ await doD1Create(name, opts);
633
+ });
634
+
635
+ d1Cmd
636
+ .command("delete <name>")
637
+ .summary("Delete a D1 database")
638
+ .description(
639
+ `Delete a D1 database (WARNING: destructive operation).
640
+
641
+ ARGUMENTS:
642
+ name Database name
643
+
644
+ EXAMPLES:
645
+ hoox infra d1 delete trade-data-db`
646
+ )
647
+ .action(async function (this: Command, name: string) {
648
+ const opts = getOptions(this);
649
+ await doD1Delete(name, opts);
650
+ });
651
+
652
+ // -- kv -----------------------------------------------------------------
653
+
654
+ const kvCmd = infraCmd
655
+ .command("kv")
656
+ .summary("Manage KV namespaces")
657
+ .description(
658
+ `KV (Key-Value) namespaces for configuration and caching.
659
+
660
+ EXAMPLES:
661
+ hoox infra kv list
662
+ hoox infra kv create CONFIG_KV
663
+ hoox infra kv delete <id>`
664
+ );
665
+
666
+ kvCmd
667
+ .command("list")
668
+ .summary("List all KV namespaces")
669
+ .description(
670
+ `List all KV namespaces in your Cloudflare account.
671
+
672
+ EXAMPLES:
673
+ hoox infra kv list`
674
+ )
675
+ .action(async function (this: Command) {
676
+ const opts = getOptions(this);
677
+ await doKvList(opts);
678
+ });
679
+
680
+ kvCmd
681
+ .command("create <name>")
682
+ .summary("Create a new KV namespace")
683
+ .description(
684
+ `Create a new KV namespace.
685
+
686
+ ARGUMENTS:
687
+ name KV namespace title
688
+
689
+ EXAMPLES:
690
+ hoox infra kv create CONFIG_KV`
691
+ )
692
+ .action(async function (this: Command, name: string) {
693
+ const opts = getOptions(this);
694
+ await doKvCreate(name, opts);
695
+ });
696
+
697
+ kvCmd
698
+ .command("delete <id>")
699
+ .summary("Delete a KV namespace by ID")
700
+ .description(
701
+ `Delete a KV namespace by its ID (WARNING: destructive operation).
702
+
703
+ ARGUMENTS:
704
+ id KV namespace ID (get from 'kv list')
705
+
706
+ EXAMPLES:
707
+ hoox infra kv delete c5917667a21745e390ff969f32b1847d`
708
+ )
709
+ .action(async function (this: Command, id: string) {
710
+ const opts = getOptions(this);
711
+ await doKvDelete(id, opts);
712
+ });
713
+
714
+ // -- r2 -----------------------------------------------------------------
715
+
716
+ const r2Cmd = infraCmd
717
+ .command("r2")
718
+ .summary("Manage R2 object storage buckets")
719
+ .description(
720
+ `R2 is Cloudflare's S3-compatible object storage.
721
+
722
+ EXAMPLES:
723
+ hoox infra r2 list
724
+ hoox infra r2 create trade-reports
725
+ hoox infra r2 delete trade-reports`
726
+ );
727
+
728
+ r2Cmd
729
+ .command("list")
730
+ .summary("List all R2 buckets")
731
+ .description(
732
+ `List all R2 buckets in your Cloudflare account.
733
+
734
+ EXAMPLES:
735
+ hoox infra r2 list`
736
+ )
737
+ .action(async function (this: Command) {
738
+ const opts = getOptions(this);
739
+ await doR2List(opts);
740
+ });
741
+
742
+ r2Cmd
743
+ .command("create <name>")
744
+ .summary("Create a new R2 bucket")
745
+ .description(
746
+ `Create a new R2 bucket.
747
+
748
+ ARGUMENTS:
749
+ name Bucket name
750
+
751
+ EXAMPLES:
752
+ hoox infra r2 create trade-reports`
753
+ )
754
+ .action(async function (this: Command, name: string) {
755
+ const opts = getOptions(this);
756
+ await doR2Create(name, opts);
757
+ });
758
+
759
+ r2Cmd
760
+ .command("delete <name>")
761
+ .summary("Delete an R2 bucket")
762
+ .description(
763
+ `Delete an R2 bucket (WARNING: destructive operation).
764
+
765
+ ARGUMENTS:
766
+ name Bucket name
767
+
768
+ EXAMPLES:
769
+ hoox infra r2 delete trade-reports`
770
+ )
771
+ .action(async function (this: Command, name: string) {
772
+ const opts = getOptions(this);
773
+ await doR2Delete(name, opts);
774
+ });
775
+
776
+ // -- queues -------------------------------------------------------------
777
+
778
+ const queuesCmd = infraCmd
779
+ .command("queues")
780
+ .summary("Manage Cloudflare Queues")
781
+ .description(
782
+ `Queues for asynchronous message processing between workers.
783
+
784
+ EXAMPLES:
785
+ hoox infra queues list
786
+ hoox infra queues create trade-execution
787
+ hoox infra queues delete trade-execution`
788
+ );
789
+
790
+ queuesCmd
791
+ .command("list")
792
+ .summary("List all Queues")
793
+ .description(
794
+ `List all queues in your Cloudflare account.
795
+
796
+ EXAMPLES:
797
+ hoox infra queues list`
798
+ )
799
+ .action(async function (this: Command) {
800
+ const opts = getOptions(this);
801
+ await doQueueList(opts);
802
+ });
803
+
804
+ queuesCmd
805
+ .command("create <name>")
806
+ .summary("Create a new Queue")
807
+ .description(
808
+ `Create a new queue.
809
+
810
+ ARGUMENTS:
811
+ name Queue name
812
+
813
+ EXAMPLES:
814
+ hoox infra queues create trade-execution`
815
+ )
816
+ .action(async function (this: Command, name: string) {
817
+ const opts = getOptions(this);
818
+ await doQueueCreate(name, opts);
819
+ });
820
+
821
+ queuesCmd
822
+ .command("delete <name>")
823
+ .summary("Delete a Queue")
824
+ .description(
825
+ `Delete a queue (WARNING: destructive operation).
826
+
827
+ ARGUMENTS:
828
+ name Queue name
829
+
830
+ EXAMPLES:
831
+ hoox infra queues delete trade-execution`
832
+ )
833
+ .action(async function (this: Command, name: string) {
834
+ const opts = getOptions(this);
835
+ await doQueueDelete(name, opts);
836
+ });
837
+
838
+ // -- vectorize ---------------------------------------------------------
839
+
840
+ const vectorizeCmd = infraCmd
841
+ .command("vectorize")
842
+ .summary("Manage Vectorize indexes")
843
+ .description(
844
+ `Vectorize is Cloudflare's vector database for AI-powered search.
845
+
846
+ EXAMPLES:
847
+ hoox infra vectorize list
848
+ hoox infra vectorize create my-rag-index
849
+ hoox infra vectorize delete my-rag-index`
850
+ );
851
+
852
+ vectorizeCmd
853
+ .command("list")
854
+ .summary("List all Vectorize indexes")
855
+ .description("List all Vectorize indexes in your Cloudflare account.")
856
+ .action(async function (this: Command) {
857
+ const opts = getOptions(this);
858
+ await doVectorizeList(opts);
859
+ });
860
+
861
+ vectorizeCmd
862
+ .command("create <name>")
863
+ .summary("Create a new Vectorize index")
864
+ .description("Create a new Vectorize index with default dimensions (768) and cosine metric.")
865
+ .action(async function (this: Command, name: string) {
866
+ const opts = getOptions(this);
867
+ await doVectorizeCreate(name, opts);
868
+ });
869
+
870
+ vectorizeCmd
871
+ .command("delete <name>")
872
+ .summary("Delete a Vectorize index")
873
+ .description("Delete a Vectorize index (WARNING: destructive operation).")
874
+ .action(async function (this: Command, name: string) {
875
+ const opts = getOptions(this);
876
+ await doVectorizeDelete(name, opts);
877
+ });
878
+
879
+ // -- analytics ---------------------------------------------------------
880
+
881
+ const analyticsCmd = infraCmd
882
+ .command("analytics")
883
+ .summary("Manage Analytics Engine datasets")
884
+ .description(
885
+ `Analytics Engine for storing and querying time-series data.
886
+
887
+ NOTE: Analytics Engine datasets must be created via Cloudflare Dashboard.
888
+ The CLI provides creation instructions.
889
+
890
+ EXAMPLES:
891
+ hoox infra analytics list
892
+ hoox infra analytics create hoox-analytics`
893
+ );
894
+
895
+ analyticsCmd
896
+ .command("list")
897
+ .summary("List all Analytics Engine datasets")
898
+ .description("List all Analytics Engine datasets in your account.")
899
+ .action(async function (this: Command) {
900
+ const opts = getOptions(this);
901
+ await doAnalyticsList(opts);
902
+ });
903
+
904
+ analyticsCmd
905
+ .command("create <name>")
906
+ .summary("Show instructions for creating an Analytics Engine dataset")
907
+ .description("Analytics Engine datasets must be created via Cloudflare Dashboard.")
908
+ .action(async function (this: Command, name: string) {
909
+ const opts = getOptions(this);
910
+ await doAnalyticsCreate(name, opts);
911
+ });
912
+ }
913
+
914
+ // ---------------------------------------------------------------------------
915
+ // Exports (for testing)
916
+ // ---------------------------------------------------------------------------
917
+
918
+ export {
919
+ doD1List,
920
+ doD1Create,
921
+ doD1Delete,
922
+ doKvList,
923
+ doKvCreate,
924
+ doKvDelete,
925
+ doR2List,
926
+ doR2Create,
927
+ doR2Delete,
928
+ doQueueList,
929
+ doQueueCreate,
930
+ doQueueDelete,
931
+ doVectorizeList,
932
+ doVectorizeCreate,
933
+ doVectorizeDelete,
934
+ doAnalyticsList,
935
+ doAnalyticsCreate,
936
+ doProvision,
937
+ displayListResult,
938
+ handleCreate,
939
+ handleDelete,
940
+ };