@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jango-blockchained/hoox-cli",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Hoox CLI — manage Cloudflare Workers, infrastructure, secrets, and deployments (@jango-blockchained/hoox-cli)",
5
5
  "type": "module",
6
6
  "main": "./bin/hoox.js",
@@ -21,8 +21,10 @@ import {
21
21
  formatSuccess,
22
22
  formatError,
23
23
  formatTable,
24
+ getFormatOptions,
24
25
  } from "../../utils/formatters.js";
25
- import { CLIError, ExitCode } from "../../utils/errors.js";
26
+ import { ExitCode } from "../../utils/errors.js";
27
+ import { isGitTracked, gitUntrackFile } from "../../utils/git.js";
26
28
  import type { FormatOptions } from "../../utils/formatters.js";
27
29
  import type {
28
30
  CheckResult,
@@ -104,13 +106,6 @@ function buildReport(categories: CheckCategory[]): CheckReport {
104
106
  };
105
107
  }
106
108
 
107
- /**
108
- * Return a single-character icon for a check result.
109
- */
110
- function checkIcon(success: boolean): string {
111
- return success ? icons.success : icons.error;
112
- }
113
-
114
109
  // ---------------------------------------------------------------------------
115
110
  // Submodule Gitignore Helpers
116
111
  // ---------------------------------------------------------------------------
@@ -136,42 +131,6 @@ async function getWorkerDirs(): Promise<string[]> {
136
131
  }
137
132
  }
138
133
 
139
- /**
140
- * Check if a file or directory is tracked by git in a given directory.
141
- */
142
- async function isGitTracked(dir: string, filename: string): Promise<boolean> {
143
- try {
144
- const proc = Bun.spawn(["git", "ls-files", "--error-unmatch", filename], {
145
- cwd: dir,
146
- stdout: "pipe",
147
- stderr: "pipe",
148
- });
149
- const exitCode = await proc.exited;
150
- return exitCode === 0;
151
- } catch {
152
- return false;
153
- }
154
- }
155
-
156
- /**
157
- * Run git rm --cached to untrack a file while keeping it locally.
158
- */
159
- async function gitUntrackFile(dir: string, filename: string): Promise<void> {
160
- await new Promise<void>((resolve, reject) => {
161
- const proc = Bun.spawn(["git", "rm", "--cached", filename], {
162
- cwd: dir,
163
- stdout: "pipe",
164
- stderr: "pipe",
165
- });
166
- proc.exited
167
- .then((code) => {
168
- if (code === 0) resolve();
169
- else reject(new Error(`git rm --cached failed with code ${code}`));
170
- })
171
- .catch(reject);
172
- });
173
- }
174
-
175
134
  // ---------------------------------------------------------------------------
176
135
  // Category 1: Config checks
177
136
  // ---------------------------------------------------------------------------
