sproutboat 0.53.1 → 0.54.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,41 @@ maintained going forward by the `release` skill.
10
10
 
11
11
  ## Unreleased
12
12
 
13
+ ## [0.54.0] - 2026-11-11
14
+
15
+ 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).
16
+
17
+ ### Added
18
+
19
+ - **Profiles: `--profile <name>` and `SPROUTBOAT_PROFILE`.** A profile is an endpoint, its stored credential and a default team, so one machine can hold a Community login and a Cloud login side by side and say which one a command uses. `login --profile <name>` makes one (it fixes the endpoint, so `--api-url` must match when the profile exists), `logout` and `whoami` act on the profile in use, `profile list` shows them and `profile use <name>` changes the default. `whoami` and `deploy` now start with two lines, `profile` and `endpoint`, so a deploy never goes somewhere unannounced. A profile and an endpoint that name different places (`--profile cloud --api-url <community>`, or the same pair in the environment) are an error. Precedence: `--api-url` and `--profile`, then `SPROUTBOAT_API_URL` and `SPROUTBOAT_PROFILE`, then the default profile, then `https://dash.sproutboat.com`; `--profile` beats `SPROUTBOAT_API_URL` and `--api-url` beats `SPROUTBOAT_PROFILE`.
20
+ - **Global flags with Wrangler's meanings, accepted before or after the command:** `-c, --config <file>` (the project's config file; its directory is the project), `--cwd <dir>`, `-e, --env <name>`, `--env-file <file>` (repeat it; later files override earlier ones) and `-v, --version`. `dev` and `run` still take `--env` and `--env-file` as before, now through the same parser, and `--flag=value` works. A flag a command cannot honour is refused (`deploy --env staging` exits 2 instead of deploying), a repeated single-value flag takes its last value, and everything after `--` is left alone.
21
+ - **`r2 object get | put | delete <bucket>/<key>`, with Wrangler's flags.** `get` takes `--file <path>` or `--pipe` (bytes to stdout) and, with neither, writes `./<key>` like Wrangler; `put` takes `--file` or `--pipe` plus `--content-type`, `--content-disposition`, `--content-encoding`, `--content-language`, `--cache-control` (short forms `--ct --cd --ce --cl --cc`) and `--expires`, and uses the file's type when no content type is given; `delete` is idempotent. On a Cloud endpoint `<bucket>` is a binding from `r2_buckets` and the objects live in the app's bucket on its host, 64 MiB each. A key follows the Cloud's rules (1 to 1024 bytes of UTF-8, no control character, no backslash, no empty, `.` or `..` segment) and a bad one is refused with the reason before anything is sent. A download is an inert attachment (`application/octet-stream`, `nosniff`, `CSP: sandbox`), so the object's own type is read from `x-object-content-type` (only when it was stored) and shown in the note; a PUT always declares its length. `data:read` (get) and `data:write` (put, delete) are the grants: a key without one is told which, and how to get it (the "Deploy and data" or "Read data" key preset). A rate limit and a busy host say when to retry; a backup capture, a full quota (507) and a failed audit each have their own message; a 502 or 503 from a Cloud host that does not serve R2 yet says "remote D1/R2 isn't available on this Cloud host yet". `--profile`, `-c` and `--cwd` work as everywhere, and an unknown option is refused. Unlike Wrangler there is no local mode yet (`--local` is refused with the reason, and remote is the default), since dev keeps R2 objects in its broker store. On self-hosted Community `get` reads through the existing route by bucket name, and `put` and `delete` report that they are not supported yet. Follows sproutboat-cloud#49 (docs/host-data.md); real data needs a host agent that serves the R2 routes.
22
+ - **`deployments list | status | tail <id>`.** `list` shows the 10 most recent deployments of the project in this directory, newest first, with the live one marked; `status` shows what is live and whether a host applied it; both take `--json`. On a Cloud endpoint a deployment is a build (`GET /v1/apps/<app>/builds`): `tail <id>` follows it server-side, from its first log line to its end state, so a deploy that was detached with Ctrl-C can be picked up again; it exits 1 when the build failed or was cancelled, and Ctrl-C only stops following (exit 130, the build keeps running). On self-hosted Community a deployment is a version, `list` and `status` read `/api/projects/<app>/deployments`, and `tail` says there is nothing to follow. Needs sproutboat-cloud#49 for the build list.
23
+ - **`secret` is `secrets`, and `secrets bulk [file]` takes Wrangler's bulk file.** `secret bulk` reads JSON `{"NAME": "value", "OLD": null}` (a null deletes) or a `.env` file (no deletes), from the file or stdin, at most 100 entries, and refuses a bad name or value by name before sending anything. On a Cloud endpoint it is one `PATCH /v1/apps/<app>/secrets`, so one new version. `secrets put`, `delete` and `bulk` now wait for a host to apply that version, as `wrangler secret put` deploys at once (the Cloud already re-ships the same module with the new sealed secrets as a new record generation); `--no-deploy` returns as soon as the change is stored, which is what they did before. A host that fails to apply it is an error with the host's reason. Self-hosted Community still applies secrets on the next deploy or restart of the app (it has no redeploy route), so `--no-deploy` changes nothing there and `bulk` is one call per name.
24
+ - **`d1 execute` and `d1 migrations create | list | apply`, with Wrangler's semantics.** `d1 execute <database> --command <sql> | --file <path>` runs every statement of the script in order, in one transaction, against the local dev database (`.sproutboat/dev/d1/<BINDING>.sqlite`) unless `--remote` is given, as Wrangler does; it prints JSON (`[{ results, success, meta }]`) when `--json` is passed or stdout is not a terminal. `<database>` is a binding from `d1_databases`. `--persist-to <dir>` moves the local state, `--yes` skips the remote `--file` confirmation. Migrations are `migrations/NNNN_<name>.sql` (the top-level `migrations_dir` in `sproutboat.jsonc` changes the folder; `create` starts at 0001 and goes one past the highest), applied in order, each in one transaction with its row in the `d1_migrations` table (`id`, `name`, `applied_at`, Wrangler's schema); the first failing migration is rolled back, the earlier ones stay applied, and the command exits 1. A terminal asks before applying; CI does not. `--remote` needs a Sproutboat Cloud endpoint and its `POST /v1/apps/<app>/d1/<BINDING>/query` (`data:write` to change data, `data:read` to SELECT only; a key without the grant is told which one and how to get it). A busy host, a rate limit, a refused statement (`refused`), a SQL error (with its statement number), a paused backup capture, a memory limit and a full quota each get their own message; a 502 or 503 from a Cloud host that does not serve D1 yet says "remote D1/R2 isn't available on this Cloud host yet". On self-hosted Community it says it is not supported yet. Follows sproutboat-cloud#49 (docs/host-data.md); `--profile`, `-c` and `--cwd` work as everywhere.
25
+ - **`login` on a Sproutboat Cloud endpoint is the device-code flow (RFC 8628).** It prints a code and a link, opens the browser when it can, and stores an account API key (`CLI on <hostname>`, Developer at most) for the team you approve. `logout` revokes that key on the server and then forgets it; `whoami` shows the team, the key and its access, and exits 1 when not logged in. `--account <team>` preselects a team.
26
+ - **`deploy` to the Cloud builds from source.** It bundles the project, uploads the generated source by sha256 (skipped when the Cloud has it), asks the Cloud to build it, streams the build log (` | ` per line), then prints `✓ Built in 4.1 s · peak memory 742 MB (cap 1200 MB)` and waits for a host to apply the generation the build wrote. Assets go up by hash first. A dropped log stream resumes from the last line and then falls back to polling; Ctrl-C cancels the build (exit 130); `--no-wait` returns once the build is accepted and prints `/v1/builds/<id>`; `--dry-run` says it would build in the Cloud. A queued build the builder is holding back shows why (for example `waiting for memory (1172 MB available, a build needs 1600 MB)`) and keeps waiting. A failed build prints its reason, its peak memory when known, and exits 1; a Cloud with builds off or no builder says so, and a 429 shows the Cloud's message. Then it prints `https://<app>.<team>.sproutboat.app`. `--module app.so` and `--sproutd` with `--generation` stay as operator paths; a team key's module upload is refused with "modules are built by the Cloud; run \`sproutboat deploy\` without --module". Logged out in a terminal it logs in first; with no terminal it fails naming `SPROUTBOAT_API_TOKEN`. Other bindings and crons in `sproutboat.jsonc` are not sent yet and the deploy warns.
27
+ - **`SPROUTBOAT_API_TOKEN` always wins over a stored login** (`SPROUTBOAT_TOKEN` stays as the older name). `SPROUTBOAT_ACCOUNT` and an `account` field in `sproutboat.jsonc` choose the team, like Wrangler's `account_id`. Several teams can be stored per endpoint.
28
+ - **The deploy target is the endpoint you are logged in to.** The CLI asks it what it is (`GET /v1/config`) and deploys the Cloud way or the Community way. There is no `--cloud` flag, since Wrangler also has one target chosen by credentials.
29
+
30
+ - **`secrets put`, `list` and `delete` work on a Sproutboat Cloud app,** by name through `/v1/apps/<app>/secrets`, independent of deploys like `wrangler secret`. An app that is not deployed says to deploy first or pass `--secrets-file` on the first deploy; a 403 names `secret:write` or `secret:list`. `secrets` takes `--api-url` and `--account`.
31
+ - **`deploy --secrets-file <file>` (Cloud)** reads a `.dev.vars` or `.env` file and merges its values into the app's secrets. A build deploy never puts them in the build spec (stored in plaintext): once the build is ready they are set with `PATCH /v1/apps/<app>/secrets` and the deploy waits for that newer generation, so on a first deploy the app is briefly live before its secrets land; it cannot be combined with `--no-wait`. A module deploy sends `secrets_patch.set` in its PUT. A deploy never replaces or wipes the app's secrets, and never reads `.dev.vars` by itself.
32
+ - **`deploy` to the Cloud sends service bindings.** Each `services` entry is resolved to the same team's Cloud app of that name (`GET /v1/apps`) and sent as a `service` binding plus a `services` id map. A callee that is not deployed, a binding to the app itself, or a `compatibility_date` before 2026-11-18 fails before anything is uploaded, with the fix named. `examples/kitchen-sink` is on 2026-11-18. Depends on the Cloud secrets contract and on service binding support in sproutd.
33
+
34
+ ### Changed
35
+
36
+ - The docs link in `--help` now points to https://sproutboat.com/docs (docs.sproutboat.com redirects there).
37
+ - **`build --standalone` skips LTO unless you pass `--release`** (or set `SPROUTBOAT_RELEASE=1`). Units still compile at `-O3`; only the link changes. Cold builds are faster and the link takes about 90 MB instead of 1.1 to 1.6 GB, at some request throughput: see docs/standalone.md, "Dev and release builds", for the measured numbers. `--release` is the old `-flto -O3` build, so publish binaries with it. Release and non-release binaries have separate build-cache entries. `deploy` and non-standalone builds are unchanged.
38
+ - **The default endpoint is `https://dash.sproutboat.com`** (it was `https://dashboard.sproutboat.com`).
39
+ - **Credentials live in `~/Library/Preferences/sproutboat` on macOS,** Wrangler's choice. The old `~/.config/sproutboat/credentials.json` is still read, and the next login writes the new place. Elsewhere nothing moves.
40
+ - **`sproutboat --help` is shorter and plainer.** The tagline no longer says "on your own VPS", so it fits the Cloud and a self-hosted server alike. Each command has one short line (the detail is in `sproutboat <command> --help`), internal terms like "native-fetch sprout", "broker" and "id-less storage bindings" are gone, `delete` is under SHIP, and `kv` says it covers keys, bulk data and exports. Every command has its own emoji (`run` is 🏃, `rollback` is ⏪), and the ones that drew one cell wide now carry U+FE0F so the columns line up. A GLOBAL FLAGS section lists `-h, --help` and `-v, --version`, and the footer points to `--help` for a command's options.
41
+ - **Profiles live in a new `profiles.json`; `credentials.json` does not change.** `sproutboat init` pins the CLI in each project, so one machine often runs several versions, and every older CLI keeps reading `credentials.json` exactly as before (version 1: `activeApiUrl` plus one credential per endpoint URL). `profiles.json` next to it holds the profile names, the default profile and each profile's endpoint, and refers to the credential in `credentials.json` instead of copying it. Only a second profile on an endpoint that already has one carries its own credential in `profiles.json`; `credentials.json` then holds the credential of the profile most recently saved or switched to (`login`, `profile use`) on that endpoint. Nothing to do when you upgrade: the endpoint you were logged in to becomes the `default` profile and every other stored endpoint a profile named after its host (`dash.sproutboat.com`, `localhost-8787`). Reading changes nothing on disk; the first login, logout or `profile use` writes `profiles.json`. If you ran a pre-release build that wrote a version 2 `credentials.json`, it is converted back on first use (and `credentials.v1.json`, if present, can be deleted).
42
+
43
+ ### Fixed
44
+
45
+ - **A command never ignores a flag it does not know, and a URL is never a project directory.** In 0.53.1, `sproutboat deploy --api-url https://dash.sproutboat.com` read the URL as the project directory, and `--api-url=https://…` was ignored, so the deploy went to the saved endpoint (a stored Community login, say) instead of the one named. Now every command checks its options before it runs: an unknown one stops it with exit 2 (`unknown option --dry-runn (did you mean --dry-run?)`), `--flag=value` works for every flag that takes a value, and a positional that looks like `https://…` is refused where a project directory or a name goes, with a pointer to `--api-url`, `--profile` and `SPROUTBOAT_API_URL`. `--api-url <url>` and `--api-url=<url>` now work on `deploy` and on every command that talks to a control plane (`login`, `logout`, `whoami`, `secrets`, `versions`, `rollback`, `tail`, `domains`, `delete`, the storage commands); the commands that talk to none (`init`, `check`, `types`, `dev`, `run`, `build`, `toolchain`, `profile`, `complete`) refuse it and say to use `--profile` or `SPROUTBOAT_API_URL`. Before this, `versions`, `rollback`, `tail`, `domains`, `delete` and the storage commands had no `--api-url` at all and went to the saved endpoint.
46
+ - **`init`'s help text and `SURFACE.md` now say it adds the `dev`, `deploy` and `check` scripts,** not just `dev` and `deploy`. Wording only.
47
+
13
48
  ## [0.53.1] - 2026-11-09
