sproutboat 0.10.0 → 0.10.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/CHANGELOG.md +32 -1
- package/SURFACE.md +2 -1
- package/bin/sproutboat.cjs +34 -16
- package/package.json +1 -7
- package/src/compile.ts +14 -3
- package/src/report.ts +6 -3
- package/src/surface.ts +5 -0
- package/src/toolchain.ts +69 -15
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,35 @@ maintained going forward by the `release` skill.
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [0.10.2] - 2026-09-11
|
|
14
|
+
### Fixed
|
|
15
|
+
- The one-time Zig download no longer depends on ziglang.org being reachable.
|
|
16
|
+
It now pulls from a random Zig community mirror, with ziglang.org kept only
|
|
17
|
+
as a last resort, and tries the mirrors in turn on failure. ziglang.org
|
|
18
|
+
rate-limits the multi-megabyte tarballs and had been timing out mid-build,
|
|
19
|
+
with no fallback (baronunread/sproutboat#166). The archive sha256 is still
|
|
20
|
+
enforced whatever the source. `SPROUTBOAT_ZIG_URL` overrides the source;
|
|
21
|
+
`SPROUTBOAT_ZIG` still points at a prebuilt binary.
|
|
22
|
+
- A leftover `~/.cache/sproutboat/zig-<version>/` directory from an earlier
|
|
23
|
+
cache layout is removed on the next build instead of sitting unused.
|
|
24
|
+
|
|
25
|
+
## [0.10.1] - 2026-09-11
|
|
26
|
+
### Fixed
|
|
27
|
+
- `npm install sproutboat` / `bunx sproutboat` work again. 0.10.0's launcher
|
|
28
|
+
hard-required per-platform `@sproutboat/cli-*` packages that are not published
|
|
29
|
+
yet, so it always failed with "optional package … is missing". The launcher
|
|
30
|
+
now falls back to running `src/main.ts` with Bun when no platform binary is
|
|
31
|
+
installed (the v0.9.0 model), and `optionalDependencies` are dropped until the
|
|
32
|
+
binaries actually ship. 0.10.0 is deprecated on npm.
|
|
33
|
+
- `sproutboat --version` and the deploy banner report the real version instead
|
|
34
|
+
of a hard-coded `0.9.0` when run from the npm package rather than a
|
|
35
|
+
`bun build --compile` binary.
|
|
36
|
+
- `sproutboat build` / `deploy` locate `esbuild` via the installed dependency,
|
|
37
|
+
not just `process.execPath`'s directory, so bundling works on the Bun
|
|
38
|
+
fallback path.
|
|
39
|
+
- The launcher no longer hangs when it receives `SIGINT` / `SIGTERM` while
|
|
40
|
+
forwarding a signal to a running platform binary.
|
|
41
|
+
|
|
13
42
|
## [0.10.0] - 2026-09-11
|
|
14
43
|
### Added
|
|
15
44
|
- Per-platform native CLI: `npm install sproutboat` pulls a prebuilt binary for
|
|
@@ -341,7 +370,9 @@ its own package.
|
|
|
341
370
|
- Renamed the package to `sproutboat` (was `@sproutboat/cli`); dropped the
|
|
342
371
|
`sprout` bin alias in favour of a user-defined shell alias.
|
|
343
372
|
|
|
344
|
-
[Unreleased]: https://github.com/baronunread/sproutboat-cli/compare/v0.10.
|
|
373
|
+
[Unreleased]: https://github.com/baronunread/sproutboat-cli/compare/v0.10.2...HEAD
|
|
374
|
+
[0.10.2]: https://github.com/baronunread/sproutboat-cli/compare/v0.10.1...v0.10.2
|
|
375
|
+
[0.10.1]: https://github.com/baronunread/sproutboat-cli/compare/v0.10.0...v0.10.1
|
|
345
376
|
[0.10.0]: https://github.com/baronunread/sproutboat-cli/compare/v0.9.0...v0.10.0
|
|
346
377
|
[0.9.0]: https://github.com/baronunread/sproutboat-cli/compare/v0.8.0...v0.9.0
|
|
347
378
|
[0.8.0]: https://github.com/baronunread/sproutboat-cli/compare/v0.7.0...v0.8.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.
|
|
6
|
+
**Package:** `sproutboat` 0.10.2 · runs on Bun (use `bunx`, not `npx`)
|
|
7
7
|
|
|
8
8
|
## Commands
|
|
9
9
|
|
|
@@ -40,6 +40,7 @@ usage: sproutboat <init [name] | check [project-dir] | types [project-dir] | dev
|
|
|
40
40
|
| `SPROUTBOAT_API_URL` | Control-plane URL. Overrides the saved active endpoint. |
|
|
41
41
|
| `SPROUTBOAT_TOKEN` | API token. Overrides the saved credential for the endpoint. |
|
|
42
42
|
| `SPROUTBOAT_ZIG` | Path to a Zig binary to use instead of downloading the pinned one. |
|
|
43
|
+
| `SPROUTBOAT_ZIG_URL` | URL to download the pinned Zig tarball from, instead of a Zig community mirror. The sha256 is still enforced. |
|
|
43
44
|
| `SPROUTBOAT_UWS_TARBALL` | Path to a uWebSockets source tarball to seed the Porffor cache with, instead of the one vendored in the package. Both targets are seeded from it. |
|
|
44
45
|
| `CC` | C compiler used to build uSockets for a host build (default `cc`). A host build needs one regardless: it is what Porffor compiles its own generated C with. |
|
|
45
46
|
| `AR` | Archiver used to assemble uSockets.a for a host build (default `ar`). |
|
package/bin/sproutboat.cjs
CHANGED
|
@@ -1,32 +1,50 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// npm
|
|
2
|
+
// npm selects the packaged executable for this host when it is installed; that
|
|
3
|
+
// is a native single-file binary that needs no Bun. When it is absent (it is
|
|
4
|
+
// not published yet — see issue #134), fall back to running the TypeScript
|
|
5
|
+
// entry point with Bun, which is how the CLI shipped through v0.9.0.
|
|
3
6
|
const { spawn } = require("node:child_process");
|
|
4
|
-
const {
|
|
7
|
+
const { join } = require("node:path");
|
|
5
8
|
|
|
6
9
|
const platform = process.platform === "darwin" ? "darwin" : process.platform === "linux" ? "linux" : null;
|
|
7
10
|
const arch = process.arch === "arm64" ? "arm64" : process.arch === "x64" ? "x64" : null;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const packageName = `@sproutboat/cli-${platform}-${arch}`;
|
|
13
|
-
let executable;
|
|
11
|
+
|
|
12
|
+
let command;
|
|
13
|
+
let args;
|
|
14
|
+
let viaBun = false;
|
|
14
15
|
try {
|
|
15
|
-
|
|
16
|
+
if (!platform || !arch) throw new Error(`unsupported host ${process.platform}/${process.arch}`);
|
|
17
|
+
command = require.resolve(`@sproutboat/cli-${platform}-${arch}/bin/sproutboat`);
|
|
18
|
+
args = process.argv.slice(2);
|
|
16
19
|
} catch {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
process.
|
|
20
|
+
viaBun = true;
|
|
21
|
+
// SPROUTBOAT_BUN is an escape hatch for an unusual install; otherwise PATH.
|
|
22
|
+
command = process.env.SPROUTBOAT_BUN || "bun";
|
|
23
|
+
args = [join(__dirname, "..", "src", "main.ts"), ...process.argv.slice(2)];
|
|
20
24
|
}
|
|
21
|
-
|
|
25
|
+
|
|
26
|
+
const child = spawn(command, args, { stdio: "inherit", windowsHide: true });
|
|
27
|
+
|
|
22
28
|
child.once("error", (error) => {
|
|
23
|
-
|
|
29
|
+
if (viaBun && error.code === "ENOENT") {
|
|
30
|
+
console.error("sproutboat: this build needs Bun on PATH. Install it: https://bun.sh");
|
|
31
|
+
} else {
|
|
32
|
+
console.error(`sproutboat: could not start: ${error.message}`);
|
|
33
|
+
}
|
|
24
34
|
process.exit(1);
|
|
25
35
|
});
|
|
36
|
+
|
|
26
37
|
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
27
38
|
process.on(signal, () => child.kill(signal));
|
|
28
39
|
}
|
|
40
|
+
|
|
29
41
|
child.once("exit", (code, signal) => {
|
|
30
|
-
if (signal)
|
|
31
|
-
|
|
42
|
+
if (signal) {
|
|
43
|
+
// Re-raise so the parent's exit reflects the signal — but drop our own
|
|
44
|
+
// handler first, or the re-raise re-enters it and the process hangs.
|
|
45
|
+
process.removeAllListeners(signal);
|
|
46
|
+
process.kill(process.pid, signal);
|
|
47
|
+
} else {
|
|
48
|
+
process.exit(code ?? 1);
|
|
49
|
+
}
|
|
32
50
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sproutboat",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2",
|
|
4
4
|
"description": "Wrangler-shaped CLI for Sproutboat. Deploys workers to any control plane via --api-url / SPROUTBOAT_API_URL.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -76,12 +76,6 @@
|
|
|
76
76
|
"oxlint": "1.81.0",
|
|
77
77
|
"typescript": "7.0.2"
|
|
78
78
|
},
|
|
79
|
-
"optionalDependencies": {
|
|
80
|
-
"@sproutboat/cli-darwin-arm64": "0.10.0",
|
|
81
|
-
"@sproutboat/cli-darwin-x64": "0.10.0",
|
|
82
|
-
"@sproutboat/cli-linux-arm64": "0.10.0",
|
|
83
|
-
"@sproutboat/cli-linux-x64": "0.10.0"
|
|
84
|
-
},
|
|
85
79
|
"engines": {
|
|
86
80
|
"node": ">=18"
|
|
87
81
|
}
|
package/src/compile.ts
CHANGED
|
@@ -201,10 +201,21 @@ export async function compileSprout(input: CompileInput): Promise<void> {
|
|
|
201
201
|
const launcher = resolve(porffor, "runtime/index.js");
|
|
202
202
|
// A host build never shells `zig`, so it has no zigBin to contribute.
|
|
203
203
|
const zigDir = input.zigBin ? `${dirname(input.zigBin)}:` : "";
|
|
204
|
+
// The per-platform binary ships esbuild next to it; a compiled build finds it
|
|
205
|
+
// there. Running from the npm package under Bun, `process.execPath` is Bun
|
|
206
|
+
// itself and `npm i -g` puts no dependency `.bin` on PATH, so resolve the
|
|
207
|
+
// esbuild dependency directly. `Bun.which` is the last resort.
|
|
204
208
|
const packagedEsbuild = resolve(dirname(process.execPath), "esbuild");
|
|
205
|
-
|
|
206
|
-
if (!esbuild)
|
|
207
|
-
|
|
209
|
+
let esbuild: string | null = existsSync(packagedEsbuild) ? packagedEsbuild : null;
|
|
210
|
+
if (!esbuild) {
|
|
211
|
+
try {
|
|
212
|
+
esbuild = Bun.resolveSync("esbuild/bin/esbuild", import.meta.dir);
|
|
213
|
+
} catch {
|
|
214
|
+
esbuild = null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
esbuild ??= Bun.which("esbuild");
|
|
218
|
+
if (!esbuild) throw new Error("esbuild could not be located; install it or reinstall sproutboat");
|
|
208
219
|
const path = `${zigDir}${dirname(esbuild)}:${process.env.PATH ?? ""}`;
|
|
209
220
|
const command = PACKAGED
|
|
210
221
|
? [process.execPath, "__porffor", launcher]
|
package/src/report.ts
CHANGED
|
@@ -2,10 +2,13 @@ import { gzipSync } from "bun";
|
|
|
2
2
|
import { resourceRefs, type SproutboatConfig } from "./config";
|
|
3
3
|
import type { ArtifactManifest } from "./manifest";
|
|
4
4
|
import { bold, dim, leaf, sprout } from "./style";
|
|
5
|
+
import pkg from "../package.json" with { type: "json" };
|
|
5
6
|
|
|
6
|
-
// `bun build --compile` has no package-relative filesystem at runtime
|
|
7
|
-
// release build injects this
|
|
8
|
-
|
|
7
|
+
// `bun build --compile` has no package-relative filesystem at runtime, so the
|
|
8
|
+
// release build injects this via `--define`. Running from source (or the npm
|
|
9
|
+
// package under Bun) falls back to the bundled package.json version — `bun build
|
|
10
|
+
// --compile` inlines the JSON import, so this branch is correct there too.
|
|
11
|
+
export const CLI_VERSION = process.env.SPROUTBOAT_CLI_VERSION ?? pkg.version;
|
|
9
12
|
|
|
10
13
|
function bytes(n: number): string {
|
|
11
14
|
if (n < 1024) return `${n} B`;
|
package/src/surface.ts
CHANGED
|
@@ -194,6 +194,11 @@ export const ENV_VARS: readonly EnvVar[] = [
|
|
|
194
194
|
{ name: "SPROUTBOAT_API_URL", purpose: "Control-plane URL. Overrides the saved active endpoint." },
|
|
195
195
|
{ name: "SPROUTBOAT_TOKEN", purpose: "API token. Overrides the saved credential for the endpoint." },
|
|
196
196
|
{ name: "SPROUTBOAT_ZIG", purpose: "Path to a Zig binary to use instead of downloading the pinned one." },
|
|
197
|
+
{
|
|
198
|
+
name: "SPROUTBOAT_ZIG_URL",
|
|
199
|
+
purpose:
|
|
200
|
+
"URL to download the pinned Zig tarball from, instead of a Zig community mirror. The sha256 is still enforced.",
|
|
201
|
+
},
|
|
197
202
|
{
|
|
198
203
|
name: "SPROUTBOAT_UWS_TARBALL",
|
|
199
204
|
purpose:
|
package/src/toolchain.ts
CHANGED
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
* The build toolchain: a pinned Zig (the linux-x86_64 cross-compiler Porffor
|
|
3
3
|
* shells out to for `--musl`) plus version stamps for the artifact manifest.
|
|
4
4
|
*
|
|
5
|
-
* Zig is fetched once to ~/.cache/sproutboat/zig-<version>/ and
|
|
6
|
-
* Docker, no root.
|
|
5
|
+
* Zig is fetched once to ~/.cache/sproutboat/zig-<version>-<platform>/ and
|
|
6
|
+
* reused. No Docker, no root. It comes from a random Zig community mirror with
|
|
7
|
+
* ziglang.org as the last resort, because ziglang.org rate-limits the large
|
|
8
|
+
* tarballs and has been timing out. Override the binary with
|
|
9
|
+
* SPROUTBOAT_ZIG=/path/to/zig, or the download source with SPROUTBOAT_ZIG_URL.
|
|
7
10
|
*/
|
|
8
11
|
import { createHash } from "node:crypto";
|
|
9
12
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -101,24 +104,69 @@ async function zigComplete(dir: string, key: ZigPlatform, expectedArchive: strin
|
|
|
101
104
|
}
|
|
102
105
|
}
|
|
103
106
|
|
|
107
|
+
// Zig asks tooling not to hammer ziglang.org for the multi-megabyte tarballs:
|
|
108
|
+
// pick a random community mirror and keep the official host only as a last
|
|
109
|
+
// resort. https://ziglang.org/download/community-mirrors.txt
|
|
110
|
+
const ZIG_MIRRORS_URL = "https://ziglang.org/download/community-mirrors.txt";
|
|
111
|
+
|
|
112
|
+
function shuffle<T>(items: T[]): T[] {
|
|
113
|
+
for (let i = items.length - 1; i > 0; i--) {
|
|
114
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
115
|
+
[items[i], items[j]] = [items[j], items[i]];
|
|
116
|
+
}
|
|
117
|
+
return items;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Ordered list of URLs to try for the pinned Zig tarball: the community mirrors
|
|
122
|
+
* (shuffled) followed by ziglang.org. Falls back to ziglang.org alone if the
|
|
123
|
+
* mirror list itself is unreachable.
|
|
124
|
+
*/
|
|
125
|
+
async function zigDownloadUrls(
|
|
126
|
+
key: ZigPlatform,
|
|
127
|
+
fetcher: NonNullable<EnsureZigOptions["fetcher"]>,
|
|
128
|
+
timeoutMs: number,
|
|
129
|
+
): Promise<string[]> {
|
|
130
|
+
const file = `zig-${key}-${ZIG_VERSION}.tar.xz`;
|
|
131
|
+
const official = `https://ziglang.org/download/${ZIG_VERSION}/${file}`;
|
|
132
|
+
try {
|
|
133
|
+
const response = await fetcher(ZIG_MIRRORS_URL, { signal: AbortSignal.timeout(Math.min(timeoutMs, 10_000)) });
|
|
134
|
+
if (response.ok) {
|
|
135
|
+
const mirrors = (await response.text())
|
|
136
|
+
.split(/\s+/)
|
|
137
|
+
.filter((line) => line.startsWith("https://"))
|
|
138
|
+
.map((base) => `${base.replace(/\/+$/, "")}/${file}`);
|
|
139
|
+
if (mirrors.length > 0) return [...shuffle(mirrors), official];
|
|
140
|
+
}
|
|
141
|
+
} catch {
|
|
142
|
+
// Mirror list unreachable: the official host is the only option left.
|
|
143
|
+
}
|
|
144
|
+
return [official];
|
|
145
|
+
}
|
|
146
|
+
|
|
104
147
|
async function downloadZig(
|
|
105
|
-
|
|
148
|
+
urls: string[],
|
|
106
149
|
path: string,
|
|
107
150
|
fetcher: NonNullable<EnsureZigOptions["fetcher"]>,
|
|
108
151
|
timeoutMs: number,
|
|
109
152
|
): Promise<void> {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
153
|
+
// One shot per mirror; two at the single fallback URL so a lone flaky host
|
|
154
|
+
// still gets a retry.
|
|
155
|
+
const attemptsPer = urls.length > 1 ? 1 : 2;
|
|
156
|
+
const failures: string[] = [];
|
|
157
|
+
for (const url of urls) {
|
|
158
|
+
for (let attempt = 0; attempt < attemptsPer; attempt++) {
|
|
159
|
+
try {
|
|
160
|
+
const response = await fetcher(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
161
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
162
|
+
await writeFile(path, new Uint8Array(await response.arrayBuffer()));
|
|
163
|
+
return;
|
|
164
|
+
} catch (error) {
|
|
165
|
+
failures.push(`${url}: ${String(error)}`);
|
|
166
|
+
}
|
|
119
167
|
}
|
|
120
168
|
}
|
|
121
|
-
throw new ZigToolchainError("download", `could not download pinned Zig
|
|
169
|
+
throw new ZigToolchainError("download", `could not download pinned Zig\n ${failures.join("\n ")}`);
|
|
122
170
|
}
|
|
123
171
|
|
|
124
172
|
async function waitForZigPublisher(
|
|
@@ -151,6 +199,10 @@ export async function ensureZig(options: EnsureZigOptions = {}): Promise<string>
|
|
|
151
199
|
const root = resolve(
|
|
152
200
|
options.cacheRoot ?? process.env.SPROUTBOAT_TOOLCHAIN_CACHE ?? resolve(homedir(), ".cache/sproutboat"),
|
|
153
201
|
);
|
|
202
|
+
// Older builds cached Zig at `zig-<version>/` with no platform suffix; nothing
|
|
203
|
+
// reads that layout now, so drop it rather than leave it as dead weight.
|
|
204
|
+
const legacy = resolve(root, `zig-${ZIG_VERSION}`);
|
|
205
|
+
if (existsSync(legacy)) await rm(legacy, { recursive: true, force: true }).catch(() => {});
|
|
154
206
|
const dir = resolve(root, `zig-${ZIG_VERSION}-${key}`);
|
|
155
207
|
const expected = options.expectedSha256 ?? ZIG_SHA256[key];
|
|
156
208
|
if (await zigComplete(dir, key, expected)) return resolve(dir, "zig");
|
|
@@ -164,14 +216,16 @@ export async function ensureZig(options: EnsureZigOptions = {}): Promise<string>
|
|
|
164
216
|
return ensureZig(options);
|
|
165
217
|
}
|
|
166
218
|
const stage = resolve(root, `.zig-${ZIG_VERSION}-${key}-${process.pid}-${crypto.randomUUID()}`);
|
|
167
|
-
const
|
|
219
|
+
const fetcher = options.fetcher ?? fetch;
|
|
220
|
+
const sourceUrl = options.url ?? process.env.SPROUTBOAT_ZIG_URL;
|
|
221
|
+
const urls = sourceUrl ? [sourceUrl] : await zigDownloadUrls(key, fetcher, options.timeoutMs ?? 30_000);
|
|
168
222
|
console.log(`Fetching Zig ${ZIG_VERSION} (${key}, one-time)...`);
|
|
169
223
|
try {
|
|
170
224
|
if (await zigComplete(dir, key, expected)) return resolve(dir, "zig");
|
|
171
225
|
await rm(dir, { recursive: true, force: true });
|
|
172
226
|
await mkdir(stage);
|
|
173
227
|
const archive = resolve(stage, "zig.tar.xz");
|
|
174
|
-
await downloadZig(
|
|
228
|
+
await downloadZig(urls, archive, fetcher, options.timeoutMs ?? 60_000);
|
|
175
229
|
const actual = await sha256File(archive);
|
|
176
230
|
if (actual !== expected)
|
|
177
231
|
throw new ZigToolchainError(
|