sproutboat 0.4.11 → 0.6.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/README.md +74 -55
- package/SURFACE.md +13 -7
- package/package.json +28 -20
- package/src/assets.ts +29 -9
- package/src/broker.ts +188 -87
- package/src/build.ts +42 -7
- package/src/bundle.ts +70 -0
- package/src/compile.ts +37 -9
- package/src/config.ts +72 -30
- package/src/credentials.ts +19 -2
- package/src/dev.ts +213 -0
- package/src/json.ts +38 -0
- package/src/main.ts +473 -126
- package/src/manifest.ts +81 -14
- package/src/native-fetch-prelude.js +274 -135
- package/src/patch-porffor.ts +5 -2
- package/src/report.ts +39 -17
- package/src/source.ts +20 -2
- package/src/style.ts +9 -6
- package/src/surface.ts +195 -42
- package/src/toolchain.ts +21 -7
- package/src/update-check.ts +33 -9
- package/src/wrap.ts +71 -17
package/src/main.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
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 { isBoolean, isSafeInteger, isString, jsonObject, parseJsonValue, type JsonObject } from "./json";
|
|
5
|
+
import type { AssetFiles } from "./assets";
|
|
4
6
|
import { parseConfig, pinBindingId, resourceRefs, type SproutboatConfig } from "./config";
|
|
5
7
|
import { validateHttpSyncSource } from "./source";
|
|
6
8
|
import { buildArtifact } from "./build";
|
|
7
|
-
import {
|
|
9
|
+
import { bundleHandler, BundleError, type BundleResult } from "./bundle";
|
|
10
|
+
import { runDev } from "./dev";
|
|
11
|
+
import { hostTarget, validateManifest, type ArtifactManifest } from "./manifest";
|
|
8
12
|
import { CLI_VERSION, printDeployReport } from "./report";
|
|
9
|
-
import { activeApiUrl, savedToken, saveToken } from "./credentials";
|
|
10
|
-
import { helpText } from "./surface";
|
|
13
|
+
import { activeApiUrl, forgetToken, savedToken, saveToken } from "./credentials";
|
|
14
|
+
import { helpText, STORAGE_PRODUCTS, STORAGE_VERBS, type StorageProduct } from "./surface";
|
|
11
15
|
import { notifyIfOutdated } from "./update-check";
|
|
12
16
|
import { amber, bold, dim, leaf, ok, rose } from "./style";
|
|
13
17
|
|
|
@@ -18,29 +22,14 @@ async function responseText(response: Response, failure: string): Promise<string
|
|
|
18
22
|
fail(`${failure} (${response.status}): ${await response.text()}`);
|
|
19
23
|
}
|
|
20
24
|
|
|
21
|
-
type JsonValue = string | number | boolean | null | JsonObject | JsonValue[];
|
|
22
|
-
type JsonObject = { [key: string]: JsonValue };
|
|
23
|
-
|
|
24
|
-
function isString(value: JsonValue | undefined): value is string {
|
|
25
|
-
return value !== undefined && value === String(value);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function isSafeInteger(value: JsonValue | undefined): value is number {
|
|
29
|
-
return Number.isSafeInteger(value);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function parseJsonValue(source: string): JsonValue {
|
|
33
|
-
const value = JSON.parse(source);
|
|
34
|
-
if (value === null || value === true || value === false || value === String(value) || Number.isFinite(value) || value instanceof Object) return value;
|
|
35
|
-
throw new Error("response was not valid JSON");
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function jsonObject(value: JsonValue): JsonObject | undefined {
|
|
39
|
-
return value instanceof Object && !Array.isArray(value) ? value : undefined;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
25
|
type VersionSummary = { id: string; artifact: string; deployedAt: string; active: boolean };
|
|
43
|
-
type CliAuthorization = {
|
|
26
|
+
type CliAuthorization = {
|
|
27
|
+
deviceCode: string;
|
|
28
|
+
userCode: string;
|
|
29
|
+
verificationUri: string;
|
|
30
|
+
interval: number;
|
|
31
|
+
expiresAt: string;
|
|
32
|
+
};
|
|
44
33
|
|
|
45
34
|
function parseVersionList(source: string): VersionSummary[] | undefined {
|
|
46
35
|
const value = parseJsonValue(source);
|
|
@@ -48,13 +37,27 @@ function parseVersionList(source: string): VersionSummary[] | undefined {
|
|
|
48
37
|
const deployments: VersionSummary[] = [];
|
|
49
38
|
for (const item of value) {
|
|
50
39
|
const record = jsonObject(item);
|
|
51
|
-
if (
|
|
52
|
-
|
|
40
|
+
if (
|
|
41
|
+
!record ||
|
|
42
|
+
!isString(record.id) ||
|
|
43
|
+
!isString(record.artifact) ||
|
|
44
|
+
!isString(record.deployedAt) ||
|
|
45
|
+
(record.active !== true && record.active !== false)
|
|
46
|
+
)
|
|
47
|
+
return undefined;
|
|
48
|
+
deployments.push({
|
|
49
|
+
id: record.id,
|
|
50
|
+
artifact: record.artifact,
|
|
51
|
+
deployedAt: record.deployedAt,
|
|
52
|
+
active: record.active,
|
|
53
|
+
});
|
|
53
54
|
}
|
|
54
55
|
return deployments;
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
function parseUrlResponse(
|
|
58
|
+
function parseUrlResponse(
|
|
59
|
+
source: string,
|
|
60
|
+
): { url: string; id?: string; artifact?: string; unchanged: boolean } | undefined {
|
|
58
61
|
const record = jsonObject(parseJsonValue(source));
|
|
59
62
|
if (!record || !isString(record.url)) return undefined;
|
|
60
63
|
return {
|
|
@@ -70,15 +73,35 @@ function parseUrlResponse(source: string): { url: string; id?: string; artifact?
|
|
|
70
73
|
* the pin only changes by redeploying — and the alpha compiler's output can
|
|
71
74
|
* differ between pins. */
|
|
72
75
|
function parsePorfforDrift(source: string): { from: string; to: string } | undefined {
|
|
73
|
-
const drift = (() => {
|
|
74
|
-
|
|
76
|
+
const drift = (() => {
|
|
77
|
+
try {
|
|
78
|
+
return jsonObject(parseJsonValue(source))?.porfforDrift;
|
|
79
|
+
} catch {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
})();
|
|
83
|
+
const record = drift && jsonObject(drift);
|
|
75
84
|
return record && isString(record.from) && isString(record.to) ? { from: record.from, to: record.to } : undefined;
|
|
76
85
|
}
|
|
77
86
|
|
|
78
87
|
function parseAuthorization(source: string): CliAuthorization | undefined {
|
|
79
88
|
const record = jsonObject(parseJsonValue(source));
|
|
80
|
-
if (
|
|
81
|
-
|
|
89
|
+
if (
|
|
90
|
+
!record ||
|
|
91
|
+
!isString(record.deviceCode) ||
|
|
92
|
+
!isString(record.userCode) ||
|
|
93
|
+
!isString(record.verificationUri) ||
|
|
94
|
+
!isSafeInteger(record.interval) ||
|
|
95
|
+
!isString(record.expiresAt)
|
|
96
|
+
)
|
|
97
|
+
return undefined;
|
|
98
|
+
return {
|
|
99
|
+
deviceCode: record.deviceCode,
|
|
100
|
+
userCode: record.userCode,
|
|
101
|
+
verificationUri: record.verificationUri,
|
|
102
|
+
interval: record.interval,
|
|
103
|
+
expiresAt: record.expiresAt,
|
|
104
|
+
};
|
|
82
105
|
}
|
|
83
106
|
|
|
84
107
|
function parseToken(source: string): string | undefined {
|
|
@@ -99,6 +122,14 @@ const starterHandler = `export default {
|
|
|
99
122
|
}
|
|
100
123
|
};
|
|
101
124
|
`;
|
|
125
|
+
// .sproutboat/ holds build output (dist/) and `dev`'s local broker state
|
|
126
|
+
// (dev/, including its SQLite files) — neither belongs in version control.
|
|
127
|
+
// .dev.vars carries secret values for `sproutboat dev`, same convention as
|
|
128
|
+
// Wrangler's file of the same name.
|
|
129
|
+
const starterGitignore = `.sproutboat/
|
|
130
|
+
.dev.vars
|
|
131
|
+
node_modules/
|
|
132
|
+
`;
|
|
102
133
|
|
|
103
134
|
/** An operational failure — the command was invoked correctly but could not complete. Exit 1. */
|
|
104
135
|
function fail(message: string): never {
|
|
@@ -134,21 +165,49 @@ async function readProject(directory = process.cwd()) {
|
|
|
134
165
|
} catch {
|
|
135
166
|
fail(`entry point not found: ${parsed.value.main}`);
|
|
136
167
|
}
|
|
137
|
-
|
|
168
|
+
// #89 — resolve imports first, then hold the *bundled* module to the
|
|
169
|
+
// capability rules. Validating the entry file instead would let a dependency
|
|
170
|
+
// smuggle in a Node API the handler is not allowed to touch.
|
|
171
|
+
let bundle: BundleResult;
|
|
172
|
+
try {
|
|
173
|
+
bundle = await bundleHandler(sourcePath, projectDirectory);
|
|
174
|
+
} catch (cause) {
|
|
175
|
+
fail(cause instanceof BundleError ? cause.message : String(cause));
|
|
176
|
+
}
|
|
177
|
+
const supported = validateHttpSyncSource(bundle.code, (parsed.value.outbound ?? []).length > 0);
|
|
138
178
|
if (!supported.ok) fail(supported.errors.join("\n"));
|
|
139
|
-
return { directory: projectDirectory, config: parsed.value, sourcePath, source };
|
|
179
|
+
return { directory: projectDirectory, config: parsed.value, sourcePath, source, bundle };
|
|
140
180
|
}
|
|
141
181
|
|
|
142
182
|
async function init(name = "hello") {
|
|
143
|
-
if (!/^[a-z0-9](?:[a-z0-9-]{1,30}[a-z0-9])?$/.test(name))
|
|
183
|
+
if (!/^[a-z0-9](?:[a-z0-9-]{1,30}[a-z0-9])?$/.test(name))
|
|
184
|
+
fail("project name must be a 3–32 character lowercase slug");
|
|
144
185
|
const directory = resolve(process.cwd(), name);
|
|
145
186
|
const configPath = resolve(directory, "sproutboat.jsonc");
|
|
146
|
-
|
|
187
|
+
const handlerPath = resolve(directory, "src/index.js");
|
|
188
|
+
// Check both targets before writing either — `name` can collide with an
|
|
189
|
+
// unrelated existing directory, and a second `wx` write failing partway
|
|
190
|
+
// through used to crash with a raw EEXIST stack trace after already having
|
|
191
|
+
// created sproutboat.jsonc, leaving a half-scaffolded project behind.
|
|
192
|
+
for (const [path, label] of [
|
|
193
|
+
[configPath, "sproutboat.jsonc"],
|
|
194
|
+
[handlerPath, "src/index.js"],
|
|
195
|
+
] as const) {
|
|
196
|
+
if (await Bun.file(path).exists()) fail(`${basename(directory)} already contains ${label}`);
|
|
197
|
+
}
|
|
147
198
|
await mkdir(resolve(directory, "src"), { recursive: true });
|
|
148
199
|
await writeFile(configPath, starterConfig(name), { flag: "wx" });
|
|
149
|
-
await writeFile(
|
|
200
|
+
await writeFile(handlerPath, starterHandler, { flag: "wx" });
|
|
150
201
|
console.log(`Created ${basename(directory)}/sproutboat.jsonc`);
|
|
151
202
|
console.log(`Created ${basename(directory)}/src/index.js`);
|
|
203
|
+
// Unlike the two files above, an existing .gitignore here is not a sign this
|
|
204
|
+
// isn't a fresh project (`name` can collide with an unrelated directory) —
|
|
205
|
+
// leave it alone rather than failing init or clobbering it.
|
|
206
|
+
const gitignorePath = resolve(directory, ".gitignore");
|
|
207
|
+
if (!(await Bun.file(gitignorePath).exists())) {
|
|
208
|
+
await writeFile(gitignorePath, starterGitignore, { flag: "wx" });
|
|
209
|
+
console.log(`Created ${basename(directory)}/.gitignore`);
|
|
210
|
+
}
|
|
152
211
|
}
|
|
153
212
|
|
|
154
213
|
async function check(directory?: string) {
|
|
@@ -156,15 +215,50 @@ async function check(directory?: string) {
|
|
|
156
215
|
console.log(ok(`check passed — ${project.config.name} (${project.config.main}, native-fetch)`));
|
|
157
216
|
}
|
|
158
217
|
|
|
159
|
-
async function build(directory?: string) {
|
|
218
|
+
async function build(directory?: string, target: "linux-x86_64" | "host" = "linux-x86_64") {
|
|
160
219
|
const project = await readProject(directory);
|
|
161
|
-
console.log(
|
|
162
|
-
|
|
220
|
+
console.log(
|
|
221
|
+
target === "host"
|
|
222
|
+
? dim(`Compiling the native-fetch server with Porffor for this machine (${hostTarget()}, local only)…`)
|
|
223
|
+
: dim("Compiling the native-fetch server with Porffor + Zig (linux-x86_64, static)…"),
|
|
224
|
+
);
|
|
225
|
+
const artifact = await buildArtifact({
|
|
226
|
+
projectDir: project.directory,
|
|
227
|
+
config: project.config,
|
|
228
|
+
sourcePath: project.sourcePath,
|
|
229
|
+
source: project.bundle.code,
|
|
230
|
+
target,
|
|
231
|
+
});
|
|
163
232
|
console.log(ok(`built ${project.config.name}`));
|
|
233
|
+
if (target === "host")
|
|
234
|
+
console.log(dim(" host build — runs here, not deployable; drop --target host to build for a box"));
|
|
164
235
|
console.log(artifact.artifactDir);
|
|
165
236
|
return { project, artifact };
|
|
166
237
|
}
|
|
167
238
|
|
|
239
|
+
/** #62 — build for this machine, run it against a real broker, rebuild on save. */
|
|
240
|
+
async function dev(args: string[]) {
|
|
241
|
+
const directory = args.find((arg) => !arg.startsWith("--") && !/^\d+$/.test(arg));
|
|
242
|
+
const portIndex = args.indexOf("--port");
|
|
243
|
+
const portArg = portIndex >= 0 ? args[portIndex + 1] : undefined;
|
|
244
|
+
const port = Number(portArg ?? 8787);
|
|
245
|
+
if (!Number.isSafeInteger(port) || port < 1 || port > 65_535)
|
|
246
|
+
usageError(`invalid --port: ${portArg}`, "dev [project-dir] [--port <n>] [--no-watch]");
|
|
247
|
+
const project = await readProject(directory);
|
|
248
|
+
console.log(dim(`Building ${project.config.name} for this machine (${hostTarget()})…`));
|
|
249
|
+
await runDev({
|
|
250
|
+
projectDir: project.directory,
|
|
251
|
+
config: project.config,
|
|
252
|
+
sourcePath: project.sourcePath,
|
|
253
|
+
source: project.bundle.code,
|
|
254
|
+
port,
|
|
255
|
+
watch: !args.includes("--no-watch"),
|
|
256
|
+
// Re-read from disk on every rebuild: the point of watching is that the
|
|
257
|
+
// files changed, so the bundle captured at startup is stale by definition.
|
|
258
|
+
rebuild: async () => (await readProject(directory)).bundle.code,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
168
262
|
const PROVISION_FIELDS = [
|
|
169
263
|
["kv_namespaces", "kv"],
|
|
170
264
|
["d1_databases", "d1"],
|
|
@@ -211,6 +305,46 @@ async function provisionBindings(directory = process.cwd()): Promise<void> {
|
|
|
211
305
|
await writeFile(configPath, source);
|
|
212
306
|
}
|
|
213
307
|
|
|
308
|
+
/** #79 — wrangler parity: drop the stored credential for an endpoint. */
|
|
309
|
+
async function logout(args: string[]) {
|
|
310
|
+
const { apiUrl } = parseLoginArgs(args);
|
|
311
|
+
console.log(
|
|
312
|
+
(await forgetToken(apiUrl)) ? ok(`forgot the credential for ${apiUrl}`) : `no stored credential for ${apiUrl}`,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* #79 — wrangler parity: which endpoint, and who the stored token belongs to.
|
|
318
|
+
* The account comes from the control plane, so this also proves the token still
|
|
319
|
+
* works rather than only reporting what is on disk.
|
|
320
|
+
*/
|
|
321
|
+
async function whoami() {
|
|
322
|
+
const apiUrl = process.env.SPROUTBOAT_API_URL || (await activeApiUrl());
|
|
323
|
+
if (!apiUrl) {
|
|
324
|
+
console.log("not logged in — run `sproutboat login`");
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const token = process.env.SPROUTBOAT_TOKEN || (await savedToken(apiUrl));
|
|
328
|
+
console.log(`endpoint ${apiUrl}`);
|
|
329
|
+
if (!token) {
|
|
330
|
+
console.log(`account ${dim("no stored token — run `sproutboat login`")}`);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const response = await fetch(`${apiUrl}/api/account`, { headers: { "x-api-key": token } });
|
|
335
|
+
if (!response.ok) {
|
|
336
|
+
console.log(
|
|
337
|
+
`account ${rose(response.status === 401 ? "token rejected — run `sproutboat login`" : `control plane said ${response.status}`)}`,
|
|
338
|
+
);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const account = jsonObject(parseJsonValue(await response.text()));
|
|
342
|
+
const profile = jsonObject(account?.profile ?? null);
|
|
343
|
+
const user = jsonObject(account?.user ?? null);
|
|
344
|
+
console.log(`account ${isString(profile?.username) ? profile.username : "(no namespace reserved)"}`);
|
|
345
|
+
if (user && isString(user.email)) console.log(`email ${user.email}`);
|
|
346
|
+
}
|
|
347
|
+
|
|
214
348
|
async function deploy(args: string[]) {
|
|
215
349
|
const artifactIndex = args.indexOf("--artifact");
|
|
216
350
|
const dryRun = args.includes("--dry-run");
|
|
@@ -221,7 +355,10 @@ async function deploy(args: string[]) {
|
|
|
221
355
|
if (artifactIndex >= 0) {
|
|
222
356
|
artifactDir = args[artifactIndex + 1]
|
|
223
357
|
? resolve(args[artifactIndex + 1])
|
|
224
|
-
: usageError(
|
|
358
|
+
: usageError(
|
|
359
|
+
"deploy: --artifact needs a directory",
|
|
360
|
+
"deploy [project-dir] [--dry-run] [--artifact <dir>] [--no-wait]",
|
|
361
|
+
);
|
|
225
362
|
projectName = "";
|
|
226
363
|
} else {
|
|
227
364
|
// wrangler-style: create resources for id-less bindings and pin the ids
|
|
@@ -264,8 +401,12 @@ async function deploy(args: string[]) {
|
|
|
264
401
|
}
|
|
265
402
|
const assetsManifestFile = Bun.file(resolve(artifactDir, "assets.json"));
|
|
266
403
|
if (await assetsManifestFile.exists()) {
|
|
267
|
-
|
|
268
|
-
|
|
404
|
+
// assets.json is written by `sproutboat build` from the AssetManifest contract.
|
|
405
|
+
const assetsManifest: { files?: AssetFiles } = await assetsManifestFile.json();
|
|
406
|
+
form.set(
|
|
407
|
+
"assets_manifest",
|
|
408
|
+
new File([await assetsManifestFile.arrayBuffer()], "assets.json", { type: "application/json" }),
|
|
409
|
+
);
|
|
269
410
|
for (const key of Object.keys(assetsManifest.files ?? {})) {
|
|
270
411
|
const file = Bun.file(resolve(artifactDir, "assets", `.${key}`));
|
|
271
412
|
if (!(await file.exists())) fail(`assets.json lists ${key} but assets${key} is missing — rebuild`);
|
|
@@ -288,18 +429,29 @@ async function deploy(args: string[]) {
|
|
|
288
429
|
}
|
|
289
430
|
console.log(`\n${leaf("🌱")} ${bold(leaf(`Deployed ${projectName}`))}`);
|
|
290
431
|
console.log(` ${bold(deployed.url)}`);
|
|
291
|
-
if (deployed.id)
|
|
432
|
+
if (deployed.id)
|
|
433
|
+
console.log(
|
|
434
|
+
dim(` version ${deployed.id}${deployed.artifact ? ` · artifact ${deployed.artifact.slice(0, 12)}` : ""}`),
|
|
435
|
+
);
|
|
292
436
|
for (const cron of config?.triggers?.crons ?? []) console.log(dim(` schedule ${cron}`));
|
|
293
437
|
const drift = parsePorfforDrift(body);
|
|
294
438
|
if (drift) {
|
|
295
439
|
console.warn(amber(`\n! Porffor pin changed: ${drift.from} -> ${drift.to}`));
|
|
296
440
|
console.warn(dim(` The previous live version stays frozen at ${drift.from}; this one is built with ${drift.to}.`));
|
|
297
|
-
console.warn(
|
|
441
|
+
console.warn(
|
|
442
|
+
dim(
|
|
443
|
+
` The alpha compiler's output can differ between pins (see COMPAT.md) — roll back if this version misbehaves.`,
|
|
444
|
+
),
|
|
445
|
+
);
|
|
298
446
|
}
|
|
299
447
|
// Verify the edge actually answers (cert issuance + sprout boot). Say nothing
|
|
300
448
|
// on success — "Deployed" already implied that; only speak up if it doesn't.
|
|
301
449
|
if (!args.includes("--no-wait") && !(await waitForHealthy(deployed.url, 90_000))) {
|
|
302
|
-
console.warn(
|
|
450
|
+
console.warn(
|
|
451
|
+
amber(
|
|
452
|
+
" ! not serving after 90s — Caddy may still be issuing the cert, or the sprout is crashing (`sproutboat tail`)",
|
|
453
|
+
),
|
|
454
|
+
);
|
|
303
455
|
}
|
|
304
456
|
}
|
|
305
457
|
|
|
@@ -316,7 +468,9 @@ async function waitForHealthy(url: string, timeoutMs: number): Promise<boolean>
|
|
|
316
468
|
try {
|
|
317
469
|
const response = await fetch(url, { method: "HEAD", redirect: "manual" });
|
|
318
470
|
if (response.status < 500) return true;
|
|
319
|
-
} catch {
|
|
471
|
+
} catch {
|
|
472
|
+
/* DNS / TLS-not-yet-issued / connection refused — keep waiting */
|
|
473
|
+
}
|
|
320
474
|
await Bun.sleep(Math.min(wait, Math.max(0, deadline - Date.now())));
|
|
321
475
|
if (wait < 5000) wait += 1000;
|
|
322
476
|
}
|
|
@@ -349,9 +503,17 @@ async function login(args: string[]) {
|
|
|
349
503
|
const authorization = parseAuthorization(body);
|
|
350
504
|
if (!authorization) fail("login response did not include a valid authorization request");
|
|
351
505
|
const verificationUrl = new URL(authorization.verificationUri, `${apiUrl}/`).toString();
|
|
352
|
-
const openCommand =
|
|
353
|
-
|
|
354
|
-
|
|
506
|
+
const openCommand =
|
|
507
|
+
process.platform === "darwin"
|
|
508
|
+
? ["open", verificationUrl]
|
|
509
|
+
: process.platform === "win32"
|
|
510
|
+
? ["cmd", "/c", "start", "", verificationUrl]
|
|
511
|
+
: ["xdg-open", verificationUrl];
|
|
512
|
+
try {
|
|
513
|
+
Bun.spawn(openCommand, { stdout: "ignore", stderr: "ignore" });
|
|
514
|
+
} catch {
|
|
515
|
+
console.log(`Open ${verificationUrl}`);
|
|
516
|
+
}
|
|
355
517
|
console.log("Opening the browser to approve this CLI login.");
|
|
356
518
|
console.log(`Confirm code: ${authorization.userCode}`);
|
|
357
519
|
while (new Date(authorization.expiresAt).getTime() > Date.now()) {
|
|
@@ -374,26 +536,76 @@ async function login(args: string[]) {
|
|
|
374
536
|
}
|
|
375
537
|
|
|
376
538
|
async function apiCredentials() {
|
|
377
|
-
const apiUrl = (process.env.SPROUTBOAT_API_URL || await activeApiUrl() || defaultApiUrl).replace(/\/$/, "");
|
|
378
|
-
const token = process.env.SPROUTBOAT_TOKEN || await savedToken(apiUrl);
|
|
539
|
+
const apiUrl = (process.env.SPROUTBOAT_API_URL || (await activeApiUrl()) || defaultApiUrl).replace(/\/$/, "");
|
|
540
|
+
const token = process.env.SPROUTBOAT_TOKEN || (await savedToken(apiUrl));
|
|
379
541
|
if (!token) fail("not logged in; run sproutboat login or set SPROUTBOAT_TOKEN for this command");
|
|
380
542
|
return { apiUrl, token };
|
|
381
543
|
}
|
|
382
544
|
|
|
383
545
|
async function versions(args: string[]) {
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
546
|
+
const sub = args[0];
|
|
547
|
+
if (sub !== "list" && sub !== "view") {
|
|
548
|
+
usageError(
|
|
549
|
+
sub ? `versions: unknown subcommand "${sub}"` : "versions: missing subcommand",
|
|
550
|
+
"versions <list | view <version-id>> [project-dir]",
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
args.shift();
|
|
554
|
+
|
|
555
|
+
if (sub === "view") {
|
|
556
|
+
const id = args.shift();
|
|
557
|
+
if (!id) usageError("versions view: missing <version-id>", "versions view <version-id> [project-dir]");
|
|
558
|
+
const [project, { apiUrl, token }] = await Promise.all([readProject(args[0]), apiCredentials()]);
|
|
559
|
+
const body = await responseText(
|
|
560
|
+
await fetch(`${apiUrl}/api/projects/${project.config.name}/deployments/${encodeURIComponent(id)}`, {
|
|
561
|
+
headers: { "x-api-key": token },
|
|
562
|
+
}),
|
|
563
|
+
"could not read that version",
|
|
564
|
+
);
|
|
565
|
+
const detail = jsonObject(parseJsonValue(body));
|
|
566
|
+
if (!detail) fail("could not parse version response");
|
|
567
|
+
const manifest = jsonObject(detail.manifest ?? null);
|
|
568
|
+
console.log(`${bold(String(detail.id))} ${detail.active ? ok("active") : dim("superseded")}`);
|
|
569
|
+
console.log(` route ${String(detail.hostname)}`);
|
|
570
|
+
console.log(` artifact ${String(detail.artifact)}`);
|
|
571
|
+
console.log(
|
|
572
|
+
` deployed ${String(detail.deployedAt)}${isString(detail.deployedBy) ? ` by ${detail.deployedBy}` : ""}`,
|
|
573
|
+
);
|
|
574
|
+
if (manifest) {
|
|
575
|
+
console.log(
|
|
576
|
+
` built ${String(manifest.builtAt)} · porffor ${String(manifest.porfforVersion)} · ${String(manifest.binarySize)} bytes`,
|
|
577
|
+
);
|
|
578
|
+
} else if (isString(detail.manifestError)) {
|
|
579
|
+
console.log(` ! manifest unavailable: ${detail.manifestError}`);
|
|
580
|
+
}
|
|
581
|
+
const resources = Array.isArray(detail.resources) ? detail.resources.map((entry) => jsonObject(entry)) : [];
|
|
582
|
+
for (const resource of resources) {
|
|
583
|
+
if (resource)
|
|
584
|
+
console.log(` bound ${String(resource.kind)} ${String(resource.name)} ${dim(String(resource.id))}`);
|
|
585
|
+
}
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const [project, { apiUrl, token }] = await Promise.all([readProject(args[0]), apiCredentials()]);
|
|
590
|
+
const response = await fetch(`${apiUrl}/api/projects/${project.config.name}/deployments`, {
|
|
591
|
+
headers: { "x-api-key": token },
|
|
592
|
+
});
|
|
387
593
|
const deployments = parseVersionList(await responseText(response, "could not list versions"));
|
|
388
594
|
if (!deployments) fail("could not parse versions response");
|
|
389
|
-
for (const deployment of deployments)
|
|
595
|
+
for (const deployment of deployments)
|
|
596
|
+
console.log(
|
|
597
|
+
`${deployment.active ? "*" : " "} ${deployment.id} ${deployment.artifact.slice(0, 12)} ${deployment.deployedAt}`,
|
|
598
|
+
);
|
|
390
599
|
}
|
|
391
600
|
|
|
392
601
|
async function rollback(args: string[]) {
|
|
393
602
|
const id = args[0];
|
|
394
603
|
if (!id) usageError("rollback: missing <version-id>", "rollback <version-id> [project-dir]");
|
|
395
604
|
const [project, { apiUrl, token }] = await Promise.all([readProject(args[1]), apiCredentials()]);
|
|
396
|
-
const response = await fetch(`${apiUrl}/api/projects/${project.config.name}/deployments/${id}/activate`, {
|
|
605
|
+
const response = await fetch(`${apiUrl}/api/projects/${project.config.name}/deployments/${id}/activate`, {
|
|
606
|
+
method: "POST",
|
|
607
|
+
headers: { "x-api-key": token },
|
|
608
|
+
});
|
|
397
609
|
const deployment = parseUrlResponse(await responseText(response, "rollback rejected"));
|
|
398
610
|
if (!deployment) fail("rollback response did not include a URL");
|
|
399
611
|
console.log(ok(`rolled back ${project.config.name}`));
|
|
@@ -405,7 +617,9 @@ async function tail(args: string[]) {
|
|
|
405
617
|
const dir = args.find((arg) => !arg.startsWith("-"));
|
|
406
618
|
const [project, { apiUrl, token }] = await Promise.all([readProject(dir), apiCredentials()]);
|
|
407
619
|
const path = sproutLog ? "logs/sprout" : "logs/recent";
|
|
408
|
-
const response = await fetch(`${apiUrl}/api/projects/${project.config.name}/${path}`, {
|
|
620
|
+
const response = await fetch(`${apiUrl}/api/projects/${project.config.name}/${path}`, {
|
|
621
|
+
headers: { "x-api-key": token },
|
|
622
|
+
});
|
|
409
623
|
process.stdout.write(await responseText(response, "could not read logs"));
|
|
410
624
|
}
|
|
411
625
|
|
|
@@ -418,11 +632,20 @@ type DomainView = {
|
|
|
418
632
|
};
|
|
419
633
|
function parseDomain(source: string): DomainView | undefined {
|
|
420
634
|
const record = jsonObject(parseJsonValue(source));
|
|
421
|
-
if (!record || !isString(record.hostname) ||
|
|
635
|
+
if (!record || !isString(record.hostname) || !isBoolean(record.verified)) return undefined;
|
|
422
636
|
const v = jsonObject(record.verification ?? null);
|
|
423
|
-
const verification =
|
|
637
|
+
const verification =
|
|
638
|
+
v && isString(v.type) && isString(v.name) && isString(v.value)
|
|
639
|
+
? { type: v.type, name: v.name, value: v.value }
|
|
640
|
+
: null;
|
|
424
641
|
const serverAddresses = Array.isArray(record.serverAddresses) ? record.serverAddresses.filter(isString) : [];
|
|
425
|
-
return {
|
|
642
|
+
return {
|
|
643
|
+
hostname: record.hostname,
|
|
644
|
+
verified: record.verified,
|
|
645
|
+
verification,
|
|
646
|
+
serverAddresses,
|
|
647
|
+
warning: isString(record.warning) ? record.warning : undefined,
|
|
648
|
+
};
|
|
426
649
|
}
|
|
427
650
|
function printDomain(domain: DomainView) {
|
|
428
651
|
const status = domain.verified ? "verified" : "unverified";
|
|
@@ -431,16 +654,22 @@ function printDomain(domain: DomainView) {
|
|
|
431
654
|
console.log(" add these DNS records, then run: sproutboat domains verify " + domain.hostname);
|
|
432
655
|
console.log(` ${domain.verification.type} ${domain.verification.name} "${domain.verification.value}"`);
|
|
433
656
|
if (domain.serverAddresses[0]) {
|
|
434
|
-
console.log(
|
|
657
|
+
console.log(
|
|
658
|
+
` A ${domain.hostname} ${domain.serverAddresses[0]} (point the hostname here, DNS-only / not proxied)`,
|
|
659
|
+
);
|
|
435
660
|
}
|
|
436
661
|
}
|
|
437
662
|
if (domain.warning) console.log(amber(` ! ${domain.warning}`));
|
|
438
663
|
}
|
|
439
664
|
|
|
440
665
|
async function domains(args: string[]) {
|
|
441
|
-
const sub =
|
|
666
|
+
const sub =
|
|
667
|
+
args[0] && !args[0].startsWith("-") && ["list", "add", "verify", "delete"].includes(args[0])
|
|
668
|
+
? args.shift()!
|
|
669
|
+
: "list";
|
|
442
670
|
const host = sub === "list" ? undefined : args.shift();
|
|
443
|
-
if (sub !== "list" && !host)
|
|
671
|
+
if (sub !== "list" && !host)
|
|
672
|
+
usageError(`domains ${sub}: missing <hostname>`, `domains ${sub} <hostname> [project-dir]`);
|
|
444
673
|
const [project, { apiUrl, token }] = await Promise.all([readProject(args[0]), apiCredentials()]);
|
|
445
674
|
const base = `${apiUrl}/api/projects/${project.config.name}/domains`;
|
|
446
675
|
const auth = { "x-api-key": token };
|
|
@@ -450,20 +679,31 @@ async function domains(args: string[]) {
|
|
|
450
679
|
const body = await responseText(response, "could not list domains");
|
|
451
680
|
const list = parseJsonValue(body);
|
|
452
681
|
if (!Array.isArray(list)) fail("could not parse domains response");
|
|
453
|
-
if (list.length === 0) {
|
|
454
|
-
|
|
682
|
+
if (list.length === 0) {
|
|
683
|
+
console.log("no custom domains");
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
for (const entry of list) {
|
|
687
|
+
const d = parseDomain(JSON.stringify(entry));
|
|
688
|
+
if (d) printDomain(d);
|
|
689
|
+
}
|
|
455
690
|
return;
|
|
456
691
|
}
|
|
457
|
-
if (sub === "
|
|
692
|
+
if (sub === "delete") {
|
|
458
693
|
const response = await fetch(`${base}/${host}`, { method: "DELETE", headers: auth });
|
|
459
694
|
await responseText(response, "delete rejected");
|
|
460
695
|
console.log(ok(`removed ${host}`));
|
|
461
696
|
return;
|
|
462
697
|
}
|
|
463
698
|
const url = sub === "add" ? base : `${base}/${host}/verify`;
|
|
464
|
-
const init =
|
|
465
|
-
|
|
466
|
-
|
|
699
|
+
const init =
|
|
700
|
+
sub === "add"
|
|
701
|
+
? {
|
|
702
|
+
method: "POST",
|
|
703
|
+
headers: { ...auth, "content-type": "application/json" },
|
|
704
|
+
body: JSON.stringify({ hostname: host }),
|
|
705
|
+
}
|
|
706
|
+
: { method: "POST", headers: auth };
|
|
467
707
|
const response = await fetch(url, init);
|
|
468
708
|
const domain = parseDomain(await responseText(response, `${sub} rejected`));
|
|
469
709
|
if (!domain) fail(`${sub} response was not a domain record`);
|
|
@@ -471,11 +711,23 @@ async function domains(args: string[]) {
|
|
|
471
711
|
}
|
|
472
712
|
|
|
473
713
|
async function secrets(args: string[]) {
|
|
474
|
-
const sub = args[0] && ["list", "
|
|
475
|
-
|
|
714
|
+
const sub = args[0] && ["list", "put", "delete"].includes(args[0]) ? args.shift()! : "list";
|
|
715
|
+
|
|
716
|
+
// `--value` is opt-in; without it the value comes from stdin, so a secret does
|
|
717
|
+
// not land in shell history. It is also what disambiguates the positionals:
|
|
718
|
+
// `secrets put NAME <value> [project-dir]` could not tell a value from a path,
|
|
719
|
+
// and read the value as the project directory.
|
|
720
|
+
let inlineValue: string | undefined;
|
|
721
|
+
const positional: string[] = [];
|
|
722
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
723
|
+
if (args[index] === "--value") inlineValue = args[(index += 1)];
|
|
724
|
+
else positional.push(args[index]);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const name = sub === "list" ? undefined : positional.shift();
|
|
476
728
|
if (sub !== "list" && !name) usageError(`secrets ${sub}: missing <NAME>`, `secrets ${sub} <NAME> [project-dir]`);
|
|
477
729
|
if (name && !/^[A-Z][A-Z0-9_]*$/.test(name)) fail("secret name must be UPPER_SNAKE_CASE");
|
|
478
|
-
const [project, { apiUrl, token }] = await Promise.all([readProject(
|
|
730
|
+
const [project, { apiUrl, token }] = await Promise.all([readProject(positional[0]), apiCredentials()]);
|
|
479
731
|
const base = `${apiUrl}/api/projects/${project.config.name}/secrets`;
|
|
480
732
|
const auth = { "x-api-key": token };
|
|
481
733
|
|
|
@@ -486,78 +738,121 @@ async function secrets(args: string[]) {
|
|
|
486
738
|
console.log(names.length ? names.join("\n") : "no secrets");
|
|
487
739
|
return;
|
|
488
740
|
}
|
|
489
|
-
if (sub === "
|
|
741
|
+
if (sub === "delete") {
|
|
490
742
|
await responseText(await fetch(`${base}/${name}`, { method: "DELETE", headers: auth }), "delete rejected");
|
|
491
743
|
console.log(ok(`removed ${name}`));
|
|
492
744
|
return;
|
|
493
745
|
}
|
|
494
|
-
|
|
495
|
-
const value =
|
|
496
|
-
|
|
497
|
-
: (await Bun.stdin.text()).replace(/\r?\n$/, "");
|
|
498
|
-
if (!value) fail("no value — pass it as an argument or pipe it on stdin");
|
|
746
|
+
|
|
747
|
+
const value = inlineValue ?? (await Bun.stdin.text()).replace(/\r?\n$/, "");
|
|
748
|
+
if (!value) fail("no value — pipe it on stdin, or pass --value <value>");
|
|
499
749
|
await responseText(
|
|
500
|
-
await fetch(`${base}/${name}`, {
|
|
501
|
-
|
|
750
|
+
await fetch(`${base}/${name}`, {
|
|
751
|
+
method: "PUT",
|
|
752
|
+
headers: { ...auth, "content-type": "application/json" },
|
|
753
|
+
body: JSON.stringify({ value }),
|
|
754
|
+
}),
|
|
755
|
+
"put rejected",
|
|
502
756
|
);
|
|
503
|
-
console.log(`
|
|
757
|
+
console.log(ok(`set ${name} — applies on the next deploy or sprout restart`));
|
|
504
758
|
}
|
|
505
759
|
|
|
506
|
-
const RESOURCE_KINDS = ["kv", "d1", "r2", "queue"];
|
|
507
|
-
|
|
508
760
|
/**
|
|
509
|
-
* #
|
|
510
|
-
*
|
|
511
|
-
*
|
|
761
|
+
* #79 — one command per storage product (`kv`, `d1`, `r2`, `queues`), each with
|
|
762
|
+
* the same five verbs, over that product's own `/api/<product>` collection.
|
|
763
|
+
*
|
|
764
|
+
* Wrangler nests two of its four (`kv namespace create`, `r2 bucket create`)
|
|
765
|
+
* and leaves `d1 create` and `queues create` flat. The nesting is there to
|
|
766
|
+
* separate the container from its contents, which the verb already does — so
|
|
767
|
+
* ours are uniform, and contents take their own noun when they exist
|
|
768
|
+
* (`kv key get`, `r2 object put`).
|
|
512
769
|
*/
|
|
513
|
-
|
|
514
|
-
|
|
770
|
+
/** The account's resources of one kind, by name. */
|
|
771
|
+
async function storageRows(base: string, auth: Record<string, string>, product: StorageProduct): Promise<JsonObject[]> {
|
|
772
|
+
const body = await responseText(await fetch(base, { headers: auth }), `could not list ${product.plural}`);
|
|
773
|
+
const parsed = jsonObject(parseJsonValue(body));
|
|
774
|
+
return (parsed && Array.isArray(parsed.resources) ? parsed.resources : [])
|
|
775
|
+
.map((entry) => jsonObject(entry))
|
|
776
|
+
.filter((entry): entry is JsonObject => Boolean(entry));
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/** Resolve a name to its `<kind>_<id>` handle — the API addresses rows by id. */
|
|
780
|
+
function idForName(rows: JsonObject[], name: string, product: StorageProduct): string {
|
|
781
|
+
const match = rows.find((row) => row.name === name);
|
|
782
|
+
if (!match || !isString(match.id)) fail(`no ${product.noun} named "${name}"`);
|
|
783
|
+
return String(match.id);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
async function storage(key: string, args: string[]) {
|
|
787
|
+
const product = STORAGE_PRODUCTS.find((entry) => entry.name === key)!;
|
|
788
|
+
const sub = args[0] && STORAGE_VERBS.some((verb) => verb === args[0]) ? args.shift()! : "list";
|
|
515
789
|
const { apiUrl, token } = await apiCredentials();
|
|
516
|
-
const base = `${apiUrl}/api
|
|
790
|
+
const base = `${apiUrl}/api/${product.name}`;
|
|
517
791
|
const auth = { "x-api-key": token };
|
|
518
792
|
|
|
519
793
|
if (sub === "list") {
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
794
|
+
const rows = await storageRows(base, auth, product);
|
|
795
|
+
if (rows.length === 0) {
|
|
796
|
+
console.log(`no ${product.plural}`);
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
for (const row of rows) {
|
|
800
|
+
const bound = Array.isArray(row.projects) ? row.projects.filter(isString) : [];
|
|
801
|
+
console.log(
|
|
802
|
+
`${String(row.id).padEnd(30)} ${String(row.name).padEnd(24)} ${bound.length ? bound.join(", ") : dim("unbound")}`,
|
|
803
|
+
);
|
|
804
|
+
}
|
|
528
805
|
return;
|
|
529
806
|
}
|
|
530
807
|
|
|
808
|
+
const name = args.shift();
|
|
809
|
+
if (!name) usageError(`${key} ${sub}: missing <name>`, `${key} ${sub} <name>`);
|
|
810
|
+
|
|
531
811
|
if (sub === "create") {
|
|
532
|
-
const [kind, name] = args;
|
|
533
|
-
if (!kind || !RESOURCE_KINDS.includes(kind)) usageError(`resource create: kind must be one of ${RESOURCE_KINDS.join(", ")}`, "resource create <kind> <name>");
|
|
534
|
-
if (!name) usageError("resource create: missing <name>", "resource create <kind> <name>");
|
|
535
812
|
const body = await responseText(
|
|
536
|
-
await fetch(base, {
|
|
813
|
+
await fetch(base, {
|
|
814
|
+
method: "POST",
|
|
815
|
+
headers: { ...auth, "content-type": "application/json" },
|
|
816
|
+
body: JSON.stringify({ name }),
|
|
817
|
+
}),
|
|
537
818
|
"create rejected",
|
|
538
819
|
);
|
|
539
820
|
const record = jsonObject(jsonObject(parseJsonValue(body))?.resource ?? null);
|
|
540
821
|
if (!record || !isString(record.id)) fail("create response was not a resource");
|
|
541
|
-
console.log(ok(`created ${
|
|
822
|
+
console.log(ok(`created ${product.noun} ${bold(String(record.name))}`));
|
|
542
823
|
console.log(record.id);
|
|
543
824
|
return;
|
|
544
825
|
}
|
|
545
826
|
|
|
827
|
+
const rows = await storageRows(base, auth, product);
|
|
828
|
+
const id = idForName(rows, name, product);
|
|
829
|
+
|
|
830
|
+
if (sub === "info") {
|
|
831
|
+
const row = rows.find((entry) => entry.id === id)!;
|
|
832
|
+
const bound = Array.isArray(row.projects) ? row.projects.filter(isString) : [];
|
|
833
|
+
console.log(`${bold(String(row.name))} ${dim(String(row.id))}`);
|
|
834
|
+
console.log(` created ${String(row.createdAt)}`);
|
|
835
|
+
console.log(` bound to ${bound.length ? bound.join(", ") : "nothing"}`);
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
|
|
546
839
|
if (sub === "rename") {
|
|
547
|
-
const
|
|
548
|
-
if (!
|
|
840
|
+
const next = args.shift();
|
|
841
|
+
if (!next) usageError(`${key} rename: missing <new-name>`, `${key} rename <name> <new-name>`);
|
|
549
842
|
await responseText(
|
|
550
|
-
await fetch(`${base}/${id}`, {
|
|
843
|
+
await fetch(`${base}/${id}`, {
|
|
844
|
+
method: "PATCH",
|
|
845
|
+
headers: { ...auth, "content-type": "application/json" },
|
|
846
|
+
body: JSON.stringify({ name: next }),
|
|
847
|
+
}),
|
|
551
848
|
"rename rejected",
|
|
552
849
|
);
|
|
553
|
-
console.log(ok(`renamed ${
|
|
850
|
+
console.log(ok(`renamed ${name} → ${next}`));
|
|
554
851
|
return;
|
|
555
852
|
}
|
|
556
853
|
|
|
557
|
-
const id = args[0];
|
|
558
|
-
if (!id) usageError("resource delete: missing <id>", "resource delete <id>");
|
|
559
854
|
await responseText(await fetch(`${base}/${id}`, { method: "DELETE", headers: auth }), "delete rejected");
|
|
560
|
-
console.log(ok(`deleted ${
|
|
855
|
+
console.log(ok(`deleted ${product.noun} ${name}`));
|
|
561
856
|
}
|
|
562
857
|
|
|
563
858
|
async function deleteProject(args: string[]) {
|
|
@@ -578,10 +873,17 @@ async function deleteProject(args: string[]) {
|
|
|
578
873
|
if (!confirmed) fail(`this permanently removes "${name}", every version, and its route — re-run with --yes`);
|
|
579
874
|
|
|
580
875
|
const url = `${apiUrl}/api/projects/${encodeURIComponent(name)}?confirm=${encodeURIComponent(name)}`;
|
|
581
|
-
const body = await responseText(
|
|
876
|
+
const body = await responseText(
|
|
877
|
+
await fetch(url, { method: "DELETE", headers: { "x-api-key": token } }),
|
|
878
|
+
"delete rejected",
|
|
879
|
+
);
|
|
582
880
|
|
|
583
881
|
let result: JsonObject = {};
|
|
584
|
-
try {
|
|
882
|
+
try {
|
|
883
|
+
result = jsonObject(parseJsonValue(body)) ?? {};
|
|
884
|
+
} catch {
|
|
885
|
+
/* a 2xx already confirmed the delete */
|
|
886
|
+
}
|
|
585
887
|
const versions = isSafeInteger(result.versionsRemoved) ? result.versionsRemoved : 0;
|
|
586
888
|
const routes = Array.isArray(result.routeRemoved) ? result.routeRemoved.filter(isString) : [];
|
|
587
889
|
const failed = Array.isArray(result.artifactCleanupFailed) ? result.artifactCleanupFailed.filter(isString) : [];
|
|
@@ -605,22 +907,67 @@ function usage(): never {
|
|
|
605
907
|
|
|
606
908
|
const [command, ...args] = process.argv.slice(2);
|
|
607
909
|
if (command === undefined || command === "help" || command === "-h" || command === "--help") help();
|
|
608
|
-
if (command === "--version" || command === "-v") {
|
|
910
|
+
if (command === "--version" || command === "-v") {
|
|
911
|
+
console.log(`sproutboat ${CLI_VERSION}`);
|
|
912
|
+
process.exit(0);
|
|
913
|
+
}
|
|
609
914
|
|
|
610
915
|
await notifyIfOutdated(CLI_VERSION);
|
|
611
916
|
|
|
612
917
|
switch (command) {
|
|
613
|
-
case "init":
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
case "
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
case "
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
case "
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
918
|
+
case "init":
|
|
919
|
+
await init(args[0]);
|
|
920
|
+
break;
|
|
921
|
+
case "check":
|
|
922
|
+
await check(args[0]);
|
|
923
|
+
break;
|
|
924
|
+
case "dev":
|
|
925
|
+
await dev(args);
|
|
926
|
+
break;
|
|
927
|
+
case "build": {
|
|
928
|
+
const hostBuild = args.includes("--target") && args[args.indexOf("--target") + 1] === "host";
|
|
929
|
+
await build(
|
|
930
|
+
args.find((arg) => !arg.startsWith("--") && arg !== "host"),
|
|
931
|
+
hostBuild ? "host" : "linux-x86_64",
|
|
932
|
+
);
|
|
933
|
+
break;
|
|
934
|
+
}
|
|
935
|
+
case "login":
|
|
936
|
+
await login(args);
|
|
937
|
+
break;
|
|
938
|
+
case "logout":
|
|
939
|
+
await logout(args);
|
|
940
|
+
break;
|
|
941
|
+
case "whoami":
|
|
942
|
+
await whoami();
|
|
943
|
+
break;
|
|
944
|
+
case "deploy":
|
|
945
|
+
await deploy(args);
|
|
946
|
+
break;
|
|
947
|
+
case "versions":
|
|
948
|
+
await versions(args);
|
|
949
|
+
break;
|
|
950
|
+
case "rollback":
|
|
951
|
+
await rollback(args);
|
|
952
|
+
break;
|
|
953
|
+
case "domains":
|
|
954
|
+
await domains(args);
|
|
955
|
+
break;
|
|
956
|
+
case "secrets":
|
|
957
|
+
await secrets(args);
|
|
958
|
+
break;
|
|
959
|
+
case "kv":
|
|
960
|
+
case "d1":
|
|
961
|
+
case "r2":
|
|
962
|
+
case "queues":
|
|
963
|
+
await storage(command, args);
|
|
964
|
+
break;
|
|
965
|
+
case "tail":
|
|
966
|
+
await tail(args);
|
|
967
|
+
break;
|
|
968
|
+
case "delete":
|
|
969
|
+
await deleteProject(args);
|
|
970
|
+
break;
|
|
971
|
+
default:
|
|
972
|
+
usage();
|
|
626
973
|
}
|