sproutboat 0.4.6 → 0.4.7

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/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.4.6 · runs on Bun (use `bunx`, not `npx`)
6
+ **Package:** `sproutboat` 0.4.7 · runs on Bun (use `bunx`, not `npx`)
7
7
 
8
8
  ## Commands
9
9
 
@@ -12,7 +12,7 @@
12
12
  | `init` | `[name]` | Scaffold sproutboat.jsonc + src/index.js in ./<name>. |
13
13
  | `check` | `[project-dir]` | Validate the config and entry point without building. |
14
14
  | `build` | `[project-dir]` | Cross-compile the native-fetch sprout (Porffor + Zig). |
15
- | `deploy` | `[project-dir] [--dry-run] [--artifact <dir>] [--no-wait]` | Build (unless --artifact), print the report, upload, wait until the URL serves. --dry-run stops before upload; --no-wait skips the health check. |
15
+ | `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. --dry-run stops before upload; --no-wait skips the health check; --no-provision leaves id-less bindings as ephemeral deploy-scoped stores. |
16
16
  | `versions` | `list [project-dir]` | List the project's deployed versions. |
17
17
  | `rollback` | `<version-id> [project-dir]` | Re-activate a previous version. |
18
18
  | `tail` | `[project-dir] [--sprout]` | Print recent request logs; --sprout prints the running sprout + broker stdout/stderr instead. |
@@ -23,7 +23,7 @@
23
23
  | `login` | `[--api-url <url>] [--token <token>]` | Device-code browser flow, or store <token> for <url> directly. |
24
24
 
25
25
  ```
