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 +6 -5
- package/package.json +1 -1
- package/src/main.ts +74 -21
- package/src/report.ts +50 -27
- package/src/style.ts +18 -0
- package/src/surface.ts +4 -3
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.
|
|
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
|
|
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` |
|
|
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
|
|
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
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(
|
|
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
|
|
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(`
|
|
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(`
|
|
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(`\
|
|
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("
|
|
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(`
|
|
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
|
|
328
|
-
const
|
|
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(`
|
|
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(`
|
|
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
|
-
|
|
417
|
-
const [
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
-
|
|
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
|
|
18
|
-
const row = (cells: string[]) =>
|
|
19
|
-
"│ " + cells.map((c, i) =>
|
|
20
|
-
|
|
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
|
|
25
|
-
*
|
|
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
|
-
|
|
56
|
+
sproutBin: Uint8Array,
|
|
31
57
|
manifestBytes: number,
|
|
32
58
|
): void {
|
|
33
|
-
const gz = gzipSync(Uint8Array.from(
|
|
34
|
-
const total =
|
|
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(
|
|
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
|
|
56
|
-
console.log("Bindings the handler will see
|
|
57
|
-
if (
|
|
58
|
-
console.log(" (none — add
|
|
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
|
|
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: "
|
|
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)." },
|