@jango-blockchained/hoox-cli 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +403 -0
  2. package/bin/hoox.js +12 -0
  3. package/package.json +60 -0
  4. package/src/commands/check/check-command.test.ts +468 -0
  5. package/src/commands/check/check-command.ts +1144 -0
  6. package/src/commands/check/index.ts +10 -0
  7. package/src/commands/check/prerequisites-command.test.ts +19 -0
  8. package/src/commands/check/prerequisites-command.ts +92 -0
  9. package/src/commands/check/types.ts +103 -0
  10. package/src/commands/clone/clone-command.test.ts +442 -0
  11. package/src/commands/clone/clone-command.ts +440 -0
  12. package/src/commands/clone/index.ts +1 -0
  13. package/src/commands/config/config-command.test.ts +583 -0
  14. package/src/commands/config/config-command.ts +901 -0
  15. package/src/commands/config/env-command.test.ts +43 -0
  16. package/src/commands/config/env-command.ts +314 -0
  17. package/src/commands/config/index.ts +3 -0
  18. package/src/commands/config/kv-command.test.ts +14 -0
  19. package/src/commands/config/kv-command.ts +329 -0
  20. package/src/commands/dashboard/dashboard-command.test.ts +47 -0
  21. package/src/commands/dashboard/dashboard-command.ts +127 -0
  22. package/src/commands/dashboard/index.ts +1 -0
  23. package/src/commands/db/db-command.test.ts +21 -0
  24. package/src/commands/db/db-command.ts +314 -0
  25. package/src/commands/db/index.ts +1 -0
  26. package/src/commands/deploy/deploy-command.test.ts +304 -0
  27. package/src/commands/deploy/deploy-command.ts +1053 -0
  28. package/src/commands/deploy/index.ts +2 -0
  29. package/src/commands/deploy/telegram-service.ts +61 -0
  30. package/src/commands/deploy/types.ts +34 -0
  31. package/src/commands/dev/dev-command.test.ts +383 -0
  32. package/src/commands/dev/dev-command.ts +407 -0
  33. package/src/commands/dev/index.ts +1 -0
  34. package/src/commands/infra/index.ts +5 -0
  35. package/src/commands/infra/infra-command.test.ts +719 -0
  36. package/src/commands/infra/infra-command.ts +940 -0
  37. package/src/commands/infra/types.ts +23 -0
  38. package/src/commands/init/index.ts +1 -0
  39. package/src/commands/init/init-command.test.ts +827 -0
  40. package/src/commands/init/init-command.ts +627 -0
  41. package/src/commands/init/types.ts +185 -0
  42. package/src/commands/monitor/index.ts +2 -0
  43. package/src/commands/monitor/monitor-command.test.ts +235 -0
  44. package/src/commands/monitor/monitor-command.ts +245 -0
  45. package/src/commands/monitor/monitor-service.ts +50 -0
  46. package/src/commands/monitor/types.ts +13 -0
  47. package/src/commands/repair/index.ts +2 -0
  48. package/src/commands/repair/repair-command.test.ts +204 -0
  49. package/src/commands/repair/repair-command.ts +199 -0
  50. package/src/commands/repair/repair-service.ts +102 -0
  51. package/src/commands/repair/types.ts +13 -0
  52. package/src/commands/test/index.ts +2 -0
  53. package/src/commands/test/test-command.test.ts +319 -0
  54. package/src/commands/test/test-command.ts +412 -0
  55. package/src/commands/waf/index.ts +2 -0
  56. package/src/commands/waf/types.ts +48 -0
  57. package/src/commands/waf/waf-command.test.ts +506 -0
  58. package/src/commands/waf/waf-command.ts +548 -0
  59. package/src/index.ts +198 -0
  60. package/src/services/cloudflare/cloudflare-service.test.ts +654 -0
  61. package/src/services/cloudflare/cloudflare-service.ts +435 -0
  62. package/src/services/cloudflare/index.ts +2 -0
  63. package/src/services/cloudflare/types.ts +29 -0
  64. package/src/services/config/config-service.test.ts +395 -0
  65. package/src/services/config/config-service.ts +207 -0
  66. package/src/services/config/index.ts +9 -0
  67. package/src/services/config/types.ts +66 -0
  68. package/src/services/db/db-service.test.ts +51 -0
  69. package/src/services/db/db-service.ts +140 -0
  70. package/src/services/db/index.ts +1 -0
  71. package/src/services/docker/docker-service.ts +155 -0
  72. package/src/services/docker/index.ts +1 -0
  73. package/src/services/env/env-service.test.ts +210 -0
  74. package/src/services/env/env-service.ts +156 -0
  75. package/src/services/env/index.ts +1 -0
  76. package/src/services/kv/index.ts +1 -0
  77. package/src/services/kv/kv-sync-service.test.ts +38 -0
  78. package/src/services/kv/kv-sync-service.ts +151 -0
  79. package/src/services/prerequisites/index.ts +1 -0
  80. package/src/services/prerequisites/prerequisites-service.test.ts +89 -0
  81. package/src/services/prerequisites/prerequisites-service.ts +269 -0
  82. package/src/services/prerequisites/types.ts +48 -0
  83. package/src/services/secrets/index.ts +12 -0
  84. package/src/services/secrets/secrets-service.test.ts +486 -0
  85. package/src/services/secrets/secrets-service.ts +293 -0
  86. package/src/services/secrets/types.ts +57 -0
  87. package/src/ui/banner.ts +52 -0
  88. package/src/ui/index.ts +8 -0
  89. package/src/ui/menu.ts +473 -0
  90. package/src/utils/errors.test.ts +69 -0
  91. package/src/utils/errors.ts +23 -0
  92. package/src/utils/formatters.test.ts +180 -0
  93. package/src/utils/formatters.ts +252 -0
  94. package/src/utils/theme.ts +94 -0
