@jango-blockchained/hoox-cli 0.3.4 → 0.3.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.
Files changed (48) hide show
  1. package/README.md +2 -0
  2. package/package.json +5 -3
  3. package/src/commands/check/check-command.test.ts +28 -20
  4. package/src/commands/check/prerequisites-command.ts +9 -5
  5. package/src/commands/clone/clone-command.ts +1 -10
  6. package/src/commands/config/config-command.ts +7 -11
  7. package/src/commands/config/env-command.ts +16 -35
  8. package/src/commands/config/kv-command.ts +33 -72
  9. package/src/commands/db/db-command.ts +23 -25
  10. package/src/commands/deploy/deploy-command.ts +133 -52
  11. package/src/commands/deploy/telegram-service.ts +21 -6
  12. package/src/commands/dev/dev-command.ts +7 -16
  13. package/src/commands/infra/infra-command.test.ts +2 -2
  14. package/src/commands/infra/infra-command.ts +45 -11
  15. package/src/commands/init/init-command.test.ts +2 -2
  16. package/src/commands/monitor/monitor-command.test.ts +44 -16
  17. package/src/commands/monitor/monitor-command.ts +45 -21
  18. package/src/commands/monitor/monitor-service.ts +19 -4
  19. package/src/commands/repair/repair-command.test.ts +35 -15
  20. package/src/commands/repair/repair-command.ts +39 -18
  21. package/src/commands/repair/repair-service.ts +48 -12
  22. package/src/commands/test/test-command.ts +4 -10
  23. package/src/commands/tui/index.ts +1 -0
  24. package/src/commands/tui/tui-command.ts +87 -0
  25. package/src/commands/update/index.ts +1 -0
  26. package/src/commands/update/update-command.ts +51 -0
  27. package/src/commands/waf/waf-command.test.ts +1 -1
  28. package/src/commands/waf/waf-command.ts +11 -11
  29. package/src/index.ts +52 -1
  30. package/src/services/cloudflare/cloudflare-service.test.ts +12 -8
  31. package/src/services/cloudflare/cloudflare-service.ts +8 -10
  32. package/src/services/cloudflare/types.ts +6 -5
  33. package/src/services/db/db-service.ts +8 -17
  34. package/src/services/env/env-service.test.ts +41 -15
  35. package/src/services/env/env-service.ts +276 -46
  36. package/src/services/kv/kv-sync-service.ts +117 -34
  37. package/src/services/prerequisites/prerequisites-service.ts +137 -29
  38. package/src/services/secrets/index.ts +0 -1
  39. package/src/services/secrets/secrets-service.test.ts +34 -25
  40. package/src/services/secrets/secrets-service.ts +10 -16
  41. package/src/services/secrets/types.ts +4 -11
  42. package/src/services/update/index.ts +2 -0
  43. package/src/services/update/update-service.test.ts +76 -0
  44. package/src/services/update/update-service.ts +193 -0
  45. package/src/ui/banner.ts +5 -5
  46. package/src/ui/menu.ts +6 -1
  47. package/src/utils/formatters.ts +21 -4
  48. package/src/utils/theme.ts +1 -1
