@jango-blockchained/hoox-cli 0.5.1 → 0.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/bin/hoox.js +3 -4
  2. package/dist/index.js +32545 -0
  3. package/package.json +10 -15
  4. package/src/commands/check/check-command.test.ts +0 -476
  5. package/src/commands/check/check-command.ts +0 -1144
  6. package/src/commands/check/index.ts +0 -10
  7. package/src/commands/check/prerequisites-command.test.ts +0 -19
  8. package/src/commands/check/prerequisites-command.ts +0 -96
  9. package/src/commands/check/types.ts +0 -103
  10. package/src/commands/clone/clone-command.test.ts +0 -442
  11. package/src/commands/clone/clone-command.ts +0 -431
  12. package/src/commands/clone/index.ts +0 -1
  13. package/src/commands/config/config-command.test.ts +0 -583
  14. package/src/commands/config/config-command.ts +0 -897
  15. package/src/commands/config/env-command.test.ts +0 -43
  16. package/src/commands/config/env-command.ts +0 -295
  17. package/src/commands/config/index.ts +0 -3
  18. package/src/commands/config/kv-command.test.ts +0 -14
  19. package/src/commands/config/kv-command.ts +0 -290
  20. package/src/commands/dashboard/dashboard-command.test.ts +0 -47
  21. package/src/commands/dashboard/dashboard-command.ts +0 -128
  22. package/src/commands/dashboard/index.ts +0 -1
  23. package/src/commands/db/db-command.test.ts +0 -21
  24. package/src/commands/db/db-command.ts +0 -312
  25. package/src/commands/db/index.ts +0 -1
  26. package/src/commands/deploy/deploy-command.test.ts +0 -304
  27. package/src/commands/deploy/deploy-command.ts +0 -1135
  28. package/src/commands/deploy/index.ts +0 -2
  29. package/src/commands/deploy/telegram-service.ts +0 -76
  30. package/src/commands/deploy/types.ts +0 -34
  31. package/src/commands/dev/dev-command.test.ts +0 -383
  32. package/src/commands/dev/dev-command.ts +0 -398
  33. package/src/commands/dev/index.ts +0 -1
  34. package/src/commands/infra/index.ts +0 -5
  35. package/src/commands/infra/infra-command.test.ts +0 -719
  36. package/src/commands/infra/infra-command.ts +0 -974
  37. package/src/commands/infra/types.ts +0 -23
  38. package/src/commands/init/__tests__/cli-provisioner.test.ts +0 -17
  39. package/src/commands/init/cli-provisioner.ts +0 -71
  40. package/src/commands/init/index.ts +0 -1
  41. package/src/commands/init/init-command.test.ts +0 -876
  42. package/src/commands/init/init-command.ts +0 -593
  43. package/src/commands/init/types.ts +0 -23
  44. package/src/commands/logs/index.ts +0 -1
  45. package/src/commands/logs/logs-command.test.ts +0 -294
  46. package/src/commands/logs/logs-command.ts +0 -452
  47. package/src/commands/monitor/index.ts +0 -2
  48. package/src/commands/monitor/monitor-command.test.ts +0 -263
  49. package/src/commands/monitor/monitor-command.ts +0 -269
  50. package/src/commands/monitor/monitor-service.ts +0 -65
  51. package/src/commands/monitor/types.ts +0 -13
  52. package/src/commands/repair/index.ts +0 -2
  53. package/src/commands/repair/repair-command.test.ts +0 -224
  54. package/src/commands/repair/repair-command.ts +0 -220
  55. package/src/commands/repair/repair-service.ts +0 -138
  56. package/src/commands/repair/types.ts +0 -13
  57. package/src/commands/test/index.ts +0 -2
  58. package/src/commands/test/test-command.test.ts +0 -319
  59. package/src/commands/test/test-command.ts +0 -406
  60. package/src/commands/tui/index.ts +0 -1
  61. package/src/commands/tui/tui-command.ts +0 -87
  62. package/src/commands/update/index.ts +0 -1
  63. package/src/commands/update/update-command.ts +0 -168
  64. package/src/commands/waf/index.ts +0 -2
  65. package/src/commands/waf/types.ts +0 -48
  66. package/src/commands/waf/waf-command.test.ts +0 -506
  67. package/src/commands/waf/waf-command.ts +0 -548
  68. package/src/index.ts +0 -269
  69. package/src/services/cloudflare/cloudflare-service.test.ts +0 -658
  70. package/src/services/cloudflare/cloudflare-service.ts +0 -433
  71. package/src/services/cloudflare/index.ts +0 -2
  72. package/src/services/cloudflare/types.ts +0 -30
  73. package/src/services/config/config-service.test.ts +0 -395
  74. package/src/services/config/config-service.ts +0 -207
  75. package/src/services/config/index.ts +0 -9
  76. package/src/services/config/types.ts +0 -66
  77. package/src/services/db/db-service.test.ts +0 -51
  78. package/src/services/db/db-service.ts +0 -131
  79. package/src/services/db/index.ts +0 -1
  80. package/src/services/docker/docker-service.ts +0 -155
  81. package/src/services/docker/index.ts +0 -1
  82. package/src/services/env/env-service.test.ts +0 -236
  83. package/src/services/env/env-service.ts +0 -386
  84. package/src/services/env/index.ts +0 -1
  85. package/src/services/kv/index.ts +0 -1
  86. package/src/services/kv/kv-sync-service.test.ts +0 -38
  87. package/src/services/kv/kv-sync-service.ts +0 -234
  88. package/src/services/prerequisites/index.ts +0 -1
  89. package/src/services/prerequisites/prerequisites-service.test.ts +0 -89
  90. package/src/services/prerequisites/prerequisites-service.ts +0 -377
  91. package/src/services/prerequisites/types.ts +0 -48
  92. package/src/services/secrets/index.ts +0 -11
  93. package/src/services/secrets/secrets-service.test.ts +0 -495
  94. package/src/services/secrets/secrets-service.ts +0 -287
  95. package/src/services/secrets/types.ts +0 -50
  96. package/src/services/update/index.ts +0 -2
  97. package/src/services/update/update-service.test.ts +0 -76
  98. package/src/services/update/update-service.ts +0 -193
  99. package/src/ui/banner.ts +0 -141
  100. package/src/ui/index.ts +0 -8
  101. package/src/ui/menu.ts +0 -490
  102. package/src/utils/errors.test.ts +0 -69
  103. package/src/utils/errors.ts +0 -23
  104. package/src/utils/formatters.test.ts +0 -180
  105. package/src/utils/formatters.ts +0 -269
  106. package/src/utils/theme.ts +0 -94
