sproutboat 0.11.10 → 0.11.11
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 +10 -1
- package/SURFACE.md +4 -2
- package/package.json +10 -10
- package/src/build.ts +9 -1
- package/src/compile.ts +3 -0
- package/src/credentials.ts +23 -9
- package/src/main.ts +82 -23
- package/src/surface.ts +8 -0
- package/src/types-gen.ts +3 -0
- package/types/sproutboat.d.ts +24 -3
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,14 @@ 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.11] - 2026-09-24
|
|
12
|
+
### Fixed
|
|
13
|
+
- Updated the native runtime to `0.12.0` so repeated HMAC signing uses the raw
|
|
14
|
+
digest bytes between rounds, matching WebCrypto and existing password hashes.
|
|
15
|
+
- Updated the toolchain to `0.4.11` so `Uint8Array.from({ length: 16 }, mapFn)`
|
|
16
|
+
produces the intended key bytes in standalone native builds.
|
|
17
|
+
- Added the two-round HMAC vector to broker and standalone conformance checks.
|
|
18
|
+
|
|
11
19
|
## [0.11.10] - 2026-09-18
|
|
12
20
|
### Changed
|
|
13
21
|
- Bumped `@sproutboat/toolchain` to `0.4.10`, pinning Porffor to the
|
|
@@ -588,7 +596,8 @@ its own package.
|
|
|
588
596
|
- Renamed the package to `sproutboat` (was `@sproutboat/cli`); dropped the
|
|
589
597
|
`sprout` bin alias in favour of a user-defined shell alias.
|
|
590
598
|
|
|
591
|
-
[Unreleased]: https://github.com/baronunread/sproutboat-cli/compare/v0.11.
|
|
599
|
+
[Unreleased]: https://github.com/baronunread/sproutboat-cli/compare/v0.11.11...HEAD
|
|
600
|
+
[0.11.11]: https://github.com/baronunread/sproutboat-cli/compare/v0.11.10...v0.11.11
|
|
592
601
|
[0.11.10]: https://github.com/baronunread/sproutboat-cli/compare/v0.11.9...v0.11.10
|
|
593
602
|
[0.11.9]: https://github.com/baronunread/sproutboat-cli/compare/v0.11.8...v0.11.9
|
|
594
603
|
[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.11 · 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). |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sproutboat",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.11",
|
|
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,11 +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.
|
|
65
|
+
"@sproutboat/assets": "^0.3.0",
|
|
66
|
+
"@sproutboat/config": "^0.4.0",
|
|
67
|
+
"@sproutboat/runtime": "^0.12.0",
|
|
68
|
+
"@sproutboat/toolchain": "^0.4.11",
|
|
69
|
+
"@sproutboat/wire": "^0.9.0",
|
|
70
70
|
"esbuild": "^0.28.2"
|
|
71
71
|
},
|
|
72
72
|
"devDependencies": {
|
|
@@ -78,10 +78,10 @@
|
|
|
78
78
|
"typescript": "7.0.2"
|
|
79
79
|
},
|
|
80
80
|
"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.
|
|
81
|
+
"@sproutboat/cli-darwin-arm64": "0.11.11",
|
|
82
|
+
"@sproutboat/cli-darwin-x64": "0.11.11",
|
|
83
|
+
"@sproutboat/cli-linux-arm64": "0.11.11",
|
|
84
|
+
"@sproutboat/cli-linux-x64": "0.11.11"
|
|
85
85
|
},
|
|
86
86
|
"engines": {
|
|
87
87
|
"node": ">=18"
|
package/src/build.ts
CHANGED
|
@@ -171,6 +171,13 @@ export async function buildArtifact(input: BuildInput): Promise<BuildOutput> {
|
|
|
171
171
|
bakedAssets = { manifest, files };
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
// Hoisted above the compile: #126 bakes it into the binary, and the manifest
|
|
175
|
+
// records the same instant rather than a slightly later one.
|
|
176
|
+
const builtAt = new Date().toISOString();
|
|
177
|
+
const versionMetadata = input.config.version_metadata
|
|
178
|
+
? { binding: input.config.version_metadata, id: artifactId, tag: input.config.name, timestamp: builtAt }
|
|
179
|
+
: undefined;
|
|
180
|
+
|
|
174
181
|
if (input.reuseSproutPath) await cp(input.reuseSproutPath, sproutPath);
|
|
175
182
|
else
|
|
176
183
|
await compileSprout({
|
|
@@ -188,6 +195,7 @@ export async function buildArtifact(input: BuildInput): Promise<BuildOutput> {
|
|
|
188
195
|
extraLink,
|
|
189
196
|
extraCflags,
|
|
190
197
|
optimize: input.optimize,
|
|
198
|
+
versionMetadata,
|
|
191
199
|
});
|
|
192
200
|
|
|
193
201
|
const sprout = await readFile(sproutPath);
|
|
@@ -204,7 +212,7 @@ export async function buildArtifact(input: BuildInput): Promise<BuildOutput> {
|
|
|
204
212
|
sourceHash,
|
|
205
213
|
binaryHash: digest(sprout),
|
|
206
214
|
binarySize: (await stat(sproutPath)).size,
|
|
207
|
-
builtAt
|
|
215
|
+
builtAt,
|
|
208
216
|
};
|
|
209
217
|
await writeFile(resolve(artifactDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
210
218
|
// Bindings live beside the manifest, not in it: the artifact manifest schema is
|
package/src/compile.ts
CHANGED
|
@@ -80,6 +80,8 @@ export type CompileInput = {
|
|
|
80
80
|
optimize?: "dev" | "release";
|
|
81
81
|
/** Cross-compiler for `linux-x86_64`. Not needed, and not used, for `host`. */
|
|
82
82
|
zigBin?: string;
|
|
83
|
+
/** #126 — `env.<binding> = { id, tag, timestamp }`, baked directly into the module. */
|
|
84
|
+
versionMetadata?: { binding: string; id: string; tag: string; timestamp: string };
|
|
83
85
|
/**
|
|
84
86
|
* `linux-x86_64` (default) cross-compiles the static musl binary every box
|
|
85
87
|
* runs. `host` compiles for the machine doing the build (#62) so `sproutboat
|
|
@@ -210,6 +212,7 @@ export async function compileSprout(input: CompileInput): Promise<void> {
|
|
|
210
212
|
input.appName,
|
|
211
213
|
input.assets,
|
|
212
214
|
input.transport,
|
|
215
|
+
input.versionMetadata,
|
|
213
216
|
),
|
|
214
217
|
);
|
|
215
218
|
|
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/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/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 {};
|