@jango-blockchained/hoox-cli 0.5.1 → 0.5.6

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 (106) hide show
  1. package/bin/hoox.js +3 -4
  2. package/dist/index.js +32545 -0
  3. package/package.json +10 -15
  4. package/src/commands/check/check-command.test.ts +0 -476
  5. package/src/commands/check/check-command.ts +0 -1144
  6. package/src/commands/check/index.ts +0 -10
  7. package/src/commands/check/prerequisites-command.test.ts +0 -19
  8. package/src/commands/check/prerequisites-command.ts +0 -96
  9. package/src/commands/check/types.ts +0 -103
  10. package/src/commands/clone/clone-command.test.ts +0 -442
  11. package/src/commands/clone/clone-command.ts +0 -431
  12. package/src/commands/clone/index.ts +0 -1
  13. package/src/commands/config/config-command.test.ts +0 -583
  14. package/src/commands/config/config-command.ts +0 -897
  15. package/src/commands/config/env-command.test.ts +0 -43
  16. package/src/commands/config/env-command.ts +0 -295
  17. package/src/commands/config/index.ts +0 -3
  18. package/src/commands/config/kv-command.test.ts +0 -14
  19. package/src/commands/config/kv-command.ts +0 -290
  20. package/src/commands/dashboard/dashboard-command.test.ts +0 -47
  21. package/src/commands/dashboard/dashboard-command.ts +0 -128
  22. package/src/commands/dashboard/index.ts +0 -1
  23. package/src/commands/db/db-command.test.ts +0 -21
  24. package/src/commands/db/db-command.ts +0 -312
  25. package/src/commands/db/index.ts +0 -1
  26. package/src/commands/deploy/deploy-command.test.ts +0 -304
  27. package/src/commands/deploy/deploy-command.ts +0 -1135
  28. package/src/commands/deploy/index.ts +0 -2
  29. package/src/commands/deploy/telegram-service.ts +0 -76
  30. package/src/commands/deploy/types.ts +0 -34
  31. package/src/commands/dev/dev-command.test.ts +0 -383
  32. package/src/commands/dev/dev-command.ts +0 -398
  33. package/src/commands/dev/index.ts +0 -1
  34. package/src/commands/infra/index.ts +0 -5
  35. package/src/commands/infra/infra-command.test.ts +0 -719
  36. package/src/commands/infra/infra-command.ts +0 -974
  37. package/src/commands/infra/types.ts +0 -23
  38. package/src/commands/init/__tests__/cli-provisioner.test.ts +0 -17
  39. package/src/commands/init/cli-provisioner.ts +0 -71
  40. package/src/commands/init/index.ts +0 -1
  41. package/src/commands/init/init-command.test.ts +0 -876
  42. package/src/commands/init/init-command.ts +0 -593
  43. package/src/commands/init/types.ts +0 -23
  44. package/src/commands/logs/index.ts +0 -1
  45. package/src/commands/logs/logs-command.test.ts +0 -294
  46. package/src/commands/logs/logs-command.ts +0 -452
  47. package/src/commands/monitor/index.ts +0 -2
  48. package/src/commands/monitor/monitor-command.test.ts +0 -263
  49. package/src/commands/monitor/monitor-command.ts +0 -269
  50. package/src/commands/monitor/monitor-service.ts +0 -65
  51. package/src/commands/monitor/types.ts +0 -13
  52. package/src/commands/repair/index.ts +0 -2
  53. package/src/commands/repair/repair-command.test.ts +0 -224
  54. package/src/commands/repair/repair-command.ts +0 -220
  55. package/src/commands/repair/repair-service.ts +0 -138
  56. package/src/commands/repair/types.ts +0 -13
  57. package/src/commands/test/index.ts +0 -2
  58. package/src/commands/test/test-command.test.ts +0 -319
  59. package/src/commands/test/test-command.ts +0 -406
  60. package/src/commands/tui/index.ts +0 -1
  61. package/src/commands/tui/tui-command.ts +0 -87
  62. package/src/commands/update/index.ts +0 -1
  63. package/src/commands/update/update-command.ts +0 -168
  64. package/src/commands/waf/index.ts +0 -2
  65. package/src/commands/waf/types.ts +0 -48
  66. package/src/commands/waf/waf-command.test.ts +0 -506
  67. package/src/commands/waf/waf-command.ts +0 -548
  68. package/src/index.ts +0 -269
  69. package/src/services/cloudflare/cloudflare-service.test.ts +0 -658
  70. package/src/services/cloudflare/cloudflare-service.ts +0 -433
  71. package/src/services/cloudflare/index.ts +0 -2
  72. package/src/services/cloudflare/types.ts +0 -30
  73. package/src/services/config/config-service.test.ts +0 -395
  74. package/src/services/config/config-service.ts +0 -207
  75. package/src/services/config/index.ts +0 -9
  76. package/src/services/config/types.ts +0 -66
  77. package/src/services/db/db-service.test.ts +0 -51
  78. package/src/services/db/db-service.ts +0 -131
  79. package/src/services/db/index.ts +0 -1
  80. package/src/services/docker/docker-service.ts +0 -155
  81. package/src/services/docker/index.ts +0 -1
  82. package/src/services/env/env-service.test.ts +0 -236
  83. package/src/services/env/env-service.ts +0 -386
  84. package/src/services/env/index.ts +0 -1
  85. package/src/services/kv/index.ts +0 -1
  86. package/src/services/kv/kv-sync-service.test.ts +0 -38
  87. package/src/services/kv/kv-sync-service.ts +0 -234
  88. package/src/services/prerequisites/index.ts +0 -1
  89. package/src/services/prerequisites/prerequisites-service.test.ts +0 -89
  90. package/src/services/prerequisites/prerequisites-service.ts +0 -377
  91. package/src/services/prerequisites/types.ts +0 -48
  92. package/src/services/secrets/index.ts +0 -11
  93. package/src/services/secrets/secrets-service.test.ts +0 -495
  94. package/src/services/secrets/secrets-service.ts +0 -287
  95. package/src/services/secrets/types.ts +0 -50
  96. package/src/services/update/index.ts +0 -2
  97. package/src/services/update/update-service.test.ts +0 -76
  98. package/src/services/update/update-service.ts +0 -193
  99. package/src/ui/banner.ts +0 -141
  100. package/src/ui/index.ts +0 -8
  101. package/src/ui/menu.ts +0 -490
  102. package/src/utils/errors.test.ts +0 -69
  103. package/src/utils/errors.ts +0 -23
  104. package/src/utils/formatters.test.ts +0 -180
  105. package/src/utils/formatters.ts +0 -269
  106. package/src/utils/theme.ts +0 -94