@@ -28,7 +28,7 @@ export class DbService {
28
28
  async apply(
29
29
  dbName: string,
30
30
  remote: boolean,
31
- schemaPath?: string,
31
+ schemaPath?: string
32
32
  ): Promise<string> {
33
33
  const path = schemaPath ?? "workers/trade-worker/schema.sql";
34
34
  const args = ["d1", "execute", dbName, "--file", path];
@@ -58,20 +58,13 @@ export class DbService {
58
58
  return DbService.parseTableNames(output);
59
59
  }
60
60
 
61
- async query(
62
- dbName: string,
63
- sql: string,
64
- remote: boolean,
65
- ): Promise<string> {
61
+ async query(dbName: string, sql: string, remote: boolean): Promise<string> {
66
62
  const args = ["d1", "execute", dbName, "--command", sql, "--json"];
67
63
  if (remote) args.push("--remote");
68
64
  return await this.runWrangler(args);
69
65
  }
70
66
 
71
- async export(
72
- dbName: string,
73
- outputPath?: string,
74
- ): Promise<string> {
67
+ async export(dbName: string, outputPath?: string): Promise<string> {
75
68
  const outPath =
76
69
  outputPath ?? `backup-${new Date().toISOString().slice(0, 10)}.sql`;
77
70
  const args = ["d1", "export", dbName, "--output", outPath, "--remote"];
@@ -90,9 +83,9 @@ export class DbService {
90
83
  if (Array.isArray(parsed) && parsed.length > 0) {
91
84
  const first = parsed[0];
92
85
  if (first.results && Array.isArray(first.results)) {
93
- return first.results.map(
94
- (r: Record<string, unknown>) => String(r.name ?? ""),
95
- ).filter(Boolean);
86
+ return first.results
87
+ .map((r: Record<string, unknown>) => String(r.name ?? ""))
88
+ .filter(Boolean);
96
89
  }
97
90
  }
98
91
  } catch {
@@ -114,9 +107,7 @@ export class DbService {
114
107
  const exitCode = await proc.exited;
115
108
 
116
109
  if (exitCode !== 0) {
117
- throw new Error(
118
- stderr.trim() || `wrangler exited with code ${exitCode}`,
119
- );
110
+ throw new Error(stderr.trim() || `wrangler exited with code ${exitCode}`);
120
111
  }
121
112
 
122
113
  return stdout.trim();
@@ -128,7 +119,7 @@ export class DbService {
128
119
  if (await file.exists()) {
129
120
  const content = await file.text();
130
121
  const match = content.match(
131
- /d1\s+execute\s+\S+\s+--command=["'](.+?)["']/s,
122
+ /d1\s+execute\s+\S+\s+--command=["'](.+?)["']/s
132
123
  );
133
124
  if (match) return match[1];
134
125
  }
@@ -6,7 +6,7 @@ describe("EnvService", () => {
6
6
  it("returns all known env var definitions", () => {
7
7
  const defs = EnvService.getDefinitions();
8
8
  expect(defs.length).toBe(31);
9
- expect(defs.some(d => d.name === "CLOUDFLARE_API_TOKEN")).toBe(true);
9
+ expect(defs.some((d) => d.name === "CLOUDFLARE_API_TOKEN")).toBe(true);
10
10
  });
11
11
 
12
12
  it("each definition has required fields", () => {
@@ -35,7 +35,9 @@ describe("EnvService", () => {
35
35
  });
36
36
 
37
37
  it("includes provided values", () => {
38
- const content = EnvService.generateEnvLocal({ SUBDOMAIN_PREFIX: "myapp" });
38
+ const content = EnvService.generateEnvLocal({
39
+ SUBDOMAIN_PREFIX: "myapp",
40
+ });
39
41
  expect(content).toContain('SUBDOMAIN_PREFIX="myapp"');
40
42
  });
41
43
 
@@ -56,7 +58,9 @@ describe("EnvService", () => {
56
58
  expect(result["workers/agent-worker"]).toBeDefined();
57
59
  expect(result["workers/agent-worker"].AGENT_OPENAI_KEY).toBe("sk-123");
58
60
  expect(result["workers/telegram-worker"]).toBeDefined();
59
- expect(result["workers/telegram-worker"].TELEGRAM_BOT_TOKEN).toBe("tg-456");
61
+ expect(result["workers/telegram-worker"].TELEGRAM_BOT_TOKEN).toBe(
62
+ "tg-456"
63
+ );
60
64
  });
61
65
 
62
66
  it("omits workers with no matching vars", () => {
@@ -84,14 +88,22 @@ describe("EnvService", () => {
84
88
  };
85
89
  const result = EnvService.getWorkerDevVars(vars);
86
90
  expect(result["workers/hoox"]).toBeDefined();
87
- expect(result["workers/hoox"].WEBHOOK_API_KEY_BINDING).toBe("webhook-key");
91
+ expect(result["workers/hoox"].WEBHOOK_API_KEY_BINDING).toBe(
92
+ "webhook-key"
93
+ );
88
94
  expect(result["workers/hoox"].HA_TOKEN_BINDING).toBe("ha-token");
89
95
  expect(result["workers/trade-worker"].API_SERVICE_KEY).toBe("api-key");
90
- expect(result["workers/telegram-worker"].TELEGRAM_SECRET_TOKEN).toBe("tg-secret");
96
+ expect(result["workers/telegram-worker"].TELEGRAM_SECRET_TOKEN).toBe(
97
+ "tg-secret"
98
+ );
91
99
  expect(result["workers/web3-wallet-worker"]).toBeDefined();
92
- expect(result["workers/web3-wallet-worker"].WALLET_MNEMONIC_SECRET).toBe("mnemonic");
100
+ expect(result["workers/web3-wallet-worker"].WALLET_MNEMONIC_SECRET).toBe(
101
+ "mnemonic"
102
+ );
93
103
  expect(result["workers/email-worker"]).toBeDefined();
94
- expect(result["workers/email-worker"].EMAIL_HOST).toBe("imap.example.com");
104
+ expect(result["workers/email-worker"].EMAIL_HOST).toBe(
105
+ "imap.example.com"
106
+ );
95
107
  });
96
108
  });
97
109
 
@@ -103,12 +115,16 @@ describe("EnvService", () => {
103
115
  });
104
116
 
105
117
  it("flags 'your_' placeholder values as missing", () => {
106
- const result = EnvService.validate({ CLOUDFLARE_API_TOKEN: "your_cloudflare_api_token" });
118
+ const result = EnvService.validate({
119
+ CLOUDFLARE_API_TOKEN: "your_cloudflare_api_token",
120
+ });
107
121
  expect(result.missing).toContain("CLOUDFLARE_API_TOKEN");
108
122
  });
109
123
 
110
124
  it("flags 'generate_' placeholder values as missing", () => {
111
- const result = EnvService.validate({ SESSION_SECRET: "generate_a_32_character_secure_random_string" });
125
+ const result = EnvService.validate({
126
+ SESSION_SECRET: "generate_a_32_character_secure_random_string",
127
+ });
112
128
  expect(result.missing).toContain("SESSION_SECRET");
113
129
  });
114
130
 
@@ -147,7 +163,9 @@ describe("EnvService", () => {
147
163
  SESSION_SECRET: "short",
148
164
  };
149
165
  const result = EnvService.validate(vars);
150
- expect(result.warnings).toContain("SESSION_SECRET should be at least 32 characters");
166
+ expect(result.warnings).toContain(
167
+ "SESSION_SECRET should be at least 32 characters"
168
+ );
151
169
  });
152
170
  });
153
171
 
@@ -166,13 +184,15 @@ describe("EnvService", () => {
166
184
 
167
185
  describe("loadDotEnvAsync", () => {
168
186
  it("returns empty object for missing file", async () => {
169
- const result = await EnvService.loadDotEnvAsync("/tmp/nonexistent-file-12345.env");
187
+ const result = await EnvService.loadDotEnvAsync(
188
+ "/tmp/nonexistent-file-12345.env"
189
+ );
170
190
  expect(Object.keys(result).length).toBe(0);
171
191
  });
172
192
 
173
193
  it("parses simple key=value lines", async () => {
174
194
  const filePath = "/tmp/test-simple-12345.env";
175
- await Bun.write(filePath, 'KEY=value\nFOO=bar\n');
195
+ await Bun.write(filePath, "KEY=value\nFOO=bar\n");
176
196
  const result = await EnvService.loadDotEnvAsync(filePath);
177
197
  expect(result.KEY).toBe("value");
178
198
  expect(result.FOO).toBe("bar");
@@ -181,7 +201,10 @@ describe("EnvService", () => {
181
201
 
182
202
  it("strips double quotes from values", async () => {
183
203
  const filePath = "/tmp/test-quotes-12345.env";
184
- await Bun.write(filePath, 'KEY="quoted value"\nNESTED="val with \\"quote\\""\n');
204
+ await Bun.write(
205
+ filePath,
206
+ 'KEY="quoted value"\nNESTED="val with \\"quote\\""\n'
207
+ );
185
208
  const result = await EnvService.loadDotEnvAsync(filePath);
186
209
  // First quote-stripped value
187
210
  expect(result.KEY).toBe("quoted value");
@@ -190,7 +213,10 @@ describe("EnvService", () => {
190
213
 
191
214
  it("skips comments and blank lines", async () => {
192
215
  const filePath = "/tmp/test-comments-12345.env";
193
- await Bun.write(filePath, '# this is a comment\n\nKEY=val\n# another comment\nFOO=bar\n');
216
+ await Bun.write(
217
+ filePath,
218
+ "# this is a comment\n\nKEY=val\n# another comment\nFOO=bar\n"
219
+ );
194
220
  const result = await EnvService.loadDotEnvAsync(filePath);
195
221
  expect(result.KEY).toBe("val");
196
222
  expect(result.FOO).toBe("bar");
@@ -200,7 +226,7 @@ describe("EnvService", () => {
200
226
 
201
227
  it("handles empty values", async () => {
202
228
  const filePath = "/tmp/test-empty-12345.env";
203
- await Bun.write(filePath, 'EMPTY=\nKEY=val\n');
229
+ await Bun.write(filePath, "EMPTY=\nKEY=val\n");
204
230
  const result = await EnvService.loadDotEnvAsync(filePath);
205
231
  expect(result.EMPTY).toBe("");
206
232
  expect(result.KEY).toBe("val");
@@ -21,47 +21,238 @@ export interface EnvValidationResult {
21
21
  export class EnvService {
22
22
  static getDefinitions(): EnvVarDefinition[] {
23
23
  return [
24
- { name: "CLOUDFLARE_API_TOKEN", required: true, secret: true, section: "Cloudflare Account", hint: "API token with Workers/KV/D1/R2 permissions" },
25
- { name: "CLOUDFLARE_ACCOUNT_ID", required: true, secret: false, section: "Cloudflare Account", hint: "From Cloudflare dashboard URL" },
26
- { name: "CLOUDFLARE_SECRET_STORE_ID", required: false, secret: false, section: "Cloudflare Account", default: "", hint: "Optional" },
27
- { name: "SUBDOMAIN_PREFIX", required: true, secret: false, section: "Cloudflare Account", default: "cryptolinx", hint: "Prefix for worker URLs" },
28
- { name: "D1_INTERNAL_KEY", required: true, secret: true, section: "Internal Auth", hint: "Internal key for D1 worker auth" },
29
- { name: "TRADE_INTERNAL_KEY", required: true, secret: true, section: "Internal Auth", hint: "Internal key for trade worker auth" },
30
- { name: "AGENT_INTERNAL_KEY", required: true, secret: true, section: "Internal Auth", hint: "Internal key for agent worker auth" },
31
- { name: "WEBHOOK_API_KEY_BINDING", required: true, secret: true, section: "Internal Auth", hint: "Webhook auth key for hoox gateway" },
32
- { name: "INTERNAL_KEY_BINDING", required: true, secret: true, section: "Internal Auth", hint: "Shared internal auth key for inter-worker communication" },
33
- { name: "API_SERVICE_KEY", required: true, secret: true, section: "Internal Auth", hint: "API service key for trade-worker" },
34
- { name: "HA_TOKEN_BINDING", required: false, secret: true, section: "Internal Auth", hint: "Home Assistant token for hoox" },
35
- { name: "TELEGRAM_BOT_TOKEN", required: false, secret: true, section: "Telegram", hint: "From @BotFather" },
36
- { name: "TELEGRAM_SECRET_TOKEN", required: false, secret: true, section: "Telegram", hint: "Telegram webhook secret token" },
37
- { name: "AGENT_OPENAI_KEY", required: false, secret: true, section: "AI Providers", hint: "OpenAI API key" },
38
- { name: "AGENT_ANTHROPIC_KEY", required: false, secret: true, section: "AI Providers", hint: "Anthropic API key" },
39
- { name: "AGENT_GOOGLE_KEY", required: false, secret: true, section: "AI Providers", hint: "Google AI API key" },
40
- { name: "BINANCE_API_KEY", required: false, secret: true, section: "Exchanges", hint: "Binance API key" },
41
- { name: "BINANCE_API_SECRET", required: false, secret: true, section: "Exchanges", hint: "Binance API secret" },
42
- { name: "MEXC_API_KEY", required: false, secret: true, section: "Exchanges", hint: "MEXC API key" },
43
- { name: "MEXC_API_SECRET", required: false, secret: true, section: "Exchanges", hint: "MEXC API secret" },
44
- { name: "BYBIT_API_KEY", required: false, secret: true, section: "Exchanges", hint: "Bybit API key" },
45
- { name: "BYBIT_API_SECRET", required: false, secret: true, section: "Exchanges", hint: "Bybit API secret" },
24
+ {
25
+ name: "CLOUDFLARE_API_TOKEN",
26
+ required: true,
27
+ secret: true,
28
+ section: "Cloudflare Account",
29
+ hint: "API token with Workers/KV/D1/R2 permissions",
30
+ },
31
+ {
32
+ name: "CLOUDFLARE_ACCOUNT_ID",
33
+ required: true,
34
+ secret: false,
35
+ section: "Cloudflare Account",
36
+ hint: "From Cloudflare dashboard URL",
37
+ },
38
+ {
39
+ name: "CLOUDFLARE_SECRET_STORE_ID",
40
+ required: false,
41
+ secret: false,
42
+ section: "Cloudflare Account",
43
+ default: "",
44
+ hint: "Optional",
45
+ },
46
+ {
47
+ name: "SUBDOMAIN_PREFIX",
48
+ required: true,
49
+ secret: false,
50
+ section: "Cloudflare Account",
51
+ default: "cryptolinx",
52
+ hint: "Prefix for worker URLs",
53
+ },
54
+ {
55
+ name: "D1_INTERNAL_KEY",
56
+ required: true,
57
+ secret: true,
58
+ section: "Internal Auth",
59
+ hint: "Internal key for D1 worker auth",
60
+ },
61
+ {
62
+ name: "TRADE_INTERNAL_KEY",
63
+ required: true,
64
+ secret: true,
65
+ section: "Internal Auth",
66
+ hint: "Internal key for trade worker auth",
67
+ },
68
+ {
69
+ name: "AGENT_INTERNAL_KEY",
70
+ required: true,
71
+ secret: true,
72
+ section: "Internal Auth",
73
+ hint: "Internal key for agent worker auth",
74
+ },
75
+ {
76
+ name: "WEBHOOK_API_KEY_BINDING",
77
+ required: true,
78
+ secret: true,
79
+ section: "Internal Auth",
80
+ hint: "Webhook auth key for hoox gateway",
81
+ },
82
+ {
83
+ name: "INTERNAL_KEY_BINDING",
84
+ required: true,
85
+ secret: true,
86
+ section: "Internal Auth",
87
+ hint: "Shared internal auth key for inter-worker communication",
88
+ },
89
+ {
90
+ name: "API_SERVICE_KEY",
91
+ required: true,
92
+ secret: true,
93
+ section: "Internal Auth",
94
+ hint: "API service key for trade-worker",
95
+ },
96
+ {
97
+ name: "HA_TOKEN_BINDING",
98
+ required: false,
99
+ secret: true,
100
+ section: "Internal Auth",
101
+ hint: "Home Assistant token for hoox",
102
+ },
103
+ {
104
+ name: "TELEGRAM_BOT_TOKEN",
105
+ required: false,
106
+ secret: true,
107
+ section: "Telegram",
108
+ hint: "From @BotFather",
109
+ },
110
+ {
111
+ name: "TELEGRAM_SECRET_TOKEN",
112
+ required: false,
113
+ secret: true,
114
+ section: "Telegram",
115
+ hint: "Telegram webhook secret token",
116
+ },
117
+ {
118
+ name: "AGENT_OPENAI_KEY",
119
+ required: false,
120
+ secret: true,
121
+ section: "AI Providers",
122
+ hint: "OpenAI API key",
123
+ },
124
+ {
125
+ name: "AGENT_ANTHROPIC_KEY",
126
+ required: false,
127
+ secret: true,
128
+ section: "AI Providers",
129
+ hint: "Anthropic API key",
130
+ },
131
+ {
132
+ name: "AGENT_GOOGLE_KEY",
133
+ required: false,
134
+ secret: true,
135
+ section: "AI Providers",
136
+ hint: "Google AI API key",
137
+ },
138
+ {
139
+ name: "BINANCE_API_KEY",
140
+ required: false,
141
+ secret: true,
142
+ section: "Exchanges",
143
+ hint: "Binance API key",
144
+ },
145
+ {
146
+ name: "BINANCE_API_SECRET",
147
+ required: false,
148
+ secret: true,
149
+ section: "Exchanges",
150
+ hint: "Binance API secret",
151
+ },
152
+ {
153
+ name: "MEXC_API_KEY",
154
+ required: false,
155
+ secret: true,
156
+ section: "Exchanges",
157
+ hint: "MEXC API key",
158
+ },
159
+ {
160
+ name: "MEXC_API_SECRET",
161
+ required: false,
162
+ secret: true,
163
+ section: "Exchanges",
164
+ hint: "MEXC API secret",
165
+ },
166
+ {
167
+ name: "BYBIT_API_KEY",
168
+ required: false,
169
+ secret: true,
170
+ section: "Exchanges",
171
+ hint: "Bybit API key",
172
+ },
173
+ {
174
+ name: "BYBIT_API_SECRET",
175
+ required: false,
176
+ secret: true,
177
+ section: "Exchanges",
178
+ hint: "Bybit API secret",
179
+ },
46
180
  // Email
47
- { name: "EMAIL_HOST", required: false, secret: true, section: "Email", hint: "Email IMAP server host" },
48
- { name: "EMAIL_USER", required: false, secret: true, section: "Email", hint: "Email IMAP username" },
49
- { name: "EMAIL_PASS", required: false, secret: true, section: "Email", hint: "Email IMAP password" },
50
- { name: "INTERNAL_KEY", required: false, secret: true, section: "Email", hint: "Internal auth key for email-worker" },
181
+ {
182
+ name: "EMAIL_HOST",
183
+ required: false,
184
+ secret: true,
185
+ section: "Email",
186
+ hint: "Email IMAP server host",
187
+ },
188
+ {
189
+ name: "EMAIL_USER",
190
+ required: false,
191
+ secret: true,
192
+ section: "Email",
193
+ hint: "Email IMAP username",
194
+ },
195
+ {
196
+ name: "EMAIL_PASS",
197
+ required: false,
198
+ secret: true,
199
+ section: "Email",
200
+ hint: "Email IMAP password",
201
+ },
202
+ {
203
+ name: "INTERNAL_KEY",
204
+ required: false,
205
+ secret: true,
206
+ section: "Email",
207
+ hint: "Internal auth key for email-worker",
208
+ },
51
209
  // Wallet
52
- { name: "WALLET_MNEMONIC_SECRET", required: false, secret: true, section: "Wallet", hint: "Wallet mnemonic phrase for web3-wallet-worker" },
53
- { name: "WALLET_PK_SECRET", required: false, secret: true, section: "Wallet", hint: "Wallet private key for web3-wallet-worker" },
54
- { name: "DASHBOARD_USER", required: true, secret: false, section: "Dashboard", default: "admin", hint: "Dashboard login username" },
55
- { name: "DASHBOARD_PASS", required: true, secret: true, section: "Dashboard", hint: "Dashboard login password" },
56
- { name: "SESSION_SECRET", required: true, secret: true, section: "Dashboard", hint: "32+ char random string for session signing" },
210
+ {
211
+ name: "WALLET_MNEMONIC_SECRET",
212
+ required: false,
213
+ secret: true,
214
+ section: "Wallet",
215
+ hint: "Wallet mnemonic phrase for web3-wallet-worker",
216
+ },
217
+ {
218
+ name: "WALLET_PK_SECRET",
219
+ required: false,
220
+ secret: true,
221
+ section: "Wallet",
222
+ hint: "Wallet private key for web3-wallet-worker",
223
+ },
224
+ {
225
+ name: "DASHBOARD_USER",
226
+ required: true,
227
+ secret: false,
228
+ section: "Dashboard",
229
+ default: "admin",
230
+ hint: "Dashboard login username",
231
+ },
232
+ {
233
+ name: "DASHBOARD_PASS",
234
+ required: true,
235
+ secret: true,
236
+ section: "Dashboard",
237
+ hint: "Dashboard login password",
238
+ },
239
+ {
240
+ name: "SESSION_SECRET",
241
+ required: true,
242
+ secret: true,
243
+ section: "Dashboard",
244
+ hint: "32+ char random string for session signing",
245
+ },
57
246
  ];
58
247
  }
59
248
 
60
249
  static getSections(): string[] {
61
- return [...new Set(EnvService.getDefinitions().map(d => d.section))];
250
+ return [...new Set(EnvService.getDefinitions().map((d) => d.section))];
62
251
  }
63
252
 
64
- static async loadDotEnvAsync(filePath: string): Promise<Record<string, string>> {
253
+ static async loadDotEnvAsync(
254
+ filePath: string
255
+ ): Promise<Record<string, string>> {
65
256
  const file = Bun.file(filePath);
66
257
  if (!(await file.exists())) return {};
67
258
  const text = await file.text();
@@ -73,7 +264,10 @@ export class EnvService {
73
264
  if (eqIdx < 1) continue;
74
265
  const key = trimmed.substring(0, eqIdx).trim();
75
266
  let value = trimmed.substring(eqIdx + 1).trim();
76
- if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
267
+ if (
268
+ (value.startsWith('"') && value.endsWith('"')) ||
269
+ (value.startsWith("'") && value.endsWith("'"))
270
+ ) {
77
271
  value = value.slice(1, -1);
78
272
  }
79
273
  vars[key] = value;
@@ -102,11 +296,15 @@ export class EnvService {
102
296
  const v = vars ?? {};
103
297
  const defs = EnvService.getDefinitions();
104
298
  const sections = EnvService.getSections();
105
- let output = "# Hoox Local Environment Configuration\n# Generated by `hoox config env init`.\n# NEVER commit this file.\n\n";
299
+ let output =
300
+ "# Hoox Local Environment Configuration\n# Generated by `hoox config env init`.\n# NEVER commit this file.\n\n";
106
301
  for (const section of sections) {
107
302
  output += `# --- ${section.toUpperCase()} ---\n`;
108
- for (const def of defs.filter(d => d.section === section)) {
109
- const val = v[def.name] !== undefined ? v[def.name] : (def.default ?? `your_${def.name.toLowerCase()}`);
303
+ for (const def of defs.filter((d) => d.section === section)) {
304
+ const val =
305
+ v[def.name] !== undefined
306
+ ? v[def.name]
307
+ : (def.default ?? `your_${def.name.toLowerCase()}`);
110
308
  output += `${def.name}="${val}"\n`;
111
309
  }
112
310
  output += "\n";
@@ -114,15 +312,46 @@ export class EnvService {
114
312
  return output;
115
313
  }
116
314
 
117
- static getWorkerDevVars(vars: Record<string, string>): Record<string, Record<string, string>> {
315
+ static getWorkerDevVars(
316
+ vars: Record<string, string>
317
+ ): Record<string, Record<string, string>> {
118
318
  const workerMap: Record<string, string[]> = {
119
- "workers/hoox": ["WEBHOOK_API_KEY_BINDING", "INTERNAL_KEY_BINDING", "HA_TOKEN_BINDING"],
120
- "workers/trade-worker": ["API_SERVICE_KEY", "INTERNAL_KEY_BINDING", "BINANCE_API_KEY", "BINANCE_API_SECRET", "MEXC_API_KEY", "MEXC_API_SECRET", "BYBIT_API_KEY", "BYBIT_API_SECRET"],
121
- "workers/agent-worker": ["AGENT_INTERNAL_KEY", "AGENT_OPENAI_KEY", "AGENT_ANTHROPIC_KEY", "AGENT_GOOGLE_KEY"],
319
+ "workers/hoox": [
320
+ "WEBHOOK_API_KEY_BINDING",
321
+ "INTERNAL_KEY_BINDING",
322
+ "HA_TOKEN_BINDING",
323
+ ],
324
+ "workers/trade-worker": [
325
+ "API_SERVICE_KEY",
326
+ "INTERNAL_KEY_BINDING",
327
+ "BINANCE_API_KEY",
328
+ "BINANCE_API_SECRET",
329
+ "MEXC_API_KEY",
330
+ "MEXC_API_SECRET",
331
+ "BYBIT_API_KEY",
332
+ "BYBIT_API_SECRET",
333
+ ],
334
+ "workers/agent-worker": [
335
+ "AGENT_INTERNAL_KEY",
336
+ "AGENT_OPENAI_KEY",
337
+ "AGENT_ANTHROPIC_KEY",
338
+ "AGENT_GOOGLE_KEY",
339
+ ],
122
340
  "workers/d1-worker": ["D1_INTERNAL_KEY"],
123
- "workers/telegram-worker": ["TELEGRAM_BOT_TOKEN", "TELEGRAM_SECRET_TOKEN"],
124
- "workers/web3-wallet-worker": ["WALLET_MNEMONIC_SECRET", "WALLET_PK_SECRET"],
125
- "workers/email-worker": ["EMAIL_HOST", "EMAIL_USER", "EMAIL_PASS", "INTERNAL_KEY"],
341
+ "workers/telegram-worker": [
342
+ "TELEGRAM_BOT_TOKEN",
343
+ "TELEGRAM_SECRET_TOKEN",
344
+ ],
345
+ "workers/web3-wallet-worker": [
346
+ "WALLET_MNEMONIC_SECRET",
347
+ "WALLET_PK_SECRET",
348
+ ],
349
+ "workers/email-worker": [
350
+ "EMAIL_HOST",
351
+ "EMAIL_USER",
352
+ "EMAIL_PASS",
353
+ "INTERNAL_KEY",
354
+ ],
126
355
  "workers/analytics-worker": ["CLOUDFLARE_API_TOKEN"],
127
356
  };
128
357
  const result: Record<string, Record<string, string>> = {};
@@ -144,9 +373,10 @@ export class EnvService {
144
373
  let output = "";
145
374
  for (const section of sections) {
146
375
  output += `\n${section}:\n`;
147
- for (const def of defs.filter(d => d.section === section)) {
376
+ for (const def of defs.filter((d) => d.section === section)) {
148
377
  const val = vars[def.name];
149
- const displayVal = def.secret && val ? "********" : (val ?? "(not set)");
378
+ const displayVal =
379
+ def.secret && val ? "********" : (val ?? "(not set)");
150
380
  const status = val ? " [set]" : " [missing]";
151
381
  output += ` ${def.name}=${displayVal}${def.secret ? " (secret)" : ""}${status}\n`;
152
382
  }