withruntime 0.17.2 → 0.21.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.
Files changed (52) hide show
  1. package/COMPATIBILITY.md +32 -1
  2. package/README.md +19 -0
  3. package/README.src.md +19 -0
  4. package/dist/agents/index.d.ts +11 -0
  5. package/dist/agents/index.js +12 -0
  6. package/dist/agents-cli.d.ts +10 -0
  7. package/dist/agents-cli.js +362 -0
  8. package/dist/apps-cli.d.ts +14 -0
  9. package/dist/apps-cli.js +737 -0
  10. package/dist/builds/index.d.ts +10 -0
  11. package/dist/builds/index.js +11 -0
  12. package/dist/cli-words.js +11 -0
  13. package/dist/cli.d.ts +4 -0
  14. package/dist/cli.js +104 -6
  15. package/dist/client.d.ts +19 -0
  16. package/dist/client.js +20 -0
  17. package/dist/cloudflare/errors.d.ts +23 -0
  18. package/dist/cloudflare/errors.js +16 -0
  19. package/dist/cloudflare/index.d.ts +82 -9
  20. package/dist/cloudflare/index.js +162 -38
  21. package/dist/cloudflare/interpreter.js +14 -5
  22. package/dist/cloudflare/mime.d.ts +1 -0
  23. package/dist/cloudflare/mime.js +66 -0
  24. package/dist/cloudflare/streams.d.ts +8 -0
  25. package/dist/cloudflare/streams.js +30 -0
  26. package/dist/cloudflare/types.d.ts +338 -0
  27. package/dist/cloudflare/types.js +80 -0
  28. package/dist/cloudflare/worker.d.ts +297 -10
  29. package/dist/cloudflare/worker.js +203 -17
  30. package/dist/containers/index.d.ts +10 -0
  31. package/dist/containers/index.js +11 -0
  32. package/dist/daytona/sandbox.d.ts +2 -1
  33. package/dist/daytona/sandbox.js +2 -1
  34. package/dist/identity.js +18 -2
  35. package/dist/index.d.ts +3 -0
  36. package/dist/index.js +1 -0
  37. package/dist/product-names.js +3 -0
  38. package/dist/products/agents.d.ts +272 -0
  39. package/dist/products/agents.js +251 -0
  40. package/dist/products/builds.d.ts +11 -1
  41. package/dist/products/builds.js +7 -0
  42. package/dist/products/containers.d.ts +71 -0
  43. package/dist/products/containers.js +6 -0
  44. package/dist/products/index.d.ts +6 -0
  45. package/dist/products/index.js +7 -0
  46. package/dist/sandbox.d.ts +4 -0
  47. package/dist/transport.d.ts +2 -2
  48. package/dist/transport.js +1 -1
  49. package/dist/tunnel.js +1 -1
  50. package/dist/types.d.ts +11 -0
  51. package/dist/vercel/errors.d.ts +1 -1
  52. package/package.json +16 -1