@@ -1,1135 +0,0 @@
1
- /**
2
- * `hoox deploy` command group — deploy workers and dashboard to Cloudflare.
3
- *
4
- * Subcommands:
5
- * all — Deploy all enabled workers, then the dashboard
6
- * workers — Deploy all enabled workers with spinner + summary table
7
- * worker — Deploy a single worker with optional --env flag
8
- * dashboard — Build and deploy the Next.js dashboard via OpenNext
9
- */
10
- import { Command } from "commander";
11
- import { spinner, log, select } from "@clack/prompts";
12
- import { ConfigService } from "../../services/config/index.js";
13
- import { CloudflareService } from "../../services/cloudflare/index.js";
14
- import { theme, icons } from "../../utils/theme.js";
15
- import {
16
- formatSuccess,
17
- formatError,
18
- type FormatOptions,
19
- getFormatOptions,
20
- } from "../../utils/formatters.js";
21
- import { CLIError, ExitCode } from "../../utils/errors.js";
22
- import type { DeployResult } from "./types.js";
23
- import { statSync, existsSync, readFileSync, writeFileSync } from "node:fs";
24
- import { resolve } from "node:path";
25
- import { TelegramService } from "./telegram-service.js";
26
- import { EnvService } from "../../services/env/index.js";
27
- import * as jsonc from "jsonc-parser";
28
-
29
- // ---------------------------------------------------------------------------
30
- // Helpers
31
- // ---------------------------------------------------------------------------
32
-
33
- /**
34
- * Check if dashboard build exists and get its modification date.
35
- * Returns null if no build exists.
36
- */
37
- function getDashboardBuildInfo(dashboardPath: string): {
38
- exists: boolean;
39
- lastModified?: Date;
40
- age?: string;
41
- } {
42
- const workerPath = resolve(dashboardPath, ".open-next", "worker.js");
43
-
44
- if (!existsSync(workerPath)) {
45
- return { exists: false };
46
- }
47
-
48
- const stats = statSync(workerPath);
49
- const lastModified = stats.mtime;
50
- const now = new Date();
51
- const ageMs = now.getTime() - lastModified.getTime();
52
- const ageMinutes = Math.floor(ageMs / 60000);
53
- const ageHours = Math.floor(ageMinutes / 60);
54
- const ageDays = Math.floor(ageHours / 24);
55
-
56
- let age: string;
57
- if (ageDays > 0) {
58
- age = `${ageDays} day${ageDays > 1 ? "s" : ""} ago`;
59
- } else if (ageHours > 0) {
60
- age = `${ageHours} hour${ageHours > 1 ? "s" : ""} ago`;
61
- } else if (ageMinutes > 0) {
62
- age = `${ageMinutes} minute${ageMinutes > 1 ? "s" : ""} ago`;
63
- } else {
64
- age = "just now";
65
- }
66
-
67
- return { exists: true, lastModified, age };
68
- }
69
-
70
- /**
71
- * Ask user whether to rebuild or use existing build.
72
- * Returns:
73
- * - "rebuild" if user wants to rebuild
74
- * - "deploy" if user wants to use existing build
75
- * - "cancel" if user wants to abort
76
- */
77
- async function promptRebuildDecision(buildInfo: {
78
- exists: boolean;
79
- lastModified?: Date;
80
- age?: string;
81
- }): Promise<"rebuild" | "deploy" | "cancel"> {
82
- if (!buildInfo.exists) {
83
- return "rebuild"; // No build exists, auto-build
84
- }
85
-
86
- const choice = await select({
87
- message: `Dashboard was last built ${buildInfo.age}. What would you like to do?`,
88
- options: [
89
- {
90
- value: "rebuild",
91
- label: "Build new (recommended)",
92
- hint: "rebuild before deploy",
93
- },
94
- {
95
- value: "deploy",
96
- label: "Deploy existing",
97
- hint: `from ${buildInfo.age}`,
98
- },
99
- { value: "cancel", label: "Cancel", hint: "go back" },
100
- ],
101
- });
102
-
103
- if (choice === undefined || choice === "cancel") {
104
- return "cancel";
105
- }
106
-
107
- return choice as "rebuild" | "deploy";
108
- }
109
-
110
- /**
111
- * Deploy a single worker via CloudflareService.deploy().
112
- * Returns a DeployResult summarizing the outcome.
113
- */
114
- async function deploySingle(
115
- configService: ConfigService,
116
- cf: CloudflareService,
117
- workerName: string,
118
- env?: string
119
- ): Promise<DeployResult> {
120
- const workerConfig = configService.getWorker(workerName);
121
-
122
- if (!workerConfig) {
123
- return {
124
- worker: workerName,
125
- success: false,
126
- error: `Worker "${workerName}" not found in wrangler.jsonc`,
127
- };
128
- }
129
-
130
- const result = await cf.deploy(workerConfig.path, env);
131
-
132
- if (result.ok) {
133
- const rawLine = result.value.rawOutput
134
- ?.split("\n")
135
- .find((l) => l.trim())
136
- ?.trim();
137
- return {
138
- worker: workerName,
139
- url: result.value.url,
140
- success: true,
141
- size: result.value.size,
142
- startupTime: result.value.startupTime,
143
- versionId: result.value.versionId,
144
- rawOutput: rawLine,
145
- };
146
- }
147
-
148
- return {
149
- worker: workerName,
150
- success: false,
151
- error: result.error,
152
- };
153
- }
154
-
155
- /**
156
- * Published deployment order. Follows the dependency chain documented in
157
- * docs/setup_and_operations.md so that service bindings are available
158
- * before dependent workers are deployed.
159
- */
160
- const DEPLOY_ORDER: string[] = [
161
- "analytics-worker",
162
- "d1-worker",
163
- "telegram-worker",
164
- "web3-wallet-worker",
165
- "email-worker",
166
- "trade-worker",
167
- "agent-worker",
168
- "hoox",
169
- "dashboard",
170
- ];
171
-
172
- /**
173
- * Deploy all enabled workers + dashboard with interactive progress UI.
174
- * Uses a single list that updates in place with spinner and details.
175
- */
176
- async function deployAll(
177
- configService: ConfigService,
178
- cf: CloudflareService,
179
- env?: string,
180
- forceRebuildDashboard: boolean = false,
181
- autoMode: boolean = false
182
- ): Promise<DeployResult[]> {
183
- // Get enabled workers and sort by deployment order
184
- const enabled = configService.listEnabledWorkers();
185
- const workers = DEPLOY_ORDER.filter(
186
- (w) => w !== "dashboard" && enabled.includes(w)
187
- );
188
- // Append any unknown workers (not in DEPLOY_ORDER) at the end
189
- const unknown = enabled.filter(
190
- (w) => w !== "dashboard" && !DEPLOY_ORDER.includes(w)
191
- );
192
- const allItems = [...workers, ...unknown, "dashboard"];
193
- const results: DeployResult[] = [];
194
-
195
- if (allItems.length === 0) {
196
- return results;
197
- }
198
-
199
- // Use clack spinner for each worker
200
- const s = spinner();
201
- s.start(`Deploying ${allItems.length} item(s)...`);
202
-
203
- for (let i = 0; i < allItems.length; i++) {
204
- const name = allItems[i];
205
- const isDashboard = name === "dashboard";
206
-
207
- s.message(`[${i + 1}/${allItems.length}] ${name}...`);
208
-
209
- let result: DeployResult;
210
-
211
- if (isDashboard) {
212
- // Use the silent mode in deployAll since we manage our own UI
213
- result = await deployDashboard(cf, forceRebuildDashboard, true, autoMode);
214
- } else {
215
- result = await deploySingle(configService, cf, name, env);
216
- }
217
-
218
- results.push(result);
219
-
220
- if (result.success) {
221
- s.stop(`${theme.success(icons.success)} ${name} deployed`);
222
- } else {
223
- s.stop(`${theme.error(icons.error)} ${name} failed`);
224
- }
225
-
226
- // Output details below the spinner line
227
- if (result.success) {
228
- if (result.url) {
229
- log.step(` ${theme.dim("URL:")} ${result.url}`);
230
- }
231
- if (result.size) {
232
- log.step(` ${theme.dim("Size:")} ${result.size}`);
233
- }
234
- if (result.startupTime) {
235
- log.step(` ${theme.dim("Startup:")} ${result.startupTime}`);
236
- }
237
- if (result.versionId) {
238
- log.step(
239
- ` ${theme.dim("Version:")} ${result.versionId.slice(0, 8)}...`
240
- );
241
- }
242
- if (
243
- !result.url &&
244
- !result.size &&
245
- !result.startupTime &&
246
- !result.versionId &&
247
- result.rawOutput
248
- ) {
249
- log.step(` ${theme.dim("Output:")} ${result.rawOutput.slice(0, 80)}`);
250
- }
251
- } else if (result.error) {
252
- log.warn(` ${theme.error("Error:")} ${result.error}`);
253
- }
254
-
255
- // Start spinner for next worker
256
- if (i < allItems.length - 1) {
257
- s.start(`Deploying ${allItems.length} item(s)...`);
258
- }
259
- }
260
-
261
- // Summary line
262
- const succeeded = results.filter((r) => r.success).length;
263
- const failed = results.filter((r) => !r.success).length;
264
- if (failed > 0) {
265
- log.warn(
266
- `Summary: ${succeeded}/${allItems.length} deployed (${failed} failed)`
267
- );
268
- }
269
-
270
- return results;
271
- }
272
-
273
- /**
274
- * Build and deploy the dashboard (workers/dashboard) via OpenNext + wrangler.
275
- * @param silentMode If true, skips prompt and header for use in deployAll
276
- */
277
- async function deployDashboard(
278
- cf: CloudflareService,
279
- forceRebuild: boolean = false,
280
- silentMode: boolean = false,
281
- autoMode: boolean = false
282
- ): Promise<DeployResult> {
283
- const dashboardPath = "workers/dashboard";
284
-
285
- // Check existing build status
286
- const buildInfo = getDashboardBuildInfo(dashboardPath);
287
-
288
- // Determine action based on build status and user choice
289
- let action: "rebuild" | "deploy" | "cancel";
290
- if (forceRebuild) {
291
- action = "rebuild";
292
- } else if (autoMode) {
293
- action = buildInfo.exists ? "deploy" : "rebuild";
294
- } else if (silentMode) {
295
- action = "rebuild";
296
- } else {
297
- action = await promptRebuildDecision(buildInfo);
298
- }
299
-
300
- // Handle cancel
301
- if (action === "cancel") {
302
- return {
303
- worker: "dashboard",
304
- success: false,
305
- error: "Cancelled by user",
306
- };
307
- }
308
-
309
- // Only show header if not in silent mode (deployAll handles its own UI)
310
- if (!silentMode) {
311
- process.stdout.write(`\n${theme.heading("Deploying Dashboard")}\n`);
312
- process.stdout.write(`${theme.dim("─".repeat(50))}\n`);
313
- process.stdout.write(`${theme.dim("○")} dashboard\n`);
314
- process.stdout.write(`${theme.dim("─".repeat(50))}\n\n`);
315
- }
316
-
317
- // Use clack spinner
318
- const actionText =
319
- action === "rebuild" ? "Building & deploying" : "Deploying";
320
- const s = spinner();
321
-
322
- try {
323
- s.start(`${actionText} dashboard...`);
324
-
325
- if (action === "rebuild") {
326
- // Build + Deploy: bun run deploy (runs opennext:build && opennext:deploy)
327
- const buildProc = Bun.spawn(["bun", "run", "opennext:deploy"], {
328
- cwd: dashboardPath,
329
- stdout: "pipe",
330
- stderr: "pipe",
331
- });
332
-
333
- const buildExit = await buildProc.exited;
334
- const output = await new Response(buildProc.stdout).text();
335
-
336
- if (buildExit !== 0) {
337
- const error = await new Response(buildProc.stderr).text();
338
- s.stop(`${theme.error(icons.error)} dashboard failed`);
339
- if (!silentMode) {
340
- process.stdout.write(
341
- ` ${theme.dim("Error:")} ${error.split("\n")[0]}\n`
342
- );
343
- }
344
- return {
345
- worker: "dashboard",
346
- success: false,
347
- error: `Dashboard deploy exited with code ${buildExit}`,
348
- };
349
- }
350
-
351
- // Parse output for metrics
352
- const sizeMatch = output.match(
353
- /Total Upload:\s*([\d.]+)\s*([KMGT]?i?B)/i
354
- );
355
- const startupMatch = output.match(/Worker Startup Time:\s*(\d+)\s*ms/i);
356
- const urlMatch = output.match(
357
- /https?:\/\/dashboard\.[a-zA-Z0-9-]+\.workers\.dev/
358
- );
359
-
360
- s.stop(`${theme.success(icons.success)} dashboard deployed`);
361
-
362
- if (!silentMode) {
363
- if (urlMatch) {
364
- process.stdout.write(` ${theme.dim("URL:")} ${urlMatch[0]}\n`);
365
- }
366
- if (sizeMatch) {
367
- process.stdout.write(
368
- ` ${theme.dim("Size:")} ${sizeMatch[1]} ${sizeMatch[2]}\n`
369
- );
370
- }
371
- if (startupMatch) {
372
- process.stdout.write(
373
- ` ${theme.dim("Startup:")} ${startupMatch[1]} ms\n`
374
- );
375
- }
376
- }
377
-
378
- return {
379
- worker: "dashboard",
380
- url: urlMatch?.[0] || "https://dashboard.cryptolinx.workers.dev",
381
- success: true,
382
- size: sizeMatch ? `${sizeMatch[1]} ${sizeMatch[2]}` : undefined,
383
- startupTime: startupMatch ? `${startupMatch[1]} ms` : undefined,
384
- };
385
- } else {
386
- // Deploy only (no build)
387
- const deployProc = Bun.spawn(["bun", "run", "opennext:deploy"], {
388
- cwd: dashboardPath,
389
- stdout: "pipe",
390
- stderr: "pipe",
391
- });
392
-
393
- const deployExit = await deployProc.exited;
394
- const output = await new Response(deployProc.stdout).text();
395
-
396
- if (deployExit !== 0) {
397
- const error = await new Response(deployProc.stderr).text();
398
- s.stop(`${theme.error(icons.error)} dashboard failed`);
399
- if (!silentMode) {
400
- process.stdout.write(
401
- ` ${theme.dim("Error:")} ${error.split("\n")[0]}\n`
402
- );
403
- }
404
- return {
405
- worker: "dashboard",
406
- success: false,
407
- error: `Dashboard deploy exited with code ${deployExit}`,
408
- };
409
- }
410
-
411
- const urlMatch = output.match(
412
- /https?:\/\/dashboard\.[a-zA-Z0-9-]+\.workers\.dev/
413
- );
414
-
415
- s.stop(`${theme.success(icons.success)} dashboard deployed`);
416
-
417
- if (!silentMode) {
418
- if (urlMatch) {
419
- process.stdout.write(` ${theme.dim("URL:")} ${urlMatch[0]}\n`);
420
- }
421
- }
422
-
423
- return {
424
- worker: "dashboard",
425
- url: urlMatch?.[0] || "https://dashboard.cryptolinx.workers.dev",
426
- success: true,
427
- };
428
- }
429
- } catch (err) {
430
- const message = err instanceof Error ? err.message : String(err);
431
- s.stop(`${theme.error(icons.error)} dashboard failed`);
432
- if (!silentMode) {
433
- process.stdout.write(` ${theme.dim("Error:")} ${message}\n`);
434
- }
435
- return {
436
- worker: "dashboard",
437
- success: false,
438
- error: message,
439
- };
440
- }
441
- }
442
-
443
- /**
444
- * Print a summary table of deploy results.
445
- * Excluded in quiet mode; uses JSON or box-drawn table depending on --json.
446
- */
447
- function printSummary(
448
- results: DeployResult[],
449
- opts: { json?: boolean; quiet?: boolean }
450
- ): void {
451
- if (opts.quiet) return;
452
-
453
- // Separate workers from dashboard for different formatting
454
- const workerResults = results.filter((r) => r.worker !== "dashboard");
455
- const dashboardResult = results.find((r) => r.worker === "dashboard");
456
-
457
- // Print worker deployments with verbose info
458
- if (workerResults.length > 0) {
459
- process.stdout.write(`\n${theme.heading("Workers Deployed")}\n`);
460
- for (const r of workerResults) {
461
- const icon = r.success
462
- ? theme.success(icons.success)
463
- : theme.error(icons.error);
464
- const status = r.success ? "deployed" : "failed";
465
- process.stdout.write(`${icon} ${r.worker}: ${status}\n`);
466
-
467
- if (r.success && r.url) {
468
- process.stdout.write(` ${theme.dim("URL:")} ${r.url}\n`);
469
- }
470
- if (r.success && r.size) {
471
- process.stdout.write(` ${theme.dim("Size:")} ${r.size}\n`);
472
- }
473
- if (r.success && r.startupTime) {
474
- process.stdout.write(` ${theme.dim("Startup:")} ${r.startupTime}\n`);
475
- }
476
- if (r.success && r.versionId) {
477
- process.stdout.write(
478
- ` ${theme.dim("Version:")} ${r.versionId.slice(0, 8)}...\n`
479
- );
480
- }
481
- if (!r.success && r.error) {
482
- process.stdout.write(` ${theme.error("Error:")} ${r.error}\n`);
483
- }
484
- }
485
- }
486
-
487
- // Print dashboard deployment
488
- if (dashboardResult) {
489
- process.stdout.write(`\n${theme.heading("Dashboard Deployed")}\n`);
490
- const icon = dashboardResult.success
491
- ? theme.success(icons.success)
492
- : theme.error(icons.error);
493
- const status = dashboardResult.success ? "deployed" : "failed";
494
- process.stdout.write(`${icon} dashboard: ${status}\n`);
495
-
496
- if (dashboardResult.success && dashboardResult.url) {
497
- process.stdout.write(` ${theme.dim("URL:")} ${dashboardResult.url}\n`);
498
- }
499
- if (dashboardResult.success && dashboardResult.size) {
500
- process.stdout.write(
501
- ` ${theme.dim("Size:")} ${dashboardResult.size}\n`
502
- );
503
- }
504
- if (dashboardResult.success && dashboardResult.startupTime) {
505
- process.stdout.write(
506
- ` ${theme.dim("Startup:")} ${dashboardResult.startupTime}\n`
507
- );
508
- }
509
- if (!dashboardResult.success && dashboardResult.error) {
510
- process.stdout.write(
511
- ` ${theme.error("Error:")} ${dashboardResult.error}\n`
512
- );
513
- }
514
- }
515
-
516
- // Summary line
517
- const succeeded = results.filter((r) => r.success).length;
518
- const failed = results.filter((r) => !r.success).length;
519
- process.stdout.write(
520
- `\n${theme.heading("Summary:")} ${succeeded} succeeded, ${failed} failed\n`
521
- );
522
- }
523
-
524
- // ---------------------------------------------------------------------------
525
- // Telegram webhook handler
526
- // ---------------------------------------------------------------------------
527
-
528
- async function doTelegramWebhook(
529
- fmt: FormatOptions,
530
- token?: string,
531
- secretToken?: string,
532
- subdomain?: string
533
- ): Promise<void> {
534
- try {
535
- // Resolve subdomain from flag or config
536
- let prefix = subdomain;
537
- if (!prefix) {
538
- const config = new ConfigService();
539
- await config.load();
540
- const global = config.getGlobal();
541
- prefix = global.subdomain_prefix ?? "hoox";
542
- }
543
-
544
- // Resolve bot token from flag or .env.local
545
- let botToken = token;
546
- if (!botToken) {
547
- const envVars = await EnvService.loadDotEnvAsync(".env.local");
548
- botToken = envVars["TELEGRAM_BOT_TOKEN"];
549
- }
550
- if (!botToken) {
551
- formatError(
552
- new CLIError(
553
- "Telegram bot token not found. Provide --token or set TELEGRAM_BOT_TOKEN in .env.local",
554
- ExitCode.ERROR
555
- ),
556
- fmt
557
- );
558
- process.exitCode = ExitCode.ERROR;
559
- return;
560
- }
561
-
562
- // Resolve secret token from flag or .env.local
563
- let webhookSecret = secretToken;
564
- if (!webhookSecret) {
565
- const envVars = await EnvService.loadDotEnvAsync(".env.local");
566
- webhookSecret = envVars["TELEGRAM_SECRET_TOKEN"];
567
- }
568
- if (!webhookSecret) {
569
- formatError(
570
- new CLIError(
571
- "Telegram secret token not found. Provide --secret-token or set TELEGRAM_SECRET_TOKEN in .env.local",
572
- ExitCode.ERROR
573
- ),
574
- fmt
575
- );
576
- process.exitCode = ExitCode.ERROR;
577
- return;
578
- }
579
-
580
- // Check current webhook status
581
- const telegram = new TelegramService();
582
- const webhookUrl = `https://telegram-worker.${prefix}.workers.dev/webhook/${webhookSecret}`;
583
-
584
- const info = await telegram.getWebhookInfo(botToken);
585
- if (info.ok && info.url) {
586
- process.stdout.write(`${theme.dim("Current webhook:")} ${info.url}\n`);
587
- if (info.pending_update_count !== undefined) {
588
- process.stdout.write(
589
- `${theme.dim("Pending updates:")} ${info.pending_update_count}\n`
590
- );
591
- }
592
- }
593
-
594
- // Set webhook
595
- process.stdout.write(
596
- `${theme.info("Setting webhook to:")} ${webhookUrl}\n`
597
- );
598
- const result = await telegram.setWebhook(
599
- botToken,
600
- webhookUrl,
601
- webhookSecret
602
- );
603
-
604
- if (result.ok) {
605
- formatSuccess("Telegram webhook set successfully", fmt);
606
- if (result.description)
607
- process.stdout.write(` ${theme.dim(result.description)}\n`);
608
- } else {
609
- formatError(
610
- new CLIError(
611
- `Telegram webhook failed: ${result.error || result.description || "Unknown error"}`,
612
- ExitCode.ERROR
613
- ),
614
- fmt
615
- );
616
- process.exitCode = ExitCode.ERROR;
617
- }
618
- } catch (err) {
619
- formatError(err instanceof Error ? err.message : String(err), fmt);
620
- process.exitCode = ExitCode.ERROR;
621
- }
622
- }
623
-
624
- // ---------------------------------------------------------------------------
625
- // Update internal URLs handler
626
- // ---------------------------------------------------------------------------
627
-
628
- async function doUpdateInternalUrls(fmt: FormatOptions): Promise<void> {
629
- try {
630
- const config = new ConfigService();
631
- await config.load();
632
- const global = config.getGlobal();
633
- const prefix = global.subdomain_prefix ?? "hoox";
634
- const workers = config.listEnabledWorkers();
635
-
636
- // Try pages/dashboard first, fall back to workers/dashboard
637
- let filePath = resolve(
638
- process.cwd(),
639
- "pages",
640
- "dashboard",
641
- "wrangler.jsonc"
642
- );
643
- if (!existsSync(filePath)) {
644
- filePath = resolve(
645
- process.cwd(),
646
- "workers",
647
- "dashboard",
648
- "wrangler.jsonc"
649
- );
650
- }
651
- if (!existsSync(filePath)) {
652
- formatError(
653
- new CLIError(
654
- "Dashboard wrangler.jsonc not found (checked pages/dashboard and workers/dashboard)",
655
- ExitCode.ERROR
656
- ),
657
- fmt
658
- );
659
- process.exitCode = ExitCode.ERROR;
660
- return;
661
- }
662
-
663
- const content = readFileSync(filePath, "utf-8");
664
- const errors: jsonc.ParseError[] = [];
665
- const parsed = jsonc.parse(content, errors) as Record<string, unknown>;
666
- if (errors.length > 0) {
667
- formatError(
668
- new CLIError(
669
- "Invalid JSONC in dashboard wrangler.jsonc",
670
- ExitCode.ERROR
671
- ),
672
- fmt
673
- );
674
- process.exitCode = ExitCode.ERROR;
675
- return;
676
- }
677
-
678
- const vars = (parsed.vars as Record<string, string>) ?? {};
679
- let changesCount = 0;
680
-
681
- for (const name of workers) {
682
- const key = `${name.toUpperCase().replace(/-/g, "_")}_URL`;
683
- const newUrl = `https://${name}.${prefix}.workers.dev`;
684
- if (vars[key] !== newUrl) {
685
- if (!fmt.quiet) {
686
- process.stdout.write(
687
- ` ${theme.info("→")} ${key}: ${vars[key] ?? "(not set)"} ${theme.dim("→")} ${newUrl}\n`
688
- );
689
- }
690
- vars[key] = newUrl;
691
- changesCount++;
692
- }
693
- }
694
-
695
- if (changesCount === 0) {
696
- formatSuccess("All service URLs already up to date.", fmt);
697
- return;
698
- }
699
-
700
- const fresh = readFileSync(filePath, "utf-8");
701
- const edits = jsonc.modify(fresh, ["vars"], vars, {
702
- formattingOptions: { tabSize: 2, insertSpaces: true },
703
- });
704
- writeFileSync(filePath, jsonc.applyEdits(fresh, edits), "utf-8");
705
- formatSuccess(
706
- `Updated ${changesCount} service URL(s) in dashboard wrangler.jsonc`,
707
- fmt
708
- );
709
- } catch (err) {
710
- formatError(err instanceof Error ? err.message : String(err), fmt);
711
- process.exitCode = ExitCode.ERROR;
712
- }
713
- }
714
-
715
- // ---------------------------------------------------------------------------
716
- // KV config handler
717
- // ---------------------------------------------------------------------------
718
-
719
- async function doKvConfig(fmt: FormatOptions): Promise<void> {
720
- try {
721
- const { KvSyncService } =
722
- await import("../../services/kv/kv-sync-service.js");
723
- const kvSync = new KvSyncService();
724
-
725
- process.stdout.write(`${theme.info("Resolving CONFIG_KV namespace...")}\n`);
726
- const namespaceId = await kvSync.resolveNamespaceId();
727
- const manifest = KvSyncService.getManifest();
728
- let setCount = 0;
729
- let errorCount = 0;
730
-
731
- for (const entry of manifest.keys) {
732
- const value = entry.default;
733
- if (!value || value === "") {
734
- if (!fmt.quiet) {
735
- process.stdout.write(
736
- ` ${theme.dim("·")} ${entry.key} ${theme.dim("(no default, skipping)")}\n`
737
- );
738
- }
739
- continue;
740
- }
741
- try {
742
- await kvSync.set(namespaceId, entry.key, value);
743
- if (!fmt.quiet) {
744
- process.stdout.write(` ${theme.success("✓")} ${entry.key}\n`);
745
- }
746
- setCount++;
747
- } catch (err) {
748
- process.stdout.write(
749
- ` ${theme.error("✗")} ${entry.key}: ${err instanceof Error ? err.message : String(err)}\n`
750
- );
751
- errorCount++;
752
- }
753
- }
754
-
755
- process.stdout.write(`\n${setCount} key(s) set, ${errorCount} error(s)\n`);
756
- if (errorCount > 0) process.exitCode = ExitCode.ERROR;
757
- } catch (err) {
758
- formatError(err instanceof Error ? err.message : String(err), fmt);
759
- process.exitCode = ExitCode.ERROR;
760
- }
761
- }
762
-
763
- // ---------------------------------------------------------------------------
764
- // Command registration
765
- // ---------------------------------------------------------------------------
766
-
767
- /**
768
- * Register the `hoox deploy` command group with subcommands:
769
- * all, workers, worker <name>, dashboard, telegram-webhook, update-internal-urls, kv-config.
770
- */
771
- export function registerDeployCommand(program: Command): void {
772
- const deployCmd = program
773
- .command("deploy")
774
- .summary("Deploy workers and/or dashboard to Cloudflare Workers")
775
- .description(
776
- `Deploy your Hoox trading system to Cloudflare's edge network.
777
-
778
- The deploy command handles building and uploading your workers and dashboard to Cloudflare Workers.
779
-
780
- DEPLOYMENT ORDER:
781
- Workers are deployed in the correct order based on their dependencies (e.g., d1-worker before trade-worker).
782
-
783
- DASHBOARD:
784
- The dashboard uses OpenNext to convert Next.js to Cloudflare Workers format. Before deploying, the CLI checks for an existing build and prompts you to rebuild or use the existing build.
785
-
786
- EXAMPLES:
787
- hoox deploy all Deploy everything (workers + dashboard)
788
- hoox deploy all --rebuild Force rebuild dashboard before deploying
789
- hoox deploy workers Deploy workers only (skip dashboard)
790
- hoox deploy worker trade-worker Deploy a specific worker
791
- hoox deploy dashboard Deploy dashboard only`
792
- );
793
-
794
- // -- deploy all ----------------------------------------------------------
795
- deployCmd
796
- .command("all")
797
- .summary("Deploy all enabled workers, then the dashboard")
798
- .description(
799
- `Deploy all enabled workers to Cloudflare Workers, then build and deploy the Next.js dashboard.
800
-
801
- This is the recommended command for production deployments as it ensures all components are deployed in the correct order.
802
-
803
- OPTIONS:
804
- --env <env> Target environment (production, staging, etc.)
805
- --rebuild Force rebuild of dashboard before deploying (skip prompt)
806
- --auto Skip dashboard rebuild prompt, use existing build if available
807
-
808
- EXAMPLES:
809
- hoox deploy all
810
- hoox deploy all --env production
811
- hoox deploy all --rebuild
812
- hoox deploy all --auto`
813
- )
814
- .option("--env <env>", "Cloudflare environment (e.g. production, staging)")
815
- .option("--rebuild", "Force rebuild of dashboard before deploying")
816
- .option(
817
- "--auto",
818
- "Skip dashboard rebuild prompt, use existing build if available"
819
- )
820
- .action(
821
- async (options: { env?: string; rebuild?: boolean; auto?: boolean }) => {
822
- const fmt = getFormatOptions(program);
823
- try {
824
- const configService = new ConfigService();
825
- await configService.load();
826
- const cf = new CloudflareService();
827
-
828
- // Deploy all (workers + dashboard) in one go
829
- await deployAll(
830
- configService,
831
- cf,
832
- options.env,
833
- options.rebuild ?? false,
834
- options.auto ?? false
835
- );
836
- } catch (err) {
837
- const message = err instanceof Error ? err.message : String(err);
838
- formatError(message, fmt);
839
- process.exitCode = ExitCode.ERROR;
840
- }
841
- }
842
- );
843
-
844
- // -- deploy workers ------------------------------------------------------
845
- deployCmd
846
- .command("workers")
847
- .summary("Deploy all enabled workers to Cloudflare")
848
- .description(
849
- `Deploy all enabled workers to Cloudflare Workers.
850
-
851
- Workers are deployed in the correct order based on their dependencies. This command skips the dashboard deployment.
852
-
853
- OPTIONS:
854
- --env <env> Target environment (production, staging, etc.)
855
-
856
- EXAMPLES:
857
- hoox deploy workers
858
- hoox deploy workers --env staging`
859
- )
860
- .option("--env <env>", "Cloudflare environment (e.g. production, staging)")
861
- .action(async (options: { env?: string }) => {
862
- try {
863
- const configService = new ConfigService();
864
- await configService.load();
865
- const cf = new CloudflareService();
866
-
867
- // Deploy only workers (no dashboard), sorted by dependency order
868
- const enabled = configService.listEnabledWorkers();
869
- const workers = DEPLOY_ORDER.filter(
870
- (w) => w !== "dashboard" && enabled.includes(w)
871
- );
872
- const unknown = enabled.filter(
873
- (w) => w !== "dashboard" && !DEPLOY_ORDER.includes(w)
874
- );
875
- const ordered = [...workers, ...unknown];
876
- const results: DeployResult[] = [];
877
-
878
- if (ordered.length === 0) {
879
- process.stdout.write(
880
- `${theme.dim("No enabled workers to deploy\n")}`
881
- );
882
- return;
883
- }
884
-
885
- // Print header
886
- process.stdout.write(`\n${theme.heading("Deploying Workers")}\n`);
887
- process.stdout.write(`${theme.dim("─".repeat(60))}\n`);
888
-
889
- for (const name of ordered) {
890
- process.stdout.write(
891
- `${theme.dim("○")} ${name.padEnd(25)} pending\n`
892
- );
893
- }
894
- process.stdout.write(`${theme.dim("─".repeat(60))}\n\n`);
895
-
896
- // Deploy each worker with clack spinner
897
- for (const name of ordered) {
898
- const s = spinner();
899
- s.start(`Deploying ${name}...`);
900
-
901
- const result = await deploySingle(
902
- configService,
903
- cf,
904
- name,
905
- options.env
906
- );
907
- results.push(result);
908
-
909
- if (result.success) {
910
- s.stop(`${theme.success(icons.success)} ${name} deployed`);
911
- if (result.url)
912
- process.stdout.write(
913
- ` ${theme.dim("URL:")} ${result.url}\n`
914
- );
915
- if (result.size)
916
- process.stdout.write(
917
- ` ${theme.dim("Size:")} ${result.size}\n`
918
- );
919
- if (result.startupTime)
920
- process.stdout.write(
921
- ` ${theme.dim("Startup:")} ${result.startupTime}\n`
922
- );
923
- if (result.versionId)
924
- process.stdout.write(
925
- ` ${theme.dim("Version:")} ${result.versionId.slice(0, 8)}...\n`
926
- );
927
- if (
928
- !result.url &&
929
- !result.size &&
930
- !result.startupTime &&
931
- !result.versionId &&
932
- result.rawOutput
933
- ) {
934
- process.stdout.write(
935
- ` ${theme.dim("Output:")} ${result.rawOutput.slice(0, 80)}\n`
936
- );
937
- }
938
- } else {
939
- s.stop(`${theme.error(icons.error)} ${name} failed`);
940
- if (result.error)
941
- process.stdout.write(
942
- ` ${theme.error("Error:")} ${result.error}\n`
943
- );
944
- }
945
- }
946
-
947
- const succeeded = results.filter((r) => r.success).length;
948
- const failed = results.filter((r) => !r.success).length;
949
- process.stdout.write(
950
- `\n${theme.heading("Summary:")} ${succeeded}/${ordered.length} deployed`
951
- );
952
- if (failed > 0)
953
- process.stdout.write(` ${theme.error(`(${failed} failed)`)}\n`);
954
- else process.stdout.write(` ${theme.success(" ✓")}\n\n`);
955
- } catch (err) {
956
- const message = err instanceof Error ? err.message : String(err);
957
- process.stdout.write(`${theme.error(`Error: ${message}`)}\n`);
958
- process.exitCode = ExitCode.ERROR;
959
- }
960
- });
961
-
962
- // -- deploy worker <name> ------------------------------------------------
963
- deployCmd
964
- .command("worker <name>")
965
- .summary("Deploy a single worker by name")
966
- .description(
967
- `Deploy a specific worker to Cloudflare Workers.
968
-
969
- ARGUMENTS:
970
- name Worker name (e.g., trade-worker, agent-worker, hoox)
971
-
972
- OPTIONS:
973
- --env <env> Target environment (production, staging, etc.)
974
-
975
- EXAMPLES:
976
- hoox deploy worker trade-worker
977
- hoox deploy worker agent-worker --env production`
978
- )
979
- .option("--env <env>", "Cloudflare environment (e.g. production, staging)")
980
- .action(async (name: string, options: { env?: string }) => {
981
- const fmt = getFormatOptions(program);
982
- try {
983
- const configService = new ConfigService();
984
- await configService.load();
985
- const cf = new CloudflareService();
986
-
987
- const result = await deploySingle(configService, cf, name, options.env);
988
-
989
- if (result.success) {
990
- const url = result.url ? ` — ${result.url}` : "";
991
- formatSuccess(`Deployed ${name}${url}`, fmt);
992
- } else {
993
- formatError(
994
- new CLIError(
995
- `Failed to deploy "${name}": ${result.error}`,
996
- ExitCode.ERROR
997
- ),
998
- fmt
999
- );
1000
- process.exitCode = ExitCode.ERROR;
1001
- }
1002
- } catch (err) {
1003
- const message = err instanceof Error ? err.message : String(err);
1004
- formatError(message, fmt);
1005
- process.exitCode = ExitCode.ERROR;
1006
- }
1007
- });
1008
-
1009
- // -- deploy dashboard ----------------------------------------------------
1010
- deployCmd
1011
- .command("dashboard")
1012
- .summary("Build and deploy the Next.js dashboard")
1013
- .description(
1014
- `Build and deploy the Hoox dashboard to Cloudflare Workers.
1015
-
1016
- The dashboard is built using OpenNext which converts Next.js to Cloudflare Workers format. Before deploying, the CLI checks for an existing build and prompts you to rebuild or use the existing build.
1017
-
1018
- OPTIONS:
1019
- --rebuild Force rebuild of dashboard before deploying (skip prompt)
1020
-
1021
- EXAMPLES:
1022
- hoox deploy dashboard Interactive: choose to rebuild or use existing
1023
- hoox deploy dashboard --rebuild Force rebuild before deploying`
1024
- )
1025
- .option("--rebuild", "Force rebuild of dashboard before deploying")
1026
- .action(async (options: { rebuild?: boolean }) => {
1027
- const fmt = getFormatOptions(program);
1028
- try {
1029
- const cf = new CloudflareService();
1030
-
1031
- const result = await deployDashboard(cf, options.rebuild);
1032
-
1033
- if (result.success) {
1034
- const url = result.url ? ` — ${result.url}` : "";
1035
- formatSuccess(`Dashboard deployed${url}`, fmt);
1036
- } else {
1037
- formatError(
1038
- new CLIError(
1039
- `Dashboard deployment failed: ${result.error}`,
1040
- ExitCode.ERROR
1041
- ),
1042
- fmt
1043
- );
1044
- process.exitCode = ExitCode.ERROR;
1045
- }
1046
- } catch (err) {
1047
- const message = err instanceof Error ? err.message : String(err);
1048
- formatError(message, fmt);
1049
- process.exitCode = ExitCode.ERROR;
1050
- }
1051
- });
1052
-
1053
- // -- deploy telegram-webhook --------------------------------------------
1054
-
1055
- deployCmd
1056
- .command("telegram-webhook")
1057
- .summary("Set Telegram bot webhook (post-deploy step)")
1058
- .description(
1059
- `Configure the Telegram bot webhook after deploying telegram-worker.
1060
-
1061
- Calls the Telegram Bot API to set the webhook URL.
1062
-
1063
- ARGUMENTS:
1064
- --token <token> Telegram bot token (from @BotFather)
1065
- --secret-token <token> Telegram webhook secret token
1066
- --subdomain <prefix> Worker subdomain prefix (default: from config)
1067
-
1068
- By default, the bot token and secret are read from .env local.
1069
- Use --token and --secret-token to override.
1070
-
1071
- EXAMPLES:
1072
- hoox deploy telegram-webhook
1073
- hoox deploy telegram-webhook --token 123456:ABC-DEF1234
1074
- hoox deploy telegram-webhook --subdomain myapp`
1075
- )
1076
- .option("--token <token>", "Telegram bot token (from @BotFather)")
1077
- .option("--secret-token <secret>", "Telegram webhook secret token")
1078
- .option(
1079
- "--subdomain <prefix>",
1080
- "Worker subdomain prefix (default: from config)"
1081
- )
1082
- .action(
1083
- async (options: {
1084
- token?: string;
1085
- secretToken?: string;
1086
- subdomain?: string;
1087
- }) => {
1088
- const fmt = getFormatOptions(program);
1089
- await doTelegramWebhook(
1090
- fmt,
1091
- options.token,
1092
- options.secretToken,
1093
- options.subdomain
1094
- );
1095
- }
1096
- );
1097
-
1098
- // -- deploy update-internal-urls ---------------------------------------
1099
-
1100
- deployCmd
1101
- .command("update-internal-urls")
1102
- .summary("Update dashboard wrangler.jsonc with current service URLs")
1103
- .description(
1104
- `Update the dashboard's wrangler.jsonc with the current service URLs.
1105
-
1106
- This is a post-deployment step that ensures the dashboard has correct
1107
- service binding URLs for all workers.
1108
-
1109
- EXAMPLES:
1110
- hoox deploy update-internal-urls`
1111
- )
1112
- .action(async () => {
1113
- const fmt = getFormatOptions(program);
1114
- await doUpdateInternalUrls(fmt);
1115
- });
1116
-
1117
- // -- deploy kv-config --------------------------------------------------
1118
-
1119
- deployCmd
1120
- .command("kv-config")
1121
- .summary("Apply KV manifest keys post-deployment")
1122
- .description(
1123
- `Apply the KV manifest key-value pairs after deploying workers.
1124
-
1125
- Sets all KV keys from the manifest to their default values.
1126
- This post-deployment step initializes the CONFIG_KV namespace.
1127
-
1128
- EXAMPLES:
1129
- hoox deploy kv-config`
1130
- )
1131
- .action(async () => {
1132
- const fmt = getFormatOptions(program);
1133
- await doKvConfig(fmt);
1134
- });
1135
- }