14
49
 
15
50
  Built on `@sproutboat/runtime` 0.43.0, `@sproutboat/wire` 0.22.3 and `@sproutboat/toolchain` 0.24.2, unchanged from 0.53.0. No runtime or wire change, so there is no new compatibility date and no recompile.
@@ -1777,7 +1812,8 @@ its own package.
1777
1812
  - Renamed the package to `sproutboat` (was `@sproutboat/cli`); dropped the
1778
1813
  `sprout` bin alias in favour of a user-defined shell alias.
1779
1814
 
1780
- [Unreleased]: https://github.com/baronunread/sproutboat-cli/compare/v0.32.0...HEAD
1815
+ [Unreleased]: https://github.com/baronunread/sproutboat-cli/compare/v0.54.0...HEAD
1816
+ [0.54.0]: https://github.com/baronunread/sproutboat-cli/compare/v0.53.1...v0.54.0
1781
1817
  [0.32.0]: https://github.com/baronunread/sproutboat-cli/compare/v0.31.0...v0.32.0
1782
1818
  [0.31.0]: https://github.com/baronunread/sproutboat-cli/compare/v0.30.0...v0.31.0
1783
1819
  [0.24.0]: https://github.com/baronunread/sproutboat-cli/compare/v0.23.0...v0.24.0