@@ -1,1144 +0,0 @@
1
- /**
2
- * `hoox check` command group — validation, health checks, and auto-repair.
3
- *
4
- * Subcommands:
5
- * prerequisites [--tool] — Validate toolchain and account prerequisites
6
- * setup [--json] — Full system validation (Config, Infrastructure, Secrets, Database)
7
- * health [--fix] — Worker connectivity and responsiveness checks
8
- * fix [--dry-run] — Repair known common issues
9
- * submodule-gitignore (sg) — Validate and fix worker submodule .gitignore files
10
- */
11
- import { Command } from "commander";
12
- import { readdir } from "node:fs/promises";
13
- import path from "node:path";
14
- import { spinner } from "@clack/prompts";
15
- import { ConfigService } from "../../services/config/index.js";
16
- import { CloudflareService } from "../../services/cloudflare/index.js";
17
- import { SecretsService } from "../../services/secrets/index.js";
18
- import { registerPrerequisitesCommand } from "./prerequisites-command.js";
19
- import { theme, icons } from "../../utils/theme.js";
20
- import {
21
- formatSuccess,
22
- formatError,
23
- formatTable,
24
- } from "../../utils/formatters.js";
25
- import { CLIError, ExitCode } from "../../utils/errors.js";
26
- import type { FormatOptions } from "../../utils/formatters.js";
27
- import type {
28
- CheckResult,
29
- CheckCategory,
30
- CheckReport,
31
- HealthCheckResult,
32
- FixAction,
33
- FixReport,
34
- } from "./types.js";
35
-
36
- // ---------------------------------------------------------------------------
37
- // Submodule Gitignore Constants
38
- // ---------------------------------------------------------------------------
39
-
40
- /** Standard .gitignore template for worker submodules */
41
- const WORKER_GITIGNORE_TEMPLATE = `# Environment and secrets (keep example, exclude actual)
42
- .dev.vars
43
- !.dev.vars.example
44
-
45
- # Wrangler config (keep example, exclude actual)
46
- wrangler.jsonc
47
- !wrangler.jsonc.example
48
-
49
- # Wrangler cache and build artifacts
50
- .wrangler/
51
- dist/
52
- build/
53
-
54
- # Dependencies
55
- node_modules/
56
-
57
- # IDE and editor files
58
- .idea/
59
- .vscode/
60
- *.swp
61
- *.swo
62
-
63
- # OS files
64
- .DS_Store
65
- Thumbs.db
66
- `;
67
-
68
- /** Critical entries that must be present in a valid worker .gitignore */
69
- const CRITICAL_GITIGNORE_ENTRIES = [
70
- "wrangler.jsonc",
71
- ".dev.vars",
72
- ".wrangler/",
73
- "node_modules/",
74
- "!.dev.vars.example",
75
- "!wrangler.jsonc.example",
76
- ];
77
-
78
- // ---------------------------------------------------------------------------
79
- // Helpers
80
- // ---------------------------------------------------------------------------
81
-
82
- /**
83
- * Build a CheckReport from categorised check results.
84
- */
85
- function buildReport(categories: CheckCategory[]): CheckReport {
86
- let total = 0;
87
- let passed = 0;
88
- let failed = 0;
89
- let warnings = 0;
90
-
91
- for (const cat of categories) {
92
- for (const check of cat.checks) {
93
- total++;
94
- if (check.success) passed++;
95
- else failed++;
96
- warnings += check.warnings.length;
97
- }
98
- }
99
-
100
- return {
101
- success: failed === 0,
102
- categories,
103
- summary: { total, passed, failed, warnings },
104
- };
105
- }
106
-
107
- /**
108
- * Return a single-character icon for a check result.
109
- */
110
- function checkIcon(success: boolean): string {
111
- return success ? icons.success : icons.error;
112
- }
113
-
114
- // ---------------------------------------------------------------------------
115
- // Submodule Gitignore Helpers
116
- // ---------------------------------------------------------------------------
117
-
118
- /**
119
- * Get all worker directories from the workers/ folder.
120
- */
121
- async function getWorkerDirs(): Promise<string[]> {
122
- const workersDir = path.resolve(process.cwd(), "workers");
123
- try {
124
- await Bun.file(workersDir).exists();
125
- } catch {
126
- return [];
127
- }
128
-
129
- try {
130
- const entries = await readdir(workersDir, { withFileTypes: true });
131
- return entries
132
- .filter((e) => e.isDirectory() && e.name !== "node_modules")
133
- .map((e) => path.join(workersDir, e.name));
134
- } catch {
135
- return [];
136
- }
137
- }
138
-
139
- /**
140
- * Check if a file or directory is tracked by git in a given directory.
141
- */
142
- async function isGitTracked(dir: string, filename: string): Promise<boolean> {
143
- try {
144
- const proc = Bun.spawn(["git", "ls-files", "--error-unmatch", filename], {
145
- cwd: dir,
146
- stdout: "pipe",
147
- stderr: "pipe",
148
- });
149
- const exitCode = await proc.exited;
150
- return exitCode === 0;
151
- } catch {
152
- return false;
153
- }
154
- }
155
-
156
- /**
157
- * Run git rm --cached to untrack a file while keeping it locally.
158
- */
159
- async function gitUntrackFile(dir: string, filename: string): Promise<void> {
160
- await new Promise<void>((resolve, reject) => {
161
- const proc = Bun.spawn(["git", "rm", "--cached", filename], {
162
- cwd: dir,
163
- stdout: "pipe",
164
- stderr: "pipe",
165
- });
166
- proc.exited
167
- .then((code) => {
168
- if (code === 0) resolve();
169
- else reject(new Error(`git rm --cached failed with code ${code}`));
170
- })
171
- .catch(reject);
172
- });
173
- }
174
-
175
- // ---------------------------------------------------------------------------
176
- // Category 1: Config checks
177
- // ---------------------------------------------------------------------------
178
-
179
- async function runConfigChecks(
180
- configService: ConfigService
181
- ): Promise<CheckCategory> {
182
- const checks: CheckResult[] = [];
183
-
184
- // Validate wrangler.jsonc structure
185
- const validation = configService.validate();
186
- checks.push({
187
- name: "wrangler.jsonc validation",
188
- success: validation.valid,
189
- errors: validation.errors,
190
- warnings: [],
191
- });
192
-
193
- // Check required global fields
194
- const global = configService.getGlobal();
195
- const globalErrors: string[] = [];
196
- if (!global.cloudflare_account_id) {
197
- globalErrors.push("global.cloudflare_account_id is required");
198
- }
199
- checks.push({
200
- name: "Global config",
201
- success: globalErrors.length === 0,
202
- errors: globalErrors,
203
- warnings: global.cloudflare_api_token
204
- ? []
205
- : [
206
- "global.cloudflare_api_token uses <USE_WRANGLER_SECRET_PUT> placeholder — set via wrangler secret",
207
- ],
208
- });
209
-
210
- // Check that each worker has a path
211
- const workers = configService.listWorkers();
212
- const pathErrors: string[] = [];
213
- for (const name of workers) {
214
- const worker = configService.getWorker(name);
215
- if (!worker?.path) {
216
- pathErrors.push(`Worker "${name}" missing path field`);
217
- }
218
- }
219
- checks.push({
220
- name: "Worker paths",
221
- success: pathErrors.length === 0,
222
- errors: pathErrors,
223
- warnings: [],
224
- });
225
-
226
- return { name: "Config", checks };
227
- }
228
-
229
- // ---------------------------------------------------------------------------
230
- // Category 2: Infrastructure checks
231
- // ---------------------------------------------------------------------------
232
-
233
- async function runInfraChecks(cf: CloudflareService): Promise<CheckCategory> {
234
- const checks: CheckResult[] = [];
235
-
236
- // D1 databases
237
- const d1Result = await cf.d1List();
238
- checks.push({
239
- name: "D1 Databases",
240
- success: d1Result.ok,
241
- errors: d1Result.ok
242
- ? []
243
- : [d1Result.error ?? "Failed to list D1 databases"],
244
- warnings: [],
245
- });
246
-
247
- // KV namespaces
248
- const kvResult = await cf.kvList();
249
- checks.push({
250
- name: "KV Namespaces",
251
- success: kvResult.ok,
252
- errors: kvResult.ok
253
- ? []
254
- : [kvResult.error ?? "Failed to list KV namespaces"],
255
- warnings: [],
256
- });
257
-
258
- // R2 buckets
259
- const r2Result = await cf.r2List();
260
- checks.push({
261
- name: "R2 Buckets",
262
- success: r2Result.ok,
263
- errors: r2Result.ok ? [] : [r2Result.error ?? "Failed to list R2 buckets"],
264
- warnings: [],
265
- });
266
-
267
- // Queues
268
- const queueResult = await cf.queueList();
269
- checks.push({
270
- name: "Queues",
271
- success: queueResult.ok,
272
- errors: queueResult.ok
273
- ? []
274
- : [queueResult.error ?? "Failed to list queues"],
275
- warnings: [],
276
- });
277
-
278
- return { name: "Infrastructure", checks };
279
- }
280
-
281
- // ---------------------------------------------------------------------------
282
- // Category 3: Secrets checks
283
- // ---------------------------------------------------------------------------
284
-
285
- async function runSecretsChecks(
286
- secretsService: SecretsService,
287
- configService: ConfigService,
288
- cf: CloudflareService
289
- ): Promise<CheckCategory> {
290
- const checks: CheckResult[] = [];
291
- const enabledWorkers = configService.listEnabledWorkers();
292
- const errors: string[] = [];
293
- const warnings: string[] = [];
294
-
295
- for (const workerName of enabledWorkers) {
296
- const result = await secretsService.checkLocalSecrets(workerName);
297
- if (result.missing.length > 0) {
298
- errors.push(
299
- `Worker "${workerName}" missing secrets: ${result.missing.join(", ")}`
300
- );
301
- }
302
- for (const secret of result.secrets) {
303
- if (secret.set && secret.source) {
304
- warnings.push(
305
- `Worker "${workerName}": ${secret.name} is set locally (source: ${secret.source})`
306
- );
307
- }
308
- }
309
- }
310
-
311
- checks.push({
312
- name: "Worker Secrets (local)",
313
- success: errors.length === 0,
314
- errors,
315
- warnings,
316
- });
317
-
318
- // Check remote secrets via CloudflareService
319
- const remoteErrors: string[] = [];
320
- const remoteWarnings: string[] = [];
321
- for (const workerName of enabledWorkers) {
322
- const secrets = secretsService.listSecrets(workerName);
323
- if (secrets.length === 0) continue;
324
-
325
- const result = await cf.secretList(workerName);
326
- if (!result.ok) {
327
- remoteErrors.push(`Worker "${workerName}": ${result.error}`);
328
- } else {
329
- remoteWarnings.push(
330
- `Worker "${workerName}": remote secrets listed OK (${secrets.length} expected)`
331
- );
332
- }
333
- }
334
-
335
- checks.push({
336
- name: "Worker Secrets (Cloudflare)",
337
- success: remoteErrors.length === 0,
338
- errors: remoteErrors,
339
- warnings: remoteWarnings,
340
- });
341
-
342
- return { name: "Secrets", checks };
343
- }
344
-
345
- // ---------------------------------------------------------------------------
346
- // Category 4: Database checks
347
- // ---------------------------------------------------------------------------
348
-
349
- async function runDatabaseChecks(
350
- cf: CloudflareService,
351
- configService: ConfigService
352
- ): Promise<CheckCategory> {
353
- const checks: CheckResult[] = [];
354
-
355
- // Look for the D1 worker's database_name
356
- const d1Worker = configService.getWorker("d1-worker");
357
- const dbName =
358
- (d1Worker?.vars as Record<string, string> | undefined)?.database_name ??
359
- "my-database";
360
-
361
- // Check that database exists via wrangler d1 list
362
- const d1ListResult = await cf.d1List();
363
- if (d1ListResult.ok) {
364
- const hasDb = d1ListResult.data.includes(dbName);
365
- checks.push({
366
- name: `D1 Database "${dbName}"`,
367
- success: hasDb,
368
- errors: hasDb
369
- ? []
370
- : [
371
- `Database "${dbName}" not found. Create with: wrangler d1 create ${dbName}`,
372
- ],
373
- warnings: [],
374
- });
375
- } else {
376
- checks.push({
377
- name: `D1 Database "${dbName}"`,
378
- success: false,
379
- errors: [d1ListResult.error ?? "Could not verify database existence"],
380
- warnings: [],
381
- });
382
- }
383
-
384
- // Verify tables exist (best effort via wrangler d1 execute)
385
- const requiredTables = [
386
- "trade_signals",
387
- "trades",
388
- "positions",
389
- "balances",
390
- "system_logs",
391
- ];
392
-
393
- // Only run table check if the database exists
394
- try {
395
- const hasDb = d1ListResult.ok && d1ListResult.data.includes(dbName);
396
- if (!hasDb) {
397
- checks.push({
398
- name: "Required Tables",
399
- success: true,
400
- errors: [],
401
- warnings: ["Skipped: database does not exist yet"],
402
- });
403
- } else {
404
- const sqlResult = await cf.d1Execute(
405
- dbName,
406
- `SELECT name FROM sqlite_master WHERE type='table'`,
407
- true
408
- );
409
-
410
- if (sqlResult.ok) {
411
- // wrangler d1 execute --json outputs the result rows
412
- // Parse the JSON output to extract table names
413
- const tableNames: string[] = [];
414
- const lines = sqlResult.data.split("\n");
415
- for (const line of lines) {
416
- try {
417
- const parsed = JSON.parse(line.trim());
418
- if (parsed.results) {
419
- for (const row of parsed.results) {
420
- if (row.name) tableNames.push(row.name);
421
- }
422
- }
423
- } catch {
424
- // skip non-JSON lines
425
- }
426
- }
427
-
428
- const missing = requiredTables.filter((t) => !tableNames.includes(t));
429
- if (missing.length === 0) {
430
- checks.push({
431
- name: "Required Tables",
432
- success: true,
433
- errors: [],
434
- warnings: [],
435
- });
436
- } else {
437
- checks.push({
438
- name: "Required Tables",
439
- success: false,
440
- errors: [
441
- `Missing tables: ${missing.join(", ")}. Run migrations first.`,
442
- ],
443
- warnings: [],
444
- });
445
- }
446
- } else {
447
- // Query failed — show warning instead of hard error
448
- checks.push({
449
- name: "Required Tables",
450
- success: true,
451
- errors: [],
452
- warnings: [
453
- `Table schema not verified: ${sqlResult.error}. Run: wrangler d1 execute ${dbName} --command="SELECT name FROM sqlite_master WHERE type='table'" --remote`,
454
- ],
455
- });
456
- }
457
- }
458
- } catch {
459
- checks.push({
460
- name: "Required Tables",
461
- success: false,
462
- errors: ["Could not verify database tables"],
463
- warnings: [],
464
- });
465
- }
466
-
467
- return { name: "Database", checks };
468
- }
469
-
470
- /**
471
- * Render the check report as a human-readable table (non-JSON mode).
472
- */
473
- function renderReport(report: CheckReport): void {
474
- for (const category of report.categories) {
475
- const allPassed = category.checks.every((c) => c.success);
476
- const icon = allPassed ? icons.success : icons.error;
477
- process.stdout.write(`\n${icon} ${theme.heading(category.name)}\n`);
478
-
479
- for (const check of category.checks) {
480
- const marker = check.success
481
- ? theme.success(` ${icons.success}`)
482
- : theme.error(` ${icons.error}`);
483
- process.stdout.write(`${marker} ${check.name}\n`);
484
-
485
- for (const err of check.errors) {
486
- process.stdout.write(` ${theme.error(icons.error)} ${err}\n`);
487
- }
488
- for (const warn of check.warnings) {
489
- process.stdout.write(` ${theme.warning(icons.warning)} ${warn}\n`);
490
- }
491
- }
492
- }
493
-
494
- const { summary } = report;
495
- process.stdout.write(
496
- `\n${summary.failed > 0 ? theme.error(icons.error) : theme.success(icons.success)} ` +
497
- `Summary: ${summary.passed}/${summary.total} passed, ${summary.failed} failed, ${summary.warnings} warnings\n`
498
- );
499
- }
500
-
501
- // ---------------------------------------------------------------------------
502
- // Subcommand: check setup
503
- // ---------------------------------------------------------------------------
504
-
505
- async function handleSetup(opts: FormatOptions): Promise<void> {
506
- const s = spinner();
507
-
508
- try {
509
- const configService = new ConfigService();
510
- s.start("Loading config...");
511
- await configService.load();
512
- s.stop("Config loaded");
513
-
514
- const cf = new CloudflareService();
515
- const secretsService = await SecretsService.create();
516
-
517
- const categories: CheckCategory[] = [];
518
-
519
- // Category 1: Config
520
- s.start("Validating config...");
521
- categories.push(await runConfigChecks(configService));
522
- s.stop("Config validation complete");
523
-
524
- // Category 2: Infrastructure
525
- s.start("Checking infrastructure (D1, KV, R2, Queues)...");
526
- categories.push(await runInfraChecks(cf));
527
- s.stop("Infrastructure checks complete");
528
-
529
- // Category 3: Secrets
530
- s.start("Checking secrets...");
531
- categories.push(await runSecretsChecks(secretsService, configService, cf));
532
- s.stop("Secrets checks complete");
533
-
534
- // Category 4: Database
535
- s.start("Checking database...");
536
- categories.push(await runDatabaseChecks(cf, configService));
537
- s.stop("Database checks complete");
538
-
539
- const report = buildReport(categories);
540
-
541
- if (opts.json) {
542
- process.stdout.write(JSON.stringify(report, null, 2) + "\n");
543
- } else {
544
- renderReport(report);
545
- }
546
-
547
- if (!report.success) {
548
- process.exitCode = ExitCode.ERROR;
549
- }
550
- } catch (err) {
551
- const message = err instanceof Error ? err.message : String(err);
552
- formatError(message, opts);
553
- process.exitCode = ExitCode.ERROR;
554
- }
555
- }
556
-
557
- // ---------------------------------------------------------------------------
558
- // Subcommand: check health
559
- // ---------------------------------------------------------------------------
560
-
561
- async function handleHealth(
562
- opts: FormatOptions,
563
- autoFix: boolean
564
- ): Promise<void> {
565
- const s = spinner();
566
- const results: HealthCheckResult[] = [];
567
-
568
- try {
569
- const configService = new ConfigService();
570
- await configService.load();
571
- const cf = new CloudflareService();
572
-
573
- const enabledWorkers = configService.listEnabledWorkers();
574
-
575
- if (enabledWorkers.length === 0) {
576
- formatSuccess("No enabled workers to check", opts);
577
- return;
578
- }
579
-
580
- s.start(`Health-checking ${enabledWorkers.length} worker(s)...`);
581
-
582
- for (const workerName of enabledWorkers) {
583
- s.message(`Probing ${workerName}...`);
584
-
585
- try {
586
- // Check connectivity via CloudflareService.tail (briefly)
587
- const tailResult = await cf.tail(workerName);
588
- const healthy = tailResult.ok;
589
-
590
- results.push({
591
- worker: workerName,
592
- status: healthy ? "healthy" : "degraded",
593
- connectivity: healthy,
594
- error: tailResult.ok
595
- ? undefined
596
- : (tailResult.error ?? "Unknown error"),
597
- });
598
- } catch (err) {
599
- const message = err instanceof Error ? err.message : String(err);
600
- results.push({
601
- worker: workerName,
602
- status: "down",
603
- connectivity: false,
604
- error: message,
605
- });
606
- }
607
- }
608
-
609
- s.stop("Health check complete");
610
-
611
- if (opts.json) {
612
- process.stdout.write(JSON.stringify(results, null, 2) + "\n");
613
- } else {
614
- const rows = results.map((r) => ({
615
- Worker: r.worker,
616
- Status: r.status,
617
- Connectivity: r.connectivity ? "connected" : "failed",
618
- Error: r.error ?? "-",
619
- }));
620
- formatTable(rows, opts);
621
- }
622
-
623
- if (autoFix && results.some((r) => !r.connectivity)) {
624
- process.stdout.write(
625
- `\n${theme.warning(icons.warning)} Auto-fix flag set but health issues require manual investigation.\n`
626
- );
627
- process.stdout.write(`${theme.dim("Try: hoox check fix")}\n`);
628
- }
629
-
630
- const allHealthy = results.every((r) => r.status === "healthy");
631
- if (!allHealthy) {
632
- process.exitCode = ExitCode.ERROR;
633
- }
634
- } catch (err) {
635
- s.stop("Health check failed");
636
- const message = err instanceof Error ? err.message : String(err);
637
- formatError(message, opts);
638
- process.exitCode = ExitCode.ERROR;
639
- }
640
- }
641
-
642
- // ---------------------------------------------------------------------------
643
- // Subcommand: check fix
644
- // ---------------------------------------------------------------------------
645
-
646
- async function handleFix(opts: FormatOptions, dryRun: boolean): Promise<void> {
647
- const s = spinner();
648
- const actions: FixAction[] = [];
649
-
650
- try {
651
- const configService = new ConfigService();
652
- s.start("Loading config...");
653
- await configService.load();
654
- s.stop("Config loaded");
655
-
656
- const enabledWorkers = configService.listEnabledWorkers();
657
-
658
- // Fix 1: Check for missing .dev.vars files
659
- for (const workerName of enabledWorkers) {
660
- const worker = configService.getWorker(workerName);
661
- if (!worker?.path) continue;
662
-
663
- const devVarsPath = `${worker.path}/.dev.vars`;
664
- const devVarsFile = Bun.file(devVarsPath);
665
-
666
- if (!(await devVarsFile.exists())) {
667
- const action: FixAction = {
668
- description: `Create .dev.vars for worker "${workerName}"`,
669
- type: "file",
670
- target: devVarsPath,
671
- change: `Create ${devVarsPath} with placeholder secrets`,
672
- applied: false,
673
- };
674
-
675
- if (!dryRun) {
676
- try {
677
- const secrets =
678
- (worker.secrets ?? [])
679
- .map((s: string) => `${s}=placeholder_${s.toLowerCase()}`)
680
- .join("\n") + "\n";
681
- await Bun.write(devVarsPath, secrets);
682
- action.applied = true;
683
- } catch (err) {
684
- action.error = err instanceof Error ? err.message : String(err);
685
- }
686
- }
687
-
688
- actions.push(action);
689
- }
690
- }
691
-
692
- // Fix 2: Check for missing NODEJS_COMPAT flag in wrangler.jsonc
693
- for (const workerName of enabledWorkers) {
694
- const worker = configService.getWorker(workerName);
695
- if (!worker?.path) continue;
696
-
697
- const wranglerPath = `${worker.path}/wrangler.jsonc`;
698
- const wranglerFile = Bun.file(wranglerPath);
699
-
700
- if (await wranglerFile.exists()) {
701
- const content = await wranglerFile.text();
702
- // Strip comments for checking
703
- const strippedContent = content
704
- .replace(/\/\/.*$/gm, "")
705
- .replace(/\/\*[\s\S]*?\*\//g, "");
706
-
707
- if (!strippedContent.includes("nodejs_compat")) {
708
- const action: FixAction = {
709
- description: `Add nodejs_compat to compatibility_flags for worker "${workerName}"`,
710
- type: "flag",
711
- target: wranglerPath,
712
- change:
713
- 'Add "nodejs_compat" to compatibility_flags array in wrangler.jsonc',
714
- applied: false,
715
- };
716
-
717
- if (!dryRun) {
718
- try {
719
- // Parse JSONC, add compatibility_flags if not present
720
- const { parse } = await import("jsonc-parser");
721
- const config = parse(content) as Record<string, unknown>;
722
-
723
- if (!config.compatibility_flags) {
724
- config.compatibility_flags = ["nodejs_compat"];
725
- } else if (Array.isArray(config.compatibility_flags)) {
726
- const flags = config.compatibility_flags as string[];
727
- if (!flags.includes("nodejs_compat")) {
728
- flags.push("nodejs_compat");
729
- }
730
- }
731
-
732
- const updated = JSON.stringify(config, null, 2) + "\n";
733
- await Bun.write(wranglerPath, updated);
734
- action.applied = true;
735
- } catch (err) {
736
- action.error = err instanceof Error ? err.message : String(err);
737
- }
738
- }
739
-
740
- actions.push(action);
741
- }
742
- }
743
- }
744
-
745
- // Fix 3: Check wrangler.jsonc structure (must have name field)
746
- for (const workerName of enabledWorkers) {
747
- const worker = configService.getWorker(workerName);
748
- if (!worker?.path) continue;
749
-
750
- const wranglerPath = `${worker.path}/wrangler.jsonc`;
751
- const wranglerFile = Bun.file(wranglerPath);
752
-
753
- if (await wranglerFile.exists()) {
754
- const content = await wranglerFile.text();
755
- const strippedContent = content
756
- .replace(/\/\/.*$/gm, "")
757
- .replace(/\/\*[\s\S]*?\*\//g, "");
758
-
759
- try {
760
- const config = JSON.parse(strippedContent) as Record<string, unknown>;
761
- if (!config.name) {
762
- const action: FixAction = {
763
- description: `Add missing "name" field to wrangler.jsonc for worker "${workerName}"`,
764
- type: "config",
765
- target: wranglerPath,
766
- change: `Set "name" to "${workerName}" in ${wranglerPath}`,
767
- applied: false,
768
- };
769
-
770
- if (!dryRun) {
771
- config.name = workerName;
772
- const updated = JSON.stringify(config, null, 2) + "\n";
773
- await Bun.write(wranglerPath, updated);
774
- action.applied = true;
775
- }
776
-
777
- actions.push(action);
778
- }
779
- } catch {
780
- actions.push({
781
- description: `Invalid JSON in wrangler.jsonc for worker "${workerName}"`,
782
- type: "config",
783
- target: wranglerPath,
784
- change: "Fix JSON syntax errors manually",
785
- applied: false,
786
- error:
787
- "Could not parse wrangler.jsonc — fix JSON syntax errors manually",
788
- });
789
- }
790
- }
791
- }
792
-
793
- s.stop(`Fix scan complete (${dryRun ? "dry-run" : "applied"})`);
794
-
795
- // Calculate summary
796
- const applied = actions.filter((a) => a.applied).length;
797
- const failed = actions.filter((a) => a.error).length;
798
- const skipped = actions.length - applied - failed;
799
-
800
- const report: FixReport = {
801
- actions,
802
- dryRun,
803
- summary: {
804
- total: actions.length,
805
- applied,
806
- skipped,
807
- failed,
808
- },
809
- };
810
-
811
- if (opts.json) {
812
- process.stdout.write(JSON.stringify(report, null, 2) + "\n");
813
- } else {
814
- for (const action of actions) {
815
- const icon = action.applied
816
- ? theme.success(icons.success)
817
- : action.error
818
- ? theme.error(icons.error)
819
- : theme.warning(icons.warning);
820
- const status = action.applied
821
- ? "applied"
822
- : action.error
823
- ? "failed"
824
- : dryRun
825
- ? "would apply"
826
- : "skipped";
827
- process.stdout.write(`${icon} [${status}] ${action.description}\n`);
828
- if (action.error) {
829
- process.stdout.write(
830
- ` ${theme.error(icons.error)} ${action.error}\n`
831
- );
832
- }
833
- }
834
-
835
- process.stdout.write(
836
- `\n${theme.heading("Summary:")} ${report.summary.total} issues found — ` +
837
- `${report.summary.applied} applied, ${report.summary.skipped} skipped, ${report.summary.failed} failed\n`
838
- );
839
- }
840
-
841
- if (report.summary.failed > 0) {
842
- process.exitCode = ExitCode.ERROR;
843
- }
844
- } catch (err) {
845
- s.stop("Fix scan failed");
846
- const message = err instanceof Error ? err.message : String(err);
847
- formatError(message, opts);
848
- process.exitCode = ExitCode.ERROR;
849
- }
850
- }
851
-
852
- // ---------------------------------------------------------------------------
853
- // Subcommand: check submodule-gitignore
854
- // ---------------------------------------------------------------------------
855
-
856
- /**
857
- * Result of the submodule gitignore check.
858
- */
859
- interface SubmoduleGitignoreResult {
860
- valid: boolean;
861
- issues: string[];
862
- fixed: string[];
863
- workersChecked: number;
864
- }
865
-
866
- /**
867
- * Check and fix submodule .gitignore files for all workers.
868
- *
869
- * - Ensures each worker has a proper .gitignore
870
- * - Verifies critical entries exist (wrangler.jsonc, .dev.vars, etc.)
871
- * - Untracks wrangler.jsonc if it's mistakenly tracked by git
872
- */
873
- async function checkSubmoduleGitignore(
874
- opts: FormatOptions
875
- ): Promise<SubmoduleGitignoreResult> {
876
- const workerDirs = await getWorkerDirs();
877
- const issues: string[] = [];
878
- const fixed: string[] = [];
879
-
880
- for (const workerDir of workerDirs) {
881
- const workerName = path.basename(workerDir);
882
- const gitignorePath = path.join(workerDir, ".gitignore");
883
-
884
- // 1. Check if .gitignore exists
885
- const gitignoreFile = Bun.file(gitignorePath);
886
- if (!(await gitignoreFile.exists())) {
887
- // Create standard .gitignore
888
- await Bun.write(gitignorePath, WORKER_GITIGNORE_TEMPLATE);
889
- fixed.push(`${workerName}: created .gitignore`);
890
- continue;
891
- }
892
-
893
- // 2. Read and check critical entries
894
- const content = await gitignoreFile.text();
895
- const lines = content.split("\n").map((l) => l.trim());
896
-
897
- for (const entry of CRITICAL_GITIGNORE_ENTRIES) {
898
- if (!lines.includes(entry)) {
899
- issues.push(`${workerName}: missing "${entry}" in .gitignore`);
900
- }
901
- }
902
-
903
- // 3. Check if wrangler.jsonc is tracked by git (it shouldn't be)
904
- const wranglerPath = path.join(workerDir, "wrangler.jsonc");
905
- const wranglerFile = Bun.file(wranglerPath);
906
- if (await wranglerFile.exists()) {
907
- try {
908
- const tracked = await isGitTracked(workerDir, "wrangler.jsonc");
909
- if (tracked) {
910
- await gitUntrackFile(workerDir, "wrangler.jsonc");
911
- fixed.push(
912
- `${workerName}: removed wrangler.jsonc from git tracking (git rm --cached)`
913
- );
914
- }
915
- } catch {
916
- // git ls-files may fail if not in a git repo or other issues
917
- // Skip this check gracefully
918
- }
919
- }
920
- }
921
-
922
- return {
923
- valid: issues.length === 0,
924
- issues,
925
- fixed,
926
- workersChecked: workerDirs.length,
927
- };
928
- }
929
-
930
- /**
931
- * Handle the `hoox check submodule-gitignore` subcommand.
932
- */
933
- async function handleSubmoduleGitignore(opts: FormatOptions): Promise<void> {
934
- const s = spinner();
935
-
936
- try {
937
- s.start("Checking submodule .gitignore files...");
938
- const result = await checkSubmoduleGitignore(opts);
939
- s.stop("Gitignore check complete");
940
-
941
- if (opts.json) {
942
- process.stdout.write(
943
- JSON.stringify(
944
- {
945
- valid: result.valid,
946
- issues: result.issues,
947
- fixed: result.fixed,
948
- workersChecked: result.workersChecked,
949
- },
950
- null,
951
- 2
952
- ) + "\n"
953
- );
954
- } else {
955
- if (result.issues.length > 0) {
956
- process.stdout.write(`\n${theme.error(icons.error)} Issues found:\n`);
957
- for (const issue of result.issues) {
958
- process.stdout.write(` ${theme.error(icons.error)} ${issue}\n`);
959
- }
960
- }
961
-
962
- if (result.fixed.length > 0) {
963
- process.stdout.write(`\n${theme.success(icons.success)} Fixed:\n`);
964
- for (const fix of result.fixed) {
965
- process.stdout.write(` ${theme.success(icons.success)} ${fix}\n`);
966
- }
967
- }
968
-
969
- if (result.issues.length === 0 && result.fixed.length === 0) {
970
- process.stdout.write(
971
- `${theme.success(icons.success)} All workers have valid .gitignore files\n`
972
- );
973
- }
974
-
975
- process.stdout.write(
976
- `\n${theme.dim(`Checked ${result.workersChecked} worker(s)`)}\n`
977
- );
978
- }
979
-
980
- if (!result.valid) {
981
- process.exitCode = ExitCode.ERROR;
982
- }
983
- } catch (err) {
984
- s.stop("Gitignore check failed");
985
- const message = err instanceof Error ? err.message : String(err);
986
- formatError(message, opts);
987
- process.exitCode = ExitCode.ERROR;
988
- }
989
- }
990
-
991
- // ---------------------------------------------------------------------------
992
- // Command registration
993
- // ---------------------------------------------------------------------------
994
-
995
- /**
996
- * Register the `hoox check` command group with subcommands:
997
- * setup, health, fix, submodule-gitignore.
998
- */
999
- export function registerCheckCommand(program: Command): void {
1000
- const checkCmd = program
1001
- .command("check")
1002
- .summary("Validate, health-check, and repair your Hoox setup")
1003
- .description(
1004
- `Validate your Hoox setup and check for common issues.
1005
-
1006
- CHECK CATEGORIES:
1007
- Config - wrangler.jsonc structure, global settings, worker paths
1008
- Infrastructure - D1 databases, KV namespaces, R2 buckets, Queues
1009
- Secrets - Local and remote secret configuration
1010
- Database - Database existence and table schema
1011
-
1012
- EXAMPLES:
1013
- hoox check setup Full system validation
1014
- hoox check health Check worker connectivity
1015
- hoox check fix Auto-repair common issues
1016
- hoox check submodule-gitignore Fix worker .gitignore files
1017
- hoox check sg Alias for submodule-gitignore`
1018
- );
1019
-
1020
- // -- check setup -----------------------------------------------------------
1021
- checkCmd
1022
- .command("setup")
1023
- .summary("Full system validation")
1024
- .description(
1025
- `Run a comprehensive validation of your entire Hoox setup.
1026
-
1027
- Checks performed:
1028
- 1. Config - wrangler.jsonc validation, global settings, worker paths
1029
- 2. Infrastructure - D1 databases, KV namespaces, R2 buckets, Queues
1030
- 3. Secrets - Local (.dev.vars) and remote (Cloudflare) secrets
1031
- 4. Database - Database existence and required tables
1032
-
1033
- OUTPUT:
1034
- Returns a detailed report with pass/fail status for each check.
1035
- Use --json for machine-readable output.
1036
-
1037
- EXAMPLES:
1038
- hoox check setup
1039
- hoox check setup --json`
1040
- )
1041
- .action(async (_, cmd: Command) => {
1042
- const rootCmd = cmd.parent?.parent as Command | undefined;
1043
- const opts: FormatOptions = {
1044
- json: Boolean(rootCmd?.optsWithGlobals()?.json),
1045
- quiet: Boolean(rootCmd?.optsWithGlobals()?.quiet),
1046
- };
1047
- await handleSetup(opts);
1048
- });
1049
-
1050
- // -- check health ----------------------------------------------------------
1051
- checkCmd
1052
- .command("health")
1053
- .summary("Check worker connectivity and responsiveness")
1054
- .description(
1055
- `Run health checks on all enabled workers to verify they are running and responsive.
1056
-
1057
- Each worker is probed to verify:
1058
- - Worker is deployed to Cloudflare
1059
- - Worker is responding to requests
1060
- - No critical errors in recent logs
1061
-
1062
- OPTIONS:
1063
- --fix Attempt automatic repair for detected issues (informational)
1064
-
1065
- OUTPUT:
1066
- Returns a table showing each worker's status: healthy, degraded, or down.
1067
-
1068
- EXAMPLES:
1069
- hoox check health
1070
- hoox check health --json
1071
- hoox check health --fix`
1072
- )
1073
- .option("--fix", "Attempt automatic repair for detected issues")
1074
- .action(async (options: { fix?: boolean }, cmd: Command) => {
1075
- const rootCmd = cmd.parent?.parent as Command | undefined;
1076
- const opts: FormatOptions = {
1077
- json: Boolean(rootCmd?.optsWithGlobals()?.json),
1078
- quiet: Boolean(rootCmd?.optsWithGlobals()?.quiet),
1079
- };
1080
- await handleHealth(opts, Boolean(options.fix));
1081
- });
1082
-
1083
- // -- check fix -------------------------------------------------------------
1084
- checkCmd
1085
- .command("fix")
1086
- .summary("Auto-repair common issues")
1087
- .description(
1088
- `Automatically repair known common issues in your Hoox setup.
1089
-
1090
- Repairs performed:
1091
- 1. Missing .dev.vars files - Creates placeholder files for local development
1092
- 2. Missing nodejs_compat flag - Adds compatibility_flags to wrangler.jsonc
1093
- 3. Missing name field - Ensures each worker has a name in wrangler.jsonc
1094
-
1095
- OPTIONS:
1096
- --dry-run Preview changes without applying them
1097
-
1098
- EXAMPLES:
1099
- hoox check fix Apply fixes automatically
1100
- hoox check fix --dry-run Preview what would be fixed`
1101
- )
1102
- .option("--dry-run", "Preview changes without applying them")
1103
- .action(async (options: { dryRun?: boolean }, cmd: Command) => {
1104
- const rootCmd = cmd.parent?.parent as Command | undefined;
1105
- const opts: FormatOptions = {
1106
- json: Boolean(rootCmd?.optsWithGlobals()?.json),
1107
- quiet: Boolean(rootCmd?.optsWithGlobals()?.quiet),
1108
- };
1109
- await handleFix(opts, Boolean(options.dryRun));
1110
- });
1111
-
1112
- // -- check prerequisites --------------------------------------------------
1113
- registerPrerequisitesCommand(checkCmd);
1114
-
1115
- // -- check submodule-gitignore ---------------------------------------------
1116
- checkCmd
1117
- .command("submodule-gitignore")
1118
- .alias("sg")
1119
- .summary("Validate and fix worker submodule .gitignore files")
1120
- .description(
1121
- `Validate and fix .gitignore files in worker submodules.
1122
-
1123
- Ensures each worker has a proper .gitignore that excludes:
1124
- - .dev.vars (local secrets)
1125
- - wrangler.jsonc (worker config)
1126
- - .wrangler/ (wrangler cache)
1127
- - node_modules/ (dependencies)
1128
- - IDE and OS files
1129
-
1130
- Also removes wrangler.jsonc from git tracking if mistakenly tracked.
1131
-
1132
- EXAMPLES:
1133
- hoox check submodule-gitignore
1134
- hoox check sg # alias`
1135
- )
1136
- .action(async (_, cmd: Command) => {
1137
- const rootCmd = cmd.parent?.parent as Command | undefined;
1138
- const opts: FormatOptions = {
1139
- json: Boolean(rootCmd?.optsWithGlobals()?.json),
1140
- quiet: Boolean(rootCmd?.optsWithGlobals()?.quiet),
1141
- };
1142
- await handleSubmoduleGitignore(opts);
1143
- });
1144
- }