withruntime 0.17.2 → 0.18.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.
@@ -0,0 +1,12 @@
1
+ import type { CliKit } from "./cli.js";
2
+ import type { Runtime } from "./client.js";
3
+ type Out = {
4
+ json: boolean;
5
+ write: (text: string) => void;
6
+ error: (text: string) => void;
7
+ };
8
+ export declare const APP_HELP = "runtime apps <command>\n\n An App serves your web server from an image or a build. Copies start when\n requests come and stop when they end, between --min-copies and --max-copies.\n In a private beta: outside it, each command says how to ask to join.\n\n create [<name>] --vcpu <1-16> --memory <128-65536> --max-copies <1-1000>\n [--min-copies 0] [--concurrency 100] [--spares 2] [--port 8080]\n [--health-path /healthz] [--label key=value]...\n Make an App, waiting for its first deploy; prints its id\n ls [--all] Your Apps: state, live version, copies; --all, deleted too\n get <app> One App, in full (an id or its name)\n update <app> [--name <name>] [--vcpu n] [--memory n] [--min-copies n] [--max-copies n]\n [--concurrency n] [--spares n] [--port n] [--health-path /path] [--label key=value]...\n Change its settings\n rm <app> Delete it and stop its copies\n deploy <app> (--image <image> | --build <buildId>) [--env KEY=VALUE]... [--keep-env]\n [--port 8080] [--health-path /healthz] [--no-wait]\n A new version. Waits until it is live, or says why it\n failed; --no-wait prints the deploy's id at once.\n --image: an image of yours (id, name, name:tag);\n --build: a build that succeeded (runtime builds).\n --env KEY alone sends KEY's value from your environment.\n --keep-env keeps the last deploy's environment, sealed\n deploys <app> [<deployId>] [--wait] Each version's deploy: state and address; with an id,\n that one (--wait: until it ends)\n versions <app> Its versions; the live and previous ones keep saved copies\n copies <app> Its copies now: version, region, state\n rollback <app> [--no-wait] Put the previous version live again\n\nExamples\n runtime apps create web --vcpu 1 --memory 512 --max-copies 3\n runtime apps deploy web --image web:latest --env NODE_ENV=production\n runtime apps rollback web\n";
9
+ export declare const BUILD_HELP = "runtime builds <command>\n\n A build turns a commit of a connected repository into a saved server an App\n deploys. A push to the production branch builds and deploys by itself.\n In a private beta: outside it, each command says how to ask to join.\n\n repos connect <owner/name | https URL> --deploy <app name> [--branch main] [--no-previews]\n [--root <folder>] [--install <command>] [--build <command>] [--start <command>]\n [--port 8080] [--health-path /healthz] [--build-env KEY=VALUE]... [--name <name>]\n Connect a repository; prints its id. --deploy takes an\n App's name, or function:<name>. --start runs once the\n build is done, with PORT set: the server the App deploys\n repos ls Connected repositories\n repos change <repoId> [the options of connect but the repository]\n Change its settings; its id and builds stay\n repos disconnect <repoId> Stop building it; its builds stay\n repos install A link to install Runtime's GitHub App, so a private\n repository builds\n create <repoId> [--ref main] [--branch <name>] [--name <name>]\n Build a branch, tag or commit now; prints the build's id\n run <repoId> [--ref main] [--branch <name>] [--name <name>] [--raw]\n The same, then follows its log; exits 0 once it succeeds\n ls [--repository <repoId>] [--commit <sha>] [--state <state>]...\n Builds, oldest first; --commit: a full sha\n get <buildId> One build: state, commit, step times, what it made\n cancel <buildId> Stop a build that has not ended\n logs <buildId> [--follow] [--raw] Its last 200 lines, or every line as it comes until it\n ends; on a terminal control characters show as \\uXXXX\n unless --raw\n\nExamples\n runtime builds repos connect acme/web --deploy web --start \"npm start\"\n runtime builds run <repoId> --ref main\n runtime builds ls --commit $(git rev-parse HEAD)\n";
10
+ export declare function appsCommand(argv: string[], env: NodeJS.ProcessEnv, out: Out, client: () => Promise<Runtime>, kit: CliKit): Promise<number>;
11
+ export declare function buildsCommand(argv: string[], out: Out, client: () => Promise<Runtime>, kit: CliKit): Promise<number>;
12
+ export {};
@@ -0,0 +1,564 @@
1
+ import { me, named } from "./cli-name.js";
2
+ import { RuntimeError } from "./errors.js";
3
+ import { logStream } from "./terminal.js";
4
+ import { apps, } from "./products/apps.js";
5
+ import { builds, } from "./products/builds.js";
6
+ export const APP_HELP = `runtime apps <command>
7
+
8
+ An App serves your web server from an image or a build. Copies start when
9
+ requests come and stop when they end, between --min-copies and --max-copies.
10
+ In a private beta: outside it, each command says how to ask to join.
11
+
12
+ create [<name>] --vcpu <1-16> --memory <128-65536> --max-copies <1-1000>
13
+ [--min-copies 0] [--concurrency 100] [--spares 2] [--port 8080]
14
+ [--health-path /healthz] [--label key=value]...
15
+ Make an App, waiting for its first deploy; prints its id
16
+ ls [--all] Your Apps: state, live version, copies; --all, deleted too
17
+ get <app> One App, in full (an id or its name)
18
+ update <app> [--name <name>] [--vcpu n] [--memory n] [--min-copies n] [--max-copies n]
19
+ [--concurrency n] [--spares n] [--port n] [--health-path /path] [--label key=value]...
20
+ Change its settings
21
+ rm <app> Delete it and stop its copies
22
+ deploy <app> (--image <image> | --build <buildId>) [--env KEY=VALUE]... [--keep-env]
23
+ [--port 8080] [--health-path /healthz] [--no-wait]
24
+ A new version. Waits until it is live, or says why it
25
+ failed; --no-wait prints the deploy's id at once.
26
+ --image: an image of yours (id, name, name:tag);
27
+ --build: a build that succeeded (runtime builds).
28
+ --env KEY alone sends KEY's value from your environment.
29
+ --keep-env keeps the last deploy's environment, sealed
30
+ deploys <app> [<deployId>] [--wait] Each version's deploy: state and address; with an id,
31
+ that one (--wait: until it ends)
32
+ versions <app> Its versions; the live and previous ones keep saved copies
33
+ copies <app> Its copies now: version, region, state
34
+ rollback <app> [--no-wait] Put the previous version live again
35
+
36
+ Examples
37
+ runtime apps create web --vcpu 1 --memory 512 --max-copies 3
38
+ runtime apps deploy web --image web:latest --env NODE_ENV=production
39
+ runtime apps rollback web
40
+ `;
41
+ export const BUILD_HELP = `runtime builds <command>
42
+
43
+ A build turns a commit of a connected repository into a saved server an App
44
+ deploys. A push to the production branch builds and deploys by itself.
45
+ In a private beta: outside it, each command says how to ask to join.
46
+
47
+ repos connect <owner/name | https URL> --deploy <app name> [--branch main] [--no-previews]
48
+ [--root <folder>] [--install <command>] [--build <command>] [--start <command>]
49
+ [--port 8080] [--health-path /healthz] [--build-env KEY=VALUE]... [--name <name>]
50
+ Connect a repository; prints its id. --deploy takes an
51
+ App's name, or function:<name>. --start runs once the
52
+ build is done, with PORT set: the server the App deploys
53
+ repos ls Connected repositories
54
+ repos change <repoId> [the options of connect but the repository]
55
+ Change its settings; its id and builds stay
56
+ repos disconnect <repoId> Stop building it; its builds stay
57
+ repos install A link to install Runtime's GitHub App, so a private
58
+ repository builds
59
+ create <repoId> [--ref main] [--branch <name>] [--name <name>]
60
+ Build a branch, tag or commit now; prints the build's id
61
+ run <repoId> [--ref main] [--branch <name>] [--name <name>] [--raw]
62
+ The same, then follows its log; exits 0 once it succeeds
63
+ ls [--repository <repoId>] [--commit <sha>] [--state <state>]...
64
+ Builds, oldest first; --commit: a full sha
65
+ get <buildId> One build: state, commit, step times, what it made
66
+ cancel <buildId> Stop a build that has not ended
67
+ logs <buildId> [--follow] [--raw] Its last 200 lines, or every line as it comes until it
68
+ ends; on a terminal control characters show as \\uXXXX
69
+ unless --raw
70
+
71
+ Examples
72
+ runtime builds repos connect acme/web --deploy web --start "npm start"
73
+ runtime builds run <repoId> --ref main
74
+ runtime builds ls --commit $(git rev-parse HEAD)
75
+ `;
76
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
77
+ const when = (ms) => (ms == null ? "-" : new Date(ms).toISOString());
78
+ const ENDED = ["succeeded", "failed", "cancelled", "superseded"];
79
+ /** An App's id from its id or its name: a name is looked up among the Apps
80
+ * not deleted, and must be one App's alone. */
81
+ async function appIdOf(api, target, kit) {
82
+ if (UUID.test(target))
83
+ return target;
84
+ const found = (await api.list({ name: target, state: ["waiting", "live", "failed"] })).data;
85
+ if (found.length === 1)
86
+ return found[0].id;
87
+ throw kit.usage(found.length
88
+ ? `${found.length} Apps are named ${target}; give one's id (${me} apps ls).`
89
+ : `No App is named ${target}. Your Apps: ${me} apps ls`);
90
+ }
91
+ /** The flags an App's settings are given with, as the API names them. */
92
+ function settings(args, kit) {
93
+ const { integer, flag } = kit;
94
+ const number = (name, field) => integer(args, name) === undefined ? {} : { [field]: integer(args, name) };
95
+ return {
96
+ ...number("vcpu", "vcpu"),
97
+ ...number("memory", "memoryMiB"),
98
+ ...number("min-copies", "minCopies"),
99
+ ...number("max-copies", "maxCopies"),
100
+ ...number("concurrency", "concurrency"),
101
+ ...number("spares", "spares"),
102
+ ...number("port", "port"),
103
+ ...(flag(args, "health-path") === undefined ? {} : { healthPath: flag(args, "health-path") }),
104
+ };
105
+ }
106
+ const SETTINGS = [
107
+ "vcpu",
108
+ "memory",
109
+ "min-copies",
110
+ "max-copies",
111
+ "concurrency",
112
+ "spares",
113
+ "port",
114
+ "health-path",
115
+ "label",
116
+ ];
117
+ /** A deploy that ended badly, said with its own words, hint and last output. */
118
+ function deployFailure(deploy) {
119
+ if (!deploy.error)
120
+ return undefined;
121
+ const last = deploy.error.lastOutput?.trim();
122
+ return new RuntimeError({
123
+ code: deploy.error.code,
124
+ status: 0,
125
+ message: `Version ${deploy.version} did not go live: ${deploy.error.message}${last ? `\nIts last output:\n${last}` : ""}`,
126
+ hint: deploy.error.hint,
127
+ details: { deploy },
128
+ });
129
+ }
130
+ export async function appsCommand(argv, env, out, client, kit) {
131
+ const { parse, flag, has, need, pairs, envPairs, table, usage } = kit;
132
+ const [verb, ...rest] = argv;
133
+ const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
134
+ if (!verb || verb === "help" || verb === "--help") {
135
+ print(named(APP_HELP), { usage: named(APP_HELP) });
136
+ return 0;
137
+ }
138
+ const api = apps((await client()).transport);
139
+ const idOf = (target) => appIdOf(api, need(target, "an App's id or name"), kit);
140
+ switch (verb) {
141
+ case "create": {
142
+ const args = parse(rest, [], SETTINGS);
143
+ if (args.positional.length > 1)
144
+ throw usage(`Give one name: ${me} apps create web --vcpu 1 --memory 512 --max-copies 2`);
145
+ const given = settings(args, kit);
146
+ for (const [option, field] of [
147
+ ["vcpu", "vcpu"],
148
+ ["memory", "memoryMiB"],
149
+ ["max-copies", "maxCopies"],
150
+ ])
151
+ if (given[field] === undefined)
152
+ throw usage(`Give --${option}: ${me} apps create ${args.positional[0] ?? "web"} --vcpu 1 --memory 512 --max-copies 2`);
153
+ const input = {
154
+ ...given,
155
+ ...(args.positional[0] ? { name: args.positional[0] } : {}),
156
+ ...(args.flags.has("label") ? { labels: pairs(args, "label") } : {}),
157
+ };
158
+ const app = await api.create(input);
159
+ if (!out.json)
160
+ out.error(`Created ${app.name ?? app.id}, waiting for its first version. Deploy one: ${me} apps deploy ${app.name ?? app.id} --image <image>`);
161
+ print(app.id, app);
162
+ return 0;
163
+ }
164
+ case "ls":
165
+ case "list": {
166
+ const args = parse(rest, ["all"], []);
167
+ const all = await (await api.list({
168
+ limit: 100,
169
+ ...(has(args, "all")
170
+ ? { state: ["waiting", "live", "failed", "deleting", "deleted"] }
171
+ : {}),
172
+ })).toArray(Infinity);
173
+ print(all.length
174
+ ? table([
175
+ ["ID", "NAME", "STATE", "LIVE", "COPIES", "SIZE", "CREATED"],
176
+ ...all.map((a) => [
177
+ a.id,
178
+ a.name ?? "-",
179
+ a.state,
180
+ a.liveVersion === null ? "-" : `v${a.liveVersion}`,
181
+ `${a.minCopies}-${a.maxCopies}`,
182
+ `${a.vcpu} vCPU, ${a.memoryMiB} MiB`,
183
+ when(a.createdAt),
184
+ ]),
185
+ ])
186
+ : `No Apps. Make one: ${me} apps create web --vcpu 1 --memory 512 --max-copies 2`, all);
187
+ return 0;
188
+ }
189
+ case "get": {
190
+ const app = await api.get(await idOf(rest[0]));
191
+ print(JSON.stringify(app, null, 2), app);
192
+ return 0;
193
+ }
194
+ case "update": {
195
+ const args = parse(rest, [], [...SETTINGS, "name"]);
196
+ const id = await idOf(args.positional[0]);
197
+ const change = {
198
+ ...settings(args, kit),
199
+ ...(flag(args, "name") === undefined ? {} : { name: flag(args, "name") }),
200
+ ...(args.flags.has("label") ? { labels: pairs(args, "label") } : {}),
201
+ };
202
+ if (!Object.keys(change).length)
203
+ throw usage(`Give a setting to change, such as ${me} apps update ${args.positional[0]} --max-copies 4`);
204
+ const app = await api.update(id, change);
205
+ print(`${app.name ?? app.id} updated.`, app);
206
+ return 0;
207
+ }
208
+ case "rm":
209
+ case "delete": {
210
+ const app = await api.delete(await idOf(rest[0]));
211
+ print(`${app.name ?? app.id} ${app.state}.`, app);
212
+ return 0;
213
+ }
214
+ case "deploy": {
215
+ const args = parse(rest, ["keep-env", "no-wait"], ["image", "build", "env", "port", "health-path"]);
216
+ const id = await idOf(args.positional[0]);
217
+ const image = flag(args, "image");
218
+ const build = flag(args, "build");
219
+ if ((image === undefined) === (build === undefined))
220
+ throw usage(`Give --image <image> to deploy an image of yours, or --build <buildId> for a build that succeeded.`);
221
+ const port = kit.integer(args, "port");
222
+ const healthPath = flag(args, "health-path");
223
+ const wait = !has(args, "no-wait");
224
+ const input = {
225
+ ...(image !== undefined ? { image } : { build: build }),
226
+ ...(args.flags.has("env") ? { env: envPairs(args, env) } : {}),
227
+ ...(has(args, "keep-env") ? { keepEnv: true } : {}),
228
+ ...(port === undefined ? {} : { port }),
229
+ ...(healthPath === undefined ? {} : { healthPath }),
230
+ wait,
231
+ };
232
+ if (wait && !out.json)
233
+ out.error(`Deploying ${args.positional[0]}...`);
234
+ const deploy = await api.deploy(id, input, { timeoutMs: 15 * 60_000 });
235
+ const failed = deployFailure(deploy);
236
+ if (failed && !out.json)
237
+ throw failed;
238
+ print(deploy.state === "live"
239
+ ? `Version ${deploy.version} is live at ${deploy.url}`
240
+ : `Version ${deploy.version}: ${deploy.state}. Follow it: ${me} apps deploys ${args.positional[0]} ${deploy.id} --wait`, deploy);
241
+ return failed ? 1 : 0;
242
+ }
243
+ case "deploys": {
244
+ const args = parse(rest, ["wait"], []);
245
+ const id = await idOf(args.positional[0]);
246
+ const deployId = args.positional[1];
247
+ if (deployId) {
248
+ const deploy = has(args, "wait")
249
+ ? await api.waitDeploy(id, deployId, { timeoutMs: 15 * 60_000 })
250
+ : await api.getDeploy(id, deployId);
251
+ const failed = deployFailure(deploy);
252
+ if (failed && !out.json)
253
+ throw failed;
254
+ print(JSON.stringify(deploy, null, 2), deploy);
255
+ return failed ? 1 : 0;
256
+ }
257
+ // Newest first; a version's deploy is read once per line, so the list
258
+ // stops at the last 20.
259
+ const versions = (await api.versions(id)).sort((a, b) => b.version - a.version).slice(0, 20);
260
+ const deploys = await Promise.all(versions.map((v) => api.getDeploy(id, v.deploy)));
261
+ print(deploys.length
262
+ ? table([
263
+ ["VERSION", "DEPLOY", "STATE", "FROM", "ADDRESS"],
264
+ ...deploys.map((d, i) => [
265
+ `v${d.version}${versions[i].live ? " (live)" : versions[i].previous ? " (previous)" : ""}`,
266
+ d.id,
267
+ d.state,
268
+ versions[i].build ? `build ${versions[i].build}` : "image",
269
+ d.url,
270
+ ]),
271
+ ])
272
+ : `No deploys yet. Deploy one: ${me} apps deploy ${args.positional[0]} --image <image>`, deploys);
273
+ return 0;
274
+ }
275
+ case "versions": {
276
+ const id = await idOf(rest[0]);
277
+ const versions = (await api.versions(id)).sort((a, b) => b.version - a.version);
278
+ print(versions.length
279
+ ? table([
280
+ ["VERSION", "LIVE", "PREVIOUS", "SAVED", "DEPLOY", "BUILD"],
281
+ ...versions.map((v) => [
282
+ `v${v.version}`,
283
+ v.live ? "yes" : "-",
284
+ v.previous ? "yes" : "-",
285
+ v.saved ? "yes" : "-",
286
+ v.deploy,
287
+ v.build ?? "-",
288
+ ]),
289
+ ])
290
+ : "No versions yet.", versions);
291
+ return 0;
292
+ }
293
+ case "copies": {
294
+ const id = await idOf(rest[0]);
295
+ const copies = await api.copies(id);
296
+ print(copies.length
297
+ ? table([
298
+ ["COPY", "VERSION", "REGION", "STATE", "SINCE"],
299
+ ...copies.map((c) => [c.id, `v${c.version}`, c.region, c.state, when(c.since)]),
300
+ ])
301
+ : "No copies awake. A request starts one.", copies);
302
+ return 0;
303
+ }
304
+ case "rollback": {
305
+ const args = parse(rest, ["no-wait"], []);
306
+ const id = await idOf(args.positional[0]);
307
+ const deploy = await api.rollback(id, {
308
+ wait: !has(args, "no-wait"),
309
+ timeoutMs: 15 * 60_000,
310
+ });
311
+ const failed = deployFailure(deploy);
312
+ if (failed && !out.json)
313
+ throw failed;
314
+ print(deploy.state === "live"
315
+ ? `Version ${deploy.version} is live again at ${deploy.url}`
316
+ : `Rolling back to version ${deploy.version}: ${deploy.state}.`, deploy);
317
+ return failed ? 1 : 0;
318
+ }
319
+ }
320
+ throw kit.unknown("apps command", verb, [
321
+ "create",
322
+ "ls",
323
+ "get",
324
+ "update",
325
+ "rm",
326
+ "deploy",
327
+ "deploys",
328
+ "versions",
329
+ "copies",
330
+ "rollback",
331
+ ]);
332
+ }
333
+ /** A repository as connect takes it: GitHub's owner/name, a github.com
334
+ * address read as that, or any other https address as plain git. */
335
+ function source(repository) {
336
+ const github = /^(?:https:\/\/github\.com\/)?([\w.-]+\/[\w.-]+?)(?:\.git)?\/?$/.exec(repository);
337
+ return github ? { provider: "github", repository: github[1] } : { provider: "git", repository };
338
+ }
339
+ /** connect's and change's settings. */
340
+ function repositorySettings(args, kit) {
341
+ const { flag, has, integer, pairs } = kit;
342
+ const text = (name, field) => flag(args, name) === undefined ? {} : { [field]: flag(args, name) };
343
+ const deploy = flag(args, "deploy");
344
+ return {
345
+ ...(deploy === undefined
346
+ ? {}
347
+ : {
348
+ deploy: (/^(app|function|container):/.test(deploy)
349
+ ? deploy
350
+ : `app:${deploy}`),
351
+ }),
352
+ ...text("branch", "productionBranch"),
353
+ ...(has(args, "no-previews") ? { previews: false } : {}),
354
+ ...text("root", "rootDirectory"),
355
+ ...text("install", "installCommand"),
356
+ ...text("build", "buildCommand"),
357
+ ...text("start", "startCommand"),
358
+ ...(integer(args, "port") === undefined ? {} : { port: integer(args, "port") }),
359
+ ...text("health-path", "healthPath"),
360
+ ...(args.flags.has("build-env") ? { buildEnv: pairs(args, "build-env") } : {}),
361
+ ...text("name", "name"),
362
+ };
363
+ }
364
+ const REPOSITORY_VALUES = [
365
+ "deploy",
366
+ "branch",
367
+ "root",
368
+ "install",
369
+ "build",
370
+ "start",
371
+ "port",
372
+ "health-path",
373
+ "build-env",
374
+ "name",
375
+ ];
376
+ export async function buildsCommand(argv, out, client, kit) {
377
+ const { parse, flag, has, need, table, usage } = kit;
378
+ const [verb, ...rest] = argv;
379
+ const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
380
+ if (!verb || verb === "help" || verb === "--help") {
381
+ print(named(BUILD_HELP), { usage: named(BUILD_HELP) });
382
+ return 0;
383
+ }
384
+ const api = builds((await client()).transport);
385
+ const buildRow = (b) => [
386
+ b.id,
387
+ b.state,
388
+ b.ref,
389
+ b.commit ? b.commit.slice(0, 12) : "-",
390
+ when(b.createdAt),
391
+ b.reason ?? "-",
392
+ ];
393
+ /** A build's log, its last lines or followed to its end. */
394
+ const logs = async (id, follow, raw) => {
395
+ const [stdout, stderr] = [logStream(process.stdout, raw), logStream(process.stderr, raw)];
396
+ const write = (line) => (line.stream === "stderr" ? stderr : stdout)(line.text.endsWith("\n") ? line.text : `${line.text}\n`);
397
+ if (!follow) {
398
+ const log = await api.logs(id);
399
+ if (out.json) {
400
+ out.write(JSON.stringify(log));
401
+ return 0;
402
+ }
403
+ log.lines.forEach(write);
404
+ if (log.failingStep)
405
+ out.error(`[${log.state} at ${log.failingStep.name}${log.failingStep.exitCode === null ? "" : `, exit code ${log.failingStep.exitCode}`}]`);
406
+ else
407
+ out.error(ENDED.includes(log.state)
408
+ ? `[${log.state}${log.reason ? `, ${log.reason}` : ""}]`
409
+ : `[${log.state}; follow it: ${me} builds logs ${id} --follow]`);
410
+ return 0;
411
+ }
412
+ // A follow lasts until the build ends: no deadline of the SDK's own.
413
+ for await (const event of api.logs(id, { follow: true }, { timeoutMs: 0 })) {
414
+ if (out.json)
415
+ out.write(JSON.stringify(event));
416
+ else if (event.type === "line")
417
+ write(event);
418
+ else if (event.type === "gap")
419
+ out.error("[older lines were dropped before they were read; going on from the oldest kept]");
420
+ if (event.type === "terminal") {
421
+ if (!out.json) {
422
+ out.error(`[${event.state}, ${event.reason}]`);
423
+ if (event.failingStep)
424
+ out.error(`${event.failingStep.name} failed${event.failingStep.exitCode === null ? "" : ` with exit code ${event.failingStep.exitCode}`}. Its last lines:\n${event.failingStep.lastLines.join("\n")}`);
425
+ }
426
+ return event.state === "succeeded" ? 0 : 1;
427
+ }
428
+ }
429
+ return 1;
430
+ };
431
+ switch (verb) {
432
+ case "repos":
433
+ case "repositories": {
434
+ const [action, ...more] = rest;
435
+ switch (action) {
436
+ case "connect": {
437
+ const args = parse(more, ["no-previews"], REPOSITORY_VALUES);
438
+ const repository = need(args.positional[0], "the repository, such as acme/web");
439
+ const given = repositorySettings(args, kit);
440
+ if (!given.deploy)
441
+ throw usage(`Give --deploy with the App it deploys: ${me} builds repos connect ${repository} --deploy web`);
442
+ const repo = await api.repositories.connect({
443
+ ...source(repository),
444
+ ...given,
445
+ });
446
+ if (!out.json)
447
+ out.error(`Connected ${repo.repository}: a push to ${repo.productionBranch} builds and deploys ${repo.deploy}. Build now: ${me} builds run ${repo.id}`);
448
+ print(repo.id, repo);
449
+ return 0;
450
+ }
451
+ case "ls":
452
+ case "list": {
453
+ const all = await (await api.repositories.list({ limit: 100 })).toArray(Infinity);
454
+ print(all.length
455
+ ? table([
456
+ ["ID", "REPOSITORY", "BRANCH", "DEPLOYS", "STATE"],
457
+ ...all.map((r) => [
458
+ r.id,
459
+ r.repository,
460
+ r.productionBranch,
461
+ r.deploy,
462
+ r.state,
463
+ ]),
464
+ ])
465
+ : `No repositories. Connect one: ${me} builds repos connect acme/web --deploy web`, all);
466
+ return 0;
467
+ }
468
+ case "change":
469
+ case "update": {
470
+ const args = parse(more, ["no-previews"], REPOSITORY_VALUES);
471
+ const id = need(args.positional[0], "a repository's id");
472
+ const change = repositorySettings(args, kit);
473
+ if (!Object.keys(change).length)
474
+ throw usage(`Give a setting to change, such as --branch main or --start "npm start".`);
475
+ const repo = await api.repositories.update(id, change);
476
+ print(`${repo.repository} changed.`, repo);
477
+ return 0;
478
+ }
479
+ case "disconnect":
480
+ case "rm": {
481
+ const repo = await api.repositories.disconnect(need(more[0], "a repository's id"));
482
+ print(`${repo.repository} disconnected; its builds stay.`, repo);
483
+ return 0;
484
+ }
485
+ case "install": {
486
+ const link = await api.repositories.installGithub();
487
+ print(`Open this within 15 minutes, install the App on the repositories Runtime may read, and confirm the organization:\n${link.url}`, link);
488
+ return 0;
489
+ }
490
+ }
491
+ throw kit.unknown("repos command", action ?? "", [
492
+ "connect",
493
+ "ls",
494
+ "change",
495
+ "disconnect",
496
+ "install",
497
+ ]);
498
+ }
499
+ case "create":
500
+ case "run": {
501
+ const args = parse(rest, verb === "run" ? ["raw"] : [], ["ref", "branch", "name"]);
502
+ const repositoryId = need(args.positional[0], "a repository's id (runtime builds repos ls)");
503
+ const build = await api.create({
504
+ repositoryId,
505
+ ref: flag(args, "ref") ?? flag(args, "branch") ?? "main",
506
+ ...(flag(args, "branch") ? { branch: flag(args, "branch") } : {}),
507
+ ...(flag(args, "name") ? { name: flag(args, "name") } : {}),
508
+ });
509
+ if (verb === "create") {
510
+ if (!out.json)
511
+ out.error(`Building ${build.ref}${build.commit ? ` at ${build.commit.slice(0, 12)}` : ""}. Follow it: ${me} builds logs ${build.id} --follow`);
512
+ print(build.id, build);
513
+ return 0;
514
+ }
515
+ if (!out.json)
516
+ out.error(`Build ${build.id}, ${build.ref}${build.commit ? ` at ${build.commit.slice(0, 12)}` : ""}:`);
517
+ else
518
+ out.write(JSON.stringify(build));
519
+ return logs(build.id, true, has(args, "raw"));
520
+ }
521
+ case "ls":
522
+ case "list": {
523
+ const args = parse(rest, [], ["repository", "commit", "state"]);
524
+ const commit = flag(args, "commit");
525
+ if (commit !== undefined && !/^([0-9a-f]{40}|[0-9a-f]{64})$/.test(commit))
526
+ throw usage(`--commit takes a full sha in lower case: ${me} builds ls --commit $(git rev-parse HEAD)`);
527
+ const states = args.flags.get("state");
528
+ const all = await (await api.list({
529
+ limit: 100,
530
+ ...(flag(args, "repository") ? { repositoryId: flag(args, "repository") } : {}),
531
+ ...(commit ? { commit } : {}),
532
+ ...(states ? { state: states } : {}),
533
+ })).toArray(Infinity);
534
+ print(all.length
535
+ ? table([["ID", "STATE", "REF", "COMMIT", "CREATED", "REASON"], ...all.map(buildRow)])
536
+ : "No builds.", all);
537
+ return 0;
538
+ }
539
+ case "get": {
540
+ const build = await api.get(need(rest[0], "a build's id"));
541
+ print(JSON.stringify(build, null, 2), build);
542
+ return 0;
543
+ }
544
+ case "cancel": {
545
+ const build = await api.cancel(need(rest[0], "a build's id"));
546
+ print(`${build.id} ${build.state}.`, build);
547
+ return 0;
548
+ }
549
+ case "logs": {
550
+ const args = parse(rest, ["f", "follow", "raw"], []);
551
+ const id = need(args.positional[0], "a build's id");
552
+ return logs(id, has(args, "f") || has(args, "follow"), has(args, "raw"));
553
+ }
554
+ }
555
+ throw kit.unknown("builds command", verb, [
556
+ "repos",
557
+ "create",
558
+ "run",
559
+ "ls",
560
+ "get",
561
+ "cancel",
562
+ "logs",
563
+ ]);
564
+ }
package/dist/cli-words.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { me } from "./cli-name.js";
1
2
  import { RuntimeError } from "./errors.js";
