sproutboat 0.52.1 → 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 +28 -0
- package/README.md +27 -11
- package/SURFACE.md +6 -5
- package/package.json +7 -7
- package/src/dev.ts +24 -24
- package/src/env-file.ts +248 -0
- package/src/init-package.ts +99 -0
- package/src/main.ts +116 -10
- package/src/run.ts +168 -0
- package/src/surface.ts +16 -5
- package/src/tail-log.ts +471 -0
- package/src/tail.ts +40 -3
package/src/main.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { mkdir, open, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
|
-
import { rmSync } from "node:fs";
|
|
3
|
+
import { existsSync, rmSync } from "node:fs";
|
|
4
4
|
import { basename, dirname, relative, resolve } from "node:path";
|
|
5
5
|
import { isBoolean, isSafeInteger, isString, jsonObject, parseJsonValue, type JsonObject } from "./json";
|
|
6
6
|
import type { AssetFiles } from "./assets";
|
|
@@ -18,11 +18,16 @@ 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";
|
|
23
|
+
import { constants as osConstants } from "node:os";
|
|
22
24
|
import { inspectToolchain } from "./toolchain";
|
|
23
25
|
import { isLinuxTarget, parseTarget, TARGET_NAMES, type BuildTarget } from "./targets";
|
|
24
26
|
import { writeDeployFiles } from "./standalone-deploy";
|
|
25
|
-
import { parseTailArgs, runTail, TAIL_USAGE, tailQuery } from "./tail";
|
|
27
|
+
import { parseTailArgs, runTail, TAIL_USAGE, tailQuery, type TailArgs } from "./tail";
|
|
28
|
+
import { followLog, LogFilter, readLogStream } from "./tail-log";
|
|
29
|
+
import { EnvFileError, MissingSecretsError } from "./env-file";
|
|
30
|
+
import { parseRunArgs, planRun, RUN_USAGE, RunError } from "./run";
|
|
26
31
|
|
|
27
32
|
// A compiled Bun executable is not a general-purpose `bun` command. Porffor
|
|
28
33
|
// is acquired after installation, so run its external ESM launcher through an
|
|
@@ -168,11 +173,11 @@ const starterConfig = (name: string) => `{
|
|
|
168
173
|
"$schema": "https://sproutboat.com/schema.json",
|
|
169
174
|
"name": "${name}",
|
|
170
175
|
"main": "src/index.js",
|
|
171
|
-
"compatibility_date": "2026-
|
|
176
|
+
"compatibility_date": "2026-09-28"
|
|
172
177
|
}
|
|
173
178
|
`;
|
|
174
179
|
const starterHandler = `export default {
|
|
175
|
-
fetch() {
|
|
180
|
+
fetch(request, env, ctx) {
|
|
176
181
|
return new Response("hello from Sproutboat");
|
|
177
182
|
}
|
|
178
183
|
};
|
|
@@ -261,6 +266,21 @@ async function init(name = "hello") {
|
|
|
261
266
|
] as const) {
|
|
262
267
|
if (await Bun.file(path).exists()) fail(`${basename(directory)} already contains ${label}`);
|
|
263
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
|
+
}
|
|
264
284
|
await mkdir(resolve(directory, "src"), { recursive: true });
|
|
265
285
|
await writeFile(configPath, starterConfig(name), { flag: "wx" });
|
|
266
286
|
await writeFile(handlerPath, starterHandler, { flag: "wx" });
|
|
@@ -274,6 +294,19 @@ async function init(name = "hello") {
|
|
|
274
294
|
await writeFile(gitignorePath, starterGitignore, { flag: "wx" });
|
|
275
295
|
console.log(`Created ${basename(directory)}/.gitignore`);
|
|
276
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")}.`));
|
|
277
310
|
}
|
|
278
311
|
|
|
279
312
|
async function check(directory?: string) {
|
|
@@ -398,13 +431,30 @@ async function emitOnly(directory: string | undefined, target: BuildTarget, stan
|
|
|
398
431
|
}
|
|
399
432
|
|
|
400
433
|
/** #62 — build for this machine, run it against a real broker, rebuild on save. */
|
|
434
|
+
const DEV_USAGE = "dev [project-dir] [--port <n>] [--no-watch] [--env <name>] [--env-file <file>]...";
|
|
435
|
+
|
|
401
436
|
async function dev(args: string[]) {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
437
|
+
let directory: string | undefined;
|
|
438
|
+
let portArg: string | undefined;
|
|
439
|
+
let envName: string | undefined;
|
|
440
|
+
const envFiles: string[] = [];
|
|
441
|
+
for (let at = 0; at < args.length; at += 1) {
|
|
442
|
+
const arg = args[at];
|
|
443
|
+
if (arg === "--no-watch") continue;
|
|
444
|
+
if (arg === "--port" || arg === "--env" || arg === "--env-file") {
|
|
445
|
+
const value = args[at + 1];
|
|
446
|
+
if (value === undefined || value.startsWith("--")) usageError(`${arg} needs a value`, DEV_USAGE);
|
|
447
|
+
at += 1;
|
|
448
|
+
if (arg === "--port") portArg = value;
|
|
449
|
+
else if (arg === "--env") envName = value;
|
|
450
|
+
else envFiles.push(resolve(value));
|
|
451
|
+
} else if (!arg.startsWith("--") && directory === undefined) directory = arg;
|
|
452
|
+
}
|
|
405
453
|
const port = Number(portArg ?? 8787);
|
|
406
|
-
if (!Number.isSafeInteger(port) || port < 1 || port > 65_535)
|
|
407
|
-
|
|
454
|
+
if (!Number.isSafeInteger(port) || port < 1 || port > 65_535) usageError(`invalid --port: ${portArg}`, DEV_USAGE);
|
|
455
|
+
if (envName !== undefined && !/^[A-Za-z0-9_-]+$/.test(envName))
|
|
456
|
+
usageError("--env takes a name (letters, digits, - and _)", DEV_USAGE);
|
|
457
|
+
for (const file of envFiles) if (!existsSync(file)) fail(`env file not found: ${file}`);
|
|
408
458
|
const project = await readProject(directory);
|
|
409
459
|
await refreshTypes(project.directory, project.config);
|
|
410
460
|
console.log(dim(`Building ${project.config.name} for this machine (${hostTarget()})…`));
|
|
@@ -414,6 +464,8 @@ async function dev(args: string[]) {
|
|
|
414
464
|
sourcePath: project.sourcePath,
|
|
415
465
|
source: project.bundle.code,
|
|
416
466
|
port,
|
|
467
|
+
envFiles,
|
|
468
|
+
envName,
|
|
417
469
|
watch: !args.includes("--no-watch"),
|
|
418
470
|
// Re-read from disk on every rebuild: the point of watching is that the
|
|
419
471
|
// files changed, so the bundle captured at startup is stale by definition.
|
|
@@ -425,6 +477,33 @@ async function dev(args: string[]) {
|
|
|
425
477
|
});
|
|
426
478
|
}
|
|
427
479
|
|
|
480
|
+
/** #275: start a standalone binary with its port, data directory and secrets supplied at launch. */
|
|
481
|
+
async function run(args: string[]) {
|
|
482
|
+
const result = parseRunArgs(args);
|
|
483
|
+
if ("error" in result) usageError(result.error, RUN_USAGE);
|
|
484
|
+
let plan;
|
|
485
|
+
try {
|
|
486
|
+
plan = await planRun(result.args, { cwd: process.cwd(), inherited: process.env });
|
|
487
|
+
} catch (error) {
|
|
488
|
+
if (error instanceof RunError || error instanceof EnvFileError || error instanceof MissingSecretsError)
|
|
489
|
+
fail(error.message);
|
|
490
|
+
throw error;
|
|
491
|
+
}
|
|
492
|
+
for (const warning of plan.warnings) console.warn(amber(`! ${warning}`));
|
|
493
|
+
for (const line of plan.summary) console.log(dim(line));
|
|
494
|
+
if (result.args.check) {
|
|
495
|
+
console.log(ok(`ready to launch ${basename(plan.binary)} (not started: --check)`));
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
console.log(ok(`launching ${plan.binary}`));
|
|
499
|
+
const child = Bun.spawn([plan.binary], { env: plan.env, stdin: "inherit", stdout: "inherit", stderr: "inherit" });
|
|
500
|
+
// Forward termination to the binary, which is the only process this started.
|
|
501
|
+
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"] as const) process.on(signal, () => child.kill(signal));
|
|
502
|
+
const code = await child.exited;
|
|
503
|
+
const signalNumber = child.signalCode ? osConstants.signals[child.signalCode] : undefined;
|
|
504
|
+
process.exit(signalNumber === undefined ? code : 128 + signalNumber);
|
|
505
|
+
}
|
|
506
|
+
|
|
428
507
|
const PROVISION_FIELDS = [
|
|
429
508
|
["kv_namespaces", "kv"],
|
|
430
509
|
["d1_databases", "d1"],
|
|
@@ -792,10 +871,32 @@ async function rollback(args: string[]) {
|
|
|
792
871
|
console.log(deployment.url);
|
|
793
872
|
}
|
|
794
873
|
|
|
874
|
+
/** #268: follow a standalone binary's SB_LOG=json records from a file or stdin. Local only: no login. */
|
|
875
|
+
async function tailLog(parsed: TailArgs, file: string) {
|
|
876
|
+
// Ctrl-C (or a terminated pipe reader) ends the stream and exits 0.
|
|
877
|
+
const controller = new AbortController();
|
|
878
|
+
for (const signal of ["SIGINT", "SIGTERM"] as const) process.once(signal, () => controller.abort());
|
|
879
|
+
process.stdout.on("error", () => controller.abort());
|
|
880
|
+
const sink = {
|
|
881
|
+
format: parsed.format,
|
|
882
|
+
filter: new LogFilter({ status: parsed.status, method: parsed.method, search: parsed.search }),
|
|
883
|
+
signal: controller.signal,
|
|
884
|
+
stdout: (text: string) => void process.stdout.write(text),
|
|
885
|
+
stderr: (text: string) => void process.stderr.write(text),
|
|
886
|
+
};
|
|
887
|
+
try {
|
|
888
|
+
if (file === "-") await readLogStream(Bun.stdin.stream(), sink);
|
|
889
|
+
else await followLog({ ...sink, path: resolve(file), lines: parsed.lines, follow: parsed.follow });
|
|
890
|
+
} catch (error) {
|
|
891
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
795
895
|
async function tail(args: string[]) {
|
|
796
896
|
const result = parseTailArgs(args);
|
|
797
897
|
if ("error" in result) usageError(result.error, TAIL_USAGE);
|
|
798
898
|
const parsed = result.args;
|
|
899
|
+
if (parsed.file !== undefined) return tailLog(parsed, parsed.file);
|
|
799
900
|
const [project, { apiUrl, token }] = await Promise.all([readProjectConfig(parsed.dir), apiCredentials()]);
|
|
800
901
|
const base = `${apiUrl}/api/projects/${project.config.name}`;
|
|
801
902
|
if (parsed.recent || parsed.sprout) {
|
|
@@ -1359,7 +1460,9 @@ if (command === "--version" || command === "-v") {
|
|
|
1359
1460
|
}
|
|
1360
1461
|
if (args.includes("-h") || args.includes("--help")) commandHelp(command);
|
|
1361
1462
|
|
|
1362
|
-
|
|
1463
|
+
// A local log needs no network: skip the version check for it (#268).
|
|
1464
|
+
if (!(command === "tail" && args.some((arg) => arg === "--file" || arg.startsWith("--file="))))
|
|
1465
|
+
await notifyIfOutdated(CLI_VERSION);
|
|
1363
1466
|
|
|
1364
1467
|
switch (command) {
|
|
1365
1468
|
case "init":
|
|
@@ -1374,6 +1477,9 @@ switch (command) {
|
|
|
1374
1477
|
case "dev":
|
|
1375
1478
|
await dev(args);
|
|
1376
1479
|
break;
|
|
1480
|
+
case "run":
|
|
1481
|
+
await run(args);
|
|
1482
|
+
break;
|
|
1377
1483
|
case "build": {
|
|
1378
1484
|
const withRestore = args.includes("--with-restore");
|
|
1379
1485
|
if (withRestore && (!args.includes("--standalone") || args.includes("--emit-only")))
|
package/src/run.ts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sproutboat run`: launch a standalone binary with the settings it takes at
|
|
3
|
+
* launch (#275), without baking any of them into it.
|
|
4
|
+
*
|
|
5
|
+
* A standalone binary reads its port, data directory and secrets from the
|
|
6
|
+
* environment and nothing from argv. This command builds that environment from
|
|
7
|
+
* three layers and starts the binary with it. Highest precedence first:
|
|
8
|
+
*
|
|
9
|
+
* 1. explicit launch options: `--port` (PORT) and `--data-dir` (SB_DATA_DIR);
|
|
10
|
+
* 2. the files given with `--env-file` (dotenv syntax, see env-file.ts; repeat
|
|
11
|
+
* the flag and a later file overrides an earlier one);
|
|
12
|
+
* 3. the environment this command inherited, which is how a service manager
|
|
13
|
+
* hands a production binary its settings.
|
|
14
|
+
*
|
|
15
|
+
* Unlike `dev`, which warns, a launch fails when a declared secret (the
|
|
16
|
+
* config's `secrets` plus any `--require`) is unset or empty, as
|
|
17
|
+
* `wrangler deploy` does for a missing required secret. Nothing starts. The same binary therefore moves between a
|
|
18
|
+
* laptop and a server with a different env file, port and data directory.
|
|
19
|
+
* Nothing is written to disk and no value is ever printed: the summary names
|
|
20
|
+
* secrets and the layer each came from.
|
|
21
|
+
*/
|
|
22
|
+
import { existsSync } from "node:fs";
|
|
23
|
+
import { readFile } from "node:fs/promises";
|
|
24
|
+
import { resolve } from "node:path";
|
|
25
|
+
import { parseConfig } from "./config";
|
|
26
|
+
import {
|
|
27
|
+
mergeEntries,
|
|
28
|
+
MissingSecretsError,
|
|
29
|
+
missingSecrets,
|
|
30
|
+
readEnvFile,
|
|
31
|
+
resolveEnvironment,
|
|
32
|
+
type EnvEntry,
|
|
33
|
+
type EnvSource,
|
|
34
|
+
} from "./env-file";
|
|
35
|
+
|
|
36
|
+
export const RUN_USAGE =
|
|
37
|
+
"run [project-dir] [--binary <file>] [--env-file <file>]... [--port <n>] [--data-dir <dir>] [--require <NAME,...>] [--check]";
|
|
38
|
+
|
|
39
|
+
const VALUE_FLAGS = ["--binary", "--env-file", "--port", "--data-dir", "--require"] as const;
|
|
40
|
+
const NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
41
|
+
|
|
42
|
+
export type RunArgs = {
|
|
43
|
+
dir: string | undefined;
|
|
44
|
+
binary: string | undefined;
|
|
45
|
+
/** Every `--env-file`, in the order given. */
|
|
46
|
+
envFiles: string[];
|
|
47
|
+
port: number | undefined;
|
|
48
|
+
dataDir: string | undefined;
|
|
49
|
+
require: string[];
|
|
50
|
+
check: boolean;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** Parse `run` arguments; `error` is a usage error message. */
|
|
54
|
+
export function parseRunArgs(args: string[]): { args: RunArgs } | { error: string } {
|
|
55
|
+
const parsed: RunArgs = {
|
|
56
|
+
dir: undefined,
|
|
57
|
+
binary: undefined,
|
|
58
|
+
envFiles: [],
|
|
59
|
+
port: undefined,
|
|
60
|
+
dataDir: undefined,
|
|
61
|
+
require: [],
|
|
62
|
+
check: false,
|
|
63
|
+
};
|
|
64
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
65
|
+
const arg = args[index];
|
|
66
|
+
if (arg === "--check") parsed.check = true;
|
|
67
|
+
else if (arg.startsWith("--")) {
|
|
68
|
+
const eq = arg.indexOf("=");
|
|
69
|
+
const flag = eq < 0 ? arg : arg.slice(0, eq);
|
|
70
|
+
if (!VALUE_FLAGS.some((known) => known === flag)) return { error: `run: unknown flag "${flag}"` };
|
|
71
|
+
const value = eq < 0 ? args[++index] : arg.slice(eq + 1);
|
|
72
|
+
if (value === undefined || (eq < 0 && value.startsWith("--"))) return { error: `run: ${flag} needs a value` };
|
|
73
|
+
if (flag === "--binary") parsed.binary = value;
|
|
74
|
+
else if (flag === "--env-file") parsed.envFiles.push(value);
|
|
75
|
+
else if (flag === "--data-dir") parsed.dataDir = value;
|
|
76
|
+
else if (flag === "--port") {
|
|
77
|
+
const port = Number(value);
|
|
78
|
+
if (!Number.isInteger(port) || port < 1 || port > 65_535)
|
|
79
|
+
return { error: `run: --port must be a whole number from 1 to 65535 (got "${value}")` };
|
|
80
|
+
parsed.port = port;
|
|
81
|
+
} else {
|
|
82
|
+
for (const name of value.split(",").map((part) => part.trim())) {
|
|
83
|
+
if (name === "") continue;
|
|
84
|
+
if (!NAME.test(name)) return { error: `run: --require takes variable names (letters, digits, underscore)` };
|
|
85
|
+
parsed.require.push(name);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} else if (parsed.dir === undefined) parsed.dir = arg;
|
|
89
|
+
else return { error: `run: unexpected argument "${arg}"` };
|
|
90
|
+
}
|
|
91
|
+
return { args: parsed };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A launch that cannot proceed for a reason the user can fix; the message is safe to print. */
|
|
95
|
+
export class RunError extends Error {}
|
|
96
|
+
|
|
97
|
+
export type RunPlan = {
|
|
98
|
+
binary: string;
|
|
99
|
+
/** The complete environment the binary starts with. Contains secret values: never print it. */
|
|
100
|
+
env: Record<string, string>;
|
|
101
|
+
declared: string[];
|
|
102
|
+
/** Lines naming what was resolved and from where. No values of secrets. */
|
|
103
|
+
summary: string[];
|
|
104
|
+
/** Env-file lines that were skipped, as `file:line: reason`. Never a value. */
|
|
105
|
+
warnings: string[];
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/** Resolve what to launch and with which environment, or throw. Starts nothing. */
|
|
109
|
+
export async function planRun(
|
|
110
|
+
args: RunArgs,
|
|
111
|
+
io: { cwd: string; inherited: Record<string, string | undefined> },
|
|
112
|
+
): Promise<RunPlan> {
|
|
113
|
+
const directory = resolve(io.cwd, args.dir ?? ".");
|
|
114
|
+
const configPath = resolve(directory, "sproutboat.jsonc");
|
|
115
|
+
let name: string | undefined;
|
|
116
|
+
const declared: string[] = [];
|
|
117
|
+
if (existsSync(configPath)) {
|
|
118
|
+
const parsed = parseConfig(await readFile(configPath, "utf8"));
|
|
119
|
+
if (!parsed.ok) throw new RunError(parsed.errors.join("\n"));
|
|
120
|
+
name = parsed.value.name;
|
|
121
|
+
declared.push(...(parsed.value.secrets ?? []));
|
|
122
|
+
} else if (args.binary === undefined) {
|
|
123
|
+
throw new RunError(
|
|
124
|
+
`no sproutboat.jsonc found in ${directory}; pass --binary <file> to run a binary without a project`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
for (const required of args.require) if (!declared.includes(required)) declared.push(required);
|
|
128
|
+
|
|
129
|
+
const binary = args.binary !== undefined ? resolve(io.cwd, args.binary) : resolve(directory, "dist", name ?? "");
|
|
130
|
+
if (!existsSync(binary)) {
|
|
131
|
+
throw new RunError(
|
|
132
|
+
`no binary at ${binary}; build it first with: sproutboat build --standalone${args.dir ? ` ${args.dir}` : ""}`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const layers: EnvEntry[][] = [];
|
|
137
|
+
const warnings: string[] = [];
|
|
138
|
+
for (const path of args.envFiles) {
|
|
139
|
+
const parsed = await readEnvFile(resolve(io.cwd, path));
|
|
140
|
+
layers.push(parsed.entries);
|
|
141
|
+
warnings.push(...parsed.warnings);
|
|
142
|
+
}
|
|
143
|
+
const file = mergeEntries(layers);
|
|
144
|
+
const options: Record<string, string> = {};
|
|
145
|
+
if (args.port !== undefined) options.PORT = String(args.port);
|
|
146
|
+
if (args.dataDir !== undefined) options.SB_DATA_DIR = resolve(io.cwd, args.dataDir);
|
|
147
|
+
const { env, sources } = resolveEnvironment({ inherited: io.inherited, file, options });
|
|
148
|
+
|
|
149
|
+
const missing = missingSecrets(declared, env);
|
|
150
|
+
if (missing.length > 0) {
|
|
151
|
+
throw new MissingSecretsError(
|
|
152
|
+
missing,
|
|
153
|
+
args.envFiles.length > 0 ? args.envFiles.join(" or ") : "an env file (--env-file)",
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const origin = (key: string): EnvSource | undefined => sources.get(key);
|
|
158
|
+
const summary: string[] = [];
|
|
159
|
+
summary.push(
|
|
160
|
+
declared.length === 0
|
|
161
|
+
? "secrets none declared"
|
|
162
|
+
: `secrets ${declared.map((secret) => `${secret} (${origin(secret) ?? "unset"})`).join(", ")}`,
|
|
163
|
+
);
|
|
164
|
+
if (env.PORT) summary.push(`port ${env.PORT} (${origin("PORT")})`);
|
|
165
|
+
const dataKey = env.SB_DATA_DIR ? "SB_DATA_DIR" : env.SPROUTBOAT_DATA ? "SPROUTBOAT_DATA" : undefined;
|
|
166
|
+
if (dataKey) summary.push(`data ${env[dataKey]} (${origin(dataKey)})`);
|
|
167
|
+
return { binary, env, declared, summary, warnings };
|
|
168
|
+
}
|
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",
|
|
@@ -89,9 +90,19 @@ export const COMMANDS: readonly Command[] = [
|
|
|
89
90
|
name: "dev",
|
|
90
91
|
group: "Develop",
|
|
91
92
|
emoji: "⚡",
|
|
92
|
-
args: "[project-dir] [--port <n>] [--no-watch]",
|
|
93
|
+
args: "[project-dir] [--port <n>] [--no-watch] [--env <name>] [--env-file <file>]...",
|
|
93
94
|
brief: "[project-dir] [--port <n>]",
|
|
94
|
-
summary:
|
|
95
|
+
summary:
|
|
96
|
+
"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.",
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: "run",
|
|
100
|
+
group: "Develop",
|
|
101
|
+
emoji: "🚀",
|
|
102
|
+
args: "[project-dir] [--binary <file>] [--env-file <file>]... [--port <n>] [--data-dir <dir>] [--require <NAME,...>] [--check]",
|
|
103
|
+
brief: "[--env-file <file>] [--port <n>]",
|
|
104
|
+
summary:
|
|
105
|
+
"Launch a standalone binary with its port, data directory and declared secrets supplied at launch, never baked in. Precedence: --port/--data-dir, then the --env-file files (later overrides earlier), then the environment. A missing declared secret fails the launch, like wrangler deploy; --check validates without starting.",
|
|
95
106
|
},
|
|
96
107
|
{
|
|
97
108
|
name: "build",
|
|
@@ -141,10 +152,10 @@ export const COMMANDS: readonly Command[] = [
|
|
|
141
152
|
name: "tail",
|
|
142
153
|
group: "Ship",
|
|
143
154
|
emoji: "📡",
|
|
144
|
-
args: "[project-dir] [--format pretty|json] [--status ok|error|<code>|5xx] [--method GET,POST] [--sampling-rate <0..1>] [--search <text>] [--outcome <name>] [--recent] [--sprout]",
|
|
155
|
+
args: "[project-dir] [--format pretty|json] [--status ok|error|<code>|5xx] [--method GET,POST] [--sampling-rate <0..1>] [--search <text>] [--outcome <name>] [--recent] [--sprout] [--file <log|-> [--lines <n>] [--no-follow]]",
|
|
145
156
|
brief: "[--status 5xx] [--search <text>]",
|
|
146
157
|
summary:
|
|
147
|
-
"Stream live request events, console output and exceptions (like wrangler tail); --recent prints the last 100 request records, --sprout the running sprout + broker stdout/stderr.",
|
|
158
|
+
"Stream live request events, console output and exceptions (like wrangler tail); --recent prints the last 100 request records, --sprout the running sprout + broker stdout/stderr. --file follows the JSON records a standalone binary writes with SB_LOG=json (no login needed).",
|
|
148
159
|
},
|
|
149
160
|
|
|
150
161
|
...storageCommands,
|