sproutboat 0.8.0 → 0.10.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 +365 -0
- package/README.md +49 -4
- package/SURFACE.md +12 -5
- package/bin/sproutboat.cjs +32 -0
- package/package.json +27 -10
- package/scripts/build-cli.ts +44 -0
- package/src/assets.ts +4 -121
- package/src/broker.ts +4 -1139
- package/src/build.ts +31 -16
- package/src/compile.ts +80 -28
- package/src/config.ts +4 -379
- package/src/dev.ts +263 -60
- package/src/json.ts +3 -38
- package/src/main.ts +269 -16
- package/src/manifest.ts +4 -167
- package/src/patch-porffor.ts +5 -144
- package/src/porffor-toolchain.ts +5 -0
- package/src/report.ts +5 -7
- package/src/source.ts +4 -44
- package/src/surface.ts +33 -3
- package/src/toolchain.ts +228 -44
- package/src/types-gen.ts +93 -0
- package/src/wrap.ts +5 -267
- package/types/sproutboat.d.ts +248 -0
- package/src/native-fetch-prelude.js +0 -950
- package/src/transport-broker.js +0 -290
- package/src/transport-embedded.js +0 -1567
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `sproutboat` (the CLI) are documented here. Format
|
|
4
|
+
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning
|
|
5
|
+
follows [SemVer](https://semver.org/), pre-1.0 so a minor bump can still carry
|
|
6
|
+
a breaking change.
|
|
7
|
+
|
|
8
|
+
Reconstructed from git history on 2026-09-03 for everything through v0.4.11;
|
|
9
|
+
maintained going forward by the `release` skill.
|
|
10
|
+
|
|
11
|
+
## [Unreleased]
|
|
12
|
+
|
|
13
|
+
## [0.10.0] - 2026-09-11
|
|
14
|
+
### Added
|
|
15
|
+
- Per-platform native CLI: `npm install sproutboat` pulls a prebuilt binary for
|
|
16
|
+
the host (`@sproutboat/cli-{darwin,linux}-{arm64,x64}`) behind a thin launcher
|
|
17
|
+
that preserves signals and embeds its own version, so the CLI runs without Bun
|
|
18
|
+
on `PATH`.
|
|
19
|
+
- Porffor is fetched on the first build, not installed as a dependency: the
|
|
20
|
+
pinned commit is downloaded into `~/.cache/sproutboat`, SHA-256 verified, and
|
|
21
|
+
patched there. Installation clones nothing and runs no Git, Make or compiler.
|
|
22
|
+
- `request.cf.clientIp` in standalone builds, from a server-set
|
|
23
|
+
`x-sb-remote-addr`, with `SB_TRUSTED_PROXIES` for `X-Forwarded-For` resolution
|
|
24
|
+
behind a reverse proxy (baronunread/sproutboat#163).
|
|
25
|
+
- `env.<D1>.backup(name?)` — an online, integrity-checked single-file snapshot
|
|
26
|
+
of a D1 database via `VACUUM INTO`, on both the embedded and broker transports
|
|
27
|
+
(baronunread/sproutboat#164).
|
|
28
|
+
- Rate Limiting binding: `ratelimiters: [{ binding, limit, period }]` in
|
|
29
|
+
`sproutboat.jsonc` gives `env.<NAME>.limit({ key }) -> { success }`, a
|
|
30
|
+
fixed-window counter on both transports (baronunread/sproutboat#69).
|
|
31
|
+
- `crypto.subtle` subset: `digest` (SHA-256/384/512) and HMAC
|
|
32
|
+
`importKey` / `sign` / `verify`, backed by reference SHA-2 as inline C so it
|
|
33
|
+
works on both transports (baronunread/sproutboat#133). No ECDSA or AES yet.
|
|
34
|
+
- `crypto.scryptVerify(password, salt, expected, { N, r, p })` — a verify-only
|
|
35
|
+
scrypt (RFC 7914) for migrating password hashes made by Node/Bun `scrypt`
|
|
36
|
+
(baronunread/sproutboat#153). Not a blessed KDF for new credentials.
|
|
37
|
+
- `x-sb-cpu-ms`: per-invocation CPU time, carried on the handler response.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
- `303` (and every other status not in Porffor's table) no longer resets the
|
|
41
|
+
connection on standalone builds (baronunread/sproutboat#156).
|
|
42
|
+
- Handler `console.log` / `console.error` reach stderr, unbuffered, in
|
|
43
|
+
standalone builds instead of vanishing (baronunread/sproutboat#165).
|
|
44
|
+
- `sproutboat dev` no longer leaves the previous sprout running across a
|
|
45
|
+
rebuild, delivers no triggers to a candidate that failed to start, and cleans
|
|
46
|
+
up every failed setup path on rebuild and on shutdown.
|
|
47
|
+
- The Zig toolchain cache is hardened against a partial or concurrent download.
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
- Porffor pin bumped alpha-4 → **alpha-5** (`1f4ae4ae`); the same `UWS_COMMIT`,
|
|
51
|
+
so no uWebSockets re-vendor.
|
|
52
|
+
- Config parsing, the artifact manifest, the binding broker, the wire assets and
|
|
53
|
+
the whole native-fetch runtime (prelude + transports) now come from published
|
|
54
|
+
`@sproutboat/*` packages; the CLI keeps thin re-export shims. The Porffor pin
|
|
55
|
+
and its source patches moved to `@sproutboat/toolchain`.
|
|
56
|
+
- Broker cron / queue / alarm delivery is gated by a local signal, so only a
|
|
57
|
+
promoted candidate runs timers.
|
|
58
|
+
|
|
59
|
+
### Performance
|
|
60
|
+
- Embedded transport caches prepared statements per database (FIFO, 32/db)
|
|
61
|
+
instead of recompiling the SQL on every binding op — the op boundary drops
|
|
62
|
+
from ~0.6 ms to tens of µs (baronunread/sproutboat#155).
|
|
63
|
+
- The broker's due-queue and due-alarm polls are indexed.
|
|
64
|
+
|
|
65
|
+
### Docs
|
|
66
|
+
- `docs/standalone.md` documents the single-threaded execution model and the
|
|
67
|
+
`SO_REUSEPORT` multi-process recipe for scaling past one core
|
|
68
|
+
(baronunread/sproutboat#154); the embedded transport also sets
|
|
69
|
+
`PRAGMA busy_timeout` so shared-data-dir writers wait instead of failing.
|
|
70
|
+
Also: the client-IP behaviour and backing up D1.
|
|
71
|
+
|
|
72
|
+
## [0.9.0] - 2026-09-07
|
|
73
|
+
### Added
|
|
74
|
+
- KV content management: `kv key get`, `put`, `delete`, and `list`; bounded
|
|
75
|
+
`kv bulk get`, `put`, and `delete`; and cursor-paginated `kv export` files
|
|
76
|
+
that can be restored with `kv bulk put`.
|
|
77
|
+
- CLI integration coverage for pagination, bounded batches, text and file
|
|
78
|
+
output, overwrite refusal, and cleanup after failed exports.
|
|
79
|
+
- `sproutboat-env.d.ts` generation from project bindings.
|
|
80
|
+
- Focused runnable examples for every supported binding.
|
|
81
|
+
|
|
82
|
+
### Fixed
|
|
83
|
+
- Static assets keep arbitrary binary bytes when they travel through the broker.
|
|
84
|
+
- KV exports use a permission-restricted temporary file and move into place only
|
|
85
|
+
after a complete export. Failed exports leave neither partial output nor a
|
|
86
|
+
temporary file behind.
|
|
87
|
+
- First-host builds no longer require Git or Make.
|
|
88
|
+
|
|
89
|
+
### Changed
|
|
90
|
+
- Development binaries compile with Porffor `-O0` for faster iteration.
|
|
91
|
+
- The installation docs state that the CLI requires Bun and should be invoked
|
|
92
|
+
with `bunx` rather than `npx`.
|
|
93
|
+
|
|
94
|
+
## [0.8.0] — 2026-09-07
|
|
95
|
+
### Added
|
|
96
|
+
- `sproutboat build --standalone`: one executable that carries its own bindings.
|
|
97
|
+
SQLite is compiled into the sprout, so a ~2 MB file serves KV, D1, R2, queues,
|
|
98
|
+
Durable Objects, alarms, analytics and assets with nothing beside it on disk —
|
|
99
|
+
no Bun, no broker, no control plane. State lives in `<name>.data/store.sqlite`
|
|
100
|
+
plus `d1/<binding>.sqlite`, the same layout `sproutboat dev` writes, which is
|
|
101
|
+
what lets one conformance suite hold both backends to the same behaviour.
|
|
102
|
+
Secrets come from the environment (then `<data>/secrets.json`) and a missing
|
|
103
|
+
one refuses the boot, listing every name at once, rather than throwing on the
|
|
104
|
+
first request that needs it. Cron ticks, queue batches and DO alarms run on
|
|
105
|
+
in-process timers; assets are baked in, capped at 8 MB.
|
|
106
|
+
- Outbound TLS from a standalone binary. `fetch("https://…")` verifies against
|
|
107
|
+
curl's Mozilla-derived root set via BearSSL, linked in beside SQLite
|
|
108
|
+
(1.86 → 2.02 MB). `SB_CA_BUNDLE` adds a private or corporate CA to that set;
|
|
109
|
+
it only ever adds trust, and nothing disables verification.
|
|
110
|
+
- Durable Object alarms: `setAlarm` / `getAlarm` / `deleteAlarm` and the
|
|
111
|
+
`alarm()` handler. At most one alarm is pending per object and a later
|
|
112
|
+
`setAlarm` replaces it, matching Workers. Delivery claims before running, so
|
|
113
|
+
an `alarm()` that schedules its own next run is not erased by the delivery
|
|
114
|
+
that invoked it.
|
|
115
|
+
- Service bindings: `env.<BINDING>.fetch()` reaches another deployment on the
|
|
116
|
+
same node through the edge on loopback. This is the CLI half; a binding that
|
|
117
|
+
resolves to nothing reports the target as undeployed rather than failing as a
|
|
118
|
+
502.
|
|
119
|
+
- Binary values in the binding frame. An R2 object body now travels beside the
|
|
120
|
+
JSON rather than encoded inside it.
|
|
121
|
+
- `SB_FETCH_MAX_BYTES` (32 MiB default) caps an outbound response body. An
|
|
122
|
+
unbounded upstream could previously drive a sprout's memory to whatever it
|
|
123
|
+
chose to send.
|
|
124
|
+
|
|
125
|
+
### Fixed
|
|
126
|
+
- `d1.exec` ran only the first statement of a multi-statement script, so a
|
|
127
|
+
schema built in one `exec()` call silently created only its first table.
|
|
128
|
+
- `r2.get` / `head` / `put` / `list` returned flat fields where the shim reads
|
|
129
|
+
`r.object`, and `ae.query` omitted the `count` its caller reads.
|
|
130
|
+
- Trigger authentication accepted *any* caller when no token was configured.
|
|
131
|
+
The hole predates this release; a standalone binary listening on a public
|
|
132
|
+
interface is what made it reachable.
|
|
133
|
+
- Binary R2 values were corrupted in transit — `0x08` and `0x0c` arrived as
|
|
134
|
+
`b` and `f`.
|
|
135
|
+
- A retried binding call could apply a write twice. Every request now carries an
|
|
136
|
+
id and the broker replays the cached reply for a repeat of a mutating op
|
|
137
|
+
instead of performing it again.
|
|
138
|
+
|
|
139
|
+
### Changed
|
|
140
|
+
- A dropped broker connection is retried four times with 0/5/25/100 ms backoff
|
|
141
|
+
instead of failing after one attempt, which covers a broker restart mid-call.
|
|
142
|
+
- The bundled (Bun) standalone backend is gone. The embedded one passes the same
|
|
143
|
+
suite at 1.9 MB against 63 MB, and TLS removed its last real advantage.
|
|
144
|
+
- A native-fetch binary cannot see `argv` — Porffor's runtime init calls
|
|
145
|
+
`porf_init(0, NULL)` — so a standalone binary is configured through `PORT`,
|
|
146
|
+
`SB_DATA_DIR` / `SPROUTBOAT_DATA` and the environment only, never flags.
|
|
147
|
+
|
|
148
|
+
### Performance
|
|
149
|
+
- An 8 MB R2 put through the broker went from 255 MB to 149 MB peak RSS.
|
|
150
|
+
- The broker's frame reader no longer re-concatenates its buffer per chunk.
|
|
151
|
+
|
|
152
|
+
## [0.7.0] — 2026-09-06
|
|
153
|
+
### Added
|
|
154
|
+
- `compatibility_date` now reaches the artifact instead of being validated and
|
|
155
|
+
dropped. It is recorded in `manifest.json` as `compatibilityDate` and baked
|
|
156
|
+
into the binary as `__sbCompat`, so a future runtime change can be gated on
|
|
157
|
+
`__sbCompat >= "YYYY-MM-DD"` and old binaries keep the semantics they were
|
|
158
|
+
compiled with. The manifest field is optional and `schemaVersion` stays at 2,
|
|
159
|
+
so artifacts built before this release remain deployable and rollback keeps
|
|
160
|
+
working.
|
|
161
|
+
- A version-skew warning. Control planes advertise `x-sproutboat-control` and
|
|
162
|
+
`x-sproutboat-min-cli` on every `/api/` response; when this CLI is below the
|
|
163
|
+
advertised minimum it says so once per run, instead of leaving the user with
|
|
164
|
+
an unexplained 400. A control plane that predates the handshake sends no
|
|
165
|
+
headers and nothing changes.
|
|
166
|
+
- `CONTRACTS.md`, generated from source in the same style as `SURFACE.md`: the
|
|
167
|
+
broker ops, storage tables, manifest fields and config keys that a release may
|
|
168
|
+
not break, plus golden fixtures for the manifest and the storage format so a
|
|
169
|
+
regression fails a test rather than a user's deployment.
|
|
170
|
+
|
|
171
|
+
## [0.6.1] — 2026-09-06
|
|
172
|
+
### Fixed
|
|
173
|
+
- `sproutboat queues` help (and the generated `SURFACE.md`) claimed queue
|
|
174
|
+
consumers "are not implemented yet". They are implemented, and they run on
|
|
175
|
+
the deployed path as well as under `dev`: the supervisor passes
|
|
176
|
+
`--sprout-url` to the broker, which delivers messages in batches, retries a
|
|
177
|
+
failed or explicitly-retried message after 5s, and stops delivering it after
|
|
178
|
+
5 attempts. The summary now describes what actually happens.
|
|
179
|
+
|
|
180
|
+
### Changed
|
|
181
|
+
- Tooling only, no change to how the CLI behaves: oxfmt is scoped to the JS
|
|
182
|
+
family with the pre-commit hook's glob matched to it, TypeScript moves
|
|
183
|
+
5.9 → 7.0, and the GitHub Actions group is bumped.
|
|
184
|
+
|
|
185
|
+
## [0.6.0] — 2026-09-05
|
|
186
|
+
### Added
|
|
187
|
+
- `bindings.json` now carries `vars` — the baked plain values a sprout was
|
|
188
|
+
built with — so the control plane can show what a version was compiled
|
|
189
|
+
against. They ride along for display only; the broker never serves them,
|
|
190
|
+
they are compiled into the sprout itself. A project whose only binding
|
|
191
|
+
config is `vars` now gets a sidecar written at all, where before it got
|
|
192
|
+
none.
|
|
193
|
+
|
|
194
|
+
### Changed
|
|
195
|
+
- Tooling only, no change to how the CLI behaves: the tree is now formatted
|
|
196
|
+
with oxfmt 0.66.0 (the config landed in 0.5.0 but was never run over the
|
|
197
|
+
tree), markdown is excluded from formatting, a lefthook pre-commit hook
|
|
198
|
+
formats staged files and lints, the last 10 oxlint warnings are cleared,
|
|
199
|
+
and CI gates lint alongside typecheck and test.
|
|
200
|
+
- README rewritten shorter: a logo lockup that survives both GitHub themes
|
|
201
|
+
(`docs/logo-light.svg` / `docs/logo-dark.svg` behind a `<picture>`), the
|
|
202
|
+
everyday commands as a five-row table, and the full command inventory left
|
|
203
|
+
to the generated `SURFACE.md` instead of duplicated by hand.
|
|
204
|
+
|
|
205
|
+
## [0.5.0] — 2026-09-03
|
|
206
|
+
### Added
|
|
207
|
+
- `dev [--port <n>] [--no-watch]` — run the project on this machine against a
|
|
208
|
+
real broker (KV/D1/secrets/etc. all work), rebuilding on save.
|
|
209
|
+
- `build --target host` — compile for the machine doing the build instead of
|
|
210
|
+
cross-compiling for a box; what `dev` uses, and runnable standalone.
|
|
211
|
+
- Handlers may now `import` — relative modules across the project, and npm
|
|
212
|
+
packages from the project's own `node_modules`. The entry point is bundled
|
|
213
|
+
before it reaches Porffor; the capability checks run against that bundled
|
|
214
|
+
output, so a dependency can't reach `process`/`Bun`/`node:*` any more than
|
|
215
|
+
hand-written code can.
|
|
216
|
+
- `sproutboat init` scaffolds a `.gitignore` (`.sproutboat/`, `.dev.vars`,
|
|
217
|
+
`node_modules/`) alongside the project files, unless one already exists.
|
|
218
|
+
|
|
219
|
+
### Fixed
|
|
220
|
+
- Async `fetch` handlers hung indefinitely — `__sbEntry` chained the #28
|
|
221
|
+
CPU-time tag onto the handler's own promise, and Porffor's native-fetch
|
|
222
|
+
server only resolves a promise a handler returns directly, never one
|
|
223
|
+
derived from `.then()`.
|
|
224
|
+
- `new Proxy(...)` compiles under Porffor alpha-4 and then silently ignores
|
|
225
|
+
every trap — a trapped property just reads back `undefined`. `check` now
|
|
226
|
+
rejects it before that reaches a deploy as an unexplained 502.
|
|
227
|
+
- `sproutboat init` crashed with a raw `EEXIST` stack trace, and could leave
|
|
228
|
+
a half-scaffolded project, if `src/index.js` already existed but
|
|
229
|
+
`sproutboat.jsonc` didn't. Both targets are checked before either is
|
|
230
|
+
written.
|
|
231
|
+
- Re-running `sproutboat build` (or `dev`'s rebuild-on-save) could fail to
|
|
232
|
+
link: the artifact directory is content-addressed, so an unchanged rebuild
|
|
233
|
+
targeted the previous binary, which was `chmod 0555` and possibly still
|
|
234
|
+
running.
|
|
235
|
+
- The broker's local dev state directory was never created before opening
|
|
236
|
+
its SQLite file, so a first `sproutboat dev` run failed outright.
|
|
237
|
+
|
|
238
|
+
### Changed
|
|
239
|
+
- Lint: adopted the anti-slop Oxlint plugin and migrated the tree onto it —
|
|
240
|
+
no more bare `unknown`/`Record<string, unknown>` at binding boundaries,
|
|
241
|
+
every non-const type assertion carries a `SAFETY:` comment.
|
|
242
|
+
|
|
243
|
+
## [0.4.11] — 2026-09-02
|
|
244
|
+
### Changed
|
|
245
|
+
- README rewritten for current commands and config; points at the docs site.
|
|
246
|
+
|
|
247
|
+
## [0.4.10] — 2026-09-02
|
|
248
|
+
### Changed
|
|
249
|
+
- `domains`: prints the A record to add, and any DNS reachability warning.
|
|
250
|
+
|
|
251
|
+
## [0.4.9] — 2026-09-02
|
|
252
|
+
### Changed
|
|
253
|
+
- `deploy` (#80): dropped the client-side dedup check in favour of trusting
|
|
254
|
+
the server's own no-op response.
|
|
255
|
+
|
|
256
|
+
## [0.4.8] — 2026-09-02
|
|
257
|
+
### Fixed
|
|
258
|
+
- `deploy` (#80): no longer skipped the upload when only assets or bindings
|
|
259
|
+
had changed but the sprout binary hadn't.
|
|
260
|
+
|
|
261
|
+
## [0.4.7] — 2026-09-02
|
|
262
|
+
### Added
|
|
263
|
+
- `deploy` auto-provisions an id-less storage binding (wrangler-style):
|
|
264
|
+
creates the account-level resource, writes its id back into
|
|
265
|
+
`sproutboat.jsonc`.
|
|
266
|
+
|
|
267
|
+
## [0.4.6] — 2026-09-02
|
|
268
|
+
### Added
|
|
269
|
+
- `sproutboat resource` — manage account-level storage resources directly.
|
|
270
|
+
- `sproutboat.jsonc` storage bindings accept `{ binding, id }`, not just a
|
|
271
|
+
bare name.
|
|
272
|
+
### Changed
|
|
273
|
+
- Broker keys KV/R2/queue/D1 stores by resource id when one is bound, so the
|
|
274
|
+
data survives a redeploy and can be shared across projects.
|
|
275
|
+
|
|
276
|
+
## [0.4.5] — 2026-09-02
|
|
277
|
+
### Changed
|
|
278
|
+
- `deploy` dropped the "✓ serving" line — silence now means the health check
|
|
279
|
+
passed.
|
|
280
|
+
|
|
281
|
+
## [0.4.4] — 2026-09-02
|
|
282
|
+
### Added
|
|
283
|
+
- `--version`, a once-a-day update-available notice, a richer deploy echo.
|
|
284
|
+
### Changed
|
|
285
|
+
- Misuse now exits `2` (getopt convention) instead of `1`.
|
|
286
|
+
|
|
287
|
+
## [0.4.3] — 2026-09-02
|
|
288
|
+
### Changed
|
|
289
|
+
- `--help` output: grouped, emoji-labelled, aligned — was one wall-of-text
|
|
290
|
+
usage line.
|
|
291
|
+
|
|
292
|
+
## [0.4.2] — 2026-09-02
|
|
293
|
+
### Added
|
|
294
|
+
- `tail --sprout` streams the running sprout's and broker's stdout/stderr.
|
|
295
|
+
### Changed
|
|
296
|
+
- `deploy` waits for the health check and prints every binding in the
|
|
297
|
+
report; `delete` takes flexible args plus `?confirm`; the banner reads the
|
|
298
|
+
real installed version.
|
|
299
|
+
|
|
300
|
+
## [0.4.0] — 2026-09-01
|
|
301
|
+
### Added
|
|
302
|
+
- `sproutboat domains` and `sproutboat secrets` commands.
|
|
303
|
+
- `deploy` uploads the `bindings.json` / `assets.json` sidecars alongside
|
|
304
|
+
the sprout binary.
|
|
305
|
+
- The worker self-reports per-invocation CPU time (`x-sb-cpu-ms`).
|
|
306
|
+
- Deploy surfaces a Porffor pin drift warning when the live version was
|
|
307
|
+
built against a different compiler pin than the one about to deploy.
|
|
308
|
+
### Changed
|
|
309
|
+
- Renamed "worker" to "sprout" throughout the CLI, broker, and examples.
|
|
310
|
+
|
|
311
|
+
## [0.3.0] — 2026-09-01
|
|
312
|
+
### Added
|
|
313
|
+
- Published to npm via Trusted Publishing (OIDC) — no token secret in CI.
|
|
314
|
+
### Changed
|
|
315
|
+
- `src/wrap.ts` extracted with `runtime/*` subpath exports, so the monorepo
|
|
316
|
+
can consume the binding/manifest contracts as a dependency instead of a
|
|
317
|
+
hand-vendored copy.
|
|
318
|
+
- CSPRNG-backed `crypto.getRandomValues`; the deploy binary is stripped.
|
|
319
|
+
- One long-lived broker connection per worker instead of reconnecting on
|
|
320
|
+
every binding call; `env.<SECRET>` memoized; `assets.get` made synchronous
|
|
321
|
+
with the broker service; WAL + `synchronous=NORMAL` and parameterised
|
|
322
|
+
`LIMIT` back-ported from the monorepo's broker.
|
|
323
|
+
|
|
324
|
+
## [0.2.1] — 2026-08-31
|
|
325
|
+
### Changed
|
|
326
|
+
- Ships a prebuilt uWebSockets archive, so the first build needs neither
|
|
327
|
+
`git` nor `make` on `PATH`.
|
|
328
|
+
|
|
329
|
+
## [0.2.0] — 2026-08-31
|
|
330
|
+
Initial release, extracted from the `sproutboat` monorepo (`apps/cli`) as
|
|
331
|
+
its own package.
|
|
332
|
+
### Added
|
|
333
|
+
- Cross-compiles a handler to a static `linux-x86_64` binary with Porffor +
|
|
334
|
+
Zig — no Docker.
|
|
335
|
+
- Static assets binding, with the `examples/kitchen-sink` Astro app as a
|
|
336
|
+
worked example.
|
|
337
|
+
- `SURFACE.md`, generated and drift-checked against the actual command/env
|
|
338
|
+
surface.
|
|
339
|
+
### Changed
|
|
340
|
+
- Patches Porffor at build time rather than via a `postinstall` hook.
|
|
341
|
+
- Renamed the package to `sproutboat` (was `@sproutboat/cli`); dropped the
|
|
342
|
+
`sprout` bin alias in favour of a user-defined shell alias.
|
|
343
|
+
|
|
344
|
+
[Unreleased]: https://github.com/baronunread/sproutboat-cli/compare/v0.10.0...HEAD
|
|
345
|
+
[0.10.0]: https://github.com/baronunread/sproutboat-cli/compare/v0.9.0...v0.10.0
|
|
346
|
+
[0.9.0]: https://github.com/baronunread/sproutboat-cli/compare/v0.8.0...v0.9.0
|
|
347
|
+
[0.8.0]: https://github.com/baronunread/sproutboat-cli/compare/v0.7.0...v0.8.0
|
|
348
|
+
[0.7.0]: https://github.com/baronunread/sproutboat-cli/compare/v0.6.1...v0.7.0
|
|
349
|
+
[0.6.1]: https://github.com/baronunread/sproutboat-cli/compare/v0.6.0...v0.6.1
|
|
350
|
+
[0.6.0]: https://github.com/baronunread/sproutboat-cli/compare/v0.5.0...v0.6.0
|
|
351
|
+
[0.5.0]: https://github.com/baronunread/sproutboat-cli/compare/v0.4.11...v0.5.0
|
|
352
|
+
[0.4.11]: https://github.com/baronunread/sproutboat-cli/compare/v0.4.10...v0.4.11
|
|
353
|
+
[0.4.10]: https://github.com/baronunread/sproutboat-cli/compare/v0.4.9...v0.4.10
|
|
354
|
+
[0.4.9]: https://github.com/baronunread/sproutboat-cli/compare/v0.4.8...v0.4.9
|
|
355
|
+
[0.4.8]: https://github.com/baronunread/sproutboat-cli/compare/v0.4.7...v0.4.8
|
|
356
|
+
[0.4.7]: https://github.com/baronunread/sproutboat-cli/compare/v0.4.6...v0.4.7
|
|
357
|
+
[0.4.6]: https://github.com/baronunread/sproutboat-cli/compare/v0.4.5...v0.4.6
|
|
358
|
+
[0.4.5]: https://github.com/baronunread/sproutboat-cli/compare/v0.4.4...v0.4.5
|
|
359
|
+
[0.4.4]: https://github.com/baronunread/sproutboat-cli/compare/v0.4.3...v0.4.4
|
|
360
|
+
[0.4.3]: https://github.com/baronunread/sproutboat-cli/compare/v0.4.2...v0.4.3
|
|
361
|
+
[0.4.2]: https://github.com/baronunread/sproutboat-cli/compare/v0.4.0...v0.4.2
|
|
362
|
+
[0.4.0]: https://github.com/baronunread/sproutboat-cli/compare/v0.3.0...v0.4.0
|
|
363
|
+
[0.3.0]: https://github.com/baronunread/sproutboat-cli/compare/v0.2.1...v0.3.0
|
|
364
|
+
[0.2.1]: https://github.com/baronunread/sproutboat-cli/compare/v0.2.0...v0.2.1
|
|
365
|
+
[0.2.0]: https://github.com/baronunread/sproutboat-cli/releases/tag/v0.2.0
|
package/README.md
CHANGED
|
@@ -8,6 +8,11 @@ https://sproutboat.com
|
|
|
8
8
|
The CLI for [Sproutboat](https://github.com/baronunread/sproutboat). Compiles a
|
|
9
9
|
`fetch` handler to a native binary and ships it to any Sproutboat control plane.
|
|
10
10
|
|
|
11
|
+
## Issues
|
|
12
|
+
|
|
13
|
+
Tracked centrally in [baronunread/sproutboat](https://github.com/baronunread/sproutboat/issues)
|
|
14
|
+
(label `area:cli`). Please file there.
|
|
15
|
+
|
|
11
16
|
## Overview
|
|
12
17
|
|
|
13
18
|
Wrangler-shaped, MIT licensed. `build` and `deploy` cross-compile your handler
|
|
@@ -19,10 +24,23 @@ for agents: [sproutboat.com/llms.txt](https://sproutboat.com/llms.txt)).
|
|
|
19
24
|
|
|
20
25
|
## Using
|
|
21
26
|
|
|
27
|
+
The npm package installs a small Node launcher and the matching native
|
|
28
|
+
platform package, so `npm install -g sproutboat` and `npm exec sproutboat` work
|
|
29
|
+
without Bun on `darwin` and `linux` for `arm64` and `x64`. The self-hosted
|
|
30
|
+
runtime exports remain in the root package for Bun-based platform integrations.
|
|
31
|
+
Nothing is needed on the machine that *runs* a sprout: that gets a static
|
|
32
|
+
binary.
|
|
33
|
+
|
|
34
|
+
The first build downloads the pinned Porffor source into
|
|
35
|
+
`~/.cache/sproutboat`, verifies its SHA-256, applies Sproutboat's compiler
|
|
36
|
+
patches there, and publishes the cache entry atomically. Installation itself
|
|
37
|
+
does not clone Porffor or invoke Git, Make, or a compiler. Warm-cache builds can
|
|
38
|
+
run offline.
|
|
39
|
+
|
|
22
40
|
```sh
|
|
23
|
-
|
|
41
|
+
npm exec sproutboat init hello
|
|
24
42
|
cd hello
|
|
25
|
-
|
|
43
|
+
npm exec sproutboat dev # runs it right here, no control plane needed
|
|
26
44
|
```
|
|
27
45
|
|
|
28
46
|
Happy with it? Ship it:
|
|
@@ -32,10 +50,10 @@ bunx sproutboat login --api-url https://control.example.com # one browser appr
|
|
|
32
50
|
bunx sproutboat deploy
|
|
33
51
|
```
|
|
34
52
|
|
|
35
|
-
Or install it once and drop the `
|
|
53
|
+
Or install it once and drop the `npm exec`:
|
|
36
54
|
|
|
37
55
|
```sh
|
|
38
|
-
|
|
56
|
+
npm install -g sproutboat # then: sproutboat deploy, sproutboat tail, ...
|
|
39
57
|
```
|
|
40
58
|
|
|
41
59
|
`login` is one-time. It writes a long-lived token to
|
|
@@ -63,6 +81,32 @@ Run `sproutboat` with no arguments for the grouped list.
|
|
|
63
81
|
[`SURFACE.md`](SURFACE.md) is the generated inventory: every command, every
|
|
64
82
|
argument, every env var, kept honest by a drift test.
|
|
65
83
|
|
|
84
|
+
### KV data and waitlist export
|
|
85
|
+
|
|
86
|
+
Manage one namespace by its account-level name:
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
sproutboat kv key list registrations --prefix email:
|
|
90
|
+
sproutboat kv key get registrations email:person@example.com --text
|
|
91
|
+
sproutboat kv key put registrations email:person@example.com joined
|
|
92
|
+
sproutboat kv key delete registrations email:person@example.com --yes
|
|
93
|
+
sproutboat kv export registrations --prefix email: --output registrations.json
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Bulk put, and therefore exported dumps, use the version 1 text-value format:
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
[
|
|
100
|
+
{ "key": "email:person@example.com", "value": "joined" }
|
|
101
|
+
]
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`kv bulk get` and `kv bulk delete` accept a JSON array of key strings. `kv bulk
|
|
105
|
+
put` accepts the entry array above. The CLI sends large inputs in bounded
|
|
106
|
+
batches. Export pages through the namespace and writes incrementally to a
|
|
107
|
+
permission-restricted temporary file, then renames it into place only after a
|
|
108
|
+
complete export. Existing output files require `--force`.
|
|
109
|
+
|
|
66
110
|
## Config
|
|
67
111
|
|
|
68
112
|
`sproutboat.jsonc`: the entry point plus Cloudflare-shaped `env.*` bindings.
|
|
@@ -81,6 +125,7 @@ argument, every env var, kept honest by a drift test.
|
|
|
81
125
|
"queues": ["JOBS"],
|
|
82
126
|
"analytics_engine_datasets": ["METRICS"], // bare name only, no id
|
|
83
127
|
"durable_objects": { "COUNTER": "Counter" },
|
|
128
|
+
"ratelimiters": [{ "binding": "API", "limit": 100, "period": 60 }], // env.API.limit({ key })
|
|
84
129
|
"outbound": ["api.example.com"],
|
|
85
130
|
"triggers": { "crons": ["*/5 * * * *"] },
|
|
86
131
|
"assets": { "directory": "public", "binding": "ASSETS", "run_sprout_first": ["/api/*"] }
|
package/SURFACE.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> Generated by `src/surface.test.ts` from `src/surface.ts` + the pinned
|
|
4
4
|
> toolchain constants. Do not edit by hand — run `UPDATE_SURFACE=1 bun test`.
|
|
5
5
|
|
|
6
|
-
**Package:** `sproutboat` 0.
|
|
6
|
+
**Package:** `sproutboat` 0.9.0 · runs on Bun (use `bunx`, not `npx`)
|
|
7
7
|
|
|
8
8
|
## Commands
|
|
9
9
|
|
|
@@ -11,13 +11,14 @@
|
|
|
11
11
|
| --- | --- | --- |
|
|
12
12
|
| `init` | `[name]` | Scaffold sproutboat.jsonc + src/index.js in ./<name>. |
|
|
13
13
|
| `check` | `[project-dir]` | Validate the config and entry point without building. |
|
|
14
|
+
| `types` | `[project-dir]` | Write sproutboat-env.d.ts from the config, so an editor knows what `env` holds. `dev` and `deploy` refresh it when the config is newer. |
|
|
14
15
|
| `dev` | `[project-dir] [--port <n>] [--no-watch]` | Run the project on this machine against a real broker, rebuilding on save. |
|
|
15
16
|
| `build` | `[project-dir] [--target host] [--standalone]` | 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. |
|
|
16
17
|
| `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. |
|
|
17
18
|
| `versions` | `<list | view <version-id>> [project-dir]` | List the project's deployed versions, or show one version's artifact and bindings. |
|
|
18
19
|
| `rollback` | `<version-id> [project-dir]` | Re-activate a previous version. |
|
|
19
20
|
| `tail` | `[project-dir] [--sprout]` | Print recent request logs; --sprout prints the running sprout + broker stdout/stderr instead. |
|
|
20
|
-
| `kv` | `<list | create <name> | info <name> | rename <name> <new> | delete <name
|
|
21
|
+
| `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. |
|
|
21
22
|
| `d1` | `<list | create <name> | info <name> | rename <name> <new> | delete <name>>` | D1 databases. `create` prints the id to bind from sproutboat.jsonc. |
|
|
22
23
|
| `r2` | `<list | create <name> | info <name> | rename <name> <new> | delete <name>>` | R2 buckets. `create` prints the id to bind from sproutboat.jsonc. |
|
|
23
24
|
| `queues` | `<list | create <name> | info <name> | rename <name> <new> | delete <name>>` | Queues. `create` prints the id to bind from sproutboat.jsonc; consumers deliver in batches with retries, and stop after 5 attempts. |
|
|
@@ -29,7 +30,7 @@
|
|
|
29
30
|
| `whoami` | — | Show the active endpoint and the account the stored token belongs to. |
|
|
30
31
|
|
|
31
32
|
```
|
|
32
|
-
usage: sproutboat <init [name] | check [project-dir] | dev [project-dir] [--port <n>] [--no-watch] | build [project-dir] [--target host] [--standalone] | 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] [--sprout] | kv <list | create <name> | info <name> | rename <name> <new> | delete <name>> | 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>
|
|
33
|
+
usage: sproutboat <init [name] | check [project-dir] | types [project-dir] | dev [project-dir] [--port <n>] [--no-watch] | build [project-dir] [--target host] [--standalone] | 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] [--sprout] | 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>
|
|
33
34
|
```
|
|
34
35
|
|
|
35
36
|
## Environment variables
|
|
@@ -39,8 +40,14 @@ usage: sproutboat <init [name] | check [project-dir] | dev [project-dir] [--port
|
|
|
39
40
|
| `SPROUTBOAT_API_URL` | Control-plane URL. Overrides the saved active endpoint. |
|
|
40
41
|
| `SPROUTBOAT_TOKEN` | API token. Overrides the saved credential for the endpoint. |
|
|
41
42
|
| `SPROUTBOAT_ZIG` | Path to a Zig binary to use instead of downloading the pinned one. |
|
|
42
|
-
| `SPROUTBOAT_UWS_TARBALL` | Path to a
|
|
43
|
+
| `SPROUTBOAT_UWS_TARBALL` | Path to a uWebSockets source tarball to seed the Porffor cache with, instead of the one vendored in the package. Both targets are seeded from it. |
|
|
44
|
+
| `CC` | C compiler used to build uSockets for a host build (default `cc`). A host build needs one regardless: it is what Porffor compiles its own generated C with. |
|
|
45
|
+
| `AR` | Archiver used to assemble uSockets.a for a host build (default `ar`). |
|
|
43
46
|
| `SPROUTBOAT_COMPILE_TIMEOUT_MS` | Porffor compile timeout in ms (default 600000). |
|
|
47
|
+
| `SPROUTBOAT_TOOLCHAIN_CACHE` | Managed Porffor and compiler cache root (default ~/.cache/sproutboat). |
|
|
48
|
+
| `SPROUTBOAT_PORFFOR_DIR` | Contributor override for an existing Porffor source checkout. Required files are validated before use. |
|
|
49
|
+
| `SPROUTBOAT_BUILD_UWS_FROM_SOURCE` | Set to 1 to explicitly allow the contributor-only Git and Make uWebSockets fallback. |
|
|
50
|
+
| `SPROUTBOAT_CLI_VERSION` | CLI version embedded by the release executable build. Not normally set by users. |
|
|
44
51
|
| `SPROUTBOAT_VARS_JSON` | JSON object of baked `vars` (UPPER_SNAKE -> string), read by the wrapper when generating the sprout module. |
|
|
45
52
|
| `SPROUTBOAT_BINDINGS_JSON` | The artifact's bindings.json, read by the wrapper to emit the `__sbInstallBindings` line. |
|
|
46
53
|
| `SPROUTBOAT_CONFIG_DIR` | Directory for credentials.json (default ~/.config/sproutboat). |
|
|
@@ -64,7 +71,7 @@ usage: sproutboat <init [name] | check [project-dir] | dev [project-dir] [--port
|
|
|
64
71
|
| | |
|
|
65
72
|
| --- | --- |
|
|
66
73
|
| Zig | `0.16.0` (`zig cc -target x86_64-linux-musl`, static) |
|
|
67
|
-
| Provenance stamp | `zig-musl/0.16.0+porffor/
|
|
74
|
+
| Provenance stamp | `zig-musl/0.16.0+porffor/1f4ae4a+uws/360c276d` |
|
|
68
75
|
| Artifact schema | `2` |
|
|
69
76
|
| Runtime | `native-fetch` |
|
|
70
77
|
| Capability profile | `http-sync-v0` |
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// npm only selects the packaged executable. The CLI itself always runs in Bun.
|
|
3
|
+
const { spawn } = require("node:child_process");
|
|
4
|
+
const { resolve } = require("node:path");
|
|
5
|
+
|
|
6
|
+
const platform = process.platform === "darwin" ? "darwin" : process.platform === "linux" ? "linux" : null;
|
|
7
|
+
const arch = process.arch === "arm64" ? "arm64" : process.arch === "x64" ? "x64" : null;
|
|
8
|
+
if (!platform || !arch) {
|
|
9
|
+
console.error(`sproutboat: unsupported platform ${process.platform}/${process.arch}`);
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
const packageName = `@sproutboat/cli-${platform}-${arch}`;
|
|
13
|
+
let executable;
|
|
14
|
+
try {
|
|
15
|
+
executable = require.resolve(`${packageName}/bin/sproutboat`);
|
|
16
|
+
} catch {
|
|
17
|
+
console.error(`sproutboat: optional package ${packageName} is missing for ${platform}/${arch}`);
|
|
18
|
+
console.error("Reinstall sproutboat without --omit=optional, or use a direct release download.");
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
const child = spawn(resolve(executable), process.argv.slice(2), { stdio: "inherit" });
|
|
22
|
+
child.once("error", (error) => {
|
|
23
|
+
console.error(`sproutboat: could not start bundled executable: ${error.message}`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
});
|
|
26
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
27
|
+
process.on(signal, () => child.kill(signal));
|
|
28
|
+
}
|
|
29
|
+
child.once("exit", (code, signal) => {
|
|
30
|
+
if (signal) process.kill(process.pid, signal);
|
|
31
|
+
else process.exit(code ?? 1);
|
|
32
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sproutboat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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",
|
|
@@ -19,15 +19,19 @@
|
|
|
19
19
|
"url": "git+https://github.com/baronunread/sproutboat-cli.git"
|
|
20
20
|
},
|
|
21
21
|
"bin": {
|
|
22
|
-
"sproutboat": "
|
|
22
|
+
"sproutboat": "bin/sproutboat.cjs"
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
|
+
"CHANGELOG.md",
|
|
26
|
+
"SURFACE.md",
|
|
25
27
|
"src",
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
28
|
+
"bin/sproutboat.cjs",
|
|
29
|
+
"scripts",
|
|
30
|
+
"types",
|
|
31
|
+
"vendor"
|
|
29
32
|
],
|
|
30
33
|
"type": "module",
|
|
34
|
+
"types": "./types/sproutboat.d.ts",
|
|
31
35
|
"exports": {
|
|
32
36
|
"./runtime/config": "./src/config.ts",
|
|
33
37
|
"./runtime/source": "./src/source.ts",
|
|
@@ -35,13 +39,15 @@
|
|
|
35
39
|
"./runtime/assets": "./src/assets.ts",
|
|
36
40
|
"./runtime/broker": "./src/broker.ts",
|
|
37
41
|
"./runtime/wrap": "./src/wrap.ts",
|
|
38
|
-
"./
|
|
39
|
-
"./
|
|
42
|
+
"./package.json": "./package.json",
|
|
43
|
+
"./types": "./types/sproutboat.d.ts"
|
|
40
44
|
},
|
|
41
45
|
"scripts": {
|
|
42
46
|
"prepare": "lefthook install || true",
|
|
43
47
|
"typecheck": "tsc --noEmit",
|
|
44
48
|
"lint": "oxlint .",
|
|
49
|
+
"examples": "bun examples/smoke.ts",
|
|
50
|
+
"build:cli": "bun scripts/build-cli.ts",
|
|
45
51
|
"fmt": "oxfmt .",
|
|
46
52
|
"fmt:check": "oxfmt --check .",
|
|
47
53
|
"test": "bun test",
|
|
@@ -54,8 +60,13 @@
|
|
|
54
60
|
"kitchen-sink:standalone": "bun examples/kitchen-sink/harness-standalone.ts"
|
|
55
61
|
},
|
|
56
62
|
"dependencies": {
|
|
57
|
-
"
|
|
58
|
-
"
|
|
63
|
+
"@sproutboat/artifact": "^0.2.0",
|
|
64
|
+
"@sproutboat/assets": "^0.2.0",
|
|
65
|
+
"@sproutboat/config": "^0.3.0",
|
|
66
|
+
"@sproutboat/runtime": "^0.4.0",
|
|
67
|
+
"@sproutboat/toolchain": "^0.3.0",
|
|
68
|
+
"@sproutboat/wire": "^0.4.0",
|
|
69
|
+
"esbuild": "^0.28.2"
|
|
59
70
|
},
|
|
60
71
|
"devDependencies": {
|
|
61
72
|
"@oxlint/plugins": "1.81.0",
|
|
@@ -65,7 +76,13 @@
|
|
|
65
76
|
"oxlint": "1.81.0",
|
|
66
77
|
"typescript": "7.0.2"
|
|
67
78
|
},
|
|
79
|
+
"optionalDependencies": {
|
|
80
|
+
"@sproutboat/cli-darwin-arm64": "0.10.0",
|
|
81
|
+
"@sproutboat/cli-darwin-x64": "0.10.0",
|
|
82
|
+
"@sproutboat/cli-linux-arm64": "0.10.0",
|
|
83
|
+
"@sproutboat/cli-linux-x64": "0.10.0"
|
|
84
|
+
},
|
|
68
85
|
"engines": {
|
|
69
|
-
"
|
|
86
|
+
"node": ">=18"
|
|
70
87
|
}
|
|
71
88
|
}
|