sproutboat 0.21.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -10,6 +10,48 @@ maintained going forward by the `release` skill.
10
10
 
11
11
  ## Unreleased
12
12
 
13
+ ## [0.23.0] - 2026-10-22
14
+
15
+ ### Added
16
+
17
+ - Experimental local backup of a D1 database to LTX files, in standalone
18
+ binaries (`@sproutboat/runtime` 0.22.0). It's off by default. To turn it on, set
19
+ the compatibility date to `2026-10-22` or later, `SB_LTX_ENABLE=1`, and
20
+ `SB_LTX_DATABASE=<binding>`. Every committed transaction is then written to a
21
+ durable LTX file, and the app resumes from the last one after a restart.
22
+ - Only one process may write to the database.
23
+ - Files stay on local disk; uploading them is not part of this release.
24
+ - See the runtime's `LTX_REPLICATION.md` for limits and recovery behaviour.
25
+
26
+ ## [0.22.0] - 2026-10-22
27
+
28
+ ### Added
29
+
30
+ - `sproutboat build --standalone --emit-only` writes
31
+ `.sproutboat/porffor-units/linux-x86_64-embedded-release/sprout.generated.js`,
32
+ byte for byte what a full standalone build compiles, prints its path and
33
+ stops. It runs every check that can fail a build, but acquires no Zig,
34
+ SQLite, BearSSL or Porffor and writes no binary, so it takes well under a
35
+ second where a full build takes tens of seconds. Without `--standalone` it
36
+ emits the broker-transport module the same way.
37
+
38
+ ### Fixed
39
+
40
+ - The `d1`, `queue` and `analytics` examples read the request body with
41
+ `await request.text()` rather than as `request.body`. From compatibility date
42
+ `2026-10-21`, `request.body` is a `ReadableStream`, as in Workers, so
43
+ `JSON.parse(request.body)` threw `SyntaxError: Unexpected token` and a D1
44
+ example moved to a current date answered every POST with a 500. The new form
45
+ works at every date. `bun run examples` now also builds the d1 example at
46
+ `2026-10-22` (`d1@2026-10-22`), so an example that only works before that
47
+ date fails the run.
48
+
49
+ ### Changed
50
+
51
+ - `@sproutboat/toolchain` 0.12.1: heap-backed BigInts stay alive through
52
+ conservative garbage collection, which fixes crashes and wrong checksums in
53
+ BigInt-heavy code such as CRC64 and LTX encoding. `@sproutboat/wire` 0.14.1.
54
+
13
55
  ## [0.21.0] - 2026-10-21
14
56
 
15
57
  ### Changed
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.21.0
6
+ **Package:** `sproutboat` 0.23.0
7
7
 
8
8
  ## Commands
9
9
 
@@ -13,7 +13,7 @@
13
13
  | `check` | `[project-dir]` | Validate the config and entry point without building. |
14
14
  | `types` | `[project-dir]` | Write sproutboat-env.d.ts from the config, so an editor knows what `env` holds. `dev` and `deploy` refresh it when the config is newer. |
15
15
  | `dev` | `[project-dir] [--port <n>] [--no-watch]` | Run the project on this machine against a real broker, rebuilding on save. |
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
+ | `build` | `[project-dir] [--target host] [--standalone] [--emit-only]` | Cross-compile the native-fetch sprout (Porffor + Zig). `--target host` builds for this machine instead, to run locally — not deployable. `--standalone` emits one executable carrying its own bindings, with SQLite compiled in (~2 MB) and no broker process. `--emit-only` stops after writing `.sproutboat/porffor-units/<shape>/sprout.generated.js`, the module Porffor would compile, and prints its path: every check still runs, nothing is compiled. |
17
17
  | `toolchain` | `doctor [--json]` | Report the selected compiler inputs, cache locations, overrides, and host prerequisites without downloading or compiling anything. |
