@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.
- package/README.md +2 -0
- package/package.json +5 -3
- package/src/commands/check/check-command.test.ts +28 -20
- package/src/commands/check/prerequisites-command.ts +9 -5
- package/src/commands/clone/clone-command.ts +1 -10
- package/src/commands/config/config-command.ts +7 -11
- package/src/commands/config/env-command.ts +16 -35
- package/src/commands/config/kv-command.ts +33 -72
- package/src/commands/db/db-command.ts +23 -25
- package/src/commands/deploy/deploy-command.ts +133 -52
- package/src/commands/deploy/telegram-service.ts +21 -6
- package/src/commands/dev/dev-command.ts +7 -16
- package/src/commands/infra/infra-command.test.ts +2 -2
- package/src/commands/infra/infra-command.ts +45 -11
- package/src/commands/init/init-command.test.ts +2 -2
- package/src/commands/monitor/monitor-command.test.ts +44 -16
- package/src/commands/monitor/monitor-command.ts +45 -21
- package/src/commands/monitor/monitor-service.ts +19 -4
- package/src/commands/repair/repair-command.test.ts +35 -15
- package/src/commands/repair/repair-command.ts +39 -18
- package/src/commands/repair/repair-service.ts +48 -12
- package/src/commands/test/test-command.ts +4 -10
- package/src/commands/tui/index.ts +1 -0
- package/src/commands/tui/tui-command.ts +87 -0
- package/src/commands/update/index.ts +1 -0
- package/src/commands/update/update-command.ts +51 -0
- package/src/commands/waf/waf-command.test.ts +1 -1
- package/src/commands/waf/waf-command.ts +11 -11
- package/src/index.ts +52 -1
- package/src/services/cloudflare/cloudflare-service.test.ts +12 -8
- package/src/services/cloudflare/cloudflare-service.ts +8 -10
- package/src/services/cloudflare/types.ts +6 -5
- package/src/services/db/db-service.ts +8 -17
- package/src/services/env/env-service.test.ts +41 -15
- package/src/services/env/env-service.ts +276 -46
- package/src/services/kv/kv-sync-service.ts +117 -34
- package/src/services/prerequisites/prerequisites-service.ts +137 -29
- package/src/services/secrets/index.ts +0 -1
- package/src/services/secrets/secrets-service.test.ts +34 -25
- package/src/services/secrets/secrets-service.ts +10 -16
- package/src/services/secrets/types.ts +4 -11
- package/src/services/update/index.ts +2 -0
- package/src/services/update/update-service.test.ts +76 -0
- package/src/services/update/update-service.ts +193 -0
- package/src/ui/banner.ts +5 -5
- package/src/ui/menu.ts +6 -1
- package/src/utils/formatters.ts +21 -4
- 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
|
|
94
|
-
(r: Record<string, unknown>) => String(r.name ?? "")
|
|
95
|
-
|
|
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({
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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({
|
|
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({
|
|
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(
|
|
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(
|
|
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,
|
|
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(
|
|
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(
|
|
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,
|
|
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
|
-
{
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
{
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
{
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
{
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
{
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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(
|
|
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 (
|
|
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 =
|
|
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 =
|
|
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(
|
|
315
|
+
static getWorkerDevVars(
|
|
316
|
+
vars: Record<string, string>
|
|
317
|
+
): Record<string, Record<string, string>> {
|
|
118
318
|
const workerMap: Record<string, string[]> = {
|
|
119
|
-
"workers/hoox": [
|
|
120
|
-
|
|
121
|
-
|
|
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": [
|
|
124
|
-
|
|
125
|
-
|
|
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 =
|
|
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
|
}
|