@jango-blockchained/hoox-cli 0.5.2 → 0.5.6

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