package/README.md CHANGED
@@ -57,7 +57,7 @@ npm run dev # runs it right here, no control plane needed
57
57
  Happy with it? Log in once, then ship it:
58
58
 
59
59
  ```sh
60
- npx sproutboat login --api-url https://control.example.com # one browser approval
60
+ npx sproutboat login # one approval in the browser; add --api-url for a self-hosted box
61
61
  npm run deploy # or: npx sproutboat deploy
62
62
  ```
63
63
 
@@ -68,10 +68,99 @@ not pinned to anything, so keep it for `init`, `login` and `whoami`:
68
68
  npm i -g sproutboat # then: sproutboat init, sproutboat whoami, ...
69
69
  ```
70
70
 
71
- `login` is one-time. It writes a long-lived token to
72
- `~/.config/sproutboat/credentials.json`, keyed by API URL, so you can hold
73
- logins for several instances at once. For CI or an agent, skip it and set
74
- `SPROUTBOAT_API_URL` + `SPROUTBOAT_TOKEN`.
71
+ `login` is one-time. It writes the credential to `credentials.json` in the user
72
+ config directory (mode 0600), keyed by API URL, so you can hold logins for
73
+ several instances at once. Profile names and the default profile are in `profiles.json` beside it;
74
+ `credentials.json` keeps the format older CLIs read, so projects pinned to an older version keep working. The directory follows Wrangler: `SPROUTBOAT_CONFIG_DIR`
75
+ or `XDG_CONFIG_HOME` if set, else `~/Library/Preferences/sproutboat` on macOS and
76
+ `~/.config/sproutboat` elsewhere (the old macOS place is still read).
77
+
78
+ ### Sproutboat Cloud
79
+
80
+ The default endpoint is `https://dash.sproutboat.com`, the Sproutboat Cloud.
81
+ `login` there is the device-code flow, like `gh auth login`:
82
+
83
+ ```sh
84
+ sproutboat login # prints a code and a link, opens the browser when it can
85
+ sproutboat whoami # the team, the key and what it may do
86
+ sproutboat deploy # uploads the app, waits for a host, prints the live URL
87
+ sproutboat logout # deletes the key on the server, then the local copy
88
+ ```
89
+
90
+ Open the link on any device where you are signed in, pick the team, check the
91
+ machine name and Approve. That mints an account API key named
92
+ `CLI on <hostname>` for that team, with your role capped at Developer. It shows
93
+ up under Settings, API keys, where you can revoke it. `--account <team>`
94
+ preselects the team on the approval page.
95
+
96
+ - **Which target.** Like Wrangler, there is one target, chosen by your
97
+ credentials: the endpoint you are logged in to (`--api-url`, then
98
+ `SPROUTBOAT_API_URL`, then the saved active endpoint, then the default).
99
+ The CLI asks the endpoint what it is (`GET /v1/config`) and deploys the Cloud
100
+ way to a Cloud and the Community way to a self-hosted box. There is no
101
+ `--cloud` flag.
102
+ - **CI.** `SPROUTBOAT_API_TOKEN` (an account API key, like `CLOUDFLARE_API_TOKEN`)
103
+ always wins over a stored login. `SPROUTBOAT_TOKEN` is the older name. With no
104
+ terminal and no token, `deploy` fails and names `SPROUTBOAT_API_TOKEN`; in a
105
+ terminal it starts the login itself.
106
+ - **Which team.** `account` in `sproutboat.jsonc` (like `account_id`), else
107
+ `--account`, else `SPROUTBOAT_ACCOUNT`, else your only stored team, else a
108
+ prompt. Precedence is `--account`, the config field, then the environment
109
+ variable. A key from the environment belongs to one team, so a different
110
+ `account` is an error.
111
+ - **Build.** The Cloud builds your app. `deploy` bundles the project here, uploads
112
+ that one file by its sha256 (it skips the upload when the Cloud already has
113
+ it), asks the Cloud to build it and streams the build log:
114
+
115
+ ```
116
+ Bundling src/index.js (212 KiB)
117
+ Uploading source a3f1c9d2e044 (212 KiB)
118
+ Building queued (position 1)
119
+ | porffor: compiled in 1.0 s
120
+ ✓ Built in 4.1 s · peak memory 742 MB (cap 1200 MB)
121
+ ```
122
+
123
+ An identical build is answered from the Cloud's cache. Ctrl-C cancels the
124
+ build. `--no-wait` returns once the build is accepted and prints
125
+ `/v1/builds/<id>`. A queued build shows why it is held back (such as waiting for memory) and keeps
126
+ waiting. A build that fails prints the reason and its peak memory, and exits 1; a source
127
+ is limited to 8 MiB. `--module app.so` and `--sproutd` with `--generation`
128
+ are operator paths for a prebuilt or locally compiled module; a team key is
129
+ refused with "modules are built by the Cloud", and regular deploys never need
130
+ them.
131
+ - **Result.** The first deploy creates the app (`Created`), later ones say
132
+ `Updated`. The app is served at `https://<app>.<team>.sproutboat.app`, and the
133
+ deploy waits until a host has applied the generation it printed.
134
+ `--dry-run` prints the plan and says it would build in the Cloud, without
135
+ uploading; `--no-wait` skips the wait.
136
+ - **Secrets.** As with Wrangler, secrets belong to the app and a deploy never
137
+ wipes them. `sproutboat secrets put NAME` (value on stdin, or `--value`),
138
+ `secrets list` and `secrets delete NAME` act on the deployed app at once; they
139
+ need the `secret:write` and `secret:list` permissions and say so on a 403. An app
140
+ that is not deployed yet has nowhere to keep them: deploy first, or pass
141
+ `deploy --secrets-file <file>` (`.dev.vars` or `.env` syntax, `UPPER_SNAKE` names)
142
+ on the first deploy; its values are merged into the app's secrets and nothing
143
+ else is removed. A build deploy stores its spec in plaintext, so the secrets
144
+ are never in it: once the build is ready they are set with one
145
+ `PATCH /v1/apps/<app>/secrets`, and the deploy waits for that newer
146
+ generation. On a first deploy the app is briefly live before its secrets
147
+ land, and `--secrets-file` cannot be combined with `--no-wait`. A deploy never reads `.dev.vars` on its
148
+ own (the Community deploy does not either): name the file. Production
149
+ values do not belong in `.dev.vars`; keep a separate file.
150
+ - **Service bindings.** `services: [{ "binding": "PEER", "service": "quote-service" }]`
151
+ binds to the Cloud app of that name in the same team. `deploy` looks the app
152
+ up first and refuses, before uploading anything, when it is not deployed
153
+ (deploy it first, with `sproutboat deploy` in its directory), when it is the
154
+ app itself, or when `compatibility_date` is older than `2026-11-18`, the date
155
+ from which the platform runtime serves service bindings between Cloud apps.
156
+ 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.
161
+
162
+ For self-hosted (Community) endpoints, `login` keeps the browser flow, and for
163
+ CI or an agent you can skip it and set `SPROUTBOAT_API_URL` + `SPROUTBOAT_API_TOKEN`.
75
164
 
