sproutboat 0.53.1 → 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 +69 -1
- package/README.md +122 -9
- package/SURFACE.md +42 -22
- package/package.json +20 -12
- package/scripts/embed-templates.ts +87 -0
- package/src/account-field.ts +100 -0
- package/src/build.ts +13 -2
- package/src/cloud-bindings.ts +227 -0
- package/src/cloud-deploy.ts +794 -0
- package/src/cloud-domains.ts +55 -0
- package/src/cloud-fake.ts +719 -0
- package/src/cloud-secrets.ts +121 -0
- package/src/cloud.ts +375 -0
- package/src/compile.ts +34 -18
- package/src/credentials.ts +455 -52
- package/src/d1-cli.ts +455 -0
- package/src/data-errors.ts +139 -0
- package/src/deployments-cli.ts +184 -0
- package/src/dev.ts +67 -4
- package/src/fake-login.ts +28 -0
- package/src/global-flags.ts +113 -0
- package/src/init-package.ts +2 -1
- package/src/init.ts +328 -0
- package/src/main.ts +1247 -193
- package/src/options.ts +242 -0
- package/src/r2-objects.ts +365 -0
- package/src/run.ts +12 -6
- package/src/secrets-bulk.ts +51 -0
- package/src/sql-split.ts +68 -0
- package/src/standalone-build.ts +21 -3
- package/src/surface.ts +192 -64
- package/src/target.ts +95 -0
- package/src/templates-embedded.ts +72 -0
- package/src/templates.ts +53 -0
- package/src/toolchain.ts +3 -0
- package/src/types-cmd.ts +193 -0
- package/src/types-gen.ts +104 -68
- package/types/cloudflare-workers.d.ts +43 -0
- package/types/sproutboat.d.ts +160 -3
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,73 @@ 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
|
+
|
|
45
|
+
## [0.54.0] - 2026-11-11
|
|
46
|
+
|
|
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).
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
|
|
51
|
+
- **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`.
|
|
52
|
+
- **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.
|
|
53
|
+
- **`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.
|
|
54
|
+
- **`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.
|
|
55
|
+
- **`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.
|
|
56
|
+
- **`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.
|
|
57
|
+
- **`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.
|
|
58
|
+
- **`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.
|
|
59
|
+
- **`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.
|
|
60
|
+
- **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.
|
|
61
|
+
|
|
62
|
+
- **`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`.
|
|
63
|
+
- **`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.
|
|
64
|
+
- **`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.
|
|
65
|
+
|
|
66
|
+
### Changed
|
|
67
|
+
|
|
68
|
+
- The docs link in `--help` now points to https://sproutboat.com/docs (docs.sproutboat.com redirects there).
|
|
69
|
+
- **`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.
|
|
70
|
+
- **The default endpoint is `https://dash.sproutboat.com`** (it was `https://dashboard.sproutboat.com`).
|
|
71
|
+
- **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.
|
|
72
|
+
- **`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.
|
|
73
|
+
- **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).
|
|
74
|
+
|
|
75
|
+
### Fixed
|
|
76
|
+
|
|
77
|
+
- **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.
|
|
78
|
+
- **`init`'s help text and `SURFACE.md` now say it adds the `dev`, `deploy` and `check` scripts,** not just `dev` and `deploy`. Wording only.
|
|
79
|
+
|
|
13
80
|
## [0.53.1] - 2026-11-09
|
|
14
81
|
|
|
15
82
|
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 +1844,8 @@ its own package.
|
|
|
1777
1844
|
- Renamed the package to `sproutboat` (was `@sproutboat/cli`); dropped the
|
|
1778
1845
|
`sprout` bin alias in favour of a user-defined shell alias.
|
|
1779
1846
|
|
|
1780
|
-
[Unreleased]: https://github.com/baronunread/sproutboat-cli/compare/v0.
|
|
1847
|
+
[Unreleased]: https://github.com/baronunread/sproutboat-cli/compare/v0.54.0...HEAD
|
|
1848
|
+
[0.54.0]: https://github.com/baronunread/sproutboat-cli/compare/v0.53.1...v0.54.0
|
|
1781
1849
|
[0.32.0]: https://github.com/baronunread/sproutboat-cli/compare/v0.31.0...v0.32.0
|
|
1782
1850
|
[0.31.0]: https://github.com/baronunread/sproutboat-cli/compare/v0.30.0...v0.31.0
|
|
1783
1851
|
[0.24.0]: https://github.com/baronunread/sproutboat-cli/compare/v0.23.0...v0.24.0
|
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="
|
|
3
|
+
<img src="docs/logo-light.svg" alt="sproutboat" width="220" height="34">
|
|
4
4
|
</picture>
|
|
5
5
|
|
|
6
6
|
https://sproutboat.com
|
|
@@ -54,10 +54,24 @@ 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
|
|
60
|
-
npx sproutboat login --api-url
|
|
74
|
+
npx sproutboat login # one approval in the browser; add --api-url for a self-hosted box
|
|
61
75
|
npm run deploy # or: npx sproutboat deploy
|
|
62
76
|
```
|
|
63
77
|
|
|
@@ -68,10 +82,109 @@ not pinned to anything, so keep it for `init`, `login` and `whoami`:
|
|
|
68
82
|
npm i -g sproutboat # then: sproutboat init, sproutboat whoami, ...
|
|
69
83
|
```
|
|
70
84
|
|
|
71
|
-
`login` is one-time. It writes
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
`
|
|
85
|
+
`login` is one-time. It writes the credential to `credentials.json` in the user
|
|
86
|
+
config directory (mode 0600), keyed by API URL, so you can hold logins for
|
|
87
|
+
several instances at once. Profile names and the default profile are in `profiles.json` beside it;
|
|
88
|
+
`credentials.json` keeps the format older CLIs read, so projects pinned to an older version keep working. The directory follows Wrangler: `SPROUTBOAT_CONFIG_DIR`
|
|
89
|
+
or `XDG_CONFIG_HOME` if set, else `~/Library/Preferences/sproutboat` on macOS and
|
|
90
|
+
`~/.config/sproutboat` elsewhere (the old macOS place is still read).
|
|
91
|
+
|
|
92
|
+
### Sproutboat Cloud
|
|
93
|
+
|
|
94
|
+
The default endpoint is `https://dash.sproutboat.com`, the Sproutboat Cloud.
|
|
95
|
+
`login` there is the device-code flow, like `gh auth login`:
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
sproutboat login # prints a code and a link, opens the browser when it can
|
|
99
|
+
sproutboat whoami # the team, the key and what it may do
|
|
100
|
+
sproutboat deploy # uploads the app, waits for a host, prints the live URL
|
|
101
|
+
sproutboat logout # deletes the key on the server, then the local copy
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Open the link on any device where you are signed in, pick the team, check the
|
|
105
|
+
machine name and Approve. That mints an account API key named
|
|
106
|
+
`CLI on <hostname>` for that team, with your role capped at Developer. It shows
|
|
107
|
+
up under Settings, API keys, where you can revoke it. `--account <team>`
|
|
108
|
+
preselects the team on the approval page.
|
|
109
|
+
|
|
110
|
+
- **Which target.** Like Wrangler, there is one target, chosen by your
|
|
111
|
+
credentials: the endpoint you are logged in to (`--api-url`, then
|
|
112
|
+
`SPROUTBOAT_API_URL`, then the saved active endpoint, then the default).
|
|
113
|
+
The CLI asks the endpoint what it is (`GET /v1/config`) and deploys the Cloud
|
|
114
|
+
way to a Cloud and the Community way to a self-hosted box. There is no
|
|
115
|
+
`--cloud` flag.
|
|
116
|
+
- **CI.** `SPROUTBOAT_API_TOKEN` (an account API key, like `CLOUDFLARE_API_TOKEN`)
|
|
117
|
+
always wins over a stored login. `SPROUTBOAT_TOKEN` is the older name. With no
|
|
118
|
+
terminal and no token, `deploy` fails and names `SPROUTBOAT_API_TOKEN`; in a
|
|
119
|
+
terminal it starts the login itself.
|
|
120
|
+
- **Which team.** `account` in `sproutboat.jsonc` (like `account_id`), else
|
|
121
|
+
`--account`, else `SPROUTBOAT_ACCOUNT`, else your only stored team, else a
|
|
122
|
+
prompt. Precedence is `--account`, the config field, then the environment
|
|
123
|
+
variable. A key from the environment belongs to one team, so a different
|
|
124
|
+
`account` is an error.
|
|
125
|
+
- **Build.** The Cloud builds your app. `deploy` bundles the project here, uploads
|
|
126
|
+
that one file by its sha256 (it skips the upload when the Cloud already has
|
|
127
|
+
it), asks the Cloud to build it and streams the build log:
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
Bundling src/index.js (212 KiB)
|
|
131
|
+
Uploading source a3f1c9d2e044 (212 KiB)
|
|
132
|
+
Building queued (position 1)
|
|
133
|
+
| porffor: compiled in 1.0 s
|
|
134
|
+
✓ Built in 4.1 s · peak memory 742 MB (cap 1200 MB)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
An identical build is answered from the Cloud's cache. Ctrl-C cancels the
|
|
138
|
+
build. `--no-wait` returns once the build is accepted and prints
|
|
139
|
+
`/v1/builds/<id>`. A queued build shows why it is held back (such as waiting for memory) and keeps
|
|
140
|
+
waiting. A build that fails prints the reason and its peak memory, and exits 1; a source
|
|
141
|
+
is limited to 8 MiB. `--module app.so` and `--sproutd` with `--generation`
|
|
142
|
+
are operator paths for a prebuilt or locally compiled module; a team key is
|
|
143
|
+
refused with "modules are built by the Cloud", and regular deploys never need
|
|
144
|
+
them.
|
|
145
|
+
- **Result.** The first deploy creates the app (`Created`), later ones say
|
|
146
|
+
`Updated`. The app is served at `https://<app>.<team>.sproutboat.app`, and the
|
|
147
|
+
deploy waits until a host has applied the generation it printed.
|
|
148
|
+
`--dry-run` prints the plan and says it would build in the Cloud, without
|
|
149
|
+
uploading; `--no-wait` skips the wait.
|
|
150
|
+
- **Secrets.** As with Wrangler, secrets belong to the app and a deploy never
|
|
151
|
+
wipes them. `sproutboat secrets put NAME` (value on stdin, or `--value`),
|
|
152
|
+
`secrets list` and `secrets delete NAME` act on the deployed app at once; they
|
|
153
|
+
need the `secret:write` and `secret:list` permissions and say so on a 403. An app
|
|
154
|
+
that is not deployed yet has nowhere to keep them: deploy first, or pass
|
|
155
|
+
`deploy --secrets-file <file>` (`.dev.vars` or `.env` syntax, `UPPER_SNAKE` names)
|
|
156
|
+
on the first deploy; its values are merged into the app's secrets and nothing
|
|
157
|
+
else is removed. A build deploy stores its spec in plaintext, so the secrets
|
|
158
|
+
are never in it: once the build is ready they are set with one
|
|
159
|
+
`PATCH /v1/apps/<app>/secrets`, and the deploy waits for that newer
|
|
160
|
+
generation. On a first deploy the app is briefly live before its secrets
|
|
161
|
+
land, and `--secrets-file` cannot be combined with `--no-wait`. A deploy never reads `.dev.vars` on its
|
|
162
|
+
own (the Community deploy does not either): name the file. Production
|
|
163
|
+
values do not belong in `.dev.vars`; keep a separate file.
|
|
164
|
+
- **Service bindings.** `services: [{ "binding": "PEER", "service": "quote-service" }]`
|
|
165
|
+
binds to the Cloud app of that name in the same team. `deploy` looks the app
|
|
166
|
+
up first and refuses, before uploading anything, when it is not deployed
|
|
167
|
+
(deploy it first, with `sproutboat deploy` in its directory), when it is the
|
|
168
|
+
app itself, or when `compatibility_date` is older than `2026-11-18`, the date
|
|
169
|
+
from which the platform runtime serves service bindings between Cloud apps.
|
|
170
|
+
Hosts need the matching sproutd support.
|
|
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.
|
|
185
|
+
|
|
186
|
+
For self-hosted (Community) endpoints, `login` keeps the browser flow, and for
|
|
187
|
+
CI or an agent you can skip it and set `SPROUTBOAT_API_URL` + `SPROUTBOAT_API_TOKEN`.
|
|
75
188
|
|
|
76
189
|
## Commands
|
|
77
190
|
|
|
@@ -79,8 +192,8 @@ The everyday loop:
|
|
|
79
192
|
|
|
80
193
|
| Command | What it does |
|
|
81
194
|
| --- | --- |
|
|
82
|
-
| `init [name]` | Scaffold
|
|
83
|
-
| `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 |
|
|
84
197
|
| `deploy` | Build, provision bindings, upload, wait until the URL serves |
|
|
85
198
|
| `tail [filters]` | Live request events, console output and exceptions, like `wrangler tail` |
|
|
86
199
|
| `rollback <id>` | Re-activate a previous version |
|
|
@@ -200,7 +313,7 @@ files in the project and from its own `node_modules`, and may also export
|
|
|
200
313
|
is a global, not a parameter. Binding calls are synchronous until the compatibility
|
|
201
314
|
date that makes each one return a Promise, as on Workers (D1 from 2026-10-24, KV from
|
|
202
315
|
2026-10-25, R2 from 2026-10-26); `await` works at every date, so always await them.
|
|
203
|
-
[`examples
|
|
316
|
+
[`examples/`](examples) has one small example per binding.
|
|
204
317
|
|
|
205
318
|
For objects larger than one request, use resumable R2 multipart uploads. The
|
|
206
319
|
[large-object guide](docs/r2-large-objects.md) covers part sizing, browser
|
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.
|
|
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 and
|
|
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` | `[
|
|
15
|
-
| `dev` | `[project-dir] [--port <n>] [--no-watch] [--env <name>] [--env-file <file>]...` | Run the project on this machine
|
|
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
|
-
| `build` | `[project-dir] [--target host|linux-x64|linux-arm64] [--standalone] [--deploy-files] [--with-restore] [--emit-only]` |
|
|
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),
|
|
20
|
-
| `
|
|
21
|
-
| `
|
|
22
|
-
| `
|
|
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
|
|
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]
|
|
28
|
-
| `secrets` | `<list | put <NAME> [--value <value>] | delete <NAME
|
|
29
|
-
| `
|
|
30
|
-
| `
|
|
31
|
-
| `
|
|
32
|
-
| `
|
|
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
|
+
| `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 [
|
|
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]>
|
|
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
|
|
44
|
-
| `
|
|
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. |
|
|
@@ -49,7 +68,8 @@ usage: sproutboat <init [name] | check [project-dir] | types [project-dir] | dev
|
|
|
49
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). |
|
|
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
|
-
| `SPROUTBOAT_PORFFOR_CHANNEL` | Porffor channel for standalone builds: `alpha-
|
|
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
|
+
| `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.
|
|
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
|
|
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
|
|
61
|
-
"kitchen-sink:
|
|
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.
|
|
68
|
-
"@sproutboat/toolchain": "^0.
|
|
69
|
-
"@sproutboat/wire": "^0.22.
|
|
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.
|
|
82
|
-
"@sproutboat/cli-darwin-x64": "0.
|
|
83
|
-
"@sproutboat/cli-linux-arm64": "0.
|
|
84
|
-
"@sproutboat/cli-linux-x64": "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
|
+
}
|