@jango-blockchained/hoox-cli 0.5.2 → 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 (114) 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 -1087
  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 -97
  9. package/src/commands/check/types.ts +0 -103
  10. package/src/commands/clone/clone-command.test.ts +0 -440
  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 -896
  15. package/src/commands/config/env-command.test.ts +0 -43
  16. package/src/commands/config/env-command.ts +0 -292
  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 -130
  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 -311
  25. package/src/commands/db/index.ts +0 -1
  26. package/src/commands/deploy/deploy-command.test.ts +0 -324
  27. package/src/commands/deploy/deploy-command.ts +0 -1054
  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 -405
  32. package/src/commands/dev/dev-command.ts +0 -398
  33. package/src/commands/dev/index.ts +0 -1
  34. package/src/commands/disclaimer/disclaimer-command.ts +0 -21
  35. package/src/commands/disclaimer/index.ts +0 -1
  36. package/src/commands/infra/index.ts +0 -5
  37. package/src/commands/infra/infra-command.test.ts +0 -719
  38. package/src/commands/infra/infra-command.ts +0 -974
  39. package/src/commands/infra/types.ts +0 -23
  40. package/src/commands/init/__tests__/cli-provisioner.test.ts +0 -17
  41. package/src/commands/init/cli-provisioner.ts +0 -71
  42. package/src/commands/init/index.ts +0 -1
  43. package/src/commands/init/init-command.test.ts +0 -854
  44. package/src/commands/init/init-command.ts +0 -597
  45. package/src/commands/init/types.ts +0 -23
  46. package/src/commands/logs/index.ts +0 -1
  47. package/src/commands/logs/logs-command.test.ts +0 -293
  48. package/src/commands/logs/logs-command.ts +0 -452
  49. package/src/commands/monitor/index.ts +0 -2
  50. package/src/commands/monitor/monitor-command.test.ts +0 -271
  51. package/src/commands/monitor/monitor-command.ts +0 -269
  52. package/src/commands/monitor/monitor-service.ts +0 -65
  53. package/src/commands/monitor/types.ts +0 -13
  54. package/src/commands/repair/index.ts +0 -2
  55. package/src/commands/repair/repair-command.test.ts +0 -243
  56. package/src/commands/repair/repair-command.ts +0 -220
  57. package/src/commands/repair/repair-service.ts +0 -164
  58. package/src/commands/repair/types.ts +0 -13
  59. package/src/commands/schema/index.ts +0 -1
  60. package/src/commands/schema/schema-command.ts +0 -137
  61. package/src/commands/test/index.ts +0 -2
  62. package/src/commands/test/test-command.test.ts +0 -318
  63. package/src/commands/test/test-command.ts +0 -406
  64. package/src/commands/tui/index.ts +0 -1
  65. package/src/commands/tui/tui-command.ts +0 -87
  66. package/src/commands/update/index.ts +0 -1
  67. package/src/commands/update/update-command.ts +0 -109
  68. package/src/commands/waf/index.ts +0 -2
  69. package/src/commands/waf/types.ts +0 -48
  70. package/src/commands/waf/waf-command.test.ts +0 -506
  71. package/src/commands/waf/waf-command.ts +0 -545
  72. package/src/index.ts +0 -261
  73. package/src/services/cloudflare/cloudflare-service.test.ts +0 -658
  74. package/src/services/cloudflare/cloudflare-service.ts +0 -433
  75. package/src/services/cloudflare/index.ts +0 -2
  76. package/src/services/cloudflare/types.ts +0 -30
  77. package/src/services/config/config-service.test.ts +0 -395
  78. package/src/services/config/config-service.ts +0 -202
  79. package/src/services/config/index.ts +0 -9
  80. package/src/services/config/types.ts +0 -66
  81. package/src/services/db/db-service.test.ts +0 -63
  82. package/src/services/db/db-service.ts +0 -131
  83. package/src/services/db/index.ts +0 -1
  84. package/src/services/docker/docker-service.ts +0 -155
  85. package/src/services/docker/index.ts +0 -1
  86. package/src/services/env/env-service.test.ts +0 -259
  87. package/src/services/env/env-service.ts +0 -372
  88. package/src/services/env/index.ts +0 -1
  89. package/src/services/kv/index.ts +0 -1
  90. package/src/services/kv/kv-sync-service.test.ts +0 -38
  91. package/src/services/kv/kv-sync-service.ts +0 -243
  92. package/src/services/prerequisites/index.ts +0 -1
  93. package/src/services/prerequisites/prerequisites-service.test.ts +0 -89
  94. package/src/services/prerequisites/prerequisites-service.ts +0 -377
  95. package/src/services/prerequisites/types.ts +0 -48
  96. package/src/services/schema/index.ts +0 -1
  97. package/src/services/schema/schema-service.ts +0 -99
  98. package/src/services/secrets/index.ts +0 -11
  99. package/src/services/secrets/secrets-service.test.ts +0 -502
  100. package/src/services/secrets/secrets-service.ts +0 -287
  101. package/src/services/secrets/types.ts +0 -50
  102. package/src/services/update/index.ts +0 -2
  103. package/src/services/update/update-service.test.ts +0 -76
  104. package/src/services/update/update-service.ts +0 -193
  105. package/src/ui/banner.ts +0 -141
  106. package/src/ui/index.ts +0 -8
  107. package/src/ui/menu.ts +0 -490
  108. package/src/utils/error-handler.ts +0 -62
  109. package/src/utils/errors.test.ts +0 -69
  110. package/src/utils/errors.ts +0 -24
  111. package/src/utils/formatters.test.ts +0 -180
  112. package/src/utils/formatters.ts +0 -269
  113. package/src/utils/git.ts +0 -134
  114. package/src/utils/theme.ts +0 -92
