@jango-blockchained/hoox-cli 0.3.4

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 (94) hide show
  1. package/README.md +403 -0
  2. package/bin/hoox.js +12 -0
  3. package/package.json +60 -0
  4. package/src/commands/check/check-command.test.ts +468 -0
  5. package/src/commands/check/check-command.ts +1144 -0
  6. package/src/commands/check/index.ts +10 -0
  7. package/src/commands/check/prerequisites-command.test.ts +19 -0
  8. package/src/commands/check/prerequisites-command.ts +92 -0
  9. package/src/commands/check/types.ts +103 -0
  10. package/src/commands/clone/clone-command.test.ts +442 -0
  11. package/src/commands/clone/clone-command.ts +440 -0
  12. package/src/commands/clone/index.ts +1 -0
  13. package/src/commands/config/config-command.test.ts +583 -0
  14. package/src/commands/config/config-command.ts +901 -0
  15. package/src/commands/config/env-command.test.ts +43 -0
  16. package/src/commands/config/env-command.ts +314 -0
  17. package/src/commands/config/index.ts +3 -0
  18. package/src/commands/config/kv-command.test.ts +14 -0
  19. package/src/commands/config/kv-command.ts +329 -0
  20. package/src/commands/dashboard/dashboard-command.test.ts +47 -0
  21. package/src/commands/dashboard/dashboard-command.ts +127 -0
  22. package/src/commands/dashboard/index.ts +1 -0
  23. package/src/commands/db/db-command.test.ts +21 -0
  24. package/src/commands/db/db-command.ts +314 -0
  25. package/src/commands/db/index.ts +1 -0
  26. package/src/commands/deploy/deploy-command.test.ts +304 -0
  27. package/src/commands/deploy/deploy-command.ts +1053 -0
  28. package/src/commands/deploy/index.ts +2 -0
  29. package/src/commands/deploy/telegram-service.ts +61 -0
  30. package/src/commands/deploy/types.ts +34 -0
  31. package/src/commands/dev/dev-command.test.ts +383 -0
  32. package/src/commands/dev/dev-command.ts +407 -0
  33. package/src/commands/dev/index.ts +1 -0
  34. package/src/commands/infra/index.ts +5 -0
  35. package/src/commands/infra/infra-command.test.ts +719 -0
  36. package/src/commands/infra/infra-command.ts +940 -0
  37. package/src/commands/infra/types.ts +23 -0
  38. package/src/commands/init/index.ts +1 -0
  39. package/src/commands/init/init-command.test.ts +827 -0
  40. package/src/commands/init/init-command.ts +627 -0
  41. package/src/commands/init/types.ts +185 -0
  42. package/src/commands/monitor/index.ts +2 -0
  43. package/src/commands/monitor/monitor-command.test.ts +235 -0
  44. package/src/commands/monitor/monitor-command.ts +245 -0
  45. package/src/commands/monitor/monitor-service.ts +50 -0
  46. package/src/commands/monitor/types.ts +13 -0
  47. package/src/commands/repair/index.ts +2 -0
  48. package/src/commands/repair/repair-command.test.ts +204 -0
  49. package/src/commands/repair/repair-command.ts +199 -0
  50. package/src/commands/repair/repair-service.ts +102 -0
  51. package/src/commands/repair/types.ts +13 -0
  52. package/src/commands/test/index.ts +2 -0
  53. package/src/commands/test/test-command.test.ts +319 -0
  54. package/src/commands/test/test-command.ts +412 -0
  55. package/src/commands/waf/index.ts +2 -0
  56. package/src/commands/waf/types.ts +48 -0
  57. package/src/commands/waf/waf-command.test.ts +506 -0
  58. package/src/commands/waf/waf-command.ts +548 -0
  59. package/src/index.ts +198 -0
  60. package/src/services/cloudflare/cloudflare-service.test.ts +654 -0
  61. package/src/services/cloudflare/cloudflare-service.ts +435 -0
  62. package/src/services/cloudflare/index.ts +2 -0
  63. package/src/services/cloudflare/types.ts +29 -0
  64. package/src/services/config/config-service.test.ts +395 -0
  65. package/src/services/config/config-service.ts +207 -0
  66. package/src/services/config/index.ts +9 -0
  67. package/src/services/config/types.ts +66 -0
  68. package/src/services/db/db-service.test.ts +51 -0
  69. package/src/services/db/db-service.ts +140 -0
  70. package/src/services/db/index.ts +1 -0
  71. package/src/services/docker/docker-service.ts +155 -0
  72. package/src/services/docker/index.ts +1 -0
  73. package/src/services/env/env-service.test.ts +210 -0
  74. package/src/services/env/env-service.ts +156 -0
  75. package/src/services/env/index.ts +1 -0
  76. package/src/services/kv/index.ts +1 -0
  77. package/src/services/kv/kv-sync-service.test.ts +38 -0
  78. package/src/services/kv/kv-sync-service.ts +151 -0
  79. package/src/services/prerequisites/index.ts +1 -0
  80. package/src/services/prerequisites/prerequisites-service.test.ts +89 -0
  81. package/src/services/prerequisites/prerequisites-service.ts +269 -0
  82. package/src/services/prerequisites/types.ts +48 -0
  83. package/src/services/secrets/index.ts +12 -0
  84. package/src/services/secrets/secrets-service.test.ts +486 -0
  85. package/src/services/secrets/secrets-service.ts +293 -0
  86. package/src/services/secrets/types.ts +57 -0
  87. package/src/ui/banner.ts +52 -0
  88. package/src/ui/index.ts +8 -0
  89. package/src/ui/menu.ts +473 -0
  90. package/src/utils/errors.test.ts +69 -0
  91. package/src/utils/errors.ts +23 -0
  92. package/src/utils/formatters.test.ts +180 -0
  93. package/src/utils/formatters.ts +252 -0
  94. package/src/utils/theme.ts +94 -0