76
165
  ## Commands
77
166
 
package/SURFACE.md CHANGED
@@ -3,45 +3,64 @@
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.53.1
6
+ **Package:** `sproutboat` 0.54.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 and deploy scripts) in ./<name>. |
12
+ | `init` | `[name]` | Scaffold sproutboat.jsonc, src/index.js and package.json (sproutboat devDependency, dev, deploy and check scripts) in ./<name>. |
13
13
  | `check` | `[project-dir]` | Validate the config and entry point without building. |
14
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] [--env <name>] [--env-file <file>]...` | Run the project on this machine against a real broker, 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. |
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. |
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
- | `build` | `[project-dir] [--target host|linux-x64|linux-arm64] [--standalone] [--deploy-files] [--with-restore] [--emit-only]` | Cross-compile the native-fetch sprout (Porffor + Zig). `--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 broker 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. `--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. |
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. |
19
- | `deploy` | `[project-dir] [--dry-run] [--artifact <dir>] [--no-wait] [--no-provision]` | Build (unless --artifact), auto-provision id-less storage bindings and pin their ids into sproutboat.jsonc, print the report, upload, wait until the URL serves. The control plane skips an upload that matches the live artifact byte-for-byte. --dry-run stops before upload; --no-wait skips the health check; --no-provision leaves id-less bindings as ephemeral deploy-scoped stores. |
20
- | `versions` | `<list | view <version-id>> [project-dir]` | List the project's deployed versions, or show one version's artifact and bindings. |
21
- | `rollback` | `<version-id> [project-dir]` | Re-activate a previous version. |
22
- | `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]]` | Stream live request events, console output and exceptions (like wrangler tail); --recent prints the last 100 request records, --sprout the running sprout + broker stdout/stderr. --file follows the JSON records a standalone binary writes with SB_LOG=json (no login needed). |
19
+ | `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>]` | Build (unless --artifact), create the storage bindings that have no id yet and write their ids into sproutboat.jsonc, print the report, upload, wait until the URL serves. The control plane skips an upload that matches the live artifact byte-for-byte. --dry-run stops before upload; --no-wait skips the health check; --no-provision leaves those bindings as temporary stores that last only for the deploy. On a Sproutboat Cloud endpoint (the default, https://dash.sproutboat.com) it bundles the project, uploads the source by sha256 (skipped when unchanged) and the assets by hash, has the Cloud build it while streaming the build log (Ctrl-C cancels the build), waits for a host to apply the generation the build wrote and prints https://<app>.<team>.sproutboat.app; --no-wait returns once the build is accepted and prints /v1/builds/<id>, and --dry-run says it would build in the Cloud. --module and --sproutd with --generation are the operator paths (an uploaded or locally compiled module, refused for team keys); regular deploys do not need them. On the Cloud, `services` in sproutboat.jsonc become service bindings to the team's Cloud apps of those names (each must be deployed first; compatibility_date 2026-11-18 or later; an app cannot bind itself), resolved before anything is uploaded; --secrets-file <file> (.dev.vars or .env format) sets those secrets once the build is ready (and cannot be combined with --no-wait), and a deploy never removes secrets it does not name (manage them with `secrets`); logged out in a terminal it starts the login, with no terminal it needs SPROUTBOAT_API_TOKEN. |
20
+ | `deployments` | `<list | status | tail <id>> [--json] [--api-url <url>] [--account <team>]` | The deployments of the project in this directory. `list` shows the 10 most recent, newest first, with the live one marked; `status` shows what is live and whether a host applied it; both take --json. On a Cloud endpoint a deployment is a build: `tail <id>` follows it server-side (its log, then its end state), from its first line, so a deploy that was detached with Ctrl-C can be picked up again; Ctrl-C stops following and never cancels the build. On self-hosted Community a deployment is a version (as in `versions list`), `status` shows the active one, and `tail` says there is nothing to follow, since a deploy there is finished when `deploy` returns. |
21
+ | `versions` | `<list | view <version-id>> [project-dir] [--api-url <url>]` | List the project's deployed versions, or show one version's artifact and bindings. |
22
+ | `rollback` | `<version-id> [project-dir] [--api-url <url>]` | Re-activate a previous version. |
23
+ | `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>]` | Stream live request events, console output and exceptions (like wrangler tail); --recent prints the last 100 request records, --sprout the running app's own stdout and stderr logs. --file follows the JSON records a standalone binary writes with SB_LOG=json (no login needed). |
24
+ | `delete` | `[project-dir] [--name <project>] --yes [--api-url <url>]` | Delete the project, every version, and its route. |
23
25
  | `kv` | `<list | create <name> | info <name> | rename <name> <new> | delete <name>> | <key | bulk | export> ...` | KV namespaces. `create` prints the id to bind from sproutboat.jsonc; key and bulk operations manage contents, and export writes a restorable JSON dump. |
24
- | `d1` | `<list | create <name> | info <name> | rename <name> <new> | delete <name>>` | D1 databases. `create` prints the id to bind from sproutboat.jsonc. |
25
- | `r2` | `<list | create <name> | info <name> | rename <name> <new> | delete <name>>` | R2 buckets. `create` prints the id to bind from sproutboat.jsonc. |
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
+ | `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. |
26
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. |
27
- | `domains` | `<list | add <host> | verify <host> | delete <host>> [project-dir]` | Attach a custom domain to the project (TXT-verified). No sub-command lists. |
28
- | `secrets` | `<list | put <NAME> [--value <value>] | delete <NAME>> [project-dir]` | Manage encrypted project secrets (read as env.NAME). `put` reads the value from stdin unless --value is given, so it stays out of shell history; applies on next deploy. |
29
- | `delete` | `[project-dir] [--name <project>] --yes` | Delete the project, every version, and its route. |
30
- | `login` | `[--api-url <url>] [--token <token>]` | Device-code browser flow, or store <token> for <url> directly. |
31
- | `logout` | `[--api-url <url>]` | Forget the stored credential for the active endpoint, or for <url>. |
32
- | `whoami` | — | Show the active endpoint and the account the stored token belongs to. |
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. |
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
+ | `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
+ | `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). |
33
+ | `whoami` | `[--api-url <url>] [--account <team>]` | Show the profile and endpoint in use, and the account or team the credential belongs to (exits 1 when not logged in or the key is rejected). |
34
+ | `profile` | `<list | use <name>>` | A profile is an endpoint, its stored credential and a default team. `list` shows them and marks the default, which a command uses when nothing else names one; `use <name>` makes another profile the default. `login --profile <name>` creates one, and `--profile <name>` or SPROUTBOAT_PROFILE picks one for a single command. |
33
35
  | `complete` | `[bash | zsh | fish]` | Print a shell completion script for the top-level commands (defaults to $SHELL). |
