@jango-blockchained/hoox-cli 0.5.1 → 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 -1144
- 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 -96
- package/src/commands/check/types.ts +0 -103
- package/src/commands/clone/clone-command.test.ts +0 -442
- 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 -897
- package/src/commands/config/env-command.test.ts +0 -43
- package/src/commands/config/env-command.ts +0 -295
- 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 -128
- 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 -312
- package/src/commands/db/index.ts +0 -1
- package/src/commands/deploy/deploy-command.test.ts +0 -304
- package/src/commands/deploy/deploy-command.ts +0 -1135
- 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 -383
- package/src/commands/dev/dev-command.ts +0 -398
- package/src/commands/dev/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 -876
- package/src/commands/init/init-command.ts +0 -593
- 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 -294
- 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 -263
- 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 -224
- package/src/commands/repair/repair-command.ts +0 -220
- package/src/commands/repair/repair-service.ts +0 -138
- package/src/commands/repair/types.ts +0 -13
- package/src/commands/test/index.ts +0 -2
- package/src/commands/test/test-command.test.ts +0 -319
- 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 -168
- 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 -548
- package/src/index.ts +0 -269
- 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 -207
- 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 -51
- 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 -236
- package/src/services/env/env-service.ts +0 -386
- 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 -234
- 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/secrets/index.ts +0 -11
- package/src/services/secrets/secrets-service.test.ts +0 -495
- 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/errors.test.ts +0 -69
- package/src/utils/errors.ts +0 -23
- package/src/utils/formatters.test.ts +0 -180
- package/src/utils/formatters.ts +0 -269
- package/src/utils/theme.ts +0 -94
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
import { parse as parseJsonc } from "jsonc-parser";
|
|
2
|
-
import type {
|
|
3
|
-
Result,
|
|
4
|
-
SecretCheckResult,
|
|
5
|
-
SecretStatus,
|
|
6
|
-
WorkersJsonc,
|
|
7
|
-
} from "./types.js";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Manages Cloudflare Worker secrets defined in `wrangler.jsonc`.
|
|
11
|
-
*
|
|
12
|
-
* Reads worker-level `secrets` arrays, checks local `.dev.vars` files,
|
|
13
|
-
* syncs secrets to Cloudflare via `wrangler secret put`, and generates
|
|
14
|
-
* `.dev.vars` templates.
|
|
15
|
-
*
|
|
16
|
-
* Use the static `create()` factory to instantiate — the constructor
|
|
17
|
-
* is private so the config is parsed once and the sync methods remain
|
|
18
|
-
* synchronous.
|
|
19
|
-
*
|
|
20
|
-
* @example
|
|
21
|
-
* ```ts
|
|
22
|
-
* const svc = await SecretsService.create("wrangler.jsonc");
|
|
23
|
-
* const names = svc.listSecrets("trade-worker");
|
|
24
|
-
* const check = await svc.checkLocalSecrets("trade-worker");
|
|
25
|
-
* ```
|
|
26
|
-
*/
|
|
27
|
-
export class SecretsService {
|
|
28
|
-
private config: WorkersJsonc;
|
|
29
|
-
private configPath: string;
|
|
30
|
-
|
|
31
|
-
// -----------------------------------------------------------------------
|
|
32
|
-
// Construction
|
|
33
|
-
// -----------------------------------------------------------------------
|
|
34
|
-
|
|
35
|
-
/** Use {@link create} instead — private so config is always loaded. */
|
|
36
|
-
private constructor(config: WorkersJsonc, configPath: string) {
|
|
37
|
-
this.config = config;
|
|
38
|
-
this.configPath = configPath;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Factory that reads and parses the wrangler.jsonc config file.
|
|
43
|
-
* Throws if the file doesn't exist or can't be parsed.
|
|
44
|
-
*/
|
|
45
|
-
static async create(configPath?: string): Promise<SecretsService> {
|
|
46
|
-
const path = configPath ?? "wrangler.jsonc";
|
|
47
|
-
const file = Bun.file(path);
|
|
48
|
-
if (!(await file.exists())) {
|
|
49
|
-
throw new Error(`Config file not found: ${path}`);
|
|
50
|
-
}
|
|
51
|
-
const text = await file.text();
|
|
52
|
-
const config = parseJsonc(text) as WorkersJsonc;
|
|
53
|
-
return new SecretsService(config, path);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// -----------------------------------------------------------------------
|
|
57
|
-
// Public API
|
|
58
|
-
// -----------------------------------------------------------------------
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Returns secret names declared for a worker in `wrangler.jsonc`.
|
|
62
|
-
* Synchronous because the config was loaded during construction.
|
|
63
|
-
*/
|
|
64
|
-
listSecrets(workerName: string): string[] {
|
|
65
|
-
const worker = this.config.workers[workerName];
|
|
66
|
-
return worker?.secrets ?? [];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Returns a map of every worker → its declared secret names.
|
|
71
|
-
* Workers that declare no secrets are omitted from the result.
|
|
72
|
-
*/
|
|
73
|
-
listAllSecrets(): Record<string, string[]> {
|
|
74
|
-
const result: Record<string, string[]> = {};
|
|
75
|
-
for (const [name, worker] of Object.entries(this.config.workers)) {
|
|
76
|
-
if (worker.secrets && worker.secrets.length > 0) {
|
|
77
|
-
result[name] = worker.secrets;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return result;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Checks a worker's local `.dev.vars` file and reports which secrets
|
|
85
|
-
* are set (with real, non-placeholder values) and which are missing.
|
|
86
|
-
*/
|
|
87
|
-
async checkLocalSecrets(workerName: string): Promise<SecretCheckResult> {
|
|
88
|
-
const worker = this.config.workers[workerName];
|
|
89
|
-
if (!worker) {
|
|
90
|
-
return {
|
|
91
|
-
worker: workerName,
|
|
92
|
-
secrets: [],
|
|
93
|
-
allSet: false,
|
|
94
|
-
missing: [],
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const requiredSecrets = worker.secrets ?? [];
|
|
99
|
-
const devVarsPath = `${worker.path}/.dev.vars`;
|
|
100
|
-
const file = Bun.file(devVarsPath);
|
|
101
|
-
|
|
102
|
-
if (!(await file.exists())) {
|
|
103
|
-
return {
|
|
104
|
-
worker: workerName,
|
|
105
|
-
secrets: requiredSecrets.map((s) => ({ name: s, set: false })),
|
|
106
|
-
allSet: requiredSecrets.length === 0,
|
|
107
|
-
missing: [...requiredSecrets],
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const content = await file.text();
|
|
112
|
-
const secretMap = this.parseDotEnv(content);
|
|
113
|
-
|
|
114
|
-
const secrets: SecretStatus[] = [];
|
|
115
|
-
const missing: string[] = [];
|
|
116
|
-
|
|
117
|
-
for (const name of requiredSecrets) {
|
|
118
|
-
const value = secretMap.get(name);
|
|
119
|
-
if (value !== undefined && !this.isPlaceholder(value)) {
|
|
120
|
-
secrets.push({ name, set: true, source: devVarsPath });
|
|
121
|
-
} else {
|
|
122
|
-
secrets.push({
|
|
123
|
-
name,
|
|
124
|
-
set: false,
|
|
125
|
-
source: value !== undefined ? devVarsPath : undefined,
|
|
126
|
-
});
|
|
127
|
-
missing.push(name);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return {
|
|
132
|
-
worker: workerName,
|
|
133
|
-
secrets,
|
|
134
|
-
allSet: missing.length === 0,
|
|
135
|
-
missing,
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Creates (or overwrites) a `.dev.vars` template file for a worker
|
|
141
|
-
* with placeholder values for every secret declared in `wrangler.jsonc`.
|
|
142
|
-
*/
|
|
143
|
-
async generateDevVars(workerName: string): Promise<Result<string>> {
|
|
144
|
-
const worker = this.config.workers[workerName];
|
|
145
|
-
if (!worker) {
|
|
146
|
-
return {
|
|
147
|
-
ok: false,
|
|
148
|
-
error: `Worker "${workerName}" not found in config`,
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const secrets = worker.secrets ?? [];
|
|
153
|
-
const content =
|
|
154
|
-
secrets.length > 0
|
|
155
|
-
? secrets.map((s) => `${s}=placeholder_${s.toLowerCase()}`).join("\n") +
|
|
156
|
-
"\n"
|
|
157
|
-
: "";
|
|
158
|
-
const devVarsPath = `${worker.path}/.dev.vars`;
|
|
159
|
-
|
|
160
|
-
try {
|
|
161
|
-
await Bun.write(devVarsPath, content);
|
|
162
|
-
return { ok: true, value: devVarsPath };
|
|
163
|
-
} catch (err: unknown) {
|
|
164
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
165
|
-
return { ok: false, error: `Failed to write .dev.vars: ${message}` };
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Syncs a worker's secrets to Cloudflare via `wrangler secret put`.
|
|
171
|
-
*
|
|
172
|
-
* For each required secret the service first looks for a real value in
|
|
173
|
-
* the worker's `.dev.vars` file. Secrets with placeholder values are
|
|
174
|
-
* skipped and reported as errors.
|
|
175
|
-
*/
|
|
176
|
-
async syncToCloudflare(workerName: string): Promise<Result<string[]>> {
|
|
177
|
-
const worker = this.config.workers[workerName];
|
|
178
|
-
if (!worker) {
|
|
179
|
-
return {
|
|
180
|
-
ok: false,
|
|
181
|
-
error: `Worker "${workerName}" not found in config`,
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const secrets = worker.secrets ?? [];
|
|
186
|
-
const synced: string[] = [];
|
|
187
|
-
const errors: string[] = [];
|
|
188
|
-
|
|
189
|
-
// Pre-load existing .dev.vars to avoid prompting in a service.
|
|
190
|
-
const devVarsPath = `${worker.path}/.dev.vars`;
|
|
191
|
-
const devVarsFile = Bun.file(devVarsPath);
|
|
192
|
-
let existingValues: Map<string, string> = new Map();
|
|
193
|
-
|
|
194
|
-
if (await devVarsFile.exists()) {
|
|
195
|
-
const content = await devVarsFile.text();
|
|
196
|
-
existingValues = this.parseDotEnv(content);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
for (const secret of secrets) {
|
|
200
|
-
try {
|
|
201
|
-
const value = existingValues.get(secret);
|
|
202
|
-
if (value !== undefined && !this.isPlaceholder(value)) {
|
|
203
|
-
await this.execWranglerSecretPut(worker.path, secret, value);
|
|
204
|
-
synced.push(secret);
|
|
205
|
-
} else {
|
|
206
|
-
errors.push(
|
|
207
|
-
`Secret "${secret}": no valid value in .dev.vars (run generateDevVars then edit)`
|
|
208
|
-
);
|
|
209
|
-
}
|
|
210
|
-
} catch (err: unknown) {
|
|
211
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
212
|
-
errors.push(`Secret "${secret}": ${message}`);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
if (errors.length > 0) {
|
|
217
|
-
return { ok: false, error: errors.join("; ") };
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
return { ok: true, value: synced };
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// -----------------------------------------------------------------------
|
|
224
|
-
// Private helpers
|
|
225
|
-
// -----------------------------------------------------------------------
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Parses a `.env` / `.dev.vars` style file into a `Map<key, value>`.
|
|
229
|
-
* Skips empty lines and comments (lines starting with `#`).
|
|
230
|
-
*/
|
|
231
|
-
private parseDotEnv(content: string): Map<string, string> {
|
|
232
|
-
const map = new Map<string, string>();
|
|
233
|
-
for (const line of content.split("\n")) {
|
|
234
|
-
const trimmed = line.trim();
|
|
235
|
-
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
236
|
-
|
|
237
|
-
const eqIdx = trimmed.indexOf("=");
|
|
238
|
-
if (eqIdx < 1) continue;
|
|
239
|
-
|
|
240
|
-
const key = trimmed.substring(0, eqIdx).trim();
|
|
241
|
-
const value = trimmed.substring(eqIdx + 1).trim();
|
|
242
|
-
map.set(key, value);
|
|
243
|
-
}
|
|
244
|
-
return map;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/** Returns `true` when a value looks like an unfilled template. */
|
|
248
|
-
private isPlaceholder(value: string): boolean {
|
|
249
|
-
return (
|
|
250
|
-
value.startsWith("placeholder_") ||
|
|
251
|
-
value.startsWith("your_") ||
|
|
252
|
-
value.startsWith("generate_") ||
|
|
253
|
-
value === ""
|
|
254
|
-
);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Runs `wrangler secret put <name>` inside the worker's directory.
|
|
259
|
-
* Marked `protected` so unit tests can stub it without touching the
|
|
260
|
-
* real `Bun.spawn`.
|
|
261
|
-
*/
|
|
262
|
-
protected async execWranglerSecretPut(
|
|
263
|
-
workerPath: string,
|
|
264
|
-
name: string,
|
|
265
|
-
value: string
|
|
266
|
-
): Promise<void> {
|
|
267
|
-
const proc = Bun.spawn(["wrangler", "secret", "put", name], {
|
|
268
|
-
cwd: workerPath,
|
|
269
|
-
stdin: "pipe",
|
|
270
|
-
stdout: "pipe",
|
|
271
|
-
stderr: "pipe",
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
proc.stdin.write(value + "\n");
|
|
275
|
-
proc.stdin.end();
|
|
276
|
-
|
|
277
|
-
const exitCode = await proc.exited;
|
|
278
|
-
if (exitCode !== 0) {
|
|
279
|
-
const stderrText = await new Response(proc.stderr).text();
|
|
280
|
-
throw new Error(
|
|
281
|
-
`wrangler exited with code ${exitCode}: ${stderrText.trim()}`
|
|
282
|
-
);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
export default SecretsService;
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SecretsService types — secret management for Cloudflare Workers.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
// Result
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
|
|
9
|
-
import type { Result } from "@jango-blockchained/hoox-shared";
|
|
10
|
-
|
|
11
|
-
// Re-export the shared Result<T> for convenience
|
|
12
|
-
export type { Result };
|
|
13
|
-
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
// Domain types
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
|
|
18
|
-
export interface SecretStatus {
|
|
19
|
-
/** Name of the secret (e.g. "TELEGRAM_BOT_TOKEN"). */
|
|
20
|
-
name: string;
|
|
21
|
-
/** Whether the secret has a real (non-placeholder) value set. */
|
|
22
|
-
set: boolean;
|
|
23
|
-
/** Where the value was found ("workers/<name>/.dev.vars" or undefined). */
|
|
24
|
-
source?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface SecretCheckResult {
|
|
28
|
-
/** Worker name as it appears in wrangler.jsonc. */
|
|
29
|
-
worker: string;
|
|
30
|
-
/** Per-secret status entries. */
|
|
31
|
-
secrets: SecretStatus[];
|
|
32
|
-
/** True when *every* required secret is present with a real value. */
|
|
33
|
-
allSet: boolean;
|
|
34
|
-
/** Secret names that are either missing or still placeholder. */
|
|
35
|
-
missing: string[];
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// ---------------------------------------------------------------------------
|
|
39
|
-
// Internal config shape (subset of wrangler.jsonc used by SecretsService)
|
|
40
|
-
// ---------------------------------------------------------------------------
|
|
41
|
-
|
|
42
|
-
export interface WorkerSecretConfig {
|
|
43
|
-
enabled: boolean;
|
|
44
|
-
path: string;
|
|
45
|
-
secrets?: string[];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export interface WorkersJsonc {
|
|
49
|
-
workers: Record<string, WorkerSecretConfig>;
|
|
50
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, mock } from "bun:test";
|
|
2
|
-
import { UpdateService } from "./update-service.js";
|
|
3
|
-
|
|
4
|
-
function createMockPrereqs(
|
|
5
|
-
overrides?: Partial<{
|
|
6
|
-
outdated: boolean;
|
|
7
|
-
current: string;
|
|
8
|
-
minimum: string;
|
|
9
|
-
}>
|
|
10
|
-
) {
|
|
11
|
-
const defaults = { outdated: false, current: "4.0.0", minimum: "3.88.0" };
|
|
12
|
-
const config = { ...defaults, ...overrides };
|
|
13
|
-
|
|
14
|
-
return {
|
|
15
|
-
checkWranglerVersion: mock(() => Promise.resolve(config)),
|
|
16
|
-
checkBun: mock(() => Promise.resolve({})),
|
|
17
|
-
checkGit: mock(() => Promise.resolve({})),
|
|
18
|
-
checkNode: mock(() => Promise.resolve({})),
|
|
19
|
-
checkWrangler: mock(() => Promise.resolve({})),
|
|
20
|
-
checkCloudflareAuth: mock(() => Promise.resolve({})),
|
|
21
|
-
checkDocker: mock(() => Promise.resolve({})),
|
|
22
|
-
checkRepository: mock(() => Promise.resolve({})),
|
|
23
|
-
runAll: mock(() => Promise.resolve({ checks: [], allPassed: true })),
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
describe("UpdateService", () => {
|
|
28
|
-
describe("checkAndPromptUpdate", () => {
|
|
29
|
-
it("returns updated=false when wrangler is up to date", async () => {
|
|
30
|
-
const mockPrereqs = createMockPrereqs({ outdated: false });
|
|
31
|
-
const svc = new UpdateService(undefined, mockPrereqs as any);
|
|
32
|
-
|
|
33
|
-
const result = await svc.checkAndPromptUpdate({ yes: true });
|
|
34
|
-
|
|
35
|
-
expect(result.updated).toBe(false);
|
|
36
|
-
expect(result.error).toBeUndefined();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("auto-updates when wrangler is outdated and --yes is set", async () => {
|
|
40
|
-
const mockPrereqs = createMockPrereqs({
|
|
41
|
-
outdated: true,
|
|
42
|
-
current: "3.87.0",
|
|
43
|
-
});
|
|
44
|
-
const svc = new UpdateService(undefined, mockPrereqs as any);
|
|
45
|
-
|
|
46
|
-
const result = await svc.checkAndPromptUpdate({ yes: true });
|
|
47
|
-
|
|
48
|
-
expect(typeof result.updated).toBe("boolean");
|
|
49
|
-
}, 30000);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
describe("updateWrangler", () => {
|
|
53
|
-
it("returns result when called (does not throw)", async () => {
|
|
54
|
-
const mockPrereqs = createMockPrereqs({
|
|
55
|
-
outdated: false,
|
|
56
|
-
current: "4.0.0",
|
|
57
|
-
});
|
|
58
|
-
const svc = new UpdateService(undefined, mockPrereqs as any);
|
|
59
|
-
|
|
60
|
-
const result = await svc.updateWrangler();
|
|
61
|
-
|
|
62
|
-
expect(typeof result.updated).toBe("boolean");
|
|
63
|
-
}, 30000);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
describe("checkLatestVersion", () => {
|
|
67
|
-
it("returns a version string or null", async () => {
|
|
68
|
-
const svc = new UpdateService();
|
|
69
|
-
const version = await svc.checkLatestVersion();
|
|
70
|
-
expect(version === null || typeof version === "string").toBe(true);
|
|
71
|
-
if (typeof version === "string") {
|
|
72
|
-
expect(version).toMatch(/^\d+\.\d+\.\d+$/);
|
|
73
|
-
}
|
|
74
|
-
}, 30000);
|
|
75
|
-
});
|
|
76
|
-
});
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* UpdateService — manages wrangler version updates.
|
|
3
|
-
*
|
|
4
|
-
* Checks current wrangler version, compares against minimum, and updates
|
|
5
|
-
* via `bun update wrangler` when needed. Integrates with PrerequisitesService
|
|
6
|
-
* for version checking.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { PrerequisitesService } from "../prerequisites/index.js";
|
|
10
|
-
import { theme } from "../../utils/theme.js";
|
|
11
|
-
import { confirm } from "@clack/prompts";
|
|
12
|
-
|
|
13
|
-
export interface UpdateResult {
|
|
14
|
-
updated: boolean;
|
|
15
|
-
previousVersion?: string;
|
|
16
|
-
newVersion?: string;
|
|
17
|
-
error?: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export class UpdateService {
|
|
21
|
-
private readonly prereqs: PrerequisitesService;
|
|
22
|
-
private readonly cwd: string;
|
|
23
|
-
|
|
24
|
-
constructor(cwd?: string, prereqs?: PrerequisitesService) {
|
|
25
|
-
this.prereqs = prereqs ?? new PrerequisitesService();
|
|
26
|
-
this.cwd = cwd ?? process.cwd();
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Check if wrangler is outdated. If yes, prompt user (TTY) or
|
|
31
|
-
* auto-update (non-TTY / --yes flag). Never throws — errors are
|
|
32
|
-
* returned in UpdateResult.
|
|
33
|
-
*/
|
|
34
|
-
async checkAndPromptUpdate(options?: {
|
|
35
|
-
yes?: boolean;
|
|
36
|
-
}): Promise<UpdateResult> {
|
|
37
|
-
try {
|
|
38
|
-
const versionCheck = await this.prereqs.checkWranglerVersion();
|
|
39
|
-
|
|
40
|
-
if (!versionCheck.outdated) {
|
|
41
|
-
process.stdout.write(
|
|
42
|
-
` ${theme.success("✓")} Wrangler ${versionCheck.current} is up to date\n`
|
|
43
|
-
);
|
|
44
|
-
return { updated: false };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const current = versionCheck.current ?? "unknown";
|
|
48
|
-
const minimum = versionCheck.minimum ?? "unknown";
|
|
49
|
-
|
|
50
|
-
// Fetch latest available version for the prompt
|
|
51
|
-
const latest = await this.checkLatestVersion();
|
|
52
|
-
|
|
53
|
-
// Determine if we should prompt or auto-update
|
|
54
|
-
let shouldUpdate: boolean;
|
|
55
|
-
if (options?.yes ?? !process.stdout.isTTY) {
|
|
56
|
-
shouldUpdate = true;
|
|
57
|
-
} else {
|
|
58
|
-
shouldUpdate = await this.promptUpdate(current, minimum, latest);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (!shouldUpdate) {
|
|
62
|
-
process.stdout.write(
|
|
63
|
-
` ${theme.warning("!")} Skipping wrangler update (current: ${current})\n`
|
|
64
|
-
);
|
|
65
|
-
return { updated: false };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return await this.runUpdate();
|
|
69
|
-
} catch (err) {
|
|
70
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
71
|
-
process.stdout.write(
|
|
72
|
-
` ${theme.error("!")} Wrangler update check failed: ${message}\n`
|
|
73
|
-
);
|
|
74
|
-
return { updated: false, error: message };
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Force-update wrangler regardless of current version.
|
|
80
|
-
* Used by the standalone `hoox update` command.
|
|
81
|
-
*/
|
|
82
|
-
async updateWrangler(): Promise<UpdateResult> {
|
|
83
|
-
process.stdout.write(` ${theme.info("i")} Checking wrangler version...\n`);
|
|
84
|
-
|
|
85
|
-
const before = await this.prereqs.checkWranglerVersion();
|
|
86
|
-
const previousVersion = before.current;
|
|
87
|
-
|
|
88
|
-
if (!previousVersion) {
|
|
89
|
-
process.stdout.write(
|
|
90
|
-
` ${theme.warning("!")} Wrangler is not installed. Install with: bun add -g wrangler\n`
|
|
91
|
-
);
|
|
92
|
-
return { updated: false, error: "Wrangler not installed" };
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const result = await this.runUpdate(previousVersion);
|
|
96
|
-
return { ...result, previousVersion };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Check the latest available wrangler version from the npm registry.
|
|
101
|
-
* Uses the npm registry JSON API directly — no dependency on npm CLI.
|
|
102
|
-
* Returns null if the check fails (network error, etc.).
|
|
103
|
-
*/
|
|
104
|
-
async checkLatestVersion(): Promise<string | null> {
|
|
105
|
-
try {
|
|
106
|
-
const res = await fetch("https://registry.npmjs.org/wrangler/latest");
|
|
107
|
-
if (!res.ok) return null;
|
|
108
|
-
const data = (await res.json()) as { version?: string };
|
|
109
|
-
return data.version ?? null;
|
|
110
|
-
} catch {
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// ── Private helpers ──────────────────────────────────────────────
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Prompt the user whether to update wrangler.
|
|
119
|
-
* Returns true if the user wants to update.
|
|
120
|
-
*/
|
|
121
|
-
private async promptUpdate(
|
|
122
|
-
current: string,
|
|
123
|
-
minimum: string,
|
|
124
|
-
latest: string | null
|
|
125
|
-
): Promise<boolean> {
|
|
126
|
-
const latestStr = latest ? `, latest: ${latest}` : "";
|
|
127
|
-
process.stdout.write(
|
|
128
|
-
`\n ${theme.warning("!")} Wrangler ${current} is outdated (minimum: ${minimum}${latestStr})\n`
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
const result = await confirm({
|
|
132
|
-
message: "Update wrangler?",
|
|
133
|
-
initialValue: true,
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
// Confirm returns boolean | symbol (CLACK_CANCEL)
|
|
137
|
-
return result === true;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Run `bun update wrangler` in the project root and verify the result.
|
|
142
|
-
*/
|
|
143
|
-
private async runUpdate(previousVersion?: string): Promise<UpdateResult> {
|
|
144
|
-
process.stdout.write(` ${theme.info("i")} Updating wrangler...\n`);
|
|
145
|
-
|
|
146
|
-
try {
|
|
147
|
-
const proc = Bun.spawn(["bun", "update", "wrangler"], {
|
|
148
|
-
cwd: this.cwd,
|
|
149
|
-
stdout: "ignore",
|
|
150
|
-
stderr: "pipe",
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
const stderr = await new Response(proc.stderr).text();
|
|
154
|
-
const exitCode = await proc.exited;
|
|
155
|
-
|
|
156
|
-
if (exitCode !== 0) {
|
|
157
|
-
const errorMsg = stderr.split("\n")[0] || "bun update wrangler failed";
|
|
158
|
-
process.stdout.write(
|
|
159
|
-
` ${theme.error("!")} Update failed: ${errorMsg}\n`
|
|
160
|
-
);
|
|
161
|
-
return { updated: false, error: errorMsg };
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Verify the new version
|
|
165
|
-
const verify = await this.prereqs.checkWranglerVersion();
|
|
166
|
-
const newVersion = verify.current;
|
|
167
|
-
|
|
168
|
-
const versionChanged = previousVersion
|
|
169
|
-
? previousVersion !== newVersion
|
|
170
|
-
: true;
|
|
171
|
-
|
|
172
|
-
if (versionChanged) {
|
|
173
|
-
process.stdout.write(
|
|
174
|
-
` ${theme.success("✓")} Wrangler updated from ${previousVersion ?? "?"} to ${newVersion}\n`
|
|
175
|
-
);
|
|
176
|
-
} else {
|
|
177
|
-
process.stdout.write(
|
|
178
|
-
` ${theme.warning("!")} Wrangler version unchanged (${newVersion})\n`
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return {
|
|
183
|
-
updated: versionChanged,
|
|
184
|
-
previousVersion,
|
|
185
|
-
newVersion,
|
|
186
|
-
};
|
|
187
|
-
} catch (err) {
|
|
188
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
189
|
-
process.stdout.write(` ${theme.error("!")} Update failed: ${message}\n`);
|
|
190
|
-
return { updated: false, error: message };
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|