sproutboat 0.11.10 → 0.11.12
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 +25 -1
- package/SURFACE.md +5 -3
- package/THIRD_PARTY_NOTICES.md +3 -5
- package/package.json +10 -11
- package/scripts/build-cli.ts +2 -44
- package/src/build.ts +12 -1
- package/src/bundle.ts +3 -3
- package/src/compile.ts +8 -18
- package/src/credentials.ts +23 -9
- package/src/dev.ts +10 -0
- package/src/main.ts +82 -23
- package/src/surface.ts +8 -0
- package/src/toolchain.ts +3 -8
- package/src/types-gen.ts +3 -0
- package/types/sproutboat.d.ts +24 -3
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,28 @@ a breaking change.
|
|
|
8
8
|
Reconstructed from git history on 2026-09-03 for everything through v0.4.11;
|
|
9
9
|
maintained going forward by the `release` skill.
|
|
10
10
|
|
|
11
|
+
## [0.11.12] - 2026-09-24
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- Porffor alpha 9 handles ESM modules without the external esbuild executable.
|
|
15
|
+
The CLI now requires `@sproutboat/toolchain@^0.4.12`, runtime `^0.12.1`,
|
|
16
|
+
and wire `^0.9.1`.
|
|
17
|
+
Removed the CLI's esbuild dependency, platform binary copies, and compile-time
|
|
18
|
+
PATH lookup. Handler validation and shimming still use Bun's built-in bundler.
|
|
19
|
+
The required `esbuildVersion` manifest field remains for older artifact
|
|
20
|
+
readers and now records `unused`.
|
|
21
|
+
- Local `dev` rebuilds keep a stable generated entry path for each process, so
|
|
22
|
+
Porffor alpha 9 can reuse unchanged compiled C units. In a local `hello`
|
|
23
|
+
handler-edit comparison, compilation took 549 ms with the stable path versus
|
|
24
|
+
1,505 ms with changing paths. Candidate binaries remain isolated.
|
|
25
|
+
|
|
26
|
+
## [0.11.11] - 2026-09-24
|
|
27
|
+
### Fixed
|
|
28
|
+
- Updated the native runtime to `0.12.0` so repeated HMAC signing uses the raw
|
|
29
|
+
digest bytes between rounds, matching WebCrypto and existing password hashes.
|
|
30
|
+
- Updated the toolchain to `0.4.11` so `Uint8Array.from({ length: 16 }, mapFn)`
|
|
31
|
+
produces the intended key bytes in standalone native builds.
|
|
32
|
+
- Added the two-round HMAC vector to broker and standalone conformance checks.
|
|
11
33
|
## [0.11.10] - 2026-09-18
|
|
12
34
|
### Changed
|
|
13
35
|
- Bumped `@sproutboat/toolchain` to `0.4.10`, pinning Porffor to the
|
|
@@ -588,7 +610,9 @@ its own package.
|
|
|
588
610
|
- Renamed the package to `sproutboat` (was `@sproutboat/cli`); dropped the
|
|
589
611
|
`sprout` bin alias in favour of a user-defined shell alias.
|
|
590
612
|
|
|
591
|
-
[Unreleased]: https://github.com/baronunread/sproutboat-cli/compare/v0.11.
|
|
613
|
+
[Unreleased]: https://github.com/baronunread/sproutboat-cli/compare/v0.11.12...HEAD
|
|
614
|
+
[0.11.12]: https://github.com/baronunread/sproutboat-cli/compare/v0.11.11...v0.11.12
|
|
615
|
+
[0.11.11]: https://github.com/baronunread/sproutboat-cli/compare/v0.11.10...v0.11.11
|
|
592
616
|
[0.11.10]: https://github.com/baronunread/sproutboat-cli/compare/v0.11.9...v0.11.10
|
|
593
617
|
[0.11.9]: https://github.com/baronunread/sproutboat-cli/compare/v0.11.8...v0.11.9
|
|
594
618
|
[0.11.8]: https://github.com/baronunread/sproutboat-cli/compare/v0.11.7...v0.11.8
|
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.11.
|
|
6
|
+
**Package:** `sproutboat` 0.11.12 · runs on Bun (use `bunx`, not `npx`)
|
|
7
7
|
|
|
8
8
|
## Commands
|
|
9
9
|
|
|
@@ -29,9 +29,10 @@
|
|
|
29
29
|
| `login` | `[--api-url <url>] [--token <token>]` | Device-code browser flow, or store <token> for <url> directly. |
|
|
30
30
|
| `logout` | `[--api-url <url>]` | Forget the stored credential for the active endpoint, or for <url>. |
|
|
31
31
|
| `whoami` | — | Show the active endpoint and the account the stored token belongs to. |
|
|
32
|
+
| `complete` | `[bash | zsh | fish]` | Print a shell completion script for the top-level commands (defaults to $SHELL). |
|
|
32
33
|
|
|
33
34
|
```
|
|
34
|
-
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>
|
|
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
36
|
```
|
|
36
37
|
|
|
37
38
|
## Environment variables
|
|
@@ -57,6 +58,7 @@ usage: sproutboat <init [name] | check [project-dir] | types [project-dir] | dev
|
|
|
57
58
|
| `NO_COLOR` | When set, disables coloured terminal output (https://no-color.org). Output is also plain whenever stdout is not a TTY. |
|
|
58
59
|
| `SPROUTBOAT_NO_UPDATE_CHECK` | When set, skips the once-a-day npm check for a newer `sproutboat` release (also skipped when CI is set). |
|
|
59
60
|
| `XDG_CONFIG_HOME` | Base for the default credentials dir when SPROUTBOAT_CONFIG_DIR is unset. |
|
|
61
|
+
| `SHELL` | Picks the default shell for `complete` with no argument (bash/zsh/fish). |
|
|
60
62
|
| `PORFFOR_VERSION` | Override the Porffor identity string recorded in the manifest. |
|
|
61
63
|
| `SB_BROKER_PORT` | Loopback port of the binding broker, read by the compiled sprout at runtime (set by the control plane, or by `src/broker.ts` for local runs). |
|
|
62
64
|
| `SB_BROKER_TOKEN` | Per-deployment auth token the sprout sends on every broker frame, and the broker sends back on scheduled/queue triggers (paired with SB_BROKER_PORT). |
|
|
@@ -74,7 +76,7 @@ usage: sproutboat <init [name] | check [project-dir] | types [project-dir] | dev
|
|
|
74
76
|
| | |
|
|
75
77
|
| --- | --- |
|
|
76
78
|
| Zig | `0.16.0` (`zig cc -target x86_64-linux-musl`, static) |
|
|
77
|
-
| Provenance stamp | `zig-musl/0.16.0+porffor/
|
|
79
|
+
| Provenance stamp | `zig-musl/0.16.0+porffor/de4eb58+uws/360c276d` |
|
|
78
80
|
| Artifact schema | `2` |
|
|
79
81
|
| Runtime | `native-fetch` |
|
|
80
82
|
| Capability profile | `http-sync-v0` |
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -6,10 +6,9 @@ to its upstream source.
|
|
|
6
6
|
|
|
7
7
|
| Component | Version or revision | Distribution role | License | Source |
|
|
8
8
|
| --- | --- | --- | --- | --- |
|
|
9
|
-
| Porffor | `
|
|
9
|
+
| Porffor | `de4eb588264885b3a1596f75010e371a2052033f` | Acquired on first application build | MIT | https://github.com/CanadaHonk/porffor |
|
|
10
10
|
| Zig | `0.16.0` | Acquired cross-compiler | MIT | https://ziglang.org |
|
|
11
11
|
| uWebSockets and uSockets | `360c276d609d59af56ae6932adb95154ace9f15f` | Embedded source and static archive | Apache-2.0 | https://github.com/uNetworking/uWebSockets |
|
|
12
|
-
| esbuild | `0.28.2` | Embedded platform executable | MIT | https://esbuild.github.io |
|
|
13
12
|
| SQLite | `3.50.4` | Acquired for standalone builds | Public domain | https://sqlite.org |
|
|
14
13
|
| BearSSL | `0.6` | Acquired for standalone builds | MIT | https://bearssl.org |
|
|
15
14
|
|
|
@@ -17,7 +16,7 @@ to its upstream source.
|
|
|
17
16
|
|
|
18
17
|
| Component | SHA-256 |
|
|
19
18
|
| --- | --- |
|
|
20
|
-
| Porffor source archive | `
|
|
19
|
+
| Porffor source archive | `1a62187a93356b36cb6ac703ce9770c8917e4fa65545fd7c33ee8a180ae5a4c9` |
|
|
21
20
|
| Embedded uWebSockets archive | `e83736f3f8cf9d56a1ebe6ea61625a7af12386763374d47c14cff472ada7484a` |
|
|
22
21
|
| SQLite amalgamation archive | `1d3049dd0f830a025a53105fc79fd2ab9431aea99e137809d064d8ee8356b032` |
|
|
23
22
|
| BearSSL source archive | `6705bba1714961b41a728dfc5debbe348d2966c117649392f8c8139efc83ff14` |
|
|
@@ -25,5 +24,4 @@ to its upstream source.
|
|
|
25
24
|
|
|
26
25
|
The complete Apache-2.0 notice for uWebSockets and uSockets is retained in the
|
|
27
26
|
embedded source archive. Porffor and BearSSL retain their MIT notices in the
|
|
28
|
-
source archives acquired at the revisions above.
|
|
29
|
-
available from its upstream distribution.
|
|
27
|
+
source archives acquired at the revisions above.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sproutboat",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.12",
|
|
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",
|
|
@@ -62,12 +62,11 @@
|
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
64
|
"@sproutboat/artifact": "^0.2.0",
|
|
65
|
-
"@sproutboat/assets": "^0.
|
|
66
|
-
"@sproutboat/config": "^0.
|
|
67
|
-
"@sproutboat/runtime": "^0.
|
|
68
|
-
"@sproutboat/toolchain": "^0.4.
|
|
69
|
-
"@sproutboat/wire": "^0.
|
|
70
|
-
"esbuild": "^0.28.2"
|
|
65
|
+
"@sproutboat/assets": "^0.3.0",
|
|
66
|
+
"@sproutboat/config": "^0.4.0",
|
|
67
|
+
"@sproutboat/runtime": "^0.12.1",
|
|
68
|
+
"@sproutboat/toolchain": "^0.4.12",
|
|
69
|
+
"@sproutboat/wire": "^0.9.1"
|
|
71
70
|
},
|
|
72
71
|
"devDependencies": {
|
|
73
72
|
"@oxlint/plugins": "1.81.0",
|
|
@@ -78,10 +77,10 @@
|
|
|
78
77
|
"typescript": "7.0.2"
|
|
79
78
|
},
|
|
80
79
|
"optionalDependencies": {
|
|
81
|
-
"@sproutboat/cli-darwin-arm64": "0.11.
|
|
82
|
-
"@sproutboat/cli-darwin-x64": "0.11.
|
|
83
|
-
"@sproutboat/cli-linux-arm64": "0.11.
|
|
84
|
-
"@sproutboat/cli-linux-x64": "0.11.
|
|
80
|
+
"@sproutboat/cli-darwin-arm64": "0.11.12",
|
|
81
|
+
"@sproutboat/cli-darwin-x64": "0.11.12",
|
|
82
|
+
"@sproutboat/cli-linux-arm64": "0.11.12",
|
|
83
|
+
"@sproutboat/cli-linux-x64": "0.11.12"
|
|
85
84
|
},
|
|
86
85
|
"engines": {
|
|
87
86
|
"node": ">=18"
|
package/scripts/build-cli.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* GitHub's only x64 macOS image (`macos-13`) routinely sits queued for half an
|
|
16
16
|
* hour while every other leg finishes in one minute.
|
|
17
17
|
*/
|
|
18
|
-
import { chmod, copyFile, mkdir } from "node:fs/promises";
|
|
18
|
+
import { chmod, copyFile, mkdir, rm } from "node:fs/promises";
|
|
19
19
|
import { resolve } from "node:path";
|
|
20
20
|
import { ensureZigArchive, type ZigPlatform } from "../src/toolchain";
|
|
21
21
|
|
|
@@ -39,49 +39,11 @@ function hostTarget(): Target {
|
|
|
39
39
|
|
|
40
40
|
const root = resolve(import.meta.dir, "..");
|
|
41
41
|
|
|
42
|
-
/** esbuild ships one binary per platform (`@esbuild/<os>-<arch>`); a cross
|
|
43
|
-
* target's copy is never installed locally (npm/bun refuse it — the package
|
|
44
|
-
* declares `os`/`cpu` and gets skipped for any other host), so fetch that
|
|
45
|
-
* platform's tarball from the registry instead, same shape as ensureZig's
|
|
46
|
-
* prebuilt-archive fetches. */
|
|
47
|
-
async function esbuildBinaryFor(target: Target): Promise<string> {
|
|
48
|
-
const host = hostTarget();
|
|
49
|
-
if (target.platform === host.platform && target.arch === host.arch) {
|
|
50
|
-
try {
|
|
51
|
-
return Bun.resolveSync(`@esbuild/${target.platform}-${target.arch}/bin/esbuild`, import.meta.dir);
|
|
52
|
-
} catch {
|
|
53
|
-
throw new Error("host esbuild binary is missing; run bun install first");
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
// SAFETY: package.json is this repo's own manifest, with a required esbuild dependency entry.
|
|
57
|
-
const pkg = (await Bun.file(resolve(root, "package.json")).json()) as { dependencies: { esbuild: string } };
|
|
58
|
-
const version = pkg.dependencies.esbuild.replace(/^[\^~]/, "");
|
|
59
|
-
const name = `${target.platform}-${target.arch}`;
|
|
60
|
-
const cacheDir = resolve(
|
|
61
|
-
process.env.SPROUTBOAT_TOOLCHAIN_CACHE ?? `${process.env.HOME}/.cache/sproutboat`,
|
|
62
|
-
`esbuild-${version}-${name}`,
|
|
63
|
-
);
|
|
64
|
-
const cached = resolve(cacheDir, "esbuild");
|
|
65
|
-
if (await Bun.file(cached).exists()) return cached;
|
|
66
|
-
const res = await fetch(`https://registry.npmjs.org/@esbuild/${name}/-/${name}-${version}.tgz`);
|
|
67
|
-
if (!res.ok) throw new Error(`could not fetch @esbuild/${name}@${version}: HTTP ${res.status}`);
|
|
68
|
-
const tarPath = resolve(cacheDir, "esbuild.tgz");
|
|
69
|
-
await mkdir(cacheDir, { recursive: true });
|
|
70
|
-
await Bun.write(tarPath, await res.arrayBuffer());
|
|
71
|
-
const untar = Bun.spawn(["tar", "-xzf", tarPath, "-C", cacheDir, "--strip-components=2", "package/bin/esbuild"], {
|
|
72
|
-
stdout: "pipe",
|
|
73
|
-
stderr: "pipe",
|
|
74
|
-
});
|
|
75
|
-
const [code, err] = await Promise.all([untar.exited, new Response(untar.stderr).text()]);
|
|
76
|
-
if (code !== 0) throw new Error(`could not extract @esbuild/${name}: ${err.trim()}`);
|
|
77
|
-
await chmod(cached, 0o755);
|
|
78
|
-
return cached;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
42
|
async function buildOne(target: Target): Promise<void> {
|
|
82
43
|
const packageDir = resolve(root, "platform-packages", `${target.platform}-${target.arch}`);
|
|
83
44
|
const out = resolve(packageDir, "bin", "sproutboat");
|
|
84
45
|
await mkdir(resolve(out, ".."), { recursive: true });
|
|
46
|
+
await rm(resolve(packageDir, "bin", "esbuild"), { force: true });
|
|
85
47
|
// SAFETY: package.json is this release's manifest and npm requires its version.
|
|
86
48
|
const version = ((await Bun.file(resolve(root, "package.json")).json()) as { version: string }).version;
|
|
87
49
|
// SAFETY: every checked-in platform package manifest owns a required string version.
|
|
@@ -103,9 +65,6 @@ async function buildOne(target: Target): Promise<void> {
|
|
|
103
65
|
{ cwd: root, stdout: "inherit", stderr: "inherit" },
|
|
104
66
|
);
|
|
105
67
|
if ((await child.exited) !== 0) throw new Error(`compile failed for ${target.platform}-${target.arch}`);
|
|
106
|
-
const esbuild = await esbuildBinaryFor(target);
|
|
107
|
-
const packagedEsbuild = resolve(packageDir, "bin", "esbuild");
|
|
108
|
-
await copyFile(esbuild, packagedEsbuild);
|
|
109
68
|
// The compressed archive (~50 MB), not the ~400 MB extracted install: Zig's
|
|
110
69
|
// lib/ carries libc/libc++ sources for every target it can cross-compile to,
|
|
111
70
|
// which only matters once, on extract. `ensureZig` (src/toolchain.ts) looks
|
|
@@ -114,7 +73,6 @@ async function buildOne(target: Target): Promise<void> {
|
|
|
114
73
|
// same shape as the vendored uWebSockets archive.
|
|
115
74
|
await copyFile(await ensureZigArchive(target.zigPlatform), resolve(packageDir, "bin", "zig.tar.xz"));
|
|
116
75
|
await copyFile(resolve(root, "THIRD_PARTY_NOTICES.md"), resolve(packageDir, "THIRD_PARTY_NOTICES.md"));
|
|
117
|
-
await chmod(packagedEsbuild, 0o755);
|
|
118
76
|
await chmod(out, 0o755);
|
|
119
77
|
}
|
|
120
78
|
|
package/src/build.ts
CHANGED
|
@@ -47,6 +47,8 @@ export type BuildInput = {
|
|
|
47
47
|
reuseSproutPath?: string;
|
|
48
48
|
/** An isolated artifact directory, used by dev candidates to avoid mixing snapshots. */
|
|
49
49
|
outputDirectory?: string;
|
|
50
|
+
/** Stable compiler input used only by the local dev loop. */
|
|
51
|
+
generatedPath?: string;
|
|
50
52
|
};
|
|
51
53
|
|
|
52
54
|
export type BuildOutput = {
|
|
@@ -171,10 +173,18 @@ export async function buildArtifact(input: BuildInput): Promise<BuildOutput> {
|
|
|
171
173
|
bakedAssets = { manifest, files };
|
|
172
174
|
}
|
|
173
175
|
|
|
176
|
+
// Hoisted above the compile: #126 bakes it into the binary, and the manifest
|
|
177
|
+
// records the same instant rather than a slightly later one.
|
|
178
|
+
const builtAt = new Date().toISOString();
|
|
179
|
+
const versionMetadata = input.config.version_metadata
|
|
180
|
+
? { binding: input.config.version_metadata, id: artifactId, tag: input.config.name, timestamp: builtAt }
|
|
181
|
+
: undefined;
|
|
182
|
+
|
|
174
183
|
if (input.reuseSproutPath) await cp(input.reuseSproutPath, sproutPath);
|
|
175
184
|
else
|
|
176
185
|
await compileSprout({
|
|
177
186
|
sourcePath: input.sourcePath,
|
|
187
|
+
generatedPath: input.generatedPath,
|
|
178
188
|
source: input.source,
|
|
179
189
|
outPath: sproutPath,
|
|
180
190
|
vars: input.config.vars ?? {},
|
|
@@ -188,6 +198,7 @@ export async function buildArtifact(input: BuildInput): Promise<BuildOutput> {
|
|
|
188
198
|
extraLink,
|
|
189
199
|
extraCflags,
|
|
190
200
|
optimize: input.optimize,
|
|
201
|
+
versionMetadata,
|
|
191
202
|
});
|
|
192
203
|
|
|
193
204
|
const sprout = await readFile(sproutPath);
|
|
@@ -204,7 +215,7 @@ export async function buildArtifact(input: BuildInput): Promise<BuildOutput> {
|
|
|
204
215
|
sourceHash,
|
|
205
216
|
binaryHash: digest(sprout),
|
|
206
217
|
binarySize: (await stat(sproutPath)).size,
|
|
207
|
-
builtAt
|
|
218
|
+
builtAt,
|
|
208
219
|
};
|
|
209
220
|
await writeFile(resolve(artifactDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
210
221
|
// Bindings live beside the manifest, not in it: the artifact manifest schema is
|
package/src/bundle.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* #89 — bundle the handler before Porffor sees it.
|
|
3
3
|
*
|
|
4
|
-
* Porffor
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Porffor alpha 9 supports ESM imports. We still bundle first so capability
|
|
5
|
+
* validation sees every dependency and the runtime wrapper receives one
|
|
6
|
+
* controlled default export.
|
|
7
7
|
*
|
|
8
8
|
* Bun's bundler resolves relative imports across the project and bare
|
|
9
9
|
* specifiers out of the project's own `node_modules`, then emits a single ESM
|
package/src/compile.ts
CHANGED
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
* unusable it stops with its specific integrity or archive error. Contributors
|
|
11
11
|
* may explicitly opt into Porffor's source fallback.
|
|
12
12
|
*/
|
|
13
|
-
import { existsSync } from "node:fs";
|
|
14
13
|
import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
15
14
|
import { dirname, resolve } from "node:path";
|
|
16
15
|
import { ensurePorfforPatched } from "./patch-porffor";
|
|
@@ -50,6 +49,8 @@ const COMPILE_TIMEOUT_MS = Number(process.env.SPROUTBOAT_COMPILE_TIMEOUT_MS || 6
|
|
|
50
49
|
|
|
51
50
|
export type CompileInput = {
|
|
52
51
|
sourcePath: string;
|
|
52
|
+
/** Stable compiler input for dev rebuilds, independent of the candidate binary path. */
|
|
53
|
+
generatedPath?: string;
|
|
53
54
|
/** The bundled module (#89). Falls back to reading `sourcePath` verbatim. */
|
|
54
55
|
source?: string;
|
|
55
56
|
outPath: string;
|
|
@@ -80,6 +81,8 @@ export type CompileInput = {
|
|
|
80
81
|
optimize?: "dev" | "release";
|
|
81
82
|
/** Cross-compiler for `linux-x86_64`. Not needed, and not used, for `host`. */
|
|
82
83
|
zigBin?: string;
|
|
84
|
+
/** #126 — `env.<binding> = { id, tag, timestamp }`, baked directly into the module. */
|
|
85
|
+
versionMetadata?: { binding: string; id: string; tag: string; timestamp: string };
|
|
83
86
|
/**
|
|
84
87
|
* `linux-x86_64` (default) cross-compiles the static musl binary every box
|
|
85
88
|
* runs. `host` compiles for the machine doing the build (#62) so `sproutboat
|
|
@@ -193,7 +196,8 @@ export async function compileSprout(input: CompileInput): Promise<void> {
|
|
|
193
196
|
// OS may still be executing. The linker cannot overwrite either, so clear it
|
|
194
197
|
// first rather than failing with "can't write output file".
|
|
195
198
|
await rm(input.outPath, { force: true });
|
|
196
|
-
const generatedPath = resolve(outDir, "sprout.generated.js");
|
|
199
|
+
const generatedPath = input.generatedPath ?? resolve(outDir, "sprout.generated.js");
|
|
200
|
+
await mkdir(dirname(generatedPath), { recursive: true });
|
|
197
201
|
const [source, prelude] = await Promise.all([
|
|
198
202
|
input.source === undefined ? readFile(input.sourcePath, "utf8") : Promise.resolve(input.source),
|
|
199
203
|
loadPrelude(input.transport ?? "broker"),
|
|
@@ -210,6 +214,7 @@ export async function compileSprout(input: CompileInput): Promise<void> {
|
|
|
210
214
|
input.appName,
|
|
211
215
|
input.assets,
|
|
212
216
|
input.transport,
|
|
217
|
+
input.versionMetadata,
|
|
213
218
|
),
|
|
214
219
|
);
|
|
215
220
|
|
|
@@ -219,22 +224,7 @@ export async function compileSprout(input: CompileInput): Promise<void> {
|
|
|
219
224
|
// below instead and has no zigBin to contribute here.
|
|
220
225
|
const zigDir = input.zigBin ? `${dirname(input.zigBin)}:` : "";
|
|
221
226
|
const hostCompiler = input.target === "host" ? await resolveHostCompiler() : undefined;
|
|
222
|
-
|
|
223
|
-
// there. Running from the npm package under Bun, `process.execPath` is Bun
|
|
224
|
-
// itself and `npm i -g` puts no dependency `.bin` on PATH, so resolve the
|
|
225
|
-
// esbuild dependency directly. `Bun.which` is the last resort.
|
|
226
|
-
const packagedEsbuild = resolve(dirname(process.execPath), "esbuild");
|
|
227
|
-
let esbuild: string | null = existsSync(packagedEsbuild) ? packagedEsbuild : null;
|
|
228
|
-
if (!esbuild) {
|
|
229
|
-
try {
|
|
230
|
-
esbuild = Bun.resolveSync("esbuild/bin/esbuild", import.meta.dir);
|
|
231
|
-
} catch {
|
|
232
|
-
esbuild = null;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
esbuild ??= Bun.which("esbuild");
|
|
236
|
-
if (!esbuild) throw new Error("esbuild could not be located; install it or reinstall sproutboat");
|
|
237
|
-
const path = `${zigDir}${dirname(esbuild)}:${process.env.PATH ?? ""}`;
|
|
227
|
+
const path = `${zigDir}${process.env.PATH ?? ""}`;
|
|
238
228
|
const command = PACKAGED
|
|
239
229
|
? [process.execPath, "__porffor", launcher]
|
|
240
230
|
: [process.execPath, resolve(import.meta.dir, "main.ts"), "__porffor", launcher];
|
package/src/credentials.ts
CHANGED
|
@@ -59,23 +59,37 @@ export async function activeApiUrl(): Promise<string | undefined> {
|
|
|
59
59
|
return (await readCredentials()).activeApiUrl;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
/**
|
|
63
|
-
|
|
62
|
+
/**
|
|
63
|
+
* #79 `logout`: drop one endpoint's token. Returns `removed: false` when none
|
|
64
|
+
* was stored. baronunread/sproutboat#204 — used to repoint `activeApiUrl` to
|
|
65
|
+
* an arbitrary surviving profile chosen by object key order; now it just
|
|
66
|
+
* clears the active endpoint, so the next command falls through to
|
|
67
|
+
* `resolveApiUrl()`'s documented default instead of a silently-picked one.
|
|
68
|
+
*/
|
|
69
|
+
export async function forgetToken(apiUrl: string): Promise<{ removed: boolean; activeApiUrl?: string }> {
|
|
64
70
|
const credentials = await readCredentials();
|
|
65
|
-
if (!credentials.profiles[apiUrl]) return false;
|
|
71
|
+
if (!credentials.profiles[apiUrl]) return { removed: false };
|
|
66
72
|
delete credentials.profiles[apiUrl];
|
|
67
|
-
if (credentials.activeApiUrl === apiUrl)
|
|
68
|
-
credentials.activeApiUrl = Object.keys(credentials.profiles)[0];
|
|
69
|
-
}
|
|
73
|
+
if (credentials.activeApiUrl === apiUrl) credentials.activeApiUrl = undefined;
|
|
70
74
|
await writeCredentials(credentials);
|
|
71
|
-
return true;
|
|
75
|
+
return { removed: true, activeApiUrl: credentials.activeApiUrl };
|
|
72
76
|
}
|
|
73
77
|
|
|
74
|
-
|
|
78
|
+
/**
|
|
79
|
+
* baronunread/sproutboat#204 — used to unconditionally repoint the
|
|
80
|
+
* machine-wide active endpoint to whatever was just logged into, so logging
|
|
81
|
+
* into a local dev control plane for one project silently became the deploy
|
|
82
|
+
* target for every unrelated project on the machine. Now the active endpoint
|
|
83
|
+
* is only set the first time (nothing saved yet); a later login to a second
|
|
84
|
+
* endpoint keeps the existing active endpoint and reports it, so the caller
|
|
85
|
+
* can say so instead of silently switching.
|
|
86
|
+
*/
|
|
87
|
+
export async function saveToken(apiUrl: string, token: string): Promise<{ activeApiUrl: string }> {
|
|
75
88
|
const credentials = await readCredentials();
|
|
76
89
|
credentials.profiles[apiUrl] = { token };
|
|
77
|
-
credentials.activeApiUrl = apiUrl;
|
|
90
|
+
if (!credentials.activeApiUrl) credentials.activeApiUrl = apiUrl;
|
|
78
91
|
await writeCredentials(credentials);
|
|
92
|
+
return { activeApiUrl: credentials.activeApiUrl };
|
|
79
93
|
}
|
|
80
94
|
|
|
81
95
|
/** Atomic 0600 write of the whole credentials file. */
|
package/src/dev.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* project, one port.
|
|
12
12
|
*/
|
|
13
13
|
import { existsSync, watch, type FSWatcher } from "node:fs";
|
|
14
|
+
import { randomUUID } from "node:crypto";
|
|
14
15
|
import { mkdir, readFile, rm } from "node:fs/promises";
|
|
15
16
|
import { connect } from "node:net";
|
|
16
17
|
import { dirname, resolve } from "node:path";
|
|
@@ -21,6 +22,7 @@ import { amber, dim, leaf, ok } from "./style";
|
|
|
21
22
|
import type { SproutboatConfig } from "./config";
|
|
22
23
|
|
|
23
24
|
const RESTART_DEBOUNCE_MS = 120;
|
|
25
|
+
const DEV_COMPILE_SESSION = randomUUID();
|
|
24
26
|
|
|
25
27
|
export type DevInput = {
|
|
26
28
|
projectDir: string;
|
|
@@ -121,6 +123,14 @@ async function start(input: DevInput, port: number): Promise<Running> {
|
|
|
121
123
|
optimize: "dev",
|
|
122
124
|
reuseSproutPath: input.reuseSproutPath,
|
|
123
125
|
outputDirectory: candidateDir,
|
|
126
|
+
// Alpha 9 keys its C-unit cache by the entry's absolute path. Keep that
|
|
127
|
+
// path fixed across saves while each candidate keeps its own binary.
|
|
128
|
+
generatedPath: resolve(
|
|
129
|
+
input.projectDir,
|
|
130
|
+
".sproutboat/dev-compile",
|
|
131
|
+
`${process.pid}-${DEV_COMPILE_SESSION}`,
|
|
132
|
+
"sprout.generated.js",
|
|
133
|
+
),
|
|
124
134
|
});
|
|
125
135
|
} catch (error) {
|
|
126
136
|
await rm(candidateDir, { recursive: true, force: true });
|
package/src/main.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { buildStandalone } 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";
|
|
17
|
-
import { helpText, STORAGE_PRODUCTS, STORAGE_VERBS, type StorageProduct } from "./surface";
|
|
17
|
+
import { CLI_NAME, COMMANDS, helpText, STORAGE_PRODUCTS, STORAGE_VERBS, type StorageProduct } from "./surface";
|
|
18
18
|
import { notifyIfOutdated } from "./update-check";
|
|
19
19
|
import { controlVersionWarning } from "./api-version";
|
|
20
20
|
import { amber, bold, dim, leaf, ok, rose } from "./style";
|
|
@@ -35,6 +35,26 @@ if (internalModeAt > 0) {
|
|
|
35
35
|
|
|
36
36
|
const defaultApiUrl = "https://dashboard.sproutboat.com";
|
|
37
37
|
|
|
38
|
+
type ApiUrlSource = "SPROUTBOAT_API_URL" | "--api-url" | "saved active endpoint" | "default";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* baronunread/sproutboat#204 — the single place `defaultApiUrl` and
|
|
42
|
+
* `activeApiUrl()` are read. Used to be implemented three different ways
|
|
43
|
+
* across `apiCredentials()`, `whoami()` and `parseLoginArgs()`, none of which
|
|
44
|
+
* told the user which endpoint it picked; `whoami` could report "not logged
|
|
45
|
+
* in" on a machine where `deploy` would happily target prod. Precedence: an
|
|
46
|
+
* explicit `--api-url` flag, then `SPROUTBOAT_API_URL`, then the saved active
|
|
47
|
+
* endpoint, then the default.
|
|
48
|
+
*/
|
|
49
|
+
async function resolveApiUrl(explicitApiUrl?: string): Promise<{ apiUrl: string; source: ApiUrlSource }> {
|
|
50
|
+
if (explicitApiUrl) return { apiUrl: explicitApiUrl.replace(/\/$/, ""), source: "--api-url" };
|
|
51
|
+
const envUrl = process.env.SPROUTBOAT_API_URL;
|
|
52
|
+
if (envUrl) return { apiUrl: envUrl.replace(/\/$/, ""), source: "SPROUTBOAT_API_URL" };
|
|
53
|
+
const saved = await activeApiUrl();
|
|
54
|
+
if (saved) return { apiUrl: saved.replace(/\/$/, ""), source: "saved active endpoint" };
|
|
55
|
+
return { apiUrl: defaultApiUrl, source: "default" };
|
|
56
|
+
}
|
|
57
|
+
|
|
38
58
|
/** Warn at most once per run: every API response carries the headers, and one
|
|
39
59
|
* command makes several calls. */
|
|
40
60
|
let skewWarned = false;
|
|
@@ -407,10 +427,15 @@ async function provisionBindings(directory = process.cwd()): Promise<void> {
|
|
|
407
427
|
|
|
408
428
|
/** #79 — wrangler parity: drop the stored credential for an endpoint. */
|
|
409
429
|
async function logout(args: string[]) {
|
|
410
|
-
const { apiUrl } = parseLoginArgs(args);
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
430
|
+
const { apiUrl } = await parseLoginArgs(args);
|
|
431
|
+
const result = await forgetToken(apiUrl);
|
|
432
|
+
if (!result.removed) {
|
|
433
|
+
console.log(`no stored credential for ${apiUrl}`);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
console.log(ok(`forgot the credential for ${apiUrl}`));
|
|
437
|
+
if (result.activeApiUrl === undefined)
|
|
438
|
+
console.log(dim("no active endpoint remains — the next command falls through to the default"));
|
|
414
439
|
}
|
|
415
440
|
|
|
416
441
|
/**
|
|
@@ -419,13 +444,9 @@ async function logout(args: string[]) {
|
|
|
419
444
|
* works rather than only reporting what is on disk.
|
|
420
445
|
*/
|
|
421
446
|
async function whoami() {
|
|
422
|
-
const apiUrl
|
|
423
|
-
if (!apiUrl) {
|
|
424
|
-
console.log("not logged in — run `sproutboat login`");
|
|
425
|
-
return;
|
|
426
|
-
}
|
|
447
|
+
const { apiUrl, source } = await resolveApiUrl();
|
|
427
448
|
const token = process.env.SPROUTBOAT_TOKEN || (await savedToken(apiUrl));
|
|
428
|
-
console.log(`endpoint ${apiUrl}`);
|
|
449
|
+
console.log(`endpoint ${apiUrl} (${source})`);
|
|
429
450
|
if (!token) {
|
|
430
451
|
console.log(`account ${dim("no stored token — run `sproutboat login`")}`);
|
|
431
452
|
return;
|
|
@@ -489,7 +510,8 @@ async function deploy(args: string[]) {
|
|
|
489
510
|
console.log("\n--dry-run: not uploading.");
|
|
490
511
|
return;
|
|
491
512
|
}
|
|
492
|
-
const { apiUrl, token } = await apiCredentials();
|
|
513
|
+
const { apiUrl, token, source } = await apiCredentials();
|
|
514
|
+
console.log(`endpoint ${apiUrl} (${source})`);
|
|
493
515
|
const form = new FormData();
|
|
494
516
|
form.set("manifest", new File([await manifest.arrayBuffer()], "manifest.json", { type: "application/json" }));
|
|
495
517
|
form.set("sprout", new File([await sprout.arrayBuffer()], "sprout", { type: "application/octet-stream" }));
|
|
@@ -516,7 +538,7 @@ async function deploy(args: string[]) {
|
|
|
516
538
|
}
|
|
517
539
|
}
|
|
518
540
|
|
|
519
|
-
const response = await fetch(`${apiUrl
|
|
541
|
+
const response = await fetch(`${apiUrl}/api/projects/${projectName}/deployments`, {
|
|
520
542
|
method: "POST",
|
|
521
543
|
headers: { "x-api-key": token },
|
|
522
544
|
body: form,
|
|
@@ -579,25 +601,34 @@ async function waitForHealthy(url: string, timeoutMs: number): Promise<boolean>
|
|
|
579
601
|
return false;
|
|
580
602
|
}
|
|
581
603
|
|
|
582
|
-
function parseLoginArgs(args: string[]) {
|
|
583
|
-
let
|
|
604
|
+
async function parseLoginArgs(args: string[]) {
|
|
605
|
+
let explicitApiUrl: string | undefined;
|
|
584
606
|
let token: string | undefined;
|
|
585
607
|
for (let index = 0; index < args.length; index += 2) {
|
|
586
608
|
const value = args[index + 1];
|
|
587
|
-
if (args[index] === "--api-url" && value)
|
|
609
|
+
if (args[index] === "--api-url" && value) explicitApiUrl = value;
|
|
588
610
|
else if (args[index] === "--token" && value) token = value;
|
|
589
611
|
else usageError(`login: unexpected argument "${args[index]}"`, "login [--api-url <url>] [--token <token>]");
|
|
590
612
|
}
|
|
591
|
-
|
|
613
|
+
const { apiUrl, source } = await resolveApiUrl(explicitApiUrl);
|
|
614
|
+
return { apiUrl, source, token };
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/** Announce a login that left the machine's active endpoint pointed elsewhere. */
|
|
618
|
+
function reportActiveEndpoint(apiUrl: string, active: string): void {
|
|
619
|
+
if (active !== apiUrl)
|
|
620
|
+
console.log(dim(`active endpoint remains ${active} — \`sproutboat logout\` it first to switch`));
|
|
592
621
|
}
|
|
593
622
|
|
|
594
623
|
async function login(args: string[]) {
|
|
595
|
-
const { apiUrl, token: directToken } = parseLoginArgs(args);
|
|
624
|
+
const { apiUrl, source, token: directToken } = await parseLoginArgs(args);
|
|
625
|
+
console.log(`endpoint ${apiUrl} (${source})`);
|
|
596
626
|
// Self-hosted / non-interactive: skip the browser flow and store the token
|
|
597
627
|
// the admin already holds (e.g. SPROUTBOAT_BOOTSTRAP_TOKEN).
|
|
598
628
|
if (directToken) {
|
|
599
|
-
await saveToken(apiUrl, directToken);
|
|
629
|
+
const { activeApiUrl: active } = await saveToken(apiUrl, directToken);
|
|
600
630
|
console.log(`Saved credentials for ${apiUrl}.`);
|
|
631
|
+
reportActiveEndpoint(apiUrl, active);
|
|
601
632
|
return;
|
|
602
633
|
}
|
|
603
634
|
const response = await fetch(`${apiUrl}/api/cli/authorizations`, { method: "POST" });
|
|
@@ -620,7 +651,7 @@ async function login(args: string[]) {
|
|
|
620
651
|
console.log(`Confirm code: ${authorization.userCode}`);
|
|
621
652
|
while (new Date(authorization.expiresAt).getTime() > Date.now()) {
|
|
622
653
|
await Bun.sleep(Math.max(authorization.interval, 1) * 1000);
|
|
623
|
-
const exchange = await fetch(`${apiUrl
|
|
654
|
+
const exchange = await fetch(`${apiUrl}/api/cli/authorizations/token`, {
|
|
624
655
|
method: "POST",
|
|
625
656
|
headers: { "content-type": "application/json" },
|
|
626
657
|
body: JSON.stringify({ deviceCode: authorization.deviceCode }),
|
|
@@ -630,18 +661,19 @@ async function login(args: string[]) {
|
|
|
630
661
|
if (!exchange.ok) fail(`login failed (${exchange.status}): ${result}`);
|
|
631
662
|
const token = parseToken(result);
|
|
632
663
|
if (!token) fail("login response did not include a CLI token");
|
|
633
|
-
await saveToken(apiUrl, token);
|
|
664
|
+
const { activeApiUrl: active } = await saveToken(apiUrl, token);
|
|
634
665
|
console.log(ok("login approved — credentials saved for this endpoint"));
|
|
666
|
+
reportActiveEndpoint(apiUrl, active);
|
|
635
667
|
return;
|
|
636
668
|
}
|
|
637
669
|
fail("login expired before approval");
|
|
638
670
|
}
|
|
639
671
|
|
|
640
672
|
async function apiCredentials() {
|
|
641
|
-
const apiUrl
|
|
673
|
+
const { apiUrl, source } = await resolveApiUrl();
|
|
642
674
|
const token = process.env.SPROUTBOAT_TOKEN || (await savedToken(apiUrl));
|
|
643
675
|
if (!token) fail("not logged in; run sproutboat login or set SPROUTBOAT_TOKEN for this command");
|
|
644
|
-
return { apiUrl, token };
|
|
676
|
+
return { apiUrl, token, source };
|
|
645
677
|
}
|
|
646
678
|
|
|
647
679
|
async function versions(args: string[]) {
|
|
@@ -1231,12 +1263,36 @@ async function toolchain(args: string[]): Promise<void> {
|
|
|
1231
1263
|
console.log(`macOS SDK: ${report.prerequisites.sdk ?? "missing: run xcode-select --install"}`);
|
|
1232
1264
|
}
|
|
1233
1265
|
|
|
1266
|
+
/** `sproutboat <command> --help`: that command's full usage line + summary, exit 0. */
|
|
1267
|
+
function commandHelp(name: string): never {
|
|
1268
|
+
const found = COMMANDS.find((c) => c.name === name);
|
|
1269
|
+
if (!found) usage();
|
|
1270
|
+
console.log(`${dim(`usage: ${CLI_NAME} ${name}${found.args ? ` ${found.args}` : ""}`)}\n\n${found.summary}`);
|
|
1271
|
+
process.exit(0);
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
const BASH_COMPLETION = (names: string) =>
|
|
1275
|
+
`_sproutboat_complete() {\n local cur=\${COMP_WORDS[COMP_CWORD]}\n if [ "$COMP_CWORD" -eq 1 ]; then\n COMPREPLY=($(compgen -W "${names}" -- "$cur"))\n fi\n}\ncomplete -F _sproutboat_complete ${CLI_NAME}\n`;
|
|
1276
|
+
const ZSH_COMPLETION = (names: string) => `#compdef ${CLI_NAME}\n_arguments '1: :(${names})'\n`;
|
|
1277
|
+
const FISH_COMPLETION = (names: string) => `complete -c ${CLI_NAME} -n "__fish_use_subcommand" -a "${names}"\n`;
|
|
1278
|
+
|
|
1279
|
+
/** `sproutboat complete [shell]`: a static completion script for the top-level commands. */
|
|
1280
|
+
function complete(shell?: string): void {
|
|
1281
|
+
const names = COMMANDS.map((c) => c.name).join(" ");
|
|
1282
|
+
const scripts = { bash: BASH_COMPLETION(names), zsh: ZSH_COMPLETION(names), fish: FISH_COMPLETION(names) };
|
|
1283
|
+
const key = shell ?? process.env.SHELL?.split("/").pop() ?? "bash";
|
|
1284
|
+
const script = key === "bash" || key === "zsh" || key === "fish" ? scripts[key] : undefined;
|
|
1285
|
+
if (!script) usageError(`complete: unsupported shell "${key}"`, "complete [bash | zsh | fish]");
|
|
1286
|
+
console.log(script);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1234
1289
|
const [command, ...args] = process.argv.slice(2);
|
|
1235
1290
|
if (command === undefined || command === "help" || command === "-h" || command === "--help") help();
|
|
1236
1291
|
if (command === "--version" || command === "-v") {
|
|
1237
1292
|
console.log(`sproutboat ${CLI_VERSION}`);
|
|
1238
1293
|
process.exit(0);
|
|
1239
1294
|
}
|
|
1295
|
+
if (args.includes("-h") || args.includes("--help")) commandHelp(command);
|
|
1240
1296
|
|
|
1241
1297
|
await notifyIfOutdated(CLI_VERSION);
|
|
1242
1298
|
|
|
@@ -1302,6 +1358,9 @@ switch (command) {
|
|
|
1302
1358
|
case "delete":
|
|
1303
1359
|
await deleteProject(args);
|
|
1304
1360
|
break;
|
|
1361
|
+
case "complete":
|
|
1362
|
+
complete(args[0]);
|
|
1363
|
+
break;
|
|
1305
1364
|
default:
|
|
1306
1365
|
usage();
|
|
1307
1366
|
}
|
package/src/surface.ts
CHANGED
|
@@ -195,6 +195,13 @@ export const COMMANDS: readonly Command[] = [
|
|
|
195
195
|
args: "",
|
|
196
196
|
summary: "Show the active endpoint and the account the stored token belongs to.",
|
|
197
197
|
},
|
|
198
|
+
{
|
|
199
|
+
name: "complete",
|
|
200
|
+
group: "Account",
|
|
201
|
+
emoji: "⌨",
|
|
202
|
+
args: "[bash | zsh | fish]",
|
|
203
|
+
summary: "Print a shell completion script for the top-level commands (defaults to $SHELL).",
|
|
204
|
+
},
|
|
198
205
|
];
|
|
199
206
|
|
|
200
207
|
export type EnvVar = { name: string; purpose: string };
|
|
@@ -264,6 +271,7 @@ export const ENV_VARS: readonly EnvVar[] = [
|
|
|
264
271
|
purpose: "When set, skips the once-a-day npm check for a newer `sproutboat` release (also skipped when CI is set).",
|
|
265
272
|
},
|
|
266
273
|
{ name: "XDG_CONFIG_HOME", purpose: "Base for the default credentials dir when SPROUTBOAT_CONFIG_DIR is unset." },
|
|
274
|
+
{ name: "SHELL", purpose: "Picks the default shell for `complete` with no argument (bash/zsh/fish)." },
|
|
267
275
|
{ name: "PORFFOR_VERSION", purpose: "Override the Porffor identity string recorded in the manifest." },
|
|
268
276
|
{
|
|
269
277
|
name: "SB_BROKER_PORT",
|
package/src/toolchain.ts
CHANGED
|
@@ -677,14 +677,9 @@ export function porfforVersion(): string {
|
|
|
677
677
|
}
|
|
678
678
|
|
|
679
679
|
export function esbuildVersion(): string {
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
const parsed = JSON.parse(readFileSync(pkg, "utf8")) as { version?: string };
|
|
684
|
-
return parsed.version || "unknown";
|
|
685
|
-
} catch {
|
|
686
|
-
return "unknown";
|
|
687
|
-
}
|
|
680
|
+
// Legacy artifact field, retained so older control planes can read manifests.
|
|
681
|
+
// Handler bundling uses Bun.build and Porffor alpha 9 needs no esbuild binary.
|
|
682
|
+
return "unused";
|
|
688
683
|
}
|
|
689
684
|
|
|
690
685
|
function uwsCommit(): string {
|
package/src/types-gen.ts
CHANGED
|
@@ -55,6 +55,9 @@ function entries(config: SproutboatConfig): Entry[] {
|
|
|
55
55
|
if (config.assets?.binding) {
|
|
56
56
|
out.push({ name: config.assets.binding, type: "Fetcher", note: `static assets: ${config.assets.directory}` });
|
|
57
57
|
}
|
|
58
|
+
if (config.version_metadata) {
|
|
59
|
+
out.push({ name: config.version_metadata, type: "VersionMetadata" });
|
|
60
|
+
}
|
|
58
61
|
return out;
|
|
59
62
|
}
|
|
60
63
|
|
package/types/sproutboat.d.ts
CHANGED
|
@@ -14,12 +14,19 @@
|
|
|
14
14
|
declare global {
|
|
15
15
|
// ---------------------------------------------------------------- KV
|
|
16
16
|
|
|
17
|
+
interface KVPutOptions {
|
|
18
|
+
/** Seconds from now until the key expires. Must be at least 60, matching Cloudflare. */
|
|
19
|
+
expirationTtl?: number;
|
|
20
|
+
/** Absolute expiration as unix seconds. */
|
|
21
|
+
expiration?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
17
24
|
interface KVNamespace {
|
|
18
|
-
/** The stored string, or `null`. Synchronous: no `await`. */
|
|
25
|
+
/** The stored string, or `null`. Synchronous: no `await`. An expired key reads as absent. */
|
|
19
26
|
get(key: string): string | null;
|
|
20
|
-
put(key: string, value: string): void;
|
|
27
|
+
put(key: string, value: string, options?: KVPutOptions): void;
|
|
21
28
|
delete(key: string): void;
|
|
22
|
-
/** Keys under `prefix` (all keys when omitted). */
|
|
29
|
+
/** Keys under `prefix` (all keys when omitted). Expired keys are omitted. */
|
|
23
30
|
list(prefix?: string): string[];
|
|
24
31
|
}
|
|
25
32
|
|
|
@@ -320,6 +327,20 @@ declare global {
|
|
|
320
327
|
/** Runs after `storage.setAlarm`, with no request in flight. */
|
|
321
328
|
alarm?(): void | Promise<void>;
|
|
322
329
|
}
|
|
330
|
+
|
|
331
|
+
// ------------------------------------------------------- Version metadata
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* #126 — named by `version_metadata` in `sproutboat.jsonc`. Baked into the
|
|
335
|
+
* binary at build time from the artifact's own digest: `id` matches the
|
|
336
|
+
* `artifact` field `sproutboat versions list` prints for this build, `tag`
|
|
337
|
+
* is the project name, `timestamp` is when it was built.
|
|
338
|
+
*/
|
|
339
|
+
interface VersionMetadata {
|
|
340
|
+
id: string;
|
|
341
|
+
tag: string;
|
|
342
|
+
timestamp: string;
|
|
343
|
+
}
|
|
323
344
|
}
|
|
324
345
|
|
|
325
346
|
export {};
|