34
36
 
35
37
  ```
36
- usage: sproutboat <init [name] | check [project-dir] | types [project-dir] | dev [project-dir] [--port <n>] [--no-watch] [--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] [--deploy-files] [--with-restore] [--emit-only] | toolchain doctor [--json] | deploy [project-dir] [--dry-run] [--artifact <dir>] [--no-wait] [--no-provision] | versions <list | view <version-id>> [project-dir] | rollback <version-id> [project-dir] | 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]] | 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>> | r2 <list | create <name> | info <name> | rename <name> <new> | delete <name>> | queues <list | create <name> | info <name> | rename <name> <new> | delete <name>> | domains <list | add <host> | verify <host> | delete <host>> [project-dir] | secrets <list | put <NAME> [--value <value>] | delete <NAME>> [project-dir] | delete [project-dir] [--name <project>] --yes | login [--api-url <url>] [--token <token>] | logout [--api-url <url>] | whoami | complete [bash | zsh | fish]>
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]>
37
39
  ```
38
40
 
41
+ ## Global flags
42
+
43
+ | Flag | Purpose |
44
+ | --- | --- |
45
+ | `-c, --config` | Path to the project's config file |
46
+ | ` --cwd` | Run as if sproutboat was started in this directory |
47
+ | `-e, --env` | Environment for `dev`: selects the .dev.vars.<name> and .env.<name> files |
48
+ | ` --env-file` | Path to an .env file to load; repeat it, and later files override earlier ones |
49
+ | `-h, --help` | Show help |
50
+ | ` --profile` | Use a stored login profile (default: SPROUTBOAT_PROFILE, else the active profile) |
51
+ | `-v, --version` | Show version number |
52
+
39
53
  ## Environment variables