18
18
  | `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. |
19
19
  | `versions` | `<list | view <version-id>> [project-dir]` | List the project's deployed versions, or show one version's artifact and bindings. |
@@ -32,7 +32,7 @@
32
32
  | `complete` | `[bash | zsh | fish]` | Print a shell completion script for the top-level commands (defaults to $SHELL). |
33
33
 
34
34
  ```
35
- usage: sproutboat <init [name] | check [project-dir] | types [project-dir] | dev [project-dir] [--port <n>] [--no-watch] | build [project-dir] [--target host] [--standalone] | toolchain doctor [--json] | deploy [project-dir] [--dry-run] [--artifact <dir>] [--no-wait] [--no-provision] | versions <list | view <version-id>> [project-dir] | rollback <version-id> [project-dir] | tail [project-dir] [--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 | complete [bash | zsh | fish]>
35
+ usage: sproutboat <init [name] | check [project-dir] | types [project-dir] | dev [project-dir] [--port <n>] [--no-watch] | build [project-dir] [--target host] [--standalone] [--emit-only] | toolchain doctor [--json] | deploy [project-dir] [--dry-run] [--artifact <dir>] [--no-wait] [--no-provision] | versions <list | view <version-id>> [project-dir] | rollback <version-id> [project-dir] | tail [project-dir] [--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 | complete [bash | zsh | fish]>
36
36
  ```
37
37
 
38
38
  ## Environment variables
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sproutboat",
3
- "version": "0.21.0",
3
+ "version": "0.23.0",
4
4
  "description": "Wrangler-shaped CLI for Sproutboat. Deploys workers to any control plane via --api-url / SPROUTBOAT_API_URL.",
5
5
  "keywords": [
6
6
  "cli",
@@ -64,9 +64,9 @@
64
64
  "@sproutboat/artifact": "^0.3.0",
65
65
  "@sproutboat/assets": "^0.3.0",
66
66
  "@sproutboat/config": "^0.5.0",
67
- "@sproutboat/runtime": "^0.21.1",
68
- "@sproutboat/toolchain": "^0.12.0",
69
- "@sproutboat/wire": "^0.14.0"
67
+ "@sproutboat/runtime": "^0.22.0",
68
+ "@sproutboat/toolchain": "^0.12.1",
69
+ "@sproutboat/wire": "^0.14.2"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@oxlint/plugins": "1.85.0",
@@ -78,10 +78,10 @@
78
78
  "typescript": "7.0.2"
79
79
  },
80
80
  "optionalDependencies": {
81
- "@sproutboat/cli-darwin-arm64": "0.21.0",
82
- "@sproutboat/cli-darwin-x64": "0.21.0",
83
- "@sproutboat/cli-linux-arm64": "0.21.0",
84
- "@sproutboat/cli-linux-x64": "0.21.0"
81
+ "@sproutboat/cli-darwin-arm64": "0.23.0",
82
+ "@sproutboat/cli-darwin-x64": "0.23.0",
83
+ "@sproutboat/cli-linux-arm64": "0.23.0",
84
+ "@sproutboat/cli-linux-x64": "0.23.0"
85
85
  },
86
86
  "engines": {
87
87
  "node": ">=18"
package/src/build.ts CHANGED
@@ -111,12 +111,13 @@ async function acquireArtifactLock(artifactDir: string): Promise<string> {
111
111
  }
112
112
 
113
113
  /**
114
- * Cross-compile a handler into a linux-x86_64 native-fetch server binary with
115
- * Porffor + Zig — no Docker. The binary is not run here (the build host may not
116
- * be linux); the control plane starts it once on deploy and rejects it if it
117
- * does not come up.
114
+ * Everything a build derives before the native compile: bindings, assets,
115
+ * version metadata and the wrapped module itself. Every check that can fail a
116
+ * build on its source or config (a missing asset dir, oversized baked assets,
117
+ * a handler without a default export) runs here, so `emitGenerated` fails
118
+ * exactly where `buildArtifact` would.
118
119
  */
119
- export async function buildArtifact(input: BuildInput): Promise<BuildOutput> {
120
+ async function prepareBuild(input: BuildInput) {
120
121
  const source = input.source ?? (await readFile(input.sourcePath));
121
122
  const sourceHash = digest(source);
122
123
  const target = input.target ?? "linux-x86_64";
@@ -220,6 +221,93 @@ export async function buildArtifact(input: BuildInput): Promise<BuildOutput> {
220
221
  input.transport,
221
222
  versionMetadata,
222
223
  );
224
+ return {
225
+ sourceHash,
226
+ target,
227
+ embedded,
228
+ host,
229
+ bindings,
230
+ assetDir,
231
+ assetManifest,
232
+ bakedAssets,
233
+ builtAt,
234
+ versionId,
235
+ versionMetadata,
236
+ generatedSource,
237
+ };
238
+ }
239
+
240
+ /**
241
+ * Porffor caches compiled C units per entry path, so compile from one path per
242
+ * project and build shape: unchanged units (the uWebSockets server alone is
243
+ * ~1.75 s of clang) are reused instead of rebuilt, and Porffor keeps one build
244
+ * dir per shape instead of one per artifact. `sprout.generated.js` lives here.
245
+ */
246
+ function unitDirFor(input: BuildInput): string {
247
+ return resolve(
248
+ input.projectDir,
249
+ ".sproutboat/porffor-units",
250
+ `${input.target ?? "linux-x86_64"}-${input.transport ?? "broker"}-${input.optimize ?? "release"}`,
251
+ );
252
+ }
253
+
254
+ /**
255
+ * Write the wrapped module a build would compile, at the same path, and stop.
256
+ *
257
+ * Runs every check `buildArtifact` runs before compiling, then hands
258
+ * `compileSprout` `emitOnly`: no Zig, SQLite, BearSSL, Porffor or uWebSockets
259
+ * is acquired, nothing is compiled, and no artifact directory is written.
260
+ */
261
+ export async function emitGenerated(input: BuildInput): Promise<{ generatedPath: string }> {
262
+ const { bindings, bakedAssets, versionMetadata, generatedSource } = await prepareBuild(input);
263
+ const unitDir = unitDirFor(input);
264
+ const generatedPath = input.generatedPath ?? resolve(unitDir, "sprout.generated.js");
265
+ // Same lock as a full build, so an emit never races a compile reading the file.
266
+ const unitLock = input.generatedPath ? null : await acquireArtifactLock(unitDir);
267
+ try {
268
+ await compileSprout({
269
+ sourcePath: input.sourcePath,
270
+ generatedPath,
271
+ generatedSource,
272
+ outPath: resolve(unitDir, "sprout"),
273
+ vars: input.config.vars ?? {},
274
+ bindings,
275
+ target: input.target,
276
+ compatibilityDate: input.config.compatibility_date,
277
+ transport: input.transport,
278
+ appName: input.config.name,
279
+ assets: bakedAssets,
280
+ optimize: input.optimize,
281
+ versionMetadata,
282
+ emitOnly: true,
283
+ });
284
+ } finally {
285
+ if (unitLock) await rm(unitLock, { recursive: true, force: true });
286
+ }
287
+ return { generatedPath };
288
+ }
289
+
290
+ /**
291
+ * Cross-compile a handler into a linux-x86_64 native-fetch server binary with
292
+ * Porffor + Zig — no Docker. The binary is not run here (the build host may not
293
+ * be linux); the control plane starts it once on deploy and rejects it if it
294
+ * does not come up.
295
+ */
296
+ export async function buildArtifact(input: BuildInput): Promise<BuildOutput> {
297
+ const {
298
+ sourceHash,
299
+ target,
300
+ embedded,
301
+ host,
302
+ bindings,
303
+ assetDir,
304
+ assetManifest,
305
+ bakedAssets,
306
+ builtAt,
307
+ versionId,
308
+ versionMetadata,
309
+ generatedSource,
310
+ } = await prepareBuild(input);
223
311
  const compileKey = digest(
224
312
  JSON.stringify({
225
313
  generatedSource,
@@ -248,15 +336,7 @@ export async function buildArtifact(input: BuildInput): Promise<BuildOutput> {
248
336
  const [sqliteObject, tls] = embedded
249
337
  ? await Promise.all([ensureSqliteObject({ target, zigBin }), ensureBearssl({ target, zigBin })])
250
338
  : [null, null];
251
- // Porffor caches compiled C units per entry path, so compile from one
252
- // path per project and build shape: unchanged units (the uWebSockets
253
- // server alone is ~1.75 s of clang) are reused instead of rebuilt, and
254
- // Porffor keeps one build dir per shape instead of one per artifact.
255
- const unitDir = resolve(
256
- input.projectDir,
257
- ".sproutboat/porffor-units",
258
- `${input.target ?? "linux-x86_64"}-${input.transport ?? "broker"}-${input.optimize ?? "release"}`,
259
- );
339
+ const unitDir = unitDirFor(input);
260
340
  const unitLock = input.generatedPath ? null : await acquireArtifactLock(unitDir);
261
341
  try {
262
342
  await compileSprout({
package/src/compile.ts CHANGED
@@ -93,6 +93,13 @@ export type CompileInput = {
93
93
  * cannot execute the deploy artifact.
94
94
  */
95
95
  target?: "linux-x86_64" | "host";
96
+ /**
97
+ * Write `generatedPath` exactly as a full compile would, then stop: no
98
+ * Porffor, no uWebSockets, no compiler, nothing at `outPath`. For callers
99
+ * that need the wrapped module (prelude, bindings, handler) and never run the
100
+ * binary, such as a qualification run that only inspects the source.
101
+ */
102
+ emitOnly?: boolean;
96
103
  };
97
104
 
98
105
  /**
@@ -197,8 +204,48 @@ export function porfforArgs(
197
204
  return args;
198
205
  }
199
206
 
200
- /** Compile `sourcePath` to a native binary at `outPath` (mode 0555). */
207
+ /** The wrapped module the compiler sees: `generatedSource` if given, else the source wrapped here. */
208
+ async function generateSource(input: CompileInput): Promise<string> {
209
+ if (input.generatedSource !== undefined) return input.generatedSource;
210
+ const [source, prelude] = await Promise.all([
211
+ input.source === undefined ? readFile(input.sourcePath, "utf8") : Promise.resolve(input.source),
212
+ loadPrelude(input.transport ?? "broker"),
213
+ ]);
214
+ return wrapNativeFetchHandler(
215
+ source,
216
+ prelude,
217
+ input.vars,
218
+ input.bindings ?? EMPTY_BINDINGS,
219
+ undefined,
220
+ input.compatibilityDate,
221
+ input.appName,
222
+ input.assets,
223
+ input.transport,
224
+ input.versionMetadata,
225
+ );
226
+ }
227
+
228
+ /** Write the wrapped module to `generatedPath` (default: beside `outPath`) and return that path. */
229
+ async function writeGenerated(input: CompileInput): Promise<string> {
230
+ const generatedPath = input.generatedPath ?? resolve(dirname(input.outPath), "sprout.generated.js");
231
+ const generatedSource = await generateSource(input);
232
+ await mkdir(dirname(generatedPath), { recursive: true });
233
+ await writeFile(generatedPath, generatedSource);
234
+ return generatedPath;
235
+ }
236
+
237
+ /**
238
+ * Compile `sourcePath` to a native binary at `outPath` (mode 0555).
239
+ *
240
+ * With `emitOnly`, write `generatedPath` and return before any of the native
241
+ * compile's inputs are acquired. Generation needs only the runtime prelude and
242
+ * the wrapper, never Porffor itself.
243
+ */
201
244
  export async function compileSprout(input: CompileInput): Promise<void> {
245
+ if (input.emitOnly) {
246
+ await writeGenerated(input);
247
+ return;
248
+ }
202
249
  const porffor = await ensurePorffor();
203
250
  await ensurePorfforPatched(porffor);
204
251
 
@@ -238,29 +285,7 @@ export async function compileSprout(input: CompileInput): Promise<void> {
238
285
  // OS may still be executing. The linker cannot overwrite either, so clear it
239
286
  // first rather than failing with "can't write output file".
240
287
  await rm(input.outPath, { force: true });
241
- const generatedPath = input.generatedPath ?? resolve(outDir, "sprout.generated.js");
242
- await mkdir(dirname(generatedPath), { recursive: true });
243
- const generatedSource =
244
- input.generatedSource ??
245
- (await (async () => {
246
- const [source, prelude] = await Promise.all([
247
- input.source === undefined ? readFile(input.sourcePath, "utf8") : Promise.resolve(input.source),
248
- loadPrelude(input.transport ?? "broker"),
249
- ]);
250
- return wrapNativeFetchHandler(
251
- source,
252
- prelude,
253
- input.vars,
254
- input.bindings ?? EMPTY_BINDINGS,
255
- undefined,
256
- input.compatibilityDate,
257
- input.appName,
258
- input.assets,
259
- input.transport,
260
- input.versionMetadata,
261
- );
262
- })());
263
- await writeFile(generatedPath, generatedSource);
288
+ const generatedPath = await writeGenerated(input);
264
289
 
265
290
  const launcher = resolve(porffor, "runtime/index.js");
266
291
  // A musl build shells `zig` bare (Porffor hardcodes the literal command), so
package/src/main.ts CHANGED
@@ -6,11 +6,11 @@ import { isBoolean, isSafeInteger, isString, jsonObject, parseJsonValue, type Js
6
6
  import type { AssetFiles } from "./assets";
7
7
  import { parseConfig, pinBindingId, resourceRefs, type SproutboatConfig } from "./config";
8
8
  import { validateHttpSyncSource } from "./source";
9
- import { buildArtifact } from "./build";
9
+ import { buildArtifact, emitGenerated } from "./build";
10
10
  import { generateTypes, TYPES_FILE } from "./types-gen";
11
11
  import { bundleHandler, BundleError, type BundleResult } from "./bundle";
12
12
  import { runDev } from "./dev";
13
- import { buildStandalone } from "./standalone-build";
13
+ import { buildStandalone, emitStandalone } from "./standalone-build";
14
14
  import { hostTarget, validateManifest, type ArtifactManifest } from "./manifest";
15
15
  import { CLI_VERSION, printDeployReport } from "./report";
16
16
  import { activeApiUrl, forgetToken, savedToken, saveToken } from "./credentials";
@@ -353,6 +353,33 @@ async function buildStandaloneBinary(directory: string | undefined, target: "lin
353
353
  }
354
354
  }
355
355
 
356
+ /**
357
+ * `build --emit-only`: run the build up to and including writing
358
+ * `sprout.generated.js` (prelude, bindings and handler, exactly what Porffor
359
+ * would compile), print its path and stop. Every source and config check still
360
+ * fails the build; no compiler, SQLite or BearSSL is acquired and no binary is
361
+ * written.
362
+ */
363
+ async function emitOnly(directory: string | undefined, target: "linux-x86_64" | "host", standalone: boolean) {
364
+ const project = await readProject(directory);
365
+ console.log(
366
+ dim(`Emitting ${standalone ? "the standalone " : ""}${project.config.name} source (${target}, no native compile)…`),
367
+ );
368
+ try {
369
+ const input = {
370
+ projectDir: project.directory,
371
+ config: project.config,
372
+ sourcePath: project.sourcePath,
373
+ source: project.bundle.code,
374
+ target,
375
+ };
376
+ const { generatedPath } = standalone ? await emitStandalone(input) : await emitGenerated(input);
377
+ console.log(generatedPath);
378
+ } catch (error) {
379
+ fail(error instanceof Error ? error.message : String(error));
380
+ }
381
+ }
382
+
356
383
  /** #62 — build for this machine, run it against a real broker, rebuild on save. */
357
384
  async function dev(args: string[]) {
358
385
  const directory = args.find((arg) => !arg.startsWith("--") && !/^\d+$/.test(arg));
@@ -1314,8 +1341,10 @@ switch (command) {
1314
1341
  case "build": {
1315
1342
  const hostBuild = args.includes("--target") && args[args.indexOf("--target") + 1] === "host";
1316
1343
  const directory = args.find((arg) => !arg.startsWith("--") && arg !== "host");
1317
- if (args.includes("--standalone")) await buildStandaloneBinary(directory, hostBuild ? "host" : "linux-x86_64");
1318
- else await build(directory, hostBuild ? "host" : "linux-x86_64");
1344
+ const target = hostBuild ? "host" : "linux-x86_64";
1345
+ if (args.includes("--emit-only")) await emitOnly(directory, target, args.includes("--standalone"));
1346
+ else if (args.includes("--standalone")) await buildStandaloneBinary(directory, target);
1347
+ else await build(directory, target);
1319
1348
  break;
1320
1349
  }
1321
1350
  case "toolchain":
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import { chmod, cp, mkdir } from "node:fs/promises";
11
11
  import { resolve } from "node:path";
12
- import { buildArtifact, type BuildInput } from "./build";
12
+ import { buildArtifact, emitGenerated, type BuildInput } from "./build";
13
13
  import type { Bindings } from "./wrap";
14
14
 
15
15
  export type StandaloneBuildInput = BuildInput & {
@@ -37,11 +37,26 @@ export function unsupportedBindings(bindings: Pick<Partial<Bindings>, "services"
37
37
  return reasons;
38
38
  }
39
39
 
40
- export async function buildStandalone(input: StandaloneBuildInput): Promise<StandaloneBuildResult> {
40
+ function assertStandaloneSupported(input: BuildInput): void {
41
41
  const blocked = unsupportedBindings(input.config);
42
42
  if (blocked.length > 0) {
43
43
  throw new Error(`cannot build a standalone binary for this project:\n - ${blocked.join("\n - ")}`);
44
44
  }
45
+ }
46
+
47
+ /**
48
+ * `sproutboat build --standalone --emit-only`: the standalone build up to and
49
+ * including `sprout.generated.js`, byte for byte what a full standalone build
50
+ * compiles, at the same path. Every check that fails a standalone build fails
51
+ * this too; nothing native is acquired or compiled and no binary is copied out.
52
+ */
53
+ export async function emitStandalone(input: BuildInput): Promise<{ generatedPath: string }> {
54
+ assertStandaloneSupported(input);
55
+ return emitGenerated({ ...input, transport: "embedded" });
56
+ }
57
+
58
+ export async function buildStandalone(input: StandaloneBuildInput): Promise<StandaloneBuildResult> {
59
+ assertStandaloneSupported(input);
45
60
  const artifact = await buildArtifact({ ...input, transport: "embedded" });
46
61
 
47
62
  // The sprout *is* the binary: assets and bindings are compiled into it, and
package/src/surface.ts CHANGED
@@ -97,10 +97,10 @@ export const COMMANDS: readonly Command[] = [
97
97
  name: "build",
98
98
  group: "Develop",
99
99
  emoji: "🔨",
100
- args: "[project-dir] [--target host] [--standalone]",
100
+ args: "[project-dir] [--target host] [--standalone] [--emit-only]",
101
101
  brief: "[project-dir]",
102
102
  summary:
103
- "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.",
103
+ "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. `--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.",
104
104
  },
105
105
  {
106
106
  name: "toolchain",