@jango-blockchained/hoox-cli 0.5.2 → 0.5.6
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 +32545 -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,896 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `hoox config` command group — configuration and secrets management.
|
|
3
|
-
*
|
|
4
|
-
* Subcommands:
|
|
5
|
-
* show Display current wrangler.jsonc config
|
|
6
|
-
* set <k> <v> Update a config value in wrangler.jsonc
|
|
7
|
-
* secrets ... Manage Cloudflare secrets (list, set, delete, sync)
|
|
8
|
-
* keys ... Manage internal auth keys (generate, list)
|
|
9
|
-
*/
|
|
10
|
-
import { existsSync, mkdirSync } from "node:fs";
|
|
11
|
-
|
|
12
|
-
import { Command } from "commander";
|
|
13
|
-
import { modify, applyEdits } from "jsonc-parser";
|
|
14
|
-
import type { FormattingOptions } from "jsonc-parser";
|
|
15
|
-
import { spinner } from "@clack/prompts";
|
|
16
|
-
|
|
17
|
-
import { ConfigService } from "../../services/config/index.js";
|
|
18
|
-
import { SecretsService } from "../../services/secrets/index.js";
|
|
19
|
-
import { registerEnvCommand } from "./env-command.js";
|
|
20
|
-
import { registerKvCommand } from "./kv-command.js";
|
|
21
|
-
import { CLIError, ExitCode } from "../../utils/errors.js";
|
|
22
|
-
import {
|
|
23
|
-
formatSuccess,
|
|
24
|
-
formatError,
|
|
25
|
-
formatTable,
|
|
26
|
-
formatKeyValue,
|
|
27
|
-
formatJson,
|
|
28
|
-
} from "../../utils/formatters.js";
|
|
29
|
-
import type { FormatOptions } from "../../utils/formatters.js";
|
|
30
|
-
import { theme, icons } from "../../utils/theme.js";
|
|
31
|
-
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
// Helpers
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
|
|
36
|
-
const FORMATTING_OPTIONS: FormattingOptions = {
|
|
37
|
-
tabSize: 2,
|
|
38
|
-
insertSpaces: true,
|
|
39
|
-
eol: "\n",
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Convert a dot-separated user key path (e.g. "workers.d1-worker.vars.db")
|
|
44
|
-
* into a jsonc-parser `JSONPath` array `["workers","d1-worker","vars","db"]`.
|
|
45
|
-
*/
|
|
46
|
-
function keyToPath(key: string): (string | number)[] {
|
|
47
|
-
return key.split(".");
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Read the raw content of wrangler.jsonc.
|
|
52
|
-
*/
|
|
53
|
-
async function readConfigRaw(configPath?: string): Promise<string> {
|
|
54
|
-
const path = configPath ?? "wrangler.jsonc";
|
|
55
|
-
const file = Bun.file(path);
|
|
56
|
-
if (!(await file.exists())) {
|
|
57
|
-
throw new CLIError(
|
|
58
|
-
`Config file not found: ${path}`,
|
|
59
|
-
ExitCode.INVALID_USAGE
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
return await file.text();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Write content back to wrangler.jsonc.
|
|
67
|
-
*/
|
|
68
|
-
async function writeConfigRaw(
|
|
69
|
-
content: string,
|
|
70
|
-
configPath?: string
|
|
71
|
-
): Promise<void> {
|
|
72
|
-
const path = configPath ?? "wrangler.jsonc";
|
|
73
|
-
await Bun.write(path, content);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Get FormatOptions from the global commander opts (--json / --quiet).
|
|
78
|
-
*/
|
|
79
|
-
/**
|
|
80
|
-
* Get FormatOptions from the command opts using optsWithGlobals() so that
|
|
81
|
-
* globally defined flags (--json, --quiet) are resolved correctly even when
|
|
82
|
-
* called from deeply nested subcommands.
|
|
83
|
-
*/
|
|
84
|
-
function formatOpts(program: Command): FormatOptions {
|
|
85
|
-
const opts = program.optsWithGlobals<{ json?: boolean; quiet?: boolean }>();
|
|
86
|
-
return { json: Boolean(opts.json), quiet: Boolean(opts.quiet) };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Prompt the user for a secret value via stdin (password-style masked input).
|
|
91
|
-
*
|
|
92
|
-
* Uses terminal raw mode to suppress echo. Falls back to plain text read
|
|
93
|
-
* when raw mode is unavailable (e.g. non-TTY environments).
|
|
94
|
-
*/
|
|
95
|
-
async function promptSecret(promptText: string): Promise<string> {
|
|
96
|
-
process.stdout.write(`${theme.info(icons.info)} ${promptText}: `);
|
|
97
|
-
|
|
98
|
-
// Attempt to use raw mode for password masking
|
|
99
|
-
if (process.stdin.isTTY) {
|
|
100
|
-
let prevRaw = false;
|
|
101
|
-
try {
|
|
102
|
-
prevRaw =
|
|
103
|
-
(
|
|
104
|
-
process.stdin as unknown as { isRawMode?: () => boolean }
|
|
105
|
-
).isRawMode?.() ?? false;
|
|
106
|
-
} catch {
|
|
107
|
-
// isRawMode not available
|
|
108
|
-
}
|
|
109
|
-
try {
|
|
110
|
-
if (process.stdin.setRawMode) process.stdin.setRawMode(true);
|
|
111
|
-
} catch {
|
|
112
|
-
// Raw mode not supported — fall through to plain read
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
let input = "";
|
|
116
|
-
try {
|
|
117
|
-
for await (const chunk of Bun.stdin.stream() as unknown as AsyncIterable<Uint8Array>) {
|
|
118
|
-
const text = new TextDecoder().decode(chunk);
|
|
119
|
-
for (const char of text) {
|
|
120
|
-
if (char === "\n" || char === "\r") {
|
|
121
|
-
process.stdout.write("\n");
|
|
122
|
-
return input;
|
|
123
|
-
}
|
|
124
|
-
if (char === "\x03") {
|
|
125
|
-
// Ctrl+C
|
|
126
|
-
process.stdout.write("\n");
|
|
127
|
-
throw new CLIError("Operation cancelled", ExitCode.INVALID_USAGE);
|
|
128
|
-
}
|
|
129
|
-
if (char === "\x7f") {
|
|
130
|
-
// Backspace
|
|
131
|
-
if (input.length > 0) {
|
|
132
|
-
input = input.slice(0, -1);
|
|
133
|
-
process.stdout.write("\b \b");
|
|
134
|
-
}
|
|
135
|
-
} else if (char >= "\x20") {
|
|
136
|
-
// Printable chars only (skip control characters)
|
|
137
|
-
input += char;
|
|
138
|
-
process.stdout.write("*");
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
} finally {
|
|
143
|
-
try {
|
|
144
|
-
if (process.stdin.setRawMode) process.stdin.setRawMode(prevRaw);
|
|
145
|
-
} catch {
|
|
146
|
-
// ignore
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
return input;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Non-TTY fallback: read single line from stdin
|
|
153
|
-
return await readLine();
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/** Read a single line from stdin (non-TTY fallback). */
|
|
157
|
-
async function readLine(): Promise<string> {
|
|
158
|
-
let line = "";
|
|
159
|
-
for await (const chunk of Bun.stdin.stream() as unknown as AsyncIterable<Uint8Array>) {
|
|
160
|
-
const text = new TextDecoder().decode(chunk);
|
|
161
|
-
const newlineIdx = text.indexOf("\n");
|
|
162
|
-
if (newlineIdx >= 0) {
|
|
163
|
-
line += text.substring(0, newlineIdx);
|
|
164
|
-
break;
|
|
165
|
-
}
|
|
166
|
-
line += text;
|
|
167
|
-
}
|
|
168
|
-
return line.trim();
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Generate a cryptographically random hex key of the specified byte length.
|
|
173
|
-
*/
|
|
174
|
-
function generateKey(bytes = 32): string {
|
|
175
|
-
const buf = new Uint8Array(bytes);
|
|
176
|
-
crypto.getRandomValues(buf);
|
|
177
|
-
return Array.from(buf, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// ---------------------------------------------------------------------------
|
|
181
|
-
// Command registration
|
|
182
|
-
// ---------------------------------------------------------------------------
|
|
183
|
-
|
|
184
|
-
export function registerConfigCommand(program: Command): void {
|
|
185
|
-
const configCmd = program
|
|
186
|
-
.command("config")
|
|
187
|
-
.summary("Manage configuration, secrets, and keys")
|
|
188
|
-
.description(
|
|
189
|
-
`Manage your Hoox configuration and secrets.
|
|
190
|
-
|
|
191
|
-
CONFIGURATION:
|
|
192
|
-
The main configuration lives in wrangler.jsonc at the project root.
|
|
193
|
-
Use 'config show' to view current settings and 'config set' to modify them.
|
|
194
|
-
|
|
195
|
-
SECRETS:
|
|
196
|
-
Secrets are stored in Cloudflare and managed via 'config secrets'.
|
|
197
|
-
Local development uses .dev.vars files in each worker directory.
|
|
198
|
-
|
|
199
|
-
KEYS:
|
|
200
|
-
Generate and manage internal auth keys for inter-worker communication.
|
|
201
|
-
Keys are stored in the .keys/ directory (add to .gitignore).
|
|
202
|
-
|
|
203
|
-
EXAMPLES:
|
|
204
|
-
hoox config show Display current configuration
|
|
205
|
-
hoox config set workers.agent-worker.enabled false
|
|
206
|
-
hoox config secrets list List secrets for a worker
|
|
207
|
-
hoox config secrets set trade-worker BINANCE_KEY_BINDING
|
|
208
|
-
hoox config keys generate Generate new internal keys`
|
|
209
|
-
);
|
|
210
|
-
|
|
211
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
212
|
-
// show
|
|
213
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
214
|
-
configCmd
|
|
215
|
-
.command("show")
|
|
216
|
-
.summary("Display current wrangler.jsonc configuration")
|
|
217
|
-
.description(
|
|
218
|
-
`Display the current wrangler.jsonc configuration in a formatted table.
|
|
219
|
-
|
|
220
|
-
Output includes:
|
|
221
|
-
- Global settings (account_id, subdomain_prefix, etc.)
|
|
222
|
-
- All workers with their enabled status, path, secrets count, and vars
|
|
223
|
-
|
|
224
|
-
OPTIONS:
|
|
225
|
-
--json Output raw JSON instead of formatted table
|
|
226
|
-
|
|
227
|
-
EXAMPLES:
|
|
228
|
-
hoox config show
|
|
229
|
-
hoox config show --json`
|
|
230
|
-
)
|
|
231
|
-
.action(async () => {
|
|
232
|
-
const opts = formatOpts(program);
|
|
233
|
-
|
|
234
|
-
try {
|
|
235
|
-
if (opts.json) {
|
|
236
|
-
// Raw JSON output — just dump the file content parsed as JSON
|
|
237
|
-
const raw = await readConfigRaw();
|
|
238
|
-
const parsed = JSON.parse(raw);
|
|
239
|
-
formatJson(parsed, opts);
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const svc = new ConfigService();
|
|
244
|
-
await svc.load();
|
|
245
|
-
|
|
246
|
-
// Global section
|
|
247
|
-
const global = svc.getGlobal();
|
|
248
|
-
const globalPairs: Record<string, string> = {};
|
|
249
|
-
for (const [k, v] of Object.entries(global)) {
|
|
250
|
-
globalPairs[k] = v ?? "(not set)";
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
process.stdout.write(`${theme.heading("\nGlobal Configuration")}\n`);
|
|
254
|
-
formatKeyValue(globalPairs, opts);
|
|
255
|
-
|
|
256
|
-
// Workers table
|
|
257
|
-
const workers = svc.listWorkers();
|
|
258
|
-
if (workers.length > 0) {
|
|
259
|
-
process.stdout.write(`\n${theme.heading("Workers")}\n`);
|
|
260
|
-
const rows = workers.map((name) => {
|
|
261
|
-
const w = svc.getWorker(name)!;
|
|
262
|
-
return {
|
|
263
|
-
Worker: name,
|
|
264
|
-
Enabled: w.enabled ? `${theme.success("•")} yes` : "-",
|
|
265
|
-
Path: w.path,
|
|
266
|
-
Secrets: w.secrets?.length ? String(w.secrets.length) : "-",
|
|
267
|
-
Vars:
|
|
268
|
-
w.vars && Object.keys(w.vars).length
|
|
269
|
-
? String(Object.keys(w.vars).length)
|
|
270
|
-
: "-",
|
|
271
|
-
};
|
|
272
|
-
});
|
|
273
|
-
formatTable(rows, opts);
|
|
274
|
-
}
|
|
275
|
-
} catch (err: unknown) {
|
|
276
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
277
|
-
formatError(
|
|
278
|
-
new CLIError(`Failed to show config: ${message}`, ExitCode.ERROR),
|
|
279
|
-
opts
|
|
280
|
-
);
|
|
281
|
-
}
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
285
|
-
// set <key> <value>
|
|
286
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
287
|
-
configCmd
|
|
288
|
-
.command("set <key> <value>")
|
|
289
|
-
.summary("Update a config value in wrangler.jsonc")
|
|
290
|
-
.description(
|
|
291
|
-
`Update a configuration value in wrangler.jsonc.
|
|
292
|
-
|
|
293
|
-
ARGUMENTS:
|
|
294
|
-
key Dot-notation path to the config value (e.g., workers.agent-worker.enabled)
|
|
295
|
-
value New value (auto-detects type: boolean, number, or string)
|
|
296
|
-
|
|
297
|
-
PATH EXAMPLES:
|
|
298
|
-
global.subdomain_prefix → Set subdomain
|
|
299
|
-
workers.trade-worker.enabled → Enable/disable worker
|
|
300
|
-
workers.agent-worker.vars.interval → Set worker variable
|
|
301
|
-
|
|
302
|
-
EXAMPLES:
|
|
303
|
-
hoox config set global.subdomain_prefix myapp
|
|
304
|
-
hoox config set workers.trade-worker.enabled false
|
|
305
|
-
hoox config set workers.agent-worker.vars.interval 5`
|
|
306
|
-
)
|
|
307
|
-
.action(async (key: string, value: string) => {
|
|
308
|
-
const opts = formatOpts(program);
|
|
309
|
-
|
|
310
|
-
try {
|
|
311
|
-
const raw = await readConfigRaw();
|
|
312
|
-
const jsonPath = keyToPath(key);
|
|
313
|
-
|
|
314
|
-
// Attempt the edit — jsonc-parser throws on invalid paths
|
|
315
|
-
let edits;
|
|
316
|
-
try {
|
|
317
|
-
edits = modify(raw, jsonPath, parseValue(value), {
|
|
318
|
-
formattingOptions: FORMATTING_OPTIONS,
|
|
319
|
-
});
|
|
320
|
-
} catch (err: unknown) {
|
|
321
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
322
|
-
throw new CLIError(
|
|
323
|
-
`Invalid key path "${key}": ${msg}`,
|
|
324
|
-
ExitCode.INVALID_USAGE
|
|
325
|
-
);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const updated = applyEdits(raw, edits);
|
|
329
|
-
await writeConfigRaw(updated);
|
|
330
|
-
|
|
331
|
-
formatSuccess(`Updated "${key}" = "${value}" in wrangler.jsonc`, opts);
|
|
332
|
-
} catch (err: unknown) {
|
|
333
|
-
if (err instanceof CLIError) {
|
|
334
|
-
formatError(err, opts);
|
|
335
|
-
} else {
|
|
336
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
337
|
-
formatError(
|
|
338
|
-
new CLIError(`Failed to set config: ${message}`, ExitCode.ERROR),
|
|
339
|
-
opts
|
|
340
|
-
);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
346
|
-
// secrets subcommand group
|
|
347
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
348
|
-
const secretsCmd = configCmd
|
|
349
|
-
.command("secrets")
|
|
350
|
-
.summary("Manage Cloudflare Worker secrets")
|
|
351
|
-
.description(
|
|
352
|
-
`Manage secrets for your Cloudflare Workers.
|
|
353
|
-
|
|
354
|
-
Secrets are defined in wrangler.jsonc under each worker's 'secrets' array.
|
|
355
|
-
They are stored securely in Cloudflare and uploaded via 'wrangler secret put'.
|
|
356
|
-
|
|
357
|
-
LOCAL DEVELOPMENT:
|
|
358
|
-
For local development, create .dev.vars files in each worker directory.
|
|
359
|
-
The 'config secrets sync' command can generate these from your config.
|
|
360
|
-
|
|
361
|
-
EXAMPLES:
|
|
362
|
-
hoox config secrets list List all secrets
|
|
363
|
-
hoox config secrets list trade-worker List secrets for one worker
|
|
364
|
-
hoox config secrets set trade-worker API_KEY Set a secret value
|
|
365
|
-
hoox config secrets delete trade-worker API_KEY Delete a secret
|
|
366
|
-
hoox config secrets sync Sync secrets to .dev.vars`
|
|
367
|
-
);
|
|
368
|
-
|
|
369
|
-
// secrets list [worker]
|
|
370
|
-
secretsCmd
|
|
371
|
-
.command("list [worker]")
|
|
372
|
-
.summary("List secrets for all workers or a specific worker")
|
|
373
|
-
.description(
|
|
374
|
-
`List the secrets declared in wrangler.jsonc for workers.
|
|
375
|
-
|
|
376
|
-
ARGUMENTS:
|
|
377
|
-
worker Optional worker name to filter by
|
|
378
|
-
|
|
379
|
-
EXAMPLES:
|
|
380
|
-
hoox config secrets list
|
|
381
|
-
hoox config secrets list trade-worker`
|
|
382
|
-
)
|
|
383
|
-
.action(async (worker?: string) => {
|
|
384
|
-
const opts = formatOpts(program);
|
|
385
|
-
|
|
386
|
-
try {
|
|
387
|
-
const svc = await SecretsService.create();
|
|
388
|
-
|
|
389
|
-
if (worker) {
|
|
390
|
-
const secrets = svc.listSecrets(worker);
|
|
391
|
-
if (secrets.length === 0) {
|
|
392
|
-
process.stdout.write(
|
|
393
|
-
`${theme.dim(`No secrets declared for worker "${worker}".`)}\n`
|
|
394
|
-
);
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
if (opts.json) {
|
|
399
|
-
formatJson({ worker, secrets }, opts);
|
|
400
|
-
} else {
|
|
401
|
-
process.stdout.write(
|
|
402
|
-
`${theme.heading(`\nSecrets for ${worker}`)}\n`
|
|
403
|
-
);
|
|
404
|
-
for (const s of secrets) {
|
|
405
|
-
process.stdout.write(` ${theme.label("•")} ${s}\n`);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
} else {
|
|
409
|
-
const all = svc.listAllSecrets();
|
|
410
|
-
const workers = Object.keys(all);
|
|
411
|
-
|
|
412
|
-
if (workers.length === 0) {
|
|
413
|
-
process.stdout.write(
|
|
414
|
-
`${theme.dim("No secrets declared for any worker.")}\n`
|
|
415
|
-
);
|
|
416
|
-
return;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
if (opts.json) {
|
|
420
|
-
formatJson(all, opts);
|
|
421
|
-
} else {
|
|
422
|
-
process.stdout.write(`${theme.heading("\nSecrets by Worker")}\n`);
|
|
423
|
-
for (const [name, secrets] of Object.entries(all)) {
|
|
424
|
-
process.stdout.write(
|
|
425
|
-
`\n ${theme.bold(name)} (${secrets.length})\n`
|
|
426
|
-
);
|
|
427
|
-
for (const s of secrets) {
|
|
428
|
-
process.stdout.write(` ${theme.dim("•")} ${s}\n`);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
} catch (err: unknown) {
|
|
434
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
435
|
-
formatError(
|
|
436
|
-
new CLIError(`Failed to list secrets: ${message}`, ExitCode.ERROR),
|
|
437
|
-
opts
|
|
438
|
-
);
|
|
439
|
-
}
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
// secrets set <worker> <name>
|
|
443
|
-
secretsCmd
|
|
444
|
-
.command("set <worker> <name>")
|
|
445
|
-
.summary("Set a secret value for a worker")
|
|
446
|
-
.description(
|
|
447
|
-
`Set a secret value for a worker and sync to Cloudflare.
|
|
448
|
-
|
|
449
|
-
ARGUMENTS:
|
|
450
|
-
worker Worker name (e.g., trade-worker, agent-worker)
|
|
451
|
-
name Secret name (must be declared in wrangler.jsonc)
|
|
452
|
-
|
|
453
|
-
The command will prompt for the secret value (hidden input).
|
|
454
|
-
It writes to the worker's .dev.vars file and syncs to Cloudflare.
|
|
455
|
-
|
|
456
|
-
EXAMPLES:
|
|
457
|
-
hoox config secrets set trade-worker BINANCE_KEY_BINDING
|
|
458
|
-
hoox config secrets set agent-worker OPENAI_KEY`
|
|
459
|
-
)
|
|
460
|
-
.action(async (workerName: string, secretName: string) => {
|
|
461
|
-
const opts = formatOpts(program);
|
|
462
|
-
|
|
463
|
-
try {
|
|
464
|
-
const svc = await SecretsService.create();
|
|
465
|
-
const declared = svc.listSecrets(workerName);
|
|
466
|
-
|
|
467
|
-
if (!declared.includes(secretName) && declared.length > 0) {
|
|
468
|
-
throw new CLIError(
|
|
469
|
-
`Secret "${secretName}" is not declared for worker "${workerName}". ` +
|
|
470
|
-
`Declared secrets: ${declared.join(", ")}`,
|
|
471
|
-
ExitCode.INVALID_USAGE
|
|
472
|
-
);
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
const value = await promptSecret(`Enter value for "${secretName}"`);
|
|
476
|
-
if (!value) {
|
|
477
|
-
throw new CLIError(
|
|
478
|
-
"Secret value cannot be empty",
|
|
479
|
-
ExitCode.INVALID_USAGE
|
|
480
|
-
);
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// Write value to .dev.vars (workers/<name> mirrors the project layout)
|
|
484
|
-
const devVarsPath = `workers/${workerName}/.dev.vars`;
|
|
485
|
-
await updateDevVars(devVarsPath, secretName, value);
|
|
486
|
-
|
|
487
|
-
formatSuccess(`Secret "${secretName}" updated in ${devVarsPath}`, opts);
|
|
488
|
-
|
|
489
|
-
// Sync to Cloudflare with spinner
|
|
490
|
-
const syncSpin = spinner();
|
|
491
|
-
syncSpin.start("Syncing to Cloudflare...");
|
|
492
|
-
const result = await svc.syncToCloudflare(workerName);
|
|
493
|
-
if (result.ok) {
|
|
494
|
-
syncSpin.stop(`Secret "${secretName}" synced to Cloudflare`);
|
|
495
|
-
} else {
|
|
496
|
-
syncSpin.stop(`Sync partial: ${result.error ?? "unknown error"}`);
|
|
497
|
-
formatError(
|
|
498
|
-
new CLIError(
|
|
499
|
-
`Sync partial: ${result.error ?? "unknown error"}`,
|
|
500
|
-
ExitCode.ERROR
|
|
501
|
-
),
|
|
502
|
-
opts
|
|
503
|
-
);
|
|
504
|
-
}
|
|
505
|
-
} catch (err: unknown) {
|
|
506
|
-
if (err instanceof CLIError) {
|
|
507
|
-
formatError(err, opts);
|
|
508
|
-
} else {
|
|
509
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
510
|
-
formatError(
|
|
511
|
-
new CLIError(`Failed to set secret: ${message}`, ExitCode.ERROR),
|
|
512
|
-
opts
|
|
513
|
-
);
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
// secrets delete <worker> <name>
|
|
519
|
-
secretsCmd
|
|
520
|
-
.command("delete <worker> <name>")
|
|
521
|
-
.summary("Delete a secret from Cloudflare")
|
|
522
|
-
.description(
|
|
523
|
-
`Delete a secret from Cloudflare Workers.
|
|
524
|
-
|
|
525
|
-
ARGUMENTS:
|
|
526
|
-
worker Worker name (e.g., trade-worker, agent-worker)
|
|
527
|
-
name Secret name to delete
|
|
528
|
-
|
|
529
|
-
This removes the secret from Cloudflare and from the worker's .dev.vars file.
|
|
530
|
-
|
|
531
|
-
EXAMPLES:
|
|
532
|
-
hoox config secrets delete trade-worker BINANCE_KEY_BINDING`
|
|
533
|
-
)
|
|
534
|
-
.action(async (workerName: string, secretName: string) => {
|
|
535
|
-
const opts = formatOpts(program);
|
|
536
|
-
|
|
537
|
-
try {
|
|
538
|
-
const svc = await SecretsService.create();
|
|
539
|
-
const declared = svc.listSecrets(workerName);
|
|
540
|
-
|
|
541
|
-
if (!declared.includes(secretName)) {
|
|
542
|
-
throw new CLIError(
|
|
543
|
-
`Secret "${secretName}" is not declared for worker "${workerName}".`,
|
|
544
|
-
ExitCode.INVALID_USAGE
|
|
545
|
-
);
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
// Wrangler secret delete
|
|
549
|
-
const proc = Bun.spawn(["wrangler", "secret", "delete", secretName], {
|
|
550
|
-
cwd: `workers/${workerName}`,
|
|
551
|
-
stdout: "pipe",
|
|
552
|
-
stderr: "pipe",
|
|
553
|
-
});
|
|
554
|
-
|
|
555
|
-
const exitCode = await proc.exited;
|
|
556
|
-
if (exitCode !== 0) {
|
|
557
|
-
const stderrText = await new Response(proc.stderr).text();
|
|
558
|
-
throw new CLIError(
|
|
559
|
-
`wrangler exited with code ${exitCode}: ${stderrText.trim()}`,
|
|
560
|
-
ExitCode.ERROR
|
|
561
|
-
);
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
// Also remove from .dev.vars if present
|
|
565
|
-
const devVarsPath = `workers/${workerName}/.dev.vars`;
|
|
566
|
-
const devFile = Bun.file(devVarsPath);
|
|
567
|
-
if (await devFile.exists()) {
|
|
568
|
-
let content = await devFile.text();
|
|
569
|
-
const lines = content.split("\n");
|
|
570
|
-
const filtered = lines.filter(
|
|
571
|
-
(line) => !line.startsWith(`${secretName}=`) && line.trim() !== ""
|
|
572
|
-
);
|
|
573
|
-
// Keep commented lines, remove empty ones
|
|
574
|
-
await Bun.write(
|
|
575
|
-
devVarsPath,
|
|
576
|
-
filtered.join("\n") + (filtered.length > 0 ? "\n" : "")
|
|
577
|
-
);
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
formatSuccess(`Secret "${secretName}" deleted from Cloudflare`, opts);
|
|
581
|
-
} catch (err: unknown) {
|
|
582
|
-
if (err instanceof CLIError) {
|
|
583
|
-
formatError(err, opts);
|
|
584
|
-
} else {
|
|
585
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
586
|
-
formatError(
|
|
587
|
-
new CLIError(`Failed to delete secret: ${message}`, ExitCode.ERROR),
|
|
588
|
-
opts
|
|
589
|
-
);
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
});
|
|
593
|
-
|
|
594
|
-
// secrets sync [worker]
|
|
595
|
-
secretsCmd
|
|
596
|
-
.command("sync [worker]")
|
|
597
|
-
.summary("Sync secrets to Cloudflare")
|
|
598
|
-
.description(
|
|
599
|
-
`Sync secrets from .dev.vars files to Cloudflare Workers.
|
|
600
|
-
|
|
601
|
-
ARGUMENTS:
|
|
602
|
-
worker Optional worker name to sync (syncs all if not specified)
|
|
603
|
-
|
|
604
|
-
This reads .dev.vars files and uploads secrets to Cloudflare via wrangler.
|
|
605
|
-
|
|
606
|
-
EXAMPLES:
|
|
607
|
-
hoox config secrets sync Sync all workers
|
|
608
|
-
hoox config secrets sync trade-worker Sync specific worker`
|
|
609
|
-
)
|
|
610
|
-
.action(async (workerName?: string) => {
|
|
611
|
-
const opts = formatOpts(program);
|
|
612
|
-
|
|
613
|
-
try {
|
|
614
|
-
const svc = await SecretsService.create();
|
|
615
|
-
|
|
616
|
-
if (workerName) {
|
|
617
|
-
const syncSpin = spinner();
|
|
618
|
-
syncSpin.start(`Syncing secrets for "${workerName}"...`);
|
|
619
|
-
const result = await svc.syncToCloudflare(workerName);
|
|
620
|
-
if (result.ok) {
|
|
621
|
-
syncSpin.stop(
|
|
622
|
-
`Synced ${result.value?.length ?? 0} secrets for "${workerName}"`
|
|
623
|
-
);
|
|
624
|
-
} else {
|
|
625
|
-
syncSpin.stop(`Sync failed: ${result.error ?? "unknown error"}`);
|
|
626
|
-
formatError(
|
|
627
|
-
new CLIError(
|
|
628
|
-
`Sync failed: ${result.error ?? "unknown error"}`,
|
|
629
|
-
ExitCode.ERROR
|
|
630
|
-
),
|
|
631
|
-
opts
|
|
632
|
-
);
|
|
633
|
-
}
|
|
634
|
-
} else {
|
|
635
|
-
const all = svc.listAllSecrets();
|
|
636
|
-
const workers = Object.keys(all);
|
|
637
|
-
|
|
638
|
-
if (workers.length === 0) {
|
|
639
|
-
formatSuccess("No secrets to sync.", opts);
|
|
640
|
-
return;
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
let synced = 0;
|
|
644
|
-
let failed = 0;
|
|
645
|
-
const syncSpin = spinner();
|
|
646
|
-
|
|
647
|
-
for (const name of workers) {
|
|
648
|
-
syncSpin.start(`Syncing ${name}...`);
|
|
649
|
-
const result = await svc.syncToCloudflare(name);
|
|
650
|
-
if (result.ok) {
|
|
651
|
-
syncSpin.stop(
|
|
652
|
-
`${theme.success("synced")} ${result.value?.length ?? 0} for ${name}`
|
|
653
|
-
);
|
|
654
|
-
synced++;
|
|
655
|
-
} else {
|
|
656
|
-
syncSpin.stop(`${theme.error("failed")} ${name}`);
|
|
657
|
-
failed++;
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
if (failed === 0) {
|
|
662
|
-
formatSuccess(`All ${synced} workers synced successfully`, opts);
|
|
663
|
-
} else {
|
|
664
|
-
formatError(
|
|
665
|
-
new CLIError(
|
|
666
|
-
`${synced} synced, ${failed} failed`,
|
|
667
|
-
ExitCode.ERROR
|
|
668
|
-
),
|
|
669
|
-
opts
|
|
670
|
-
);
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
} catch (err: unknown) {
|
|
674
|
-
if (err instanceof CLIError) {
|
|
675
|
-
formatError(err, opts);
|
|
676
|
-
} else {
|
|
677
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
678
|
-
formatError(
|
|
679
|
-
new CLIError(`Failed to sync secrets: ${message}`, ExitCode.ERROR),
|
|
680
|
-
opts
|
|
681
|
-
);
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
});
|
|
685
|
-
|
|
686
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
687
|
-
// env subcommand group
|
|
688
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
689
|
-
registerEnvCommand(configCmd);
|
|
690
|
-
|
|
691
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
692
|
-
// kv subcommand group
|
|
693
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
694
|
-
registerKvCommand(configCmd);
|
|
695
|
-
|
|
696
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
697
|
-
// keys subcommand group
|
|
698
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
699
|
-
const keysCmd = configCmd
|
|
700
|
-
.command("keys")
|
|
701
|
-
.summary("Manage internal auth keys")
|
|
702
|
-
.description(
|
|
703
|
-
`Generate and manage internal auth keys for inter-worker communication.
|
|
704
|
-
|
|
705
|
-
Keys are stored in the .keys/ directory as .env files (add .keys/ to .gitignore).
|
|
706
|
-
These keys are used for authentication between workers.
|
|
707
|
-
|
|
708
|
-
EXAMPLES:
|
|
709
|
-
hoox config keys generate Generate new keys
|
|
710
|
-
hoox config keys list List existing keys`
|
|
711
|
-
);
|
|
712
|
-
|
|
713
|
-
// keys generate
|
|
714
|
-
keysCmd
|
|
715
|
-
.command("generate")
|
|
716
|
-
.summary("Generate new internal auth keys")
|
|
717
|
-
.description(
|
|
718
|
-
`Generate new internal auth keys and save to .keys/ directory.
|
|
719
|
-
|
|
720
|
-
Creates the following keys:
|
|
721
|
-
- INTERNAL_KEY_BINDING (32 char)
|
|
722
|
-
- WEBHOOK_API_KEY_BINDING (32 char)
|
|
723
|
-
- AGENT_INTERNAL_KEY (32 char)
|
|
724
|
-
- TG_BOT_TOKEN_BINDING (16 char)
|
|
725
|
-
- INTERNAL_KEY_BINDING (32 char)
|
|
726
|
-
|
|
727
|
-
WARNING: Add .keys/ to your .gitignore to avoid committing secrets!
|
|
728
|
-
|
|
729
|
-
EXAMPLES:
|
|
730
|
-
hoox config keys generate`
|
|
731
|
-
)
|
|
732
|
-
.action(async () => {
|
|
733
|
-
const opts = formatOpts(program);
|
|
734
|
-
|
|
735
|
-
try {
|
|
736
|
-
const keysDir = ".keys";
|
|
737
|
-
if (!existsSync(keysDir)) {
|
|
738
|
-
mkdirSync(keysDir, { recursive: true });
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
const keys: Record<string, string> = {
|
|
742
|
-
INTERNAL_KEY_BINDING: generateKey(),
|
|
743
|
-
WEBHOOK_API_KEY_BINDING: generateKey(),
|
|
744
|
-
AGENT_INTERNAL_KEY: generateKey(),
|
|
745
|
-
TG_BOT_TOKEN_BINDING: generateKey(16),
|
|
746
|
-
};
|
|
747
|
-
|
|
748
|
-
for (const [name, value] of Object.entries(keys)) {
|
|
749
|
-
const filePath = `${keysDir}/${name.toLowerCase()}.env`;
|
|
750
|
-
await Bun.write(filePath, `${name}=${value}\n`);
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
formatSuccess(
|
|
754
|
-
`Generated ${Object.keys(keys).length} keys in ${keysDir}/`,
|
|
755
|
-
opts
|
|
756
|
-
);
|
|
757
|
-
|
|
758
|
-
if (!opts.quiet && !opts.json) {
|
|
759
|
-
process.stdout.write(
|
|
760
|
-
`${theme.warning("!")} Keep these keys secret. Add ${keysDir}/ to .gitignore.\n`
|
|
761
|
-
);
|
|
762
|
-
formatKeyValue(keys, opts);
|
|
763
|
-
}
|
|
764
|
-
} catch (err: unknown) {
|
|
765
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
766
|
-
formatError(
|
|
767
|
-
new CLIError(`Failed to generate keys: ${message}`, ExitCode.ERROR),
|
|
768
|
-
opts
|
|
769
|
-
);
|
|
770
|
-
}
|
|
771
|
-
});
|
|
772
|
-
|
|
773
|
-
// keys list
|
|
774
|
-
keysCmd
|
|
775
|
-
.command("list")
|
|
776
|
-
.summary("List existing internal auth keys")
|
|
777
|
-
.description(
|
|
778
|
-
`List existing internal auth keys from the .keys/ directory.
|
|
779
|
-
|
|
780
|
-
Shows key names (values are hidden for security).
|
|
781
|
-
|
|
782
|
-
EXAMPLES:
|
|
783
|
-
hoox config keys list`
|
|
784
|
-
)
|
|
785
|
-
.action(async () => {
|
|
786
|
-
const opts = formatOpts(program);
|
|
787
|
-
|
|
788
|
-
try {
|
|
789
|
-
const keysDir = ".keys";
|
|
790
|
-
if (!existsSync(keysDir)) {
|
|
791
|
-
process.stdout.write(`${theme.dim("No .keys/ directory found.")}\n`);
|
|
792
|
-
return;
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
// Use Bun.Glob to list .keys/*.env files
|
|
796
|
-
const glob = new Bun.Glob("*.env");
|
|
797
|
-
const entries: string[] = [];
|
|
798
|
-
for await (const f of glob.scan({ cwd: keysDir, absolute: false })) {
|
|
799
|
-
entries.push(f);
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
if (entries.length === 0) {
|
|
803
|
-
process.stdout.write(
|
|
804
|
-
`${theme.dim("No key files found in .keys/")}\n`
|
|
805
|
-
);
|
|
806
|
-
return;
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
const keyMap: Record<string, string> = {};
|
|
810
|
-
for (const entry of entries) {
|
|
811
|
-
const filePath = `${keysDir}/${entry}`;
|
|
812
|
-
const content = await (await Bun.file(filePath).text()).trim();
|
|
813
|
-
const eqIdx = content.indexOf("=");
|
|
814
|
-
if (eqIdx > 0) {
|
|
815
|
-
keyMap[content.substring(0, eqIdx)] = "****";
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
if (opts.json) {
|
|
820
|
-
formatJson({ keys: entries.length, files: entries }, opts);
|
|
821
|
-
} else {
|
|
822
|
-
process.stdout.write(
|
|
823
|
-
`${theme.heading(`\nKey files in ${keysDir}/`)}\n`
|
|
824
|
-
);
|
|
825
|
-
formatKeyValue(keyMap, opts);
|
|
826
|
-
}
|
|
827
|
-
} catch (err: unknown) {
|
|
828
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
829
|
-
formatError(
|
|
830
|
-
new CLIError(`Failed to list keys: ${message}`, ExitCode.ERROR),
|
|
831
|
-
opts
|
|
832
|
-
);
|
|
833
|
-
}
|
|
834
|
-
});
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
// ---------------------------------------------------------------------------
|
|
838
|
-
// Internal helpers (not exported)
|
|
839
|
-
// ---------------------------------------------------------------------------
|
|
840
|
-
|
|
841
|
-
/**
|
|
842
|
-
* Parse a string value and attempt to coerce it to a proper type for JSON.
|
|
843
|
-
* Numbers and booleans are detected; everything else stays as a string.
|
|
844
|
-
*/
|
|
845
|
-
function parseValue(raw: string): string | number | boolean {
|
|
846
|
-
// Boolean
|
|
847
|
-
if (raw === "true") return true;
|
|
848
|
-
if (raw === "false") return false;
|
|
849
|
-
|
|
850
|
-
// Integer / float
|
|
851
|
-
if (/^-?\d+(\.\d+)?$/.test(raw)) {
|
|
852
|
-
return Number(raw);
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
// Keep as string
|
|
856
|
-
return raw;
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
/**
|
|
860
|
-
* Update or add a key=value entry in a .dev.vars file.
|
|
861
|
-
*/
|
|
862
|
-
async function updateDevVars(
|
|
863
|
-
filePath: string,
|
|
864
|
-
key: string,
|
|
865
|
-
value: string
|
|
866
|
-
): Promise<void> {
|
|
867
|
-
const file = Bun.file(filePath);
|
|
868
|
-
if (await file.exists()) {
|
|
869
|
-
const content = await file.text();
|
|
870
|
-
const lines = content.split("\n");
|
|
871
|
-
const updated: string[] = [];
|
|
872
|
-
let found = false;
|
|
873
|
-
|
|
874
|
-
for (const line of lines) {
|
|
875
|
-
if (line.startsWith(`${key}=`)) {
|
|
876
|
-
updated.push(`${key}=${value}`);
|
|
877
|
-
found = true;
|
|
878
|
-
} else {
|
|
879
|
-
updated.push(line);
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
if (!found) {
|
|
884
|
-
updated.push(`${key}=${value}`);
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
// Remove trailing empty lines
|
|
888
|
-
while (updated.length > 0 && updated[updated.length - 1] === "") {
|
|
889
|
-
updated.pop();
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
await Bun.write(filePath, updated.join("\n") + "\n");
|
|
893
|
-
} else {
|
|
894
|
-
await Bun.write(filePath, `${key}=${value}\n`);
|
|
895
|
-
}
|
|
896
|
-
}
|