sproutboat 0.54.0 → 0.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/main.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env bun
2
- import { mkdir, open, readFile, rename, writeFile } from "node:fs/promises";
2
+ import { mkdir, open, readFile, rename, stat, writeFile } from "node:fs/promises";
3
3
  import { existsSync, rmSync } from "node:fs";
4
+ import { isIP } from "node:net";
4
5
  import { basename, dirname, relative, resolve } from "node:path";
5
6
  import { tmpdir } from "node:os";
6
7
  import { isBoolean, isSafeInteger, isString, jsonObject, parseJsonValue, type JsonObject } from "./json";
@@ -8,7 +9,8 @@ import type { AssetFiles } from "./assets";
8
9
  import { pinBindingId, resourceRefs, type SproutboatConfig } from "./config";
9
10
  import { validateHttpSyncSource } from "./source";
10
11
  import { buildArtifact, emitGenerated } from "./build";
11
- import { generateTypes, TYPES_FILE } from "./types-gen";
12
+ import { LEGACY_TYPES_FILE, TYPES_FILE } from "./types-gen";
13
+ import { checkTypes, parseTypesArgs, planTypes, refreshTypes, removeLegacyTypes, TYPES_USAGE } from "./types-cmd";
12
14
  import { bundleHandler, BundleError, type BundleResult } from "./bundle";
13
15
  import { runDev } from "./dev";
14
16
  import { buildStandalone, emitStandalone, isReleaseBuild } from "./standalone-build";
