sproutboat 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sproutboat contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,86 @@
1
+ # sproutboat
2
+
3
+ The CLI for [Sproutboat](https://github.com/baronunread/sproutboat) — a
4
+ Wrangler-shaped tool for deploying workers to any Sproutboat control plane.
5
+ MIT licensed.
6
+
7
+ ```sh
8
+ bunx sproutboat login --api-url https://control.example.com
9
+ bunx sproutboat init hello
10
+ cd hello
11
+ bunx sproutboat deploy
12
+ ```
13
+
14
+ Or install it once and drop the `bunx`:
15
+
16
+ ```sh
17
+ bun add -g sproutboat # then: sproutboat deploy, sproutboat tail, ...
18
+ ```
19
+
20
+ Want it shorter? Alias it yourself: `alias sprout='sproutboat'`.
21
+
22
+ For CI, skip `login` and set `SPROUTBOAT_API_URL` + `SPROUTBOAT_TOKEN`.
23
+
24
+ ## Commands
25
+
26
+ | Command | What it does |
27
+ | --- | --- |
28
+ | `init [name]` | Scaffold `sproutboat.jsonc` + `src/index.js` |
29
+ | `check` | Validate the config and entry point |
30
+ | `build` | Cross-compile the worker artifact |
31
+ | `deploy [--dry-run] [--artifact <dir>]` | Build and upload (`--dry-run` prints the report only) |
32
+ | `login [--api-url <url>] [--token <token>]` | Browser device flow, or store a token |
33
+ | `tail [name]` | Recent logs |
34
+ | `versions list [name]` | Deployed versions |
35
+ | `rollback <id>` | Activate a previous version |
36
+ | `delete --yes` | Delete the project |
37
+
38
+ Credentials are keyed by API URL in `~/.config/sproutboat/credentials.json`, so
39
+ you can hold logins for several instances at once.
40
+
41
+ ## Config
42
+
43
+ `sproutboat.jsonc` — the entry point plus Cloudflare-shaped `env.*` bindings:
44
+
45
+ ```jsonc
46
+ {
47
+ "name": "app",
48
+ "main": "src/index.js",
49
+ "compatibility_date": "2026-08-26",
50
+
51
+ "vars": { "SITE": "hi" },
52
+ "secrets": ["API_KEY"],
53
+ "kv_namespaces": ["CACHE"],
54
+ "d1_databases": ["DB"],
55
+ "r2_buckets": ["UPLOADS"],
56
+ "queues": ["JOBS"],
57
+ "analytics_engine_datasets": ["METRICS"],
58
+ "durable_objects": { "COUNTER": "Counter" },
59
+ "outbound": ["api.example.com"],
60
+ "triggers": { "crons": ["*/5 * * * *"] },
61
+ "assets": { "directory": "public", "binding": "ASSETS" }
62
+ }
63
+ ```
64
+
65
+ The handler is one `export default { fetch }`, optionally with `scheduled` /
66
+ `queue` handlers and exported Durable Object classes. See
67
+ [`examples/kitchen-sink/`](examples/kitchen-sink) for one app that uses every
68
+ binding, with an Astro UI and a runnable end-to-end harness.
69
+
70
+ ## Requirements
71
+
72
+ - [Bun](https://bun.sh) 1.4+
73
+ - `git` and `make` on `PATH` (first build only — compiles uWebSockets once)
74
+ - No Docker. Builds cross-compile to a static `linux-x86_64` binary with Porffor
75
+ and Zig. Windows: build from WSL.
76
+
77
+ ## Limits (v1)
78
+
79
+ Binding values are text/JSON and travel one at a time over a loopback frame; an
80
+ upload is capped at 1 MiB by the worker's HTTP server (large-object R2 is
81
+ [#56](https://github.com/baronunread/sproutboat/issues/56)). No WebSockets yet.
82
+
83
+ ---
84
+
85
+ Contributors: [`MIGRATION.md`](MIGRATION.md) maps this repo back to the monorepo.
86
+ [`SURFACE.md`](SURFACE.md) is a generated inventory of every command and env var.
package/SURFACE.md ADDED
@@ -0,0 +1,49 @@
1
+ # sproutboat — surface
2
+
3
+ > Generated by `src/surface.test.ts` from `src/surface.ts` + the pinned
4
+ > toolchain constants. Do not edit by hand — run `UPDATE_SURFACE=1 bun test`.
5
+
6
+ **Package:** `sproutboat` 0.2.0 · runs on Bun (use `bunx`, not `npx`)
7
+
8
+ ## Commands
9
+
10
+ | Command | Arguments | Summary |
11
+ | --- | --- | --- |
12
+ | `init` | `[name]` | Scaffold sproutboat.jsonc + src/index.js in ./<name>. |
13
+ | `check` | `[project-dir]` | Validate the config and entry point without building. |
14
+ | `build` | `[project-dir]` | Cross-compile the native-fetch worker (Porffor + Zig). |
15
+ | `deploy` | `[project-dir] [--dry-run] [--artifact <dir>]` | Build (unless --artifact), print the report, upload. --dry-run stops before upload. |
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. |
18
+ | `versions` | `list [project-dir]` | List the project's deployed versions. |
19
+ | `rollback` | `<version-id> [project-dir]` | Re-activate a previous version. |
20
+ | `delete` | `--yes [project-dir]` | Delete the project and every version. |
21
+
22
+ ```
23
+ 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] | delete --yes [project-dir]>
24
+ ```
25
+
26
+ ## Environment variables
27
+
28
+ | Variable | Purpose |
29
+ | --- | --- |
30
+ | `SPROUTBOAT_API_URL` | Control-plane URL. Overrides the saved active endpoint. |
31
+ | `SPROUTBOAT_TOKEN` | API token. Overrides the saved credential for the endpoint. |
32
+ | `SPROUTBOAT_ZIG` | Path to a Zig binary to use instead of downloading the pinned one. |
33
+ | `SPROUTBOAT_COMPILE_TIMEOUT_MS` | Porffor compile timeout in ms (default 600000). |
34
+ | `SPROUTBOAT_CONFIG_DIR` | Directory for credentials.json (default ~/.config/sproutboat). |
35
+ | `XDG_CONFIG_HOME` | Base for the default credentials dir when SPROUTBOAT_CONFIG_DIR is unset. |
36
+ | `PORFFOR_VERSION` | Override the Porffor identity string recorded in the manifest. |
37
+ | `SB_BROKER_PORT` | Loopback port of the binding broker, read by the compiled worker at runtime (set by the control plane, or by `src/broker.ts` for local runs). |
38
+ | `SB_BROKER_TOKEN` | Per-deployment auth token the worker sends on every broker frame, and the broker sends back on scheduled/queue triggers (paired with SB_BROKER_PORT). |
39
+ | `SB_WORKER_URL` | http://127.0.0.1:<PORT> of the worker; when set, `src/broker.ts` runs the cron scheduler and queue consumer and delivers triggers to it. |
40
+
41
+ ## Build toolchain (pinned)
42
+
43
+ | | |
44
+ | --- | --- |
45
+ | Zig | `0.16.0` (`zig cc -target x86_64-linux-musl`, static) |
46
+ | Provenance stamp | `zig-musl/0.16.0+porffor/a415d19+uws/360c276d` |
47
+ | Artifact schema | `2` |
48
+ | Runtime | `native-fetch` |
49
+ | Capability profile | `http-sync-v0` |
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "sproutboat",
3
+ "version": "0.2.0",
4
+ "description": "Wrangler-shaped CLI for Sproutboat. Deploys workers to any control plane via --api-url / SPROUTBOAT_API_URL.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "homepage": "https://github.com/baronunread/sproutboat-cli",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/baronunread/sproutboat-cli.git"
11
+ },
12
+ "bugs": "https://github.com/baronunread/sproutboat-cli/issues",
13
+ "keywords": [
14
+ "sproutboat",
15
+ "cli",
16
+ "deploy",
17
+ "porffor",
18
+ "serverless",
19
+ "edge",
20
+ "wrangler"
21
+ ],
22
+ "bin": {
23
+ "sproutboat": "src/main.ts"
24
+ },
25
+ "files": [
26
+ "src",
27
+ "!src/*.test.ts",
28
+ "SURFACE.md"
29
+ ],
30
+ "engines": {
31
+ "bun": ">=1.4.0"
32
+ },
33
+ "scripts": {
34
+ "typecheck": "tsc --noEmit",
35
+ "test": "bun test",
36
+ "example:kitchen-sink": "bun examples/kitchen-sink/harness.ts",
37
+ "surface": "UPDATE_SURFACE=1 bun test src/surface.test.ts",
38
+ "version": "bun run surface && git add SURFACE.md",
39
+ "prepublishOnly": "bun run typecheck && bun test",
40
+ "style": "bunx style-doctor@latest --quiet"
41
+ },
42
+ "dependencies": {
43
+ "esbuild": "^0.28.2",
44
+ "porffor": "github:CanadaHonk/porffor#alpha-4"
45
+ },
46
+ "devDependencies": {
47
+ "@types/bun": "1.4.0",
48
+ "typescript": "5.9.2"
49
+ }
50
+ }
package/src/assets.ts ADDED
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Static-asset manifest. `sproutboat build` copies the project's `assets`
3
+ * directory next to the artifact and writes `assets.json` (this manifest); the
4
+ * edge serves matching files directly (assets-first, like Cloudflare), and the
5
+ * broker's `assets.get` op backs `env.<ASSETS>.fetch(request)` for the paths
6
+ * the worker chooses to serve itself.
7
+ */
8
+ import { createHash } from "node:crypto";
9
+ import { readdirSync, readFileSync } from "node:fs";
10
+ import { join, posix } from "node:path";
11
+
12
+ export type AssetEntry = { hash: string; size: number; type: string };
13
+ /** `/absolute/posix/path` -> entry. Serialised into `assets.json`. */
14
+ export type AssetFiles = { [path: string]: AssetEntry };
15
+ export type AssetManifest = {
16
+ /** Fallback for requests that match no file, applied by the broker. */
17
+ notFound: "none" | "single-page-application" | "404-page";
18
+ /** `true` = every path to the sprout first; string[] = selective (leading `!` negates). */
19
+ runSproutFirst: boolean | string[];
20
+ files: AssetFiles;
21
+ };
22
+
23
+ const TYPES = new Map<string, string>([
24
+ ["html", "text/html; charset=utf-8"], ["css", "text/css; charset=utf-8"],
25
+ ["js", "text/javascript; charset=utf-8"], ["mjs", "text/javascript; charset=utf-8"],
26
+ ["json", "application/json; charset=utf-8"], ["map", "application/json; charset=utf-8"],
27
+ ["txt", "text/plain; charset=utf-8"], ["xml", "application/xml; charset=utf-8"],
28
+ ["svg", "image/svg+xml"], ["png", "image/png"], ["jpg", "image/jpeg"], ["jpeg", "image/jpeg"],
29
+ ["gif", "image/gif"], ["webp", "image/webp"], ["avif", "image/avif"], ["ico", "image/x-icon"],
30
+ ["woff2", "font/woff2"], ["woff", "font/woff"], ["ttf", "font/ttf"], ["wasm", "application/wasm"],
31
+ ["webmanifest", "application/manifest+json"],
32
+ ]);
33
+
34
+ export function contentType(name: string): string {
35
+ const dot = name.lastIndexOf(".");
36
+ return (dot >= 0 ? TYPES.get(name.slice(dot + 1).toLowerCase()) : undefined) ?? "application/octet-stream";
37
+ }
38
+
39
+ /** Walk `dir` recursively, returning `{ "/path": {hash,size,type} }`. */
40
+ export function walkAssets(dir: string) {
41
+ const out: AssetFiles = {};
42
+ const recurse = (abs: string, rel: string): void => {
43
+ for (const ent of readdirSync(abs, { withFileTypes: true })) {
44
+ if (ent.name.startsWith(".")) continue;
45
+ const childAbs = join(abs, ent.name);
46
+ const childRel = posix.join(rel, ent.name);
47
+ if (ent.isDirectory()) { recurse(childAbs, childRel); continue; }
48
+ if (!ent.isFile()) continue;
49
+ const body = readFileSync(childAbs);
50
+ out[`/${childRel}`] = { hash: createHash("sha256").update(body).digest("hex"), size: body.byteLength, type: contentType(ent.name) };
51
+ }
52
+ };
53
+ recurse(dir, "");
54
+ return out;
55
+ }
56
+
57
+ /** Does `pathname` hit the sprout before assets, given a `runSproutFirst` spec? */
58
+ export function isSproutFirst(spec: boolean | string[], pathname: string): boolean {
59
+ if (!Array.isArray(spec)) return spec;
60
+ let matched = false;
61
+ for (const raw of spec) {
62
+ const negate = raw.startsWith("!");
63
+ const pattern = negate ? raw.slice(1) : raw;
64
+ if (globMatch(pattern, pathname)) matched = !negate;
65
+ }
66
+ return matched;
67
+ }
68
+
69
+ /** `*` matches within a segment, `**` across segments. Anchored both ends. */
70
+ function globMatch(pattern: string, path: string): boolean {
71
+ const rx = pattern
72
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
73
+ .replace(/\*\*/g, " ")
74
+ .replace(/\*/g, "[^/]*")
75
+ .replace(/ /g, ".*");
76
+ return new RegExp(`^${rx}$`).test(path);
77
+ }