withruntime 0.17.1 → 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.
- package/DAYTONA.md +33 -2
- package/compatibility-lock.json +15 -15
- package/dist/apps-cli.d.ts +12 -0
- package/dist/apps-cli.js +564 -0
- package/dist/billing-cli.d.ts +4 -2
- package/dist/billing-cli.js +27 -1
- package/dist/cli-words.js +10 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +33 -2
- package/dist/daytona/index.d.ts +61 -0
- package/dist/daytona/index.js +57 -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 +1 -1
- package/dist/index.js +1 -1
- package/dist/products/apps.d.ts +6 -2
- package/dist/products/browser.d.ts +4 -2
- package/dist/products/browser.js +4 -2
- package/dist/products/builds.d.ts +11 -1
- package/dist/products/builds.js +7 -0
- package/dist/products/limits.d.ts +18 -0
- package/dist/products/observability.d.ts +1 -1
- package/dist/transport.d.ts +1 -1
- package/dist/transport.js +1 -1
- package/dist/vercel/errors.d.ts +1 -1
- package/dist/wallet.d.ts +12 -0
- package/dist/wallet.js +7 -0
- package/package.json +1 -1
package/dist/apps-cli.js
ADDED
|
@@ -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/billing-cli.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Runtime } from "./client.js";
|
|
2
|
-
import type { claimWalletAccount, openWalletAccount } from "./wallet.js";
|
|
2
|
+
import type { claimWalletAccount, openWalletAccount, rekeyWalletAccount } from "./wallet.js";
|
|
3
3
|
type Out = {
|
|
4
4
|
json: boolean;
|
|
5
5
|
write: (text: string) => void;
|
|
@@ -9,11 +9,13 @@ type Args = {
|
|
|
9
9
|
positional: string[];
|
|
10
10
|
flags: Map<string, string[]>;
|
|
11
11
|
};
|
|
12
|
-
export declare const BILLING_HELP = "runtime billing <command>\n\n topup <usd> [--wait] Buy credit with stablecoins (USDC on Base, Solana or Tempo).\n Prints an address per network and the exact amount to send.\n Send exactly that, from any wallet, before the time shown.\n --wait keeps checking until the credit lands.\n status <id> [--wait] Whether a top-up's payment has arrived\n\n auto-top-up Show auto top-up: the card is charged when credit runs low\n auto-top-up on [--below <usd>] [--amount <usd>] [--monthly <usd>]\n Charge <amount> plus tax when credit falls below <below>,\n at most <monthly> a month: $10, $20 and $500 unless given,\n or as last set. At least $10, $20 and $100. 5% more as\n credit each time, and 200 sandboxes at once while on.\n A person saves the card at\n https://withruntime.com/account/billing\n auto-top-up off Turn it off. Running sandboxes are never stopped.\n\n No account yet? An agent can open one with a wallet alone:\n topup <usd> --new-account --accept-terms [--wait]\n Opens an account and its first top-up. Prints a claim code,\n shown once. --wait saves the key here once the money lands.\n claim <code> [--wait] Exchange a paid account's claim code for its key, saved here\n\n $10 to $10,000 a top-up; $500 in an account's first week. By card:\n https://withruntime.com/account/billing\n";
|
|
12
|
+
export declare const BILLING_HELP = "runtime billing <command>\n\n topup <usd> [--wait] Buy credit with stablecoins (USDC on Base, Solana or Tempo).\n Prints an address per network and the exact amount to send.\n Send exactly that, from any wallet, before the time shown.\n --wait keeps checking until the credit lands.\n status <id> [--wait] Whether a top-up's payment has arrived\n\n auto-top-up Show auto top-up: the card is charged when credit runs low\n auto-top-up on [--below <usd>] [--amount <usd>] [--monthly <usd>]\n Charge <amount> plus tax when credit falls below <below>,\n at most <monthly> a month: $10, $20 and $500 unless given,\n or as last set. At least $10, $20 and $100. 5% more as\n credit each time, and 200 sandboxes at once while on.\n A person saves the card at\n https://withruntime.com/account/billing\n auto-top-up off Turn it off. Running sandboxes are never stopped.\n\n No account yet? An agent can open one with a wallet alone:\n topup <usd> --new-account --accept-terms [--wait]\n Opens an account and its first top-up. Prints a claim code,\n shown once. --wait saves the key here once the money lands.\n claim <code> [--wait] Exchange a paid account's claim code for its key, saved here\n claim <code> --new-key Lost the key? Replace it with the claim code. The new key is\n saved here; every other key stops working.\n\n $10 to $10,000 a top-up; $500 in an account's first week. By card:\n https://withruntime.com/account/billing\n";
|
|
13
13
|
/** Wallet-account commands need no key, and the claim saves the one they get. */
|
|
14
14
|
export type WalletDeps = {
|
|
15
15
|
open: typeof openWalletAccount;
|
|
16
16
|
claim: typeof claimWalletAccount;
|
|
17
|
+
/** Replaces a lost key with the claim code. */
|
|
18
|
+
rekey: typeof rekeyWalletAccount;
|
|
17
19
|
/** Saves a claimed key as this machine's connection, as `login --with-key` does. */
|
|
18
20
|
save: (key: string, orgId: string) => Promise<void>;
|
|
19
21
|
};
|
package/dist/billing-cli.js
CHANGED
|
@@ -23,6 +23,8 @@ export const BILLING_HELP = `runtime billing <command>
|
|
|
23
23
|
Opens an account and its first top-up. Prints a claim code,
|
|
24
24
|
shown once. --wait saves the key here once the money lands.
|
|
25
25
|
claim <code> [--wait] Exchange a paid account's claim code for its key, saved here
|
|
26
|
+
claim <code> --new-key Lost the key? Replace it with the claim code. The new key is
|
|
27
|
+
saved here; every other key stops working.
|
|
26
28
|
|
|
27
29
|
$10 to $10,000 a top-up; $500 in an account's first week. By card:
|
|
28
30
|
https://withruntime.com/account/billing
|
|
@@ -53,7 +55,15 @@ function describe(topup) {
|
|
|
53
55
|
lines.push("", ...(topup.payBy ? [`Send before ${topup.payBy}.`] : []), "A different amount, token or network cannot be matched or returned automatically.", `The credit lands a few minutes after the transfer: \`${me} billing status ${topup.purchaseId}\`.`);
|
|
54
56
|
return lines.join("\n");
|
|
55
57
|
}
|
|
56
|
-
const FLAGS = new Set([
|
|
58
|
+
const FLAGS = new Set([
|
|
59
|
+
"wait",
|
|
60
|
+
"new-account",
|
|
61
|
+
"accept-terms",
|
|
62
|
+
"below",
|
|
63
|
+
"amount",
|
|
64
|
+
"monthly",
|
|
65
|
+
"new-key",
|
|
66
|
+
]);
|
|
57
67
|
export async function billingCommand(argv, args, out, client, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), wallet) {
|
|
58
68
|
const [verb, value] = args.positional;
|
|
59
69
|
const has = (name) => args.flags.get(name)?.at(-1) === "true";
|
|
@@ -68,6 +78,11 @@ export async function billingCommand(argv, args, out, client, sleep = (ms) => ne
|
|
|
68
78
|
if ((verb === "topup" && has("new-account")) || verb === "claim") {
|
|
69
79
|
if (!wallet)
|
|
70
80
|
throw usage("Wallet accounts are not available here.");
|
|
81
|
+
if (has("new-key")) {
|
|
82
|
+
if (verb !== "claim")
|
|
83
|
+
throw usage("--new-key is for `billing claim <code> --new-key`.");
|
|
84
|
+
return rekeyCommand(value, out, wallet);
|
|
85
|
+
}
|
|
71
86
|
return walletCommand(verb, value, has("accept-terms"), wait, out, sleep, wallet);
|
|
72
87
|
}
|
|
73
88
|
if (verb === "auto-top-up")
|
|
@@ -177,6 +192,17 @@ function dollarsOf(value) {
|
|
|
177
192
|
throw usage("Say how many dollars of credit to buy, such as `billing topup 20`.");
|
|
178
193
|
return usd;
|
|
179
194
|
}
|
|
195
|
+
/** `claim <code> --new-key`: the key was lost; the claim code makes a new one. */
|
|
196
|
+
async function rekeyCommand(code, out, wallet) {
|
|
197
|
+
if (!code)
|
|
198
|
+
throw usage("Give the claim code.");
|
|
199
|
+
const replaced = await wallet.rekey(code);
|
|
200
|
+
await wallet.save(replaced.apiKey, replaced.orgId);
|
|
201
|
+
out.write(out.json
|
|
202
|
+
? JSON.stringify({ status: "replaced", orgId: replaced.orgId, connected: true })
|
|
203
|
+
: "Replaced. This machine is connected with the new key, saved and not shown. Every other key of this account no longer works.");
|
|
204
|
+
return 0;
|
|
205
|
+
}
|
|
180
206
|
/** `topup <usd> --new-account` and `claim <code>`: an account with no key. */
|
|
181
207
|
async function walletCommand(verb, value, acceptTerms, wait, out, sleep, wallet) {
|
|
182
208
|
const me = named("runtime");
|
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
|