@jango-blockchained/hoox-cli 0.5.0 → 0.5.2

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 (42) hide show
  1. package/package.json +1 -1
  2. package/src/commands/check/check-command.ts +8 -65
  3. package/src/commands/check/prerequisites-command.ts +7 -6
  4. package/src/commands/clone/clone-command.test.ts +8 -10
  5. package/src/commands/config/config-command.test.ts +4 -4
  6. package/src/commands/config/config-command.ts +10 -11
  7. package/src/commands/config/env-command.test.ts +2 -2
  8. package/src/commands/config/env-command.ts +40 -43
  9. package/src/commands/config/kv-command.ts +60 -60
  10. package/src/commands/dashboard/dashboard-command.ts +32 -30
  11. package/src/commands/db/db-command.ts +79 -80
  12. package/src/commands/deploy/deploy-command.test.ts +54 -34
  13. package/src/commands/deploy/deploy-command.ts +3 -84
  14. package/src/commands/dev/dev-command.test.ts +84 -62
  15. package/src/commands/disclaimer/disclaimer-command.ts +21 -0
  16. package/src/commands/disclaimer/index.ts +1 -0
  17. package/src/commands/init/init-command.test.ts +69 -91
  18. package/src/commands/init/init-command.ts +19 -15
  19. package/src/commands/logs/logs-command.test.ts +0 -1
  20. package/src/commands/monitor/monitor-command.test.ts +37 -29
  21. package/src/commands/repair/repair-command.test.ts +60 -41
  22. package/src/commands/repair/repair-service.ts +26 -0
  23. package/src/commands/schema/index.ts +1 -0
  24. package/src/commands/schema/schema-command.ts +137 -0
  25. package/src/commands/test/test-command.test.ts +2 -3
  26. package/src/commands/update/update-command.ts +6 -65
  27. package/src/commands/waf/waf-command.ts +56 -59
  28. package/src/index.ts +5 -13
  29. package/src/services/config/config-service.ts +1 -6
  30. package/src/services/db/db-service.test.ts +13 -1
  31. package/src/services/env/env-service.test.ts +41 -18
  32. package/src/services/env/env-service.ts +45 -59
  33. package/src/services/kv/kv-sync-service.ts +14 -5
  34. package/src/services/schema/index.ts +1 -0
  35. package/src/services/schema/schema-service.ts +99 -0
  36. package/src/services/secrets/secrets-service.test.ts +42 -35
  37. package/src/services/secrets/types.ts +1 -1
  38. package/src/ui/menu.ts +1 -1
  39. package/src/utils/error-handler.ts +62 -0
  40. package/src/utils/errors.ts +1 -0
  41. package/src/utils/git.ts +134 -0
  42. package/src/utils/theme.ts +0 -2
@@ -18,7 +18,11 @@ import {
18
18
  } from "@jango-blockchained/hoox-shared";
19
19
  import type { WorkersJsonConfig } from "@jango-blockchained/hoox-shared";
20
20
  import { CloudflareService } from "../../services/cloudflare/index.js";
21
- import { formatSuccess, formatError } from "../../utils/formatters.js";
21
+ import {
22
+ getFormatOptions,
23
+ formatSuccess,
24
+ formatError,
25
+ } from "../../utils/formatters.js";
22
26
  import { CLIError, ExitCode } from "../../utils/errors.js";
23
27
  import { theme } from "../../utils/theme.js";
24
28
  import { CLIProvisioner } from "./cli-provisioner.js";
@@ -204,7 +208,7 @@ EXAMPLES:
204
208
  .option("--resume", "Resume from saved wizard state")
205
209
  .option("--accept-risk", "Skip the risk acknowledgment confirmation")
206
210
  .action(async (options: InitOptions) => {
207
- const globalOpts = program.opts() as { json?: boolean; quiet?: boolean };
211
+ const globalOpts = getFormatOptions(program);
208
212
  const isNonInteractive = Boolean(options.token && options.account);
209
213
 
210
214
  try {
@@ -213,7 +217,7 @@ EXAMPLES:
213
217
  const message = err instanceof Error ? err.message : String(err);
214
218
  p.log.error(message);
215
219
  formatError(new CLIError(message, ExitCode.ERROR), globalOpts);
216
- process.exit(ExitCode.ERROR);
220
+ process.exitCode = ExitCode.ERROR;
217
221
  }
218
222
  });
