sproutboat 0.54.0 → 0.55.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/CHANGELOG.md CHANGED
@@ -10,6 +10,38 @@ maintained going forward by the `release` skill.
10
10
 
11
11
  ## Unreleased
12
12
 
13
+ ## [0.55.0] - 2026-11-13
14
+
15
+ ### Added
16
+
17
+ - **`sproutboat init --template <name>` writes a starter, with the shape of `npm create cloudflare`.** Four starters, each standard Workers TypeScript that runs unchanged on workerd, with a README, tests (Node's own test runner, `npm test`), a `tsconfig.json` and a current `worker-configuration.d.ts`: `realtime-room` (a chat room on a Durable Object with hibernating WebSockets: `acceptWebSocket`, tags, `serializeAttachment`, presence, history), `api-d1` (a JSON API on D1 with a migration, validation and keyset pagination), `site-api` (static assets plus `/api/*` routes and KV in one Worker) and `jobs` (a cron trigger that enqueues, a queue consumer with retries and idempotence, KV for state). `--list-templates` prints them. In a terminal `init` asks for the template, the directory, whether to install dependencies and whether to use git (both default to yes); `-y` (`--yes`, `--accept-defaults`) takes every default, `--install` / `--no-install` and `--git` / `--no-git` decide the two steps, and without a terminal nothing runs that a flag did not ask for. `--git` makes a first commit and does nothing inside an existing repository. `init` without `--template` is the hello starter it always was. The starters are embedded in the CLI as text (`src/templates-embedded.ts`, about 90 KiB, rewritten by `bun run templates:embed`), so the compiled executable needs no network or files beside it.
18
+ - **`sproutboat dev --ip <address>`.** `dev` listens on 127.0.0.1 as before; `--ip 0.0.0.0` makes it reachable from other machines, as `wrangler dev --ip` does.
19
+ - **A project with `assets` and no `main` is a Worker with only static assets, as in Wrangler.** `check`, `dev`, `build` and `deploy` write a small entry into `.sproutboat/assets-only.js` that answers any request no file matched with `env.<binding>.fetch(request)`, so `not_found_handling` (the single-page app shell, the nearest 404 page, or a plain 404) applies as it does on Cloudflare. The assets binding is `assets.binding`, or `ASSETS` when the config names none. Nothing else in the config changes: a project with a `main` is untouched, and one with neither still gets `main must be a relative entry point under src/`.
20
+ - **`domains list` works against a Cloud.** It prints the app's routes from its record, marked `built-in` (under `sproutboat.app`) or `custom`. `domains add`, `verify` and `delete` on a Cloud say that custom domains are added by an operator for now and that deploys keep them, instead of calling Community's `/api/projects/<name>/domains`, which a Cloud does not have. `domains` takes `--account <team>`.
21
+
22
+ ### Changed
23
+
24
+ - **`examples/` holds only what a person reads.** The conformance, stress, runtime-comparison, workers-verbatim and workers-contract fixtures, the Cloudflare pattern ports (`cf-*`), `request`, `upload` and the harness library moved to `test/fixtures/`; `kitchen-sink` and `quote-service`, which are deployed on Sproutboat Cloud and check every generation swap, moved to `dogfood/`. Their deployed names and the `bun run kitchen-sink` scripts are unchanged.
25
+
26
+ - **`sproutboat types` writes `worker-configuration.d.ts`, like `wrangler types`.** The file declares `interface Env` (and `Cloudflare.Env`, which `import { env } from "cloudflare:workers"` reads) with a type per binding, so a handler is typed as `export default { ... } satisfies ExportedHandler<Env>` and `env.KV`, `env.DB` and the rest autocomplete and are checked. Vars are literal types (`--strict-vars=false` makes them `string`); a Durable Object binding is typed with its class when the entry is TypeScript, so `await env.COUNTER.getByName("a").increment()` is checked. The command takes Wrangler's positional (the file to write), `--env-interface`, `--strict-vars`, `--include-runtime`, `--include-env` and `--check`, which exits 1 when the file is missing or out of date by the hash in its header. `--include-runtime` references the runtime types the CLI ships instead of copying them in, and there is no `--include-main-module`. The ambient types gain the Workers shapes the runtime already had: `ExportedHandler`, `ScheduledController`, `DurableObjectNamespaceAsync` with RPC stubs and `getByName`, `DurableObjectStorageAsync` with `sql`, `FetcherAsync`, `R2ObjectBodyAsync.writeHttpMetadata`, and a `cloudflare:workers` module (`DurableObject`, `WorkerEntrypoint`, `RpcTarget`, `env`). A project at or after compatibility date 2026-09-28 gets no global `env`; earlier ones still do. `dev` and `deploy` refresh the default file when a plain `sproutboat types` wrote it and the config has moved on, and leave one made with other flags alone.
27
+
28
+ - **`deploy` to a Cloud sends every binding.** KV, D1, R2, queues, Analytics Engine, Durable Objects, rate limiters, the secret names, the assets binding and `triggers.crons` now go to the Cloud in the app record, in a build deploy and a module deploy alike, so `examples/kitchen-sink` runs there with all its bindings. The warning that they were "not sent to the Cloud by the CLI yet" is gone. Each app's Cloud storage is private to it, so the record's resource id is the binding name in lowercase (`DB` is `db`; a queue's is its queue name), and `d1 execute --remote` and `r2 object` address the binding name as before. Every queue is also a queue consumer (`max_batch_size`, `max_batch_timeout` in milliseconds, `max_retries`, `dead_letter_queue`, with Cloudflare's defaults). An account-level `id` in a binding is ignored with a warning. Before anything is uploaded the deploy refuses more than 16 crons, 16 queue consumers or 64 bindings (service bindings included), a cron or resource name the Cloud would refuse, and a `durable_objects` class the entry module does not export. `--dry-run` prints every binding, consumer and cron. A queue's `retry_delay` rides in the binding's config; the record has no field for it yet.
29
+ - **`sproutboat dev` relays WebSockets.** The stable port now forwards an upgrade to the sprout and passes frames both ways, so a browser can use a WebSocket or a Durable Object chat under `dev`. Before, the upgrade went through a plain `fetch` and the client got a protocol error. Stopping `dev` closes open sockets instead of waiting for them.
30
+ - **`examples/kitchen-sink` is a dashboard.** Every tile calls its real binding: a KV counter, a D1 guestbook that shows the SQL it ran, R2 upload and list, a Durable Object chat over WebSocket (hibernation API), a queue and cron feed read from D1, a service binding to `quote-service`, and secret names. Latency is measured in the worker with `performance.now()` and returned in `Server-Timing`; sizes are measured at build time. The Astro UI is gone; `sproutboat.standalone.jsonc` builds the same app as a standalone binary.
31
+
32
+ - **Every example is a standard Cloudflare Worker, and CI says so.** `examples/*` are rewritten as TypeScript in the Workers style: `export default { async fetch(request, env, ctx) } satisfies ExportedHandler<Env>`, `scheduled(controller, env, ctx)`, `queue(batch, env, ctx)`, Durable Objects that extend `DurableObject` and are called over RPC (`env.COUNTERS.getByName(name).increment()`), a `WorkerEntrypoint` with an RPC method, every binding call awaited, `await request.json()`, `Response.json()`, and a schema created once instead of on every request. All are at compatibility date 2026-11-18, and each has a README (what it shows, run, deploy, curl lines) and a generated `worker-configuration.d.ts`. `bun run examples:workerd` translates each `sproutboat.jsonc` into a `wrangler.jsonc`, typechecks the entry against the runtime types `wrangler types` generates (no Sproutboat declaration in the program), runs it under workerd with `wrangler dev` and sends the same requests `bun run examples` sends the standalone binary; `bun test` runs it (so the CI check job does), and `bun run examples:types` verifies every `worker-configuration.d.ts` is current and typechecks, also under `bun test`. What is skipped is printed with its reason: `analytics` (`query()` is a Sproutboat extension) and `outbound-fetch` (the private-address refusal is Sproutboat policy) send only the requests Workers can answer. examples/README.md states the two rules the examples follow: standard Workers style, and no test-only code in examples.
33
+
34
+ - **`examples/conformance`: the binding conformance suite has its own fixture.** The routes and vectors that existed only for tests (login and notes, the crypto vectors with `crypto.scryptVerify`, the async echo, the egress refusals, `/admin/stats`, the direct-transfer tickets, the service-binding probe) moved out of `examples/kitchen-sink/src/index.js` into a worker of their own, with its own checks on both backends (`bun run conformance`, `bun run conformance:standalone`: 34 and 37 checks). `kitchen-sink` is the demo only: the dashboard's API, its limits, the chat room and its cron jobs (77 and 75 checks), no `binary-fixture.bin` in the deployed site, no `ViewCounter` or `QUOTE_URL`. Its routes under `/api`, `/healthz` and the chat (`/api/chat`, the `?probe=1` probe, the 96 KiB history cap) are unchanged. `bun run kitchen-sink` and `kitchen-sink:standalone` still exist and run the demo's checks and then the conformance fixture's. Both harness pairs now share `examples/lib/backends.ts`.
35
+ - **`kitchen-sink` is Workers-style and runs on workerd.** Its entry points take `env` as an argument (`fetch(request, env)`, `scheduled(controller, env)`, `queue(batch, env)`), `ChatRoom` extends `DurableObject`, and its tables are created once per process instead of on every request. Running it under workerd found two bugs that Sproutboat's runtime had hidden, both fixed: `env.SESSIONS.list("")` (the pre-2026-10-25 argument, which throws on Workers) and a chat broadcast that sent to a socket already closing. It has a `worker-configuration.d.ts` and JSDoc types for `env`; the entry stays JavaScript because the Code panels excerpt it and the harness compiles it as it is.
36
+
37
+ ### Fixed
38
+
39
+ - **`deploy` to a Cloud no longer unroutes a custom domain.** A deploy used to write the app's whole `hosts` list as just `<project>.<team>.sproutboat.app`, so a domain an operator had routed (`sbcloud route add risulta.dev`, or `--host` in `tools/sbcloud-deploy`) was dropped by the next `sproutboat deploy`, silently. It now sends `hosts_patch: { add: [that name] }`, which the Cloud merges into the live list when the record is written (for a deploy from source, when the build is applied, so a domain added while it builds is kept too). Routes the deploy did not add stay, as `wrangler deploy` leaves dashboard routes alone when the config declares none. Against a Cloud that predates `hosts_patch` the CLI merges the live routes itself and says so. Needs no Cloud change to work, and is atomic once the Cloud has it.
40
+
41
+ ### Changed
42
+
43
+ - **`sproutboat types` writes `worker-configuration.d.ts`, not `sproutboat-env.d.ts`, and its positional is the output file.** A positional that is an existing directory is still read as the project directory. The old file is removed when it is still exactly as the old command generated it; commit the new one and add it to your tsconfig `include`. The old file declared `env` as a global for every date; the new one declares `Env`, and `env` only for dates before 2026-09-28, where it is the only way to reach the bindings.
44
+
13
45
  ## [0.54.0] - 2026-11-11
14
46
 
15
47
  Built on `@sproutboat/runtime` 0.44.0, `@sproutboat/wire` 0.22.4 and `@sproutboat/toolchain` 0.24.2 (runtime and wire from 0.43.0 and 0.22.3). Runtime 0.44.0 adds service bindings between Cloud apps from compatibility date 2026-11-18; every app is recompiled on its next build, since the runtime prelude changed. The Cloud-only features below need a Cloud control plane with sproutboat-cloud#42, #45 and #49; remote D1 and R2 also need a host whose agent serves the data routes (sproutd#93).
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  <picture>
2
2
  <source media="(prefers-color-scheme: dark)" srcset="docs/logo-dark.svg">
3
- <img src="docs/logo-light.svg" alt="sproutboat" width="220" height="32">
3
+ <img src="docs/logo-light.svg" alt="sproutboat" width="220" height="34">
4
4
  </picture>
5
5
 
6
6
  https://sproutboat.com
@@ -54,6 +54,20 @@ npm install
54
54
  npm run dev # runs it right here, no control plane needed
55
55
  ```
56
56
 
57
+ Want a starting point with more in it? `init` has four starters, each standard Workers TypeScript with a README,
58
+ tests and a current `worker-configuration.d.ts`, and each one runs unchanged on Cloudflare:
59
+
60
+ ```sh
61
+ npx sproutboat init --list-templates
62
+ npx sproutboat init chat --template realtime-room # Durable Object, hibernating WebSockets, presence
63
+ npx sproutboat init tasks --template api-d1 # JSON API on D1 with migrations and validation
64
+ npx sproutboat init site --template site-api # static site and its API routes in one Worker
65
+ npx sproutboat init nightly --template jobs # cron enqueues, a queue consumer works, KV holds state
66
+ ```
67
+
68
+ In a terminal `init` asks for the template, the directory, whether to install and whether to use git, the way
69
+ `npm create cloudflare` does; `-y` takes the defaults and `--no-install` / `--no-git` skip a step.
70
+
57
71
  Happy with it? Log in once, then ship it:
58
72
 
59
73
  ```sh
@@ -154,10 +168,20 @@ preselects the team on the approval page.
154
168
  app itself, or when `compatibility_date` is older than `2026-11-18`, the date
155
169
  from which the platform runtime serves service bindings between Cloud apps.
156
170
  Hosts need the matching sproutd support.
157
- - **Not sent yet.** Other bindings (KV, D1, R2, queues, Durable Objects) and cron
158
- triggers in `sproutboat.jsonc` are not sent to the Cloud by the CLI yet; the
159
- deploy warns and the app runs without them. `secrets` names in
160
- `sproutboat.jsonc` only declare them. Static assets are uploaded by content hash.
171
+ - **Every other binding.** `kv_namespaces`, `d1_databases`, `r2_buckets`, `queues`,
172
+ `analytics_engine_datasets`, `durable_objects`, `ratelimiters`, the `secrets` names, the
173
+ `assets` binding and `triggers.crons` go to the Cloud in the app record. On the Cloud each
174
+ app's storage is private to it, so nothing is provisioned and no id is needed: the binding
175
+ is the name (`DB` is the database `d1 execute --remote` addresses, `UPLOADS` the bucket
176
+ `r2 object` addresses), and the record's resource id is that name in lowercase (a queue's
177
+ is its queue name). An account-level `id` pinned in `sproutboat.jsonc` is ignored, with a
178
+ warning. The worker consumes every queue it binds, so each queue becomes one queue
179
+ consumer carrying its `max_batch_size`, `max_batch_timeout`, `max_retries` and
180
+ `dead_letter_queue` (Cloudflare's defaults when unset). `deploy` refuses, before
181
+ uploading anything, more than 16 crons, 16 queues or 64 bindings in all, a cron or name the
182
+ Cloud would refuse, and a `durable_objects` class the entry module does not export.
183
+ `deploy --dry-run` lists what would be sent. Storage itself is created by the host on first
184
+ use and survives deploys.
161
185
 
162
186
  For self-hosted (Community) endpoints, `login` keeps the browser flow, and for
163
187
  CI or an agent you can skip it and set `SPROUTBOAT_API_URL` + `SPROUTBOAT_API_TOKEN`.
@@ -168,8 +192,8 @@ The everyday loop:
168
192
 
169
193
  | Command | What it does |
170
194
  | --- | --- |
171
- | `init [name]` | Scaffold `sproutboat.jsonc`, `src/index.js` and a `package.json` that pins `sproutboat` |
172
- | `dev [--port <n>]` | Run it here against a real broker, rebuilding on save |
195
+ | `init [name] [--template <name>]` | Scaffold a project: the hello starter, or one of four TypeScript starters with tests (`--list-templates`) |
196
+ | `dev [--port <n>] [--ip <address>]` | Run it here against a real broker, rebuilding on save; `--ip 0.0.0.0` makes it reachable on your network |
173
197
  | `deploy` | Build, provision bindings, upload, wait until the URL serves |
174
198
  | `tail [filters]` | Live request events, console output and exceptions, like `wrangler tail` |
175
199
  | `rollback <id>` | Re-activate a previous version |
@@ -289,7 +313,7 @@ files in the project and from its own `node_modules`, and may also export
289
313
  is a global, not a parameter. Binding calls are synchronous until the compatibility
290
314
  date that makes each one return a Promise, as on Workers (D1 from 2026-10-24, KV from
291
315
  2026-10-25, R2 from 2026-10-26); `await` works at every date, so always await them.
292
- [`examples/kitchen-sink/`](examples/kitchen-sink) uses every binding.
316
+ [`examples/`](examples) has one small example per binding.
293
317
 
294
318
  For objects larger than one request, use resumable R2 multipart uploads. The
295
319
  [large-object guide](docs/r2-large-objects.md) covers part sizing, browser
package/SURFACE.md CHANGED
@@ -3,16 +3,16 @@
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.54.0
6
+ **Package:** `sproutboat` 0.55.0
7
7
 
8
8
  ## Commands
9
9
 
10
10
  | Command | Arguments | Summary |
11
11
  | --- | --- | --- |
12
- | `init` | `[name]` | Scaffold sproutboat.jsonc, src/index.js and package.json (sproutboat devDependency, dev, deploy and check scripts) in ./<name>. |
12
+ | `init` | `[name] [--template <name>] [--list-templates] [--git | --no-git] [--install | --no-install] [-y, --yes]` | Scaffold a project in ./<name> (the last path segment is the app's name), with the shape of `npm create cloudflare`. Without --template it writes sproutboat.jsonc, src/index.js and package.json (sproutboat devDependency, dev, deploy and check scripts). `--template <name>` writes a starter instead, standard Workers TypeScript with tests, a README and a current worker-configuration.d.ts: realtime-room (a chat room on Durable Objects and hibernating WebSockets), api-d1 (a JSON API on D1 with migrations and validation), site-api (a static site and its API in one Worker) or jobs (a cron trigger, a queue and KV); `--list-templates` prints them with their descriptions. In a terminal it asks for the template, the directory, whether to install dependencies and whether to use git (both default to yes); `-y` takes every default without asking, and anywhere else nothing runs that a flag did not ask for. `--install` runs the project's package manager (npm, pnpm, yarn or bun, by lockfile, else the tool that launched the CLI); `--git` runs `git init` and makes a first commit, and does nothing inside an existing repository. It never overwrites a file. |
13
13
  | `check` | `[project-dir]` | Validate the config and entry point without building. |
14
- | `types` | `[project-dir]` | Write sproutboat-env.d.ts from the config, so an editor knows what `env` holds. `dev` and `deploy` refresh it when the config is newer. |
15
- | `dev` | `[project-dir] [--port <n>] [--no-watch] [-e, --env <name>] [--env-file <file>]...` | Run the project on this machine with local storage, rebuilding on save. Secrets come from .dev.vars or the .env files (never both), as in Wrangler; a declared secret with no value is a warning. |
14
+ | `types` | `[path] [--env-interface <name>] [--strict-vars[=false]] [--include-runtime[=false]] [--include-env[=false]] [--check]` | Generate the project's `Env` as TypeScript from sproutboat.jsonc, like `wrangler types`: the file is worker-configuration.d.ts unless <path> names another; it declares `interface Env` (and `Cloudflare.Env`, which `import { env } from "cloudflare:workers"` reads) with a type per binding, literal types for vars (--strict-vars=false makes them `string`), and a Durable Object binding typed with its class when the entry is TypeScript. It references the runtime types shipped with the CLI (--include-runtime=false leaves that out, for a project that brings its own) instead of copying them in, and there is no --include-main-module. Type a handler as `export default { ... } satisfies ExportedHandler<Env>`. --check exits 1 when the file is missing or out of date, by the hash in its first lines. KV, D1, R2, Durable Object and Fetcher types follow the compatibility date, as the runtime does. `dev` and `deploy` refresh the default file when it was written by a plain `sproutboat types` and the config has moved on; they leave one made with other flags alone. A positional that is an existing directory is still read as the project directory. The file earlier releases wrote, sproutboat-env.d.ts, is removed when it is still as generated. |
15
+ | `dev` | `[project-dir] [--port <n>] [--ip <address>] [--no-watch] [-e, --env <name>] [--env-file <file>]...` | Run the project on this machine with local storage, rebuilding on save. It listens on 127.0.0.1 unless `--ip` names another address (`--ip 0.0.0.0` is reachable from other machines on the network, as in Wrangler). Secrets come from .dev.vars or the .env files (never both), as in Wrangler; a declared secret with no value is a warning. |
16
16
  | `run` | `[project-dir] [--binary <file>] [--env-file <file>]... [--port <n>] [--data-dir <dir>] [--require <NAME,...>] [--check]` | Launch a standalone binary with its port, data directory and declared secrets supplied at launch, never baked in. Precedence: --port/--data-dir, then the --env-file files (later overrides earlier), then the environment. A missing declared secret fails the launch, like wrangler deploy; --check validates without starting. |
17
17
  | `build` | `[project-dir] [--target host|linux-x64|linux-arm64] [--standalone] [--release] [--deploy-files] [--with-restore] [--emit-only]` | Compile the app to a native binary (Porffor + Zig), cross-compiled for Linux x64 by default. `--target host` builds for this machine instead, to run locally, not deployable. `--standalone` emits one executable carrying its own bindings, with SQLite compiled in (~2 MB) and no separate storage process; add `--target linux-arm64` for an aarch64 box (the default is `linux-x64`) and `--deploy-files` to write a Dockerfile, a systemd socket and service, a fly.toml and a push script beside it. `--release` makes a standalone build the full `-flto -O3` one to publish; without it the link skips LTO (1.5 to 2x faster cold, ~90 MB link memory instead of 1.1 to 1.6 GB, fewer requests per second); `SPROUTBOAT_RELEASE=1` does the same, and a non-standalone build is always the release build. `--with-restore` adds an offline `APP ltx-restore --generation DIR --output NEW_DB [--txid N]` startup command to a standalone executable; it runs before app initialization. `--emit-only` stops after writing `.sproutboat/porffor-units/<shape>/sprout.generated.js`, the module Porffor would compile, and prints its path: every check still runs, nothing is compiled. |
18
18
  | `toolchain` | `doctor [--json]` | Report the selected compiler inputs, cache locations, overrides, and host prerequisites without downloading or compiling anything. |
@@ -26,7 +26,7 @@
26
26
  | `d1` | `<list | create <name> | info <name> | rename <name> <new> | delete <name>> | execute <database> (--command <sql> | --file <path>) [--json] [--local | --remote] | migrations <create | list | apply> <database> ...` | D1 databases. `create` prints the id to bind from sproutboat.jsonc; `execute` runs SQL against the local dev database (.sproutboat/dev/d1/<BINDING>.sqlite) unless --remote, as in Wrangler, and prints JSON when stdout is not a terminal; `migrations create <database> <name>` writes migrations/NNNN_<name>.sql (migrations_dir in sproutboat.jsonc changes the folder), `migrations list` shows the ones not applied, and `migrations apply` applies them in order, each with its row in the d1_migrations table in one transaction, after a confirmation in a terminal. <database> is the binding name from d1_databases. --remote needs a Sproutboat Cloud endpoint (app:write to change data, app:read for SELECT only); self-hosted Community has no remote D1 route yet and says so. |
27
27
  | `r2` | `<list | create <name> | info <name> | rename <name> <new> | delete <name>> | object <get <bucket>/<key> [--file <path> | --pipe] | put <bucket>/<key> (--file <path> | --pipe) [--content-type <t>] [--cache-control <v>] ... | delete <bucket>/<key>>` | R2 buckets. `create` prints the id to bind from sproutboat.jsonc; `object get | put | delete <bucket>/<key>` read and write the objects of a Cloud app's bucket (<bucket> is the binding name from r2_buckets) with Wrangler's flags (--file, --pipe, --content-type, --content-disposition, --content-encoding, --content-language, --cache-control, --expires; `get` without --file writes ./<key>), 64 MiB per object, remote only (--local is refused). A key is 1 to 1024 bytes with no control character, backslash, or empty, . or .. segment, checked before anything is sent. The key needs the data:read grant to get and data:write to put or delete (the error says which it lacks and how to get it). On self-hosted Community `get` reads through the existing route by bucket name, and `put` and `delete` say they are not supported yet. |
28
28
  | `queues` | `<list | create <name> | info <name> | rename <name> <new> | delete <name>>` | Queues. `create` prints the id to bind from sproutboat.jsonc; consumers deliver in batches with retries, and stop after 5 attempts. |
29
- | `domains` | `<list | add <host> | verify <host> | delete <host>> [project-dir] [--api-url <url>]` | Attach a custom domain to the project (TXT-verified). No sub-command lists. |
29
+ | `domains` | `<list | add <host> | verify <host> | delete <host>> [project-dir] [--api-url <url>] [--account <team>]` | List the project's domains, or attach one (TXT-verified). On the Cloud only list works: custom domains are added by an operator, and deploys keep them. |
30
30
  | `secrets` | `<list | put <NAME> [--value <value>] | delete <NAME> | bulk [file.json | file.env]> [project-dir] [--no-deploy] [--api-url <url>] [--account <team>]` | Manage encrypted project secrets (read as env.NAME); `secret` is the same command, as in Wrangler. `put` reads the value from stdin unless --value is given, so it stays out of shell history. `bulk` takes the file of `wrangler secret bulk`: JSON {"NAME": "value", "OLD": null} (null deletes) or a .env file, from the file or stdin, at most 100 entries. On a Cloud endpoint secrets are per app and independent of deploys: put, delete and bulk act on the deployed app, and by default wait for a host to apply them as a new version of the running module (like wrangler secret, which deploys at once); --no-deploy returns as soon as the change is stored. Permissions secret:write and secret:list; an app that is not deployed yet says to deploy first or pass --secrets-file on the first deploy. Self-hosted Community applies secrets on the next deploy or restart of the app and has no redeploy route yet, so --no-deploy changes nothing there. |
31
31
  | `login` | `[--api-url <url>] [--token <token>] [--account <team>]` | Sign in, as the profile named by --profile (the first login makes `default`). On the Cloud: the device-code flow (RFC 8628), which prints a code and a link, opens the browser when it can, and stores an account API key named `CLI on <hostname>` for the team you approve (--account preselects it). Elsewhere the browser flow, or store <token> for <url> directly. |
32
32
  | `logout` | `[--api-url <url>] [--account <team>]` | Forget the stored credential of the profile in use, or for <url>. On the Cloud it also revokes the key on the server (the default team, or --account). |
@@ -35,7 +35,7 @@
35
35
  | `complete` | `[bash | zsh | fish]` | Print a shell completion script for the top-level commands (defaults to $SHELL). |
36
36
 
37
37
  ```
38
- usage: sproutboat <init [name] | check [project-dir] | types [project-dir] | dev [project-dir] [--port <n>] [--no-watch] [-e, --env <name>] [--env-file <file>]... | run [project-dir] [--binary <file>] [--env-file <file>]... [--port <n>] [--data-dir <dir>] [--require <NAME,...>] [--check] | build [project-dir] [--target host|linux-x64|linux-arm64] [--standalone] [--release] [--deploy-files] [--with-restore] [--emit-only] | toolchain doctor [--json] | deploy [project-dir] [--dry-run] [--artifact <dir>] [--no-wait] [--no-provision] [--api-url <url>] [--account <team>] [--module <file.so>] [--sproutd <dir> --generation <dir>] [--secrets-file <file>] | deployments <list | status | tail <id>> [--json] [--api-url <url>] [--account <team>] | versions <list | view <version-id>> [project-dir] [--api-url <url>] | rollback <version-id> [project-dir] [--api-url <url>] | tail [project-dir] [--format pretty|json] [--status ok|error|<code>|5xx] [--method GET,POST] [--sampling-rate <0..1>] [--search <text>] [--outcome <name>] [--recent] [--sprout] [--file <log|-> [--lines <n>] [--no-follow]] [--api-url <url>] | delete [project-dir] [--name <project>] --yes [--api-url <url>] | kv <list | create <name> | info <name> | rename <name> <new> | delete <name>> | <key | bulk | export> ... | d1 <list | create <name> | info <name> | rename <name> <new> | delete <name>> | execute <database> (--command <sql> | --file <path>) [--json] [--local | --remote] | migrations <create | list | apply> <database> ... | r2 <list | create <name> | info <name> | rename <name> <new> | delete <name>> | object <get <bucket>/<key> [--file <path> | --pipe] | put <bucket>/<key> (--file <path> | --pipe) [--content-type <t>] [--cache-control <v>] ... | delete <bucket>/<key>> | queues <list | create <name> | info <name> | rename <name> <new> | delete <name>> | domains <list | add <host> | verify <host> | delete <host>> [project-dir] [--api-url <url>] | secrets <list | put <NAME> [--value <value>] | delete <NAME> | bulk [file.json | file.env]> [project-dir] [--no-deploy] [--api-url <url>] [--account <team>] | login [--api-url <url>] [--token <token>] [--account <team>] | logout [--api-url <url>] [--account <team>] | whoami [--api-url <url>] [--account <team>] | profile <list | use <name>> | complete [bash | zsh | fish]>
38
+ usage: sproutboat <init [name] [--template <name>] [--list-templates] [--git | --no-git] [--install | --no-install] [-y, --yes] | check [project-dir] | types [path] [--env-interface <name>] [--strict-vars[=false]] [--include-runtime[=false]] [--include-env[=false]] [--check] | dev [project-dir] [--port <n>] [--ip <address>] [--no-watch] [-e, --env <name>] [--env-file <file>]... | run [project-dir] [--binary <file>] [--env-file <file>]... [--port <n>] [--data-dir <dir>] [--require <NAME,...>] [--check] | build [project-dir] [--target host|linux-x64|linux-arm64] [--standalone] [--release] [--deploy-files] [--with-restore] [--emit-only] | toolchain doctor [--json] | deploy [project-dir] [--dry-run] [--artifact <dir>] [--no-wait] [--no-provision] [--api-url <url>] [--account <team>] [--module <file.so>] [--sproutd <dir> --generation <dir>] [--secrets-file <file>] | deployments <list | status | tail <id>> [--json] [--api-url <url>] [--account <team>] | versions <list | view <version-id>> [project-dir] [--api-url <url>] | rollback <version-id> [project-dir] [--api-url <url>] | tail [project-dir] [--format pretty|json] [--status ok|error|<code>|5xx] [--method GET,POST] [--sampling-rate <0..1>] [--search <text>] [--outcome <name>] [--recent] [--sprout] [--file <log|-> [--lines <n>] [--no-follow]] [--api-url <url>] | delete [project-dir] [--name <project>] --yes [--api-url <url>] | kv <list | create <name> | info <name> | rename <name> <new> | delete <name>> | <key | bulk | export> ... | d1 <list | create <name> | info <name> | rename <name> <new> | delete <name>> | execute <database> (--command <sql> | --file <path>) [--json] [--local | --remote] | migrations <create | list | apply> <database> ... | r2 <list | create <name> | info <name> | rename <name> <new> | delete <name>> | object <get <bucket>/<key> [--file <path> | --pipe] | put <bucket>/<key> (--file <path> | --pipe) [--content-type <t>] [--cache-control <v>] ... | delete <bucket>/<key>> | queues <list | create <name> | info <name> | rename <name> <new> | delete <name>> | domains <list | add <host> | verify <host> | delete <host>> [project-dir] [--api-url <url>] [--account <team>] | secrets <list | put <NAME> [--value <value>] | delete <NAME> | bulk [file.json | file.env]> [project-dir] [--no-deploy] [--api-url <url>] [--account <team>] | login [--api-url <url>] [--token <token>] [--account <team>] | logout [--api-url <url>] [--account <team>] | whoami [--api-url <url>] [--account <team>] | profile <list | use <name>> | complete [bash | zsh | fish]>
39
39
  ```
40
40
 
41
41
  ## Global flags
@@ -68,7 +68,7 @@ usage: sproutboat <init [name] | check [project-dir] | types [project-dir] | dev
68
68
  | `CXX` | C++ compiler for a host build's native-fetch link step (default `c++` if present, else the vendored Zig cross-compiler targeting this host). |
69
69
  | `AR` | Archiver used to assemble uSockets.a for a host build (default `ar` if present, else Zig's). |
70
70
  | `SPROUTBOAT_COMPILE_TIMEOUT_MS` | Porffor compile timeout in ms (default 600000). |
71
- | `SPROUTBOAT_PORFFOR_CHANNEL` | Porffor channel for standalone builds: `alpha-22` (default) or `alpha-19` (the previous compiler, kept as a fallback for one release). |
71
+ | `SPROUTBOAT_PORFFOR_CHANNEL` | Porffor channel for standalone builds: `alpha-23` (default), `alpha-22` or `alpha-19` (the oldest compiler, kept as a fallback for one release). |
72
72
  | `SPROUTBOAT_RELEASE` | Same as `build --standalone --release`: link with LTO. Any value but empty, `0` or `false` turns it on. |
73
73
  | `SPROUTBOAT_FLTO` | Overrides link-time optimization on alpha 22 builds: `1` turns it on, `0` turns it off. |
74
74
  | `SPROUTBOAT_TOOLCHAIN_CACHE` | Managed Porffor and compiler cache root (default ~/.cache/sproutboat). |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sproutboat",
3
- "version": "0.54.0",
3
+ "version": "0.55.0",
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",
@@ -48,25 +48,32 @@
48
48
  "typecheck": "tsc --noEmit",
49
49
  "lint": "oxlint .",
50
50
  "examples": "bun examples/smoke.ts",
51
+ "examples:types": "bun examples/typecheck.ts",
52
+ "examples:workerd": "bun examples/workerd.ts",
53
+ "templates:embed": "bun scripts/embed-templates.ts",
51
54
  "build:cli": "bun scripts/build-cli.ts",
52
55
  "fmt": "oxfmt .",
53
56
  "fmt:check": "oxfmt --check .",
54
57
  "test": "bun test",
55
- "example:kitchen-sink": "bun examples/kitchen-sink/harness.ts",
58
+ "example:kitchen-sink": "bun dogfood/kitchen-sink/harness.ts",
56
59
  "surface": "UPDATE_SURFACE=1 bun test src/surface.test.ts",
57
60
  "version": "bun run surface && git add SURFACE.md",
58
61
  "prepublishOnly": "bun run typecheck && bun test",
59
62
  "style": "bunx style-doctor@latest --quiet",
60
- "kitchen-sink": "bun examples/kitchen-sink/harness.ts",
61
- "kitchen-sink:standalone": "bun examples/kitchen-sink/harness-standalone.ts"
63
+ "kitchen-sink": "bun run kitchen-sink:demo && bun run conformance",
64
+ "kitchen-sink:demo": "bun dogfood/kitchen-sink/harness.ts",
65
+ "conformance": "bun test/fixtures/conformance/harness.ts",
66
+ "kitchen-sink:standalone": "bun run kitchen-sink:demo:standalone && bun run conformance:standalone",
67
+ "kitchen-sink:demo:standalone": "bun dogfood/kitchen-sink/harness-standalone.ts",
68
+ "conformance:standalone": "bun test/fixtures/conformance/harness-standalone.ts"
62
69
  },
63
70
  "dependencies": {
64
71
  "@sproutboat/artifact": "^0.3.0",
65
72
  "@sproutboat/assets": "^0.3.0",
66
73
  "@sproutboat/config": "^0.6.0",
67
- "@sproutboat/runtime": "^0.44.0",
68
- "@sproutboat/toolchain": "^0.24.2",
69
- "@sproutboat/wire": "^0.22.4"
74
+ "@sproutboat/runtime": "^0.45.0",
75
+ "@sproutboat/toolchain": "^0.26.1",
76
+ "@sproutboat/wire": "^0.22.5"
70
77
  },
71
78
  "devDependencies": {
72
79
  "@oxlint/plugins": "1.85.0",
@@ -75,13 +82,14 @@
75
82
  "lefthook": "2.1.14",
76
83
  "oxfmt": "0.70.0",
77
84
  "oxlint": "1.85.0",
78
- "typescript": "7.0.2"
85
+ "typescript": "7.0.2",
86
+ "wrangler": "4.161.0"
79
87
  },
80
88
  "optionalDependencies": {
81
- "@sproutboat/cli-darwin-arm64": "0.54.0",
82
- "@sproutboat/cli-darwin-x64": "0.54.0",
83
- "@sproutboat/cli-linux-arm64": "0.54.0",
84
- "@sproutboat/cli-linux-x64": "0.54.0"
89
+ "@sproutboat/cli-darwin-arm64": "0.55.0",
90
+ "@sproutboat/cli-darwin-x64": "0.55.0",
91
+ "@sproutboat/cli-linux-arm64": "0.55.0",
92
+ "@sproutboat/cli-linux-x64": "0.55.0"
85
93
  },
86
94
  "engines": {
87
95
  "node": ">=18"
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Embed the starter templates in the CLI.
3
+ *
4
+ * `templates/<name>/` are real projects: CI runs each one (typecheck, tests, standalone, workerd). `sproutboat init
5
+ * --template <name>` has to find them in the compiled executable, which carries no files of its own, so this writes
6
+ * every text file of every template into `src/templates-embedded.ts`, a module the executable bundles like any other.
7
+ *
8
+ * bun scripts/embed-templates.ts # rewrite src/templates-embedded.ts
9
+ * bun scripts/embed-templates.ts --check # exit 1 when it is out of date (src/templates.test.ts runs this check)
10
+ *
11
+ * What is left out, because a project generates or installs it: worker-configuration.d.ts (`init` runs
12
+ * `sproutboat types` on the new project, so its hash matches the project's name), node_modules, build output,
13
+ * lockfiles and `.dev.vars`.
14
+ */
15
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
16
+ import { join, relative, resolve } from "node:path";
17
+ import { isString, jsonObject, parseJsonValue } from "../src/json";
18
+
19
+ const ROOT = resolve(import.meta.dir, "..");
20
+ const TEMPLATES = join(ROOT, "templates");
21
+ const OUTPUT = join(ROOT, "src/templates-embedded.ts");
22
+
23
+ const SKIPPED_NAMES = new Set([
24
+ "worker-configuration.d.ts",
25
+ "node_modules",
26
+ ".sproutboat",
27
+ "dist",
28
+ ".dev.vars",
29
+ "package-lock.json",
30
+ "bun.lock",
31
+ "pnpm-lock.yaml",
32
+ "yarn.lock",
33
+ ".DS_Store",
34
+ ]);
35
+
36
+ export interface EmbeddedTemplate {
37
+ description: string;
38
+ files: Map<string, string>;
39
+ }
40
+
41
+ function filesOf(dir: string): string[] {
42
+ return readdirSync(dir, { withFileTypes: true })
43
+ .filter((entry) => !SKIPPED_NAMES.has(entry.name))
44
+ .flatMap((entry) => (entry.isDirectory() ? filesOf(join(dir, entry.name)) : [join(dir, entry.name)]))
45
+ .sort();
46
+ }
47
+
48
+ export function readTemplates(): Map<string, EmbeddedTemplate> {
49
+ const found = new Map<string, EmbeddedTemplate>();
50
+ if (!existsSync(TEMPLATES)) return found;
51
+ for (const entry of readdirSync(TEMPLATES, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
52
+ if (!entry.isDirectory() || entry.name.startsWith(".") || SKIPPED_NAMES.has(entry.name)) continue;
53
+ const dir = join(TEMPLATES, entry.name);
54
+ const files = new Map<string, string>();
55
+ for (const file of filesOf(dir)) files.set(relative(dir, file).split("\\").join("/"), readFileSync(file, "utf8"));
56
+ const description = jsonObject(parseJsonValue(files.get("package.json") ?? "{}"))?.description;
57
+ if (!isString(description))
58
+ throw new Error(`templates/${entry.name}/package.json needs a description: it is the listing`);
59
+ found.set(entry.name, { description, files });
60
+ }
61
+ return found;
62
+ }
63
+
64
+ export function render(templates: Map<string, EmbeddedTemplate>): string {
65
+ const plain = Object.fromEntries(
66
+ [...templates].map(([name, { description, files }]) => [name, { description, files: Object.fromEntries(files) }]),
67
+ );
68
+ return `// Generated by scripts/embed-templates.ts from templates/. Do not edit: run \`bun run templates:embed\`.
69
+ // The starter templates \`sproutboat init --template\` writes, as text, so the compiled executable carries them.
70
+ export const EMBEDDED_TEMPLATES: Record<string, { description: string; files: Record<string, string> }> = ${JSON.stringify(plain, null, 2)};
71
+ `;
72
+ }
73
+
74
+ if (import.meta.main) {
75
+ const next = render(readTemplates());
76
+ if (process.argv.includes("--check")) {
77
+ const current = existsSync(OUTPUT) ? readFileSync(OUTPUT, "utf8") : "";
78
+ if (current !== next) {
79
+ console.error("src/templates-embedded.ts is out of date. Run `bun run templates:embed`.");
80
+ process.exit(1);
81
+ }
82
+ console.log("src/templates-embedded.ts is up to date.");
83
+ } else {
84
+ await Bun.write(OUTPUT, next);
85
+ console.log(`wrote src/templates-embedded.ts (${(next.length / 1024).toFixed(0)} KiB)`);
86
+ }
87
+ }
@@ -50,13 +50,44 @@ const MIGRATIONS_DIR_ERROR = 'migrations_dir must be a relative folder inside th
50
50
  const validMigrationsDir = (dir: JsonValue | undefined): boolean =>
51
51
  isString(dir) && dir !== "" && !dir.startsWith("/") && !/^[A-Za-z]:/.test(dir) && !dir.split(/[\\/]/).includes("..");
52
52
 
53
+ /** The `main` a project with assets and no `main` gets: a file that does not exist, written by `readProject` into `.sproutboat/`. */
54
+ export const ASSETS_ONLY_MAIN = "src/.assets-only.js";
55
+
56
+ /**
57
+ * The entry a Worker with only static assets runs, as Wrangler allows (`assets` and no `main`). The platform serves a matching
58
+ * file before any code runs; a request that matches none reaches this, and `env.<binding>.fetch` answers it with the project's
59
+ * `not_found_handling`: the single-page app shell, the nearest 404 page, or a plain 404.
60
+ */
61
+ export function assetsOnlySource(binding: string): string {
62
+ return [
63
+ "// Written by sproutboat: this project has `assets` and no `main`, so it is a Worker with only static assets.",
64
+ "export default {",
65
+ " fetch(request, env) {",
66
+ ` return env.${binding}.fetch(request);`,
67
+ " },",
68
+ "};",
69
+ "",
70
+ ].join("\n");
71
+ }
72
+
73
+ /** `assets` with a directory and no `main`: the assets-only case. The entry and the binding it calls are filled in. */
74
+ function withAssetsOnlyEntry(config: JsonObject): JsonObject {
75
+ const assets = jsonObject(config.assets ?? null);
76
+ if (config.main !== undefined || assets === undefined) return config;
77
+ return { ...config, main: ASSETS_ONLY_MAIN, assets: { ...assets, binding: assets.binding ?? "ASSETS" } };
78
+ }
79
+
53
80
  /**
54
81
  * parseConfig for a file that may carry the CLI-owned fields `account` and `migrations_dir`: they are validated here and
55
82
  * removed before the shared schema sees them.
56
83
  */
57
84
  export function parseProjectConfig(source: string): ConfigValidation {
58
85
  const loose = parseLoose(source);
59
- if (loose === undefined || !("account" in loose || "migrations_dir" in loose)) return parseConfig(source);
86
+ if (loose === undefined) return parseConfig(source);
87
+ if (!("account" in loose || "migrations_dir" in loose)) {
88
+ const filled = withAssetsOnlyEntry(loose);
89
+ return filled === loose ? parseConfig(source) : parseConfig(JSON.stringify(filled));
90
+ }
60
91
  const errors: string[] = [];
61
92
  if ("account" in loose && (!isString(loose.account) || !ACCOUNT_RE.test(loose.account)))
62
93
  errors.push("account must be the URL name of a team (lowercase letters, digits and dashes)");
@@ -65,5 +96,5 @@ export function parseProjectConfig(source: string): ConfigValidation {
65
96
  const rest = Object.fromEntries(
66
97
  Object.entries(loose).filter(([key]) => key !== "account" && key !== "migrations_dir"),
67
98
  );
68
- return parseConfig(JSON.stringify(rest));
99
+ return parseConfig(JSON.stringify(withAssetsOnlyEntry(rest)));
69
100
  }
package/src/build.ts CHANGED
@@ -342,8 +342,8 @@ export async function buildArtifact(input: BuildInput): Promise<BuildOutput> {
342
342
  generatedSource,
343
343
  target,
344
344
  optimize: input.optimize ?? "release",
345
- // a non-LTO link is a different binary from the release one; only the alpha 22 channel honours it
346
- lto: porfforChannel(input.withRestore) === "alpha-22" ? ltoEnabled({ ...input, target }) : true,
345
+ // a non-LTO link is a different binary from the release one; only the alpha 21 line's channels honour it
346
+ lto: porfforChannel(input.withRestore) !== "alpha-19" ? ltoEnabled({ ...input, target }) : true,
347
347
  toolchain: toolchainStamp(),
348
348
  toolchainPackageVersion,
349
349
  porffor: [PORFFOR_COMMIT_FULL, PORFFOR_ARCHIVE_SHA256],
@@ -351,7 +351,7 @@ export async function buildArtifact(input: BuildInput): Promise<BuildOutput> {
351
351
  channel: porfforChannel(input.withRestore),
352
352
  // the alpha 22 channel's own inputs: the patched tree, the shim, the bridge, the build recipe, LTO
353
353
  channelInputs:
354
- porfforChannel(input.withRestore) === "alpha-22"
354
+ porfforChannel(input.withRestore) !== "alpha-19"
355
355
  ? [A21_STANDALONE_FINGERPRINT, process.env.SPROUTBOAT_FLTO ?? ""]
356
356
  : null,
357
357
  native: embedded ? [sqliteStamp(), bearsslStamp()] : subtle ? [bearsslStamp()] : [],