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.
- package/COMPATIBILITY.md +32 -1
- package/README.md +19 -0
- package/README.src.md +19 -0
- package/dist/agents/index.d.ts +11 -0
- package/dist/agents/index.js +12 -0
- package/dist/agents-cli.d.ts +10 -0
- package/dist/agents-cli.js +362 -0
- package/dist/apps-cli.d.ts +14 -0
- package/dist/apps-cli.js +737 -0
- package/dist/builds/index.d.ts +10 -0
- package/dist/builds/index.js +11 -0
- package/dist/cli-words.js +11 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +104 -6
- package/dist/client.d.ts +19 -0
- package/dist/client.js +20 -0
- package/dist/cloudflare/errors.d.ts +23 -0
- package/dist/cloudflare/errors.js +16 -0
- package/dist/cloudflare/index.d.ts +82 -9
- package/dist/cloudflare/index.js +162 -38
- package/dist/cloudflare/interpreter.js +14 -5
- package/dist/cloudflare/mime.d.ts +1 -0
- package/dist/cloudflare/mime.js +66 -0
- package/dist/cloudflare/streams.d.ts +8 -0
- package/dist/cloudflare/streams.js +30 -0
- package/dist/cloudflare/types.d.ts +338 -0
- package/dist/cloudflare/types.js +80 -0
- package/dist/cloudflare/worker.d.ts +297 -10
- package/dist/cloudflare/worker.js +203 -17
- package/dist/containers/index.d.ts +10 -0
- package/dist/containers/index.js +11 -0
- package/dist/daytona/sandbox.d.ts +2 -1
- package/dist/daytona/sandbox.js +2 -1
- package/dist/identity.js +18 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/product-names.js +3 -0
- package/dist/products/agents.d.ts +272 -0
- package/dist/products/agents.js +251 -0
- package/dist/products/builds.d.ts +11 -1
- package/dist/products/builds.js +7 -0
- package/dist/products/containers.d.ts +71 -0
- package/dist/products/containers.js +6 -0
- package/dist/products/index.d.ts +6 -0
- package/dist/products/index.js +7 -0
- package/dist/sandbox.d.ts +4 -0
- package/dist/transport.d.ts +2 -2
- package/dist/transport.js +1 -1
- package/dist/tunnel.js +1 -1
- package/dist/types.d.ts +11 -0
- package/dist/vercel/errors.d.ts +1 -1
- package/package.json +16 -1
package/dist/apps-cli.js
ADDED
|
@@ -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
|
+
}
|