@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.
- package/bin/hoox.js +3 -4
- package/dist/index.js +32545 -0
- package/package.json +10 -15
- package/src/commands/check/check-command.test.ts +0 -476
- package/src/commands/check/check-command.ts +0 -1087
- package/src/commands/check/index.ts +0 -10
- package/src/commands/check/prerequisites-command.test.ts +0 -19
- package/src/commands/check/prerequisites-command.ts +0 -97
- package/src/commands/check/types.ts +0 -103
- package/src/commands/clone/clone-command.test.ts +0 -440
- package/src/commands/clone/clone-command.ts +0 -431
- package/src/commands/clone/index.ts +0 -1
- package/src/commands/config/config-command.test.ts +0 -583
- package/src/commands/config/config-command.ts +0 -896
- package/src/commands/config/env-command.test.ts +0 -43
- package/src/commands/config/env-command.ts +0 -292
- package/src/commands/config/index.ts +0 -3
- package/src/commands/config/kv-command.test.ts +0 -14
- package/src/commands/config/kv-command.ts +0 -290
- package/src/commands/dashboard/dashboard-command.test.ts +0 -47
- package/src/commands/dashboard/dashboard-command.ts +0 -130
- package/src/commands/dashboard/index.ts +0 -1
- package/src/commands/db/db-command.test.ts +0 -21
- package/src/commands/db/db-command.ts +0 -311
- package/src/commands/db/index.ts +0 -1
- package/src/commands/deploy/deploy-command.test.ts +0 -324
- package/src/commands/deploy/deploy-command.ts +0 -1054
- package/src/commands/deploy/index.ts +0 -2
- package/src/commands/deploy/telegram-service.ts +0 -76
- package/src/commands/deploy/types.ts +0 -34
- package/src/commands/dev/dev-command.test.ts +0 -405
- package/src/commands/dev/dev-command.ts +0 -398
- package/src/commands/dev/index.ts +0 -1
- package/src/commands/disclaimer/disclaimer-command.ts +0 -21
- package/src/commands/disclaimer/index.ts +0 -1
- package/src/commands/infra/index.ts +0 -5
- package/src/commands/infra/infra-command.test.ts +0 -719
- package/src/commands/infra/infra-command.ts +0 -974
- package/src/commands/infra/types.ts +0 -23
- package/src/commands/init/__tests__/cli-provisioner.test.ts +0 -17
- package/src/commands/init/cli-provisioner.ts +0 -71
- package/src/commands/init/index.ts +0 -1
- package/src/commands/init/init-command.test.ts +0 -854
- package/src/commands/init/init-command.ts +0 -597
- package/src/commands/init/types.ts +0 -23
- package/src/commands/logs/index.ts +0 -1
- package/src/commands/logs/logs-command.test.ts +0 -293
- package/src/commands/logs/logs-command.ts +0 -452
- package/src/commands/monitor/index.ts +0 -2
- package/src/commands/monitor/monitor-command.test.ts +0 -271
- package/src/commands/monitor/monitor-command.ts +0 -269
- package/src/commands/monitor/monitor-service.ts +0 -65
- package/src/commands/monitor/types.ts +0 -13
- package/src/commands/repair/index.ts +0 -2
- package/src/commands/repair/repair-command.test.ts +0 -243
- package/src/commands/repair/repair-command.ts +0 -220
- package/src/commands/repair/repair-service.ts +0 -164
- package/src/commands/repair/types.ts +0 -13
- package/src/commands/schema/index.ts +0 -1
- package/src/commands/schema/schema-command.ts +0 -137
- package/src/commands/test/index.ts +0 -2
- package/src/commands/test/test-command.test.ts +0 -318
- package/src/commands/test/test-command.ts +0 -406
- package/src/commands/tui/index.ts +0 -1
- package/src/commands/tui/tui-command.ts +0 -87
- package/src/commands/update/index.ts +0 -1
- package/src/commands/update/update-command.ts +0 -109
- package/src/commands/waf/index.ts +0 -2
- package/src/commands/waf/types.ts +0 -48
- package/src/commands/waf/waf-command.test.ts +0 -506
- package/src/commands/waf/waf-command.ts +0 -545
- package/src/index.ts +0 -261
- package/src/services/cloudflare/cloudflare-service.test.ts +0 -658
- package/src/services/cloudflare/cloudflare-service.ts +0 -433
- package/src/services/cloudflare/index.ts +0 -2
- package/src/services/cloudflare/types.ts +0 -30
- package/src/services/config/config-service.test.ts +0 -395
- package/src/services/config/config-service.ts +0 -202
- package/src/services/config/index.ts +0 -9
- package/src/services/config/types.ts +0 -66
- package/src/services/db/db-service.test.ts +0 -63
- package/src/services/db/db-service.ts +0 -131
- package/src/services/db/index.ts +0 -1
- package/src/services/docker/docker-service.ts +0 -155
- package/src/services/docker/index.ts +0 -1
- package/src/services/env/env-service.test.ts +0 -259
- package/src/services/env/env-service.ts +0 -372
- package/src/services/env/index.ts +0 -1
- package/src/services/kv/index.ts +0 -1
- package/src/services/kv/kv-sync-service.test.ts +0 -38
- package/src/services/kv/kv-sync-service.ts +0 -243
- package/src/services/prerequisites/index.ts +0 -1
- package/src/services/prerequisites/prerequisites-service.test.ts +0 -89
- package/src/services/prerequisites/prerequisites-service.ts +0 -377
- package/src/services/prerequisites/types.ts +0 -48
- package/src/services/schema/index.ts +0 -1
- package/src/services/schema/schema-service.ts +0 -99
- package/src/services/secrets/index.ts +0 -11
- package/src/services/secrets/secrets-service.test.ts +0 -502
- package/src/services/secrets/secrets-service.ts +0 -287
- package/src/services/secrets/types.ts +0 -50
- package/src/services/update/index.ts +0 -2
- package/src/services/update/update-service.test.ts +0 -76
- package/src/services/update/update-service.ts +0 -193
- package/src/ui/banner.ts +0 -141
- package/src/ui/index.ts +0 -8
- package/src/ui/menu.ts +0 -490
- package/src/utils/error-handler.ts +0 -62
- package/src/utils/errors.test.ts +0 -69
- package/src/utils/errors.ts +0 -24
- package/src/utils/formatters.test.ts +0 -180
- package/src/utils/formatters.ts +0 -269
- package/src/utils/git.ts +0 -134
- 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
|
-
}
|