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/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 = { deviceCode: string; userCode: string; verificationUri: string; interval: number; expiresAt: string };
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 (!record || !isString(record.id) || !isString(record.artifact) || !isString(record.deployedAt) || (record.active !== true && record.active !== false)) return undefined;
35
- deployments.push({ id: record.id, artifact: record.artifact, deployedAt: record.deployedAt, active: record.active });
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(source: string): { url: string; id?: string; artifact?: string; unchanged: boolean } | undefined {
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 = (() => { try { return jsonObject(parseJsonValue(source))?.porfforDrift; } catch { return undefined; } })();
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 (!record || !isString(record.deviceCode) || !isString(record.userCode) || !isString(record.verificationUri) || !isSafeInteger(record.interval) || !isString(record.expiresAt)) return undefined;
64
- return { deviceCode: record.deviceCode, userCode: record.userCode, verificationUri: record.verificationUri, interval: record.interval, expiresAt: record.expiresAt };
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)) fail("project name must be a 3–32 character lowercase slug");
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 [[configPath, "sproutboat.jsonc"], [handlerPath, "src/index.js"]] as const) {
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(target === "host"
177
- ? dim(`Compiling the native-fetch server with Porffor for this machine (${hostTarget()}, local only)…`)
178
- : dim("Compiling the native-fetch server with Porffor + Zig (linux-x86_64, static)…"));
179
- const artifact = await buildArtifact({ projectDir: project.directory, config: project.config, sourcePath: project.sourcePath, source: project.bundle.code, target });
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") console.log(dim(" host build — runs here, not deployable; drop --target host to build for a box"));
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) usageError(`invalid --port: ${portArg}`, "dev [project-dir] [--port <n>] [--no-watch]");
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(await forgetToken(apiUrl)
258
- ? ok(`forgot the credential for ${apiUrl}`)
259
- : `no stored credential for ${apiUrl}`);
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) { console.log("not logged in — run `sproutboat login`"); return; }
270
- const token = process.env.SPROUTBOAT_TOKEN || await savedToken(apiUrl);
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) { console.log(`account ${dim("no stored token — run `sproutboat login`")}`); return; }
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(`account ${rose(response.status === 401 ? "token rejected — run `sproutboat login`" : `control plane said ${response.status}`)}`);
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("deploy: --artifact needs a directory", "deploy [project-dir] [--dry-run] [--artifact <dir>] [--no-wait]");
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("assets_manifest", new File([await assetsManifestFile.arrayBuffer()], "assets.json", { type: "application/json" }));
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) console.log(dim(` version ${deployed.id}${deployed.artifact ? ` · artifact ${deployed.artifact.slice(0, 12)}` : ""}`));
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(dim(` The alpha compiler's output can differ between pins (see COMPAT.md) — roll back if this version misbehaves.`));
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(amber(" ! not serving after 90s — Caddy may still be issuing the cert, or the sprout is crashing (`sproutboat tail`)"));
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 { /* DNS / TLS-not-yet-issued / connection refused — keep waiting */ }
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 = process.platform === "darwin" ? ["open", verificationUrl] : process.platform === "win32" ? ["cmd", "/c", "start", "", verificationUrl] : ["xdg-open", verificationUrl];
426
- try { Bun.spawn(openCommand, { stdout: "ignore", stderr: "ignore" }); }
427
- catch { console.log(`Open ${verificationUrl}`); }
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(sub ? `versions: unknown subcommand "${sub}"` : "versions: missing subcommand", "versions <list | view <version-id>> [project-dir]");
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)}`, { headers: { "x-api-key": token } }),
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(` deployed ${String(detail.deployedAt)}${isString(detail.deployedBy) ? ` by ${detail.deployedBy}` : ""}`);
571
+ console.log(
572
+ ` deployed ${String(detail.deployedAt)}${isString(detail.deployedBy) ? ` by ${detail.deployedBy}` : ""}`,
573
+ );
478
574
  if (manifest) {
479
- console.log(` built ${String(manifest.builtAt)} · porffor ${String(manifest.porfforVersion)} · ${String(manifest.binarySize)} bytes`);
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) console.log(` bound ${String(resource.kind)} ${String(resource.name)} ${dim(String(resource.id))}`);
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`, { headers: { "x-api-key": token } });
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) console.log(`${deployment.active ? "*" : " "} ${deployment.id} ${deployment.artifact.slice(0, 12)} ${deployment.deployedAt}`);
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`, { method: "POST", headers: { "x-api-key": token } });
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}`, { headers: { "x-api-key": token } });
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 = v && isString(v.type) && isString(v.name) && isString(v.value) ? { type: v.type, name: v.name, value: v.value } : null;
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 { hostname: record.hostname, verified: record.verified, verification, serverAddresses, warning: isString(record.warning) ? record.warning : undefined };
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(` A ${domain.hostname} ${domain.serverAddresses[0]} (point the hostname here, DNS-only / not proxied)`);
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 = args[0] && !args[0].startsWith("-") && ["list", "add", "verify", "delete"].includes(args[0]) ? args.shift()! : "list";
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) usageError(`domains ${sub}: missing <hostname>`, `domains ${sub} <hostname> [project-dir]`);
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) { console.log("no custom domains"); return; }
559
- for (const entry of list) { const d = parseDomain(JSON.stringify(entry)); if (d) printDomain(d); }
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 = sub === "add"
570
- ? { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ hostname: host }) }
571
- : { method: "POST", headers: auth };
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}`, { method: "PUT", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ value }) }),
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) { console.log(`no ${product.plural}`); return; }
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(`${String(row.id).padEnd(30)} ${String(row.name).padEnd(24)} ${bound.length ? bound.join(", ") : dim("unbound")}`);
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, { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ name }) }),
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}`, { method: "PATCH", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ name: next }) }),
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(await fetch(url, { method: "DELETE", headers: { "x-api-key": token } }), "delete rejected");
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 { result = jsonObject(parseJsonValue(body)) ?? {}; } catch { /* a 2xx already confirmed the delete */ }
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") { console.log(`sproutboat ${CLI_VERSION}`); process.exit(0); }
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": await init(args[0]); break;
757
- case "check": await check(args[0]); break;
758
- case "dev": await dev(args); break;
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(args.find((arg) => !arg.startsWith("--") && arg !== "host"), hostBuild ? "host" : "linux-x86_64");
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": await login(args); break;
765
- case "logout": await logout(args); break;
766
- case "whoami": await whoami(); break;
767
- case "deploy": await deploy(args); break;
768
- case "versions": await versions(args); break;
769
- case "rollback": await rollback(args); break;
770
- case "domains": await domains(args); break;
771
- case "secrets": await secrets(args); break;
772
- case "kv": case "d1": case "r2": case "queues": await storage(command, args); break;
773
- case "tail": await tail(args); break;
774
- case "delete": await deleteProject(args); break;
775
- default: usage();
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
  }