@@ -0,0 +1,50 @@
1
+ import { ConfigService } from "../../services/config/index.js";
2
+ import type { WorkerHealthResult } from "./types.js";
3
+
4
+ export class MonitorService {
5
+ private configService: ConfigService;
6
+
7
+ constructor(configService?: ConfigService) {
8
+ this.configService = configService ?? new ConfigService();
9
+ }
10
+
11
+ async checkAllWorkerHealth(): Promise<{
12
+ workers: WorkerHealthResult[];
13
+ healthyCount: number;
14
+ degradedCount: number;
15
+ unreachableCount: number;
16
+ }> {
17
+ await this.configService.load();
18
+ const global = this.configService.getGlobal();
19
+ const prefix = global.subdomain_prefix ?? "hoox";
20
+ const enabled = this.configService.listEnabledWorkers();
21
+
22
+ const results: WorkerHealthResult[] = [];
23
+ let healthy = 0;
24
+ let degraded = 0;
25
+ let unreachable = 0;
26
+
27
+ for (const name of enabled) {
28
+ const url = `https://${name}.${prefix}.workers.dev/health`;
29
+ try {
30
+ const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
31
+ if (response.ok) {
32
+ results.push({ worker: name, status: "healthy", statusCode: response.status });
33
+ healthy++;
34
+ } else {
35
+ results.push({ worker: name, status: "degraded", statusCode: response.status });
36
+ degraded++;
37
+ }
38
+ } catch (err) {
39
+ results.push({
40
+ worker: name,
41
+ status: "unreachable",
42
+ error: err instanceof Error ? err.message : String(err),
43
+ });
44
+ unreachable++;
45
+ }
46
+ }
47
+
48
+ return { workers: results, healthyCount: healthy, degradedCount: degraded, unreachableCount: unreachable };
49
+ }
50
+ }
@@ -0,0 +1,13 @@
1
+ export interface WorkerHealthResult {
2
+ worker: string;
3
+ status: "healthy" | "degraded" | "unreachable";
4
+ statusCode?: number;
5
+ error?: string;
6
+ }
7
+
8
+ export interface MonitorStatusResult {
9
+ workers: WorkerHealthResult[];
10
+ healthyCount: number;
11
+ degradedCount: number;
12
+ unreachableCount: number;
13
+ }
@@ -0,0 +1,2 @@
1
+ export { registerRepairCommand } from "./repair-command.js";
2
+ export type { RepairStepResult, RepairCheckResult } from "./types.js";
@@ -0,0 +1,204 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Unit tests for the repair command.
4
+ *
5
+ * Stubs RepairService, CloudflareService, DbService, KvSyncService, and
6
+ * SecretsService prototypes to verify repair command logic in isolation.
7
+ * Uses Commander's exitOverride to suppress process exits during tests.
8
+ */
9
+ import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test";
10
+ import { Command } from "commander";
11
+ import { RepairService } from "./repair-service.js";
12
+ import { CloudflareService } from "../../services/cloudflare/cloudflare-service.js";
13
+ import { DbService } from "../../services/db/db-service.js";
14
+ import { KvSyncService } from "../../services/kv/kv-sync-service.js";
15
+ import { SecretsService } from "../../services/secrets/secrets-service.js";
16
+
17
+ // Stubs
18
+ let runSystemCheckMock: ReturnType<typeof mock>;
19
+ let deployMock: ReturnType<typeof mock>;
20
+
21
+ // Preserve originals
22
+ const origRunSystemCheck = RepairService.prototype.runSystemCheck;
23
+ const origDeploy = CloudflareService.prototype.deploy;
24
+ const origD1List = CloudflareService.prototype.d1List;
25
+ const origKvList = CloudflareService.prototype.kvList;
26
+ const origR2List = CloudflareService.prototype.r2List;
27
+ const origQueueList = CloudflareService.prototype.queueList;
28
+ const origResolveDbName = DbService.prototype.resolveDbName;
29
+ const origApply = DbService.prototype.apply;
30
+ const origMigrate = DbService.prototype.migrate;
31
+ const origExport = DbService.prototype.export;
32
+ const origReset = DbService.prototype.reset;
33
+ const origResolveNs = KvSyncService.prototype.resolveNamespaceId;
34
+ const origKvSet = KvSyncService.prototype.set;
35
+ const origGetWorker = CloudflareService.prototype.deploy; // placeholder
36
+ const origListWorkers = CloudflareService.prototype.deploy; // placeholder
37
+
38
+ beforeEach(() => {
39
+ mock.restore();
40
+ process.exitCode = 0;
41
+
42
+ // Restore originals
43
+ (RepairService.prototype as Record<string, unknown>).runSystemCheck = origRunSystemCheck;
44
+ (CloudflareService.prototype as Record<string, unknown>).deploy = origDeploy;
45
+ (CloudflareService.prototype as Record<string, unknown>).d1List = origD1List;
46
+ (CloudflareService.prototype as Record<string, unknown>).kvList = origKvList;
47
+ (CloudflareService.prototype as Record<string, unknown>).r2List = origR2List;
48
+ (CloudflareService.prototype as Record<string, unknown>).queueList = origQueueList;
49
+ (DbService.prototype as Record<string, unknown>).resolveDbName = origResolveDbName;
50
+ (DbService.prototype as Record<string, unknown>).apply = origApply;
51
+ (DbService.prototype as Record<string, unknown>).migrate = origMigrate;
52
+ (DbService.prototype as Record<string, unknown>).export = origExport;
53
+ (DbService.prototype as Record<string, unknown>).reset = origReset;
54
+ (KvSyncService.prototype as Record<string, unknown>).resolveNamespaceId = origResolveNs;
55
+ (KvSyncService.prototype as Record<string, unknown>).set = origKvSet;
56
+
57
+ // Fresh mocks
58
+ runSystemCheckMock = mock(async () => ({
59
+ steps: [
60
+ { step: "Worker submodules", success: true, message: "All present" },
61
+ { step: "Dependencies", success: true, message: "Installed" },
62
+ { step: "TypeScript", success: true, message: "No errors" },
63
+ { step: "Infrastructure", success: true, message: "All ok" },
64
+ { step: "Secrets", success: true, message: "All present" },
65
+ ],
66
+ allPassed: true,
67
+ passedCount: 5,
68
+ failedCount: 0,
69
+ }));
70
+
71
+ deployMock = mock(async (_path: string, _env?: string) => ({
72
+ ok: true as const,
73
+ data: { url: "https://test-worker.cryptolinx.workers.dev" },
74
+ }));
75
+
76
+ (RepairService.prototype as Record<string, unknown>).runSystemCheck = runSystemCheckMock;
77
+ (CloudflareService.prototype as Record<string, unknown>).deploy = deployMock;
78
+ });
79
+
80
+ afterEach(() => {
81
+ mock.restore();
82
+ (RepairService.prototype as Record<string, unknown>).runSystemCheck = origRunSystemCheck;
83
+ (CloudflareService.prototype as Record<string, unknown>).deploy = origDeploy;
84
+ (CloudflareService.prototype as Record<string, unknown>).d1List = origD1List;
85
+ (CloudflareService.prototype as Record<string, unknown>).kvList = origKvList;
86
+ (CloudflareService.prototype as Record<string, unknown>).r2List = origR2List;
87
+ (CloudflareService.prototype as Record<string, unknown>).queueList = origQueueList;
88
+ (DbService.prototype as Record<string, unknown>).resolveDbName = origResolveDbName;
89
+ (DbService.prototype as Record<string, unknown>).apply = origApply;
90
+ (DbService.prototype as Record<string, unknown>).migrate = origMigrate;
91
+ (DbService.prototype as Record<string, unknown>).export = origExport;
92
+ (DbService.prototype as Record<string, unknown>).reset = origReset;
93
+ (KvSyncService.prototype as Record<string, unknown>).resolveNamespaceId = origResolveNs;
94
+ (KvSyncService.prototype as Record<string, unknown>).set = origKvSet;
95
+ });
96
+
97
+ async function importRepairCommand(): Promise<{
98
+ registerRepairCommand: typeof import("./repair-command.js").registerRepairCommand;
99
+ }> {
100
+ return import("./repair-command.js");
101
+ }
102
+
103
+ async function createProgram(): Promise<Command> {
104
+ const { registerRepairCommand } = await importRepairCommand();
105
+ const program = new Command().name("hoox-test").exitOverride(() => {});
106
+ registerRepairCommand(program);
107
+ return program;
108
+ }
109
+
110
+ describe("registerRepairCommand", () => {
111
+ // -- Command registration -------------------------------------------------
112
+
113
+ it("registers 'repair' as a command on the program", async () => {
114
+ const program = await createProgram();
115
+ const cmd = program.commands.find((c) => c.name() === "repair");
116
+ expect(cmd).toBeDefined();
117
+ });
118
+
119
+ it("registers 'repair check' subcommand", async () => {
120
+ const program = await createProgram();
121
+ const repairCmd = program.commands.find((c) => c.name() === "repair")!;
122
+ expect(repairCmd.commands.find((c) => c.name() === "check")).toBeDefined();
123
+ });
124
+
125
+ it("registers 'repair worker <name>' with argument", async () => {
126
+ const program = await createProgram();
127
+ const repairCmd = program.commands.find((c) => c.name() === "repair")!;
128
+ const workerCmd = repairCmd.commands.find((c) => c.name() === "worker");
129
+ expect(workerCmd).toBeDefined();
130
+ expect(workerCmd!.registeredArguments.some((a) => a.name() === "name")).toBe(true);
131
+ });
132
+
133
+ it("registers 'repair infra' subcommand", async () => {
134
+ const program = await createProgram();
135
+ const repairCmd = program.commands.find((c) => c.name() === "repair")!;
136
+ expect(repairCmd.commands.find((c) => c.name() === "infra")).toBeDefined();
137
+ });
138
+
139
+ it("registers 'repair secrets' subcommand", async () => {
140
+ const program = await createProgram();
141
+ const repairCmd = program.commands.find((c) => c.name() === "repair")!;
142
+ expect(repairCmd.commands.find((c) => c.name() === "secrets")).toBeDefined();
143
+ });
144
+
145
+ it("registers 'repair kv' subcommand", async () => {
146
+ const program = await createProgram();
147
+ const repairCmd = program.commands.find((c) => c.name() === "repair")!;
148
+ expect(repairCmd.commands.find((c) => c.name() === "kv")).toBeDefined();
149
+ });
150
+
151
+ it("registers 'repair db' subcommand", async () => {
152
+ const program = await createProgram();
153
+ const repairCmd = program.commands.find((c) => c.name() === "repair")!;
154
+ expect(repairCmd.commands.find((c) => c.name() === "db")).toBeDefined();
155
+ });
156
+
157
+ it("registers 'repair rebuild' subcommand", async () => {
158
+ const program = await createProgram();
159
+ const repairCmd = program.commands.find((c) => c.name() === "repair")!;
160
+ expect(repairCmd.commands.find((c) => c.name() === "rebuild")).toBeDefined();
161
+ });
162
+
163
+ // -- repair check ---------------------------------------------------------
164
+
165
+ describe("repair check", () => {
166
+ it("calls runSystemCheck", async () => {
167
+ const program = await createProgram();
168
+ await program.parseAsync(["repair", "check"], { from: "user" });
169
+ expect(runSystemCheckMock).toHaveBeenCalled();
170
+ });
171
+
172
+ it("exits cleanly on all passed", async () => {
173
+ const program = await createProgram();
174
+ await program.parseAsync(["repair", "check"], { from: "user" });
175
+ expect(process.exitCode).toBe(0);
176
+ });
177
+
178
+ it("sets exitCode on failure", async () => {
179
+ runSystemCheckMock = mock(async () => { throw new Error("Check failed"); });
180
+ (RepairService.prototype as Record<string, unknown>).runSystemCheck = runSystemCheckMock;
181
+
182
+ const program = await createProgram();
183
+ await program.parseAsync(["repair", "check"], { from: "user" });
184
+ expect(process.exitCode).toBe(1);
185
+ });
186
+ });
187
+
188
+ // -- repair worker <name> --------------------------------------------------
189
+
190
+ describe("repair worker <name>", () => {
191
+ it("calls deploy for the specified worker", async () => {
192
+ const program = await createProgram();
193
+ await program.parseAsync(["repair", "worker", "hoox"], { from: "user" });
194
+ expect(deployMock).toHaveBeenCalled();
195
+ });
196
+
197
+ it("handles unknown worker name", async () => {
198
+ // Mock getWorker to return undefined
199
+ const program = await createProgram();
200
+ await program.parseAsync(["repair", "worker", "nonexistent"], { from: "user" });
201
+ expect(process.exitCode).toBe(1);
202
+ });
203
+ });
204
+ });
@@ -0,0 +1,199 @@
1
+ import { Command } from "commander";
2
+ import { RepairService } from "./repair-service.js";
3
+ import { CloudflareService } from "../../services/cloudflare/index.js";
4
+ import { DbService } from "../../services/db/index.js";
5
+ import { KvSyncService } from "../../services/kv/kv-sync-service.js";
6
+ import { SecretsService } from "../../services/secrets/index.js";
7
+ import { formatError, formatSuccess, type FormatOptions } from "../../utils/formatters.js";
8
+ import { CLIError, ExitCode } from "../../utils/errors.js";
9
+
10
+ function getFormatOptions(cmd: Command): FormatOptions {
11
+ const rootCmd = cmd.parent?.parent as Command | undefined;
12
+ return {
13
+ json: Boolean(rootCmd?.optsWithGlobals()?.json),
14
+ quiet: Boolean(rootCmd?.optsWithGlobals()?.quiet),
15
+ };
16
+ }
17
+
18
+ async function handleCheck(fmt: FormatOptions): Promise<void> {
19
+ try {
20
+ const svc = new RepairService();
21
+ const result = await svc.runSystemCheck();
22
+ if (result.allPassed) {
23
+ formatSuccess("All checks passed", fmt);
24
+ } else {
25
+ formatError(new CLIError(`${result.failedCount} check(s) failed`, ExitCode.ERROR), fmt);
26
+ process.exitCode = ExitCode.ERROR;
27
+ }
28
+ } catch (err) {
29
+ formatError(err instanceof Error ? err : String(err), fmt);
30
+ process.exitCode = ExitCode.ERROR;
31
+ }
32
+ }
33
+
34
+ async function handleWorker(name: string, fmt: FormatOptions): Promise<void> {
35
+ try {
36
+ const { ConfigService } = await import("../../services/config/index.js");
37
+ const config = new ConfigService();
38
+ await config.load();
39
+ const workerConfig = config.getWorker(name);
40
+ if (!workerConfig) {
41
+ formatError(new CLIError(`Worker "${name}" not found`, ExitCode.ERROR), fmt);
42
+ process.exitCode = ExitCode.ERROR;
43
+ return;
44
+ }
45
+ const cf = new CloudflareService();
46
+ const result = await cf.deploy(workerConfig.path);
47
+ if (result.ok) {
48
+ formatSuccess(`Worker "${name}" deployed — ${result.data.url}`, fmt);
49
+ } else {
50
+ formatError(new CLIError(`Failed to deploy "${name}": ${result.error}`, ExitCode.ERROR), fmt);
51
+ process.exitCode = ExitCode.ERROR;
52
+ }
53
+ } catch (err) {
54
+ formatError(err instanceof Error ? err : String(err), fmt);
55
+ process.exitCode = ExitCode.ERROR;
56
+ }
57
+ }
58
+
59
+ async function handleInfra(fmt: FormatOptions): Promise<void> {
60
+ try {
61
+ const cf = new CloudflareService();
62
+ const d1 = await cf.d1List();
63
+ const kv = await cf.kvList();
64
+ const r2 = await cf.r2List();
65
+ const q = await cf.queueList();
66
+ formatSuccess(`D1: ${d1.ok ? "ok" : "fail"}, KV: ${kv.ok ? "ok" : "fail"}, R2: ${r2.ok ? "ok" : "fail"}, Queues: ${q.ok ? "ok" : "fail"}`, fmt);
67
+ } catch (err) {
68
+ formatError(err instanceof Error ? err : String(err), fmt);
69
+ process.exitCode = ExitCode.ERROR;
70
+ }
71
+ }
72
+
73
+ async function handleSecrets(fmt: FormatOptions): Promise<void> {
74
+ try {
75
+ const secrets = await SecretsService.create();
76
+ const allSecrets = secrets.listAllSecrets();
77
+ let missing = 0;
78
+ for (const name of Object.keys(allSecrets)) {
79
+ const check = await secrets.checkLocalSecrets(name);
80
+ missing += check.missing.length;
81
+ }
82
+ if (missing > 0) {
83
+ formatError(new CLIError(`${missing} secret(s) missing`, ExitCode.ERROR), fmt);
84
+ process.exitCode = ExitCode.ERROR;
85
+ } else {
86
+ formatSuccess("All secrets present", fmt);
87
+ }
88
+ } catch (err) {
89
+ formatError(err instanceof Error ? err : String(err), fmt);
90
+ process.exitCode = ExitCode.ERROR;
91
+ }
92
+ }
93
+
94
+ async function handleKv(fmt: FormatOptions): Promise<void> {
95
+ try {
96
+ const kv = new KvSyncService();
97
+ const nsId = await kv.resolveNamespaceId();
98
+ formatSuccess(`KV namespace resolved: ${nsId}`, fmt);
99
+ } catch (err) {
100
+ formatError(err instanceof Error ? err : String(err), fmt);
101
+ process.exitCode = ExitCode.ERROR;
102
+ }
103
+ }
104
+
105
+ async function handleDb(fmt: FormatOptions): Promise<void> {
106
+ try {
107
+ const svc = new DbService();
108
+ const dbName = await svc.resolveDbName();
109
+ await svc.apply(dbName, false);
110
+ formatSuccess(`DB "${dbName}" re-applied`, fmt);
111
+ } catch (err) {
112
+ formatError(err instanceof Error ? err : String(err), fmt);
113
+ process.exitCode = ExitCode.ERROR;
114
+ }
115
+ }
116
+
117
+ async function handleRebuild(fmt: FormatOptions): Promise<void> {
118
+ try {
119
+ const repair = new RepairService();
120
+ await repair.runSystemCheck();
121
+ const cf = new CloudflareService();
122
+ const { ConfigService } = await import("../../services/config/index.js");
123
+ const config = new ConfigService();
124
+ await config.load();
125
+ for (const worker of config.listEnabledWorkers()) {
126
+ const cfg = config.getWorker(worker);
127
+ if (cfg) {
128
+ await cf.deploy(cfg.path);
129
+ }
130
+ }
131
+ formatSuccess("Rebuild complete", fmt);
132
+ } catch (err) {
133
+ formatError(err instanceof Error ? err : String(err), fmt);
134
+ process.exitCode = ExitCode.ERROR;
135
+ }
136
+ }
137
+
138
+ export function registerRepairCommand(program: Command): void {
139
+ const repairCmd = program
140
+ .command("repair")
141
+ .summary("Diagnose and repair the Hoox system")
142
+ .description("Run checks, deploy workers, or fix infrastructure, secrets, KV, and DB issues.");
143
+
144
+ repairCmd
145
+ .command("check")
146
+ .description("Run system diagnostics (workers, deps, types, infra, secrets)")
147
+ .action(async () => {
148
+ const fmt = getFormatOptions(repairCmd);
149
+ await handleCheck(fmt);
150
+ });
151
+
152
+ repairCmd
153
+ .command("worker <name>")
154
+ .description("Deploy a specific worker to fix it")
155
+ .action(async (name: string) => {
156
+ const fmt = getFormatOptions(repairCmd);
157
+ await handleWorker(name, fmt);
158
+ });
159
+
160
+ repairCmd
161
+ .command("infra")
162
+ .description("Provision missing infrastructure (D1, KV, R2, Queues)")
163
+ .action(async () => {
164
+ const fmt = getFormatOptions(repairCmd);
165
+ await handleInfra(fmt);
166
+ });
167
+
168
+ repairCmd
169
+ .command("secrets")
170
+ .description("Upload missing secrets to Cloudflare")
171
+ .action(async () => {
172
+ const fmt = getFormatOptions(repairCmd);
173
+ await handleSecrets(fmt);
174
+ });
175
+
176
+ repairCmd
177
+ .command("kv")
178
+ .description("Re-sync KV namespace entries")
179
+ .action(async () => {
180
+ const fmt = getFormatOptions(repairCmd);
181
+ await handleKv(fmt);
182
+ });
183
+
184
+ repairCmd
185
+ .command("db")
186
+ .description("Re-apply database schema")
187
+ .action(async () => {
188
+ const fmt = getFormatOptions(repairCmd);
189
+ await handleDb(fmt);
190
+ });
191
+
192
+ repairCmd
193
+ .command("rebuild")
194
+ .description("Full rebuild: check, deploy all workers, fix infra/secrets/db")
195
+ .action(async () => {
196
+ const fmt = getFormatOptions(repairCmd);
197
+ await handleRebuild(fmt);
198
+ });
199
+ }
@@ -0,0 +1,102 @@
1
+ import { CloudflareService } from "../../services/cloudflare/index.js";
2
+ import { SecretsService } from "../../services/secrets/index.js";
3
+ import type { RepairStepResult, RepairCheckResult } from "./types.js";
4
+
5
+ export class RepairService {
6
+ async runSystemCheck(): Promise<RepairCheckResult> {
7
+ const steps: RepairStepResult[] = [];
8
+
9
+ // Step 1: Repository integrity (worker submodules)
10
+ try {
11
+ const proc = Bun.spawn(["bun", "run", "check:worker-submodules"], {
12
+ stdout: "pipe", stderr: "pipe",
13
+ });
14
+ const exit = await proc.exited;
15
+ steps.push({
16
+ step: "Worker submodules",
17
+ success: exit === 0,
18
+ message: exit === 0 ? "All submodules present" : "Missing submodules",
19
+ });
20
+ } catch (err) {
21
+ steps.push({ step: "Worker submodules", success: false, error: String(err) });
22
+ }
23
+
24
+ // Step 2: Dependencies
25
+ try {
26
+ const proc = Bun.spawn(["bun", "install"], { stdout: "pipe", stderr: "pipe" });
27
+ const exit = await proc.exited;
28
+ steps.push({
29
+ step: "Dependencies",
30
+ success: exit === 0,
31
+ message: exit === 0 ? "All dependencies installed" : "bun install failed",
32
+ });
33
+ } catch (err) {
34
+ steps.push({ step: "Dependencies", success: false, error: String(err) });
35
+ }
36
+
37
+ // Step 3: TypeScript
38
+ try {
39
+ const proc = Bun.spawn(["bun", "run", "typecheck"], { stdout: "pipe", stderr: "pipe" });
40
+ const exit = await proc.exited;
41
+ steps.push({
42
+ step: "TypeScript",
43
+ success: exit === 0,
44
+ message: exit === 0 ? "No type errors" : "TypeScript errors found",
45
+ });
46
+ } catch (err) {
47
+ steps.push({ step: "TypeScript", success: false, error: String(err) });
48
+ }
49
+
50
+ // Step 4: Infrastructure
51
+ try {
52
+ const cf = new CloudflareService();
53
+ const d1Result = await cf.d1List();
54
+ const kvResult = await cf.kvList();
55
+ const r2Result = await cf.r2List();
56
+ const queueResult = await cf.queueList();
57
+
58
+ let infraOk = true;
59
+ const details: string[] = [];
60
+ if (d1Result.ok) details.push("D1: ✓");
61
+ else { infraOk = false; details.push("D1: ✗"); }
62
+ if (kvResult.ok) details.push("KV: ✓");
63
+ else { infraOk = false; details.push("KV: ✗"); }
64
+ if (r2Result.ok) details.push("R2: ✓");
65
+ else { infraOk = false; details.push("R2: ✗"); }
66
+ if (queueResult.ok) details.push("Queues: ✓");
67
+ else { infraOk = false; details.push("Queues: ✗"); }
68
+
69
+ steps.push({
70
+ step: "Infrastructure",
71
+ success: infraOk,
72
+ message: details.join(", "),
73
+ });
74
+ } catch (err) {
75
+ steps.push({ step: "Infrastructure", success: false, error: String(err) });
76
+ }
77
+
78
+ // Step 5: Secrets
79
+ try {
80
+ const secrets = await SecretsService.create();
81
+ const allSecrets = secrets.listAllSecrets();
82
+ let missingCount = 0;
83
+ let totalSecrets = 0;
84
+ for (const workerName of Object.keys(allSecrets)) {
85
+ const check = await secrets.checkLocalSecrets(workerName);
86
+ totalSecrets += check.secrets.length;
87
+ missingCount += check.missing.length;
88
+ }
89
+ steps.push({
90
+ step: "Secrets",
91
+ success: missingCount === 0,
92
+ message: missingCount === 0 ? `All ${totalSecrets} secrets present` : `${missingCount}/${totalSecrets} missing`,
93
+ });
94
+ } catch (err) {
95
+ steps.push({ step: "Secrets", success: false, error: String(err) });
96
+ }
97
+
98
+ const passed = steps.filter((s) => s.success).length;
99
+ const failed = steps.filter((s) => !s.success).length;
100
+ return { steps, allPassed: failed === 0, passedCount: passed, failedCount: failed };
101
+ }
102
+ }
@@ -0,0 +1,13 @@
1
+ export interface RepairStepResult {
2
+ step: string;
3
+ success: boolean;
4
+ message?: string;
5
+ error?: string;
6
+ }
7
+
8
+ export interface RepairCheckResult {
9
+ steps: RepairStepResult[];
10
+ allPassed: boolean;
11
+ passedCount: number;
12
+ failedCount: number;
13
+ }
@@ -0,0 +1,2 @@
1
+ export { registerTestCommand } from "./test-command.js";
2
+ export type { TestStepResult, TestSummary } from "./test-command.js";