@@ -1,1054 +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", "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
- // Telegram webhook handler
445
- // ---------------------------------------------------------------------------
446
-
447
- async function doTelegramWebhook(
448
- fmt: FormatOptions,
449
- token?: string,
450
- secretToken?: string,
451
- subdomain?: string
452
- ): Promise<void> {
453
- try {
454
- // Resolve subdomain from flag or config
455
- let prefix = subdomain;
456
- if (!prefix) {
457
- const config = new ConfigService();
458
- await config.load();
459
- const global = config.getGlobal();
460
- prefix = global.subdomain_prefix ?? "hoox";
461
- }
462
-
463
- // Resolve bot token from flag or .env.local
464
- let botToken = token;
465
- if (!botToken) {
466
- const envVars = await EnvService.loadDotEnvAsync(".env.local");
467
- botToken = envVars["TG_BOT_TOKEN_BINDING"];
468
- }
469
- if (!botToken) {
470
- formatError(
471
- new CLIError(
472
- "Telegram bot token not found. Provide --token or set TG_BOT_TOKEN_BINDING in .env.local",
473
- ExitCode.ERROR
474
- ),
475
- fmt
476
- );
477
- process.exitCode = ExitCode.ERROR;
478
- return;
479
- }
480
-
481
- // Resolve secret token from flag or .env.local
482
- let webhookSecret = secretToken;
483
- if (!webhookSecret) {
484
- const envVars = await EnvService.loadDotEnvAsync(".env.local");
485
- webhookSecret = envVars["TELEGRAM_SECRET_TOKEN"];
486
- }
487
- if (!webhookSecret) {
488
- formatError(
489
- new CLIError(
490
- "Telegram secret token not found. Provide --secret-token or set TELEGRAM_SECRET_TOKEN in .env.local",
491
- ExitCode.ERROR
492
- ),
493
- fmt
494
- );
495
- process.exitCode = ExitCode.ERROR;
496
- return;
497
- }
498
-
499
- // Check current webhook status
500
- const telegram = new TelegramService();
501
- const webhookUrl = `https://telegram-worker.${prefix}.workers.dev/webhook/${webhookSecret}`;
502
-
503
- const info = await telegram.getWebhookInfo(botToken);
504
- if (info.ok && info.url) {
505
- process.stdout.write(`${theme.dim("Current webhook:")} ${info.url}\n`);
506
- if (info.pending_update_count !== undefined) {
507
- process.stdout.write(
508
- `${theme.dim("Pending updates:")} ${info.pending_update_count}\n`
509
- );
510
- }
511
- }
512
-
513
- // Set webhook
514
- process.stdout.write(
515
- `${theme.info("Setting webhook to:")} ${webhookUrl}\n`
516
- );
517
- const result = await telegram.setWebhook(
518
- botToken,
519
- webhookUrl,
520
- webhookSecret
521
- );
522
-
523
- if (result.ok) {
524
- formatSuccess("Telegram webhook set successfully", fmt);
525
- if (result.description)
526
- process.stdout.write(` ${theme.dim(result.description)}\n`);
527
- } else {
528
- formatError(
529
- new CLIError(
530
- `Telegram webhook failed: ${result.error || result.description || "Unknown error"}`,
531
- ExitCode.ERROR
532
- ),
533
- fmt
534
- );
535
- process.exitCode = ExitCode.ERROR;
536
- }
537
- } catch (err) {
538
- formatError(err instanceof Error ? err.message : String(err), fmt);
539
- process.exitCode = ExitCode.ERROR;
540
- }
541
- }
542
-
543
- // ---------------------------------------------------------------------------
544
- // Update internal URLs handler
545
- // ---------------------------------------------------------------------------
546
-
547
- async function doUpdateInternalUrls(fmt: FormatOptions): Promise<void> {
548
- try {
549
- const config = new ConfigService();
550
- await config.load();
551
- const global = config.getGlobal();
552
- const prefix = global.subdomain_prefix ?? "hoox";
553
- const workers = config.listEnabledWorkers();
554
-
555
- // Try pages/dashboard first, fall back to workers/dashboard
556
- let filePath = resolve(
557
- process.cwd(),
558
- "pages",
559
- "dashboard",
560
- "wrangler.jsonc"
561
- );
562
- if (!existsSync(filePath)) {
563
- filePath = resolve(
564
- process.cwd(),
565
- "workers",
566
- "dashboard",
567
- "wrangler.jsonc"
568
- );
569
- }
570
- if (!existsSync(filePath)) {
571
- formatError(
572
- new CLIError(
573
- "Dashboard wrangler.jsonc not found (checked pages/dashboard and workers/dashboard)",
574
- ExitCode.ERROR
575
- ),
576
- fmt
577
- );
578
- process.exitCode = ExitCode.ERROR;
579
- return;
580
- }
581
-
582
- const content = readFileSync(filePath, "utf-8");
583
- const errors: jsonc.ParseError[] = [];
584
- const parsed = jsonc.parse(content, errors) as Record<string, unknown>;
585
- if (errors.length > 0) {
586
- formatError(
587
- new CLIError(
588
- "Invalid JSONC in dashboard wrangler.jsonc",
589
- ExitCode.ERROR
590
- ),
591
- fmt
592
- );
593
- process.exitCode = ExitCode.ERROR;
594
- return;
595
- }
596
-
597
- const vars = (parsed.vars as Record<string, string>) ?? {};
598
- let changesCount = 0;
599
-
600
- for (const name of workers) {
601
- const key = `${name.toUpperCase().replace(/-/g, "_")}_URL`;
602
- const newUrl = `https://${name}.${prefix}.workers.dev`;
603
- if (vars[key] !== newUrl) {
604
- if (!fmt.quiet) {
605
- process.stdout.write(
606
- ` ${theme.info("→")} ${key}: ${vars[key] ?? "(not set)"} ${theme.dim("→")} ${newUrl}\n`
607
- );
608
- }
609
- vars[key] = newUrl;
610
- changesCount++;
611
- }
612
- }
613
-
614
- if (changesCount === 0) {
615
- formatSuccess("All service URLs already up to date.", fmt);
616
- return;
617
- }
618
-
619
- const fresh = readFileSync(filePath, "utf-8");
620
- const edits = jsonc.modify(fresh, ["vars"], vars, {
621
- formattingOptions: { tabSize: 2, insertSpaces: true },
622
- });
623
- writeFileSync(filePath, jsonc.applyEdits(fresh, edits), "utf-8");
624
- formatSuccess(
625
- `Updated ${changesCount} service URL(s) in dashboard wrangler.jsonc`,
626
- fmt
627
- );
628
- } catch (err) {
629
- formatError(err instanceof Error ? err.message : String(err), fmt);
630
- process.exitCode = ExitCode.ERROR;
631
- }
632
- }
633
-
634
- // ---------------------------------------------------------------------------
635
- // KV config handler
636
- // ---------------------------------------------------------------------------
637
-
638
- async function doKvConfig(fmt: FormatOptions): Promise<void> {
639
- try {
640
- const { KvSyncService } =
641
- await import("../../services/kv/kv-sync-service.js");
642
- const kvSync = new KvSyncService();
643
-
644
- process.stdout.write(`${theme.info("Resolving CONFIG_KV namespace...")}\n`);
645
- const namespaceId = await kvSync.resolveNamespaceId();
646
- const manifest = KvSyncService.getManifest();
647
- let setCount = 0;
648
- let errorCount = 0;
649
-
650
- for (const entry of manifest.keys) {
651
- const value = entry.default;
652
- if (!value || value === "") {
653
- if (!fmt.quiet) {
654
- process.stdout.write(
655
- ` ${theme.dim("·")} ${entry.key} ${theme.dim("(no default, skipping)")}\n`
656
- );
657
- }
658
- continue;
659
- }
660
- try {
661
- await kvSync.set(namespaceId, entry.key, value);
662
- if (!fmt.quiet) {
663
- process.stdout.write(` ${theme.success("✓")} ${entry.key}\n`);
664
- }
665
- setCount++;
666
- } catch (err) {
667
- process.stdout.write(
668
- ` ${theme.error("✗")} ${entry.key}: ${err instanceof Error ? err.message : String(err)}\n`
669
- );
670
- errorCount++;
671
- }
672
- }
673
-
674
- process.stdout.write(`\n${setCount} key(s) set, ${errorCount} error(s)\n`);
675
- if (errorCount > 0) process.exitCode = ExitCode.ERROR;
676
- } catch (err) {
677
- formatError(err instanceof Error ? err.message : String(err), fmt);
678
- process.exitCode = ExitCode.ERROR;
679
- }
680
- }
681
-
682
- // ---------------------------------------------------------------------------
683
- // Command registration
684
- // ---------------------------------------------------------------------------
685
-
686
- /**
687
- * Register the `hoox deploy` command group with subcommands:
688
- * all, workers, worker <name>, dashboard, telegram-webhook, update-internal-urls, kv-config.
689
- */
690
- export function registerDeployCommand(program: Command): void {
691
- const deployCmd = program
692
- .command("deploy")
693
- .summary("Deploy workers and/or dashboard to Cloudflare Workers")
694
- .description(
695
- `Deploy your Hoox trading system to Cloudflare's edge network.
696
-
697
- The deploy command handles building and uploading your workers and dashboard to Cloudflare Workers.
698
-
699
- DEPLOYMENT ORDER:
700
- Workers are deployed in the correct order based on their dependencies (e.g., d1-worker before trade-worker).
701
-
702
- DASHBOARD:
703
- 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.
704
-
705
- EXAMPLES:
706
- hoox deploy all Deploy everything (workers + dashboard)
707
- hoox deploy all --rebuild Force rebuild dashboard before deploying
708
- hoox deploy workers Deploy workers only (skip dashboard)
709
- hoox deploy worker trade-worker Deploy a specific worker
710
- hoox deploy dashboard Deploy dashboard only`
711
- );
712
-
713
- // -- deploy all ----------------------------------------------------------
714
- deployCmd
715
- .command("all")
716
- .summary("Deploy all enabled workers, then the dashboard")
717
- .description(
718
- `Deploy all enabled workers to Cloudflare Workers, then build and deploy the Next.js dashboard.
719
-
720
- This is the recommended command for production deployments as it ensures all components are deployed in the correct order.
721
-
722
- OPTIONS:
723
- --env <env> Target environment (production, staging, etc.)
724
- --rebuild Force rebuild of dashboard before deploying (skip prompt)
725
- --auto Skip dashboard rebuild prompt, use existing build if available
726
-
727
- EXAMPLES:
728
- hoox deploy all
729
- hoox deploy all --env production
730
- hoox deploy all --rebuild
731
- hoox deploy all --auto`
732
- )
733
- .option("--env <env>", "Cloudflare environment (e.g. production, staging)")
734
- .option("--rebuild", "Force rebuild of dashboard before deploying")
735
- .option(
736
- "--auto",
737
- "Skip dashboard rebuild prompt, use existing build if available"
738
- )
739
- .action(
740
- async (options: { env?: string; rebuild?: boolean; auto?: boolean }) => {
741
- const fmt = getFormatOptions(program);
742
- try {
743
- const configService = new ConfigService();
744
- await configService.load();
745
- const cf = new CloudflareService();
746
-
747
- // Deploy all (workers + dashboard) in one go
748
- await deployAll(
749
- configService,
750
- cf,
751
- options.env,
752
- options.rebuild ?? false,
753
- options.auto ?? false
754
- );
755
- } catch (err) {
756
- const message = err instanceof Error ? err.message : String(err);
757
- formatError(message, fmt);
758
- process.exitCode = ExitCode.ERROR;
759
- }
760
- }
761
- );
762
-
763
- // -- deploy workers ------------------------------------------------------
764
- deployCmd
765
- .command("workers")
766
- .summary("Deploy all enabled workers to Cloudflare")
767
- .description(
768
- `Deploy all enabled workers to Cloudflare Workers.
769
-
770
- Workers are deployed in the correct order based on their dependencies. This command skips the dashboard deployment.
771
-
772
- OPTIONS:
773
- --env <env> Target environment (production, staging, etc.)
774
-
775
- EXAMPLES:
776
- hoox deploy workers
777
- hoox deploy workers --env staging`
778
- )
779
- .option("--env <env>", "Cloudflare environment (e.g. production, staging)")
780
- .action(async (options: { env?: string }) => {
781
- try {
782
- const configService = new ConfigService();
783
- await configService.load();
784
- const cf = new CloudflareService();
785
-
786
- // Deploy only workers (no dashboard), sorted by dependency order
787
- const enabled = configService.listEnabledWorkers();
788
- const workers = DEPLOY_ORDER.filter(
789
- (w) => w !== "dashboard" && enabled.includes(w)
790
- );
791
- const unknown = enabled.filter(
792
- (w) => w !== "dashboard" && !DEPLOY_ORDER.includes(w)
793
- );
794
- const ordered = [...workers, ...unknown];
795
- const results: DeployResult[] = [];
796
-
797
- if (ordered.length === 0) {
798
- process.stdout.write(
799
- `${theme.dim("No enabled workers to deploy\n")}`
800
- );
801
- return;
802
- }
803
-
804
- // Print header
805
- process.stdout.write(`\n${theme.heading("Deploying Workers")}\n`);
806
- process.stdout.write(`${theme.dim("─".repeat(60))}\n`);
807
-
808
- for (const name of ordered) {
809
- process.stdout.write(
810
- `${theme.dim("○")} ${name.padEnd(25)} pending\n`
811
- );
812
- }
813
- process.stdout.write(`${theme.dim("─".repeat(60))}\n\n`);
814
-
815
- // Deploy each worker with clack spinner
816
- for (const name of ordered) {
817
- const s = spinner();
818
- s.start(`Deploying ${name}...`);
819
-
820
- const result = await deploySingle(
821
- configService,
822
- cf,
823
- name,
824
- options.env
825
- );
826
- results.push(result);
827
-
828
- if (result.success) {
829
- s.stop(`${theme.success(icons.success)} ${name} deployed`);
830
- if (result.url)
831
- process.stdout.write(
832
- ` ${theme.dim("URL:")} ${result.url}\n`
833
- );
834
- if (result.size)
835
- process.stdout.write(
836
- ` ${theme.dim("Size:")} ${result.size}\n`
837
- );
838
- if (result.startupTime)
839
- process.stdout.write(
840
- ` ${theme.dim("Startup:")} ${result.startupTime}\n`
841
- );
842
- if (result.versionId)
843
- process.stdout.write(
844
- ` ${theme.dim("Version:")} ${result.versionId.slice(0, 8)}...\n`
845
- );
846
- if (
847
- !result.url &&
848
- !result.size &&
849
- !result.startupTime &&
850
- !result.versionId &&
851
- result.rawOutput
852
- ) {
853
- process.stdout.write(
854
- ` ${theme.dim("Output:")} ${result.rawOutput.slice(0, 80)}\n`
855
- );
856
- }
857
- } else {
858
- s.stop(`${theme.error(icons.error)} ${name} failed`);
859
- if (result.error)
860
- process.stdout.write(
861
- ` ${theme.error("Error:")} ${result.error}\n`
862
- );
863
- }
864
- }
865
-
866
- const succeeded = results.filter((r) => r.success).length;
867
- const failed = results.filter((r) => !r.success).length;
868
- process.stdout.write(
869
- `\n${theme.heading("Summary:")} ${succeeded}/${ordered.length} deployed`
870
- );
871
- if (failed > 0)
872
- process.stdout.write(` ${theme.error(`(${failed} failed)`)}\n`);
873
- else process.stdout.write(` ${theme.success(" ✓")}\n\n`);
874
- } catch (err) {
875
- const message = err instanceof Error ? err.message : String(err);
876
- process.stdout.write(`${theme.error(`Error: ${message}`)}\n`);
877
- process.exitCode = ExitCode.ERROR;
878
- }
879
- });
880
-
881
- // -- deploy worker <name> ------------------------------------------------
882
- deployCmd
883
- .command("worker <name>")
884
- .summary("Deploy a single worker by name")
885
- .description(
886
- `Deploy a specific worker to Cloudflare Workers.
887
-
888
- ARGUMENTS:
889
- name Worker name (e.g., trade-worker, agent-worker, hoox)
890
-
891
- OPTIONS:
892
- --env <env> Target environment (production, staging, etc.)
893
-
894
- EXAMPLES:
895
- hoox deploy worker trade-worker
896
- hoox deploy worker agent-worker --env production`
897
- )
898
- .option("--env <env>", "Cloudflare environment (e.g. production, staging)")
899
- .action(async (name: string, options: { env?: string }) => {
900
- const fmt = getFormatOptions(program);
901
- try {
902
- const configService = new ConfigService();
903
- await configService.load();
904
- const cf = new CloudflareService();
905
-
906
- const result = await deploySingle(configService, cf, name, options.env);
907
-
908
- if (result.success) {
909
- const url = result.url ? ` — ${result.url}` : "";
910
- formatSuccess(`Deployed ${name}${url}`, fmt);
911
- } else {
912
- formatError(
913
- new CLIError(
914
- `Failed to deploy "${name}": ${result.error}`,
915
- ExitCode.ERROR
916
- ),
917
- fmt
918
- );
919
- process.exitCode = ExitCode.ERROR;
920
- }
921
- } catch (err) {
922
- const message = err instanceof Error ? err.message : String(err);
923
- formatError(message, fmt);
924
- process.exitCode = ExitCode.ERROR;
925
- }
926
- });
927
-
928
- // -- deploy dashboard ----------------------------------------------------
929
- deployCmd
930
- .command("dashboard")
931
- .summary("Build and deploy the Next.js dashboard")
932
- .description(
933
- `Build and deploy the Hoox dashboard to Cloudflare Workers.
934
-
935
- 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.
936
-
937
- OPTIONS:
938
- --rebuild Force rebuild of dashboard before deploying (skip prompt)
939
-
940
- EXAMPLES:
941
- hoox deploy dashboard Interactive: choose to rebuild or use existing
942
- hoox deploy dashboard --rebuild Force rebuild before deploying`
943
- )
944
- .option("--rebuild", "Force rebuild of dashboard before deploying")
945
- .action(async (options: { rebuild?: boolean }) => {
946
- const fmt = getFormatOptions(program);
947
- try {
948
- const cf = new CloudflareService();
949
-
950
- const result = await deployDashboard(cf, options.rebuild);
951
-
952
- if (result.success) {
953
- const url = result.url ? ` — ${result.url}` : "";
954
- formatSuccess(`Dashboard deployed${url}`, fmt);
955
- } else {
956
- formatError(
957
- new CLIError(
958
- `Dashboard deployment failed: ${result.error}`,
959
- ExitCode.ERROR
960
- ),
961
- fmt
962
- );
963
- process.exitCode = ExitCode.ERROR;
964
- }
965
- } catch (err) {
966
- const message = err instanceof Error ? err.message : String(err);
967
- formatError(message, fmt);
968
- process.exitCode = ExitCode.ERROR;
969
- }
970
- });
971
-
972
- // -- deploy telegram-webhook --------------------------------------------
973
-
974
- deployCmd
975
- .command("telegram-webhook")
976
- .summary("Set Telegram bot webhook (post-deploy step)")
977
- .description(
978
- `Configure the Telegram bot webhook after deploying telegram-worker.
979
-
980
- Calls the Telegram Bot API to set the webhook URL.
981
-
982
- ARGUMENTS:
983
- --token <token> Telegram bot token (from @BotFather)
984
- --secret-token <token> Telegram webhook secret token
985
- --subdomain <prefix> Worker subdomain prefix (default: from config)
986
-
987
- By default, the bot token and secret are read from .env local.
988
- Use --token and --secret-token to override.
989
-
990
- EXAMPLES:
991
- hoox deploy telegram-webhook
992
- hoox deploy telegram-webhook --token 123456:ABC-DEF1234
993
- hoox deploy telegram-webhook --subdomain myapp`
994
- )
995
- .option("--token <token>", "Telegram bot token (from @BotFather)")
996
- .option("--secret-token <secret>", "Telegram webhook secret token")
997
- .option(
998
- "--subdomain <prefix>",
999
- "Worker subdomain prefix (default: from config)"
1000
- )
1001
- .action(
1002
- async (options: {
1003
- token?: string;
1004
- secretToken?: string;
1005
- subdomain?: string;
1006
- }) => {
1007
- const fmt = getFormatOptions(program);
1008
- await doTelegramWebhook(
1009
- fmt,
1010
- options.token,
1011
- options.secretToken,
1012
- options.subdomain
1013
- );
1014
- }
1015
- );
1016
-
1017
- // -- deploy update-internal-urls ---------------------------------------
1018
-
1019
- deployCmd
1020
- .command("update-internal-urls")
1021
- .summary("Update dashboard wrangler.jsonc with current service URLs")
1022
- .description(
1023
- `Update the dashboard's wrangler.jsonc with the current service URLs.
1024
-
1025
- This is a post-deployment step that ensures the dashboard has correct
1026
- service binding URLs for all workers.
1027
-
1028
- EXAMPLES:
1029
- hoox deploy update-internal-urls`
1030
- )
1031
- .action(async () => {
1032
- const fmt = getFormatOptions(program);
1033
- await doUpdateInternalUrls(fmt);
1034
- });
1035
-
1036
- // -- deploy kv-config --------------------------------------------------
1037
-
1038
- deployCmd
1039
- .command("kv-config")
1040
- .summary("Apply KV manifest keys post-deployment")
1041
- .description(
1042
- `Apply the KV manifest key-value pairs after deploying workers.
1043
-
1044
- Sets all KV keys from the manifest to their default values.
1045
- This post-deployment step initializes the CONFIG_KV namespace.
1046
-
1047
- EXAMPLES:
1048
- hoox deploy kv-config`
1049
- )
1050
- .action(async () => {
1051
- const fmt = getFormatOptions(program);
1052
- await doKvConfig(fmt);
1053
- });
1054
- }