sproutboat 0.4.3 → 0.4.4
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 +2 -1
- package/package.json +1 -1
- package/src/credentials.ts +1 -1
- package/src/main.ts +39 -14
- package/src/report.ts +1 -1
- package/src/surface.ts +1 -0
- package/src/update-check.ts +55 -0
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.4 · runs on Bun (use `bunx`, not `npx`)
|
|
7
7
|
|
|
8
8
|
## Commands
|
|
9
9
|
|
|
@@ -38,6 +38,7 @@ usage: sproutboat <init [name] | check [project-dir] | build [project-dir] | dep
|
|
|
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
40
|
| `NO_COLOR` | When set, disables coloured terminal output (https://no-color.org). Output is also plain whenever stdout is not a TTY. |
|
|
41
|
+
| `SPROUTBOAT_NO_UPDATE_CHECK` | When set, skips the once-a-day npm check for a newer `sproutboat` release (also skipped when CI is set). |
|
|
41
42
|
| `XDG_CONFIG_HOME` | Base for the default credentials dir when SPROUTBOAT_CONFIG_DIR is unset. |
|
|
42
43
|
| `PORFFOR_VERSION` | Override the Porffor identity string recorded in the manifest. |
|
|
43
44
|
| `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/credentials.ts
CHANGED
|
@@ -28,7 +28,7 @@ function parseCredentials(value: JsonValue): Credentials | undefined {
|
|
|
28
28
|
return { version: 1, activeApiUrl: isString(input.activeApiUrl) ? input.activeApiUrl : undefined, profiles };
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
function configDirectory(): string {
|
|
31
|
+
export function configDirectory(): string {
|
|
32
32
|
const configured = process.env.SPROUTBOAT_CONFIG_DIR || process.env.XDG_CONFIG_HOME;
|
|
33
33
|
if (configured && isAbsolute(configured)) return resolve(configured, "sproutboat");
|
|
34
34
|
return resolve(homedir(), ".config", "sproutboat");
|
package/src/main.ts
CHANGED
|
@@ -5,9 +5,10 @@ import { parseConfig, type SproutboatConfig } from "./config";
|
|
|
5
5
|
import { validateHttpSyncSource } from "./source";
|
|
6
6
|
import { buildArtifact } from "./build";
|
|
7
7
|
import { validateManifest, type ArtifactManifest } from "./manifest";
|
|
8
|
-
import { printDeployReport } from "./report";
|
|
8
|
+
import { CLI_VERSION, printDeployReport } from "./report";
|
|
9
9
|
import { activeApiUrl, savedToken, saveToken } from "./credentials";
|
|
10
|
-
import { helpText
|
|
10
|
+
import { helpText } from "./surface";
|
|
11
|
+
import { notifyIfOutdated } from "./update-check";
|
|
11
12
|
import { amber, bold, dim, leaf, ok, rose } from "./style";
|
|
12
13
|
|
|
13
14
|
const defaultApiUrl = "https://dashboard.sproutboat.com";
|
|
@@ -66,9 +67,14 @@ function parseVersionList(source: string): VersionSummary[] | undefined {
|
|
|
66
67
|
return deployments;
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
function parseUrlResponse(source: string): { url: string } | undefined {
|
|
70
|
+
function parseUrlResponse(source: string): { url: string; id?: string; artifact?: string } | undefined {
|
|
70
71
|
const record = jsonObject(parseJsonValue(source));
|
|
71
|
-
|
|
72
|
+
if (!record || !isString(record.url)) return undefined;
|
|
73
|
+
return {
|
|
74
|
+
url: record.url,
|
|
75
|
+
id: isString(record.id) ? record.id : undefined,
|
|
76
|
+
artifact: isString(record.artifact) ? record.artifact : undefined,
|
|
77
|
+
};
|
|
72
78
|
}
|
|
73
79
|
|
|
74
80
|
/** #55: `{ from, to }` when this deploy moves the live version onto a different
|
|
@@ -106,11 +112,22 @@ const starterHandler = `export default {
|
|
|
106
112
|
};
|
|
107
113
|
`;
|
|
108
114
|
|
|
115
|
+
/** An operational failure — the command was invoked correctly but could not complete. Exit 1. */
|
|
109
116
|
function fail(message: string): never {
|
|
110
117
|
console.error(`${rose("✗")} ${message}`);
|
|
111
118
|
process.exit(1);
|
|
112
119
|
}
|
|
113
120
|
|
|
121
|
+
/**
|
|
122
|
+
* The command was invoked wrong (missing/unknown arg). Exit 2, the getopt/argparse
|
|
123
|
+
* convention, so scripts can tell "you typed it wrong" apart from "it broke".
|
|
124
|
+
* `usage` is the grammar line without the `usage: ` prefix.
|
|
125
|
+
*/
|
|
126
|
+
function usageError(what: string, usage: string): never {
|
|
127
|
+
console.error(`${rose("✗")} ${what}\n ${dim(`usage: sproutboat ${usage}`)}`);
|
|
128
|
+
process.exit(2);
|
|
129
|
+
}
|
|
130
|
+
|
|
114
131
|
async function readProject(directory = process.cwd()) {
|
|
115
132
|
const projectDirectory = resolve(directory);
|
|
116
133
|
const configPath = resolve(projectDirectory, "sproutboat.jsonc");
|
|
@@ -168,7 +185,9 @@ async function deploy(args: string[]) {
|
|
|
168
185
|
let artifactDir: string;
|
|
169
186
|
let config: SproutboatConfig | undefined;
|
|
170
187
|
if (artifactIndex >= 0) {
|
|
171
|
-
artifactDir = args[artifactIndex + 1]
|
|
188
|
+
artifactDir = args[artifactIndex + 1]
|
|
189
|
+
? resolve(args[artifactIndex + 1])
|
|
190
|
+
: usageError("deploy: --artifact needs a directory", "deploy [project-dir] [--dry-run] [--artifact <dir>] [--no-wait]");
|
|
172
191
|
projectName = "";
|
|
173
192
|
} else {
|
|
174
193
|
const built = await build(directory);
|
|
@@ -239,6 +258,8 @@ async function deploy(args: string[]) {
|
|
|
239
258
|
if (!deployed) fail("deployment response did not include a URL");
|
|
240
259
|
console.log(`\n${leaf("🌱")} ${bold(leaf(`Deployed ${projectName}`))}`);
|
|
241
260
|
console.log(` ${bold(deployed.url)}`);
|
|
261
|
+
if (deployed.id) console.log(dim(` version ${deployed.id}${deployed.artifact ? ` · artifact ${deployed.artifact.slice(0, 12)}` : ""}`));
|
|
262
|
+
for (const cron of config?.triggers?.crons ?? []) console.log(dim(` schedule ${cron}`));
|
|
242
263
|
const drift = parsePorfforDrift(body);
|
|
243
264
|
if (drift) {
|
|
244
265
|
console.warn(amber(`\n! Porffor pin changed: ${drift.from} -> ${drift.to}`));
|
|
@@ -280,7 +301,7 @@ function parseLoginArgs(args: string[]) {
|
|
|
280
301
|
const value = args[index + 1];
|
|
281
302
|
if (args[index] === "--api-url" && value) apiUrl = value;
|
|
282
303
|
else if (args[index] === "--token" && value) token = value;
|
|
283
|
-
else
|
|
304
|
+
else usageError(`login: unexpected argument "${args[index]}"`, "login [--api-url <url>] [--token <token>]");
|
|
284
305
|
}
|
|
285
306
|
return { apiUrl: apiUrl.replace(/\/$/, ""), token };
|
|
286
307
|
}
|
|
@@ -331,7 +352,7 @@ async function apiCredentials() {
|
|
|
331
352
|
}
|
|
332
353
|
|
|
333
354
|
async function versions(args: string[]) {
|
|
334
|
-
if (args[0] !== "list")
|
|
355
|
+
if (args[0] !== "list") usageError(args[0] ? `versions: unknown subcommand "${args[0]}"` : "versions: missing subcommand", "versions list [project-dir]");
|
|
335
356
|
const [project, { apiUrl, token }] = await Promise.all([readProject(args[1]), apiCredentials()]);
|
|
336
357
|
const response = await fetch(`${apiUrl}/api/projects/${project.config.name}/deployments`, { headers: { "x-api-key": token } });
|
|
337
358
|
const deployments = parseVersionList(await responseText(response, "could not list versions"));
|
|
@@ -341,7 +362,7 @@ async function versions(args: string[]) {
|
|
|
341
362
|
|
|
342
363
|
async function rollback(args: string[]) {
|
|
343
364
|
const id = args[0];
|
|
344
|
-
if (!id)
|
|
365
|
+
if (!id) usageError("rollback: missing <version-id>", "rollback <version-id> [project-dir]");
|
|
345
366
|
const [project, { apiUrl, token }] = await Promise.all([readProject(args[1]), apiCredentials()]);
|
|
346
367
|
const response = await fetch(`${apiUrl}/api/projects/${project.config.name}/deployments/${id}/activate`, { method: "POST", headers: { "x-api-key": token } });
|
|
347
368
|
const deployment = parseUrlResponse(await responseText(response, "rollback rejected"));
|
|
@@ -379,7 +400,7 @@ function printDomain(domain: DomainView) {
|
|
|
379
400
|
async function domains(args: string[]) {
|
|
380
401
|
const sub = args[0] && !args[0].startsWith("-") && ["list", "add", "verify", "rm"].includes(args[0]) ? args.shift()! : "list";
|
|
381
402
|
const host = sub === "list" ? undefined : args.shift();
|
|
382
|
-
if (sub !== "list" && !host)
|
|
403
|
+
if (sub !== "list" && !host) usageError(`domains ${sub}: missing <hostname>`, `domains ${sub} <hostname> [project-dir]`);
|
|
383
404
|
const [project, { apiUrl, token }] = await Promise.all([readProject(args[0]), apiCredentials()]);
|
|
384
405
|
const base = `${apiUrl}/api/projects/${project.config.name}/domains`;
|
|
385
406
|
const auth = { "x-api-key": token };
|
|
@@ -412,7 +433,7 @@ async function domains(args: string[]) {
|
|
|
412
433
|
async function secrets(args: string[]) {
|
|
413
434
|
const sub = args[0] && ["list", "set", "rm"].includes(args[0]) ? args.shift()! : "list";
|
|
414
435
|
const name = sub === "list" ? undefined : args.shift();
|
|
415
|
-
if (sub !== "list" && !name)
|
|
436
|
+
if (sub !== "list" && !name) usageError(`secrets ${sub}: missing <NAME>`, `secrets ${sub} <NAME> [project-dir]`);
|
|
416
437
|
if (name && !/^[A-Z][A-Z0-9_]*$/.test(name)) fail("secret name must be UPPER_SNAKE_CASE");
|
|
417
438
|
const [project, { apiUrl, token }] = await Promise.all([readProject(args[0]), apiCredentials()]);
|
|
418
439
|
const base = `${apiUrl}/api/projects/${project.config.name}/secrets`;
|
|
@@ -452,7 +473,7 @@ async function deleteProject(args: string[]) {
|
|
|
452
473
|
if (arg === "--yes" || arg === "-y") confirmed = true;
|
|
453
474
|
else if (arg === "--name") explicitName = args[(index += 1)];
|
|
454
475
|
else if (!arg.startsWith("-")) positional.push(arg);
|
|
455
|
-
else
|
|
476
|
+
else usageError(`delete: unknown flag "${arg}"`, "delete [project-dir] [--name <project>] --yes");
|
|
456
477
|
}
|
|
457
478
|
|
|
458
479
|
const { apiUrl, token } = await apiCredentials();
|
|
@@ -479,14 +500,18 @@ function help(): never {
|
|
|
479
500
|
process.exit(0);
|
|
480
501
|
}
|
|
481
502
|
|
|
482
|
-
/** An unrecognised command: a short pointer on stderr, exit
|
|
503
|
+
/** An unrecognised command: a short pointer on stderr, exit 2 (misuse, not failure). */
|
|
483
504
|
function usage(): never {
|
|
484
|
-
console.error(
|
|
485
|
-
process.exit(
|
|
505
|
+
console.error(`${rose("✗")} unknown command "${command}"\n ${dim("run `sproutboat` for the list of commands")}`);
|
|
506
|
+
process.exit(2);
|
|
486
507
|
}
|
|
487
508
|
|
|
488
509
|
const [command, ...args] = process.argv.slice(2);
|
|
489
510
|
if (command === undefined || command === "help" || command === "-h" || command === "--help") help();
|
|
511
|
+
if (command === "--version" || command === "-v") { console.log(`sproutboat ${CLI_VERSION}`); process.exit(0); }
|
|
512
|
+
|
|
513
|
+
await notifyIfOutdated(CLI_VERSION);
|
|
514
|
+
|
|
490
515
|
switch (command) {
|
|
491
516
|
case "init": await init(args[0]); break;
|
|
492
517
|
case "check": await check(args[0]); break;
|
package/src/report.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { ArtifactManifest } from "./manifest";
|
|
|
5
5
|
import { bold, dim, leaf, sprout } from "./style";
|
|
6
6
|
|
|
7
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;
|
|
8
|
+
export const CLI_VERSION = (JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")) as { version: string }).version;
|
|
9
9
|
|
|
10
10
|
function bytes(n: number): string {
|
|
11
11
|
if (n < 1024) return `${n} B`;
|
package/src/surface.ts
CHANGED
|
@@ -66,6 +66,7 @@ export const ENV_VARS: readonly EnvVar[] = [
|
|
|
66
66
|
{ name: "SPROUTBOAT_BINDINGS_JSON", purpose: "The artifact's bindings.json, read by the wrapper to emit the `__sbInstallBindings` line." },
|
|
67
67
|
{ name: "SPROUTBOAT_CONFIG_DIR", purpose: "Directory for credentials.json (default ~/.config/sproutboat)." },
|
|
68
68
|
{ name: "NO_COLOR", purpose: "When set, disables coloured terminal output (https://no-color.org). Output is also plain whenever stdout is not a TTY." },
|
|
69
|
+
{ name: "SPROUTBOAT_NO_UPDATE_CHECK", purpose: "When set, skips the once-a-day npm check for a newer `sproutboat` release (also skipped when CI is set)." },
|
|
69
70
|
{ name: "XDG_CONFIG_HOME", purpose: "Base for the default credentials dir when SPROUTBOAT_CONFIG_DIR is unset." },
|
|
70
71
|
{ name: "PORFFOR_VERSION", purpose: "Override the Porffor identity string recorded in the manifest." },
|
|
71
72
|
{ 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)." },
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "update available 0.4.4" notice, wrangler-style. Checks npm at most once a
|
|
3
|
+
* day, caches the answer next to the credentials, and never fails or blocks the
|
|
4
|
+
* command for more than a second. Silent when up to date, offline, in CI, or
|
|
5
|
+
* when SPROUTBOAT_NO_UPDATE_CHECK is set.
|
|
6
|
+
*/
|
|
7
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
8
|
+
import { resolve } from "node:path";
|
|
9
|
+
import { configDirectory } from "./credentials";
|
|
10
|
+
import { dim } from "./style";
|
|
11
|
+
|
|
12
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
13
|
+
const REGISTRY = "https://registry.npmjs.org/sproutboat/latest";
|
|
14
|
+
|
|
15
|
+
type Cache = { checkedAt: number; latest: string };
|
|
16
|
+
|
|
17
|
+
function cachePath(): string {
|
|
18
|
+
return resolve(configDirectory(), "update-check.json");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Numeric x.y.z compare; a trailing `-tag` (prerelease) sorts before its release. */
|
|
22
|
+
function isNewer(latest: string, current: string): boolean {
|
|
23
|
+
const parts = (v: string) => v.split("-")[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
24
|
+
const [a, b] = [parts(latest), parts(current)];
|
|
25
|
+
for (let i = 0; i < 3; i++) {
|
|
26
|
+
if ((a[i] ?? 0) !== (b[i] ?? 0)) return (a[i] ?? 0) > (b[i] ?? 0);
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function latestVersion(): Promise<string | undefined> {
|
|
32
|
+
try {
|
|
33
|
+
const cached = JSON.parse(await readFile(cachePath(), "utf8")) as Cache;
|
|
34
|
+
if (Date.now() - cached.checkedAt < CACHE_TTL_MS && typeof cached.latest === "string") return cached.latest;
|
|
35
|
+
} catch { /* no cache yet, or unreadable — fetch below */ }
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const response = await fetch(REGISTRY, { signal: AbortSignal.timeout(1000), headers: { accept: "application/json" } });
|
|
39
|
+
if (!response.ok) return undefined;
|
|
40
|
+
const latest = ((await response.json()) as { version?: string }).version;
|
|
41
|
+
if (typeof latest !== "string") return undefined;
|
|
42
|
+
await writeFile(cachePath(), JSON.stringify({ checkedAt: Date.now(), latest } satisfies Cache)).catch(() => {});
|
|
43
|
+
return latest;
|
|
44
|
+
} catch { /* offline / slow / DNS — skip silently */ }
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Print one dim line to stderr if a newer `sproutboat` is on npm. Never throws. */
|
|
49
|
+
export async function notifyIfOutdated(current: string): Promise<void> {
|
|
50
|
+
if (process.env.SPROUTBOAT_NO_UPDATE_CHECK || process.env.CI) return;
|
|
51
|
+
const latest = await latestVersion();
|
|
52
|
+
if (latest && isNewer(latest, current)) {
|
|
53
|
+
console.error(dim(` update available: sproutboat ${current} → ${latest} · bump the dependency or run \`bunx sproutboat@latest\``));
|
|
54
|
+
}
|
|
55
|
+
}
|