sproutboat 0.52.0 → 0.53.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/CHANGELOG.md +30 -0
- package/README.md +6 -2
- package/SURFACE.md +5 -4
- package/package.json +8 -8
- package/src/build.ts +3 -2
- package/src/compile.ts +18 -1
- package/src/dev.ts +24 -24
- package/src/env-file.ts +248 -0
- package/src/main.ts +87 -10
- package/src/run.ts +168 -0
- package/src/surface.ts +14 -4
- package/src/tail-log.ts +471 -0
- package/src/tail.ts +40 -3
- package/src/toolchain.ts +13 -0
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";
|
|
@@ -19,10 +19,14 @@ import { notifyIfOutdated } from "./update-check";
|
|
|
19
19
|
import { controlVersionWarning } from "./api-version";
|
|
20
20
|
import { amber, bold, dim, leaf, ok, rose } from "./style";
|
|
21
21
|
import { pathToFileURL } from "node:url";
|
|
22
|
+
import { constants as osConstants } from "node:os";
|
|
22
23
|
import { inspectToolchain } from "./toolchain";
|
|
23
24
|
import { isLinuxTarget, parseTarget, TARGET_NAMES, type BuildTarget } from "./targets";
|
|
24
25
|
import { writeDeployFiles } from "./standalone-deploy";
|
|
25
|
-
import { parseTailArgs, runTail, TAIL_USAGE, tailQuery } from "./tail";
|
|
26
|
+
import { parseTailArgs, runTail, TAIL_USAGE, tailQuery, type TailArgs } from "./tail";
|
|
27
|
+
import { followLog, LogFilter, readLogStream } from "./tail-log";
|
|
28
|
+
import { EnvFileError, MissingSecretsError } from "./env-file";
|
|
29
|
+
import { parseRunArgs, planRun, RUN_USAGE, RunError } from "./run";
|
|
26
30
|
|
|
27
31
|
// A compiled Bun executable is not a general-purpose `bun` command. Porffor
|
|
28
32
|
// is acquired after installation, so run its external ESM launcher through an
|
|
@@ -168,11 +172,11 @@ const starterConfig = (name: string) => `{
|
|
|
168
172
|
"$schema": "https://sproutboat.com/schema.json",
|
|
169
173
|
"name": "${name}",
|
|
170
174
|
"main": "src/index.js",
|
|
171
|
-
"compatibility_date": "2026-
|
|
175
|
+
"compatibility_date": "2026-09-28"
|
|
172
176
|
}
|
|
173
177
|
`;
|
|
174
178
|
const starterHandler = `export default {
|
|
175
|
-
fetch() {
|
|
179
|
+
fetch(request, env, ctx) {
|
|
176
180
|
return new Response("hello from Sproutboat");
|
|
177
181
|
}
|
|
178
182
|
};
|
|
@@ -398,13 +402,30 @@ async function emitOnly(directory: string | undefined, target: BuildTarget, stan
|
|
|
398
402
|
}
|
|
399
403
|
|
|
400
404
|
/** #62 — build for this machine, run it against a real broker, rebuild on save. */
|
|
405
|
+
const DEV_USAGE = "dev [project-dir] [--port <n>] [--no-watch] [--env <name>] [--env-file <file>]...";
|
|
406
|
+
|
|
401
407
|
async function dev(args: string[]) {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
408
|
+
let directory: string | undefined;
|
|
409
|
+
let portArg: string | undefined;
|
|
410
|
+
let envName: string | undefined;
|
|
411
|
+
const envFiles: string[] = [];
|
|
412
|
+
for (let at = 0; at < args.length; at += 1) {
|
|
413
|
+
const arg = args[at];
|
|
414
|
+
if (arg === "--no-watch") continue;
|
|
415
|
+
if (arg === "--port" || arg === "--env" || arg === "--env-file") {
|
|
416
|
+
const value = args[at + 1];
|
|
417
|
+
if (value === undefined || value.startsWith("--")) usageError(`${arg} needs a value`, DEV_USAGE);
|
|
418
|
+
at += 1;
|
|
419
|
+
if (arg === "--port") portArg = value;
|
|
420
|
+
else if (arg === "--env") envName = value;
|
|
421
|
+
else envFiles.push(resolve(value));
|
|
422
|
+
} else if (!arg.startsWith("--") && directory === undefined) directory = arg;
|
|
423
|
+
}
|
|
405
424
|
const port = Number(portArg ?? 8787);
|
|
406
|
-
if (!Number.isSafeInteger(port) || port < 1 || port > 65_535)
|
|
407
|
-
|
|
425
|
+
if (!Number.isSafeInteger(port) || port < 1 || port > 65_535) usageError(`invalid --port: ${portArg}`, DEV_USAGE);
|
|
426
|
+
if (envName !== undefined && !/^[A-Za-z0-9_-]+$/.test(envName))
|
|
427
|
+
usageError("--env takes a name (letters, digits, - and _)", DEV_USAGE);
|
|
428
|
+
for (const file of envFiles) if (!existsSync(file)) fail(`env file not found: ${file}`);
|
|
408
429
|
const project = await readProject(directory);
|
|
409
430
|
await refreshTypes(project.directory, project.config);
|
|
410
431
|
console.log(dim(`Building ${project.config.name} for this machine (${hostTarget()})…`));
|
|
@@ -414,6 +435,8 @@ async function dev(args: string[]) {
|
|
|
414
435
|
sourcePath: project.sourcePath,
|
|
415
436
|
source: project.bundle.code,
|
|
416
437
|
port,
|
|
438
|
+
envFiles,
|
|
439
|
+
envName,
|
|
417
440
|
watch: !args.includes("--no-watch"),
|
|
418
441
|
// Re-read from disk on every rebuild: the point of watching is that the
|
|
419
442
|
// files changed, so the bundle captured at startup is stale by definition.
|
|
@@ -425,6 +448,33 @@ async function dev(args: string[]) {
|
|
|
425
448
|
});
|
|
426
449
|
}
|
|
427
450
|
|
|
451
|
+
/** #275: start a standalone binary with its port, data directory and secrets supplied at launch. */
|
|
452
|
+
async function run(args: string[]) {
|
|
453
|
+
const result = parseRunArgs(args);
|
|
454
|
+
if ("error" in result) usageError(result.error, RUN_USAGE);
|
|
455
|
+
let plan;
|
|
456
|
+
try {
|
|
457
|
+
plan = await planRun(result.args, { cwd: process.cwd(), inherited: process.env });
|
|
458
|
+
} catch (error) {
|
|
459
|
+
if (error instanceof RunError || error instanceof EnvFileError || error instanceof MissingSecretsError)
|
|
460
|
+
fail(error.message);
|
|
461
|
+
throw error;
|
|
462
|
+
}
|
|
463
|
+
for (const warning of plan.warnings) console.warn(amber(`! ${warning}`));
|
|
464
|
+
for (const line of plan.summary) console.log(dim(line));
|
|
465
|
+
if (result.args.check) {
|
|
466
|
+
console.log(ok(`ready to launch ${basename(plan.binary)} (not started: --check)`));
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
console.log(ok(`launching ${plan.binary}`));
|
|
470
|
+
const child = Bun.spawn([plan.binary], { env: plan.env, stdin: "inherit", stdout: "inherit", stderr: "inherit" });
|
|
471
|
+
// Forward termination to the binary, which is the only process this started.
|
|
472
|
+
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"] as const) process.on(signal, () => child.kill(signal));
|
|
473
|
+
const code = await child.exited;
|
|
474
|
+
const signalNumber = child.signalCode ? osConstants.signals[child.signalCode] : undefined;
|
|
475
|
+
process.exit(signalNumber === undefined ? code : 128 + signalNumber);
|
|
476
|
+
}
|
|
477
|
+
|
|
428
478
|
const PROVISION_FIELDS = [
|
|
429
479
|
["kv_namespaces", "kv"],
|
|
430
480
|
["d1_databases", "d1"],
|
|
@@ -792,10 +842,32 @@ async function rollback(args: string[]) {
|
|
|
792
842
|
console.log(deployment.url);
|
|
793
843
|
}
|
|
794
844
|
|
|
845
|
+
/** #268: follow a standalone binary's SB_LOG=json records from a file or stdin. Local only: no login. */
|
|
846
|
+
async function tailLog(parsed: TailArgs, file: string) {
|
|
847
|
+
// Ctrl-C (or a terminated pipe reader) ends the stream and exits 0.
|
|
848
|
+
const controller = new AbortController();
|
|
849
|
+
for (const signal of ["SIGINT", "SIGTERM"] as const) process.once(signal, () => controller.abort());
|
|
850
|
+
process.stdout.on("error", () => controller.abort());
|
|
851
|
+
const sink = {
|
|
852
|
+
format: parsed.format,
|
|
853
|
+
filter: new LogFilter({ status: parsed.status, method: parsed.method, search: parsed.search }),
|
|
854
|
+
signal: controller.signal,
|
|
855
|
+
stdout: (text: string) => void process.stdout.write(text),
|
|
856
|
+
stderr: (text: string) => void process.stderr.write(text),
|
|
857
|
+
};
|
|
858
|
+
try {
|
|
859
|
+
if (file === "-") await readLogStream(Bun.stdin.stream(), sink);
|
|
860
|
+
else await followLog({ ...sink, path: resolve(file), lines: parsed.lines, follow: parsed.follow });
|
|
861
|
+
} catch (error) {
|
|
862
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
795
866
|
async function tail(args: string[]) {
|
|
796
867
|
const result = parseTailArgs(args);
|
|
797
868
|
if ("error" in result) usageError(result.error, TAIL_USAGE);
|
|
798
869
|
const parsed = result.args;
|
|
870
|
+
if (parsed.file !== undefined) return tailLog(parsed, parsed.file);
|
|
799
871
|
const [project, { apiUrl, token }] = await Promise.all([readProjectConfig(parsed.dir), apiCredentials()]);
|
|
800
872
|
const base = `${apiUrl}/api/projects/${project.config.name}`;
|
|
801
873
|
if (parsed.recent || parsed.sprout) {
|
|
@@ -1359,7 +1431,9 @@ if (command === "--version" || command === "-v") {
|
|
|
1359
1431
|
}
|
|
1360
1432
|
if (args.includes("-h") || args.includes("--help")) commandHelp(command);
|
|
1361
1433
|
|
|
1362
|
-
|
|
1434
|
+
// A local log needs no network: skip the version check for it (#268).
|
|
1435
|
+
if (!(command === "tail" && args.some((arg) => arg === "--file" || arg.startsWith("--file="))))
|
|
1436
|
+
await notifyIfOutdated(CLI_VERSION);
|
|
1363
1437
|
|
|
1364
1438
|
switch (command) {
|
|
1365
1439
|
case "init":
|
|
@@ -1374,6 +1448,9 @@ switch (command) {
|
|
|
1374
1448
|
case "dev":
|
|
1375
1449
|
await dev(args);
|
|
1376
1450
|
break;
|
|
1451
|
+
case "run":
|
|
1452
|
+
await run(args);
|
|
1453
|
+
break;
|
|
1377
1454
|
case "build": {
|
|
1378
1455
|
const withRestore = args.includes("--with-restore");
|
|
1379
1456
|
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
|
@@ -89,9 +89,19 @@ export const COMMANDS: readonly Command[] = [
|
|
|
89
89
|
name: "dev",
|
|
90
90
|
group: "Develop",
|
|
91
91
|
emoji: "⚡",
|
|
92
|
-
args: "[project-dir] [--port <n>] [--no-watch]",
|
|
92
|
+
args: "[project-dir] [--port <n>] [--no-watch] [--env <name>] [--env-file <file>]...",
|
|
93
93
|
brief: "[project-dir] [--port <n>]",
|
|
94
|
-
summary:
|
|
94
|
+
summary:
|
|
95
|
+
"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.",
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: "run",
|
|
99
|
+
group: "Develop",
|
|
100
|
+
emoji: "🚀",
|
|
101
|
+
args: "[project-dir] [--binary <file>] [--env-file <file>]... [--port <n>] [--data-dir <dir>] [--require <NAME,...>] [--check]",
|
|
102
|
+
brief: "[--env-file <file>] [--port <n>]",
|
|
103
|
+
summary:
|
|
104
|
+
"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
105
|
},
|
|
96
106
|
{
|
|
97
107
|
name: "build",
|
|
@@ -141,10 +151,10 @@ export const COMMANDS: readonly Command[] = [
|
|
|
141
151
|
name: "tail",
|
|
142
152
|
group: "Ship",
|
|
143
153
|
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]",
|
|
154
|
+
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
155
|
brief: "[--status 5xx] [--search <text>]",
|
|
146
156
|
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.",
|
|
157
|
+
"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
158
|
},
|
|
149
159
|
|
|
150
160
|
...storageCommands,
|