@@ -871,7 +830,7 @@ interface SubmoduleGitignoreResult {
871
830
  * - Untracks wrangler.jsonc if it's mistakenly tracked by git
872
831
  */
873
832
  async function checkSubmoduleGitignore(
874
- opts: FormatOptions
833
+ _opts: FormatOptions
875
834
  ): Promise<SubmoduleGitignoreResult> {
876
835
  const workerDirs = await getWorkerDirs();
877
836
  const issues: string[] = [];
@@ -1039,11 +998,7 @@ EXAMPLES:
1039
998
  hoox check setup --json`
1040
999
  )
1041
1000
  .action(async (_, cmd: Command) => {
1042
- const rootCmd = cmd.parent?.parent as Command | undefined;
1043
- const opts: FormatOptions = {
1044
- json: Boolean(rootCmd?.optsWithGlobals()?.json),
1045
- quiet: Boolean(rootCmd?.optsWithGlobals()?.quiet),
1046
- };
1001
+ const opts = getFormatOptions(cmd);
1047
1002
  await handleSetup(opts);
1048
1003
  });
1049
1004
 
@@ -1072,11 +1027,7 @@ EXAMPLES:
1072
1027
  )
1073
1028
  .option("--fix", "Attempt automatic repair for detected issues")
1074
1029
  .action(async (options: { fix?: boolean }, cmd: Command) => {
1075
- const rootCmd = cmd.parent?.parent as Command | undefined;
1076
- const opts: FormatOptions = {
1077
- json: Boolean(rootCmd?.optsWithGlobals()?.json),
1078
- quiet: Boolean(rootCmd?.optsWithGlobals()?.quiet),
1079
- };
1030
+ const opts = getFormatOptions(cmd);
1080
1031
  await handleHealth(opts, Boolean(options.fix));
1081
1032
  });
1082
1033
 
@@ -1101,11 +1052,7 @@ EXAMPLES:
1101
1052
  )
1102
1053
  .option("--dry-run", "Preview changes without applying them")
1103
1054
  .action(async (options: { dryRun?: boolean }, cmd: Command) => {
1104
- const rootCmd = cmd.parent?.parent as Command | undefined;
1105
- const opts: FormatOptions = {
1106
- json: Boolean(rootCmd?.optsWithGlobals()?.json),
1107
- quiet: Boolean(rootCmd?.optsWithGlobals()?.quiet),
1108
- };
1055
+ const opts = getFormatOptions(cmd);
1109
1056
  await handleFix(opts, Boolean(options.dryRun));
1110
1057
  });
1111
1058
 
@@ -1134,11 +1081,7 @@ EXAMPLES:
1134
1081
  hoox check sg # alias`
1135
1082
  )
1136
1083
  .action(async (_, cmd: Command) => {
1137
- const rootCmd = cmd.parent?.parent as Command | undefined;
1138
- const opts: FormatOptions = {
1139
- json: Boolean(rootCmd?.optsWithGlobals()?.json),
1140
- quiet: Boolean(rootCmd?.optsWithGlobals()?.quiet),
1141
- };
1084
+ const opts = getFormatOptions(cmd);
1142
1085
  await handleSubmoduleGitignore(opts);
1143
1086
  });
1144
1087
  }
@@ -2,7 +2,11 @@ import { Command } from "commander";
2
2
  import { PrerequisitesService } from "../../services/prerequisites/index.js";
3
3
  import type { PrerequisitesReport } from "../../services/prerequisites/prerequisites-service.js";
4
4
  import { theme, icons } from "../../utils/theme.js";
5
- import { formatError, formatJson } from "../../utils/formatters.js";
5
+ import {
6
+ getFormatOptions,
7
+ formatError,
8
+ formatJson,
9
+ } from "../../utils/formatters.js";
6
10
  import { CLIError, ExitCode } from "../../utils/errors.js";
7
11
  import type { FormatOptions } from "../../utils/formatters.js";
8
12
 
@@ -46,6 +50,7 @@ async function handlePrerequisites(
46
50
  if (opts.json) {
47
51
  formatJson(report, opts);
48
52
  } else if (opts.quiet) {
53
+ // quiet mode — no output
49
54
  } else {
50
55
  renderReport(report);
51
56
  }
@@ -86,11 +91,7 @@ EXAMPLES:
86
91
  )
87
92
  .option("--tool <name>", "Only check a specific tool")
88
93
  .action(async (options: { tool?: string }, cmd: Command) => {
89
- const rootCmd = cmd.parent?.parent as Command | undefined;
90
- const opts: FormatOptions = {
91
- json: Boolean(rootCmd?.optsWithGlobals()?.json),
92
- quiet: Boolean(rootCmd?.optsWithGlobals()?.quiet),
93
- };
94
+ const opts = getFormatOptions(cmd);
94
95
  await handlePrerequisites(opts, options.tool);
95
96
  });
96
97
  }
