sproutboat 0.4.0 → 0.4.2

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/SURFACE.md CHANGED
@@ -3,7 +3,7 @@
3
3
  > Generated by `src/surface.test.ts` from `src/surface.ts` + the pinned
4
4
  > toolchain constants. Do not edit by hand — run `UPDATE_SURFACE=1 bun test`.
5
5
 
6
- **Package:** `sproutboat` 0.4.0 · runs on Bun (use `bunx`, not `npx`)
6
+ **Package:** `sproutboat` 0.4.2 · runs on Bun (use `bunx`, not `npx`)
7
7
 
8
8
  ## Commands
9
9
 
@@ -12,17 +12,17 @@
12
12
  | `init` | `[name]` | Scaffold sproutboat.jsonc + src/index.js in ./<name>. |
13
13
  | `check` | `[project-dir]` | Validate the config and entry point without building. |
14
14
  | `build` | `[project-dir]` | Cross-compile the native-fetch sprout (Porffor + Zig). |
15
- | `deploy` | `[project-dir] [--dry-run] [--artifact <dir>]` | Build (unless --artifact), print the report, upload. --dry-run stops before upload. |
15
+ | `deploy` | `[project-dir] [--dry-run] [--artifact <dir>] [--no-wait]` | Build (unless --artifact), print the report, upload, wait until the URL serves. --dry-run stops before upload; --no-wait skips the health check. |
16
16
  | `login` | `[--api-url <url>] [--token <token>]` | Device-code browser flow, or store <token> for <url> directly. |
17
- | `tail` | `[project-dir]` | Print the project's recent request logs. |
17
+ | `tail` | `[project-dir] [--sprout]` | Print recent request logs; --sprout prints the running sprout + broker stdout/stderr instead. |
18
18
  | `versions` | `list [project-dir]` | List the project's deployed versions. |
19
19
  | `rollback` | `<version-id> [project-dir]` | Re-activate a previous version. |
20
20
  | `domains` | `[list | add <host> | verify <host> | rm <host>] [project-dir]` | Attach a custom domain to the project (TXT-verified). No sub-command lists. |
21
21
  | `secrets` | `[list | set <NAME> [value] | rm <NAME>] [project-dir]` | Manage encrypted project secrets (read as env.NAME). `set` takes the value from the arg or stdin; applies on next deploy. |
22
- | `delete` | `--yes [project-dir]` | Delete the project and every version. |
22
+ | `delete` | `[project-dir] [--name <project>] --yes` | Delete the project, every version, and its route. |
23
23
 
24
24
  ```
25
- usage: sproutboat <init [name] | check [project-dir] | build [project-dir] | deploy [project-dir] [--dry-run] [--artifact <dir>] | login [--api-url <url>] [--token <token>] | tail [project-dir] | versions list [project-dir] | rollback <version-id> [project-dir] | domains [list | add <host> | verify <host> | rm <host>] [project-dir] | secrets [list | set <NAME> [value] | rm <NAME>] [project-dir] | delete --yes [project-dir]>
25
+ usage: sproutboat <init [name] | check [project-dir] | build [project-dir] | deploy [project-dir] [--dry-run] [--artifact <dir>] [--no-wait] | login [--api-url <url>] [--token <token>] | tail [project-dir] [--sprout] | versions list [project-dir] | rollback <version-id> [project-dir] | domains [list | add <host> | verify <host> | rm <host>] [project-dir] | secrets [list | set <NAME> [value] | rm <NAME>] [project-dir] | delete [project-dir] [--name <project>] --yes>
26
26
  ```
27
27
 
28
28
  ## Environment variables
@@ -37,6 +37,7 @@ usage: sproutboat <init [name] | check [project-dir] | build [project-dir] | dep
37
37
  | `SPROUTBOAT_VARS_JSON` | JSON object of baked `vars` (UPPER_SNAKE -> string), read by the wrapper when generating the sprout module. |