@@ -29,7 +31,13 @@ import {
29
31
  import { extractGlobalFlags, globalFlags, locateProject, setGlobalFlags } from "./global-flags";
30
32
  import { checkOptions, ENDPOINT_COMMANDS } from "./options";
31
33
  import { chooseTarget, TargetError, type Target } from "./target";
32
- import { configAccount, configMigrationsDir, parseProjectConfig } from "./account-field";
34
+ import {
35
+ ASSETS_ONLY_MAIN,
36
+ assetsOnlySource,
37
+ configAccount,
38
+ configMigrationsDir,
39
+ parseProjectConfig,
40
+ } from "./account-field";
33
41
  import { DATA_ERROR_STATUSES } from "./data-errors";
34
42
  import {
35
43
  cloudLogout,
@@ -58,6 +66,12 @@ import {
58
66
  tailBuild,
59
67
  waitForApply,
60
68
  } from "./cloud-deploy";
69
+ import {
70
+ checkDurableObjectExports,
71
+ describeCloudBindings,
72
+ mapCloudBindings,
73
+ type CloudBindings,
74
+ } from "./cloud-bindings";
61
75
  import {
62
76
  APPLY_USAGE,
63
77
  applyMigrations,
@@ -84,6 +98,7 @@ import {
84
98
  putCloudSecret,
85
99
  readSecretsFile,
86
100
  } from "./cloud-secrets";
101
+ import { cloudDomainsUnsupported, describeCloudRoutes, listCloudRoutes } from "./cloud-domains";
87
102
  import { parseBulkSecrets, type BulkPlan } from "./secrets-bulk";
88
103
  import {
89
104
  cloudTarget,
@@ -129,7 +144,7 @@ import {
129
144
  import { notifyIfOutdated } from "./update-check";
130
145
  import { controlVersionWarning } from "./api-version";
131
146
  import { amber, bold, dim, leaf, ok, rose } from "./style";
132
- import { detectPackageManager, installCommand, planPackageJson, runScriptCommand } from "./init-package";
147
+ import { InitError, parseInitArgs, runInit, terminalInitIo } from "./init";
133
148
  import { pathToFileURL } from "node:url";
134
149
  import { constants as osConstants } from "node:os";
135
150
  import { inspectToolchain } from "./toolchain";
@@ -310,28 +325,6 @@ function parseToken(source: string): string | undefined {
310
325
  return record && isString(record.token) ? record.token : undefined;
311
326
  }
312
327
 
313
- const starterConfig = (name: string) => `{
314
- "$schema": "https://sproutboat.com/schema.json",
315
- "name": "${name}",
316
- "main": "src/index.js",
317
- "compatibility_date": "2026-09-28"
318
- }
319
- `;
320
- const starterHandler = `export default {
321
- fetch(request, env, ctx) {
322
- return new Response("hello from Sproutboat");
323
- }
324
- };
325
- `;
326
- // .sproutboat/ holds build output (dist/) and `dev`'s local broker state
327
- // (dev/, including its SQLite files) — neither belongs in version control.
328
- // .dev.vars carries secret values for `sproutboat dev`, same convention as
329
- // Wrangler's file of the same name.
330
- const starterGitignore = `.sproutboat/
331
- .dev.vars
332
- node_modules/
333
- `;
334
-
335
328
  /** An operational failure — the command was invoked correctly but could not complete. Exit 1. */
336
329
  function fail(message: string): never {
337
330
  console.error(`${rose("✗")} ${message}`);
@@ -383,10 +376,20 @@ async function readProjectConfig(directory?: string) {
383
376
  };
384
377
  }
385
378
 
379
+ /** What to call the entry in messages: its path, or "assets only" when the project has no `main`. */
380
+ const entryLabel = (config: { main: string }) => (config.main === ASSETS_ONLY_MAIN ? "assets only" : config.main);
381
+
386
382
  /** Full build input: configuration plus entry point, resolved imports and checks. */
387
383
  async function readProject(directory?: string) {
388
384
  const project = await readProjectConfig(directory);
389
- const sourcePath = resolve(project.directory, project.config.main);
385
+ let sourcePath = resolve(project.directory, project.config.main);
386
+ if (project.config.main === ASSETS_ONLY_MAIN) {
387
+ // A Worker with only static assets: there is no entry file, so the small one that hands every miss to the assets is written
388
+ // into .sproutboat/ (the local state folder) and bundled like any other.
389
+ sourcePath = resolve(project.directory, ".sproutboat", "assets-only.js");
390
+ await mkdir(dirname(sourcePath), { recursive: true });
391
+ await writeFile(sourcePath, assetsOnlySource(project.config.assets?.binding ?? "ASSETS"));
392
+ }
390
393
  let source: string;
391
394
  try {
392
395
  source = await readFile(sourcePath, "utf8");
@@ -409,97 +412,57 @@ async function readProject(directory?: string) {
409
412
  return { ...project, sourcePath, source, bundle };
410
413
  }
411
414
 
412
- async function init(name = "hello") {
413
- if (!/^[a-z0-9](?:[a-z0-9-]{1,30}[a-z0-9])?$/.test(name))
414
- fail("project name must be a 3–32 character lowercase slug");
415
- const directory = resolve(process.cwd(), name);
416
- const configPath = resolve(directory, "sproutboat.jsonc");
417
- const handlerPath = resolve(directory, "src/index.js");
418
- // Check both targets before writing either — `name` can collide with an
419
- // unrelated existing directory, and a second `wx` write failing partway
420
- // through used to crash with a raw EEXIST stack trace after already having
421
- // created sproutboat.jsonc, leaving a half-scaffolded project behind.
422
- for (const [path, label] of [
423
- [configPath, "sproutboat.jsonc"],
424
- [handlerPath, "src/index.js"],
425
- ] as const) {
426
- if (await Bun.file(path).exists()) fail(`${basename(directory)} already contains ${label}`);
427
- }
428
- // Plan package.json before writing anything, so an unreadable one stops init
429
- // instead of leaving a half-scaffolded project.
430
- const packagePath = resolve(directory, "package.json");
431
- let existingPackage: string | undefined;
432
- try {
433
- existingPackage = await readFile(packagePath, "utf8");
434
- } catch {
435
- existingPackage = undefined;
436
- }
437
- let packagePlan: ReturnType<typeof planPackageJson>;
415
+ async function init(args: string[]) {
416
+ const parsed = parseInitArgs(args);
417
+ if ("error" in parsed) usageError(parsed.error, usageOf("init"));
438
418
  try {
439
- packagePlan = planPackageJson(name, existingPackage, CLI_VERSION);
419
+ await runInit(parsed, terminalInitIo(CLI_VERSION));
440
420
  } catch (cause) {
441
- fail(`${basename(directory)}: ${cause instanceof Error ? cause.message : String(cause)}`);
442
- }
443
- await mkdir(resolve(directory, "src"), { recursive: true });
444
- await writeFile(configPath, starterConfig(name), { flag: "wx" });
445
- await writeFile(handlerPath, starterHandler, { flag: "wx" });
446
- console.log(`Created ${basename(directory)}/sproutboat.jsonc`);
447
- console.log(`Created ${basename(directory)}/src/index.js`);
448
- // Unlike the two files above, an existing .gitignore here is not a sign this
449
- // isn't a fresh project (`name` can collide with an unrelated directory) —
450
- // leave it alone rather than failing init or clobbering it.
451
- const gitignorePath = resolve(directory, ".gitignore");
452
- if (!(await Bun.file(gitignorePath).exists())) {
453
- await writeFile(gitignorePath, starterGitignore, { flag: "wx" });
454
- console.log(`Created ${basename(directory)}/.gitignore`);
455
- }
456
- if (packagePlan.created || packagePlan.addedDependency || packagePlan.addedScripts.length > 0) {
457
- await writeFile(packagePath, packagePlan.text);
458
- console.log(`${packagePlan.created ? "Created" : "Updated"} ${basename(directory)}/package.json`);
459
- }
460
- for (const script of packagePlan.skippedScripts)
461
- console.log(
462
- amber(
463
- ` ! package.json already has a "${script}" script; left it as is (sproutboat's would be "sproutboat ${script}")`,
464
- ),
465
- );
466
- const manager = detectPackageManager(directory);
467
- console.log(`\nNext: cd ${basename(directory)} && ${installCommand(manager)} && ${runScriptCommand(manager, "dev")}`);
468
- console.log(dim(`After "sproutboat login", ship it with ${runScriptCommand(manager, "deploy")}.`));
421
+ if (cause instanceof InitError) fail(cause.message);
422
+ throw cause;
423
+ }
469
424
  }
470
425
 
471
426
  async function check(directory?: string) {
472
427
  const project = await readProject(directory);
473
- console.log(ok(`check passed — ${project.config.name} (${project.config.main}, native-fetch)`));
428
+ console.log(ok(`check passed — ${project.config.name} (${entryLabel(project.config)}, native-fetch)`));
474
429
  }
475
430
 
476
- async function types(directory?: string) {
477
- const project = await readProject(directory);
478
- const path = resolve(project.directory, TYPES_FILE);
479
- await writeFile(path, generateTypes(project.config));
480
- console.log(ok(`wrote ${relative(process.cwd(), path) || TYPES_FILE}`));
481
- console.log(dim(" Commit it. Add it to tsconfig `include` if your project has one."));
431
+ async function types(args: string[]) {
432
+ const parsed = parseTypesArgs(args);
433
+ if ("error" in parsed) usageError(parsed.error, TYPES_USAGE);
434
+ // A positional that is a directory is the project directory, as it was before the positional became the output file.
435
+ const project = await readProjectConfig(
436
+ parsed.positional !== undefined && existsSync(parsed.positional) && (await stat(parsed.positional)).isDirectory()
437
+ ? parsed.positional
438
+ : undefined,
439
+ );
440
+ const plan = await planTypes(parsed, project.config, project.directory);
441
+ const shown = relative(process.cwd(), plan.path) || TYPES_FILE;
442
+ if (parsed.check) {
443
+ const state = await checkTypes(plan);
444
+ if (state === "current") {
445
+ console.log(ok(`types at ${shown} are up to date`));
446
+ return;
447
+ }
448
+ console.error(
449
+ `${rose("✗")} ${state === "missing" ? `${shown} does not exist` : `${shown} is out of date`}. Run \`sproutboat types\` to write it.`,
450
+ );
451
+ process.exit(1);
452
+ }
453
+ await mkdir(dirname(plan.path), { recursive: true });
454
+ await writeFile(plan.path, plan.text);
455
+ console.log(ok(`wrote ${shown}`));
456
+ if (await removeLegacyTypes(project.directory))
457
+ console.log(dim(` removed ${LEGACY_TYPES_FILE}, which this file replaces`));
458
+ console.log(dim(' Commit it, and add it to the "include" of your tsconfig. Rerun after changing sproutboat.jsonc.'));
482
459
  }
483
460
 
484
- /**
485
- * Rewrite the generated types when the config has moved on, and only then.
486
- *
487
- * Called from `dev` and `deploy` so a binding added to sproutboat.jsonc cannot
488
- * sit unreflected in the editor. It writes only when the content actually
489
- * differs, which keeps it out of the way of git and of file watchers.
490
- */
491
- async function refreshTypes(projectDir: string, config: SproutboatConfig): Promise<void> {
492
- const path = resolve(projectDir, TYPES_FILE);
493
- const wanted = generateTypes(config);
494
- try {
495
- if ((await readFile(path, "utf8")) === wanted) return;
496
- } catch {
497
- // No file yet: only write one if the project already opted in by having a
498
- // tsconfig. Generating into a plain JavaScript project is noise.
499
- if (!(await Bun.file(resolve(projectDir, "tsconfig.json")).exists())) return;
500
- }
501
- await writeFile(path, wanted);
502
- console.log(dim(` ${TYPES_FILE} refreshed from sproutboat.jsonc`));
461
+ /** `dev` and `deploy`: keep the default generated types current (types-cmd.ts says when it does and does not). */
462
+ async function refreshProjectTypes(projectDir: string, config: SproutboatConfig): Promise<void> {
463
+ const { wrote, removedLegacy } = await refreshTypes(projectDir, config);
464
+ if (wrote) console.log(dim(` ${TYPES_FILE} refreshed from sproutboat.jsonc`));
465
+ if (removedLegacy) console.log(dim(` removed ${LEGACY_TYPES_FILE}, which it replaces`));
503
466
  }
504
467
 
505
468
  async function build(directory?: string, target: BuildTarget = "linux-x86_64") {
@@ -616,11 +579,13 @@ async function emitStandaloneUnit(directory: string | undefined): Promise<string
616
579
  }
617
580
 
618
581
  /** #62 — build for this machine, run it against a real broker, rebuild on save. */
619
- const DEV_USAGE = "dev [project-dir] [--port <n>] [--no-watch] [-e, --env <name>] [--env-file <file>]...";
582
+ const DEV_USAGE =
583
+ "dev [project-dir] [--port <n>] [--ip <address>] [--no-watch] [-e, --env <name>] [--env-file <file>]...";
620
584
 
621
585
  async function dev(args: string[]) {
622
586
  let directory: string | undefined;
623
587
  let portArg: string | undefined;
588
+ let host: string | undefined;
624
589
  // --env and --env-file are global flags, already taken out of `args` (global-flags.ts).
625
590
  const envName = globalFlags().env;
626
591
  const envFiles = globalFlags().envFiles.map((file) => resolve(file));
@@ -632,15 +597,22 @@ async function dev(args: string[]) {
632
597
  if (value === undefined || value.startsWith("--")) usageError(`${arg} needs a value`, DEV_USAGE);
633
598
  at += 1;
634
599
  portArg = value;
600
+ } else if (arg === "--ip") {
601
+ const value = args[at + 1];
602
+ if (value === undefined || value.startsWith("--")) usageError(`${arg} needs a value`, DEV_USAGE);
603
+ at += 1;
604
+ host = value;
635
605
  } else if (!arg.startsWith("--") && directory === undefined) directory = arg;
636
606
  }
637
607
  const port = Number(portArg ?? 8787);
638
608
  if (!Number.isSafeInteger(port) || port < 1 || port > 65_535) usageError(`invalid --port: ${portArg}`, DEV_USAGE);
609
+ if (host !== undefined && !isIP(host) && host !== "localhost")
610
+ usageError(`invalid --ip: ${host} (an IPv4 or IPv6 address, or localhost)`, DEV_USAGE);
639
611
  if (envName !== undefined && !/^[A-Za-z0-9_-]+$/.test(envName))
640
612
  usageError("--env takes a name (letters, digits, - and _)", DEV_USAGE);
641
613
  for (const file of envFiles) if (!existsSync(file)) fail(`env file not found: ${file}`);
642
614
  const project = await readProject(directory);
643
- await refreshTypes(project.directory, project.config);
615
+ await refreshProjectTypes(project.directory, project.config);
644
616
  console.log(dim(`Building ${project.config.name} for this machine (${hostTarget()})…`));
645
617
  await runDev({
646
618
  projectDir: project.directory,
@@ -648,6 +620,7 @@ async function dev(args: string[]) {
648
620
  sourcePath: project.sourcePath,
649
621
  source: project.bundle.code,
650
622
  port,
623
+ host,
651
624
  envFiles,
652
625
  envName,
653
626
  watch: !args.includes("--no-watch"),
@@ -655,7 +628,7 @@ async function dev(args: string[]) {
655
628
  // files changed, so the bundle captured at startup is stale by definition.
656
629
  rebuild: async () => {
657
630
  const next = await readProject(directory);
658
- await refreshTypes(next.directory, next.config);
631
+ await refreshProjectTypes(next.directory, next.config);
659
632
  return { config: next.config, sourcePath: next.sourcePath, source: next.bundle.code };
660
633
  },
661
634
  });
@@ -890,7 +863,7 @@ async function deploy(args: string[]) {
890
863
  artifactDir = built.artifact.artifactDir;
891
864
  config = built.project.config;
892
865
  // After provisioning: ids may have been pinned into the config just now.
893
- await refreshTypes(built.project.directory, built.project.config);
866
+ await refreshProjectTypes(built.project.directory, built.project.config);
894
867
  }
895
868
  const manifest = Bun.file(resolve(artifactDir, "manifest.json"));
896
869
  const sprout = Bun.file(resolve(artifactDir, "sprout"));
@@ -1247,25 +1220,14 @@ async function deployCloud(args: string[], apiUrl: string, flags: ReturnType<typ
1247
1220
  const hosts = [hostFor(name, team)];
1248
1221
 
1249
1222
  const cfg = project.config;
1250
- const unsent = Object.entries({
1251
- kv_namespaces: cfg.kv_namespaces?.length,
1252
- d1_databases: cfg.d1_databases?.length,
1253
- r2_buckets: cfg.r2_buckets?.length,
1254
- queues: cfg.queues?.length,
1255
- analytics_engine_datasets: cfg.analytics_engine_datasets?.length,
1256
- durable_objects: Object.keys(cfg.durable_objects ?? {}).length,
1257
- ratelimiters: cfg.ratelimiters?.length,
1258
- "triggers.crons": cfg.triggers?.crons?.length,
1259
- })
1260
- .filter(([, count]) => (count ?? 0) > 0)
1261
- .map(([key]) => key);
1262
- if (unsent.length > 0)
1263
- io.warn(
1264
- `! ${unsent.join(", ")} in sproutboat.jsonc are not sent to the Cloud by the CLI yet; the app runs without them`,
1265
- );
1266
-
1267
1223
  const services = cfg.services ?? [];
1268
1224
  guardSync(() => checkServiceBindings(name, services, cfg.compatibility_date));
1225
+ let cloudBindings!: CloudBindings;
1226
+ guardSync(() => {
1227
+ cloudBindings = mapCloudBindings(cfg, services.length);
1228
+ checkDurableObjectExports(cfg, project.directory);
1229
+ });
1230
+ for (const warning of cloudBindings.warnings) io.warn(`! ${warning}`);
1269
1231
  let secretValues: Record<string, string> | undefined;
1270
1232
  const secretsFile = flags["--secrets-file"];
1271
1233
  if (secretsFile !== undefined) {
@@ -1299,6 +1261,7 @@ async function deployCloud(args: string[], apiUrl: string, flags: ReturnType<typ
1299
1261
  console.log(`endpoint ${apiUrl}`);
1300
1262
  console.log(`route https://${hosts[0]}`);
1301
1263
  if (assets) console.log(`assets ${assets.files} files`);
1264
+ for (const line of describeCloudBindings(cloudBindings)) console.log(`binding ${line}`);
1302
1265
  if (services.length > 0)
1303
1266
  console.log(`services ${services.map((entry) => `${entry.binding} -> ${entry.service}`).join(", ")}`);
1304
1267
  if (secretValues) console.log(`secrets ${Object.keys(secretValues).length} from ${secretsFile} (set, others kept)`);
@@ -1336,7 +1299,7 @@ async function deployCloud(args: string[], apiUrl: string, flags: ReturnType<typ
1336
1299
  };
1337
1300
  if (fromSource) {
1338
1301
  const sourcePath = await emitStandaloneUnit(directory);
1339
- console.log(` Bundling ${cfg.main} (${formatKiB(Bun.file(sourcePath).size)})`);
1302
+ console.log(` Bundling ${entryLabel(cfg)} (${formatKiB(Bun.file(sourcePath).size)})`);
1340
1303
  const controller = new AbortController();
1341
1304
  const interrupt = () => controller.abort();
1342
1305
  process.once("SIGINT", interrupt);
@@ -1348,6 +1311,7 @@ async function deployCloud(args: string[], apiUrl: string, flags: ReturnType<typ
1348
1311
  tokenFromEnv,
1349
1312
  project: name,
1350
1313
  services,
1314
+ cloudBindings,
1351
1315
  compatibilityDate: cfg.compatibility_date,
1352
1316
  secrets: secretValues,
1353
1317
  hosts,
@@ -1383,6 +1347,7 @@ async function deployCloud(args: string[], apiUrl: string, flags: ReturnType<typ
1383
1347
  assets,
1384
1348
  noWait,
1385
1349
  services,
1350
+ cloudBindings,
1386
1351
  compatibilityDate: cfg.compatibility_date,
1387
1352
  secrets: secretValues,
1388
1353
  },
@@ -1663,9 +1628,24 @@ async function domains(args: string[]) {
1663
1628
  args[0] && !args[0].startsWith("-") && ["list", "add", "verify", "delete"].includes(args[0])
1664
1629
  ? args.shift()!
1665
1630
  : "list";
1631
+ const flags = takeFlags(args, ["--account"] as const);
1666
1632
  const host = sub === "list" ? undefined : args.shift();
1667
1633
  if (sub !== "list" && !host)
1668
1634
  usageError(`domains ${sub}: missing <hostname>`, `domains ${sub} <hostname> [project-dir]`);
1635
+ const { apiUrl: targetUrl } = await resolveApiUrl();
1636
+ if ((await deployKind(targetUrl, false)) === "cloud") {
1637
+ await guard(async () => {
1638
+ if (host !== undefined) throw new CloudError(cloudDomainsUnsupported(sub, host));
1639
+ const project = await readProjectConfig(args[0]);
1640
+ const credentials = await cloudCredentials(targetUrl, flags["--account"] ?? project.account);
1641
+ for (const line of describeCloudRoutes(
1642
+ project.config.name,
1643
+ await listCloudRoutes(credentials, project.config.name),
1644
+ ))
1645
+ console.log(line);
1646
+ });
1647
+ return;
1648
+ }
1669
1649
  const [project, { apiUrl, token }] = await Promise.all([readProjectConfig(args[0]), apiCredentials()]);
1670
1650
  const base = `${apiUrl}/api/projects/${project.config.name}/domains`;
1671
1651
  const auth = { "x-api-key": token };
@@ -2527,13 +2507,13 @@ if (!(command === "tail" && args.some((arg) => arg === "--file" || arg.startsWit
2527
2507
 
2528
2508
  switch (resolveCommand(command)) {
2529
2509
  case "init":
2530
- await init(args[0]);
2510
+ await init(args);
2531
2511
  break;
2532
2512
  case "check":
2533
2513
  await check(args[0]);
2534
2514
  break;
2535
2515
  case "types":
2536
- await types(args[0]);
2516
+ await types(args);
2537
2517
  break;
2538
2518
  case "dev":
2539
2519
  await dev(args);
package/src/options.ts CHANGED
@@ -28,10 +28,19 @@ export type Options = {
28
28
  };
29
29
 
30
30
  export const COMMAND_OPTIONS = {
31
- init: {},
31
+ // As in create-cloudflare: --template picks a starter, --git and --install (and their --no-) decide the two steps
32
+ // after scaffolding, -y takes every default. --list-templates prints the starters and exits.
33
+ init: {
34
+ value: ["--template"],
35
+ flag: ["--list-templates", "--git", "--no-git", "--install", "--no-install", "--yes", "-y", "--accept-defaults"],
36
+ },
32
37
  check: {},
33
- types: {},
34
- dev: { value: ["--port"], flag: ["--no-watch"] },
38
+ // The booleans take `--strict-vars=false` as in Wrangler (types-cmd.ts reads the pair); `--no-...` is the short way.
39
+ types: {
40
+ value: ["--env-interface", "--strict-vars", "--include-runtime", "--include-env"],
41
+ flag: ["--check", "--no-strict-vars", "--no-include-runtime", "--no-include-env"],
42
+ },
43
+ dev: { value: ["--port", "--ip"], flag: ["--no-watch"] },
35
44
  run: { value: ["--binary", "--port", "--data-dir", "--require"], flag: ["--check"] },
36
45
  build: {
37
46
  value: ["--target"],
@@ -56,7 +65,7 @@ export const COMMAND_OPTIONS = {
56
65
  flag: ["--recent", "--sprout", "--no-follow"],
57
66
  },
58
67
  delete: { value: ["--name"], flag: ["--yes", "-y"] },
59
- domains: {},
68
+ domains: { value: ["--account"] },
60
69
  // `secret` is the same command (surface.ts); `list | put | delete | bulk` share these, `--value` is put's.
61
70
  secrets: { value: ["--value", "--account"], flag: ["--no-deploy"] },
62
71
  kv: {
package/src/surface.ts CHANGED
@@ -92,10 +92,10 @@ export const COMMANDS: readonly Command[] = [
92
92
  group: "Develop",
93
93
  emoji: "🌱",
94
94
  short: "Scaffold a new project",
95
- args: "[name]",
95
+ args: "[name] [--template <name>] [--list-templates] [--git | --no-git] [--install | --no-install] [-y, --yes]",
96
96
  brief: "[name]",
97
97
  summary:
98
- "Scaffold sproutboat.jsonc, src/index.js and package.json (sproutboat devDependency, dev, deploy and check scripts) in ./<name>.",
98
+ "Scaffold a project in ./<name> (the last path segment is the app's name), with the shape of `npm create cloudflare`. Without --template it writes sproutboat.jsonc, src/index.js and package.json (sproutboat devDependency, dev, deploy and check scripts). `--template <name>` writes a starter instead, standard Workers TypeScript with tests, a README and a current worker-configuration.d.ts: realtime-room (a chat room on Durable Objects and hibernating WebSockets), api-d1 (a JSON API on D1 with migrations and validation), site-api (a static site and its API in one Worker) or jobs (a cron trigger, a queue and KV); `--list-templates` prints them with their descriptions. In a terminal it asks for the template, the directory, whether to install dependencies and whether to use git (both default to yes); `-y` takes every default without asking, and anywhere else nothing runs that a flag did not ask for. `--install` runs the project's package manager (npm, pnpm, yarn or bun, by lockfile, else the tool that launched the CLI); `--git` runs `git init` and makes a first commit, and does nothing inside an existing repository. It never overwrites a file.",
99
99
  },
100
100
  {
101
101
  name: "check",
@@ -110,21 +110,21 @@ export const COMMANDS: readonly Command[] = [
110
110
  name: "types",
111
111
  group: "Develop",
112
112
  emoji: "🧾",
113
- short: "Write sproutboat-env.d.ts for your editor",
114
- args: "[project-dir]",
115
- brief: "[project-dir]",
113
+ short: "Write worker-configuration.d.ts for your editor",
114
+ args: "[path] [--env-interface <name>] [--strict-vars[=false]] [--include-runtime[=false]] [--include-env[=false]] [--check]",
115
+ brief: "[path]",
116
116
  summary:
117
- "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.",
117
+ 'Generate the project\'s `Env` as TypeScript from sproutboat.jsonc, like `wrangler types`: the file is worker-configuration.d.ts unless <path> names another; it declares `interface Env` (and `Cloudflare.Env`, which `import { env } from "cloudflare:workers"` reads) with a type per binding, literal types for vars (--strict-vars=false makes them `string`), and a Durable Object binding typed with its class when the entry is TypeScript. It references the runtime types shipped with the CLI (--include-runtime=false leaves that out, for a project that brings its own) instead of copying them in, and there is no --include-main-module. Type a handler as `export default { ... } satisfies ExportedHandler<Env>`. --check exits 1 when the file is missing or out of date, by the hash in its first lines. KV, D1, R2, Durable Object and Fetcher types follow the compatibility date, as the runtime does. `dev` and `deploy` refresh the default file when it was written by a plain `sproutboat types` and the config has moved on; they leave one made with other flags alone. A positional that is an existing directory is still read as the project directory. The file earlier releases wrote, sproutboat-env.d.ts, is removed when it is still as generated.',
118
118
  },
119
119
  {
120
120
  name: "dev",
121
121
  group: "Develop",
122
122
  emoji: "⚡",
123
123
  short: "Run the app locally, rebuilding on save",
124
- args: "[project-dir] [--port <n>] [--no-watch] [-e, --env <name>] [--env-file <file>]...",
124
+ args: "[project-dir] [--port <n>] [--ip <address>] [--no-watch] [-e, --env <name>] [--env-file <file>]...",
125
125
  brief: "[project-dir]",
126
126
  summary:
127
- "Run the project on this machine with local storage, 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.",
127
+ "Run the project on this machine with local storage, rebuilding on save. It listens on 127.0.0.1 unless `--ip` names another address (`--ip 0.0.0.0` is reachable from other machines on the network, as in Wrangler). Secrets come from .dev.vars or the .env files (never both), as in Wrangler; a declared secret with no value is a warning.",
128
128
  },
129
129
  {
130
130
  name: "run",
@@ -223,9 +223,10 @@ export const COMMANDS: readonly Command[] = [
223
223
  group: "Configure",
224
224
  emoji: "🌐",
225
225
  short: "Attach custom domains",
226
- args: "<list | add <host> | verify <host> | delete <host>> [project-dir] [--api-url <url>]",
226
+ args: "<list | add <host> | verify <host> | delete <host>> [project-dir] [--api-url <url>] [--account <team>]",
227
227
  brief: "<list | add | verify | delete>",
228
- summary: "Attach a custom domain to the project (TXT-verified). No sub-command lists.",
228
+ summary:
229
+ "List the project's domains, or attach one (TXT-verified). On the Cloud only list works: custom domains are added by an operator, and deploys keep them.",
229
230
  },
230
231
  {
231
232
  name: "secrets",
@@ -351,7 +352,7 @@ export const ENV_VARS: readonly EnvVar[] = [
351
352
  {
352
353
  name: "SPROUTBOAT_PORFFOR_CHANNEL",
353
354
  purpose:
354
- "Porffor channel for standalone builds: `alpha-22` (default) or `alpha-19` (the previous compiler, kept as a fallback for one release).",
355
+ "Porffor channel for standalone builds: `alpha-23` (default), `alpha-22` or `alpha-19` (the oldest compiler, kept as a fallback for one release).",
355
356
  },
356
357
  {
357
358
  name: "SPROUTBOAT_RELEASE",