@jango-blockchained/hoox-cli 0.5.2 → 0.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/hoox.js +3 -4
- package/dist/index.js +32989 -0
- package/package.json +10 -15
- package/src/commands/check/check-command.test.ts +0 -476
- package/src/commands/check/check-command.ts +0 -1087
- package/src/commands/check/index.ts +0 -10
- package/src/commands/check/prerequisites-command.test.ts +0 -19
- package/src/commands/check/prerequisites-command.ts +0 -97
- package/src/commands/check/types.ts +0 -103
- package/src/commands/clone/clone-command.test.ts +0 -440
- package/src/commands/clone/clone-command.ts +0 -431
- package/src/commands/clone/index.ts +0 -1
- package/src/commands/config/config-command.test.ts +0 -583
- package/src/commands/config/config-command.ts +0 -896
- package/src/commands/config/env-command.test.ts +0 -43
- package/src/commands/config/env-command.ts +0 -292
- package/src/commands/config/index.ts +0 -3
- package/src/commands/config/kv-command.test.ts +0 -14
- package/src/commands/config/kv-command.ts +0 -290
- package/src/commands/dashboard/dashboard-command.test.ts +0 -47
- package/src/commands/dashboard/dashboard-command.ts +0 -130
- package/src/commands/dashboard/index.ts +0 -1
- package/src/commands/db/db-command.test.ts +0 -21
- package/src/commands/db/db-command.ts +0 -311
- package/src/commands/db/index.ts +0 -1
- package/src/commands/deploy/deploy-command.test.ts +0 -324
- package/src/commands/deploy/deploy-command.ts +0 -1054
- package/src/commands/deploy/index.ts +0 -2
- package/src/commands/deploy/telegram-service.ts +0 -76
- package/src/commands/deploy/types.ts +0 -34
- package/src/commands/dev/dev-command.test.ts +0 -405
- package/src/commands/dev/dev-command.ts +0 -398
- package/src/commands/dev/index.ts +0 -1
- package/src/commands/disclaimer/disclaimer-command.ts +0 -21
- package/src/commands/disclaimer/index.ts +0 -1
- package/src/commands/infra/index.ts +0 -5
- package/src/commands/infra/infra-command.test.ts +0 -719
- package/src/commands/infra/infra-command.ts +0 -974
- package/src/commands/infra/types.ts +0 -23
- package/src/commands/init/__tests__/cli-provisioner.test.ts +0 -17
- package/src/commands/init/cli-provisioner.ts +0 -71
- package/src/commands/init/index.ts +0 -1
- package/src/commands/init/init-command.test.ts +0 -854
- package/src/commands/init/init-command.ts +0 -597
- package/src/commands/init/types.ts +0 -23
- package/src/commands/logs/index.ts +0 -1
- package/src/commands/logs/logs-command.test.ts +0 -293
- package/src/commands/logs/logs-command.ts +0 -452
- package/src/commands/monitor/index.ts +0 -2
- package/src/commands/monitor/monitor-command.test.ts +0 -271
- package/src/commands/monitor/monitor-command.ts +0 -269
- package/src/commands/monitor/monitor-service.ts +0 -65
- package/src/commands/monitor/types.ts +0 -13
- package/src/commands/repair/index.ts +0 -2
- package/src/commands/repair/repair-command.test.ts +0 -243
- package/src/commands/repair/repair-command.ts +0 -220
- package/src/commands/repair/repair-service.ts +0 -164
- package/src/commands/repair/types.ts +0 -13
- package/src/commands/schema/index.ts +0 -1
- package/src/commands/schema/schema-command.ts +0 -137
- package/src/commands/test/index.ts +0 -2
- package/src/commands/test/test-command.test.ts +0 -318
- package/src/commands/test/test-command.ts +0 -406
- package/src/commands/tui/index.ts +0 -1
- package/src/commands/tui/tui-command.ts +0 -87
- package/src/commands/update/index.ts +0 -1
- package/src/commands/update/update-command.ts +0 -109
- package/src/commands/waf/index.ts +0 -2
- package/src/commands/waf/types.ts +0 -48
- package/src/commands/waf/waf-command.test.ts +0 -506
- package/src/commands/waf/waf-command.ts +0 -545
- package/src/index.ts +0 -261
- package/src/services/cloudflare/cloudflare-service.test.ts +0 -658
- package/src/services/cloudflare/cloudflare-service.ts +0 -433
- package/src/services/cloudflare/index.ts +0 -2
- package/src/services/cloudflare/types.ts +0 -30
- package/src/services/config/config-service.test.ts +0 -395
- package/src/services/config/config-service.ts +0 -202
- package/src/services/config/index.ts +0 -9
- package/src/services/config/types.ts +0 -66
- package/src/services/db/db-service.test.ts +0 -63
- package/src/services/db/db-service.ts +0 -131
- package/src/services/db/index.ts +0 -1
- package/src/services/docker/docker-service.ts +0 -155
- package/src/services/docker/index.ts +0 -1
- package/src/services/env/env-service.test.ts +0 -259
- package/src/services/env/env-service.ts +0 -372
- package/src/services/env/index.ts +0 -1
- package/src/services/kv/index.ts +0 -1
- package/src/services/kv/kv-sync-service.test.ts +0 -38
- package/src/services/kv/kv-sync-service.ts +0 -243
- package/src/services/prerequisites/index.ts +0 -1
- package/src/services/prerequisites/prerequisites-service.test.ts +0 -89
- package/src/services/prerequisites/prerequisites-service.ts +0 -377
- package/src/services/prerequisites/types.ts +0 -48
- package/src/services/schema/index.ts +0 -1
- package/src/services/schema/schema-service.ts +0 -99
- package/src/services/secrets/index.ts +0 -11
- package/src/services/secrets/secrets-service.test.ts +0 -502
- package/src/services/secrets/secrets-service.ts +0 -287
- package/src/services/secrets/types.ts +0 -50
- package/src/services/update/index.ts +0 -2
- package/src/services/update/update-service.test.ts +0 -76
- package/src/services/update/update-service.ts +0 -193
- package/src/ui/banner.ts +0 -141
- package/src/ui/index.ts +0 -8
- package/src/ui/menu.ts +0 -490
- package/src/utils/error-handler.ts +0 -62
- package/src/utils/errors.test.ts +0 -69
- package/src/utils/errors.ts +0 -24
- package/src/utils/formatters.test.ts +0 -180
- package/src/utils/formatters.ts +0 -269
- package/src/utils/git.ts +0 -134
- 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
|
-
}
|