38
38
  | `SPROUTBOAT_BINDINGS_JSON` | The artifact's bindings.json, read by the wrapper to emit the `__sbInstallBindings` line. |
39
39
  | `SPROUTBOAT_CONFIG_DIR` | Directory for credentials.json (default ~/.config/sproutboat). |
40
+ | `NO_COLOR` | When set, disables coloured terminal output (https://no-color.org). Output is also plain whenever stdout is not a TTY. |
40
41
  | `XDG_CONFIG_HOME` | Base for the default credentials dir when SPROUTBOAT_CONFIG_DIR is unset. |
41
42
  | `PORFFOR_VERSION` | Override the Porffor identity string recorded in the manifest. |
42
43
  | `SB_BROKER_PORT` | Loopback port of the binding broker, read by the compiled sprout at runtime (set by the control plane, or by `src/broker.ts` for local runs). |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sproutboat",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Wrangler-shaped CLI for Sproutboat. Deploys workers to any control plane via --api-url / SPROUTBOAT_API_URL.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/main.ts CHANGED
@@ -8,6 +8,7 @@ import { validateManifest, type ArtifactManifest } from "./manifest";
8
8
  import { printDeployReport } from "./report";
9
9
  import { activeApiUrl, savedToken, saveToken } from "./credentials";
10
10
  import { usageLine } from "./surface";
11
+ import { amber, bold, dim, leaf, ok, rose } from "./style";
11
12
 
12
13
  const defaultApiUrl = "https://dashboard.sproutboat.com";
13
14
 
@@ -106,7 +107,7 @@ const starterHandler = `export default {
106
107
  `;
107
108
 
108
109
  function fail(message: string): never {
109
- console.error(`sproutboat: ${message}`);
110
+ console.error(`${rose("✗")} ${message}`);
110
111
  process.exit(1);
111
112
  }
112
113
 
@@ -147,14 +148,14 @@ async function init(name = "hello") {
147
148
 
148
149
  async function check(directory?: string) {
149
150
  const project = await readProject(directory);
150
- console.log(`check passed: ${project.config.name} (${project.config.main}, native-fetch)`);
151
+ console.log(ok(`check passed ${project.config.name} (${project.config.main}, native-fetch)`));
151
152
  }
152
153
 
153
154
  async function build(directory?: string) {
154
155
  const project = await readProject(directory);
155
- console.log("Compiling the native-fetch server with Porffor + Zig (linux-x86_64, static)...");
156
+ console.log(dim("Compiling the native-fetch server with Porffor + Zig (linux-x86_64, static)"));
156
157
  const artifact = await buildArtifact({ projectDir: project.directory, config: project.config, sourcePath: project.sourcePath });
157
- console.log(`Built ${project.config.name}`);
158
+ console.log(ok(`built ${project.config.name}`));
158
159
  console.log(artifact.artifactDir);
159
160
  return { project, artifact };
160
161
  }
@@ -201,7 +202,7 @@ async function deploy(args: string[]) {
201
202
  if (!deployments) fail("could not parse deployment list response");
202
203
  const active = deployments.find((deployment) => deployment.active && deployment.artifact === digest);
203
204
  if (active) {
204
- console.log(`Nothing to deploy — artifact ${digest.slice(0, 12)} is already active`);
205
+ console.log(ok(`nothing to deploy — artifact ${digest.slice(0, 12)} is already active`));
205
206
  console.log(`https://${active.hostname}`);
206
207
  return;
207
208
  }
@@ -236,14 +237,40 @@ async function deploy(args: string[]) {
236
237
  const body = await responseText(response, "deployment rejected");
237
238
  const deployed = parseUrlResponse(body);
238
239
  if (!deployed) fail("deployment response did not include a URL");
239
- console.log(`\nDeployed ${projectName}`);
240
- console.log(` ${deployed.url}`);
240
+ console.log(`\n${leaf("🌱")} ${bold(leaf(`Deployed ${projectName}`))}`);
241
+ console.log(` ${bold(deployed.url)}`);
241
242
  const drift = parsePorfforDrift(body);
242
243
  if (drift) {
243
- console.warn(`\n! Porffor pin changed: ${drift.from} -> ${drift.to}`);
244
- console.warn(` The previous live version stays frozen at ${drift.from}; this one is built with ${drift.to}.`);
245
- console.warn(` The alpha compiler's output can differ between pins (see COMPAT.md) — roll back if this version misbehaves.`);
244
+ console.warn(amber(`\n! Porffor pin changed: ${drift.from} -> ${drift.to}`));
245
+ console.warn(dim(` The previous live version stays frozen at ${drift.from}; this one is built with ${drift.to}.`));
246
+ console.warn(dim(` The alpha compiler's output can differ between pins (see COMPAT.md) — roll back if this version misbehaves.`));
246
247
  }
248
+ if (!args.includes("--no-wait")) {
249
+ const healthy = await waitForHealthy(deployed.url, 90_000);
250
+ console.log(healthy
251
+ ? ` ${ok("serving")}`
252
+ : amber(" ! not serving after 90s — Caddy may still be issuing the cert, or the sprout is crashing (`sproutboat tail`)"));
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Poll the deployment URL until the edge returns any non-5xx response. Connection
258
+ * and TLS errors (the cert is issued on the first HTTPS request) count as "not
259
+ * ready yet". Returns false on timeout without failing the deploy — the artifact
260
+ * is already active server-side.
261
+ */
262
+ async function waitForHealthy(url: string, timeoutMs: number): Promise<boolean> {
263
+ const deadline = Date.now() + timeoutMs;
264
+ let wait = 1000;
265
+ while (Date.now() < deadline) {
266
+ try {
267
+ const response = await fetch(url, { method: "HEAD", redirect: "manual" });
268
+ if (response.status < 500) return true;
269
+ } catch { /* DNS / TLS-not-yet-issued / connection refused — keep waiting */ }
270
+ await Bun.sleep(Math.min(wait, Math.max(0, deadline - Date.now())));
271
+ if (wait < 5000) wait += 1000;
272
+ }
273
+ return false;
247
274
  }
248
275
 
249
276
  function parseLoginArgs(args: string[]) {
@@ -290,7 +317,7 @@ async function login(args: string[]) {
290
317
  const token = parseToken(result);
291
318
  if (!token) fail("login response did not include a CLI token");
292
319
  await saveToken(apiUrl, token);
293
- console.log("Login approved. Credentials were saved locally for this API endpoint.");
320
+ console.log(ok("login approved credentials saved for this endpoint"));
294
321
  return;
295
322
  }
296
323
  fail("login expired before approval");
@@ -319,13 +346,16 @@ async function rollback(args: string[]) {
319
346
  const response = await fetch(`${apiUrl}/api/projects/${project.config.name}/deployments/${id}/activate`, { method: "POST", headers: { "x-api-key": token } });
320
347
  const deployment = parseUrlResponse(await responseText(response, "rollback rejected"));
321
348
  if (!deployment) fail("rollback response did not include a URL");
322
- console.log(`Rolled back ${project.config.name}`);
349
+ console.log(ok(`rolled back ${project.config.name}`));
323
350
  console.log(deployment.url);
324
351
  }
325
352
 
326
353
  async function tail(args: string[]) {
327
- const [project, { apiUrl, token }] = await Promise.all([readProject(args[0]), apiCredentials()]);
328
- const response = await fetch(`${apiUrl}/api/projects/${project.config.name}/logs/recent`, { headers: { "x-api-key": token } });
354
+ const sproutLog = args.includes("--sprout");
355
+ const dir = args.find((arg) => !arg.startsWith("-"));
356
+ const [project, { apiUrl, token }] = await Promise.all([readProject(dir), apiCredentials()]);
357
+ const path = sproutLog ? "logs/sprout" : "logs/recent";
358
+ const response = await fetch(`${apiUrl}/api/projects/${project.config.name}/${path}`, { headers: { "x-api-key": token } });
329
359
  process.stdout.write(await responseText(response, "could not read logs"));
330
360
  }
331
361
 
@@ -366,7 +396,7 @@ async function domains(args: string[]) {
366
396
  if (sub === "rm") {
367
397
  const response = await fetch(`${base}/${host}`, { method: "DELETE", headers: auth });
368
398
  await responseText(response, "delete rejected");
369
- console.log(`Removed ${host}`);
399
+ console.log(ok(`removed ${host}`));
370
400
  return;
371
401
  }
372
402
  const url = sub === "add" ? base : `${base}/${host}/verify`;
@@ -397,7 +427,7 @@ async function secrets(args: string[]) {
397
427
  }
398
428
  if (sub === "rm") {
399
429
  await responseText(await fetch(`${base}/${name}`, { method: "DELETE", headers: auth }), "delete rejected");
400
- console.log(`Removed ${name}`);
430
+ console.log(ok(`removed ${name}`));
401
431
  return;
402
432
  }
403
433
  // set: value from the next arg, else stdin (keeps it out of shell history).
@@ -413,11 +443,34 @@ async function secrets(args: string[]) {
413
443
  }
414
444
 
415
445
  async function deleteProject(args: string[]) {
416
- if (args[0] !== "--yes") fail("refusing to delete without --yes");
417
- const [project, { apiUrl, token }] = await Promise.all([readProject(args[1]), apiCredentials()]);
418
- const response = await fetch(`${apiUrl}/api/projects/${project.config.name}`, { method: "DELETE", headers: { "x-api-key": token } });
419
- await responseText(response, "delete rejected");
420
- console.log(`Deleted ${project.config.name}`);
446
+ // `sproutboat delete [project-dir] [--name <project>] --yes` flags in any order.
447
+ const positional: string[] = [];
448
+ let confirmed = false;
449
+ let explicitName: string | undefined;
450
+ for (let index = 0; index < args.length; index += 1) {
451
+ const arg = args[index];
452
+ if (arg === "--yes" || arg === "-y") confirmed = true;
453
+ else if (arg === "--name") explicitName = args[(index += 1)];
454
+ else if (!arg.startsWith("-")) positional.push(arg);
455
+ else fail(`unknown flag ${arg}\nusage: sproutboat delete [project-dir] [--name <project>] --yes`);
456
+ }
457
+
458
+ const { apiUrl, token } = await apiCredentials();
459
+ const name = explicitName ?? (await readProject(positional[0])).config.name;
460
+ if (!confirmed) fail(`this permanently removes "${name}", every version, and its route — re-run with --yes`);
461
+
462
+ const url = `${apiUrl}/api/projects/${encodeURIComponent(name)}?confirm=${encodeURIComponent(name)}`;
463
+ const body = await responseText(await fetch(url, { method: "DELETE", headers: { "x-api-key": token } }), "delete rejected");
464
+
465
+ let result: JsonObject = {};
466
+ try { result = jsonObject(parseJsonValue(body)) ?? {}; } catch { /* a 2xx already confirmed the delete */ }
467
+ const versions = isSafeInteger(result.versionsRemoved) ? result.versionsRemoved : 0;
468
+ const routes = Array.isArray(result.routeRemoved) ? result.routeRemoved.filter(isString) : [];
469
+ const failed = Array.isArray(result.artifactCleanupFailed) ? result.artifactCleanupFailed.filter(isString) : [];
470
+
471
+ console.log(ok(`deleted ${name} — ${versions} version${versions === 1 ? "" : "s"} removed`));
472
+ for (const route of routes) console.log(` released ${route}`);
473
+ if (failed.length) console.log(` ! ${failed.length} artifact file(s) left on disk — remove them manually`);
421
474
  }
422
475
 
423
476
  function usage(): never {
package/src/report.ts CHANGED
@@ -1,8 +1,11 @@
1
+ import { readFileSync } from "node:fs";
1
2
  import { gzipSync } from "bun";
2
3
  import type { SproutboatConfig } from "./config";
3
4
  import type { ArtifactManifest } from "./manifest";
5
+ import { bold, dim, leaf, sprout } from "./style";
4
6
 
5
- const CLI_VERSION = "0.1.0";
7
+ // Read from package.json so the banner never drifts from the published version.
8
+ const CLI_VERSION = (JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")) as { version: string }).version;
6
9
 
7
10
  function bytes(n: number): string {
8
11
  if (n < 1024) return `${n} B`;
@@ -11,55 +14,75 @@ function bytes(n: number): string {
11
14
  return `${(kib / 1024).toFixed(2)} MiB`;
12
15
  }
13
16
 
14
- /** Minimal box table. `align` marks columns to right-pad-left (numbers). */
17
+ /** Minimal box table with a green frame. `align` marks columns to right-pad-left (numbers). */
15
18
  function table(headers: string[], rows: string[][], align: boolean[] = []): string {
16
19
  const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length)));
17
- const line = (l: string, m: string, r: string) => l + widths.map((w) => "─".repeat(w + 2)).join(m) + r;
18
- const row = (cells: string[]) =>
19
- "│ " + cells.map((c, i) => (align[i] ? (c ?? "").padStart(widths[i]) : (c ?? "").padEnd(widths[i]))).join(" │ ") + " │";
20
- return [line("┌", "┬", "┐"), row(headers), line("├", "┼", ""), ...rows.map(row), line("└", "┴", "")].join("\n");
20
+ const rule = (l: string, m: string, r: string) => leaf(l + widths.map((w) => "─".repeat(w + 2)).join(m) + r);
21
+ const row = (cells: string[], head = false) =>
22
+ leaf("│ ") + cells.map((c, i) => {
23
+ const cell = align[i] ? (c ?? "").padStart(widths[i]) : (c ?? "").padEnd(widths[i]);
24
+ return head ? bold(cell) : cell;
25
+ }).join(leaf(" │ ")) + leaf(" │");
26
+ return [rule("┌", "┬", "┐"), row(headers, true), rule("├", "┼", "┤"), ...rows.map((r) => row(r)), rule("└", "┴", "┘")].join("\n");
27
+ }
28
+
29
+ /** Every binding the compiled sprout will see, flattened to (name, type, detail) rows. */
30
+ function bindingRows(config: SproutboatConfig): string[][] {
31
+ const rows: string[][] = [];
32
+ for (const [name, value] of Object.entries(config.vars ?? {})) rows.push([`env.${name}`, "var", JSON.stringify(value)]);
33
+ const list = (names: string[] | undefined, type: string, detail = "") => {
34
+ for (const name of names ?? []) rows.push([`env.${name}`, type, detail]);
35
+ };
36
+ list(config.kv_namespaces, "kv");
37
+ list(config.secrets, "secret", "value withheld — set with `sproutboat secrets`");
38
+ list(config.d1_databases, "d1");
39
+ list(config.r2_buckets, "r2");
40
+ list(config.queues, "queue");
41
+ list(config.analytics_engine_datasets, "analytics");
42
+ for (const [name, className] of Object.entries(config.durable_objects ?? {})) rows.push([`env.${name}`, "durable object", className]);
43
+ for (const host of config.outbound ?? []) rows.push([`fetch()`, "outbound", host]);
44
+ for (const cron of config.triggers?.crons ?? []) rows.push([`scheduled()`, "cron", cron]);
45
+ if (config.assets?.binding) rows.push([`env.${config.assets.binding}`, "assets", config.assets.directory ?? ""]);
46
+ return rows;
21
47
  }
22
48
 
23
49
  /**
24
- * Wrangler-shaped build/deploy summary. Prints what the artifact contains, its
25
- * upload size, and the bindings the handler will see. Returns nothing.
50
+ * Wrangler-shaped build/deploy summary: what the artifact contains, its upload
51
+ * size, and every binding the handler will see. Returns nothing.
26
52
  */
27
53
  export function printDeployReport(
28
54
  config: SproutboatConfig,
29
55
  manifest: ArtifactManifest,
30
- sprout: Uint8Array,
56
+ sproutBin: Uint8Array,
31
57
  manifestBytes: number,
32
58
  ): void {
33
- const gz = gzipSync(Uint8Array.from(sprout)).length;
34
- const total = sprout.length + manifestBytes;
59
+ const gz = gzipSync(Uint8Array.from(sproutBin)).length;
60
+ const total = sproutBin.length + manifestBytes;
35
61
 
36
- console.log(`\n🌱 sproutboat ${CLI_VERSION}`);
37
- console.log("─".repeat(19));
38
- console.log(`Compiled ${manifest.project} with Porffor ${manifest.porfforVersion}`);
39
- console.log(` toolchain ${manifest.buildImage}`);
40
- console.log(` compat ${config.compatibility_date}`);
62
+ console.log(`\n${sprout("🌱")} ${bold(leaf(`sproutboat ${CLI_VERSION}`))}`);
63
+ console.log(leaf("─".repeat(19)));
64
+ console.log(`Compiled ${bold(manifest.project)} with Porffor ${manifest.porfforVersion}`);
65
+ console.log(dim(` toolchain ${manifest.buildImage}`));
66
+ console.log(dim(` compat ${config.compatibility_date}`));
41
67
  console.log();
42
68
 
43
- console.log("Artifact:");
69
+ console.log(bold("Artifact"));
44
70
  console.log(table(
45
71
  ["File", "Type", "Size"],
46
72
  [
47
- ["sprout", manifest.runtime, bytes(sprout.length)],
73
+ ["sprout", manifest.runtime, bytes(sproutBin.length)],
48
74
  ["manifest.json", "json", bytes(manifestBytes)],
49
75
  ],
50
76
  [false, false, true],
51
77
  ));
52
- console.log(`Total upload: ${bytes(total)} (sprout gzip: ${bytes(gz)})`);
78
+ console.log(`Total upload: ${bold(bytes(total))} ${dim(`(sprout gzip: ${bytes(gz)})`)}`);
53
79
  console.log();
54
80
 
55
- const vars = Object.entries(config.vars ?? {});
56
- console.log("Bindings the handler will see:");
57
- if (vars.length === 0) {
58
- console.log(" (none — add [vars] to sproutboat.jsonc)");
81
+ const rows = bindingRows(config);
82
+ console.log(bold("Bindings the handler will see"));
83
+ if (rows.length === 0) {
84
+ console.log(dim(" (none — add vars / kv_namespaces / secrets / … to sproutboat.jsonc)"));
59
85
  } else {
60
- console.log(table(
61
- ["Binding", "Type", "Value"],
62
- vars.map(([k, v]) => [`env.${k}`, "var", JSON.stringify(v)]),
63
- ));
86
+ console.log(table(["Binding", "Type", "Detail"], rows));
64
87
  }
65
88
  }
package/src/style.ts ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Green/leaf terminal styling. Every helper is a no-op when stdout is not a TTY
3
+ * or NO_COLOR is set (https://no-color.org), so piped/CI output stays plain.
4
+ */
5
+ const enabled = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
6
+ const paint = (code: string) => (text: string): string => (enabled ? `\x1b[${code}m${text}\x1b[0m` : text);
7
+
8
+ export const leaf = paint("32"); // green — headings, structure, the accent
9
+ export const sprout = paint("92"); // bright green — success
10
+ export const dim = paint("2"); // secondary detail
11
+ export const bold = paint("1");
12
+ export const amber = paint("33"); // warnings
13
+ export const rose = paint("31"); // errors
14
+
15
+ /** "✓ message" with a green tick. */
16
+ export const ok = (message: string): string => `${sprout("✓")} ${message}`;
17
+ /** "▸ message" with a green marker — section headers. */
18
+ export const step = (message: string): string => `${leaf("▸")} ${bold(message)}`;
package/src/surface.ts CHANGED
@@ -11,14 +11,14 @@ export const COMMANDS: readonly Command[] = [
11
11
  { name: "init", args: "[name]", summary: "Scaffold sproutboat.jsonc + src/index.js in ./<name>." },
12
12
  { name: "check", args: "[project-dir]", summary: "Validate the config and entry point without building." },
13
13
  { name: "build", args: "[project-dir]", summary: "Cross-compile the native-fetch sprout (Porffor + Zig)." },
14
- { name: "deploy", args: "[project-dir] [--dry-run] [--artifact <dir>]", summary: "Build (unless --artifact), print the report, upload. --dry-run stops before upload." },
14
+ { name: "deploy", args: "[project-dir] [--dry-run] [--artifact <dir>] [--no-wait]", summary: "Build (unless --artifact), print the report, upload, wait until the URL serves. --dry-run stops before upload; --no-wait skips the health check." },
15
15
  { name: "login", args: "[--api-url <url>] [--token <token>]", summary: "Device-code browser flow, or store <token> for <url> directly." },
16
- { name: "tail", args: "[project-dir]", summary: "Print the project's recent request logs." },
16
+ { name: "tail", args: "[project-dir] [--sprout]", summary: "Print recent request logs; --sprout prints the running sprout + broker stdout/stderr instead." },
17
17
  { name: "versions", args: "list [project-dir]", summary: "List the project's deployed versions." },
18
18
  { name: "rollback", args: "<version-id> [project-dir]", summary: "Re-activate a previous version." },
19
19
  { name: "domains", args: "[list | add <host> | verify <host> | rm <host>] [project-dir]", summary: "Attach a custom domain to the project (TXT-verified). No sub-command lists." },
20
20
  { name: "secrets", args: "[list | set <NAME> [value] | rm <NAME>] [project-dir]", summary: "Manage encrypted project secrets (read as env.NAME). `set` takes the value from the arg or stdin; applies on next deploy." },
21
- { name: "delete", args: "--yes [project-dir]", summary: "Delete the project and every version." },
21
+ { name: "delete", args: "[project-dir] [--name <project>] --yes", summary: "Delete the project, every version, and its route." },
22
22
  ];
23
23
 
24
24
  export type EnvVar = { name: string; purpose: string };
@@ -32,6 +32,7 @@ export const ENV_VARS: readonly EnvVar[] = [
32
32
  { name: "SPROUTBOAT_VARS_JSON", purpose: "JSON object of baked `vars` (UPPER_SNAKE -> string), read by the wrapper when generating the sprout module." },
33
33
  { name: "SPROUTBOAT_BINDINGS_JSON", purpose: "The artifact's bindings.json, read by the wrapper to emit the `__sbInstallBindings` line." },
34
34
  { name: "SPROUTBOAT_CONFIG_DIR", purpose: "Directory for credentials.json (default ~/.config/sproutboat)." },
35
+ { name: "NO_COLOR", purpose: "When set, disables coloured terminal output (https://no-color.org). Output is also plain whenever stdout is not a TTY." },
35
36
  { name: "XDG_CONFIG_HOME", purpose: "Base for the default credentials dir when SPROUTBOAT_CONFIG_DIR is unset." },
36
37
  { name: "PORFFOR_VERSION", purpose: "Override the Porffor identity string recorded in the manifest." },
37
38
  { name: "SB_BROKER_PORT", purpose: "Loopback port of the binding broker, read by the compiled sprout at runtime (set by the control plane, or by `src/broker.ts` for local runs)." },