@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.
- 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 -1144
- 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 -96
- package/src/commands/check/types.ts +0 -103
- package/src/commands/clone/clone-command.test.ts +0 -442
- 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 -897
- package/src/commands/config/env-command.test.ts +0 -43
- package/src/commands/config/env-command.ts +0 -295
- 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 -128
- 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 -312
- package/src/commands/db/index.ts +0 -1
- package/src/commands/deploy/deploy-command.test.ts +0 -304
- package/src/commands/deploy/deploy-command.ts +0 -1135
- 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 -383
- package/src/commands/dev/dev-command.ts +0 -398
- package/src/commands/dev/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 -876
- package/src/commands/init/init-command.ts +0 -593
- 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 -294
- 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 -263
- 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 -224
- package/src/commands/repair/repair-command.ts +0 -220
- package/src/commands/repair/repair-service.ts +0 -138
- package/src/commands/repair/types.ts +0 -13
- package/src/commands/test/index.ts +0 -2
- package/src/commands/test/test-command.test.ts +0 -319
- 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 -168
- 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 -548
- package/src/index.ts +0 -269
- 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 -207
- 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 -51
- 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 -236
- package/src/services/env/env-service.ts +0 -386
- 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 -234
- 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/secrets/index.ts +0 -11
- package/src/services/secrets/secrets-service.test.ts +0 -495
- 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/errors.test.ts +0 -69
- package/src/utils/errors.ts +0 -23
- package/src/utils/formatters.test.ts +0 -180
- package/src/utils/formatters.ts +0 -269
- package/src/utils/theme.ts +0 -94
|
@@ -1,593 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `hoox init` — interactive setup wizard for Hoox Workspace.
|
|
3
|
-
*
|
|
4
|
-
* Uses the shared WizardEngine from @jango-blockchained/hoox-shared.
|
|
5
|
-
*
|
|
6
|
-
* Interactive flow uses @clack/prompts for the UI layer.
|
|
7
|
-
* Non-interactive mode (--token, --account, --secret-store, --prefix):
|
|
8
|
-
* Skips all prompts and writes config with base workers only.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import * as p from "@clack/prompts";
|
|
12
|
-
import { Command } from "commander";
|
|
13
|
-
import {
|
|
14
|
-
WizardEngine,
|
|
15
|
-
serializeState,
|
|
16
|
-
deserializeState,
|
|
17
|
-
WIZARD_STATE_PATH,
|
|
18
|
-
} from "@jango-blockchained/hoox-shared";
|
|
19
|
-
import type { WorkersJsonConfig } from "@jango-blockchained/hoox-shared";
|
|
20
|
-
import { CloudflareService } from "../../services/cloudflare/index.js";
|
|
21
|
-
import { formatSuccess, formatError } from "../../utils/formatters.js";
|
|
22
|
-
import { CLIError, ExitCode } from "../../utils/errors.js";
|
|
23
|
-
import { theme } from "../../utils/theme.js";
|
|
24
|
-
import { CLIProvisioner } from "./cli-provisioner.js";
|
|
25
|
-
import type { InitOptions } from "./types.js";
|
|
26
|
-
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
28
|
-
// Helpers
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
|
|
31
|
-
async function getExistingAccountId(): Promise<string | undefined> {
|
|
32
|
-
try {
|
|
33
|
-
const file = Bun.file("wrangler.jsonc");
|
|
34
|
-
if (!(await file.exists())) return undefined;
|
|
35
|
-
const raw = await file.text();
|
|
36
|
-
const match = raw.match(/"cloudflare_account_id"\s*:\s*"([^"]+)"/);
|
|
37
|
-
return match ? match[1] : undefined;
|
|
38
|
-
} catch {
|
|
39
|
-
return undefined;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async function validateApiToken(
|
|
44
|
-
cf: CloudflareService,
|
|
45
|
-
token: string
|
|
46
|
-
): Promise<string | undefined> {
|
|
47
|
-
const prev = process.env.CLOUDFLARE_API_TOKEN;
|
|
48
|
-
process.env.CLOUDFLARE_API_TOKEN = token;
|
|
49
|
-
try {
|
|
50
|
-
const result = await cf.whoami();
|
|
51
|
-
if (result.ok) return undefined;
|
|
52
|
-
return `Authentication failed: ${result.error}`;
|
|
53
|
-
} finally {
|
|
54
|
-
if (prev) {
|
|
55
|
-
process.env.CLOUDFLARE_API_TOKEN = prev;
|
|
56
|
-
} else {
|
|
57
|
-
delete process.env.CLOUDFLARE_API_TOKEN;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async function writeWorkersJsonc(
|
|
63
|
-
config: WorkersJsonConfig,
|
|
64
|
-
opts?: { json?: boolean; quiet?: boolean }
|
|
65
|
-
): Promise<void> {
|
|
66
|
-
const { format, applyEdits } = await import("jsonc-parser");
|
|
67
|
-
|
|
68
|
-
const raw = JSON.stringify(config, null, 2);
|
|
69
|
-
const formattingOpts = {
|
|
70
|
-
insertSpaces: true,
|
|
71
|
-
tabSize: 2,
|
|
72
|
-
eol: "\n",
|
|
73
|
-
};
|
|
74
|
-
const edits = format(raw, undefined, formattingOpts);
|
|
75
|
-
const formatted = applyEdits(raw, edits);
|
|
76
|
-
|
|
77
|
-
const header =
|
|
78
|
-
"// Hoox Workspace Configuration\n" +
|
|
79
|
-
"// Generated by `hoox init`. Edit manually or re-run the wizard.\n" +
|
|
80
|
-
"// Secrets are referenced by name — actual values are stored in Cloudflare.\n\n";
|
|
81
|
-
|
|
82
|
-
await Bun.write("wrangler.jsonc", header + formatted);
|
|
83
|
-
|
|
84
|
-
if (!opts?.quiet) {
|
|
85
|
-
formatSuccess("wrangler.jsonc written", opts);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
async function createDevVars(
|
|
90
|
-
config: WorkersJsonConfig,
|
|
91
|
-
secrets: Record<string, Record<string, string>>,
|
|
92
|
-
opts?: { json?: boolean; quiet?: boolean }
|
|
93
|
-
): Promise<void> {
|
|
94
|
-
for (const [workerName, worker] of Object.entries(config.workers)) {
|
|
95
|
-
const lines: string[] = [];
|
|
96
|
-
lines.push(`# .dev.vars — local secrets for ${workerName}`);
|
|
97
|
-
lines.push(`# Generated by \`hoox init\`. NEVER commit this file.`);
|
|
98
|
-
lines.push("");
|
|
99
|
-
|
|
100
|
-
// Add secrets from the collected data
|
|
101
|
-
for (const [, integrationSecrets] of Object.entries(secrets)) {
|
|
102
|
-
for (const [key, value] of Object.entries(integrationSecrets)) {
|
|
103
|
-
lines.push(`${key}=${value}`);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (lines.length <= 3) continue;
|
|
108
|
-
|
|
109
|
-
const content = lines.join("\n") + "\n";
|
|
110
|
-
const filePath = `${worker.path}/.dev.vars`;
|
|
111
|
-
const dir = `${worker.path}`;
|
|
112
|
-
|
|
113
|
-
try {
|
|
114
|
-
await Bun.write(filePath, content);
|
|
115
|
-
if (!opts?.quiet) {
|
|
116
|
-
formatSuccess(`Created ${filePath}`, opts);
|
|
117
|
-
}
|
|
118
|
-
} catch {
|
|
119
|
-
const { mkdir } = await import("node:fs/promises");
|
|
120
|
-
await mkdir(dir, { recursive: true });
|
|
121
|
-
await Bun.write(filePath, content);
|
|
122
|
-
if (!opts?.quiet) {
|
|
123
|
-
formatSuccess(`Created ${filePath}`, opts);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
async function saveState(engine: WizardEngine): Promise<void> {
|
|
130
|
-
const state = engine.getState();
|
|
131
|
-
const json = serializeState(state);
|
|
132
|
-
await Bun.write(WIZARD_STATE_PATH, json);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
async function loadSavedState(): Promise<WizardEngine | null> {
|
|
136
|
-
try {
|
|
137
|
-
const file = Bun.file(WIZARD_STATE_PATH);
|
|
138
|
-
if (!(await file.exists())) return null;
|
|
139
|
-
const json = await file.text();
|
|
140
|
-
const state = deserializeState(json);
|
|
141
|
-
return new WizardEngine(state);
|
|
142
|
-
} catch {
|
|
143
|
-
return null;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
async function cleanupState(): Promise<void> {
|
|
148
|
-
try {
|
|
149
|
-
const file = Bun.file(WIZARD_STATE_PATH);
|
|
150
|
-
if (await file.exists()) {
|
|
151
|
-
await Bun.write(WIZARD_STATE_PATH, "");
|
|
152
|
-
}
|
|
153
|
-
} catch {
|
|
154
|
-
// ignore
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// ---------------------------------------------------------------------------
|
|
159
|
-
// Main command registration
|
|
160
|
-
// ---------------------------------------------------------------------------
|
|
161
|
-
|
|
162
|
-
export function registerInitCommand(program: Command): void {
|
|
163
|
-
program
|
|
164
|
-
.command("init")
|
|
165
|
-
.summary("Interactive setup wizard for Hoox Workspace")
|
|
166
|
-
.description(
|
|
167
|
-
`Initialize a new Hoox workspace with interactive prompts.
|
|
168
|
-
|
|
169
|
-
This wizard helps you set up:
|
|
170
|
-
- Cloudflare API token and account
|
|
171
|
-
- Subdomain prefix for worker URLs
|
|
172
|
-
- Secret store configuration
|
|
173
|
-
- Worker selection with preset templates
|
|
174
|
-
- Infrastructure provisioning (D1, KV)
|
|
175
|
-
- Initial worker configuration
|
|
176
|
-
|
|
177
|
-
INTERACTIVE MODE:
|
|
178
|
-
Run without flags to use the interactive wizard.
|
|
179
|
-
|
|
180
|
-
NON-INTERACTIVE MODE:
|
|
181
|
-
Provide all required options to skip prompts.
|
|
182
|
-
|
|
183
|
-
OPTIONS:
|
|
184
|
-
--token <token> Cloudflare API token
|
|
185
|
-
--account <id> Cloudflare Account ID
|
|
186
|
-
--secret-store <id> Secret Store ID
|
|
187
|
-
--prefix <prefix> Subdomain prefix (default: cryptolinx)
|
|
188
|
-
--preset <name> Worker preset (minimal, standard, full)
|
|
189
|
-
--resume Resume from saved wizard state
|
|
190
|
-
--accept-risk Skip risk acknowledgment
|
|
191
|
-
|
|
192
|
-
EXAMPLES:
|
|
193
|
-
hoox init Interactive wizard
|
|
194
|
-
hoox init --token cfut_xxx --account xxx Non-interactive`
|
|
195
|
-
)
|
|
196
|
-
.option("--token <token>", "Cloudflare API token (non-interactive)")
|
|
197
|
-
.option("--account <id>", "Cloudflare Account ID (non-interactive)")
|
|
198
|
-
.option("--secret-store <id>", "Secret Store ID (non-interactive)")
|
|
199
|
-
.option(
|
|
200
|
-
"--prefix <prefix>",
|
|
201
|
-
"Subdomain prefix (non-interactive, default: cryptolinx)"
|
|
202
|
-
)
|
|
203
|
-
.option("--preset <name>", "Worker preset (minimal, standard, full)")
|
|
204
|
-
.option("--resume", "Resume from saved wizard state")
|
|
205
|
-
.option("--accept-risk", "Skip the risk acknowledgment confirmation")
|
|
206
|
-
.action(async (options: InitOptions) => {
|
|
207
|
-
const globalOpts = program.opts() as { json?: boolean; quiet?: boolean };
|
|
208
|
-
const isNonInteractive = Boolean(options.token && options.account);
|
|
209
|
-
|
|
210
|
-
try {
|
|
211
|
-
await runInitCommand(options, globalOpts, isNonInteractive);
|
|
212
|
-
} catch (err) {
|
|
213
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
214
|
-
p.log.error(message);
|
|
215
|
-
formatError(new CLIError(message, ExitCode.ERROR), globalOpts);
|
|
216
|
-
process.exit(ExitCode.ERROR);
|
|
217
|
-
}
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// ---------------------------------------------------------------------------
|
|
222
|
-
// Core logic
|
|
223
|
-
// ---------------------------------------------------------------------------
|
|
224
|
-
|
|
225
|
-
export async function runInitCommand(
|
|
226
|
-
options: InitOptions,
|
|
227
|
-
globalOpts: { json?: boolean; quiet?: boolean },
|
|
228
|
-
isNonInteractive: boolean
|
|
229
|
-
): Promise<void> {
|
|
230
|
-
// ── Non-interactive mode ───────────────────────────────────────────────
|
|
231
|
-
if (isNonInteractive) {
|
|
232
|
-
if (!globalOpts.quiet) {
|
|
233
|
-
p.intro("Hoox Setup Wizard");
|
|
234
|
-
p.note("Non-interactive mode — using provided flags.", "Mode");
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const token = options.token!;
|
|
238
|
-
const account = options.account!;
|
|
239
|
-
const secretStore = options.secretStore ?? "";
|
|
240
|
-
const prefix = options.prefix ?? "cryptolinx";
|
|
241
|
-
|
|
242
|
-
// Validate token
|
|
243
|
-
const cf = new CloudflareService();
|
|
244
|
-
const error = await validateApiToken(cf, token);
|
|
245
|
-
if (error) {
|
|
246
|
-
formatError(new CLIError(error, ExitCode.ERROR), globalOpts);
|
|
247
|
-
process.exit(ExitCode.ERROR);
|
|
248
|
-
}
|
|
249
|
-
if (!globalOpts.quiet) {
|
|
250
|
-
formatSuccess("Cloudflare API token validated", globalOpts);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// Use engine to build config
|
|
254
|
-
const engine = new WizardEngine();
|
|
255
|
-
engine.execute({ checksPassed: true });
|
|
256
|
-
engine.execute({
|
|
257
|
-
apiToken: token,
|
|
258
|
-
accountId: account,
|
|
259
|
-
secretStoreId: secretStore,
|
|
260
|
-
subdomain: prefix,
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
// Apply preset if provided
|
|
264
|
-
if (
|
|
265
|
-
options.preset &&
|
|
266
|
-
["minimal", "standard", "full"].includes(options.preset)
|
|
267
|
-
) {
|
|
268
|
-
engine.execute({ preset: options.preset });
|
|
269
|
-
} else {
|
|
270
|
-
engine.execute({ preset: "minimal" as const });
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
const config = engine.buildConfig();
|
|
274
|
-
await writeWorkersJsonc(config, globalOpts);
|
|
275
|
-
await createDevVars(config, {}, globalOpts);
|
|
276
|
-
|
|
277
|
-
if (!globalOpts.quiet) {
|
|
278
|
-
p.outro("Setup complete! Run hoox check setup to verify.");
|
|
279
|
-
}
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// ── Interactive mode ─────────────────────────────────────────────────
|
|
284
|
-
|
|
285
|
-
p.intro(theme.heading("Hoox Setup Wizard"));
|
|
286
|
-
|
|
287
|
-
// Try to resume from saved state
|
|
288
|
-
let engine: WizardEngine | null = null;
|
|
289
|
-
if (options.resume) {
|
|
290
|
-
engine = await loadSavedState();
|
|
291
|
-
if (engine) {
|
|
292
|
-
const resumeStep = engine.getCurrentStep().label;
|
|
293
|
-
const resumed = await p.confirm({
|
|
294
|
-
message: `Found saved wizard state. Resume from "${resumeStep}"?`,
|
|
295
|
-
initialValue: true,
|
|
296
|
-
});
|
|
297
|
-
if (p.isCancel(resumed)) {
|
|
298
|
-
p.cancel("Setup cancelled.");
|
|
299
|
-
process.exit(0);
|
|
300
|
-
}
|
|
301
|
-
if (!resumed) {
|
|
302
|
-
engine = null;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
if (!engine) {
|
|
308
|
-
engine = new WizardEngine();
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
const skipRiskWarning = options.acceptRisk;
|
|
312
|
-
if (!skipRiskWarning && engine.getCurrentStep().id === "PREREQUISITES") {
|
|
313
|
-
const accepted = await p.confirm({
|
|
314
|
-
message:
|
|
315
|
-
"Hoox connects to live trading exchanges and can execute real trades with real money. " +
|
|
316
|
-
"By continuing, you acknowledge that you are solely responsible for all trading activity " +
|
|
317
|
-
"and accept the risk of financial loss. Do you accept these terms?",
|
|
318
|
-
initialValue: false,
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
if (p.isCancel(accepted)) {
|
|
322
|
-
p.cancel("Setup cancelled.");
|
|
323
|
-
process.exit(0);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
if (!accepted) {
|
|
327
|
-
p.outro("Setup cancelled. See DISCLAIMER.md for full terms.");
|
|
328
|
-
process.exit(0);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// Step 0: Prerequisites check (always passes — CLI is running)
|
|
332
|
-
engine.execute({ checksPassed: true });
|
|
333
|
-
await saveState(engine);
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// ── Step 1: Cloudflare API token ──────────────────────────────────────
|
|
337
|
-
if (engine.getCurrentStep().id === "CLOUDFLARE_CONFIG") {
|
|
338
|
-
const cf = new CloudflareService();
|
|
339
|
-
let apiToken: string | symbol = "";
|
|
340
|
-
let tokenValid = false;
|
|
341
|
-
|
|
342
|
-
while (!tokenValid) {
|
|
343
|
-
apiToken = await p.password({
|
|
344
|
-
message: "Cloudflare API token:",
|
|
345
|
-
validate(value) {
|
|
346
|
-
if (!value) return "API token is required";
|
|
347
|
-
return;
|
|
348
|
-
},
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
if (p.isCancel(apiToken)) {
|
|
352
|
-
p.cancel("Setup cancelled.");
|
|
353
|
-
process.exit(0);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
p.log.step("Validating Cloudflare API token...");
|
|
357
|
-
const tokenError = await validateApiToken(cf, apiToken as string);
|
|
358
|
-
if (tokenError) {
|
|
359
|
-
p.log.error(tokenError);
|
|
360
|
-
p.log.warn("Please check your token and try again.");
|
|
361
|
-
} else {
|
|
362
|
-
tokenValid = true;
|
|
363
|
-
formatSuccess("Cloudflare API token validated", globalOpts);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
const defaultAccountId = await getExistingAccountId();
|
|
368
|
-
const accountResult = await p.text({
|
|
369
|
-
message: "Cloudflare Account ID:",
|
|
370
|
-
placeholder: "abc123...",
|
|
371
|
-
defaultValue: defaultAccountId ?? "",
|
|
372
|
-
validate(value) {
|
|
373
|
-
if (!value) return "Account ID is required";
|
|
374
|
-
if (!/^[a-f0-9]{32}$/i.test(value.trim())) {
|
|
375
|
-
return "Account ID should be a 32-character hex string";
|
|
376
|
-
}
|
|
377
|
-
return;
|
|
378
|
-
},
|
|
379
|
-
});
|
|
380
|
-
if (p.isCancel(accountResult)) {
|
|
381
|
-
p.cancel("Setup cancelled.");
|
|
382
|
-
process.exit(0);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
const secretStoreResult = await p.text({
|
|
386
|
-
message: "Cloudflare Secret Store ID:",
|
|
387
|
-
placeholder: "optional",
|
|
388
|
-
defaultValue: "",
|
|
389
|
-
});
|
|
390
|
-
if (p.isCancel(secretStoreResult)) {
|
|
391
|
-
p.cancel("Setup cancelled.");
|
|
392
|
-
process.exit(0);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
const prefixResult = await p.text({
|
|
396
|
-
message: "Subdomain prefix:",
|
|
397
|
-
placeholder: "cryptolinx",
|
|
398
|
-
defaultValue: "cryptolinx",
|
|
399
|
-
validate(value) {
|
|
400
|
-
if (!value) return "Subdomain prefix is required";
|
|
401
|
-
return;
|
|
402
|
-
},
|
|
403
|
-
});
|
|
404
|
-
if (p.isCancel(prefixResult)) {
|
|
405
|
-
p.cancel("Setup cancelled.");
|
|
406
|
-
process.exit(0);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
engine.execute({
|
|
410
|
-
apiToken: apiToken as string,
|
|
411
|
-
accountId: accountResult,
|
|
412
|
-
secretStoreId: secretStoreResult,
|
|
413
|
-
subdomain: prefixResult,
|
|
414
|
-
});
|
|
415
|
-
await saveState(engine);
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// ── Step 2: Worker selection with presets ──────────────────────────────
|
|
419
|
-
if (engine.getCurrentStep().id === "WORKER_SELECTION") {
|
|
420
|
-
const presetChoice = await p.select({
|
|
421
|
-
message: "Select a worker preset:",
|
|
422
|
-
options: [
|
|
423
|
-
{
|
|
424
|
-
value: "minimal",
|
|
425
|
-
label: "Minimal",
|
|
426
|
-
hint: "Gateway + D1 — webhook processing only",
|
|
427
|
-
},
|
|
428
|
-
{
|
|
429
|
-
value: "standard",
|
|
430
|
-
label: "Standard",
|
|
431
|
-
hint: "Trading + analytics + Telegram",
|
|
432
|
-
},
|
|
433
|
-
{
|
|
434
|
-
value: "full",
|
|
435
|
-
label: "Full",
|
|
436
|
-
hint: "All workers + AI + DeFi + email",
|
|
437
|
-
},
|
|
438
|
-
],
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
if (p.isCancel(presetChoice)) {
|
|
442
|
-
p.cancel("Setup cancelled.");
|
|
443
|
-
process.exit(0);
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
engine.execute({ preset: presetChoice as string });
|
|
447
|
-
await saveState(engine);
|
|
448
|
-
|
|
449
|
-
// Show selected workers
|
|
450
|
-
const state = engine.getState();
|
|
451
|
-
p.log.step(`Selected workers: ${state.selectedWorkers.join(", ")}`);
|
|
452
|
-
if (state.selectedIntegrations.length > 0) {
|
|
453
|
-
p.log.step(`Integrations: ${state.selectedIntegrations.join(", ")}`);
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
// ── Step 3: Provisioning ──────────────────────────────────────────────
|
|
458
|
-
if (engine.getCurrentStep().id === "PROVISIONING") {
|
|
459
|
-
const plan = engine.getProvisioningPlan();
|
|
460
|
-
const hasResources =
|
|
461
|
-
plan.d1Databases.length > 0 || plan.kvNamespaces.length > 0;
|
|
462
|
-
|
|
463
|
-
if (hasResources) {
|
|
464
|
-
const shouldProvision = await p.confirm({
|
|
465
|
-
message: `Provision Cloudflare resources? (${[
|
|
466
|
-
...plan.d1Databases.map((d) => `D1:${d}`),
|
|
467
|
-
...plan.kvNamespaces.map((k) => `KV:${k}`),
|
|
468
|
-
].join(", ")})`,
|
|
469
|
-
initialValue: true,
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
if (p.isCancel(shouldProvision)) {
|
|
473
|
-
p.cancel("Setup cancelled.");
|
|
474
|
-
process.exit(0);
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
if (shouldProvision) {
|
|
478
|
-
p.log.step("Provisioning infrastructure...");
|
|
479
|
-
const provisioner = new CLIProvisioner();
|
|
480
|
-
const result = await provisioner.provision(plan);
|
|
481
|
-
|
|
482
|
-
if (result.success) {
|
|
483
|
-
formatSuccess(`Created: ${result.created.join(", ")}`, globalOpts);
|
|
484
|
-
} else {
|
|
485
|
-
p.log.warn(`Provisioning had issues: ${result.errors.join("; ")}`);
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
engine.execute({ provisioningResults: result });
|
|
489
|
-
} else {
|
|
490
|
-
engine.execute({
|
|
491
|
-
provisioningResults: { success: true, created: [], errors: [] },
|
|
492
|
-
});
|
|
493
|
-
}
|
|
494
|
-
} else {
|
|
495
|
-
engine.execute({});
|
|
496
|
-
}
|
|
497
|
-
await saveState(engine);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// ── Step 4: Secrets ──────────────────────────────────────────────────
|
|
501
|
-
if (engine.getCurrentStep().id === "SECRETS") {
|
|
502
|
-
const state = engine.getState();
|
|
503
|
-
const collectedSecrets: Record<string, Record<string, string>> = {};
|
|
504
|
-
|
|
505
|
-
if (state.selectedIntegrations.length > 0) {
|
|
506
|
-
p.log.step("Collecting integration secrets...");
|
|
507
|
-
|
|
508
|
-
for (const key of state.selectedIntegrations) {
|
|
509
|
-
const { INTEGRATIONS } =
|
|
510
|
-
await import("@jango-blockchained/hoox-shared");
|
|
511
|
-
const integration = INTEGRATIONS.find((i) => i.key === key);
|
|
512
|
-
if (!integration || Object.keys(integration.secrets).length === 0)
|
|
513
|
-
continue;
|
|
514
|
-
|
|
515
|
-
const secretEntries = Object.entries(integration.secrets);
|
|
516
|
-
const groupFields: Record<string, () => Promise<string | symbol>> = {};
|
|
517
|
-
|
|
518
|
-
for (const [secretName] of secretEntries) {
|
|
519
|
-
groupFields[secretName] = () =>
|
|
520
|
-
p.password({
|
|
521
|
-
message: `${integration.secrets[secretName]}:`,
|
|
522
|
-
validate(value) {
|
|
523
|
-
if (!value) return "This secret is required";
|
|
524
|
-
return;
|
|
525
|
-
},
|
|
526
|
-
});
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
const collected = await p.group(groupFields, {
|
|
530
|
-
onCancel: () => {
|
|
531
|
-
p.cancel("Setup cancelled.");
|
|
532
|
-
process.exit(0);
|
|
533
|
-
},
|
|
534
|
-
});
|
|
535
|
-
|
|
536
|
-
collectedSecrets[key] = {};
|
|
537
|
-
for (const [secretName] of secretEntries) {
|
|
538
|
-
const val = collected[secretName];
|
|
539
|
-
collectedSecrets[key][secretName] =
|
|
540
|
-
typeof val === "string" ? val : "";
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
engine.execute({ secrets: collectedSecrets });
|
|
546
|
-
await saveState(engine);
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
// ── Step 5: Config Write ──────────────────────────────────────────────
|
|
550
|
-
if (engine.getCurrentStep().id === "CONFIG_WRITE") {
|
|
551
|
-
p.log.step("Writing configuration...");
|
|
552
|
-
|
|
553
|
-
const config = engine.buildConfig();
|
|
554
|
-
const state = engine.getState();
|
|
555
|
-
|
|
556
|
-
await writeWorkersJsonc(config, globalOpts);
|
|
557
|
-
await createDevVars(config, state.secrets, globalOpts);
|
|
558
|
-
|
|
559
|
-
engine.execute({});
|
|
560
|
-
await cleanupState(); // Clean up state file — wizard complete
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
// ── Step 6: Deploy ──────────────────────────────────────────────────
|
|
564
|
-
if (engine.getCurrentStep().id === "DEPLOY") {
|
|
565
|
-
const shouldDeploy = await p.confirm({
|
|
566
|
-
message: "Deploy workers now?",
|
|
567
|
-
initialValue: false,
|
|
568
|
-
});
|
|
569
|
-
|
|
570
|
-
if (p.isCancel(shouldDeploy)) {
|
|
571
|
-
p.cancel("Setup cancelled.");
|
|
572
|
-
process.exit(0);
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
if (shouldDeploy) {
|
|
576
|
-
p.log.step("Deploying workers...");
|
|
577
|
-
const proc = Bun.spawn(["hoox", "deploy", "all"], {
|
|
578
|
-
stdout: "inherit",
|
|
579
|
-
stderr: "inherit",
|
|
580
|
-
});
|
|
581
|
-
await proc.exited;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
engine.execute({});
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
// ── Done ──────────────────────────────────────────────────────────────
|
|
588
|
-
p.outro(
|
|
589
|
-
theme.success("Setup complete! Run ") +
|
|
590
|
-
theme.bold("hoox check setup") +
|
|
591
|
-
theme.success(" to verify.")
|
|
592
|
-
);
|
|
593
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Types for the `hoox init` interactive setup wizard.
|
|
3
|
-
*
|
|
4
|
-
* Re-exports shared integration configs from @jango-blockchained/hoox-shared.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/** CLI flags for non-interactive mode. */
|
|
8
|
-
export interface InitOptions {
|
|
9
|
-
/** --token: Cloudflare API token (non-interactive mode) */
|
|
10
|
-
token?: string;
|
|
11
|
-
/** --account: Cloudflare account ID (non-interactive mode) */
|
|
12
|
-
account?: string;
|
|
13
|
-
/** --secret-store: Secret Store ID (non-interactive mode) */
|
|
14
|
-
secretStore?: string;
|
|
15
|
-
/** --prefix: Subdomain prefix (non-interactive mode) */
|
|
16
|
-
prefix?: string;
|
|
17
|
-
/** --accept-risk: Skip the risk acknowledgment confirmation */
|
|
18
|
-
acceptRisk?: boolean;
|
|
19
|
-
/** --resume: Resume from saved wizard state */
|
|
20
|
-
resume?: boolean;
|
|
21
|
-
/** --preset: Use a preset worker template (non-interactive) */
|
|
22
|
-
preset?: string;
|
|
23
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { registerLogsCommand } from "./logs-command.js";
|