@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.
- package/README.md +403 -0
- package/bin/hoox.js +12 -0
- package/package.json +60 -0
- package/src/commands/check/check-command.test.ts +468 -0
- package/src/commands/check/check-command.ts +1144 -0
- package/src/commands/check/index.ts +10 -0
- package/src/commands/check/prerequisites-command.test.ts +19 -0
- package/src/commands/check/prerequisites-command.ts +92 -0
- package/src/commands/check/types.ts +103 -0
- package/src/commands/clone/clone-command.test.ts +442 -0
- package/src/commands/clone/clone-command.ts +440 -0
- package/src/commands/clone/index.ts +1 -0
- package/src/commands/config/config-command.test.ts +583 -0
- package/src/commands/config/config-command.ts +901 -0
- package/src/commands/config/env-command.test.ts +43 -0
- package/src/commands/config/env-command.ts +314 -0
- package/src/commands/config/index.ts +3 -0
- package/src/commands/config/kv-command.test.ts +14 -0
- package/src/commands/config/kv-command.ts +329 -0
- package/src/commands/dashboard/dashboard-command.test.ts +47 -0
- package/src/commands/dashboard/dashboard-command.ts +127 -0
- package/src/commands/dashboard/index.ts +1 -0
- package/src/commands/db/db-command.test.ts +21 -0
- package/src/commands/db/db-command.ts +314 -0
- package/src/commands/db/index.ts +1 -0
- package/src/commands/deploy/deploy-command.test.ts +304 -0
- package/src/commands/deploy/deploy-command.ts +1053 -0
- package/src/commands/deploy/index.ts +2 -0
- package/src/commands/deploy/telegram-service.ts +61 -0
- package/src/commands/deploy/types.ts +34 -0
- package/src/commands/dev/dev-command.test.ts +383 -0
- package/src/commands/dev/dev-command.ts +407 -0
- package/src/commands/dev/index.ts +1 -0
- package/src/commands/infra/index.ts +5 -0
- package/src/commands/infra/infra-command.test.ts +719 -0
- package/src/commands/infra/infra-command.ts +940 -0
- package/src/commands/infra/types.ts +23 -0
- package/src/commands/init/index.ts +1 -0
- package/src/commands/init/init-command.test.ts +827 -0
- package/src/commands/init/init-command.ts +627 -0
- package/src/commands/init/types.ts +185 -0
- package/src/commands/monitor/index.ts +2 -0
- package/src/commands/monitor/monitor-command.test.ts +235 -0
- package/src/commands/monitor/monitor-command.ts +245 -0
- package/src/commands/monitor/monitor-service.ts +50 -0
- package/src/commands/monitor/types.ts +13 -0
- package/src/commands/repair/index.ts +2 -0
- package/src/commands/repair/repair-command.test.ts +204 -0
- package/src/commands/repair/repair-command.ts +199 -0
- package/src/commands/repair/repair-service.ts +102 -0
- package/src/commands/repair/types.ts +13 -0
- package/src/commands/test/index.ts +2 -0
- package/src/commands/test/test-command.test.ts +319 -0
- package/src/commands/test/test-command.ts +412 -0
- package/src/commands/waf/index.ts +2 -0
- package/src/commands/waf/types.ts +48 -0
- package/src/commands/waf/waf-command.test.ts +506 -0
- package/src/commands/waf/waf-command.ts +548 -0
- package/src/index.ts +198 -0
- package/src/services/cloudflare/cloudflare-service.test.ts +654 -0
- package/src/services/cloudflare/cloudflare-service.ts +435 -0
- package/src/services/cloudflare/index.ts +2 -0
- package/src/services/cloudflare/types.ts +29 -0
- package/src/services/config/config-service.test.ts +395 -0
- package/src/services/config/config-service.ts +207 -0
- package/src/services/config/index.ts +9 -0
- package/src/services/config/types.ts +66 -0
- package/src/services/db/db-service.test.ts +51 -0
- package/src/services/db/db-service.ts +140 -0
- package/src/services/db/index.ts +1 -0
- package/src/services/docker/docker-service.ts +155 -0
- package/src/services/docker/index.ts +1 -0
- package/src/services/env/env-service.test.ts +210 -0
- package/src/services/env/env-service.ts +156 -0
- package/src/services/env/index.ts +1 -0
- package/src/services/kv/index.ts +1 -0
- package/src/services/kv/kv-sync-service.test.ts +38 -0
- package/src/services/kv/kv-sync-service.ts +151 -0
- package/src/services/prerequisites/index.ts +1 -0
- package/src/services/prerequisites/prerequisites-service.test.ts +89 -0
- package/src/services/prerequisites/prerequisites-service.ts +269 -0
- package/src/services/prerequisites/types.ts +48 -0
- package/src/services/secrets/index.ts +12 -0
- package/src/services/secrets/secrets-service.test.ts +486 -0
- package/src/services/secrets/secrets-service.ts +293 -0
- package/src/services/secrets/types.ts +57 -0
- package/src/ui/banner.ts +52 -0
- package/src/ui/index.ts +8 -0
- package/src/ui/menu.ts +473 -0
- package/src/utils/errors.test.ts +69 -0
- package/src/utils/errors.ts +23 -0
- package/src/utils/formatters.test.ts +180 -0
- package/src/utils/formatters.ts +252 -0
- 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,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
|
+
}
|