@@ -0,0 +1,737 @@
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
+ import { containers, } from "./products/containers.js";
7
+ export const APP_HELP = `runtime apps <command>
8
+
9
+ An App serves your web server from an image or a build. Copies start when
10
+ requests come and stop when they end, between --min-copies and --max-copies.
11
+ In a private beta: outside it, each command says how to ask to join.
12
+
13
+ create [<name>] --vcpu <1-16> --memory <128-65536> --max-copies <1-1000>
14
+ [--min-copies 0] [--concurrency 100] [--spares 2] [--port 8080]
15
+ [--health-path /healthz] [--label key=value]...
16
+ Make an App, waiting for its first deploy; prints its id
17
+ ls [--all] Your Apps: state, live version, copies; --all, deleted too
18
+ get <app> One App, in full (an id or its name)
19
+ update <app> [--name <name>] [--vcpu n] [--memory n] [--min-copies n] [--max-copies n]
20
+ [--concurrency n] [--spares n] [--port n] [--health-path /path] [--label key=value]...
21
+ Change its settings
22
+ rm <app> Delete it and stop its copies
23
+ deploy <app> (--image <image> | --build <buildId>) [--env KEY=VALUE]... [--keep-env]
24
+ [--port 8080] [--health-path /healthz] [--no-wait]
25
+ A new version. Waits until it is live, or says why it
26
+ failed; --no-wait prints the deploy's id at once.
27
+ --image: an image of yours (id, name, name:tag);
28
+ --build: a build that succeeded (runtime builds).
29
+ --env KEY alone sends KEY's value from your environment.
30
+ --keep-env keeps the last deploy's environment, sealed
31
+ deploys <app> [<deployId>] [--wait] Each version's deploy: state and address; with an id,
32
+ that one (--wait: until it ends)
33
+ versions <app> Its versions; the live and previous ones keep saved copies
34
+ copies <app> Its copies now: version, region, state
35
+ rollback <app> [--no-wait] Put the previous version live again
36
+
37
+ Examples
38
+ runtime apps create web --vcpu 1 --memory 512 --max-copies 3
39
+ runtime apps deploy web --image web:latest --env NODE_ENV=production
40
+ runtime apps rollback web
41
+ `;
42
+ export const BUILD_HELP = `runtime builds <command>
43
+
44
+ A build turns a commit of a connected repository into a saved server an App
45
+ deploys. A push to the production branch builds and deploys by itself.
46
+ In a private beta: outside it, each command says how to ask to join.
47
+
48
+ repos connect <owner/name | https URL> --deploy <app name> [--branch main] [--no-previews]
49
+ [--root <folder>] [--install <command>] [--build <command>] [--start <command>]
50
+ [--port 8080] [--health-path /healthz] [--build-env KEY=VALUE]... [--name <name>]
51
+ Connect a repository; prints its id. --deploy takes an
52
+ App's name, or function:<name>. --start runs once the
53
+ build is done, with PORT set: the server the App deploys
54
+ repos ls Connected repositories
55
+ repos change <repoId> [the options of connect but the repository]
56
+ Change its settings; its id and builds stay
57
+ repos disconnect <repoId> Stop building it; its builds stay
58
+ repos install A link to install Runtime's GitHub App, so a private
59
+ repository builds
60
+ create <repoId> [--ref main] [--branch <name>] [--name <name>]
61
+ Build a branch, tag or commit now; prints the build's id
62
+ run <repoId> [--ref main] [--branch <name>] [--name <name>] [--raw]
63
+ The same, then follows its log; exits 0 once it succeeds
64
+ ls [--repository <repoId>] [--commit <sha>] [--state <state>]...
65
+ Builds, oldest first; --commit: a full sha
66
+ get <buildId> One build: state, commit, step times, what it made
67
+ cancel <buildId> Stop a build that has not ended
68
+ logs <buildId> [--follow] [--raw] Its last 200 lines, or every line as it comes until it
69
+ ends; on a terminal control characters show as \\uXXXX
70
+ unless --raw
71
+
72
+ Examples
73
+ runtime builds repos connect acme/web --deploy web --start "npm start"
74
+ runtime builds run <repoId> --ref main
75
+ runtime builds ls --commit $(git rev-parse HEAD)
76
+ `;
77
+ export const CONTAINER_HELP = `runtime containers <command>
78
+
79
+ A container runs one OCI image's process on its own microVM, billed at the
80
+ sandbox rates. It never sleeps unless --sleep; on exit it restarts when it
81
+ serves a port, else it stops (--on-exit sets either).
82
+ In a private beta: outside it, each command says how to ask to join.
83
+
84
+ create <image> --vcpu <1-16> --memory <128-65536> [--name <name>] [--label key=value]...
85
+ [--env KEY[=VALUE]]... [--entrypoint <program>] [--workdir /app] [--user <user>]
86
+ [--port <1-65535>] [--on-exit restart|stop] [--sleep] [-- <command...>]
87
+ Run it; prints its id. The image's digest is pinned.
88
+ --env KEY alone sends KEY's value from your environment.
89
+ --entrypoint drops the image's command; after --, the
90
+ command replaces the image's
91
+ ls [--all] [--state <state>]... [--label key=value]...
92
+ Your containers: all but deleting and deleted, unless
93
+ --all or --state
94
+ get <container> One container, in full (an id or its name): its
95
+ environment's names, never their values
96
+ update <container> [--name <name>] [--label key=value]...
97
+ Rename it or replace its labels
98
+ rm <container> [--wait] Delete it and stop its process; --wait: until deleted
99
+
100
+ Examples
101
+ runtime containers create nginx:1.27 --vcpu 1 --memory 512 --name web
102
+ runtime containers create python:3.12 --vcpu 1 --memory 1024 -- python3 -m http.server 8000
103
+ runtime containers rm web --wait
104
+ `;
105
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
106
+ const when = (ms) => (ms == null ? "-" : new Date(ms).toISOString());
107
+ const ENDED = ["succeeded", "failed", "cancelled", "superseded"];
108
+ /** An App's id from its id or its name: a name is looked up among the Apps
109
+ * not deleted, and must be one App's alone. */
110
+ async function appIdOf(api, target, kit) {
111
+ if (UUID.test(target))
112
+ return target;
113
+ const found = (await api.list({ name: target, state: ["waiting", "live", "failed"] })).data;
114
+ if (found.length === 1)
115
+ return found[0].id;
116
+ throw kit.usage(found.length
117
+ ? `${found.length} Apps are named ${target}; give one's id (${me} apps ls).`
118
+ : `No App is named ${target}. Your Apps: ${me} apps ls`);
119
+ }
120
+ /** The flags an App's settings are given with, as the API names them. */
121
+ function settings(args, kit) {
122
+ const { integer, flag } = kit;
123
+ const number = (name, field) => integer(args, name) === undefined ? {} : { [field]: integer(args, name) };
124
+ return {
125
+ ...number("vcpu", "vcpu"),
126
+ ...number("memory", "memoryMiB"),
127
+ ...number("min-copies", "minCopies"),
128
+ ...number("max-copies", "maxCopies"),
129
+ ...number("concurrency", "concurrency"),
130
+ ...number("spares", "spares"),
131
+ ...number("port", "port"),
132
+ ...(flag(args, "health-path") === undefined ? {} : { healthPath: flag(args, "health-path") }),
133
+ };
134
+ }
135
+ const SETTINGS = [
136
+ "vcpu",
137
+ "memory",
138
+ "min-copies",
139
+ "max-copies",
140
+ "concurrency",
141
+ "spares",
142
+ "port",
143
+ "health-path",
144
+ "label",
145
+ ];
146
+ /** A deploy that ended badly, said with its own words, hint and last output. */
147
+ function deployFailure(deploy) {
148
+ if (!deploy.error)
149
+ return undefined;
150
+ const last = deploy.error.lastOutput?.trim();
151
+ return new RuntimeError({
152
+ code: deploy.error.code,
153
+ status: 0,
154
+ message: `Version ${deploy.version} did not go live: ${deploy.error.message}${last ? `\nIts last output:\n${last}` : ""}`,
155
+ hint: deploy.error.hint,
156
+ details: { deploy },
157
+ });
158
+ }
159
+ export async function appsCommand(argv, env, out, client, kit) {
160
+ const { parse, flag, has, need, pairs, envPairs, table, usage } = kit;
161
+ const [verb, ...rest] = argv;
162
+ const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
163
+ if (!verb || verb === "help" || verb === "--help") {
164
+ print(named(APP_HELP), { usage: named(APP_HELP) });
165
+ return 0;
166
+ }
167
+ const api = apps((await client()).transport);
168
+ const idOf = (target) => appIdOf(api, need(target, "an App's id or name"), kit);
169
+ switch (verb) {
170
+ case "create": {
171
+ const args = parse(rest, [], SETTINGS);
172
+ if (args.positional.length > 1)
173
+ throw usage(`Give one name: ${me} apps create web --vcpu 1 --memory 512 --max-copies 2`);
174
+ const given = settings(args, kit);
175
+ for (const [option, field] of [
176
+ ["vcpu", "vcpu"],
177
+ ["memory", "memoryMiB"],
178
+ ["max-copies", "maxCopies"],
179
+ ])
180
+ if (given[field] === undefined)
181
+ throw usage(`Give --${option}: ${me} apps create ${args.positional[0] ?? "web"} --vcpu 1 --memory 512 --max-copies 2`);
182
+ const input = {
183
+ ...given,
184
+ ...(args.positional[0] ? { name: args.positional[0] } : {}),
185
+ ...(args.flags.has("label") ? { labels: pairs(args, "label") } : {}),
186
+ };
187
+ const app = await api.create(input);
188
+ if (!out.json)
189
+ out.error(`Created ${app.name ?? app.id}, waiting for its first version. Deploy one: ${me} apps deploy ${app.name ?? app.id} --image <image>`);
190
+ print(app.id, app);
191
+ return 0;
192
+ }
193
+ case "ls":
194
+ case "list": {
195
+ const args = parse(rest, ["all"], []);
196
+ const all = await (await api.list({
197
+ limit: 100,
198
+ ...(has(args, "all")
199
+ ? { state: ["waiting", "live", "failed", "deleting", "deleted"] }
200
+ : {}),
201
+ })).toArray(Infinity);
202
+ print(all.length
203
+ ? table([
204
+ ["ID", "NAME", "STATE", "LIVE", "COPIES", "SIZE", "CREATED"],
205
+ ...all.map((a) => [
206
+ a.id,
207
+ a.name ?? "-",
208
+ a.state,
209
+ a.liveVersion === null ? "-" : `v${a.liveVersion}`,
210
+ `${a.minCopies}-${a.maxCopies}`,
211
+ `${a.vcpu} vCPU, ${a.memoryMiB} MiB`,
212
+ when(a.createdAt),
213
+ ]),
214
+ ])
215
+ : `No Apps. Make one: ${me} apps create web --vcpu 1 --memory 512 --max-copies 2`, all);
216
+ return 0;
217
+ }
218
+ case "get": {
219
+ const app = await api.get(await idOf(rest[0]));
220
+ print(JSON.stringify(app, null, 2), app);
221
+ return 0;
222
+ }
223
+ case "update": {
224
+ const args = parse(rest, [], [...SETTINGS, "name"]);
225
+ const id = await idOf(args.positional[0]);
226
+ const change = {
227
+ ...settings(args, kit),
228
+ ...(flag(args, "name") === undefined ? {} : { name: flag(args, "name") }),
229
+ ...(args.flags.has("label") ? { labels: pairs(args, "label") } : {}),
230
+ };
231
+ if (!Object.keys(change).length)
232
+ throw usage(`Give a setting to change, such as ${me} apps update ${args.positional[0]} --max-copies 4`);
233
+ const app = await api.update(id, change);
234
+ print(`${app.name ?? app.id} updated.`, app);
235
+ return 0;
236
+ }
237
+ case "rm":
238
+ case "delete": {
239
+ const app = await api.delete(await idOf(rest[0]));
240
+ print(`${app.name ?? app.id} ${app.state}.`, app);
241
+ return 0;
242
+ }
243
+ case "deploy": {
244
+ const args = parse(rest, ["keep-env", "no-wait"], ["image", "build", "env", "port", "health-path"]);
245
+ const id = await idOf(args.positional[0]);
246
+ const image = flag(args, "image");
247
+ const build = flag(args, "build");
248
+ if ((image === undefined) === (build === undefined))
249
+ throw usage(`Give --image <image> to deploy an image of yours, or --build <buildId> for a build that succeeded.`);
250
+ const port = kit.integer(args, "port");
251
+ const healthPath = flag(args, "health-path");
252
+ const wait = !has(args, "no-wait");
253
+ const input = {
254
+ ...(image !== undefined ? { image } : { build: build }),
255
+ ...(args.flags.has("env") ? { env: envPairs(args, env) } : {}),
256
+ ...(has(args, "keep-env") ? { keepEnv: true } : {}),
257
+ ...(port === undefined ? {} : { port }),
258
+ ...(healthPath === undefined ? {} : { healthPath }),
259
+ wait,
260
+ };
261
+ if (wait && !out.json)
262
+ out.error(`Deploying ${args.positional[0]}...`);
263
+ const deploy = await api.deploy(id, input, { timeoutMs: 15 * 60_000 });
264
+ const failed = deployFailure(deploy);
265
+ if (failed && !out.json)
266
+ throw failed;
267
+ print(deploy.state === "live"
268
+ ? `Version ${deploy.version} is live at ${deploy.url}`
269
+ : `Version ${deploy.version}: ${deploy.state}. Follow it: ${me} apps deploys ${args.positional[0]} ${deploy.id} --wait`, deploy);
270
+ return failed ? 1 : 0;
271
+ }
272
+ case "deploys": {
273
+ const args = parse(rest, ["wait"], []);
274
+ const id = await idOf(args.positional[0]);
275
+ const deployId = args.positional[1];
276
+ if (deployId) {
277
+ const deploy = has(args, "wait")
278
+ ? await api.waitDeploy(id, deployId, { timeoutMs: 15 * 60_000 })
279
+ : await api.getDeploy(id, deployId);
280
+ const failed = deployFailure(deploy);
281
+ if (failed && !out.json)
282
+ throw failed;
283
+ print(JSON.stringify(deploy, null, 2), deploy);
284
+ return failed ? 1 : 0;
285
+ }
286
+ // Newest first; a version's deploy is read once per line, so the list
287
+ // stops at the last 20.
288
+ const versions = (await api.versions(id)).sort((a, b) => b.version - a.version).slice(0, 20);
289
+ const deploys = await Promise.all(versions.map((v) => api.getDeploy(id, v.deploy)));
290
+ print(deploys.length
291
+ ? table([
292
+ ["VERSION", "DEPLOY", "STATE", "FROM", "ADDRESS"],
293
+ ...deploys.map((d, i) => [
294
+ `v${d.version}${versions[i].live ? " (live)" : versions[i].previous ? " (previous)" : ""}`,
295
+ d.id,
296
+ d.state,
297
+ versions[i].build ? `build ${versions[i].build}` : "image",
298
+ d.url,
299
+ ]),
300
+ ])
301
+ : `No deploys yet. Deploy one: ${me} apps deploy ${args.positional[0]} --image <image>`, deploys);
302
+ return 0;
303
+ }
304
+ case "versions": {
305
+ const id = await idOf(rest[0]);
306
+ const versions = (await api.versions(id)).sort((a, b) => b.version - a.version);
307
+ print(versions.length
308
+ ? table([
309
+ ["VERSION", "LIVE", "PREVIOUS", "SAVED", "DEPLOY", "BUILD"],
310
+ ...versions.map((v) => [
311
+ `v${v.version}`,
312
+ v.live ? "yes" : "-",
313
+ v.previous ? "yes" : "-",
314
+ v.saved ? "yes" : "-",
315
+ v.deploy,
316
+ v.build ?? "-",
317
+ ]),
318
+ ])
319
+ : "No versions yet.", versions);
320
+ return 0;
321
+ }
322
+ case "copies": {
323
+ const id = await idOf(rest[0]);
324
+ const copies = await api.copies(id);
325
+ print(copies.length
326
+ ? table([
327
+ ["COPY", "VERSION", "REGION", "STATE", "SINCE"],
328
+ ...copies.map((c) => [c.id, `v${c.version}`, c.region, c.state, when(c.since)]),
329
+ ])
330
+ : "No copies awake. A request starts one.", copies);
331
+ return 0;
332
+ }
333
+ case "rollback": {
334
+ const args = parse(rest, ["no-wait"], []);
335
+ const id = await idOf(args.positional[0]);
336
+ const deploy = await api.rollback(id, {
337
+ wait: !has(args, "no-wait"),
338
+ timeoutMs: 15 * 60_000,
339
+ });
340
+ const failed = deployFailure(deploy);
341
+ if (failed && !out.json)
342
+ throw failed;
343
+ print(deploy.state === "live"
344
+ ? `Version ${deploy.version} is live again at ${deploy.url}`
345
+ : `Rolling back to version ${deploy.version}: ${deploy.state}.`, deploy);
346
+ return failed ? 1 : 0;
347
+ }
348
+ }
349
+ throw kit.unknown("apps command", verb, [
350
+ "create",
351
+ "ls",
352
+ "get",
353
+ "update",
354
+ "rm",
355
+ "deploy",
356
+ "deploys",
357
+ "versions",
358
+ "copies",
359
+ "rollback",
360
+ ]);
361
+ }
362
+ /** A repository as connect takes it: GitHub's owner/name, a github.com
363
+ * address read as that, or any other https address as plain git. */
364
+ function source(repository) {
365
+ const github = /^(?:https:\/\/github\.com\/)?([\w.-]+\/[\w.-]+?)(?:\.git)?\/?$/.exec(repository);
366
+ return github ? { provider: "github", repository: github[1] } : { provider: "git", repository };
367
+ }
368
+ /** connect's and change's settings. */
369
+ function repositorySettings(args, kit) {
370
+ const { flag, has, integer, pairs } = kit;
371
+ const text = (name, field) => flag(args, name) === undefined ? {} : { [field]: flag(args, name) };
372
+ const deploy = flag(args, "deploy");
373
+ return {
374
+ ...(deploy === undefined
375
+ ? {}
376
+ : {
377
+ deploy: (/^(app|function|container):/.test(deploy)
378
+ ? deploy
379
+ : `app:${deploy}`),
380
+ }),
381
+ ...text("branch", "productionBranch"),
382
+ ...(has(args, "no-previews") ? { previews: false } : {}),
383
+ ...text("root", "rootDirectory"),
384
+ ...text("install", "installCommand"),
385
+ ...text("build", "buildCommand"),
386
+ ...text("start", "startCommand"),
387
+ ...(integer(args, "port") === undefined ? {} : { port: integer(args, "port") }),
388
+ ...text("health-path", "healthPath"),
389
+ ...(args.flags.has("build-env") ? { buildEnv: pairs(args, "build-env") } : {}),
390
+ ...text("name", "name"),
391
+ };
392
+ }
393
+ const REPOSITORY_VALUES = [
394
+ "deploy",
395
+ "branch",
396
+ "root",
397
+ "install",
398
+ "build",
399
+ "start",
400
+ "port",
401
+ "health-path",
402
+ "build-env",
403
+ "name",
404
+ ];
405
+ export async function buildsCommand(argv, out, client, kit) {
406
+ const { parse, flag, has, need, table, usage } = kit;
407
+ const [verb, ...rest] = argv;
408
+ const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
409
+ if (!verb || verb === "help" || verb === "--help") {
410
+ print(named(BUILD_HELP), { usage: named(BUILD_HELP) });
411
+ return 0;
412
+ }
413
+ const api = builds((await client()).transport);
414
+ const buildRow = (b) => [
415
+ b.id,
416
+ b.state,
417
+ b.ref,
418
+ b.commit ? b.commit.slice(0, 12) : "-",
419
+ when(b.createdAt),
420
+ b.reason ?? "-",
421
+ ];
422
+ /** A build's log, its last lines or followed to its end. */
423
+ const logs = async (id, follow, raw) => {
424
+ const [stdout, stderr] = [logStream(process.stdout, raw), logStream(process.stderr, raw)];
425
+ const write = (line) => (line.stream === "stderr" ? stderr : stdout)(line.text.endsWith("\n") ? line.text : `${line.text}\n`);
426
+ if (!follow) {
427
+ const log = await api.logs(id);
428
+ if (out.json) {
429
+ out.write(JSON.stringify(log));
430
+ return 0;
431
+ }
432
+ log.lines.forEach(write);
433
+ if (log.failingStep)
434
+ out.error(`[${log.state} at ${log.failingStep.name}${log.failingStep.exitCode === null ? "" : `, exit code ${log.failingStep.exitCode}`}]`);
435
+ else
436
+ out.error(ENDED.includes(log.state)
437
+ ? `[${log.state}${log.reason ? `, ${log.reason}` : ""}]`
438
+ : `[${log.state}; follow it: ${me} builds logs ${id} --follow]`);
439
+ return 0;
440
+ }
441
+ // A follow lasts until the build ends: no deadline of the SDK's own.
442
+ for await (const event of api.logs(id, { follow: true }, { timeoutMs: 0 })) {
443
+ if (out.json)
444
+ out.write(JSON.stringify(event));
445
+ else if (event.type === "line")
446
+ write(event);
447
+ else if (event.type === "gap")
448
+ out.error("[older lines were dropped before they were read; going on from the oldest kept]");
449
+ if (event.type === "terminal") {
450
+ if (!out.json) {
451
+ out.error(`[${event.state}, ${event.reason}]`);
452
+ if (event.failingStep)
453
+ 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")}`);
454
+ }
455
+ return event.state === "succeeded" ? 0 : 1;
456
+ }
457
+ }
458
+ return 1;
459
+ };
460
+ switch (verb) {
461
+ case "repos":
462
+ case "repositories": {
463
+ const [action, ...more] = rest;
464
+ switch (action) {
465
+ case "connect": {
466
+ const args = parse(more, ["no-previews"], REPOSITORY_VALUES);
467
+ const repository = need(args.positional[0], "the repository, such as acme/web");
468
+ const given = repositorySettings(args, kit);
469
+ if (!given.deploy)
470
+ throw usage(`Give --deploy with the App it deploys: ${me} builds repos connect ${repository} --deploy web`);
471
+ const repo = await api.repositories.connect({
472
+ ...source(repository),
473
+ ...given,
474
+ });
475
+ if (!out.json)
476
+ out.error(`Connected ${repo.repository}: a push to ${repo.productionBranch} builds and deploys ${repo.deploy}. Build now: ${me} builds run ${repo.id}`);
477
+ print(repo.id, repo);
478
+ return 0;
479
+ }
480
+ case "ls":
481
+ case "list": {
482
+ const all = await (await api.repositories.list({ limit: 100 })).toArray(Infinity);
483
+ print(all.length
484
+ ? table([
485
+ ["ID", "REPOSITORY", "BRANCH", "DEPLOYS", "STATE"],
486
+ ...all.map((r) => [
487
+ r.id,
488
+ r.repository,
489
+ r.productionBranch,
490
+ r.deploy,
491
+ r.state,
492
+ ]),
493
+ ])
494
+ : `No repositories. Connect one: ${me} builds repos connect acme/web --deploy web`, all);
495
+ return 0;
496
+ }
497
+ case "change":
498
+ case "update": {
499
+ const args = parse(more, ["no-previews"], REPOSITORY_VALUES);
500
+ const id = need(args.positional[0], "a repository's id");
501
+ const change = repositorySettings(args, kit);
502
+ if (!Object.keys(change).length)
503
+ throw usage(`Give a setting to change, such as --branch main or --start "npm start".`);
504
+ const repo = await api.repositories.update(id, change);
505
+ print(`${repo.repository} changed.`, repo);
506
+ return 0;
507
+ }
508
+ case "disconnect":
509
+ case "rm": {
510
+ const repo = await api.repositories.disconnect(need(more[0], "a repository's id"));
511
+ print(`${repo.repository} disconnected; its builds stay.`, repo);
512
+ return 0;
513
+ }
514
+ case "install": {
515
+ const link = await api.repositories.installGithub();
516
+ print(`Open this within 15 minutes, install the App on the repositories Runtime may read, and confirm the organization:\n${link.url}`, link);
517
+ return 0;
518
+ }
519
+ }
520
+ throw kit.unknown("repos command", action ?? "", [
521
+ "connect",
522
+ "ls",
523
+ "change",
524
+ "disconnect",
525
+ "install",
526
+ ]);
527
+ }
528
+ case "create":
529
+ case "run": {
530
+ const args = parse(rest, verb === "run" ? ["raw"] : [], ["ref", "branch", "name"]);
531
+ const repositoryId = need(args.positional[0], "a repository's id (runtime builds repos ls)");
532
+ const build = await api.create({
533
+ repositoryId,
534
+ ref: flag(args, "ref") ?? flag(args, "branch") ?? "main",
535
+ ...(flag(args, "branch") ? { branch: flag(args, "branch") } : {}),
536
+ ...(flag(args, "name") ? { name: flag(args, "name") } : {}),
537
+ });
538
+ if (verb === "create") {
539
+ if (!out.json)
540
+ out.error(`Building ${build.ref}${build.commit ? ` at ${build.commit.slice(0, 12)}` : ""}. Follow it: ${me} builds logs ${build.id} --follow`);
541
+ print(build.id, build);
542
+ return 0;
543
+ }
544
+ if (!out.json)
545
+ out.error(`Build ${build.id}, ${build.ref}${build.commit ? ` at ${build.commit.slice(0, 12)}` : ""}:`);
546
+ else
547
+ out.write(JSON.stringify(build));
548
+ return logs(build.id, true, has(args, "raw"));
549
+ }
550
+ case "ls":
551
+ case "list": {
552
+ const args = parse(rest, [], ["repository", "commit", "state"]);
553
+ const commit = flag(args, "commit");
554
+ if (commit !== undefined && !/^([0-9a-f]{40}|[0-9a-f]{64})$/.test(commit))
555
+ throw usage(`--commit takes a full sha in lower case: ${me} builds ls --commit $(git rev-parse HEAD)`);
556
+ const states = args.flags.get("state");
557
+ const all = await (await api.list({
558
+ limit: 100,
559
+ ...(flag(args, "repository") ? { repositoryId: flag(args, "repository") } : {}),
560
+ ...(commit ? { commit } : {}),
561
+ ...(states ? { state: states } : {}),
562
+ })).toArray(Infinity);
563
+ print(all.length
564
+ ? table([["ID", "STATE", "REF", "COMMIT", "CREATED", "REASON"], ...all.map(buildRow)])
565
+ : "No builds.", all);
566
+ return 0;
567
+ }
568
+ case "get": {
569
+ const build = await api.get(need(rest[0], "a build's id"));
570
+ print(JSON.stringify(build, null, 2), build);
571
+ return 0;
572
+ }
573
+ case "cancel": {
574
+ const build = await api.cancel(need(rest[0], "a build's id"));
575
+ print(`${build.id} ${build.state}.`, build);
576
+ return 0;
577
+ }
578
+ case "logs": {
579
+ const args = parse(rest, ["f", "follow", "raw"], []);
580
+ const id = need(args.positional[0], "a build's id");
581
+ return logs(id, has(args, "f") || has(args, "follow"), has(args, "raw"));
582
+ }
583
+ }
584
+ throw kit.unknown("builds command", verb, [
585
+ "repos",
586
+ "create",
587
+ "run",
588
+ "ls",
589
+ "get",
590
+ "cancel",
591
+ "logs",
592
+ ]);
593
+ }
594
+ const CONTAINER_STATES = [
595
+ "waiting",
596
+ "running",
597
+ "sleeping",
598
+ "stopped",
599
+ "failed",
600
+ "deleting",
601
+ "deleted",
602
+ ];
603
+ const CONTAINER_VALUES = [
604
+ "vcpu",
605
+ "memory",
606
+ "name",
607
+ "label",
608
+ "env",
609
+ "entrypoint",
610
+ "workdir",
611
+ "user",
612
+ "port",
613
+ "on-exit",
614
+ ];
615
+ export async function containersCommand(argv, env, out, client, kit) {
616
+ const { parse, flag, has, integer, need, pairs, envPairs, table, usage } = kit;
617
+ const [verb, ...rest] = argv;
618
+ const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
619
+ if (!verb || verb === "help" || verb === "--help") {
620
+ print(named(CONTAINER_HELP), { usage: named(CONTAINER_HELP) });
621
+ return 0;
622
+ }
623
+ const api = containers((await client()).transport);
624
+ /** A container's id from its id or its name: a name is looked up among the
625
+ * containers not deleted, and must be one container's alone. */
626
+ const idOf = async (target) => {
627
+ const given = need(target, "a container's id or name");
628
+ if (UUID.test(given))
629
+ return given;
630
+ const found = (await api.list({ name: given })).data;
631
+ if (found.length === 1)
632
+ return found[0].id;
633
+ throw usage(found.length
634
+ ? `${found.length} containers are named ${given}; give one's id (${me} containers ls).`
635
+ : `No container is named ${given}. Your containers: ${me} containers ls`);
636
+ };
637
+ switch (verb) {
638
+ case "create": {
639
+ const args = parse(rest, ["sleep"], CONTAINER_VALUES);
640
+ if (args.positional.length !== 1)
641
+ throw usage(`Give one image: ${me} containers create nginx:1.27 --vcpu 1 --memory 512 [-- <command...>]`);
642
+ const image = args.positional[0];
643
+ const vcpu = integer(args, "vcpu");
644
+ const memoryMiB = integer(args, "memory");
645
+ if (vcpu === undefined || memoryMiB === undefined)
646
+ throw usage(`Give --vcpu and --memory: ${me} containers create ${image} --vcpu 1 --memory 512`);
647
+ const onExit = flag(args, "on-exit");
648
+ if (onExit !== undefined && onExit !== "restart" && onExit !== "stop")
649
+ throw usage("--on-exit takes restart or stop.");
650
+ const port = integer(args, "port");
651
+ const entrypoint = flag(args, "entrypoint");
652
+ const workingDir = flag(args, "workdir");
653
+ const user = flag(args, "user");
654
+ const name = flag(args, "name");
655
+ const input = {
656
+ image,
657
+ vcpu,
658
+ memoryMiB,
659
+ ...(name === undefined ? {} : { name }),
660
+ ...(args.flags.has("label") ? { labels: pairs(args, "label") } : {}),
661
+ ...(args.flags.has("env") ? { env: envPairs(args, env) } : {}),
662
+ ...(entrypoint === undefined ? {} : { entrypoint: [entrypoint] }),
663
+ ...(args.rest?.length ? { command: args.rest } : {}),
664
+ ...(workingDir === undefined ? {} : { workingDir }),
665
+ ...(user === undefined ? {} : { user }),
666
+ ...(port === undefined ? {} : { port }),
667
+ ...(onExit === undefined ? {} : { onExit }),
668
+ ...(has(args, "sleep") ? { sleep: true } : {}),
669
+ };
670
+ const made = await api.create(input);
671
+ if (!out.json)
672
+ out.error(`${made.name ?? made.id} is ${made.state}, running ${made.image}. Read it: ${me} containers get ${made.name ?? made.id}`);
673
+ print(made.id, made);
674
+ return 0;
675
+ }
676
+ case "ls":
677
+ case "list": {
678
+ const args = parse(rest, ["all"], ["state", "label"]);
679
+ const states = args.flags.get("state");
680
+ for (const state of states ?? [])
681
+ if (!CONTAINER_STATES.includes(state))
682
+ throw usage(`--state takes ${CONTAINER_STATES.join(", ")}.`);
683
+ // --label key=value, as the list's key:value filters.
684
+ const labels = Object.entries(pairs(args, "label")).map(([k, v]) => `${k}:${v}`);
685
+ const all = await (await api.list({
686
+ limit: 100,
687
+ ...(states
688
+ ? { state: states }
689
+ : has(args, "all")
690
+ ? { state: CONTAINER_STATES }
691
+ : {}),
692
+ ...(labels.length ? { label: labels } : {}),
693
+ })).toArray(Infinity);
694
+ print(all.length
695
+ ? table([
696
+ ["ID", "NAME", "STATE", "IMAGE", "PORT", "SIZE", "CREATED"],
697
+ ...all.map((c) => [
698
+ c.id,
699
+ c.name ?? "-",
700
+ c.state,
701
+ c.image,
702
+ c.port === null ? "-" : String(c.port),
703
+ `${c.vcpu} vCPU, ${c.memoryMiB} MiB`,
704
+ when(c.createdAt),
705
+ ]),
706
+ ])
707
+ : `No containers. Run one: ${me} containers create nginx:1.27 --vcpu 1 --memory 512`, all);
708
+ return 0;
709
+ }
710
+ case "get": {
711
+ const found = await api.get(await idOf(rest[0]));
712
+ print(JSON.stringify(found, null, 2), found);
713
+ return 0;
714
+ }
715
+ case "update": {
716
+ const args = parse(rest, [], ["name", "label"]);
717
+ const id = await idOf(args.positional[0]);
718
+ const change = {
719
+ ...(flag(args, "name") === undefined ? {} : { name: flag(args, "name") }),
720
+ ...(args.flags.has("label") ? { labels: pairs(args, "label") } : {}),
721
+ };
722
+ if (!Object.keys(change).length)
723
+ throw usage(`Give --name or --label: ${me} containers update ${args.positional[0]} --name web`);
724
+ const updated = await api.update(id, change);
725
+ print(`${updated.name ?? updated.id} updated.`, updated);
726
+ return 0;
727
+ }
728
+ case "rm":
729
+ case "delete": {
730
+ const args = parse(rest, ["wait"], []);
731
+ const gone = await api.delete(await idOf(args.positional[0]), { wait: has(args, "wait") });
732
+ print(`${gone.name ?? gone.id} ${gone.state}.`, gone);
733
+ return 0;
734
+ }
735
+ }
736
+ throw kit.unknown("containers command", verb, ["create", "ls", "get", "update", "rm"]);
737
+ }