219
223
  }
@@ -244,7 +248,7 @@ export async function runInitCommand(
244
248
  const error = await validateApiToken(cf, token);
245
249
  if (error) {
246
250
  formatError(new CLIError(error, ExitCode.ERROR), globalOpts);
247
- process.exit(ExitCode.ERROR);
251
+ process.exitCode = ExitCode.ERROR;
248
252
  }
249
253
  if (!globalOpts.quiet) {
250
254
  formatSuccess("Cloudflare API token validated", globalOpts);
@@ -296,7 +300,7 @@ export async function runInitCommand(
296
300
  });
297
301
  if (p.isCancel(resumed)) {
298
302
  p.cancel("Setup cancelled.");
299
- process.exit(0);
303
+ process.exitCode = 0;
300
304
  }
301
305
  if (!resumed) {
302
306
  engine = null;
@@ -320,12 +324,12 @@ export async function runInitCommand(
320
324
 
321
325
  if (p.isCancel(accepted)) {
322
326
  p.cancel("Setup cancelled.");
323
- process.exit(0);
327
+ process.exitCode = 0;
324
328
  }
325
329
 
326
330
  if (!accepted) {
327
331
  p.outro("Setup cancelled. See DISCLAIMER.md for full terms.");
328
- process.exit(0);
332
+ process.exitCode = 0;
329
333
  }
330
334
 
331
335
  // Step 0: Prerequisites check (always passes — CLI is running)
@@ -350,7 +354,7 @@ export async function runInitCommand(
350
354
 
351
355
  if (p.isCancel(apiToken)) {
352
356
  p.cancel("Setup cancelled.");
353
- process.exit(0);
357
+ process.exitCode = 0;
354
358
  }
355
359
 
356
360
  p.log.step("Validating Cloudflare API token...");
@@ -379,7 +383,7 @@ export async function runInitCommand(
379
383
  });
380
384
  if (p.isCancel(accountResult)) {
381
385
  p.cancel("Setup cancelled.");
382
- process.exit(0);
386
+ process.exitCode = 0;
383
387
  }
384
388
 
385
389
  const secretStoreResult = await p.text({
@@ -389,7 +393,7 @@ export async function runInitCommand(
389
393
  });
390
394
  if (p.isCancel(secretStoreResult)) {
391
395
  p.cancel("Setup cancelled.");
392
- process.exit(0);
396
+ process.exitCode = 0;
393
397
  }
394
398
 
395
399
  const prefixResult = await p.text({
@@ -403,7 +407,7 @@ export async function runInitCommand(
403
407
  });
404
408
  if (p.isCancel(prefixResult)) {
405
409
  p.cancel("Setup cancelled.");
406
- process.exit(0);
410
+ process.exitCode = 0;
407
411
  }
408
412
 
409
413
  engine.execute({
@@ -440,7 +444,7 @@ export async function runInitCommand(
440
444
 
441
445
  if (p.isCancel(presetChoice)) {
442
446
  p.cancel("Setup cancelled.");
443
- process.exit(0);
447
+ process.exitCode = 0;
444
448
  }
445
449
 
446
450
  engine.execute({ preset: presetChoice as string });
@@ -471,7 +475,7 @@ export async function runInitCommand(
471
475
 
472
476
  if (p.isCancel(shouldProvision)) {
473
477
  p.cancel("Setup cancelled.");
474
- process.exit(0);
478
+ process.exitCode = 0;
475
479
  }
476
480
 
477
481
  if (shouldProvision) {
@@ -529,7 +533,7 @@ export async function runInitCommand(
529
533
  const collected = await p.group(groupFields, {
530
534
  onCancel: () => {
531
535
  p.cancel("Setup cancelled.");
532
- process.exit(0);
536
+ process.exitCode = 0;
533
537
  },
534
538
  });
535
539
 
@@ -569,7 +573,7 @@ export async function runInitCommand(
569
573
 
570
574
  if (p.isCancel(shouldDeploy)) {
571
575
  p.cancel("Setup cancelled.");
572
- process.exit(0);
576
+ process.exitCode = 0;
573
577
  }
574
578
 
575
579
  if (shouldDeploy) {
@@ -1,4 +1,3 @@
1
- // @ts-nocheck
2
1
  /**
3
2
  * Unit tests for the logs command.
4
3
  *
@@ -1,4 +1,3 @@
1
- // @ts-nocheck
2
1
  /**
3
2
  * Unit tests for the monitor command.
4
3
  *
@@ -35,16 +34,19 @@ beforeEach(() => {
35
34
  process.exitCode = 0;
36
35
 
37
36
  // Restore originals
38
- (MonitorService.prototype as Record<string, unknown>).checkAllWorkerHealth =
39
- origCheckAll;
40
- (DbService.prototype as Record<string, unknown>).resolveDbName =
37
+ (
38
+ MonitorService.prototype as unknown as Record<string, unknown>
39
+ ).checkAllWorkerHealth = origCheckAll;
40
+ (DbService.prototype as unknown as Record<string, unknown>).resolveDbName =
41
41
  origResolveDbName;
42
- (DbService.prototype as Record<string, unknown>).query = origQuery;
43
- (DbService.prototype as Record<string, unknown>).export = origExport;
44
- (KvSyncService.prototype as Record<string, unknown>).resolveNamespaceId =
45
- origResolveNs;
46
- (KvSyncService.prototype as Record<string, unknown>).get = origGet;
47
- (KvSyncService.prototype as Record<string, unknown>).set = origSet;
42
+ (DbService.prototype as unknown as Record<string, unknown>).query = origQuery;
43
+ (DbService.prototype as unknown as Record<string, unknown>).export =
44
+ origExport;
45
+ (
46
+ KvSyncService.prototype as unknown as Record<string, unknown>
47
+ ).resolveNamespaceId = origResolveNs;
48
+ (KvSyncService.prototype as unknown as Record<string, unknown>).get = origGet;
49
+ (KvSyncService.prototype as unknown as Record<string, unknown>).set = origSet;
48
50
 
49
51
  // Fresh mocks
50
52
  checkAllWorkerHealthMock = mock(async () => ({
@@ -68,30 +70,36 @@ beforeEach(() => {
68
70
  setMock = mock(async (_nsId: string, _key: string, _value: string) => {});
69
71
 
70
72
  // Install mocks on prototypes
71
- (MonitorService.prototype as Record<string, unknown>).checkAllWorkerHealth =
72
- checkAllWorkerHealthMock;
73
- (DbService.prototype as Record<string, unknown>).resolveDbName =
73
+ (
74
+ MonitorService.prototype as unknown as Record<string, unknown>
75
+ ).checkAllWorkerHealth = checkAllWorkerHealthMock;
76
+ (DbService.prototype as unknown as Record<string, unknown>).resolveDbName =
74
77
  resolveDbNameMock;
75
- (DbService.prototype as Record<string, unknown>).query = queryMock;
76
- (DbService.prototype as Record<string, unknown>).export = exportMock;
77
- (KvSyncService.prototype as Record<string, unknown>).resolveNamespaceId =
78
- resolveNamespaceIdMock;
79
- (KvSyncService.prototype as Record<string, unknown>).get = getMock;
80
- (KvSyncService.prototype as Record<string, unknown>).set = setMock;
78
+ (DbService.prototype as unknown as Record<string, unknown>).query = queryMock;
79
+ (DbService.prototype as unknown as Record<string, unknown>).export =
80
+ exportMock;
81
+ (
82
+ KvSyncService.prototype as unknown as Record<string, unknown>
83
+ ).resolveNamespaceId = resolveNamespaceIdMock;
84
+ (KvSyncService.prototype as unknown as Record<string, unknown>).get = getMock;
85
+ (KvSyncService.prototype as unknown as Record<string, unknown>).set = setMock;
81
86
  });
82
87
 
83
88
  afterEach(() => {
84
89
  mock.restore();
85
- (MonitorService.prototype as Record<string, unknown>).checkAllWorkerHealth =
86
- origCheckAll;
87
- (DbService.prototype as Record<string, unknown>).resolveDbName =
90
+ (
91
+ MonitorService.prototype as unknown as Record<string, unknown>
92
+ ).checkAllWorkerHealth = origCheckAll;
93
+ (DbService.prototype as unknown as Record<string, unknown>).resolveDbName =
88
94
  origResolveDbName;
89
- (DbService.prototype as Record<string, unknown>).query = origQuery;
90
- (DbService.prototype as Record<string, unknown>).export = origExport;
91
- (KvSyncService.prototype as Record<string, unknown>).resolveNamespaceId =
92
- origResolveNs;
93
- (KvSyncService.prototype as Record<string, unknown>).get = origGet;
94
- (KvSyncService.prototype as Record<string, unknown>).set = origSet;
95
+ (DbService.prototype as unknown as Record<string, unknown>).query = origQuery;
96
+ (DbService.prototype as unknown as Record<string, unknown>).export =
97
+ origExport;
98
+ (
99
+ KvSyncService.prototype as unknown as Record<string, unknown>
100
+ ).resolveNamespaceId = origResolveNs;
101
+ (KvSyncService.prototype as unknown as Record<string, unknown>).get = origGet;
102
+ (KvSyncService.prototype as unknown as Record<string, unknown>).set = origSet;
95
103
  });
96
104
 
97
105
  async function importMonitorCommand(): Promise<{
@@ -185,7 +193,7 @@ describe("registerMonitorCommand", () => {
185
193
  throw new Error("Connection failed");
186
194
  });
187
195
  (
188
- MonitorService.prototype as Record<string, unknown>
196
+ MonitorService.prototype as unknown as Record<string, unknown>
189
197
  ).checkAllWorkerHealth = checkAllWorkerHealthMock;
190
198
 
191
199
  const program = await createProgram();
@@ -1,4 +1,3 @@
1
- // @ts-nocheck
2
1
  /**
3
2
  * Unit tests for the repair command.
4
3
  *
@@ -12,7 +11,6 @@ import { RepairService } from "./repair-service.js";
12
11
  import { CloudflareService } from "../../services/cloudflare/cloudflare-service.js";
13
12
  import { DbService } from "../../services/db/db-service.js";
14
13
  import { KvSyncService } from "../../services/kv/kv-sync-service.js";
15
- import { SecretsService } from "../../services/secrets/secrets-service.js";
16
14
 
17
15
  // Stubs
18
16
  let runSystemCheckMock: ReturnType<typeof mock>;
@@ -32,31 +30,39 @@ const origExport = DbService.prototype.export;
32
30
  const origReset = DbService.prototype.reset;
33
31
  const origResolveNs = KvSyncService.prototype.resolveNamespaceId;
34
32
  const origKvSet = KvSyncService.prototype.set;
35
- const origGetWorker = CloudflareService.prototype.deploy; // placeholder
36
- const origListWorkers = CloudflareService.prototype.deploy; // placeholder
37
33
 
38
34
  beforeEach(() => {
39
35
  mock.restore();
40
36
  process.exitCode = 0;
41
37
 
42
38
  // Restore originals
43
- (RepairService.prototype as Record<string, unknown>).runSystemCheck =
44
- origRunSystemCheck;
45
- (CloudflareService.prototype as Record<string, unknown>).deploy = origDeploy;
46
- (CloudflareService.prototype as Record<string, unknown>).d1List = origD1List;
47
- (CloudflareService.prototype as Record<string, unknown>).kvList = origKvList;
48
- (CloudflareService.prototype as Record<string, unknown>).r2List = origR2List;
49
- (CloudflareService.prototype as Record<string, unknown>).queueList =
50
- origQueueList;
51
- (DbService.prototype as Record<string, unknown>).resolveDbName =
39
+ (
40
+ RepairService.prototype as unknown as Record<string, unknown>
41
+ ).runSystemCheck = origRunSystemCheck;
42
+ (CloudflareService.prototype as unknown as Record<string, unknown>).deploy =
43
+ origDeploy;
44
+ (CloudflareService.prototype as unknown as Record<string, unknown>).d1List =
45
+ origD1List;
46
+ (CloudflareService.prototype as unknown as Record<string, unknown>).kvList =
47
+ origKvList;
48
+ (CloudflareService.prototype as unknown as Record<string, unknown>).r2List =
49
+ origR2List;
50
+ (
51
+ CloudflareService.prototype as unknown as Record<string, unknown>
52
+ ).queueList = origQueueList;
53
+ (DbService.prototype as unknown as Record<string, unknown>).resolveDbName =
52
54
  origResolveDbName;
53
- (DbService.prototype as Record<string, unknown>).apply = origApply;
54
- (DbService.prototype as Record<string, unknown>).migrate = origMigrate;
55
- (DbService.prototype as Record<string, unknown>).export = origExport;
56
- (DbService.prototype as Record<string, unknown>).reset = origReset;
57
- (KvSyncService.prototype as Record<string, unknown>).resolveNamespaceId =
58
- origResolveNs;
59
- (KvSyncService.prototype as Record<string, unknown>).set = origKvSet;
55
+ (DbService.prototype as unknown as Record<string, unknown>).apply = origApply;
56
+ (DbService.prototype as unknown as Record<string, unknown>).migrate =
57
+ origMigrate;
58
+ (DbService.prototype as unknown as Record<string, unknown>).export =
59
+ origExport;
60
+ (DbService.prototype as unknown as Record<string, unknown>).reset = origReset;
61
+ (
62
+ KvSyncService.prototype as unknown as Record<string, unknown>
63
+ ).resolveNamespaceId = origResolveNs;
64
+ (KvSyncService.prototype as unknown as Record<string, unknown>).set =
65
+ origKvSet;
60
66
 
61
67
  // Fresh mocks
62
68
  runSystemCheckMock = mock(async () => ({
@@ -77,30 +83,42 @@ beforeEach(() => {
77
83
  data: { url: "https://test-worker.cryptolinx.workers.dev" },
78
84
  }));
79
85
 
80
- (RepairService.prototype as Record<string, unknown>).runSystemCheck =
81
- runSystemCheckMock;
82
- (CloudflareService.prototype as Record<string, unknown>).deploy = deployMock;
86
+ (
87
+ RepairService.prototype as unknown as Record<string, unknown>
88
+ ).runSystemCheck = runSystemCheckMock;
89
+ (CloudflareService.prototype as unknown as Record<string, unknown>).deploy =
90
+ deployMock;
83
91
  });
84
92
 
85
93
  afterEach(() => {
86
94
  mock.restore();
87
- (RepairService.prototype as Record<string, unknown>).runSystemCheck =
88
- origRunSystemCheck;
89
- (CloudflareService.prototype as Record<string, unknown>).deploy = origDeploy;
90
- (CloudflareService.prototype as Record<string, unknown>).d1List = origD1List;
91
- (CloudflareService.prototype as Record<string, unknown>).kvList = origKvList;
92
- (CloudflareService.prototype as Record<string, unknown>).r2List = origR2List;
93
- (CloudflareService.prototype as Record<string, unknown>).queueList =
94
- origQueueList;
95
- (DbService.prototype as Record<string, unknown>).resolveDbName =
95
+ (
96
+ RepairService.prototype as unknown as Record<string, unknown>
97
+ ).runSystemCheck = origRunSystemCheck;
98
+ (CloudflareService.prototype as unknown as Record<string, unknown>).deploy =
99
+ origDeploy;
100
+ (CloudflareService.prototype as unknown as Record<string, unknown>).d1List =
101
+ origD1List;
102
+ (CloudflareService.prototype as unknown as Record<string, unknown>).kvList =
103
+ origKvList;
104
+ (CloudflareService.prototype as unknown as Record<string, unknown>).r2List =
105
+ origR2List;
106
+ (
107
+ CloudflareService.prototype as unknown as Record<string, unknown>
108
+ ).queueList = origQueueList;
109
+ (DbService.prototype as unknown as Record<string, unknown>).resolveDbName =
96
110
  origResolveDbName;
97
- (DbService.prototype as Record<string, unknown>).apply = origApply;
98
- (DbService.prototype as Record<string, unknown>).migrate = origMigrate;
99
- (DbService.prototype as Record<string, unknown>).export = origExport;
100
- (DbService.prototype as Record<string, unknown>).reset = origReset;
101
- (KvSyncService.prototype as Record<string, unknown>).resolveNamespaceId =
102
- origResolveNs;
103
- (KvSyncService.prototype as Record<string, unknown>).set = origKvSet;
111
+ (DbService.prototype as unknown as Record<string, unknown>).apply = origApply;
112
+ (DbService.prototype as unknown as Record<string, unknown>).migrate =
113
+ origMigrate;
114
+ (DbService.prototype as unknown as Record<string, unknown>).export =
115
+ origExport;
116
+ (DbService.prototype as unknown as Record<string, unknown>).reset = origReset;
117
+ (
118
+ KvSyncService.prototype as unknown as Record<string, unknown>
119
+ ).resolveNamespaceId = origResolveNs;
120
+ (KvSyncService.prototype as unknown as Record<string, unknown>).set =
121
+ origKvSet;
104
122
  });
105
123
 
106
124
  async function importRepairCommand(): Promise<{
@@ -194,8 +212,9 @@ describe("registerRepairCommand", () => {
194
212
  runSystemCheckMock = mock(async () => {
195
213
  throw new Error("Check failed");
196
214
  });
197
- (RepairService.prototype as Record<string, unknown>).runSystemCheck =
198
- runSystemCheckMock;
215
+ (
216
+ RepairService.prototype as unknown as Record<string, unknown>
217
+ ).runSystemCheck = runSystemCheckMock;
199
218
 
200
219
  const program = await createProgram();
201
220
  await program.parseAsync(["repair", "check"], { from: "user" });
@@ -126,6 +126,32 @@ export class RepairService {
126
126
  steps.push({ step: "Secrets", success: false, error: String(err) });
127
127
  }
128
128
 
129
+ // Step 6: Worker config schema validation
130
+ try {
131
+ const { SchemaService } =
132
+ await import("../../services/schema/schema-service.js");
133
+ const svc = new SchemaService();
134
+ const results = svc.validateAll();
135
+ const totalErrors = results.reduce(
136
+ (sum, r) => sum + r.errors.filter((e) => e.severity === "error").length,
137
+ 0
138
+ );
139
+ steps.push({
140
+ step: "Worker config schema",
141
+ success: totalErrors === 0,
142
+ message:
143
+ totalErrors === 0
144
+ ? "All workers match manifest"
145
+ : `${totalErrors} schema issue(s) found`,
146
+ });
147
+ } catch (err) {
148
+ steps.push({
149
+ step: "Worker config schema",
150
+ success: false,
151
+ error: String(err),
152
+ });
153
+ }
154
+
129
155
  const passed = steps.filter((s) => s.success).length;
130
156
  const failed = steps.filter((s) => !s.success).length;
131
157
  return {
@@ -0,0 +1 @@
1
+ export { registerSchemaCommand } from "./schema-command.js";
@@ -0,0 +1,137 @@
1
+ import { resolve } from "path";
2
+ import { Command } from "commander";
3
+ import { SchemaService } from "../../services/schema/schema-service.js";
4
+ import {
5
+ formatSuccess,
6
+ formatError,
7
+ formatKeyValue,
8
+ formatTable,
9
+ getFormatOptions,
10
+ } from "../../utils/formatters.js";
11
+ import { CLIError, ExitCode } from "../../utils/errors.js";
12
+ import { withErrorHandling } from "../../utils/error-handler.js";
13
+ import {
14
+ generateWranglerJsonc,
15
+ generateDevVars,
16
+ } from "@jango-blockchained/hoox-shared";
17
+
18
+ export function registerSchemaCommand(program: Command): void {
19
+ const schemaCmd = program
20
+ .command("schema")
21
+ .summary("Validate and manage worker configuration")
22
+ .description(
23
+ "Validate, generate, and repair worker wrangler.jsonc against canonical manifests."
24
+ );
25
+
26
+ schemaCmd
27
+ .command("validate [worker]")
28
+ .description(
29
+ "Validate worker(s) against manifest (omit worker to validate all)"
30
+ )
31
+ .action(
32
+ withErrorHandling(async (worker?: string) => {
33
+ const fmt = getFormatOptions(schemaCmd);
34
+ const svc = new SchemaService();
35
+ const results = worker
36
+ ? [svc.validateWorker(worker)]
37
+ : svc.validateAll();
38
+ let totalErrors = 0;
39
+ for (const r of results) {
40
+ if (r.passed) {
41
+ formatSuccess(`${r.worker}: ✅ passed`, fmt);
42
+ } else {
43
+ formatError(
44
+ new CLIError(
45
+ `${r.worker}: ❌ failed (${r.errors.length} issues)`,
46
+ ExitCode.ERROR
47
+ ),
48
+ fmt
49
+ );
50
+ for (const e of r.errors.filter((e) => e.severity === "error")) {
51
+ process.stderr.write(` ✗ ${e.message}\n`);
52
+ }
53
+ totalErrors += r.errors.filter(
54
+ (e) => e.severity === "error"
55
+ ).length;
56
+ }
57
+ }
58
+ if (totalErrors > 0) process.exitCode = ExitCode.ERROR;
59
+ })
60
+ );
61
+
62
+ schemaCmd
63
+ .command("list")
64
+ .description("List all workers with their binding counts")
65
+ .action(
66
+ withErrorHandling(async () => {
67
+ const fmt = getFormatOptions(schemaCmd);
68
+ const svc = new SchemaService();
69
+ const rows: Record<string, string>[] = [];
70
+ for (const name of svc.getWorkerNames()) {
71
+ const m = svc.getManifest(name)!;
72
+ const secretCount = Object.values(m.vars).filter(
73
+ (v) => v.type === "secret"
74
+ ).length;
75
+ const svcCount = m.services.length;
76
+ const infraCount = Object.keys(m.infrastructure).length;
77
+ const middleware = m.middleware.length
78
+ ? m.middleware.join(", ")
79
+ : "\u2014";
80
+ rows.push({
81
+ worker: name,
82
+ secrets: String(secretCount),
83
+ services: String(svcCount),
84
+ infra: String(infraCount),
85
+ middleware,
86
+ });
87
+ }
88
+ formatTable(rows, fmt);
89
+ })
90
+ );
91
+
92
+ schemaCmd
93
+ .command("generate <worker>")
94
+ .description("Generate wrangler.jsonc and .dev.vars from manifest")
95
+ .option("--dry-run", "Print generated content without writing")
96
+ .action(
97
+ withErrorHandling(async (worker: string, opts: { dryRun?: boolean }) => {
98
+ const fmt = getFormatOptions(schemaCmd);
99
+ const svc = new SchemaService();
100
+ const manifest = svc.getManifest(worker);
101
+ if (!manifest) {
102
+ formatError(
103
+ new CLIError(`Unknown worker "${worker}"`, ExitCode.INVALID_USAGE),
104
+ fmt
105
+ );
106
+ return;
107
+ }
108
+
109
+ const wranglerContent = generateWranglerJsonc(manifest);
110
+ const devVarsContent = generateDevVars(manifest);
111
+
112
+ if (opts.dryRun) {
113
+ process.stdout.write(`--- wrangler.jsonc (${worker}) ---\n`);
114
+ process.stdout.write(wranglerContent + "\n");
115
+ process.stdout.write(`--- .dev.vars (${worker}) ---\n`);
116
+ process.stdout.write(devVarsContent + "\n");
117
+ return;
118
+ }
119
+
120
+ const workersDir = resolve(process.cwd(), "workers", worker);
121
+ const wranglerPath = resolve(workersDir, "wrangler.jsonc");
122
+ const devVarsPath = resolve(workersDir, ".dev.vars");
123
+
124
+ await Bun.write(wranglerPath, wranglerContent);
125
+ await Bun.write(devVarsPath, devVarsContent);
126
+
127
+ formatKeyValue(
128
+ {
129
+ "wrangler.jsonc": wranglerPath,
130
+ ".dev.vars": devVarsPath,
131
+ },
132
+ { ...fmt, json: false }
133
+ );
134
+ formatSuccess(`Generated files for ${worker}`, fmt);
135
+ })
136
+ );
137
+ }
@@ -1,9 +1,8 @@
1
- // @ts-nocheck
2
1
  import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test";
3
2
  import { Command } from "commander";
4
3
  import { registerTestCommand, runStep, printSummary } from "./test-command.js";
5
- import type { TestStepResult, TestSummary } from "./test-command.js";
6
- import { CLIError, ExitCode } from "../../utils/errors.js";
4
+ import type { TestSummary } from "./test-command.js";
5
+ import { ExitCode } from "../../utils/errors.js";
7
6
 
8
7
  // ---------------------------------------------------------------------------
9
8
  // Helpers
@@ -5,71 +5,12 @@ import { UpdateService } from "../../services/update/index.js";
5
5
  import { formatError, getFormatOptions } from "../../utils/formatters.js";
6
6
  import { CLIError, ExitCode } from "../../utils/errors.js";
7
7
  import { theme } from "../../utils/theme.js";
8
-
9
- async function gitPull(cwd: string): Promise<string> {
10
- const proc = Bun.spawn(["git", "pull", "--ff-only"], {
11
- cwd,
12
- stdout: "pipe",
13
- stderr: "pipe",
14
- });
15
- const exitCode = await proc.exited;
16
- const stdout = await new Response(proc.stdout).text();
17
- const stderr = await new Response(proc.stderr).text();
18
-
19
- if (exitCode !== 0) {
20
- throw new Error(stderr.trim() || `git pull failed (exit ${exitCode})`);
21
- }
22
- return stdout.trim();
23
- }
24
-
25
- async function gitSubmoduleUpdate(
26
- cwd: string,
27
- submodulePath: string
28
- ): Promise<string> {
29
- const proc = Bun.spawn(
30
- ["git", "submodule", "update", "--remote", "--init", "--", submodulePath],
31
- {
32
- cwd,
33
- stdout: "pipe",
34
- stderr: "pipe",
35
- }
36
- );
37
- const exitCode = await proc.exited;
38
- const stdout = await new Response(proc.stdout).text();
39
- const stderr = await new Response(proc.stderr).text();
40
-
41
- if (exitCode !== 0) {
42
- throw new Error(
43
- stderr.trim() || `git submodule update failed (exit ${exitCode})`
44
- );
45
- }
46
- return stdout.trim();
47
- }
48
-
49
- async function isGitRepo(cwd: string): Promise<boolean> {
50
- const proc = Bun.spawn(["git", "rev-parse", "--is-inside-work-tree"], {
51
- cwd,
52
- stdout: "pipe",
53
- stderr: "pipe",
54
- });
55
- const exitCode = await proc.exited;
56
- return exitCode === 0;
57
- }
58
-
59
- async function isSubmodule(
60
- cwd: string,
61
- submodulePath: string
62
- ): Promise<boolean> {
63
- const proc = Bun.spawn(["git", "submodule", "status", "--", submodulePath], {
64
- cwd,
65
- stdout: "pipe",
66
- stderr: "pipe",
67
- });
68
- const exitCode = await proc.exited;
69
- if (exitCode !== 0) return false;
70
- const out = (await new Response(proc.stdout).text()).trim();
71
- return out.length > 0;
72
- }
8
+ import {
9
+ gitPull,
10
+ gitSubmoduleUpdate,
11
+ isGitRepo,
12
+ isSubmodule,
13
+ } from "../../utils/git.js";
73
14
 
74
15
  export function registerUpdateCommand(program: Command): void {
75
16
  program