sproutboat 0.5.0 → 0.6.0
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 +67 -78
- package/SURFACE.md +1 -1
- package/package.json +25 -20
- package/src/assets.ts +29 -9
- package/src/broker.ts +143 -65
- package/src/build.ts +23 -5
- package/src/bundle.ts +2 -1
- package/src/compile.ts +9 -4
- package/src/config.ts +71 -29
- package/src/dev.ts +33 -16
- package/src/json.ts +9 -1
- package/src/main.ts +274 -77
- package/src/manifest.ts +63 -14
- package/src/native-fetch-prelude.js +256 -127
- package/src/patch-porffor.ts +5 -2
- package/src/report.ts +37 -17
- package/src/source.ts +4 -1
- package/src/style.ts +9 -6
- package/src/surface.ts +150 -42
- package/src/toolchain.ts +15 -4
- package/src/update-check.ts +20 -5
- package/src/wrap.ts +32 -8
package/src/main.ts
CHANGED
|
@@ -23,7 +23,13 @@ async function responseText(response: Response, failure: string): Promise<string
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
type VersionSummary = { id: string; artifact: string; deployedAt: string; active: boolean };
|
|
26
|
-
type CliAuthorization = {
|
|
26
|
+
type CliAuthorization = {
|
|
27
|
+
deviceCode: string;
|
|
28
|
+
userCode: string;
|
|
29
|
+
verificationUri: string;
|
|
30
|
+
interval: number;
|
|
31
|
+
expiresAt: string;
|
|
32
|
+
};
|
|
27
33
|
|
|
28
34
|
function parseVersionList(source: string): VersionSummary[] | undefined {
|
|
29
35
|
const value = parseJsonValue(source);
|
|
@@ -31,13 +37,27 @@ function parseVersionList(source: string): VersionSummary[] | undefined {
|
|
|
31
37
|
const deployments: VersionSummary[] = [];
|
|
32
38
|
for (const item of value) {
|
|
33
39
|
const record = jsonObject(item);
|
|
34
|
-
if (
|
|
35
|
-
|
|
40
|
+
if (
|
|
41
|
+
!record ||
|
|
42
|
+
!isString(record.id) ||
|
|
43
|
+
!isString(record.artifact) ||
|
|
44
|
+
!isString(record.deployedAt) ||
|
|
45
|
+
(record.active !== true && record.active !== false)
|
|
46
|
+
)
|
|
47
|
+
return undefined;
|
|
48
|
+
deployments.push({
|
|
49
|
+
id: record.id,
|
|
50
|
+
artifact: record.artifact,
|
|
51
|
+
deployedAt: record.deployedAt,
|
|
52
|
+
active: record.active,
|
|
53
|
+
});
|
|
36
54
|
}
|
|
37
55
|
return deployments;
|
|
38
56
|
}
|
|
39
57
|
|
|
40
|
-
function parseUrlResponse(
|
|
58
|
+
function parseUrlResponse(
|
|
59
|
+
source: string,
|
|
60
|
+
): { url: string; id?: string; artifact?: string; unchanged: boolean } | undefined {
|
|
41
61
|
const record = jsonObject(parseJsonValue(source));
|
|
42
62
|
if (!record || !isString(record.url)) return undefined;
|
|
43
63
|
return {
|
|
@@ -53,15 +73,35 @@ function parseUrlResponse(source: string): { url: string; id?: string; artifact?
|
|
|
53
73
|
* the pin only changes by redeploying — and the alpha compiler's output can
|
|
54
74
|
* differ between pins. */
|
|
55
75
|
function parsePorfforDrift(source: string): { from: string; to: string } | undefined {
|
|
56
|
-
const drift = (() => {
|
|
76
|
+
const drift = (() => {
|
|
77
|
+
try {
|
|
78
|
+
return jsonObject(parseJsonValue(source))?.porfforDrift;
|
|
79
|
+
} catch {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
})();
|
|
57
83
|
const record = drift && jsonObject(drift);
|
|
58
84
|
return record && isString(record.from) && isString(record.to) ? { from: record.from, to: record.to } : undefined;
|
|
59
85
|
}
|
|
60
86
|
|
|
61
87
|
function parseAuthorization(source: string): CliAuthorization | undefined {
|
|
62
88
|
const record = jsonObject(parseJsonValue(source));
|
|
63
|
-
if (
|
|
64
|
-
|
|
89
|
+
if (
|
|
90
|
+
!record ||
|
|
91
|
+
!isString(record.deviceCode) ||
|
|
92
|
+
!isString(record.userCode) ||
|
|
93
|
+
!isString(record.verificationUri) ||
|
|
94
|
+
!isSafeInteger(record.interval) ||
|
|
95
|
+
!isString(record.expiresAt)
|
|
96
|
+
)
|
|
97
|
+
return undefined;
|
|
98
|
+
return {
|
|
99
|
+
deviceCode: record.deviceCode,
|
|
100
|
+
userCode: record.userCode,
|
|
101
|
+
verificationUri: record.verificationUri,
|
|
102
|
+
interval: record.interval,
|
|
103
|
+
expiresAt: record.expiresAt,
|
|
104
|
+
};
|
|
65
105
|
}
|
|
66
106
|
|
|
67
107
|
function parseToken(source: string): string | undefined {
|
|
@@ -140,7 +180,8 @@ async function readProject(directory = process.cwd()) {
|
|
|
140
180
|
}
|
|
141
181
|
|
|
142
182
|
async function init(name = "hello") {
|
|
143
|
-
if (!/^[a-z0-9](?:[a-z0-9-]{1,30}[a-z0-9])?$/.test(name))
|
|
183
|
+
if (!/^[a-z0-9](?:[a-z0-9-]{1,30}[a-z0-9])?$/.test(name))
|
|
184
|
+
fail("project name must be a 3–32 character lowercase slug");
|
|
144
185
|
const directory = resolve(process.cwd(), name);
|
|
145
186
|
const configPath = resolve(directory, "sproutboat.jsonc");
|
|
146
187
|
const handlerPath = resolve(directory, "src/index.js");
|
|
@@ -148,7 +189,10 @@ async function init(name = "hello") {
|
|
|
148
189
|
// unrelated existing directory, and a second `wx` write failing partway
|
|
149
190
|
// through used to crash with a raw EEXIST stack trace after already having
|
|
150
191
|
// created sproutboat.jsonc, leaving a half-scaffolded project behind.
|
|
151
|
-
for (const [path, label] of [
|
|
192
|
+
for (const [path, label] of [
|
|
193
|
+
[configPath, "sproutboat.jsonc"],
|
|
194
|
+
[handlerPath, "src/index.js"],
|
|
195
|
+
] as const) {
|
|
152
196
|
if (await Bun.file(path).exists()) fail(`${basename(directory)} already contains ${label}`);
|
|
153
197
|
}
|
|
154
198
|
await mkdir(resolve(directory, "src"), { recursive: true });
|
|
@@ -173,12 +217,21 @@ async function check(directory?: string) {
|
|
|
173
217
|
|
|
174
218
|
async function build(directory?: string, target: "linux-x86_64" | "host" = "linux-x86_64") {
|
|
175
219
|
const project = await readProject(directory);
|
|
176
|
-
console.log(
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
220
|
+
console.log(
|
|
221
|
+
target === "host"
|
|
222
|
+
? dim(`Compiling the native-fetch server with Porffor for this machine (${hostTarget()}, local only)…`)
|
|
223
|
+
: dim("Compiling the native-fetch server with Porffor + Zig (linux-x86_64, static)…"),
|
|
224
|
+
);
|
|
225
|
+
const artifact = await buildArtifact({
|
|
226
|
+
projectDir: project.directory,
|
|
227
|
+
config: project.config,
|
|
228
|
+
sourcePath: project.sourcePath,
|
|
229
|
+
source: project.bundle.code,
|
|
230
|
+
target,
|
|
231
|
+
});
|
|
180
232
|
console.log(ok(`built ${project.config.name}`));
|
|
181
|
-
if (target === "host")
|
|
233
|
+
if (target === "host")
|
|
234
|
+
console.log(dim(" host build — runs here, not deployable; drop --target host to build for a box"));
|
|
182
235
|
console.log(artifact.artifactDir);
|
|
183
236
|
return { project, artifact };
|
|
184
237
|
}
|
|
@@ -189,7 +242,8 @@ async function dev(args: string[]) {
|
|
|
189
242
|
const portIndex = args.indexOf("--port");
|
|
190
243
|
const portArg = portIndex >= 0 ? args[portIndex + 1] : undefined;
|
|
191
244
|
const port = Number(portArg ?? 8787);
|
|
192
|
-
if (!Number.isSafeInteger(port) || port < 1 || port > 65_535)
|
|
245
|
+
if (!Number.isSafeInteger(port) || port < 1 || port > 65_535)
|
|
246
|
+
usageError(`invalid --port: ${portArg}`, "dev [project-dir] [--port <n>] [--no-watch]");
|
|
193
247
|
const project = await readProject(directory);
|
|
194
248
|
console.log(dim(`Building ${project.config.name} for this machine (${hostTarget()})…`));
|
|
195
249
|
await runDev({
|
|
@@ -254,9 +308,9 @@ async function provisionBindings(directory = process.cwd()): Promise<void> {
|
|
|
254
308
|
/** #79 — wrangler parity: drop the stored credential for an endpoint. */
|
|
255
309
|
async function logout(args: string[]) {
|
|
256
310
|
const { apiUrl } = parseLoginArgs(args);
|
|
257
|
-
console.log(
|
|
258
|
-
? ok(`forgot the credential for ${apiUrl}`)
|
|
259
|
-
|
|
311
|
+
console.log(
|
|
312
|
+
(await forgetToken(apiUrl)) ? ok(`forgot the credential for ${apiUrl}`) : `no stored credential for ${apiUrl}`,
|
|
313
|
+
);
|
|
260
314
|
}
|
|
261
315
|
|
|
262
316
|
/**
|
|
@@ -265,15 +319,23 @@ async function logout(args: string[]) {
|
|
|
265
319
|
* works rather than only reporting what is on disk.
|
|
266
320
|
*/
|
|
267
321
|
async function whoami() {
|
|
268
|
-
const apiUrl = process.env.SPROUTBOAT_API_URL || await activeApiUrl();
|
|
269
|
-
if (!apiUrl) {
|
|
270
|
-
|
|
322
|
+
const apiUrl = process.env.SPROUTBOAT_API_URL || (await activeApiUrl());
|
|
323
|
+
if (!apiUrl) {
|
|
324
|
+
console.log("not logged in — run `sproutboat login`");
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const token = process.env.SPROUTBOAT_TOKEN || (await savedToken(apiUrl));
|
|
271
328
|
console.log(`endpoint ${apiUrl}`);
|
|
272
|
-
if (!token) {
|
|
329
|
+
if (!token) {
|
|
330
|
+
console.log(`account ${dim("no stored token — run `sproutboat login`")}`);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
273
333
|
|
|
274
334
|
const response = await fetch(`${apiUrl}/api/account`, { headers: { "x-api-key": token } });
|
|
275
335
|
if (!response.ok) {
|
|
276
|
-
console.log(
|
|
336
|
+
console.log(
|
|
337
|
+
`account ${rose(response.status === 401 ? "token rejected — run `sproutboat login`" : `control plane said ${response.status}`)}`,
|
|
338
|
+
);
|
|
277
339
|
return;
|
|
278
340
|
}
|
|
279
341
|
const account = jsonObject(parseJsonValue(await response.text()));
|
|
@@ -293,7 +355,10 @@ async function deploy(args: string[]) {
|
|
|
293
355
|
if (artifactIndex >= 0) {
|
|
294
356
|
artifactDir = args[artifactIndex + 1]
|
|
295
357
|
? resolve(args[artifactIndex + 1])
|
|
296
|
-
: usageError(
|
|
358
|
+
: usageError(
|
|
359
|
+
"deploy: --artifact needs a directory",
|
|
360
|
+
"deploy [project-dir] [--dry-run] [--artifact <dir>] [--no-wait]",
|
|
361
|
+
);
|
|
297
362
|
projectName = "";
|
|
298
363
|
} else {
|
|
299
364
|
// wrangler-style: create resources for id-less bindings and pin the ids
|
|
@@ -338,7 +403,10 @@ async function deploy(args: string[]) {
|
|
|
338
403
|
if (await assetsManifestFile.exists()) {
|
|
339
404
|
// assets.json is written by `sproutboat build` from the AssetManifest contract.
|
|
340
405
|
const assetsManifest: { files?: AssetFiles } = await assetsManifestFile.json();
|
|
341
|
-
form.set(
|
|
406
|
+
form.set(
|
|
407
|
+
"assets_manifest",
|
|
408
|
+
new File([await assetsManifestFile.arrayBuffer()], "assets.json", { type: "application/json" }),
|
|
409
|
+
);
|
|
342
410
|
for (const key of Object.keys(assetsManifest.files ?? {})) {
|
|
343
411
|
const file = Bun.file(resolve(artifactDir, "assets", `.${key}`));
|
|
344
412
|
if (!(await file.exists())) fail(`assets.json lists ${key} but assets${key} is missing — rebuild`);
|
|
@@ -361,18 +429,29 @@ async function deploy(args: string[]) {
|
|
|
361
429
|
}
|
|
362
430
|
console.log(`\n${leaf("🌱")} ${bold(leaf(`Deployed ${projectName}`))}`);
|
|
363
431
|
console.log(` ${bold(deployed.url)}`);
|
|
364
|
-
if (deployed.id)
|
|
432
|
+
if (deployed.id)
|
|
433
|
+
console.log(
|
|
434
|
+
dim(` version ${deployed.id}${deployed.artifact ? ` · artifact ${deployed.artifact.slice(0, 12)}` : ""}`),
|
|
435
|
+
);
|
|
365
436
|
for (const cron of config?.triggers?.crons ?? []) console.log(dim(` schedule ${cron}`));
|
|
366
437
|
const drift = parsePorfforDrift(body);
|
|
367
438
|
if (drift) {
|
|
368
439
|
console.warn(amber(`\n! Porffor pin changed: ${drift.from} -> ${drift.to}`));
|
|
369
440
|
console.warn(dim(` The previous live version stays frozen at ${drift.from}; this one is built with ${drift.to}.`));
|
|
370
|
-
console.warn(
|
|
441
|
+
console.warn(
|
|
442
|
+
dim(
|
|
443
|
+
` The alpha compiler's output can differ between pins (see COMPAT.md) — roll back if this version misbehaves.`,
|
|
444
|
+
),
|
|
445
|
+
);
|
|
371
446
|
}
|
|
372
447
|
// Verify the edge actually answers (cert issuance + sprout boot). Say nothing
|
|
373
448
|
// on success — "Deployed" already implied that; only speak up if it doesn't.
|
|
374
449
|
if (!args.includes("--no-wait") && !(await waitForHealthy(deployed.url, 90_000))) {
|
|
375
|
-
console.warn(
|
|
450
|
+
console.warn(
|
|
451
|
+
amber(
|
|
452
|
+
" ! not serving after 90s — Caddy may still be issuing the cert, or the sprout is crashing (`sproutboat tail`)",
|
|
453
|
+
),
|
|
454
|
+
);
|
|
376
455
|
}
|
|
377
456
|
}
|
|
378
457
|
|
|
@@ -389,7 +468,9 @@ async function waitForHealthy(url: string, timeoutMs: number): Promise<boolean>
|
|
|
389
468
|
try {
|
|
390
469
|
const response = await fetch(url, { method: "HEAD", redirect: "manual" });
|
|
391
470
|
if (response.status < 500) return true;
|
|
392
|
-
} catch {
|
|
471
|
+
} catch {
|
|
472
|
+
/* DNS / TLS-not-yet-issued / connection refused — keep waiting */
|
|
473
|
+
}
|
|
393
474
|
await Bun.sleep(Math.min(wait, Math.max(0, deadline - Date.now())));
|
|
394
475
|
if (wait < 5000) wait += 1000;
|
|
395
476
|
}
|
|
@@ -422,9 +503,17 @@ async function login(args: string[]) {
|
|
|
422
503
|
const authorization = parseAuthorization(body);
|
|
423
504
|
if (!authorization) fail("login response did not include a valid authorization request");
|
|
424
505
|
const verificationUrl = new URL(authorization.verificationUri, `${apiUrl}/`).toString();
|
|
425
|
-
const openCommand =
|
|
426
|
-
|
|
427
|
-
|
|
506
|
+
const openCommand =
|
|
507
|
+
process.platform === "darwin"
|
|
508
|
+
? ["open", verificationUrl]
|
|
509
|
+
: process.platform === "win32"
|
|
510
|
+
? ["cmd", "/c", "start", "", verificationUrl]
|
|
511
|
+
: ["xdg-open", verificationUrl];
|
|
512
|
+
try {
|
|
513
|
+
Bun.spawn(openCommand, { stdout: "ignore", stderr: "ignore" });
|
|
514
|
+
} catch {
|
|
515
|
+
console.log(`Open ${verificationUrl}`);
|
|
516
|
+
}
|
|
428
517
|
console.log("Opening the browser to approve this CLI login.");
|
|
429
518
|
console.log(`Confirm code: ${authorization.userCode}`);
|
|
430
519
|
while (new Date(authorization.expiresAt).getTime() > Date.now()) {
|
|
@@ -447,8 +536,8 @@ async function login(args: string[]) {
|
|
|
447
536
|
}
|
|
448
537
|
|
|
449
538
|
async function apiCredentials() {
|
|
450
|
-
const apiUrl = (process.env.SPROUTBOAT_API_URL || await activeApiUrl() || defaultApiUrl).replace(/\/$/, "");
|
|
451
|
-
const token = process.env.SPROUTBOAT_TOKEN || await savedToken(apiUrl);
|
|
539
|
+
const apiUrl = (process.env.SPROUTBOAT_API_URL || (await activeApiUrl()) || defaultApiUrl).replace(/\/$/, "");
|
|
540
|
+
const token = process.env.SPROUTBOAT_TOKEN || (await savedToken(apiUrl));
|
|
452
541
|
if (!token) fail("not logged in; run sproutboat login or set SPROUTBOAT_TOKEN for this command");
|
|
453
542
|
return { apiUrl, token };
|
|
454
543
|
}
|
|
@@ -456,7 +545,10 @@ async function apiCredentials() {
|
|
|
456
545
|
async function versions(args: string[]) {
|
|
457
546
|
const sub = args[0];
|
|
458
547
|
if (sub !== "list" && sub !== "view") {
|
|
459
|
-
usageError(
|
|
548
|
+
usageError(
|
|
549
|
+
sub ? `versions: unknown subcommand "${sub}"` : "versions: missing subcommand",
|
|
550
|
+
"versions <list | view <version-id>> [project-dir]",
|
|
551
|
+
);
|
|
460
552
|
}
|
|
461
553
|
args.shift();
|
|
462
554
|
|
|
@@ -465,7 +557,9 @@ async function versions(args: string[]) {
|
|
|
465
557
|
if (!id) usageError("versions view: missing <version-id>", "versions view <version-id> [project-dir]");
|
|
466
558
|
const [project, { apiUrl, token }] = await Promise.all([readProject(args[0]), apiCredentials()]);
|
|
467
559
|
const body = await responseText(
|
|
468
|
-
await fetch(`${apiUrl}/api/projects/${project.config.name}/deployments/${encodeURIComponent(id)}`, {
|
|
560
|
+
await fetch(`${apiUrl}/api/projects/${project.config.name}/deployments/${encodeURIComponent(id)}`, {
|
|
561
|
+
headers: { "x-api-key": token },
|
|
562
|
+
}),
|
|
469
563
|
"could not read that version",
|
|
470
564
|
);
|
|
471
565
|
const detail = jsonObject(parseJsonValue(body));
|
|
@@ -474,31 +568,44 @@ async function versions(args: string[]) {
|
|
|
474
568
|
console.log(`${bold(String(detail.id))} ${detail.active ? ok("active") : dim("superseded")}`);
|
|
475
569
|
console.log(` route ${String(detail.hostname)}`);
|
|
476
570
|
console.log(` artifact ${String(detail.artifact)}`);
|
|
477
|
-
console.log(
|
|
571
|
+
console.log(
|
|
572
|
+
` deployed ${String(detail.deployedAt)}${isString(detail.deployedBy) ? ` by ${detail.deployedBy}` : ""}`,
|
|
573
|
+
);
|
|
478
574
|
if (manifest) {
|
|
479
|
-
console.log(
|
|
575
|
+
console.log(
|
|
576
|
+
` built ${String(manifest.builtAt)} · porffor ${String(manifest.porfforVersion)} · ${String(manifest.binarySize)} bytes`,
|
|
577
|
+
);
|
|
480
578
|
} else if (isString(detail.manifestError)) {
|
|
481
579
|
console.log(` ! manifest unavailable: ${detail.manifestError}`);
|
|
482
580
|
}
|
|
483
581
|
const resources = Array.isArray(detail.resources) ? detail.resources.map((entry) => jsonObject(entry)) : [];
|
|
484
582
|
for (const resource of resources) {
|
|
485
|
-
if (resource)
|
|
583
|
+
if (resource)
|
|
584
|
+
console.log(` bound ${String(resource.kind)} ${String(resource.name)} ${dim(String(resource.id))}`);
|
|
486
585
|
}
|
|
487
586
|
return;
|
|
488
587
|
}
|
|
489
588
|
|
|
490
589
|
const [project, { apiUrl, token }] = await Promise.all([readProject(args[0]), apiCredentials()]);
|
|
491
|
-
const response = await fetch(`${apiUrl}/api/projects/${project.config.name}/deployments`, {
|
|
590
|
+
const response = await fetch(`${apiUrl}/api/projects/${project.config.name}/deployments`, {
|
|
591
|
+
headers: { "x-api-key": token },
|
|
592
|
+
});
|
|
492
593
|
const deployments = parseVersionList(await responseText(response, "could not list versions"));
|
|
493
594
|
if (!deployments) fail("could not parse versions response");
|
|
494
|
-
for (const deployment of deployments)
|
|
595
|
+
for (const deployment of deployments)
|
|
596
|
+
console.log(
|
|
597
|
+
`${deployment.active ? "*" : " "} ${deployment.id} ${deployment.artifact.slice(0, 12)} ${deployment.deployedAt}`,
|
|
598
|
+
);
|
|
495
599
|
}
|
|
496
600
|
|
|
497
601
|
async function rollback(args: string[]) {
|
|
498
602
|
const id = args[0];
|
|
499
603
|
if (!id) usageError("rollback: missing <version-id>", "rollback <version-id> [project-dir]");
|
|
500
604
|
const [project, { apiUrl, token }] = await Promise.all([readProject(args[1]), apiCredentials()]);
|
|
501
|
-
const response = await fetch(`${apiUrl}/api/projects/${project.config.name}/deployments/${id}/activate`, {
|
|
605
|
+
const response = await fetch(`${apiUrl}/api/projects/${project.config.name}/deployments/${id}/activate`, {
|
|
606
|
+
method: "POST",
|
|
607
|
+
headers: { "x-api-key": token },
|
|
608
|
+
});
|
|
502
609
|
const deployment = parseUrlResponse(await responseText(response, "rollback rejected"));
|
|
503
610
|
if (!deployment) fail("rollback response did not include a URL");
|
|
504
611
|
console.log(ok(`rolled back ${project.config.name}`));
|
|
@@ -510,7 +617,9 @@ async function tail(args: string[]) {
|
|
|
510
617
|
const dir = args.find((arg) => !arg.startsWith("-"));
|
|
511
618
|
const [project, { apiUrl, token }] = await Promise.all([readProject(dir), apiCredentials()]);
|
|
512
619
|
const path = sproutLog ? "logs/sprout" : "logs/recent";
|
|
513
|
-
const response = await fetch(`${apiUrl}/api/projects/${project.config.name}/${path}`, {
|
|
620
|
+
const response = await fetch(`${apiUrl}/api/projects/${project.config.name}/${path}`, {
|
|
621
|
+
headers: { "x-api-key": token },
|
|
622
|
+
});
|
|
514
623
|
process.stdout.write(await responseText(response, "could not read logs"));
|
|
515
624
|
}
|
|
516
625
|
|
|
@@ -525,9 +634,18 @@ function parseDomain(source: string): DomainView | undefined {
|
|
|
525
634
|
const record = jsonObject(parseJsonValue(source));
|
|
526
635
|
if (!record || !isString(record.hostname) || !isBoolean(record.verified)) return undefined;
|
|
527
636
|
const v = jsonObject(record.verification ?? null);
|
|
528
|
-
const verification =
|
|
637
|
+
const verification =
|
|
638
|
+
v && isString(v.type) && isString(v.name) && isString(v.value)
|
|
639
|
+
? { type: v.type, name: v.name, value: v.value }
|
|
640
|
+
: null;
|
|
529
641
|
const serverAddresses = Array.isArray(record.serverAddresses) ? record.serverAddresses.filter(isString) : [];
|
|
530
|
-
return {
|
|
642
|
+
return {
|
|
643
|
+
hostname: record.hostname,
|
|
644
|
+
verified: record.verified,
|
|
645
|
+
verification,
|
|
646
|
+
serverAddresses,
|
|
647
|
+
warning: isString(record.warning) ? record.warning : undefined,
|
|
648
|
+
};
|
|
531
649
|
}
|
|
532
650
|
function printDomain(domain: DomainView) {
|
|
533
651
|
const status = domain.verified ? "verified" : "unverified";
|
|
@@ -536,16 +654,22 @@ function printDomain(domain: DomainView) {
|
|
|
536
654
|
console.log(" add these DNS records, then run: sproutboat domains verify " + domain.hostname);
|
|
537
655
|
console.log(` ${domain.verification.type} ${domain.verification.name} "${domain.verification.value}"`);
|
|
538
656
|
if (domain.serverAddresses[0]) {
|
|
539
|
-
console.log(
|
|
657
|
+
console.log(
|
|
658
|
+
` A ${domain.hostname} ${domain.serverAddresses[0]} (point the hostname here, DNS-only / not proxied)`,
|
|
659
|
+
);
|
|
540
660
|
}
|
|
541
661
|
}
|
|
542
662
|
if (domain.warning) console.log(amber(` ! ${domain.warning}`));
|
|
543
663
|
}
|
|
544
664
|
|
|
545
665
|
async function domains(args: string[]) {
|
|
546
|
-
const sub =
|
|
666
|
+
const sub =
|
|
667
|
+
args[0] && !args[0].startsWith("-") && ["list", "add", "verify", "delete"].includes(args[0])
|
|
668
|
+
? args.shift()!
|
|
669
|
+
: "list";
|
|
547
670
|
const host = sub === "list" ? undefined : args.shift();
|
|
548
|
-
if (sub !== "list" && !host)
|
|
671
|
+
if (sub !== "list" && !host)
|
|
672
|
+
usageError(`domains ${sub}: missing <hostname>`, `domains ${sub} <hostname> [project-dir]`);
|
|
549
673
|
const [project, { apiUrl, token }] = await Promise.all([readProject(args[0]), apiCredentials()]);
|
|
550
674
|
const base = `${apiUrl}/api/projects/${project.config.name}/domains`;
|
|
551
675
|
const auth = { "x-api-key": token };
|
|
@@ -555,8 +679,14 @@ async function domains(args: string[]) {
|
|
|
555
679
|
const body = await responseText(response, "could not list domains");
|
|
556
680
|
const list = parseJsonValue(body);
|
|
557
681
|
if (!Array.isArray(list)) fail("could not parse domains response");
|
|
558
|
-
if (list.length === 0) {
|
|
559
|
-
|
|
682
|
+
if (list.length === 0) {
|
|
683
|
+
console.log("no custom domains");
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
for (const entry of list) {
|
|
687
|
+
const d = parseDomain(JSON.stringify(entry));
|
|
688
|
+
if (d) printDomain(d);
|
|
689
|
+
}
|
|
560
690
|
return;
|
|
561
691
|
}
|
|
562
692
|
if (sub === "delete") {
|
|
@@ -566,9 +696,14 @@ async function domains(args: string[]) {
|
|
|
566
696
|
return;
|
|
567
697
|
}
|
|
568
698
|
const url = sub === "add" ? base : `${base}/${host}/verify`;
|
|
569
|
-
const init =
|
|
570
|
-
|
|
571
|
-
|
|
699
|
+
const init =
|
|
700
|
+
sub === "add"
|
|
701
|
+
? {
|
|
702
|
+
method: "POST",
|
|
703
|
+
headers: { ...auth, "content-type": "application/json" },
|
|
704
|
+
body: JSON.stringify({ hostname: host }),
|
|
705
|
+
}
|
|
706
|
+
: { method: "POST", headers: auth };
|
|
572
707
|
const response = await fetch(url, init);
|
|
573
708
|
const domain = parseDomain(await responseText(response, `${sub} rejected`));
|
|
574
709
|
if (!domain) fail(`${sub} response was not a domain record`);
|
|
@@ -612,7 +747,11 @@ async function secrets(args: string[]) {
|
|
|
612
747
|
const value = inlineValue ?? (await Bun.stdin.text()).replace(/\r?\n$/, "");
|
|
613
748
|
if (!value) fail("no value — pipe it on stdin, or pass --value <value>");
|
|
614
749
|
await responseText(
|
|
615
|
-
await fetch(`${base}/${name}`, {
|
|
750
|
+
await fetch(`${base}/${name}`, {
|
|
751
|
+
method: "PUT",
|
|
752
|
+
headers: { ...auth, "content-type": "application/json" },
|
|
753
|
+
body: JSON.stringify({ value }),
|
|
754
|
+
}),
|
|
616
755
|
"put rejected",
|
|
617
756
|
);
|
|
618
757
|
console.log(ok(`set ${name} — applies on the next deploy or sprout restart`));
|
|
@@ -653,10 +792,15 @@ async function storage(key: string, args: string[]) {
|
|
|
653
792
|
|
|
654
793
|
if (sub === "list") {
|
|
655
794
|
const rows = await storageRows(base, auth, product);
|
|
656
|
-
if (rows.length === 0) {
|
|
795
|
+
if (rows.length === 0) {
|
|
796
|
+
console.log(`no ${product.plural}`);
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
657
799
|
for (const row of rows) {
|
|
658
800
|
const bound = Array.isArray(row.projects) ? row.projects.filter(isString) : [];
|
|
659
|
-
console.log(
|
|
801
|
+
console.log(
|
|
802
|
+
`${String(row.id).padEnd(30)} ${String(row.name).padEnd(24)} ${bound.length ? bound.join(", ") : dim("unbound")}`,
|
|
803
|
+
);
|
|
660
804
|
}
|
|
661
805
|
return;
|
|
662
806
|
}
|
|
@@ -666,7 +810,11 @@ async function storage(key: string, args: string[]) {
|
|
|
666
810
|
|
|
667
811
|
if (sub === "create") {
|
|
668
812
|
const body = await responseText(
|
|
669
|
-
await fetch(base, {
|
|
813
|
+
await fetch(base, {
|
|
814
|
+
method: "POST",
|
|
815
|
+
headers: { ...auth, "content-type": "application/json" },
|
|
816
|
+
body: JSON.stringify({ name }),
|
|
817
|
+
}),
|
|
670
818
|
"create rejected",
|
|
671
819
|
);
|
|
672
820
|
const record = jsonObject(jsonObject(parseJsonValue(body))?.resource ?? null);
|
|
@@ -692,7 +840,11 @@ async function storage(key: string, args: string[]) {
|
|
|
692
840
|
const next = args.shift();
|
|
693
841
|
if (!next) usageError(`${key} rename: missing <new-name>`, `${key} rename <name> <new-name>`);
|
|
694
842
|
await responseText(
|
|
695
|
-
await fetch(`${base}/${id}`, {
|
|
843
|
+
await fetch(`${base}/${id}`, {
|
|
844
|
+
method: "PATCH",
|
|
845
|
+
headers: { ...auth, "content-type": "application/json" },
|
|
846
|
+
body: JSON.stringify({ name: next }),
|
|
847
|
+
}),
|
|
696
848
|
"rename rejected",
|
|
697
849
|
);
|
|
698
850
|
console.log(ok(`renamed ${name} → ${next}`));
|
|
@@ -721,10 +873,17 @@ async function deleteProject(args: string[]) {
|
|
|
721
873
|
if (!confirmed) fail(`this permanently removes "${name}", every version, and its route — re-run with --yes`);
|
|
722
874
|
|
|
723
875
|
const url = `${apiUrl}/api/projects/${encodeURIComponent(name)}?confirm=${encodeURIComponent(name)}`;
|
|
724
|
-
const body = await responseText(
|
|
876
|
+
const body = await responseText(
|
|
877
|
+
await fetch(url, { method: "DELETE", headers: { "x-api-key": token } }),
|
|
878
|
+
"delete rejected",
|
|
879
|
+
);
|
|
725
880
|
|
|
726
881
|
let result: JsonObject = {};
|
|
727
|
-
try {
|
|
882
|
+
try {
|
|
883
|
+
result = jsonObject(parseJsonValue(body)) ?? {};
|
|
884
|
+
} catch {
|
|
885
|
+
/* a 2xx already confirmed the delete */
|
|
886
|
+
}
|
|
728
887
|
const versions = isSafeInteger(result.versionsRemoved) ? result.versionsRemoved : 0;
|
|
729
888
|
const routes = Array.isArray(result.routeRemoved) ? result.routeRemoved.filter(isString) : [];
|
|
730
889
|
const failed = Array.isArray(result.artifactCleanupFailed) ? result.artifactCleanupFailed.filter(isString) : [];
|
|
@@ -748,29 +907,67 @@ function usage(): never {
|
|
|
748
907
|
|
|
749
908
|
const [command, ...args] = process.argv.slice(2);
|
|
750
909
|
if (command === undefined || command === "help" || command === "-h" || command === "--help") help();
|
|
751
|
-
if (command === "--version" || command === "-v") {
|
|
910
|
+
if (command === "--version" || command === "-v") {
|
|
911
|
+
console.log(`sproutboat ${CLI_VERSION}`);
|
|
912
|
+
process.exit(0);
|
|
913
|
+
}
|
|
752
914
|
|
|
753
915
|
await notifyIfOutdated(CLI_VERSION);
|
|
754
916
|
|
|
755
917
|
switch (command) {
|
|
756
|
-
case "init":
|
|
757
|
-
|
|
758
|
-
|
|
918
|
+
case "init":
|
|
919
|
+
await init(args[0]);
|
|
920
|
+
break;
|
|
921
|
+
case "check":
|
|
922
|
+
await check(args[0]);
|
|
923
|
+
break;
|
|
924
|
+
case "dev":
|
|
925
|
+
await dev(args);
|
|
926
|
+
break;
|
|
759
927
|
case "build": {
|
|
760
928
|
const hostBuild = args.includes("--target") && args[args.indexOf("--target") + 1] === "host";
|
|
761
|
-
await build(
|
|
929
|
+
await build(
|
|
930
|
+
args.find((arg) => !arg.startsWith("--") && arg !== "host"),
|
|
931
|
+
hostBuild ? "host" : "linux-x86_64",
|
|
932
|
+
);
|
|
762
933
|
break;
|
|
763
934
|
}
|
|
764
|
-
case "login":
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
case "
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
case "
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
case "
|
|
774
|
-
|
|
775
|
-
|
|
935
|
+
case "login":
|
|
936
|
+
await login(args);
|
|
937
|
+
break;
|
|
938
|
+
case "logout":
|
|
939
|
+
await logout(args);
|
|
940
|
+
break;
|
|
941
|
+
case "whoami":
|
|
942
|
+
await whoami();
|
|
943
|
+
break;
|
|
944
|
+
case "deploy":
|
|
945
|
+
await deploy(args);
|
|
946
|
+
break;
|
|
947
|
+
case "versions":
|
|
948
|
+
await versions(args);
|
|
949
|
+
break;
|
|
950
|
+
case "rollback":
|
|
951
|
+
await rollback(args);
|
|
952
|
+
break;
|
|
953
|
+
case "domains":
|
|
954
|
+
await domains(args);
|
|
955
|
+
break;
|
|
956
|
+
case "secrets":
|
|
957
|
+
await secrets(args);
|
|
958
|
+
break;
|
|
959
|
+
case "kv":
|
|
960
|
+
case "d1":
|
|
961
|
+
case "r2":
|
|
962
|
+
case "queues":
|
|
963
|
+
await storage(command, args);
|
|
964
|
+
break;
|
|
965
|
+
case "tail":
|
|
966
|
+
await tail(args);
|
|
967
|
+
break;
|
|
968
|
+
case "delete":
|
|
969
|
+
await deleteProject(args);
|
|
970
|
+
break;
|
|
971
|
+
default:
|
|
972
|
+
usage();
|
|
776
973
|
}
|