26
- usage: sproutboat <init [name] | check [project-dir] | build [project-dir] | deploy [project-dir] [--dry-run] [--artifact <dir>] [--no-wait] | versions list [project-dir] | rollback <version-id> [project-dir] | tail [project-dir] [--sprout] | domains [list | add <host> | verify <host> | rm <host>] [project-dir] | secrets [list | set <NAME> [value] | rm <NAME>] [project-dir] | resource [list [kind] | create <kind> <name> | rename <id> <name> | delete <id>] | delete [project-dir] [--name <project>] --yes | login [--api-url <url>] [--token <token>]>
26
+ usage: sproutboat <init [name] | check [project-dir] | build [project-dir] | deploy [project-dir] [--dry-run] [--artifact <dir>] [--no-wait] [--no-provision] | versions list [project-dir] | rollback <version-id> [project-dir] | tail [project-dir] [--sprout] | domains [list | add <host> | verify <host> | rm <host>] [project-dir] | secrets [list | set <NAME> [value] | rm <NAME>] [project-dir] | resource [list [kind] | create <kind> <name> | rename <id> <name> | delete <id>] | delete [project-dir] [--name <project>] --yes | login [--api-url <url>] [--token <token>]>
27
27
  ```
28
28
 
29
29
  ## Environment variables
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sproutboat",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
4
4
  "description": "Wrangler-shaped CLI for Sproutboat. Deploys workers to any control plane via --api-url / SPROUTBOAT_API_URL.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/config.ts CHANGED
@@ -14,6 +14,26 @@ export function resourceRefs(field: readonly ResourceRef[] | undefined): Array<{
14
14
  return (field ?? []).map((entry) => (isString(entry) ? { binding: entry } : { binding: entry.binding, id: entry.id }));
15
15
  }
16
16
 
17
+ /**
18
+ * Rewrite a bare `"BINDING"` token inside `<field>: [ … ]` to
19
+ * `{ "binding": "BINDING", "id": "<id>" }`, leaving the rest of the source
20
+ * (comments, spacing) untouched — used by `deploy`'s auto-provisioner to pin an
21
+ * id back into `sproutboat.jsonc`. Binding names are UPPER_SNAKE, so a plain
22
+ * `"BINDING"` match inside the array is unambiguous. No-op if not found.
23
+ */
24
+ export function pinBindingId(source: string, field: string, binding: string, id: string): string {
25
+ const array = new RegExp(`("${field}"\\s*:\\s*\\[)([\\s\\S]*?)(\\])`);
26
+ // the bare token must be a whole array element — at the start of the array or
27
+ // right after a comma — never `"binding": "NAME"` inside an already-pinned
28
+ // { … } object.
29
+ const element = new RegExp(`(^\\s*|,\\s*)"${binding}"(\\s*,|\\s*$)`);
30
+ return source.replace(array, (whole, open: string, inner: string, close: string) =>
31
+ element.test(inner)
32
+ ? open + inner.replace(element, `$1{ "binding": "${binding}", "id": "${id}" }$2`) + close
33
+ : whole,
34
+ );
35
+ }
36
+
17
37
  export type SproutboatConfig = {
18
38
  $schema?: string;
19
39
  name: string;
package/src/main.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
  import { mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import { basename, resolve } from "node:path";
4
- import { parseConfig, type SproutboatConfig } from "./config";
4
+ import { parseConfig, pinBindingId, resourceRefs, type SproutboatConfig } from "./config";
5
5
  import { validateHttpSyncSource } from "./source";
6
6
  import { buildArtifact } from "./build";
7
7
  import { validateManifest, type ArtifactManifest } from "./manifest";
@@ -177,6 +177,52 @@ async function build(directory?: string) {
177
177
  return { project, artifact };
178
178
  }
179
179
 
180
+ const PROVISION_FIELDS = [
181
+ ["kv_namespaces", "kv"],
182
+ ["d1_databases", "d1"],
183
+ ["r2_buckets", "r2"],
184
+ ["queues", "queue"],
185
+ ] as const;
186
+
187
+ /**
188
+ * #74 auto-provisioning, wrangler-style. A bare-string KV/D1/R2/queue binding
189
+ * with no id gets an account-level resource created (`<project>-<binding>`) on
190
+ * deploy, and the id is written back into `sproutboat.jsonc`. `--no-provision`
191
+ * skips this — those bindings then get an ephemeral deploy-scoped store.
192
+ */
193
+ async function provisionBindings(directory = process.cwd()): Promise<void> {
194
+ const configPath = resolve(directory, "sproutboat.jsonc");
195
+ let source = await readFile(configPath, "utf8");
196
+ const parsed = parseConfig(source);
197
+ if (!parsed.ok) return; // build() re-reads and reports the config error
198
+
199
+ const bare: Array<{ field: string; kind: string; binding: string }> = [];
200
+ for (const [field, kind] of PROVISION_FIELDS) {
201
+ for (const ref of resourceRefs(parsed.value[field])) {
202
+ if (!ref.id) bare.push({ field, kind, binding: ref.binding });
203
+ }
204
+ }
205
+ if (bare.length === 0) return;
206
+
207
+ const { apiUrl, token } = await apiCredentials();
208
+ for (const { field, kind, binding } of bare) {
209
+ const name = `${parsed.value.name}-${binding.toLowerCase().replace(/_/g, "-")}`;
210
+ const body = await responseText(
211
+ await fetch(`${apiUrl}/api/resources`, {
212
+ method: "POST",
213
+ headers: { "x-api-key": token, "content-type": "application/json" },
214
+ body: JSON.stringify({ kind, name, ifExists: "return" }),
215
+ }),
216
+ `could not provision a ${kind} resource for env.${binding}`,
217
+ );
218
+ const record = jsonObject(jsonObject(parseJsonValue(body))?.resource ?? null);
219
+ if (!record || !isString(record.id)) fail(`provision response for env.${binding} was not a resource`);
220
+ source = pinBindingId(source, field, binding, record.id);
221
+ console.log(ok(`provisioned ${kind} ${bold(name)} → ${record.id}`));
222
+ }
223
+ await writeFile(configPath, source);
224
+ }
225
+
180
226
  async function deploy(args: string[]) {
181
227
  const artifactIndex = args.indexOf("--artifact");
182
228
  const dryRun = args.includes("--dry-run");
@@ -190,6 +236,9 @@ async function deploy(args: string[]) {
190
236
  : usageError("deploy: --artifact needs a directory", "deploy [project-dir] [--dry-run] [--artifact <dir>] [--no-wait]");
191
237
  projectName = "";
192
238
  } else {
239
+ // wrangler-style: create resources for id-less bindings and pin the ids
240
+ // back into sproutboat.jsonc before the build bakes them into the artifact.
241
+ if (!args.includes("--no-provision") && !dryRun) await provisionBindings(directory);
193
242
  const built = await build(directory);
194
243
  projectName = built.project.config.name;
195
244
  artifactDir = built.artifact.artifactDir;
package/src/report.ts CHANGED
@@ -35,7 +35,7 @@ function bindingRows(config: SproutboatConfig): string[][] {
35
35
  };
36
36
  const resourceList = (refs: Parameters<typeof resourceRefs>[0], type: string) => {
37
37
  for (const ref of resourceRefs(refs)) {
38
- rows.push([`env.${ref.binding}`, type, ref.id ?? "no id local dev only, a deploy needs `sproutboat resource create`"]);
38
+ rows.push([`env.${ref.binding}`, type, ref.id ?? "deploy-scoped store (--no-provision)drop the flag to auto-provision a persistent resource"]);
39
39
  }
40
40
  };
41
41
  resourceList(config.kv_namespaces, "kv");
package/src/surface.ts CHANGED
@@ -31,8 +31,8 @@ export const COMMANDS: readonly Command[] = [
31
31
  summary: "Cross-compile the native-fetch sprout (Porffor + Zig)." },
32
32
 
33
33
  { name: "deploy", group: "Ship", emoji: "🚀",
34
- args: "[project-dir] [--dry-run] [--artifact <dir>] [--no-wait]", brief: "[project-dir] [--dry-run]",
35
- summary: "Build (unless --artifact), print the report, upload, wait until the URL serves. --dry-run stops before upload; --no-wait skips the health check." },
34
+ args: "[project-dir] [--dry-run] [--artifact <dir>] [--no-wait] [--no-provision]", brief: "[project-dir] [--dry-run]",
35
+ summary: "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. --dry-run stops before upload; --no-wait skips the health check; --no-provision leaves id-less bindings as ephemeral deploy-scoped stores." },
36
36
  { name: "versions", group: "Ship", emoji: "📜", args: "list [project-dir]",
37
37
  summary: "List the project's deployed versions." },
38
38
  { name: "rollback", group: "Ship", emoji: "⏮", args: "<version-id> [project-dir]", brief: "<version-id>",