@jango-blockchained/hoox-cli 0.3.4 → 0.3.5
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
|
@@ -25,7 +25,10 @@ export class KvSyncService {
|
|
|
25
25
|
|
|
26
26
|
if (exitCode === 0) {
|
|
27
27
|
try {
|
|
28
|
-
const namespaces = JSON.parse(stdout) as Array<{
|
|
28
|
+
const namespaces = JSON.parse(stdout) as Array<{
|
|
29
|
+
id: string;
|
|
30
|
+
title: string;
|
|
31
|
+
}>;
|
|
29
32
|
const configKv = namespaces.find((n) => n.title === "CONFIG_KV");
|
|
30
33
|
if (configKv) return configKv.id;
|
|
31
34
|
} catch {
|
|
@@ -37,23 +40,21 @@ export class KvSyncService {
|
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
throw new Error(
|
|
40
|
-
"Could not resolve CONFIG_KV namespace ID. Provide --namespace-id flag."
|
|
43
|
+
"Could not resolve CONFIG_KV namespace ID. Provide --namespace-id flag."
|
|
41
44
|
);
|
|
42
45
|
}
|
|
43
46
|
|
|
44
|
-
async list(
|
|
45
|
-
namespaceId: string,
|
|
46
|
-
): Promise<Array<{ name: string }>> {
|
|
47
|
+
async list(namespaceId: string): Promise<Array<{ name: string }>> {
|
|
47
48
|
const proc = Bun.spawn(
|
|
48
49
|
["wrangler", "kv:key", "list", "--namespace-id", namespaceId],
|
|
49
|
-
{ stdout: "pipe", stderr: "pipe" }
|
|
50
|
+
{ stdout: "pipe", stderr: "pipe" }
|
|
50
51
|
);
|
|
51
52
|
const stdout = await new Response(proc.stdout).text();
|
|
52
53
|
const exitCode = await proc.exited;
|
|
53
54
|
|
|
54
55
|
if (exitCode !== 0) {
|
|
55
56
|
throw new Error(
|
|
56
|
-
`Failed to list KV keys: ${stdout.trim() || `exit ${exitCode}`}
|
|
57
|
+
`Failed to list KV keys: ${stdout.trim() || `exit ${exitCode}`}`
|
|
57
58
|
);
|
|
58
59
|
}
|
|
59
60
|
|
|
@@ -71,7 +72,7 @@ export class KvSyncService {
|
|
|
71
72
|
async get(namespaceId: string, key: string): Promise<string | null> {
|
|
72
73
|
const proc = Bun.spawn(
|
|
73
74
|
["wrangler", "kv:key", "get", "--namespace-id", namespaceId, key],
|
|
74
|
-
{ stdout: "pipe", stderr: "pipe" }
|
|
75
|
+
{ stdout: "pipe", stderr: "pipe" }
|
|
75
76
|
);
|
|
76
77
|
const stdout = await new Response(proc.stdout).text();
|
|
77
78
|
const stderr = await new Response(proc.stderr).text();
|
|
@@ -80,28 +81,24 @@ export class KvSyncService {
|
|
|
80
81
|
if (exitCode !== 0) {
|
|
81
82
|
if (stderr.includes("not found")) return null;
|
|
82
83
|
throw new Error(
|
|
83
|
-
`Failed to get key "${key}": ${stderr.trim() || `exit ${exitCode}`}
|
|
84
|
+
`Failed to get key "${key}": ${stderr.trim() || `exit ${exitCode}`}`
|
|
84
85
|
);
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
return stdout.trim() || null;
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
async set(
|
|
91
|
-
namespaceId: string,
|
|
92
|
-
key: string,
|
|
93
|
-
value: string,
|
|
94
|
-
): Promise<void> {
|
|
91
|
+
async set(namespaceId: string, key: string, value: string): Promise<void> {
|
|
95
92
|
const proc = Bun.spawn(
|
|
96
93
|
["wrangler", "kv:key", "put", "--namespace-id", namespaceId, key, value],
|
|
97
|
-
{ stdout: "pipe", stderr: "pipe" }
|
|
94
|
+
{ stdout: "pipe", stderr: "pipe" }
|
|
98
95
|
);
|
|
99
96
|
const exitCode = await proc.exited;
|
|
100
97
|
const stderr = await new Response(proc.stderr).text();
|
|
101
98
|
|
|
102
99
|
if (exitCode !== 0) {
|
|
103
100
|
throw new Error(
|
|
104
|
-
`Failed to set key "${key}": ${stderr.trim() || `exit ${exitCode}`}
|
|
101
|
+
`Failed to set key "${key}": ${stderr.trim() || `exit ${exitCode}`}`
|
|
105
102
|
);
|
|
106
103
|
}
|
|
107
104
|
}
|
|
@@ -109,14 +106,14 @@ export class KvSyncService {
|
|
|
109
106
|
async delete(namespaceId: string, key: string): Promise<void> {
|
|
110
107
|
const proc = Bun.spawn(
|
|
111
108
|
["wrangler", "kv:key", "delete", "--namespace-id", namespaceId, key],
|
|
112
|
-
{ stdout: "pipe", stderr: "pipe" }
|
|
109
|
+
{ stdout: "pipe", stderr: "pipe" }
|
|
113
110
|
);
|
|
114
111
|
const exitCode = await proc.exited;
|
|
115
112
|
const stderr = await new Response(proc.stderr).text();
|
|
116
113
|
|
|
117
114
|
if (exitCode !== 0) {
|
|
118
115
|
throw new Error(
|
|
119
|
-
`Failed to delete key "${key}": ${stderr.trim() || `exit ${exitCode}`}
|
|
116
|
+
`Failed to delete key "${key}": ${stderr.trim() || `exit ${exitCode}`}`
|
|
120
117
|
);
|
|
121
118
|
}
|
|
122
119
|
}
|
|
@@ -125,22 +122,108 @@ export class KvSyncService {
|
|
|
125
122
|
return {
|
|
126
123
|
namespace: "CONFIG_KV",
|
|
127
124
|
keys: [
|
|
128
|
-
{
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
{
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
{
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
125
|
+
{
|
|
126
|
+
key: "webhook:tradingview:ip_check_enabled",
|
|
127
|
+
type: "boolean",
|
|
128
|
+
default: "false",
|
|
129
|
+
description: "Enable TradingView IP allowlist check",
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
key: "webhook:allowed_ips",
|
|
133
|
+
type: "string",
|
|
134
|
+
default: "",
|
|
135
|
+
description: "Comma-separated allowed IPs for webhook",
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
key: "routing:dynamic:enabled",
|
|
139
|
+
type: "boolean",
|
|
140
|
+
default: "false",
|
|
141
|
+
description: "Enable dynamic routing",
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
key: "trade:max_daily_drawdown_percent",
|
|
145
|
+
type: "number",
|
|
146
|
+
default: "10",
|
|
147
|
+
description: "Max daily loss percentage before halt",
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
key: "trade:kill_switch",
|
|
151
|
+
type: "boolean",
|
|
152
|
+
default: "false",
|
|
153
|
+
description: "Emergency stop all trading",
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
key: "trade:watermark:{exchange}:{symbol}:{side}",
|
|
157
|
+
type: "number",
|
|
158
|
+
default: "",
|
|
159
|
+
description: "Per-market watermark (template key)",
|
|
160
|
+
secret: true,
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
key: "agent:openai_key",
|
|
164
|
+
type: "string",
|
|
165
|
+
default: "",
|
|
166
|
+
description: "OpenAI API key for AI agent",
|
|
167
|
+
secret: true,
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
key: "agent:anthropic_key",
|
|
171
|
+
type: "string",
|
|
172
|
+
default: "",
|
|
173
|
+
description: "Anthropic API key for AI agent",
|
|
174
|
+
secret: true,
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
key: "agent:google_key",
|
|
178
|
+
type: "string",
|
|
179
|
+
default: "",
|
|
180
|
+
description: "Google AI API key for agent",
|
|
181
|
+
secret: true,
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
key: "agent:azure_api_key",
|
|
185
|
+
type: "string",
|
|
186
|
+
default: "",
|
|
187
|
+
description: "Azure OpenAI API key",
|
|
188
|
+
secret: true,
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
key: "agent:azure_endpoint",
|
|
192
|
+
type: "string",
|
|
193
|
+
default: "",
|
|
194
|
+
description: "Azure OpenAI endpoint",
|
|
195
|
+
secret: true,
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
key: "email:scan_subject",
|
|
199
|
+
type: "string",
|
|
200
|
+
default: "",
|
|
201
|
+
description: "Email subject filter for signal scanning",
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
key: "email:coin_pattern",
|
|
205
|
+
type: "string",
|
|
206
|
+
default: "",
|
|
207
|
+
description: "Regex pattern to extract coin from email",
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
key: "email:action_pattern",
|
|
211
|
+
type: "string",
|
|
212
|
+
default: "",
|
|
213
|
+
description: "Regex pattern to extract action from email",
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
key: "email:quantity_multiplier",
|
|
217
|
+
type: "number",
|
|
218
|
+
default: "1",
|
|
219
|
+
description: "Multiplier for email signal quantity",
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
key: "email:use_imap",
|
|
223
|
+
type: "boolean",
|
|
224
|
+
default: "false",
|
|
225
|
+
description: "Use IMAP for email scanning",
|
|
226
|
+
},
|
|
144
227
|
],
|
|
145
228
|
};
|
|
146
229
|
}
|
|
@@ -100,14 +100,27 @@ export class PrerequisitesService {
|
|
|
100
100
|
async checkBun(): Promise<PrerequisiteCheck> {
|
|
101
101
|
const base = { name: "Bun", category: "tool" as const, required: ">=1.2" };
|
|
102
102
|
try {
|
|
103
|
-
const proc = Bun.spawn(["bun", "--version"], {
|
|
103
|
+
const proc = Bun.spawn(["bun", "--version"], {
|
|
104
|
+
stdout: "pipe",
|
|
105
|
+
stderr: "pipe",
|
|
106
|
+
});
|
|
104
107
|
const stdout = await new Response(proc.stdout).text();
|
|
105
108
|
const version = stdout.trim();
|
|
106
109
|
const [major, minor] = version.split(".").map(Number);
|
|
107
110
|
const passed = (major ?? 0) >= 1 && (minor ?? 0) >= 2;
|
|
108
|
-
return {
|
|
111
|
+
return {
|
|
112
|
+
...base,
|
|
113
|
+
passed,
|
|
114
|
+
version: version || "not found",
|
|
115
|
+
hint: passed ? undefined : "Install: curl -fsSL https://bun.sh | bash",
|
|
116
|
+
};
|
|
109
117
|
} catch {
|
|
110
|
-
return {
|
|
118
|
+
return {
|
|
119
|
+
...base,
|
|
120
|
+
passed: false,
|
|
121
|
+
version: "not found",
|
|
122
|
+
hint: "Install: curl -fsSL https://bun.sh | bash",
|
|
123
|
+
};
|
|
111
124
|
}
|
|
112
125
|
}
|
|
113
126
|
|
|
@@ -118,15 +131,28 @@ export class PrerequisitesService {
|
|
|
118
131
|
async checkGit(): Promise<PrerequisiteCheck> {
|
|
119
132
|
const base = { name: "Git", category: "tool" as const, required: ">=2.40" };
|
|
120
133
|
try {
|
|
121
|
-
const proc = Bun.spawn(["git", "--version"], {
|
|
134
|
+
const proc = Bun.spawn(["git", "--version"], {
|
|
135
|
+
stdout: "pipe",
|
|
136
|
+
stderr: "pipe",
|
|
137
|
+
});
|
|
122
138
|
const stdout = await new Response(proc.stdout).text();
|
|
123
139
|
const match = stdout.match(/(\d+\.\d+)/);
|
|
124
140
|
const version = match ? match[1] : "unknown";
|
|
125
141
|
const [major, minor] = version.split(".").map(Number);
|
|
126
142
|
const passed = (major ?? 0) >= 2 && (minor ?? 0) >= 40;
|
|
127
|
-
return {
|
|
143
|
+
return {
|
|
144
|
+
...base,
|
|
145
|
+
passed,
|
|
146
|
+
version,
|
|
147
|
+
hint: passed ? undefined : "Install: apt install git",
|
|
148
|
+
};
|
|
128
149
|
} catch {
|
|
129
|
-
return {
|
|
150
|
+
return {
|
|
151
|
+
...base,
|
|
152
|
+
passed: false,
|
|
153
|
+
version: "not found",
|
|
154
|
+
hint: "Install: apt install git",
|
|
155
|
+
};
|
|
130
156
|
}
|
|
131
157
|
}
|
|
132
158
|
|
|
@@ -135,16 +161,33 @@ export class PrerequisitesService {
|
|
|
135
161
|
* Runs `node --version` if available, skips gracefully if not installed.
|
|
136
162
|
*/
|
|
137
163
|
async checkNode(): Promise<PrerequisiteCheck> {
|
|
138
|
-
const base = {
|
|
164
|
+
const base = {
|
|
165
|
+
name: "Node.js",
|
|
166
|
+
category: "tool" as const,
|
|
167
|
+
required: ">=18 (optional)",
|
|
168
|
+
};
|
|
139
169
|
try {
|
|
140
|
-
const proc = Bun.spawn(["node", "--version"], {
|
|
170
|
+
const proc = Bun.spawn(["node", "--version"], {
|
|
171
|
+
stdout: "pipe",
|
|
172
|
+
stderr: "pipe",
|
|
173
|
+
});
|
|
141
174
|
const stdout = await new Response(proc.stdout).text();
|
|
142
175
|
const version = stdout.trim().replace(/^v/, "");
|
|
143
176
|
const [major] = version.split(".").map(Number);
|
|
144
177
|
const passed = (major ?? 0) >= 18;
|
|
145
|
-
return {
|
|
178
|
+
return {
|
|
179
|
+
...base,
|
|
180
|
+
passed,
|
|
181
|
+
version: version || "not found",
|
|
182
|
+
hint: passed ? undefined : "Install: nvm or https://nodejs.org",
|
|
183
|
+
};
|
|
146
184
|
} catch {
|
|
147
|
-
return {
|
|
185
|
+
return {
|
|
186
|
+
...base,
|
|
187
|
+
passed: true,
|
|
188
|
+
version: "not installed",
|
|
189
|
+
hint: "Optional — not required when using Bun",
|
|
190
|
+
};
|
|
148
191
|
}
|
|
149
192
|
}
|
|
150
193
|
|
|
@@ -153,16 +196,27 @@ export class PrerequisitesService {
|
|
|
153
196
|
* Delegates to the existing method to avoid duplicating semver logic.
|
|
154
197
|
*/
|
|
155
198
|
async checkWrangler(): Promise<PrerequisiteCheck> {
|
|
156
|
-
const base = {
|
|
199
|
+
const base = {
|
|
200
|
+
name: "Wrangler CLI",
|
|
201
|
+
category: "tool" as const,
|
|
202
|
+
required: `>=${this.MINIMUM_WRANGLER}`,
|
|
203
|
+
};
|
|
157
204
|
const result = await this.checkWranglerVersion();
|
|
158
205
|
if (!result.current) {
|
|
159
|
-
return {
|
|
206
|
+
return {
|
|
207
|
+
...base,
|
|
208
|
+
passed: false,
|
|
209
|
+
version: "not found",
|
|
210
|
+
hint: "Install: bun add -g wrangler",
|
|
211
|
+
};
|
|
160
212
|
}
|
|
161
213
|
return {
|
|
162
214
|
...base,
|
|
163
215
|
passed: !result.outdated,
|
|
164
216
|
version: result.current,
|
|
165
|
-
hint: result.outdated
|
|
217
|
+
hint: result.outdated
|
|
218
|
+
? `Update: bun add -g wrangler@latest (${result.current} < ${result.minimum})`
|
|
219
|
+
: undefined,
|
|
166
220
|
};
|
|
167
221
|
}
|
|
168
222
|
|
|
@@ -171,9 +225,16 @@ export class PrerequisitesService {
|
|
|
171
225
|
* Verifies the CLI is authenticated and extracts the user email.
|
|
172
226
|
*/
|
|
173
227
|
async checkCloudflareAuth(): Promise<PrerequisiteCheck> {
|
|
174
|
-
const base = {
|
|
228
|
+
const base = {
|
|
229
|
+
name: "Cloudflare Auth",
|
|
230
|
+
category: "account" as const,
|
|
231
|
+
required: "wrangler whoami",
|
|
232
|
+
};
|
|
175
233
|
try {
|
|
176
|
-
const proc = Bun.spawn(["wrangler", "whoami"], {
|
|
234
|
+
const proc = Bun.spawn(["wrangler", "whoami"], {
|
|
235
|
+
stdout: "pipe",
|
|
236
|
+
stderr: "pipe",
|
|
237
|
+
});
|
|
177
238
|
const stdout = await new Response(proc.stdout).text();
|
|
178
239
|
const exitCode = await proc.exited;
|
|
179
240
|
const passed = exitCode === 0 && !stdout.includes("not authenticated");
|
|
@@ -185,7 +246,12 @@ export class PrerequisitesService {
|
|
|
185
246
|
hint: passed ? undefined : "Run: wrangler login",
|
|
186
247
|
};
|
|
187
248
|
} catch {
|
|
188
|
-
return {
|
|
249
|
+
return {
|
|
250
|
+
...base,
|
|
251
|
+
passed: false,
|
|
252
|
+
version: "not authenticated",
|
|
253
|
+
hint: "Run: wrangler login",
|
|
254
|
+
};
|
|
189
255
|
}
|
|
190
256
|
}
|
|
191
257
|
|
|
@@ -194,23 +260,44 @@ export class PrerequisitesService {
|
|
|
194
260
|
* Also checks `docker compose` availability separately.
|
|
195
261
|
*/
|
|
196
262
|
async checkDocker(): Promise<PrerequisiteCheck> {
|
|
197
|
-
const base = {
|
|
263
|
+
const base = {
|
|
264
|
+
name: "Docker",
|
|
265
|
+
category: "tool" as const,
|
|
266
|
+
required: "optional",
|
|
267
|
+
};
|
|
198
268
|
try {
|
|
199
|
-
const proc = Bun.spawn(["docker", "--version"], {
|
|
269
|
+
const proc = Bun.spawn(["docker", "--version"], {
|
|
270
|
+
stdout: "pipe",
|
|
271
|
+
stderr: "pipe",
|
|
272
|
+
});
|
|
200
273
|
const stdout = await new Response(proc.stdout).text();
|
|
201
|
-
const version = stdout
|
|
274
|
+
const version = stdout
|
|
275
|
+
.trim()
|
|
276
|
+
.replace(/^Docker version /, "")
|
|
277
|
+
.replace(/,.*$/, "");
|
|
202
278
|
let composeVersion = "";
|
|
203
279
|
try {
|
|
204
|
-
const composeProc = Bun.spawn(["docker", "compose", "version"], {
|
|
280
|
+
const composeProc = Bun.spawn(["docker", "compose", "version"], {
|
|
281
|
+
stdout: "pipe",
|
|
282
|
+
stderr: "pipe",
|
|
283
|
+
});
|
|
205
284
|
composeVersion = (await new Response(composeProc.stdout).text()).trim();
|
|
206
|
-
} catch {
|
|
285
|
+
} catch {
|
|
286
|
+
/* compose optional */
|
|
287
|
+
}
|
|
207
288
|
return {
|
|
208
289
|
...base,
|
|
209
290
|
passed: true,
|
|
210
|
-
version:
|
|
291
|
+
version:
|
|
292
|
+
version + (composeVersion ? ` (compose: yes)` : " (compose: no)"),
|
|
211
293
|
};
|
|
212
294
|
} catch {
|
|
213
|
-
return {
|
|
295
|
+
return {
|
|
296
|
+
...base,
|
|
297
|
+
passed: true,
|
|
298
|
+
version: "not installed",
|
|
299
|
+
hint: "Optional — used for Docker dev runtime",
|
|
300
|
+
};
|
|
214
301
|
}
|
|
215
302
|
}
|
|
216
303
|
|
|
@@ -218,28 +305,49 @@ export class PrerequisitesService {
|
|
|
218
305
|
* Check repository integrity: wrangler.jsonc exists, .env file present, submodules initialized.
|
|
219
306
|
*/
|
|
220
307
|
async checkRepository(): Promise<PrerequisiteCheck> {
|
|
221
|
-
const base = {
|
|
308
|
+
const base = {
|
|
309
|
+
name: "Repository",
|
|
310
|
+
category: "repository" as const,
|
|
311
|
+
required: "valid",
|
|
312
|
+
};
|
|
222
313
|
try {
|
|
223
314
|
const wranglerExists = await Bun.file("wrangler.jsonc").exists();
|
|
224
|
-
const envExists =
|
|
315
|
+
const envExists =
|
|
316
|
+
(await Bun.file(".env.local").exists()) ||
|
|
317
|
+
(await Bun.file(".env.example").exists());
|
|
225
318
|
const issues: string[] = [];
|
|
226
319
|
if (!wranglerExists) issues.push("wrangler.jsonc not found");
|
|
227
320
|
if (!envExists) issues.push("no .env.local or .env.example found");
|
|
228
321
|
let submoduleOk = false;
|
|
229
322
|
try {
|
|
230
|
-
const proc = Bun.spawn(["git", "submodule", "status"], {
|
|
323
|
+
const proc = Bun.spawn(["git", "submodule", "status"], {
|
|
324
|
+
stdout: "pipe",
|
|
325
|
+
stderr: "pipe",
|
|
326
|
+
});
|
|
231
327
|
const stdout = await new Response(proc.stdout).text();
|
|
232
328
|
submoduleOk = !stdout.startsWith("-");
|
|
233
|
-
} catch {
|
|
329
|
+
} catch {
|
|
330
|
+
/* not a git repo */
|
|
331
|
+
}
|
|
234
332
|
const passed = wranglerExists && issues.length === 0;
|
|
235
333
|
return {
|
|
236
334
|
...base,
|
|
237
335
|
passed,
|
|
238
|
-
version:
|
|
336
|
+
version:
|
|
337
|
+
issues.length > 0
|
|
338
|
+
? issues.join("; ")
|
|
339
|
+
: submoduleOk
|
|
340
|
+
? "OK"
|
|
341
|
+
: "submodules may need init",
|
|
239
342
|
hint: passed ? undefined : "Run: hoox init or hoox clone --all",
|
|
240
343
|
};
|
|
241
344
|
} catch {
|
|
242
|
-
return {
|
|
345
|
+
return {
|
|
346
|
+
...base,
|
|
347
|
+
passed: false,
|
|
348
|
+
version: "check failed",
|
|
349
|
+
hint: "Check repository structure",
|
|
350
|
+
};
|
|
243
351
|
}
|
|
244
352
|
}
|
|
245
353
|
|
|
@@ -2,12 +2,31 @@ import { describe, it, expect, mock } from "bun:test";
|
|
|
2
2
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import type { Result } from "./types.js";
|
|
5
6
|
import { SecretsService } from "./secrets-service.js";
|
|
6
7
|
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
8
9
|
// Helpers
|
|
9
10
|
// ---------------------------------------------------------------------------
|
|
10
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Narrow a discriminated union Result<T> after checking ok.
|
|
14
|
+
* Calling expect(result.ok).toBe(true) does not narrow the type,
|
|
15
|
+
* so we use this to unwrap the value after asserting ok.
|
|
16
|
+
*/
|
|
17
|
+
function expectOk<T>(result: Result<T>): T {
|
|
18
|
+
expect(result.ok).toBe(true);
|
|
19
|
+
return (result as { ok: true; value: T }).value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Assert a Result<T> is an error and return the error message.
|
|
24
|
+
*/
|
|
25
|
+
function expectErr<T>(result: Result<T>): string {
|
|
26
|
+
expect(result.ok).toBe(false);
|
|
27
|
+
return (result as { ok: false; error: string }).error;
|
|
28
|
+
}
|
|
29
|
+
|
|
11
30
|
const WORKERS_JSONC = JSON.stringify({
|
|
12
31
|
global: {},
|
|
13
32
|
workers: {
|
|
@@ -256,8 +275,7 @@ describe("SecretsService", () => {
|
|
|
256
275
|
(svc as any).config.workers["telegram-worker"].path = dir;
|
|
257
276
|
|
|
258
277
|
const result = await svc.generateDevVars("telegram-worker");
|
|
259
|
-
expect(result
|
|
260
|
-
expect(result.data).toBe(join(dir, ".dev.vars"));
|
|
278
|
+
expect(expectOk(result)).toBe(join(dir, ".dev.vars"));
|
|
261
279
|
|
|
262
280
|
// Verify file content
|
|
263
281
|
const content = await Bun.file(join(dir, ".dev.vars")).text();
|
|
@@ -277,8 +295,7 @@ describe("SecretsService", () => {
|
|
|
277
295
|
(svc as any).config.workers["no-secrets-worker"].path = dir;
|
|
278
296
|
|
|
279
297
|
const result = await svc.generateDevVars("no-secrets-worker");
|
|
280
|
-
expect(result
|
|
281
|
-
expect(result.data).toBe(join(dir, ".dev.vars"));
|
|
298
|
+
expect(expectOk(result)).toBe(join(dir, ".dev.vars"));
|
|
282
299
|
|
|
283
300
|
const content = await Bun.file(join(dir, ".dev.vars")).text();
|
|
284
301
|
expect(content).toBe("");
|
|
@@ -290,8 +307,7 @@ describe("SecretsService", () => {
|
|
|
290
307
|
it("returns error for unknown worker", async () => {
|
|
291
308
|
const svc = await createService();
|
|
292
309
|
const result = await svc.generateDevVars("unknown-worker");
|
|
293
|
-
expect(result
|
|
294
|
-
expect(result.error).toContain("not found in config");
|
|
310
|
+
expect(expectErr(result)).toContain("not found in config");
|
|
295
311
|
});
|
|
296
312
|
|
|
297
313
|
it("overwrites existing .dev.vars", async () => {
|
|
@@ -341,8 +357,7 @@ describe("SecretsService", () => {
|
|
|
341
357
|
);
|
|
342
358
|
|
|
343
359
|
const result = await svc.syncToCloudflare("telegram-worker");
|
|
344
|
-
expect(result
|
|
345
|
-
expect(result.data).toEqual(["TELEGRAM_BOT_TOKEN"]);
|
|
360
|
+
expect(expectOk(result)).toEqual(["TELEGRAM_BOT_TOKEN"]);
|
|
346
361
|
expect(called).toEqual([["TELEGRAM_BOT_TOKEN", "my-real-token"]]);
|
|
347
362
|
} finally {
|
|
348
363
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -370,11 +385,10 @@ describe("SecretsService", () => {
|
|
|
370
385
|
);
|
|
371
386
|
|
|
372
387
|
const result = await svc.syncToCloudflare("trade-worker");
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
expect(
|
|
376
|
-
expect(
|
|
377
|
-
expect(result.error).toContain("BINANCE_API_SECRET");
|
|
388
|
+
const errMsg = expectErr(result);
|
|
389
|
+
// Only BINANCE_API_KEY was synced — but errors exist, so overall result is error
|
|
390
|
+
expect(errMsg).toContain("API_SERVICE_KEY");
|
|
391
|
+
expect(errMsg).toContain("BINANCE_API_SECRET");
|
|
378
392
|
expect(called).toEqual([["BINANCE_API_KEY", "real-binance-key"]]);
|
|
379
393
|
} finally {
|
|
380
394
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -397,11 +411,10 @@ describe("SecretsService", () => {
|
|
|
397
411
|
);
|
|
398
412
|
|
|
399
413
|
const result = await svc.syncToCloudflare("trade-worker");
|
|
400
|
-
|
|
401
|
-
expect(
|
|
402
|
-
expect(
|
|
403
|
-
expect(
|
|
404
|
-
expect(result.error).toContain("BINANCE_API_SECRET");
|
|
414
|
+
const errMsg = expectErr(result);
|
|
415
|
+
expect(errMsg).toContain("API_SERVICE_KEY");
|
|
416
|
+
expect(errMsg).toContain("BINANCE_API_KEY");
|
|
417
|
+
expect(errMsg).toContain("BINANCE_API_SECRET");
|
|
405
418
|
expect(called).toHaveLength(0);
|
|
406
419
|
} finally {
|
|
407
420
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -411,8 +424,7 @@ describe("SecretsService", () => {
|
|
|
411
424
|
it("returns error for unknown worker", async () => {
|
|
412
425
|
const svc = await createService();
|
|
413
426
|
const result = await svc.syncToCloudflare("unknown-worker");
|
|
414
|
-
expect(result
|
|
415
|
-
expect(result.error).toContain("not found in config");
|
|
427
|
+
expect(expectErr(result)).toContain("not found in config");
|
|
416
428
|
});
|
|
417
429
|
|
|
418
430
|
it("handles wrangler failures gracefully", async () => {
|
|
@@ -431,9 +443,7 @@ describe("SecretsService", () => {
|
|
|
431
443
|
});
|
|
432
444
|
|
|
433
445
|
const result = await svc.syncToCloudflare("telegram-worker");
|
|
434
|
-
expect(result
|
|
435
|
-
expect(result.data).toBeUndefined();
|
|
436
|
-
expect(result.error).toContain("wrangler command failed");
|
|
446
|
+
expect(expectErr(result)).toContain("wrangler command failed");
|
|
437
447
|
} finally {
|
|
438
448
|
rmSync(dir, { recursive: true, force: true });
|
|
439
449
|
}
|
|
@@ -447,8 +457,7 @@ describe("SecretsService", () => {
|
|
|
447
457
|
(svc as any).config.workers["no-secrets-worker"].path = dir;
|
|
448
458
|
|
|
449
459
|
const result = await svc.syncToCloudflare("no-secrets-worker");
|
|
450
|
-
expect(result
|
|
451
|
-
expect(result.data).toEqual([]);
|
|
460
|
+
expect(expectOk(result)).toEqual([]);
|
|
452
461
|
} finally {
|
|
453
462
|
rmSync(dir, { recursive: true, force: true });
|
|
454
463
|
}
|