2
3
  import { cliWords } from "./product-renames.js";
3
4
  /* What the CLI says in its own words where the API's would mislead: the
@@ -48,6 +49,15 @@ function issueWords(path, message) {
48
49
  * a hint that names the options rather than the request's fields. Anything
49
50
  * else is left as the API said it. */
50
51
  export function cliRefusal(error) {
52
+ // A beta's refusal names the support tool an agent calls; here, the command.
53
+ if (error.code === "product_in_beta") {
54
+ const product = typeof error.details?.product === "string" ? error.details.product : "";
55
+ const open = (error.hint ?? "").replace(/;?\s*ask Runtime support \(runtime_support\) to join it\.?$/, ".");
56
+ return {
57
+ message: error.message,
58
+ hint: `${open ? `${open} ` : ""}Ask to join: ${me} support "Please add us to the ${product || "product"} beta"`,
59
+ };
60
+ }
51
61
  const issues = error.details?.issues;
52
62
  if (error.code === "invalid_request" && Array.isArray(issues) && issues.length) {
53
63
  const lines = issues
package/dist/cli.d.ts CHANGED
@@ -24,9 +24,13 @@ declare const kit: {
24
24
  pairs: (args: Args, name: string) => {
25
25
  [k: string]: string;
26
26
  };
27
+ envPairs: (args: Args, from: NodeJS.ProcessEnv) => {
28
+ [k: string]: string;
29
+ };
27
30
  need: (value: string | undefined, what: string) => string;
28
31
  table: (rows: string[][]) => string;
29
32
  usage: (message: string) => RuntimeError;
33
+ unknown: (what: string, word: string, choices: readonly string[]) => RuntimeError;
30
34
  };
31
35
  export type CliKit = typeof kit;
32
36
  export declare function run(argv: string[], env?: NodeJS.ProcessEnv, out?: Out): Promise<number>;
package/dist/cli.js CHANGED
@@ -109,6 +109,8 @@ Products
109
109
  images Custom images from a Dockerfile, any public or private image, or a package list
110
110
  volumes Persistent disks to attach to sandboxes
111
111
  jobs A command in a fresh sandbox, once or on a cron schedule, with its logs
112
+ apps Your web server from an image or a build, copies started by requests (beta)
113
+ builds A connected repository's commits built into what an App deploys (beta)
112
114
  domains Your own hostname for a sandbox's port, with HTTPS
113
115
  ports A public TCP port to a sandbox: databases, game servers
114
116
  ips A dedicated outbound address, for allow-lists
@@ -357,9 +359,11 @@ const kit = {
357
359
  has,
358
360
  integer,
359
361
  pairs,
362
+ envPairs,
360
363
  need: (value, what) => need(value, what),
361
364
  table: (rows) => table(rows),
362
365
  usage: (message) => usage(message),
366
+ unknown: (what, word, choices) => unknown(what, word, choices),
363
367
  };
364
368
  /** The product being run, so a mistake points at its own help. */
365
369
  let product = "";
@@ -421,6 +425,8 @@ const COMMANDS = [
421
425
  "images",
422
426
  "volumes",
423
427
  "jobs",
428
+ "apps",
429
+ "builds",
424
430
  "domains",
425
431
  "ports",
426
432
  "ips",
@@ -439,6 +445,8 @@ const OWN_HELP = [
439
445
  "images",
440
446
  "volumes",
441
447
  "jobs",
448
+ "apps",
449
+ "builds",
442
450
  "keys",
443
451
  "webhooks",
444
452
  "webhook",
@@ -1095,6 +1103,14 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
1095
1103
  product = "job";
1096
1104
  return jobCommand(rest, env, out);
1097
1105
  }
1106
+ if (command === "app" || command === "apps") {
1107
+ product = "apps";
1108
+ return (await import("./apps-cli.js")).appsCommand(rest, env, out, () => client(env), kit);
1109
+ }
1110
+ if (command === "build" || command === "builds") {
1111
+ product = "builds";
1112
+ return (await import("./apps-cli.js")).buildsCommand(rest, out, () => client(env), kit);
1113
+ }
1098
1114
  // WireGuard is inside networks: `runtime networks wireguard peer add`.
1099
1115
  const wireguard = command === "networks" && rest[0] === "wireguard";
1100
1116
  const networkProduct = wireguard ? "wireguard" : command;
@@ -126,7 +126,8 @@ export declare class Sandbox {
126
126
  /** Pauses the sandbox with its memory. */
127
127
  pause(_timeout?: number): Promise<void>;
128
128
  /** Runtime has no archive tier: a stopped sandbox stays paused, kept for
129
- * its retention (30 days paid, 7 without credit, or autoDeleteInterval). */
129
+ * its retention (while the account has credit, 7 days without, or
130
+ * autoDeleteInterval rounded up to whole days). */
130
131
  archive(): Promise<void>;
131
132
  /** Ends the sandbox for good. */
132
133
  delete(_timeout?: number): Promise<void>;
@@ -270,7 +270,8 @@ export class Sandbox {
270
270
  this.#pausedByPause = true;
271
271
  }
272
272
  /** Runtime has no archive tier: a stopped sandbox stays paused, kept for
273
- * its retention (30 days paid, 7 without credit, or autoDeleteInterval). */
273
+ * its retention (while the account has credit, 7 days without, or
274
+ * autoDeleteInterval rounded up to whole days). */
274
275
  async archive() {
275
276
  await this.stop();
276
277
  }
package/dist/identity.js CHANGED
@@ -5,8 +5,24 @@ async function requestCredentials() {
5
5
  const token = process.env.RUNTIME_ID_TOKEN_REQUEST_TOKEN;
6
6
  if (url && token)
7
7
  return { url, token };
8
- // A process started before the guest wrote its environment still finds it.
9
- return fileCredentials();
8
+ // A process started without them (a server started before the guest wrote
9
+ // its environment, a sandbox whose guest software predates the variables)
10
+ // asks the host, which answers every running sandbox whatever its image or
11
+ // age, then the guest's file. Only inside a sandbox: elsewhere nothing is
12
+ // asked over the network.
13
+ if (!(await exists(sandboxMarker.path)))
14
+ return null;
15
+ return currentCredentials();
16
+ }
17
+ async function exists(path) {
18
+ try {
19
+ const { access } = await import("node:fs/promises");
20
+ await access(path);
21
+ return true;
22
+ }
23
+ catch {
24
+ return false;
25
+ }
10
26
  }
11
27
  async function fileCredentials() {
12
28
  try {
@@ -86,8 +86,12 @@ export type ListBuilds = {
86
86
  name?: string;
87
87
  label?: string[];
88
88
  state?: BuildState[];
89
+ /** Only this connected repository's builds. */
90
+ repositoryId?: string;
91
+ /** Only builds of this commit, by its full sha. */
92
+ commit?: string;
89
93
  };
90
- export type ListRepositories = Omit<ListBuilds, "state"> & {
94
+ export type ListRepositories = Omit<ListBuilds, "state" | "repositoryId" | "commit"> & {
91
95
  state?: Repository["state"][];
92
96
  };
93
97
  export type BuildLine = {
@@ -151,6 +155,12 @@ export declare function builds(t: Transport): {
151
155
  list: (query?: ListRepositories, options?: RequestOptions) => Promise<Page<Repository>>;
152
156
  update: (id: string, change: ChangeRepository, options?: RequestOptions) => Promise<Repository>;
153
157
  disconnect: (id: string, options?: RequestOptions) => Promise<Repository>;
158
+ /** A link to install Runtime's GitHub App, open for 15 minutes: a
159
+ * private repository builds once its owner installs it. */
160
+ installGithub: (options?: RequestOptions) => Promise<{
161
+ url: string;
162
+ expiresAt: string;
163
+ }>;
154
164
  };
155
165
  };
156
166
  export {};
@@ -222,6 +222,13 @@ export function builds(t) {
222
222
  path: `/v1/repositories/${encodeURIComponent(id)}`,
223
223
  ...options,
224
224
  }),
225
+ /** A link to install Runtime's GitHub App, open for 15 minutes: a
226
+ * private repository builds once its owner installs it. */
227
+ installGithub: (options) => t.json({
228
+ method: "POST",
229
+ path: "/v1/repositories/github:install",
230
+ ...options,
231
+ }),
225
232
  },
226
233
  };
227
234
  }
@@ -1,5 +1,5 @@
1
1
  import { RuntimeError } from "./errors.js";
2
- export declare const VERSION = "0.17.2";
2
+ export declare const VERSION = "0.18.0";
3
3
  export declare const DEFAULT_BASE_URL = "https://api.withruntime.com";
4
4
  /** Runtime's API as code inside a Runtime sandbox reaches it: the sandbox's
5
5
  * own host sends each request on to DEFAULT_BASE_URL over HTTPS. The API runs
package/dist/transport.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { ConnectionError, DELIBERATE, errorFor, NOTHING_RAN, RuntimeError, WAITS_FOR_ROOM, } from "./errors.js";
2
2
  import { describeRoute, envFetch, openWebSocket } from "./proxy.js";
3
3
  import { warningText } from "./product-renames.js";
4
- export const VERSION = "0.17.2";
4
+ export const VERSION = "0.18.0";
5
5
  export const DEFAULT_BASE_URL = "https://api.withruntime.com";
6
6
  /** Runtime's API as code inside a Runtime sandbox reaches it: the sandbox's
7
7
  * own host sends each request on to DEFAULT_BASE_URL over HTTPS. The API runs
@@ -45,7 +45,7 @@ export declare function translate(error: unknown, sandboxName?: string): unknown
45
45
  export declare function guard<T>(work: () => Promise<T>, sandboxName?: string): Promise<T>;
46
46
  /** A node:fs-style error, as Vercel's `sandbox.fs` throws. */
47
47
  export declare function fsError(code: "ENOENT" | "EEXIST" | "EISDIR" | "ENOTDIR" | "EACCES" | "ENOTEMPTY" | "EINVAL", syscall: string, path: string): Error & {
48
- code: "ENOENT" | "ENOTDIR" | "EEXIST" | "EISDIR" | "EACCES" | "ENOTEMPTY" | "EINVAL";
48
+ code: "ENOENT" | "EEXIST" | "ENOTDIR" | "EISDIR" | "EACCES" | "ENOTEMPTY" | "EINVAL";
49
49
  errno: number;
50
50
  syscall: string;
51
51
  path: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "withruntime",
3
- "version": "0.17.2",
3
+ "version": "0.18.0",
4
4
  "license": "Apache-2.0",
5
5
  "mcpName": "com.withruntime/runtime",
6
6
  "type": "module",