@@ -0,0 +1,43 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { EnvService } from "../../services/env/index.js";
3
+
4
+ describe("env command", () => {
5
+ describe("EnvService integration", () => {
6
+ it("validate catches missing required vars", () => {
7
+ const result = EnvService.validate({});
8
+ expect(result.missing.length).toBeGreaterThan(0);
9
+ expect(result.missing).toContain("CLOUDFLARE_API_TOKEN");
10
+ });
11
+
12
+ it("validate passes with all required vars", () => {
13
+ const vars: Record<string, string> = {
14
+ CLOUDFLARE_API_TOKEN: "cfut_xxx",
15
+ CLOUDFLARE_ACCOUNT_ID: "abc123",
16
+ SUBDOMAIN_PREFIX: "myapp",
17
+ D1_INTERNAL_KEY: "d1-key",
18
+ TRADE_INTERNAL_KEY: "trade-key",
19
+ AGENT_INTERNAL_KEY: "agent-key",
20
+ WEBHOOK_API_KEY_BINDING: "webhook-key",
21
+ INTERNAL_KEY_BINDING: "inter-key",
22
+ API_SERVICE_KEY: "api-key",
23
+ DASHBOARD_USER: "admin",
24
+ DASHBOARD_PASS: "pass123",
25
+ SESSION_SECRET: "a".repeat(32),
26
+ };
27
+ const result = EnvService.validate(vars);
28
+ expect(result.missing.length).toBe(0);
29
+ });
30
+
31
+ it("generateEnvLocal produces valid output", () => {
32
+ const content = EnvService.generateEnvLocal();
33
+ expect(content).toContain("CLOUDFLARE_API_TOKEN");
34
+ expect(content).toContain("# NEVER commit this file");
35
+ });
36
+
37
+ it("show redacts secrets", () => {
38
+ const output = EnvService.show({ CLOUDFLARE_API_TOKEN: "s3kr3t" });
39
+ expect(output).toContain("********");
40
+ expect(output).not.toContain("s3kr3t");
41
+ });
42
+ });
43
+ });
@@ -0,0 +1,314 @@
1
+ /**
2
+ * `hoox config env` subcommand group — environment variable management.
3
+ *
4
+ * Subcommands:
5
+ * init — Interactive wizard to generate .env.local and .dev.vars
6
+ * show — Display current .env.local (secrets redacted)
7
+ * validate — Check required vars are set
8
+ * generate-dev-vars — Create/update per-worker .dev.vars from .env.local
9
+ */
10
+
11
+ import { Command } from "commander";
12
+ import * as p from "@clack/prompts";
13
+
14
+ import { EnvService } from "../../services/env/index.js";
15
+ import { CLIError, ExitCode } from "../../utils/errors.js";
16
+ import {
17
+ formatSuccess,
18
+ formatError,
19
+ formatJson,
20
+ } from "../../utils/formatters.js";
21
+ import type { FormatOptions } from "../../utils/formatters.js";
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Helpers
25
+ // ---------------------------------------------------------------------------
26
+
27
+ /**
28
+ * Get FormatOptions from the command opts using optsWithGlobals() so that
29
+ * globally defined flags (--json, --quiet) are resolved correctly even when
30
+ * called from deeply nested subcommands.
31
+ */
32
+ function formatOpts(cmd: Command): FormatOptions {
33
+ const opts = cmd.optsWithGlobals<{ json?: boolean; quiet?: boolean }>();
34
+ return { json: Boolean(opts.json), quiet: Boolean(opts.quiet) };
35
+ }
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // env init — interactive wizard
39
+ // ---------------------------------------------------------------------------
40
+
41
+ async function handleInit(opts: FormatOptions): Promise<void> {
42
+ p.intro("Environment Setup");
43
+
44
+ // Warn if .env.local already exists
45
+ const existingEnv = Bun.file(".env.local");
46
+ if (await existingEnv.exists()) {
47
+ const overwrite = await p.confirm({
48
+ message: ".env.local already exists. Overwrite?",
49
+ initialValue: false,
50
+ });
51
+ if (p.isCancel(overwrite)) {
52
+ p.cancel("Setup cancelled.");
53
+ process.exit(0);
54
+ }
55
+ if (!overwrite) {
56
+ p.outro("Setup cancelled. Existing .env.local preserved.");
57
+ return;
58
+ }
59
+ }
60
+
61
+ const collected: Record<string, string> = {};
62
+ const sections = EnvService.getSections();
63
+
64
+ for (const section of sections) {
65
+ const sectionDefs = EnvService.getDefinitions().filter(
66
+ (d) => d.section === section,
67
+ );
68
+ if (sectionDefs.length === 0) continue;
69
+
70
+ p.log.step(`Configuring ${section}...`);
71
+
72
+ for (const def of sectionDefs) {
73
+ let value: string | symbol;
74
+ if (def.secret) {
75
+ value = await p.password({
76
+ message: `${def.name}:${def.hint ? ` (${def.hint})` : ""}`,
77
+ validate: def.required
78
+ ? (v: string | undefined) => (v ? undefined : "Required")
79
+ : undefined,
80
+ });
81
+ } else {
82
+ value = await p.text({
83
+ message: `${def.name}:${def.hint ? ` (${def.hint})` : ""}`,
84
+ defaultValue: def.default ?? "",
85
+ validate: def.required
86
+ ? (v: string | undefined) => (v ? undefined : "Required")
87
+ : undefined,
88
+ });
89
+ }
90
+ if (p.isCancel(value)) {
91
+ p.cancel("Setup cancelled.");
92
+ process.exit(0);
93
+ }
94
+ collected[def.name] = typeof value === "string" ? value : "";
95
+ }
96
+ }
97
+
98
+ p.log.step("Writing .env.local...");
99
+ const envContent = EnvService.generateEnvLocal(collected);
100
+ await Bun.write(".env.local", envContent);
101
+ if (!opts.quiet) formatSuccess(".env.local written", opts);
102
+
103
+ p.log.step("Writing .dev.vars files...");
104
+ const devVars = EnvService.getWorkerDevVars(collected);
105
+ for (const [workerPath, vars] of Object.entries(devVars)) {
106
+ const content = [
107
+ `# .dev.vars - local secrets for ${workerPath}`,
108
+ "# Generated by `hoox config env init`. NEVER commit this file.",
109
+ "",
110
+ ...Object.entries(vars).map(([k, v]) => `${k}=${v}`),
111
+ "",
112
+ ].join("\n");
113
+ await Bun.write(`${workerPath}/.dev.vars`, content);
114
+ if (!opts.quiet)
115
+ formatSuccess(`Created ${workerPath}/.dev.vars`, opts);
116
+ }
117
+
118
+ p.outro("Environment setup complete!");
119
+ }
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // env show
123
+ // ---------------------------------------------------------------------------
124
+
125
+ async function handleShow(opts: FormatOptions): Promise<void> {
126
+ const file = Bun.file(".env.local");
127
+ if (!(await file.exists())) {
128
+ throw new CLIError(
129
+ ".env.local not found. Run `hoox config env init`.",
130
+ ExitCode.INVALID_USAGE,
131
+ );
132
+ }
133
+
134
+ const vars = await EnvService.loadDotEnvAsync(".env.local");
135
+
136
+ if (opts.json) {
137
+ formatJson(vars, opts);
138
+ } else {
139
+ process.stdout.write(EnvService.show(vars));
140
+ }
141
+ }
142
+
143
+ // ---------------------------------------------------------------------------
144
+ // env validate
145
+ // ---------------------------------------------------------------------------
146
+
147
+ async function handleValidate(opts: FormatOptions): Promise<void> {
148
+ let vars: Record<string, string> = {};
149
+ const file = Bun.file(".env.local");
150
+ if (await file.exists()) {
151
+ vars = await EnvService.loadDotEnvAsync(".env.local");
152
+ }
153
+
154
+ const result = EnvService.validate(vars);
155
+
156
+ if (opts.json) {
157
+ formatJson(result, opts);
158
+ } else if (!opts.quiet) {
159
+ if (!(await file.exists())) {
160
+ process.stdout.write(".env.local not found. Run `hoox config env init` first.\n\n");
161
+ }
162
+ if (result.missing.length > 0) {
163
+ process.stdout.write("Missing required variables:\n");
164
+ for (const v of result.missing) {
165
+ process.stdout.write(` ${v}\n`);
166
+ }
167
+ }
168
+ if (result.warnings.length > 0) {
169
+ process.stdout.write("Warnings:\n");
170
+ for (const w of result.warnings) {
171
+ process.stdout.write(` ${w}\n`);
172
+ }
173
+ }
174
+ if (result.missing.length === 0 && result.warnings.length === 0) {
175
+ formatSuccess("All required environment variables are set", opts);
176
+ }
177
+ }
178
+
179
+ if (result.missing.length > 0) {
180
+ process.exitCode = ExitCode.ERROR;
181
+ }
182
+ }
183
+
184
+ // ---------------------------------------------------------------------------
185
+ // env generate-dev-vars
186
+ // ---------------------------------------------------------------------------
187
+
188
+ async function handleGenerateDevVars(opts: FormatOptions): Promise<void> {
189
+ const file = Bun.file(".env.local");
190
+ if (!(await file.exists())) {
191
+ throw new CLIError(
192
+ ".env.local not found. Run `hoox config env init`.",
193
+ ExitCode.INVALID_USAGE,
194
+ );
195
+ }
196
+
197
+ const vars = await EnvService.loadDotEnvAsync(".env.local");
198
+ const devVars = EnvService.getWorkerDevVars(vars);
199
+
200
+ let count = 0;
201
+ for (const [workerPath, workerVars] of Object.entries(devVars)) {
202
+ const content = [
203
+ `# .dev.vars - local secrets for ${workerPath}`,
204
+ "# Generated by `hoox config env generate-dev-vars`. NEVER commit this file.",
205
+ "",
206
+ ...Object.entries(workerVars).map(([k, v]) => `${k}=${v}`),
207
+ "",
208
+ ].join("\n");
209
+ await Bun.write(`${workerPath}/.dev.vars`, content);
210
+ count++;
211
+ }
212
+
213
+ formatSuccess(`Generated .dev.vars for ${count} worker(s)`, opts);
214
+ }
215
+
216
+ // ---------------------------------------------------------------------------
217
+ // Registration
218
+ // ---------------------------------------------------------------------------
219
+
220
+ /**
221
+ * Register the `hoox config env` subcommand group.
222
+ */
223
+ export function registerEnvCommand(parentCmd: Command): void {
224
+ const envCmd = parentCmd
225
+ .command("env")
226
+ .summary("Manage environment variables")
227
+ .description(
228
+ `Manage environment variables for the Hoox trading system.
229
+
230
+ SUBCOMMANDS:
231
+ init Interactive wizard to generate .env.local and .dev.vars
232
+ show Display current .env.local (secrets redacted)
233
+ validate Check required environment variables
234
+ generate-dev-vars Create per-worker .dev.vars from .env.local
235
+
236
+ EXAMPLES:
237
+ hoox config env init
238
+ hoox config env show
239
+ hoox config env validate
240
+ hoox config env generate-dev-vars`,
241
+ );
242
+
243
+ envCmd
244
+ .command("init")
245
+ .description("Interactive wizard to generate .env.local and .dev.vars")
246
+ .action(async (_, cmd: Command) => {
247
+ const opts = formatOpts(cmd);
248
+ try {
249
+ await handleInit(opts);
250
+ } catch (err) {
251
+ formatError(
252
+ err instanceof Error ? err : String(err),
253
+ opts,
254
+ );
255
+ process.exit(
256
+ err instanceof CLIError ? err.code : ExitCode.ERROR,
257
+ );
258
+ }
259
+ });
260
+
261
+ envCmd
262
+ .command("show")
263
+ .description("Display current .env.local (secrets redacted)")
264
+ .action(async (_, cmd: Command) => {
265
+ const opts = formatOpts(cmd);
266
+ try {
267
+ await handleShow(opts);
268
+ } catch (err) {
269
+ formatError(
270
+ err instanceof Error ? err : String(err),
271
+ opts,
272
+ );
273
+ process.exit(
274
+ err instanceof CLIError ? err.code : ExitCode.ERROR,
275
+ );
276
+ }
277
+ });
278
+
279
+ envCmd
280
+ .command("validate")
281
+ .description("Check required environment variables")
282
+ .action(async (_, cmd: Command) => {
283
+ const opts = formatOpts(cmd);
284
+ try {
285
+ await handleValidate(opts);
286
+ } catch (err) {
287
+ formatError(
288
+ err instanceof Error ? err : String(err),
289
+ opts,
290
+ );
291
+ process.exit(
292
+ err instanceof CLIError ? err.code : ExitCode.ERROR,
293
+ );
294
+ }
295
+ });
296
+
297
+ envCmd
298
+ .command("generate-dev-vars")
299
+ .description("Create per-worker .dev.vars from .env.local")
300
+ .action(async (_, cmd: Command) => {
301
+ const opts = formatOpts(cmd);
302
+ try {
303
+ await handleGenerateDevVars(opts);
304
+ } catch (err) {
305
+ formatError(
306
+ err instanceof Error ? err : String(err),
307
+ opts,
308
+ );
309
+ process.exit(
310
+ err instanceof CLIError ? err.code : ExitCode.ERROR,
311
+ );
312
+ }
313
+ });
314
+ }
@@ -0,0 +1,3 @@
1
+ export { registerConfigCommand } from "./config-command.js";
2
+ export { registerEnvCommand } from "./env-command.js";
3
+ export { registerKvCommand } from "./kv-command.js";
@@ -0,0 +1,14 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { KvSyncService } from "../../services/kv/index.js";
3
+
4
+ describe("kv command", () => {
5
+ describe("manifest", () => {
6
+ it("returns known KV keys from manifest", () => {
7
+ const keys = KvSyncService.getManifestKeys();
8
+ expect(keys.length).toBe(16);
9
+ expect(keys.some((k) => k.key === "trade:kill_switch")).toBe(true);
10
+ expect(keys.some((k) => k.key === "agent:openai_key")).toBe(true);
11
+ expect(keys.some((k) => k.key === "email:use_imap")).toBe(true);
12
+ });
13
+ });
14
+ });
@@ -0,0 +1,329 @@
1
+ /**
2
+ * `hoox config kv` subcommand group — KV namespace key management.
3
+ *
4
+ * Subcommands:
5
+ * list — List all keys
6
+ * get <key> — Get a key's value
7
+ * set <key> <value> — Set a key's value
8
+ * delete <key> — Delete a key
9
+ * apply-manifest — Apply manifest defaults
10
+ * manifest — Show expected manifest keys
11
+ */
12
+
13
+ import { Command } from "commander";
14
+ import { KvSyncService } from "../../services/kv/index.js";
15
+ import {
16
+ formatSuccess,
17
+ formatError,
18
+ formatTable,
19
+ formatJson,
20
+ } from "../../utils/formatters.js";
21
+ import { CLIError, ExitCode } from "../../utils/errors.js";
22
+ import { theme } from "../../utils/theme.js";
23
+ import type { FormatOptions } from "../../utils/formatters.js";
24
+
25
+ function formatOpts(cmd: Command): FormatOptions {
26
+ const opts = cmd.optsWithGlobals<{ json?: boolean; quiet?: boolean }>();
27
+ return { json: Boolean(opts.json), quiet: Boolean(opts.quiet) };
28
+ }
29
+
30
+ async function resolveNs(
31
+ cmd: Command,
32
+ ): Promise<string> {
33
+ const svc = new KvSyncService();
34
+ const opts = cmd.optsWithGlobals<{ namespaceId?: string }>();
35
+ return await svc.resolveNamespaceId(opts.namespaceId);
36
+ }
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // kv list
40
+ // ---------------------------------------------------------------------------
41
+
42
+ async function handleList(
43
+ opts: FormatOptions,
44
+ nsId: string,
45
+ ): Promise<void> {
46
+ const svc = new KvSyncService();
47
+ const keys = await svc.list(nsId);
48
+
49
+ if (opts.json) {
50
+ formatJson(keys, opts);
51
+ } else if (!opts.quiet) {
52
+ if (keys.length === 0) {
53
+ process.stdout.write("No keys found in KV namespace.\n");
54
+ return;
55
+ }
56
+ const rows = keys.map((k) => ({ Key: k.name }));
57
+ formatTable(rows, opts);
58
+ }
59
+ }
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // kv get
63
+ // ---------------------------------------------------------------------------
64
+
65
+ async function handleGet(
66
+ opts: FormatOptions,
67
+ nsId: string,
68
+ key: string,
69
+ ): Promise<void> {
70
+ const svc = new KvSyncService();
71
+ const value = await svc.get(nsId, key);
72
+
73
+ if (value === null) {
74
+ throw new CLIError(
75
+ `Key "${key}" not found in KV namespace`,
76
+ ExitCode.ERROR,
77
+ );
78
+ }
79
+
80
+ if (opts.json) {
81
+ formatJson({ key, value }, opts);
82
+ } else {
83
+ process.stdout.write(`${value}\n`);
84
+ }
85
+ }
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // kv set
89
+ // ---------------------------------------------------------------------------
90
+
91
+ async function handleSet(
92
+ opts: FormatOptions,
93
+ nsId: string,
94
+ key: string,
95
+ value: string,
96
+ ): Promise<void> {
97
+ const svc = new KvSyncService();
98
+ await svc.set(nsId, key, value);
99
+ formatSuccess(`Set "${key}" in KV namespace`, opts);
100
+ }
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // kv delete
104
+ // ---------------------------------------------------------------------------
105
+
106
+ async function handleDelete(
107
+ opts: FormatOptions,
108
+ nsId: string,
109
+ key: string,
110
+ ): Promise<void> {
111
+ const svc = new KvSyncService();
112
+ await svc.delete(nsId, key);
113
+ formatSuccess(`Deleted "${key}" from KV namespace`, opts);
114
+ }
115
+
116
+ // ---------------------------------------------------------------------------
117
+ // kv apply-manifest
118
+ // ---------------------------------------------------------------------------
119
+
120
+ async function handleApplyManifest(
121
+ opts: FormatOptions,
122
+ nsId: string,
123
+ ): Promise<void> {
124
+ const svc = new KvSyncService();
125
+ const manifest = KvSyncService.getManifest();
126
+ let setCount = 0;
127
+ const errors: string[] = [];
128
+
129
+ for (const keyDef of manifest.keys) {
130
+ if (keyDef.default !== undefined && keyDef.default !== "") {
131
+ try {
132
+ await svc.set(nsId, keyDef.key, keyDef.default);
133
+ setCount++;
134
+ } catch (err) {
135
+ errors.push(
136
+ `${keyDef.key}: ${err instanceof Error ? err.message : String(err)}`,
137
+ );
138
+ }
139
+ }
140
+ }
141
+
142
+ formatSuccess(
143
+ `Applied ${setCount}/${manifest.keys.length} manifest keys (${errors.length} errors)`,
144
+ opts,
145
+ );
146
+ }
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // kv manifest
150
+ // ---------------------------------------------------------------------------
151
+
152
+ async function handleManifest(opts: FormatOptions): Promise<void> {
153
+ const manifest = KvSyncService.getManifest();
154
+
155
+ if (opts.json) {
156
+ formatJson(manifest, opts);
157
+ } else if (!opts.quiet) {
158
+ process.stdout.write(
159
+ `${theme.heading(`KV Manifest: ${manifest.namespace}`)}\n\n`,
160
+ );
161
+ const rows = manifest.keys.map((k) => ({
162
+ Key: k.key,
163
+ Type: k.type,
164
+ Default: k.default || "-",
165
+ Secret: k.secret ? "yes" : "no",
166
+ Description: k.description,
167
+ }));
168
+ formatTable(rows, opts);
169
+ }
170
+ }
171
+
172
+ // ---------------------------------------------------------------------------
173
+ // Registration
174
+ // ---------------------------------------------------------------------------
175
+
176
+ export function registerKvCommand(parentCmd: Command): void {
177
+ const kvCmd = parentCmd
178
+ .command("kv")
179
+ .summary("Manage KV namespace keys")
180
+ .description(
181
+ `Manage Cloudflare KV namespace keys and apply the key manifest.
182
+
183
+ SUBCOMMANDS:
184
+ list List all keys in the KV namespace
185
+ get <key> Get a key's value
186
+ set <key> <value> Set a key's value
187
+ delete <key> Delete a key
188
+ apply-manifest Apply manifest key defaults to KV
189
+ manifest Show expected KV keys from manifest
190
+
191
+ OPTIONS:
192
+ --namespace-id <id> KV namespace ID (auto-detected from wrangler if omitted)
193
+
194
+ EXAMPLES:
195
+ hoox config kv list
196
+ hoox config kv set trade:kill_switch true
197
+ hoox config kv get trade:kill_switch
198
+ hoox config kv delete trade:kill_switch
199
+ hoox config kv apply-manifest
200
+ hoox config kv manifest`,
201
+ )
202
+ .option(
203
+ "--namespace-id <id>",
204
+ "KV namespace ID (auto-detected if omitted)",
205
+ );
206
+
207
+ // -- list
208
+ kvCmd
209
+ .command("list")
210
+ .description("List all keys in the KV namespace")
211
+ .action(async (_, cmd: Command) => {
212
+ const opts = formatOpts(cmd);
213
+ try {
214
+ const nsId = await resolveNs(cmd);
215
+ await handleList(opts, nsId);
216
+ } catch (err) {
217
+ formatError(
218
+ err instanceof Error ? err : String(err),
219
+ opts,
220
+ );
221
+ process.exit(
222
+ err instanceof CLIError ? err.code : ExitCode.ERROR,
223
+ );
224
+ }
225
+ });
226
+
227
+ // -- get
228
+ kvCmd
229
+ .command("get <key>")
230
+ .description("Get a key's value from the KV namespace")
231
+ .action(async (key: string, _, cmd: Command) => {
232
+ const opts = formatOpts(cmd);
233
+ try {
234
+ const nsId = await resolveNs(cmd);
235
+ await handleGet(opts, nsId, key);
236
+ } catch (err) {
237
+ formatError(
238
+ err instanceof Error ? err : String(err),
239
+ opts,
240
+ );
241
+ process.exit(
242
+ err instanceof CLIError ? err.code : ExitCode.ERROR,
243
+ );
244
+ }
245
+ });
246
+
247
+ // -- set
248
+ kvCmd
249
+ .command("set <key> <value>")
250
+ .description("Set a key's value in the KV namespace")
251
+ .action(
252
+ async (key: string, value: string, _, cmd: Command) => {
253
+ const opts = formatOpts(cmd);
254
+ try {
255
+ const nsId = await resolveNs(cmd);
256
+ await handleSet(opts, nsId, key, value);
257
+ } catch (err) {
258
+ formatError(
259
+ err instanceof Error ? err : String(err),
260
+ opts,
261
+ );
262
+ process.exit(
263
+ err instanceof CLIError ? err.code : ExitCode.ERROR,
264
+ );
265
+ }
266
+ },
267
+ );
268
+
269
+ // -- delete
270
+ kvCmd
271
+ .command("delete <key>")
272
+ .description("Delete a key from the KV namespace")
273
+ .action(async (key: string, _, cmd: Command) => {
274
+ const opts = formatOpts(cmd);
275
+ try {
276
+ const nsId = await resolveNs(cmd);
277
+ await handleDelete(opts, nsId, key);
278
+ } catch (err) {
279
+ formatError(
280
+ err instanceof Error ? err : String(err),
281
+ opts,
282
+ );
283
+ process.exit(
284
+ err instanceof CLIError ? err.code : ExitCode.ERROR,
285
+ );
286
+ }
287
+ });
288
+
289
+ // -- apply-manifest
290
+ kvCmd
291
+ .command("apply-manifest")
292
+ .description(
293
+ "Apply manifest key defaults to the KV namespace",
294
+ )
295
+ .action(async (_, cmd: Command) => {
296
+ const opts = formatOpts(cmd);
297
+ try {
298
+ const nsId = await resolveNs(cmd);
299
+ await handleApplyManifest(opts, nsId);
300
+ } catch (err) {
301
+ formatError(
302
+ err instanceof Error ? err : String(err),
303
+ opts,
304
+ );
305
+ process.exit(
306
+ err instanceof CLIError ? err.code : ExitCode.ERROR,
307
+ );
308
+ }
309
+ });
310
+
311
+ // -- manifest
312
+ kvCmd
313
+ .command("manifest")
314
+ .description("Show expected KV keys from manifest")
315
+ .action(async (_, cmd: Command) => {
316
+ const opts = formatOpts(cmd);
317
+ try {
318
+ await handleManifest(opts);
319
+ } catch (err) {
320
+ formatError(
321
+ err instanceof Error ? err : String(err),
322
+ opts,
323
+ );
324
+ process.exit(
325
+ err instanceof CLIError ? err.code : ExitCode.ERROR,
326
+ );
327
+ }
328
+ });
329
+ }