40
54
 
41
55
  | Variable | Purpose |
42
56
  | --- | --- |
43
- | `SPROUTBOAT_API_URL` | Control-plane URL. Overrides the saved active endpoint. |
44
- | `SPROUTBOAT_TOKEN` | API token. Overrides the saved credential for the endpoint. |
57
+ | `SPROUTBOAT_API_URL` | Control-plane URL, the same as --api-url (which wins). Overrides the active profile's endpoint; it must match SPROUTBOAT_PROFILE when both are set. |
58
+ | `SPROUTBOAT_PROFILE` | Stored login profile to use, like --profile (which wins). Its endpoint and credential apply unless --api-url or SPROUTBOAT_API_TOKEN overrides them. |
59
+ | `SPROUTBOAT_API_TOKEN` | API token (an account API key on the Cloud), like CLOUDFLARE_API_TOKEN. Always wins over the stored login; use it in CI. |
60
+ | `SPROUTBOAT_TOKEN` | Older name for SPROUTBOAT_API_TOKEN; SPROUTBOAT_API_TOKEN wins when both are set. |
61
+ | `SPROUTBOAT_ACCOUNT` | Cloud team (its URL name) a deploy targets, like account_id. Overridden by --account and the account field of sproutboat.jsonc. |
62
+ | `SPROUTBOAT_SPROUTD` | Operators only: sproutd checkout whose compile-app turns the app into a Cloud module (same as --sproutd). Regular deploys are built by the Cloud and do not need it. |
63
+ | `SPROUTBOAT_GENERATION` | Built sproutd generation to compile against (same as --generation). |
45
64
  | `SPROUTBOAT_ZIG` | Path to a Zig binary to use instead of downloading the pinned one. |
