sproutboat 0.53.0 → 0.53.1
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 +8 -0
- package/README.md +21 -9
- package/SURFACE.md +2 -2
- package/package.json +5 -5
- package/src/init-package.ts +99 -0
- package/src/main.ts +29 -0
- package/src/surface.ts +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,14 @@ maintained going forward by the `release` skill.
|
|
|
10
10
|
|
|
11
11
|
## Unreleased
|
|
12
12
|
|
|
13
|
+
## [0.53.1] - 2026-11-09
|
|
14
|
+
|
|
15
|
+
Built on `@sproutboat/runtime` 0.43.0, `@sproutboat/wire` 0.22.3 and `@sproutboat/toolchain` 0.24.2, unchanged from 0.53.0. No runtime or wire change, so there is no new compatibility date and no recompile.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **`init` adds `sproutboat` to the project's devDependencies and the `dev`, `deploy` and `check` scripts,** the way Cloudflare's templates do for Wrangler, so a project pins the CLI version that compiles it and `npm run deploy` works after `npm install`. The range is a caret on the running CLI's version. A missing `package.json` is created (`private`, version `0.0.0`); an existing one keeps its fields and indentation, a script with the same name is never overwritten (init prints a warning naming it), and a `sproutboat` already declared in any dependency section is left as is. An unreadable `package.json` stops init before any file is written. The next step it prints follows the project's package manager (a lockfile, then the launching tool, then npm), for example `cd hello && pnpm install && pnpm run dev` (`pnpm run`, because `pnpm deploy` is pnpm's own command).
|
|
20
|
+
|
|
13
21
|
## [0.53.0] - 2026-11-09
|
|
14
22
|
|
|
15
23
|
Built on `@sproutboat/runtime` 0.43.0, `@sproutboat/wire` 0.22.3 and `@sproutboat/toolchain` 0.24.2. The runtime change is the opt-in `SB_LOG=json` log below; without it nothing an app can observe changes, so there is no new compatibility date. Every app is recompiled on its next build, since the runtime prelude changed.
|
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ for agents: [sproutboat.com/llms.txt](https://sproutboat.com/llms.txt)).
|
|
|
25
25
|
## Using
|
|
26
26
|
|
|
27
27
|
The npm package installs a small Node launcher and the matching native
|
|
28
|
-
platform package, so `npm
|
|
28
|
+
platform package, so `npm i -D sproutboat`, `npm i -g sproutboat` and `npx sproutboat` work
|
|
29
29
|
without Bun on `darwin` and `linux` for `arm64` and `x64`. The self-hosted
|
|
30
30
|
runtime exports remain in the root package for Bun-based platform integrations.
|
|
31
31
|
Nothing is needed on the machine that *runs* a sprout: that gets a static
|
|
@@ -37,23 +37,35 @@ patches there, and publishes the cache entry atomically. Installation itself
|
|
|
37
37
|
does not clone Porffor or invoke Git, Make, or a compiler. Warm-cache builds can
|
|
38
38
|
run offline.
|
|
39
39
|
|
|
40
|
+
Install it in each project, as you would Wrangler. The CLI is the compiler, so
|
|
41
|
+
a project that pins its version builds the same way on every machine and in CI:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
npm i -D sproutboat # pnpm add -D sproutboat, bun add -d sproutboat, yarn add -D sproutboat
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`sproutboat init` does this for you: it writes a `package.json` with
|
|
48
|
+
`sproutboat` in `devDependencies` and `dev`, `deploy` and `check` scripts.
|
|
49
|
+
|
|
40
50
|
```sh
|
|
41
|
-
|
|
51
|
+
npx sproutboat init hello # or: pnpm dlx sproutboat init hello, bunx sproutboat init hello
|
|
42
52
|
cd hello
|
|
43
|
-
npm
|
|
53
|
+
npm install
|
|
54
|
+
npm run dev # runs it right here, no control plane needed
|
|
44
55
|
```
|
|
45
56
|
|
|
46
|
-
Happy with it?
|
|
57
|
+
Happy with it? Log in once, then ship it:
|
|
47
58
|
|
|
48
59
|
```sh
|
|
49
|
-
|
|
50
|
-
|
|
60
|
+
npx sproutboat login --api-url https://control.example.com # one browser approval
|
|
61
|
+
npm run deploy # or: npx sproutboat deploy
|
|
51
62
|
```
|
|
52
63
|
|
|
53
|
-
|
|
64
|
+
For quick commands outside a project, install it globally. A global install is
|
|
65
|
+
not pinned to anything, so keep it for `init`, `login` and `whoami`:
|
|
54
66
|
|
|
55
67
|
```sh
|
|
56
|
-
npm
|
|
68
|
+
npm i -g sproutboat # then: sproutboat init, sproutboat whoami, ...
|
|
57
69
|
```
|
|
58
70
|
|
|
59
71
|
`login` is one-time. It writes a long-lived token to
|
|
@@ -67,7 +79,7 @@ The everyday loop:
|
|
|
67
79
|
|
|
68
80
|
| Command | What it does |
|
|
69
81
|
| --- | --- |
|
|
70
|
-
| `init [name]` | Scaffold `sproutboat.jsonc
|
|
82
|
+
| `init [name]` | Scaffold `sproutboat.jsonc`, `src/index.js` and a `package.json` that pins `sproutboat` |
|
|
71
83
|
| `dev [--port <n>]` | Run it here against a real broker, rebuilding on save |
|
|
72
84
|
| `deploy` | Build, provision bindings, upload, wait until the URL serves |
|
|
73
85
|
| `tail [filters]` | Live request events, console output and exceptions, like `wrangler tail` |
|
package/SURFACE.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
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.53.
|
|
6
|
+
**Package:** `sproutboat` 0.53.1
|
|
7
7
|
|
|
8
8
|
## Commands
|
|
9
9
|
|
|
10
10
|
| Command | Arguments | Summary |
|
|
11
11
|
| --- | --- | --- |
|
|
12
|
-
| `init` | `[name]` | Scaffold sproutboat.jsonc
|
|
12
|
+
| `init` | `[name]` | Scaffold sproutboat.jsonc, src/index.js and package.json (sproutboat devDependency, dev and deploy scripts) in ./<name>. |
|
|
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] [--env <name>] [--env-file <file>]...` | Run the project on this machine against a real broker, rebuilding on save. Secrets come from .dev.vars or the .env files (never both), as in Wrangler; a declared secret with no value is a warning. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sproutboat",
|
|
3
|
-
"version": "0.53.
|
|
3
|
+
"version": "0.53.1",
|
|
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",
|
|
@@ -78,10 +78,10 @@
|
|
|
78
78
|
"typescript": "7.0.2"
|
|
79
79
|
},
|
|
80
80
|
"optionalDependencies": {
|
|
81
|
-
"@sproutboat/cli-darwin-arm64": "0.53.
|
|
82
|
-
"@sproutboat/cli-darwin-x64": "0.53.
|
|
83
|
-
"@sproutboat/cli-linux-arm64": "0.53.
|
|
84
|
-
"@sproutboat/cli-linux-x64": "0.53.
|
|
81
|
+
"@sproutboat/cli-darwin-arm64": "0.53.1",
|
|
82
|
+
"@sproutboat/cli-darwin-x64": "0.53.1",
|
|
83
|
+
"@sproutboat/cli-linux-arm64": "0.53.1",
|
|
84
|
+
"@sproutboat/cli-linux-x64": "0.53.1"
|
|
85
85
|
},
|
|
86
86
|
"engines": {
|
|
87
87
|
"node": ">=18"
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { jsonObject, parseJsonValue, type JsonObject } from "./json";
|
|
4
|
+
|
|
5
|
+
export type PackageManager = "npm" | "pnpm" | "yarn" | "bun";
|
|
6
|
+
|
|
7
|
+
// Same conventions as Wrangler's create-cloudflare templates: a private
|
|
8
|
+
// package at 0.0.0, `dev` and `deploy` scripts, and the CLI as a devDependency.
|
|
9
|
+
export const INIT_SCRIPTS = {
|
|
10
|
+
dev: "sproutboat dev",
|
|
11
|
+
deploy: "sproutboat deploy",
|
|
12
|
+
check: "sproutboat check",
|
|
13
|
+
} as const satisfies Record<string, string>;
|
|
14
|
+
|
|
15
|
+
const LOCKFILES: ReadonlyArray<readonly [string, PackageManager]> = [
|
|
16
|
+
["bun.lock", "bun"],
|
|
17
|
+
["bun.lockb", "bun"],
|
|
18
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
19
|
+
["yarn.lock", "yarn"],
|
|
20
|
+
["package-lock.json", "npm"],
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
/** A lockfile in the project wins; otherwise the tool that launched the CLI; otherwise npm. */
|
|
24
|
+
export function detectPackageManager(directory: string, userAgent = process.env.npm_config_user_agent): PackageManager {
|
|
25
|
+
for (const [file, manager] of LOCKFILES) if (existsSync(join(directory, file))) return manager;
|
|
26
|
+
const agent = /^(npm|pnpm|yarn|bun)\//.exec(userAgent ?? "")?.[1];
|
|
27
|
+
return agent === "pnpm" || agent === "yarn" || agent === "bun" ? agent : "npm";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function installCommand(manager: PackageManager): string {
|
|
31
|
+
return `${manager} install`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function runScriptCommand(manager: PackageManager, script: string): string {
|
|
35
|
+
// Only yarn can drop `run` for every script. `pnpm deploy` is pnpm's own
|
|
36
|
+
// workspace command, so the script has to be `pnpm run deploy`.
|
|
37
|
+
return manager === "yarn" ? `yarn ${script}` : `${manager} run ${script}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type PackagePlan = {
|
|
41
|
+
/** Serialized package.json to write. */
|
|
42
|
+
text: string;
|
|
43
|
+
created: boolean;
|
|
44
|
+
addedDependency: boolean;
|
|
45
|
+
addedScripts: string[];
|
|
46
|
+
/** Existing scripts left alone because the name was taken. */
|
|
47
|
+
skippedScripts: string[];
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** Throws on a package.json that is not a JSON object, before anything is written. */
|
|
51
|
+
export function planPackageJson(name: string, existing: string | undefined, cliVersion: string): PackagePlan {
|
|
52
|
+
const range = `^${cliVersion}`;
|
|
53
|
+
let pkg: JsonObject;
|
|
54
|
+
let indent: string | number = 2;
|
|
55
|
+
let created = false;
|
|
56
|
+
if (existing === undefined) {
|
|
57
|
+
created = true;
|
|
58
|
+
pkg = { name, version: "0.0.0", private: true };
|
|
59
|
+
} else {
|
|
60
|
+
let parsed: JsonObject | undefined;
|
|
61
|
+
try {
|
|
62
|
+
parsed = jsonObject(parseJsonValue(existing));
|
|
63
|
+
} catch {
|
|
64
|
+
parsed = undefined;
|
|
65
|
+
}
|
|
66
|
+
if (!parsed) throw new Error("package.json is not a valid JSON object, so init did not touch it");
|
|
67
|
+
pkg = parsed;
|
|
68
|
+
indent = /^[ \t]+(?=")/m.exec(existing)?.[0] ?? 2;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const scripts: JsonObject = jsonObject(pkg.scripts) ?? {};
|
|
72
|
+
const addedScripts: string[] = [];
|
|
73
|
+
const skippedScripts: string[] = [];
|
|
74
|
+
for (const [script, command] of Object.entries(INIT_SCRIPTS)) {
|
|
75
|
+
if (!(script in scripts)) {
|
|
76
|
+
scripts[script] = command;
|
|
77
|
+
addedScripts.push(script);
|
|
78
|
+
} else if (scripts[script] !== command) skippedScripts.push(script);
|
|
79
|
+
}
|
|
80
|
+
if (addedScripts.length > 0) pkg.scripts = scripts;
|
|
81
|
+
|
|
82
|
+
const devDependencies: JsonObject = jsonObject(pkg.devDependencies) ?? {};
|
|
83
|
+
// Already declared anywhere: the project chose its own range.
|
|
84
|
+
const declared = [pkg.dependencies, pkg.devDependencies, pkg.peerDependencies, pkg.optionalDependencies].some(
|
|
85
|
+
(section) => jsonObject(section)?.sproutboat !== undefined,
|
|
86
|
+
);
|
|
87
|
+
const addedDependency = !declared;
|
|
88
|
+
if (addedDependency) {
|
|
89
|
+
devDependencies.sproutboat = range;
|
|
90
|
+
pkg.devDependencies = Object.fromEntries(Object.entries(devDependencies).sort(([a], [b]) => a.localeCompare(b)));
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
text: `${JSON.stringify(pkg, null, indent)}\n`,
|
|
94
|
+
created,
|
|
95
|
+
addedDependency,
|
|
96
|
+
addedScripts,
|
|
97
|
+
skippedScripts,
|
|
98
|
+
};
|
|
99
|
+
}
|
package/src/main.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { CLI_NAME, COMMANDS, helpText, STORAGE_PRODUCTS, STORAGE_VERBS, type Sto
|
|
|
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";
|
|
21
|
+
import { detectPackageManager, installCommand, planPackageJson, runScriptCommand } from "./init-package";
|
|
21
22
|
import { pathToFileURL } from "node:url";
|
|
22
23
|
import { constants as osConstants } from "node:os";
|
|
23
24
|
import { inspectToolchain } from "./toolchain";
|
|
@@ -265,6 +266,21 @@ async function init(name = "hello") {
|
|
|
265
266
|
] as const) {
|
|
266
267
|
if (await Bun.file(path).exists()) fail(`${basename(directory)} already contains ${label}`);
|
|
267
268
|
}
|
|
269
|
+
// Plan package.json before writing anything, so an unreadable one stops init
|
|
270
|
+
// instead of leaving a half-scaffolded project.
|
|
271
|
+
const packagePath = resolve(directory, "package.json");
|
|
272
|
+
let existingPackage: string | undefined;
|
|
273
|
+
try {
|
|
274
|
+
existingPackage = await readFile(packagePath, "utf8");
|
|
275
|
+
} catch {
|
|
276
|
+
existingPackage = undefined;
|
|
277
|
+
}
|
|
278
|
+
let packagePlan: ReturnType<typeof planPackageJson>;
|
|
279
|
+
try {
|
|
280
|
+
packagePlan = planPackageJson(name, existingPackage, CLI_VERSION);
|
|
281
|
+
} catch (cause) {
|
|
282
|
+
fail(`${basename(directory)}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
283
|
+
}
|
|
268
284
|
await mkdir(resolve(directory, "src"), { recursive: true });
|
|
269
285
|
await writeFile(configPath, starterConfig(name), { flag: "wx" });
|
|
270
286
|
await writeFile(handlerPath, starterHandler, { flag: "wx" });
|
|
@@ -278,6 +294,19 @@ async function init(name = "hello") {
|
|
|
278
294
|
await writeFile(gitignorePath, starterGitignore, { flag: "wx" });
|
|
279
295
|
console.log(`Created ${basename(directory)}/.gitignore`);
|
|
280
296
|
}
|
|
297
|
+
if (packagePlan.created || packagePlan.addedDependency || packagePlan.addedScripts.length > 0) {
|
|
298
|
+
await writeFile(packagePath, packagePlan.text);
|
|
299
|
+
console.log(`${packagePlan.created ? "Created" : "Updated"} ${basename(directory)}/package.json`);
|
|
300
|
+
}
|
|
301
|
+
for (const script of packagePlan.skippedScripts)
|
|
302
|
+
console.log(
|
|
303
|
+
amber(
|
|
304
|
+
` ! package.json already has a "${script}" script; left it as is (sproutboat's would be "sproutboat ${script}")`,
|
|
305
|
+
),
|
|
306
|
+
);
|
|
307
|
+
const manager = detectPackageManager(directory);
|
|
308
|
+
console.log(`\nNext: cd ${basename(directory)} && ${installCommand(manager)} && ${runScriptCommand(manager, "dev")}`);
|
|
309
|
+
console.log(dim(`After "sproutboat login", ship it with ${runScriptCommand(manager, "deploy")}.`));
|
|
281
310
|
}
|
|
282
311
|
|
|
283
312
|
async function check(directory?: string) {
|
package/src/surface.ts
CHANGED
|
@@ -68,7 +68,8 @@ export const COMMANDS: readonly Command[] = [
|
|
|
68
68
|
group: "Develop",
|
|
69
69
|
emoji: "🌱",
|
|
70
70
|
args: "[name]",
|
|
71
|
-
summary:
|
|
71
|
+
summary:
|
|
72
|
+
"Scaffold sproutboat.jsonc, src/index.js and package.json (sproutboat devDependency, dev and deploy scripts) in ./<name>.",
|
|
72
73
|
},
|
|
73
74
|
{
|
|
74
75
|
name: "check",
|