@@ -1,4 +1,3 @@
1
- // @ts-nocheck
2
1
  /**
3
2
  * Unit tests for the clone command.
4
3
  *
@@ -27,7 +26,6 @@ type MockSpawnResult = {
27
26
 
28
27
  const realSpawn = Bun.spawn;
29
28
  let origCwd: string;
30
- let lastSpawnCmd: string[] = [];
31
29
 
32
30
  function successSpawn(stdout = ""): MockSpawnResult {
33
31
  return {
@@ -57,9 +55,7 @@ function enqueueSpawn(result: MockSpawnResult): void {
57
55
  }
58
56
 
59
57
  function installSpawnMock(): void {
60
- lastSpawnCmd = [];
61
- const spawnMock = mock((cmd: string[], _opts?: { cwd?: string }) => {
62
- lastSpawnCmd = [...cmd];
58
+ const spawnMock = mock((_cmd: string[], _opts?: { cwd?: string }) => {
63
59
  return spawnQueue.shift() ?? errorSpawn("unexpected spawn call", 127);
64
60
  });
65
61
  (Bun as Record<string, unknown>).spawn = spawnMock;
@@ -115,10 +111,12 @@ async function captureStdout(
115
111
 
116
112
  let output = "";
117
113
  const orig = process.stdout.write.bind(process.stdout);
118
- (process.stdout as Record<string, unknown>).write = mock((chunk: string) => {
119
- output += chunk;
120
- return true;
121
- });
114
+ (process.stdout as unknown as Record<string, unknown>).write = mock(
115
+ (chunk: string) => {
116
+ output += chunk;
117
+ return true;
118
+ }
119
+ );
122
120
 
123
121
  try {
124
122
  await program.parseAsync([...args], { from: "user" });
@@ -126,7 +124,7 @@ async function captureStdout(
126
124
  // exitOverride throws CommanderError — swallow
127
125
  }
128
126
 
129
- (process.stdout as Record<string, unknown>).write = orig;
127
+ (process.stdout as unknown as Record<string, unknown>).write = orig;
130
128
  process.chdir(origCwd);
131
129
  return output;
132
130
  }
@@ -508,10 +508,10 @@ describe("config keys", () => {
508
508
  expect(text).toContain(".keys/");
509
509
 
510
510
  // Verify files were created
511
- const internalKeyFile = Bun.file(".keys/internal_service_key.env");
511
+ const internalKeyFile = Bun.file(".keys/internal_key_binding.env");
512
512
  expect(await internalKeyFile.exists()).toBe(true);
513
513
  const content = await internalKeyFile.text();
514
- expect(content).toMatch(/^INTERNAL_SERVICE_KEY=[a-f0-9]{64}\n$/);
514
+ expect(content).toMatch(/^INTERNAL_KEY_BINDING=[a-f0-9]{64}\n$/);
515
515
  });
516
516
 
517
517
  it("lists generated key files", async () => {
@@ -572,10 +572,10 @@ describe("config keys", () => {
572
572
  const capture = await runCommand(program, ["config", "keys", "generate"]);
573
573
  capture.restore();
574
574
 
575
- const file = Bun.file(".keys/webhook_api_key.env");
575
+ const file = Bun.file(".keys/webhook_api_key_binding.env");
576
576
  expect(await file.exists()).toBe(true);
577
577
  const content = await file.text();
578
- const match = content.match(/^WEBHOOK_API_KEY=([a-f0-9]+)\n$/);
578
+ const match = content.match(/^WEBHOOK_API_KEY_BINDING=([a-f0-9]+)\n$/);
579
579
  expect(match).not.toBeNull();
580
580
  // Service keys are 32 bytes = 64 hex chars
581
581
  expect(match![1].length).toBe(64);
@@ -204,7 +204,7 @@ EXAMPLES:
204
204
  hoox config show Display current configuration
205
205
  hoox config set workers.agent-worker.enabled false
206
206
  hoox config secrets list List secrets for a worker
207
- hoox config secrets set trade-worker BINANCE_API_KEY
207
+ hoox config secrets set trade-worker BINANCE_KEY_BINDING
208
208
  hoox config keys generate Generate new internal keys`
209
209
  );
210
210
 
@@ -454,7 +454,7 @@ The command will prompt for the secret value (hidden input).
454
454
  It writes to the worker's .dev.vars file and syncs to Cloudflare.
455
455
 
456
456
  EXAMPLES:
457
- hoox config secrets set trade-worker BINANCE_API_KEY
457
+ hoox config secrets set trade-worker BINANCE_KEY_BINDING
458
458
  hoox config secrets set agent-worker OPENAI_KEY`
459
459
  )
460
460
  .action(async (workerName: string, secretName: string) => {
@@ -529,7 +529,7 @@ ARGUMENTS:
529
529
  This removes the secret from Cloudflare and from the worker's .dev.vars file.
530
530
 
531
531
  EXAMPLES:
532
- hoox config secrets delete trade-worker BINANCE_API_KEY`
532
+ hoox config secrets delete trade-worker BINANCE_KEY_BINDING`
533
533
  )
534
534
  .action(async (workerName: string, secretName: string) => {
535
535
  const opts = formatOpts(program);
@@ -718,11 +718,11 @@ EXAMPLES:
718
718
  `Generate new internal auth keys and save to .keys/ directory.
719
719
 
720
720
  Creates the following keys:
721
- - INTERNAL_SERVICE_KEY (32 char)
722
- - WEBHOOK_API_KEY (32 char)
721
+ - INTERNAL_KEY_BINDING (32 char)
722
+ - WEBHOOK_API_KEY_BINDING (32 char)
723
723
  - AGENT_INTERNAL_KEY (32 char)
724
- - TELEGRAM_BOT_TOKEN (16 char)
725
- - INTERNAL_KEY (32 char)
724
+ - TG_BOT_TOKEN_BINDING (16 char)
725
+ - INTERNAL_KEY_BINDING (32 char)
726
726
 
727
727
  WARNING: Add .keys/ to your .gitignore to avoid committing secrets!
728
728
 
@@ -739,11 +739,10 @@ EXAMPLES:
739
739
  }
740
740
 
741
741
  const keys: Record<string, string> = {
742
- INTERNAL_SERVICE_KEY: generateKey(),
743
- WEBHOOK_API_KEY: generateKey(),
742
+ INTERNAL_KEY_BINDING: generateKey(),
743
+ WEBHOOK_API_KEY_BINDING: generateKey(),
744
744
  AGENT_INTERNAL_KEY: generateKey(),
745
- TELEGRAM_BOT_TOKEN: generateKey(16),
746
- INTERNAL_KEY: generateKey(),
745
+ TG_BOT_TOKEN_BINDING: generateKey(16),
747
746
  };
748
747
 
749
748
  for (const [name, value] of Object.entries(keys)) {
@@ -14,12 +14,12 @@ describe("env command", () => {
14
14
  CLOUDFLARE_API_TOKEN: "cfut_xxx",
15
15
  CLOUDFLARE_ACCOUNT_ID: "abc123",
16
16
  SUBDOMAIN_PREFIX: "myapp",
17
- D1_INTERNAL_KEY: "d1-key",
18
17
  TRADE_INTERNAL_KEY: "trade-key",
19
18
  AGENT_INTERNAL_KEY: "agent-key",
20
19
  WEBHOOK_API_KEY_BINDING: "webhook-key",
21
20
  INTERNAL_KEY_BINDING: "inter-key",
22
- API_SERVICE_KEY: "api-key",
21
+ API_SERVICE_KEY_BINDING: "api-key",
22
+ TELEGRAM_INTERNAL_KEY: "tg-key",
23
23
  DASHBOARD_USER: "admin",
24
24
  DASHBOARD_PASS: "pass123",
25
25
  SESSION_SECRET: "a".repeat(32),
@@ -13,11 +13,8 @@ import * as p from "@clack/prompts";
13
13
 
14
14
  import { EnvService } from "../../services/env/index.js";
15
15
  import { CLIError, ExitCode } from "../../utils/errors.js";
16
- import {
17
- formatSuccess,
18
- formatError,
19
- formatJson,
20
- } from "../../utils/formatters.js";
16
+ import { formatSuccess, formatJson } from "../../utils/formatters.js";
17
+ import { withErrorHandling } from "../../utils/error-handler.js";
21
18
  import type { FormatOptions } from "../../utils/formatters.js";
22
19
 
23
20
  // ---------------------------------------------------------------------------
@@ -50,7 +47,7 @@ async function handleInit(opts: FormatOptions): Promise<void> {
50
47
  });
51
48
  if (p.isCancel(overwrite)) {
52
49
  p.cancel("Setup cancelled.");
53
- process.exit(0);
50
+ process.exitCode = 0;
54
51
  }
55
52
  if (!overwrite) {
56
53
  p.outro("Setup cancelled. Existing .env.local preserved.");
@@ -89,7 +86,7 @@ async function handleInit(opts: FormatOptions): Promise<void> {
89
86
  }
90
87
  if (p.isCancel(value)) {
91
88
  p.cancel("Setup cancelled.");
92
- process.exit(0);
89
+ process.exitCode = 0;
93
90
  }
94
91
  collected[def.name] = typeof value === "string" ? value : "";
95
92
  }
@@ -244,52 +241,52 @@ EXAMPLES:
244
241
  envCmd
245
242
  .command("init")
246
243
  .description("Interactive wizard to generate .env.local and .dev.vars")
247
- .action(async (_, cmd: Command) => {
248
- const opts = formatOpts(cmd);
249
- try {
250
- await handleInit(opts);
251
- } catch (err) {
252
- formatError(err instanceof Error ? err : String(err), opts);
253
- process.exit(err instanceof CLIError ? err.code : ExitCode.ERROR);
254
- }
255
- });
244
+ .action(
245
+ withErrorHandling(
246
+ async (_, cmd: Command) => {
247
+ const opts = formatOpts(cmd);
248
+ await handleInit(opts);
249
+ },
250
+ { service: "env" }
251
+ )
252
+ );
256
253
 
257
254
  envCmd
258
255
  .command("show")
259
256
  .description("Display current .env.local (secrets redacted)")
260
- .action(async (_, cmd: Command) => {
261
- const opts = formatOpts(cmd);
262
- try {
263
- await handleShow(opts);
264
- } catch (err) {
265
- formatError(err instanceof Error ? err : String(err), opts);
266
- process.exit(err instanceof CLIError ? err.code : ExitCode.ERROR);
267
- }
268
- });
257
+ .action(
258
+ withErrorHandling(
259
+ async (_, cmd: Command) => {
260
+ const opts = formatOpts(cmd);
261
+ await handleShow(opts);
262
+ },
263
+ { service: "env" }
264
+ )
265
+ );
269
266
 
270
267
  envCmd
271
268
  .command("validate")
272
269
  .description("Check required environment variables")
273
- .action(async (_, cmd: Command) => {
274
- const opts = formatOpts(cmd);
275
- try {
276
- await handleValidate(opts);
277
- } catch (err) {
278
- formatError(err instanceof Error ? err : String(err), opts);
279
- process.exit(err instanceof CLIError ? err.code : ExitCode.ERROR);
280
- }
281
- });
270
+ .action(
271
+ withErrorHandling(
272
+ async (_, cmd: Command) => {
273
+ const opts = formatOpts(cmd);
274
+ await handleValidate(opts);
275
+ },
276
+ { service: "env" }
277
+ )
278
+ );
282
279
 
283
280
  envCmd
284
281
  .command("generate-dev-vars")
285
282
  .description("Create per-worker .dev.vars from .env.local")
286
- .action(async (_, cmd: Command) => {
287
- const opts = formatOpts(cmd);
288
- try {
289
- await handleGenerateDevVars(opts);
290
- } catch (err) {
291
- formatError(err instanceof Error ? err : String(err), opts);
292
- process.exit(err instanceof CLIError ? err.code : ExitCode.ERROR);
293
- }
294
- });
283
+ .action(
284
+ withErrorHandling(
285
+ async (_, cmd: Command) => {
286
+ const opts = formatOpts(cmd);
287
+ await handleGenerateDevVars(opts);
288
+ },
289
+ { service: "env" }
290
+ )
291
+ );
295
292
  }
@@ -14,11 +14,11 @@ import { Command } from "commander";
14
14
  import { KvSyncService } from "../../services/kv/index.js";
15
15
  import {
16
16
  formatSuccess,
17
- formatError,
18
17
  formatTable,
19
18
  formatJson,
20
19
  } from "../../utils/formatters.js";
21
20
  import { CLIError, ExitCode } from "../../utils/errors.js";
21
+ import { withErrorHandling } from "../../utils/error-handler.js";
22
22
  import { theme } from "../../utils/theme.js";
23
23
  import type { FormatOptions } from "../../utils/formatters.js";
24
24
 
@@ -203,88 +203,88 @@ EXAMPLES:
203
203
  kvCmd
204
204
  .command("list")
205
205
  .description("List all keys in the KV namespace")
206
- .action(async (_, cmd: Command) => {
207
- const opts = formatOpts(cmd);
208
- try {
209
- const nsId = await resolveNs(cmd);
210
- await handleList(opts, nsId);
211
- } catch (err) {
212
- formatError(err instanceof Error ? err : String(err), opts);
213
- process.exit(err instanceof CLIError ? err.code : ExitCode.ERROR);
214
- }
215
- });
206
+ .action(
207
+ withErrorHandling(
208
+ async (_, cmd: Command) => {
209
+ const opts = formatOpts(cmd);
210
+ const nsId = await resolveNs(cmd);
211
+ await handleList(opts, nsId);
212
+ },
213
+ { service: "kv" }
214
+ )
215
+ );
216
216
 
217
217
  // -- get
218
218
  kvCmd
219
219
  .command("get <key>")
220
220
  .description("Get a key's value from the KV namespace")
221
- .action(async (key: string, _, cmd: Command) => {
222
- const opts = formatOpts(cmd);
223
- try {
224
- const nsId = await resolveNs(cmd);
225
- await handleGet(opts, nsId, key);
226
- } catch (err) {
227
- formatError(err instanceof Error ? err : String(err), opts);
228
- process.exit(err instanceof CLIError ? err.code : ExitCode.ERROR);
229
- }
230
- });
221
+ .action(
222
+ withErrorHandling(
223
+ async (key: string, _, cmd: Command) => {
224
+ const opts = formatOpts(cmd);
225
+ const nsId = await resolveNs(cmd);
226
+ await handleGet(opts, nsId, key);
227
+ },
228
+ { service: "kv" }
229
+ )
230
+ );
231
231
 
232
232
  // -- set
233
233
  kvCmd
234
234
  .command("set <key> <value>")
235
235
  .description("Set a key's value in the KV namespace")
236
- .action(async (key: string, value: string, _, cmd: Command) => {
237
- const opts = formatOpts(cmd);
238
- try {
239
- const nsId = await resolveNs(cmd);
240
- await handleSet(opts, nsId, key, value);
241
- } catch (err) {
242
- formatError(err instanceof Error ? err : String(err), opts);
243
- process.exit(err instanceof CLIError ? err.code : ExitCode.ERROR);
244
- }
245
- });
236
+ .action(
237
+ withErrorHandling(
238
+ async (key: string, value: string, _, cmd: Command) => {
239
+ const opts = formatOpts(cmd);
240
+ const nsId = await resolveNs(cmd);
241
+ await handleSet(opts, nsId, key, value);
242
+ },
243
+ { service: "kv" }
244
+ )
245
+ );
246
246
 
247
247
  // -- delete
248
248
  kvCmd
249
249
  .command("delete <key>")
250
250
  .description("Delete a key from the KV namespace")
251
- .action(async (key: string, _, cmd: Command) => {
252
- const opts = formatOpts(cmd);
253
- try {
254
- const nsId = await resolveNs(cmd);
255
- await handleDelete(opts, nsId, key);
256
- } catch (err) {
257
- formatError(err instanceof Error ? err : String(err), opts);
258
- process.exit(err instanceof CLIError ? err.code : ExitCode.ERROR);
259
- }
260
- });
251
+ .action(
252
+ withErrorHandling(
253
+ async (key: string, _, cmd: Command) => {
254
+ const opts = formatOpts(cmd);
255
+ const nsId = await resolveNs(cmd);
256
+ await handleDelete(opts, nsId, key);
257
+ },
258
+ { service: "kv" }
259
+ )
260
+ );
261
261
 
262
262
  // -- apply-manifest
263
263
  kvCmd
264
264
  .command("apply-manifest")
265
265
  .description("Apply manifest key defaults to the KV namespace")
266
- .action(async (_, cmd: Command) => {
267
- const opts = formatOpts(cmd);
268
- try {
269
- const nsId = await resolveNs(cmd);
270
- await handleApplyManifest(opts, nsId);
271
- } catch (err) {
272
- formatError(err instanceof Error ? err : String(err), opts);
273
- process.exit(err instanceof CLIError ? err.code : ExitCode.ERROR);
274
- }
275
- });
266
+ .action(
267
+ withErrorHandling(
268
+ async (_, cmd: Command) => {
269
+ const opts = formatOpts(cmd);
270
+ const nsId = await resolveNs(cmd);
271
+ await handleApplyManifest(opts, nsId);
272
+ },
273
+ { service: "kv" }
274
+ )
275
+ );
276
276
 
277
277
  // -- manifest
278
278
  kvCmd
279
279
  .command("manifest")
280
280
  .description("Show expected KV keys from manifest")
281
- .action(async (_, cmd: Command) => {
282
- const opts = formatOpts(cmd);
283
- try {
284
- await handleManifest(opts);
285
- } catch (err) {
286
- formatError(err instanceof Error ? err : String(err), opts);
287
- process.exit(err instanceof CLIError ? err.code : ExitCode.ERROR);
288
- }
289
- });
281
+ .action(
282
+ withErrorHandling(
283
+ async (_, cmd: Command) => {
284
+ const opts = formatOpts(cmd);
285
+ await handleManifest(opts);
286
+ },
287
+ { service: "kv" }
288
+ )
289
+ );
290
290
  }