46
65
  | `SPROUTBOAT_ZIG_URL` | URL to download the pinned Zig tarball from, instead of a Zig community mirror. The sha256 is still enforced. |
47
66
  | `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. |
@@ -50,6 +69,7 @@ usage: sproutboat <init [name] | check [project-dir] | types [project-dir] | dev
50
69
  | `AR` | Archiver used to assemble uSockets.a for a host build (default `ar` if present, else Zig's). |
51
70
  | `SPROUTBOAT_COMPILE_TIMEOUT_MS` | Porffor compile timeout in ms (default 600000). |
52
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). |
72
+ | `SPROUTBOAT_RELEASE` | Same as `build --standalone --release`: link with LTO. Any value but empty, `0` or `false` turns it on. |
53
73
  | `SPROUTBOAT_FLTO` | Overrides link-time optimization on alpha 22 builds: `1` turns it on, `0` turns it off. |
54
74
  | `SPROUTBOAT_TOOLCHAIN_CACHE` | Managed Porffor and compiler cache root (default ~/.cache/sproutboat). |
55
75
  | `SPROUTBOAT_PORFFOR_DIR` | Contributor override for an existing Porffor source checkout. Required files are validated before use. |
@@ -57,7 +77,7 @@ usage: sproutboat <init [name] | check [project-dir] | types [project-dir] | dev
57
77
  | `SPROUTBOAT_CLI_VERSION` | CLI version embedded by the release executable build. Not normally set by users. |
58
78
  | `SPROUTBOAT_VARS_JSON` | JSON object of baked `vars` (UPPER_SNAKE -> string), read by the wrapper when generating the sprout module. |
59
79
  | `SPROUTBOAT_BINDINGS_JSON` | The artifact's bindings.json, read by the wrapper to emit the `__sbInstallBindings` line. |
60
- | `SPROUTBOAT_CONFIG_DIR` | Directory for credentials.json (default ~/.config/sproutboat). |
80
+ | `SPROUTBOAT_CONFIG_DIR` | Directory for credentials.json (default ~/Library/Preferences/sproutboat on macOS, ~/.config/sproutboat elsewhere; the old macOS place is still read). |
61
81
  | `NO_COLOR` | When set, disables coloured terminal output (https://no-color.org). Output is also plain whenever stdout is not a TTY. |
62
82
  | `SPROUTBOAT_NO_UPDATE_CHECK` | When set, skips the once-a-day npm check for a newer `sproutboat` release (also skipped when CI is set). |
63
83
  | `XDG_CONFIG_HOME` | Base for the default credentials dir when SPROUTBOAT_CONFIG_DIR is unset. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sproutboat",
3
- "version": "0.53.1",
3
+ "version": "0.54.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",
@@ -64,9 +64,9 @@
64
64
  "@sproutboat/artifact": "^0.3.0",
65
65
  "@sproutboat/assets": "^0.3.0",
66
66
  "@sproutboat/config": "^0.6.0",
67
- "@sproutboat/runtime": "^0.43.0",
67
+ "@sproutboat/runtime": "^0.44.0",
68
68
  "@sproutboat/toolchain": "^0.24.2",
69
- "@sproutboat/wire": "^0.22.3"
69
+ "@sproutboat/wire": "^0.22.4"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@oxlint/plugins": "1.85.0",
@@ -78,10 +78,10 @@
78
78
  "typescript": "7.0.2"
79
79
  },
80
80
  "optionalDependencies": {
81
- "@sproutboat/cli-darwin-arm64": "0.53.1",
82
- "@sproutboat/cli-darwin-x64": "0.53.1",
83
- "@sproutboat/cli-linux-arm64": "0.53.1",
84
- "@sproutboat/cli-linux-x64": "0.53.1"
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"
85
85
  },
86
86
  "engines": {
87
87
  "node": ">=18"
@@ -0,0 +1,69 @@
1
+ import { parseConfig, type ConfigValidation } from "./config";
2
+ import { isString, jsonObject, parseJsonValue, type JsonObject, type JsonValue } from "./json";
3
+
4
+ /** Blank out comments and trailing commas the way parseConfig does, then parse. Undefined when it is not a JSON object. */
5
+ function parseLoose(source: string): JsonObject | undefined {
6
+ let json = "";
7
+ let quoted = false;
8
+ for (let index = 0; index < source.length; index++) {
9
+ const character = source[index];
10
+ if (character === '"' && source[index - 1] !== "\\") quoted = !quoted;
11
+ if (!quoted && character === "/" && source[index + 1] === "/") {
12
+ index = source.indexOf("\n", index);
13
+ if (index < 0) break;
14
+ json += "\n";
15
+ } else if (!quoted && character === "/" && source[index + 1] === "*") {
16
+ index = source.indexOf("*/", index + 2);
17
+ if (index < 0) return undefined;
18
+ index++;
19
+ } else json += character;
20
+ }
21
+ try {
22
+ return jsonObject(parseJsonValue(json.replace(/,\s*([}\]])/g, "$1")));
23
+ } catch {
24
+ return undefined;
25
+ }
26
+ }
27
+
28
+ const ACCOUNT_RE = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
29
+
30
+ /**
31
+ * The `account` field of sproutboat.jsonc (like Wrangler's `account_id`): the Cloud team a deploy targets.
32
+ * It belongs to the CLI, not to the app, so the shared config schema does not know it.
33
+ */
34
+ export function configAccount(source: string): string | undefined {
35
+ const account = parseLoose(source)?.account;
36
+ return isString(account) && account !== "" ? account : undefined;
37
+ }
38
+
39
+ /**
40
+ * `migrations_dir` (like Wrangler's, but top level: a Sproutboat app has its D1 databases by binding name only): the
41
+ * directory `d1 migrations` keeps its .sql files in, relative to the project. Defaults to `migrations`. Undefined when
42
+ * absent; an invalid value is reported by parseProjectConfig.
43
+ */
44
+ export function configMigrationsDir(source: string): string | undefined {
45
+ const dir = parseLoose(source)?.migrations_dir;
46
+ return isString(dir) && dir !== "" ? dir : undefined;
47
+ }
48
+
49
+ const MIGRATIONS_DIR_ERROR = 'migrations_dir must be a relative folder inside the project (for example "migrations")';
50
+ const validMigrationsDir = (dir: JsonValue | undefined): boolean =>
51
+ isString(dir) && dir !== "" && !dir.startsWith("/") && !/^[A-Za-z]:/.test(dir) && !dir.split(/[\\/]/).includes("..");
52
+
53
+ /**
54
+ * parseConfig for a file that may carry the CLI-owned fields `account` and `migrations_dir`: they are validated here and
55
+ * removed before the shared schema sees them.
56
+ */
57
+ export function parseProjectConfig(source: string): ConfigValidation {
58
+ const loose = parseLoose(source);
59
+ if (loose === undefined || !("account" in loose || "migrations_dir" in loose)) return parseConfig(source);
60
+ const errors: string[] = [];
61
+ if ("account" in loose && (!isString(loose.account) || !ACCOUNT_RE.test(loose.account)))
62
+ errors.push("account must be the URL name of a team (lowercase letters, digits and dashes)");
63
+ if ("migrations_dir" in loose && !validMigrationsDir(loose.migrations_dir)) errors.push(MIGRATIONS_DIR_ERROR);
64
+ if (errors.length > 0) return { ok: false, errors };
65
+ const rest = Object.fromEntries(
66
+ Object.entries(loose).filter(([key]) => key !== "account" && key !== "migrations_dir"),
67
+ );
68
+ return parseConfig(JSON.stringify(rest));
69
+ }
package/src/build.ts CHANGED
@@ -7,7 +7,7 @@ import { queueSettings, resourceRefs, type SproutboatConfig } from "./config";
7
7
  import { ensureSqliteObject } from "./sqlite";
8
8
  import { sqliteStamp } from "./sqlite";
9
9
  import { bearsslStamp, ensureBearssl } from "./bearssl";
10
- import { compileSprout, loadPrelude, porfforChannel, type Transport } from "./compile";
10
+ import { compileSprout, loadPrelude, ltoEnabled, porfforChannel, type Transport } from "./compile";
11
11
  import { compileCached } from "./compile-cache";
12
12
  import type { BuildTarget } from "./targets";
13
13
  import {
@@ -51,6 +51,13 @@ export type BuildInput = {
51
51
  * for the fast path deliberately.
52
52
  */
53
53
  optimize?: "dev" | "release";
54
+ /**
55
+ * Link-time optimization for the alpha 22 standalone build. Defaults to on (`-flto -O3`, the release
56
+ * build). `false` keeps -O3 per unit but links without LTO: about 1.5 to 2x faster cold, a link of
57
+ * ~90 MB instead of 1.1 to 1.6 GB, and a binary that serves fewer requests per second. A separate
58
+ * input to the compile cache key, so the two kinds of binary never share an entry.
59
+ */
60
+ lto?: boolean;
54
61
  /** Reuse a previously compiled native sprout when only sidecar assets changed. */
55
62
  reuseSproutPath?: string;
56
63
  /** An isolated artifact directory, used by dev candidates to avoid mixing snapshots. */
@@ -297,6 +304,7 @@ export async function emitGenerated(input: BuildInput): Promise<{ generatedPath:
297
304
  appName: input.config.name,
298
305
  assets: bakedAssets,
299
306
  optimize: input.optimize,
307
+ lto: input.lto,
300
308
  versionMetadata,
301
309
  emitOnly: true,
302
310
  withRestore: input.withRestore,
@@ -334,6 +342,8 @@ export async function buildArtifact(input: BuildInput): Promise<BuildOutput> {
334
342
  generatedSource,
335
343
  target,
336
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,
337
347
  toolchain: toolchainStamp(),
338
348
  toolchainPackageVersion,
339
349
  porffor: [PORFFOR_COMMIT_FULL, PORFFOR_ARCHIVE_SHA256],
@@ -390,6 +400,7 @@ export async function buildArtifact(input: BuildInput): Promise<BuildOutput> {
390
400
  ],
391
401
  extraCflags: tls ? ["-I", tls.includeDir] : [],
392
402
  optimize: input.optimize,
403
+ lto: input.lto,
393
404
  versionMetadata,
394
405
  withRestore: input.withRestore,
395
406
  });