@tonbo/cli 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -2
- package/dist/bin/tonbo.js +253 -55
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ tonbo login [--account <name>]
|
|
|
14
14
|
tonbo logout
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
`login` opens the browser at `https://tonbo.dev/cli/login`, where you pick the account (your personal account unless `--account` names another) and approve a personal API key named `CLI on <hostname>`. The browser returns a one-time code to a local callback on `127.0.0.1`, and the CLI redeems it with its PKCE verifier for the key. The key acts as you with your role in that account, expires after six months and replaces the key an earlier login on the same host created; there is no refresh, so an expired or revoked key asks you to run `tonbo login` again. One login holds one account: log in again to switch accounts.
|
|
17
|
+
`login` prints the login URL and opens the browser at `https://tonbo.dev/cli/login`, where you pick the account (your personal account unless `--account` names another) and approve a personal API key named `CLI on <hostname>`. The browser returns a one-time code to a local callback on `127.0.0.1`, and the CLI redeems it with its PKCE verifier for the key. The key acts as you with your role in that account, expires after six months and replaces the key an earlier login on the same host created; there is no refresh, so an expired or revoked key asks you to run `tonbo login` again. One login holds one account: log in again to switch accounts. When no browser can be opened (an SSH session, a server without a desktop), the CLI keeps waiting: open the printed URL in a browser that can reach the callback's `127.0.0.1:<port>` on that machine, through `ssh -L <port>:127.0.0.1:<port>` or the VM's port forwarding.
|
|
18
18
|
|
|
19
19
|
The login and the credentials file are shared with [`yield`](../yield) through `@tonbo/cli-core`: either CLI's `login` signs both in, and either `logout` signs both out. The login is saved at `${XDG_CONFIG_HOME:-$HOME/.config}/tonbo/credentials.json` as `{version, account_id, key_id, key, expires_at}`. The CLI atomically replaces that file with owner-only permissions and refuses a symlink or a file other users can read. A file written by an earlier CLI release is not migrated; run `tonbo login`.
|
|
20
20
|
|
|
@@ -35,6 +35,7 @@ Every command other than `login` and `logout` calls one operation of the [Manage
|
|
|
35
35
|
| `tonbo region list` | `listRegions` | `GET /v1/regions` |
|
|
36
36
|
| `tonbo member list [--limit <n>] [--cursor <c>]` | `listMembers` | `GET /v1/members` |
|
|
37
37
|
| `tonbo member update <member> [--role <role>] [--name <name>]` | `updateMember` | `PATCH /v1/members/{subject}` |
|
|
38
|
+
| `tonbo workspace list [--limit <n>] [--cursor <c>]` | `listGitWorkspaces` | `GET /v1/git/workspaces` |
|
|
38
39
|
| `tonbo agent create <name> [--role <role>] [--region <region>] [--idempotency-key <key>]` | `createAgent` | `POST /v1/agents` |
|
|
39
40
|
| `tonbo api-key list [--limit <n>] [--cursor <c>]` | `listApiKeys` | `GET /v1/api-keys` |
|
|
40
41
|
| `tonbo api-key create <name> [--member <member>] [--read-only] [--bucket <name>]… [--database <name>]… [--database-prefix <prefix>]… [--stream <name>]… [--stream-prefix <prefix>]… [--expires-in <period>]` | `createApiKey` | `POST /v1/api-keys` |
|
|
@@ -101,6 +102,14 @@ tonbo agent create my-agent --region us-east-1
|
|
|
101
102
|
|
|
102
103
|
`agent create` creates an Agent identity with a role (`member` unless `--role` names another). Every Agent has a home region, chosen when it is created and never changed afterwards. Without `--region`, Tonbo uses the only region that accepts new Agents; when several do, an interactive `agent create` asks which one to use, and a non-interactive request without `--region` is refused.
|
|
103
104
|
|
|
105
|
+
## Git workspaces
|
|
106
|
+
|
|
107
|
+
```console
|
|
108
|
+
tonbo workspace list
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`workspace list` lists the account's git workspaces, the default first: each one's id, name, whether it is the default, its region and where gitd serves it (`<git origin>/<ws_hex>`, the prefix of every repository URL in it). Every account has a default workspace from its creation. Listing needs only a role on the account; it admits nothing, and every git token is still admitted when it is minted.
|
|
112
|
+
|
|
104
113
|
## API keys
|
|
105
114
|
|
|
106
115
|
```console
|
|
@@ -132,7 +141,7 @@ tonbo database show notes
|
|
|
132
141
|
`database create` creates a database in `--region`, or in the default region, and prints its fields with its URL. `--expires-at` and `--expires-in` give it an expiry fixed for its life, by the same rules as `stream create`'s: once it passes, Tonbo deletes the database within a minute, as `database delete` would, and its name is free again; `database show` prints it, and without either the database never expires. A Machine's database never expires. Without `--json` it then writes the next step to stderr, opening the database by name from code:
|
|
133
142
|
|
|
134
143
|
```text
|
|
135
|
-
Next: npm i @tonbo/sqlite-ursula@0.11.2
|
|
144
|
+
Next: npm i --save-exact @tonbo/sqlite-ursula@0.11.2
|
|
136
145
|
open("notes") from "@tonbo/sqlite-ursula/tonbo"
|
|
137
146
|
```
|
|
138
147
|
|
|
@@ -168,6 +177,8 @@ The image is `tonbo.dev/platform`, the platform image each node release ships, o
|
|
|
168
177
|
|
|
169
178
|
`machine update` changes only what its options name. It reads the Machine and writes the change at the revision it read, or at `--revision`, so a change someone made in between is refused with `revision_mismatch` rather than overwritten. `--env` and `--unset-env` change single variables; `--port` replaces every port, naming a public port again keeps its limits, and the current default port stays the default while it is still listed unless `--default-port` names another. Ports and `--sleep-after` apply at once; image, command, shape and env at the Machine's next boot. `machine sleep` puts an awake Machine to sleep now; its next connection wakes it. `machine delete` deletes the Machine with its disk, database and logs, and its name is free for a new Machine at once.
|
|
170
179
|
|
|
180
|
+
In a personal account, `machine list`, `machine show` and `machine delete` also cover your own computers, which `yield login` registers (Machines of kind `byoc`, which run nothing Tonbo operates). They are served even while Machines are not open in any region. `machine list` prints them in a second table with their system, agent harnesses, when they last reported and the state of their login key; `machine show` adds the hostname and device id, which only you see. `machine delete` forgets a computer: it disappears from the list at once and its name is free once the forget completes. Forgetting does not revoke the computer's key, so a computer whose key is still active registers again at its next start; the command says so and suggests `yield logout` on it or `tonbo api-key revoke` first. An organization lists managed Machines only.
|
|
181
|
+
|
|
171
182
|
A refusal of machine-v2's `errors` prints the server's detail and then what to do next, for example `machine shapes` for `shape_unavailable` or a shorter name for `hostname_too_long`.
|
|
172
183
|
|
|
173
184
|
## Usage
|
package/dist/bin/tonbo.js
CHANGED
|
@@ -486,6 +486,17 @@ var managementOperations = {
|
|
|
486
486
|
"idempotencyKey": false,
|
|
487
487
|
"cli": "tonbo usage show"
|
|
488
488
|
},
|
|
489
|
+
"listGitWorkspaces": {
|
|
490
|
+
"method": "GET",
|
|
491
|
+
"path": "/git/workspaces",
|
|
492
|
+
"pathParams": [],
|
|
493
|
+
"query": [
|
|
494
|
+
"cursor",
|
|
495
|
+
"limit"
|
|
496
|
+
],
|
|
497
|
+
"idempotencyKey": false,
|
|
498
|
+
"cli": "tonbo workspace list"
|
|
499
|
+
},
|
|
489
500
|
"listDatabases": {
|
|
490
501
|
"method": "GET",
|
|
491
502
|
"path": "/databases",
|
|
@@ -1025,7 +1036,7 @@ var AuthClient = class {
|
|
|
1025
1036
|
hostname: this.hostname,
|
|
1026
1037
|
...account ? { account } : {}
|
|
1027
1038
|
}).toString();
|
|
1028
|
-
return
|
|
1039
|
+
return authorization.toString();
|
|
1029
1040
|
}, progress);
|
|
1030
1041
|
progress({ status: "started", step: "key-redemption" });
|
|
1031
1042
|
const redeemed = await requestJson(this.fetcher, new URL("/api/cli/login/token", this.loginOrigin).toString(), {
|
|
@@ -1046,16 +1057,17 @@ var AuthClient = class {
|
|
|
1046
1057
|
progress({ status: "completed", step: "credential-store" });
|
|
1047
1058
|
return login;
|
|
1048
1059
|
}
|
|
1049
|
-
listenForCode(expectedState,
|
|
1060
|
+
listenForCode(expectedState, authorizationUrl, progress) {
|
|
1050
1061
|
return new Promise((resolve, reject) => {
|
|
1051
1062
|
let settled = false;
|
|
1052
1063
|
const sockets = /* @__PURE__ */ new Set();
|
|
1053
1064
|
let authorizationStarted = false;
|
|
1054
|
-
const startAuthorization = () => {
|
|
1065
|
+
const startAuthorization = (browserOpened = true) => {
|
|
1055
1066
|
if (authorizationStarted)
|
|
1056
1067
|
return;
|
|
1057
1068
|
authorizationStarted = true;
|
|
1058
|
-
|
|
1069
|
+
if (browserOpened)
|
|
1070
|
+
progress({ status: "completed", step: "browser" });
|
|
1059
1071
|
progress({ status: "started", step: "browser-authorization" });
|
|
1060
1072
|
};
|
|
1061
1073
|
const finish = (result, completedResponseSocket) => {
|
|
@@ -1119,10 +1131,20 @@ var AuthClient = class {
|
|
|
1119
1131
|
server.listen(0, LOOPBACK_HOST, () => {
|
|
1120
1132
|
const { port } = server.address();
|
|
1121
1133
|
progress({ status: "completed", step: "callback-server" });
|
|
1134
|
+
const url = authorizationUrl(`http://${LOOPBACK_HOST}:${port}/callback`);
|
|
1135
|
+
progress({ status: "authorization-url", step: "browser", url });
|
|
1122
1136
|
progress({ status: "started", step: "browser" });
|
|
1123
|
-
void
|
|
1124
|
-
|
|
1125
|
-
|
|
1137
|
+
void Promise.resolve().then(() => this.browserOpener(url)).then(() => startAuthorization(), (error) => {
|
|
1138
|
+
if (settled || authorizationStarted)
|
|
1139
|
+
return;
|
|
1140
|
+
progress({
|
|
1141
|
+
status: "failed",
|
|
1142
|
+
step: "browser",
|
|
1143
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1144
|
+
callbackPort: port
|
|
1145
|
+
});
|
|
1146
|
+
startAuthorization(false);
|
|
1147
|
+
});
|
|
1126
1148
|
});
|
|
1127
1149
|
});
|
|
1128
1150
|
}
|
|
@@ -1134,12 +1156,27 @@ function respond(response, outcome, finished) {
|
|
|
1134
1156
|
response.writeHead(page.status, { ...page.headers, connection: "close" });
|
|
1135
1157
|
response.end(page.body);
|
|
1136
1158
|
}
|
|
1159
|
+
function browserCommand(url, platform = process.platform, env = process.env) {
|
|
1160
|
+
if (platform === "darwin")
|
|
1161
|
+
return ["open", [url]];
|
|
1162
|
+
if (platform === "win32")
|
|
1163
|
+
return ["rundll32", ["url.dll,FileProtocolHandler", url]];
|
|
1164
|
+
if (!env.DISPLAY && !env.WAYLAND_DISPLAY)
|
|
1165
|
+
throw new Error("there is no graphical session (DISPLAY and WAYLAND_DISPLAY are unset)");
|
|
1166
|
+
return ["xdg-open", [url]];
|
|
1167
|
+
}
|
|
1137
1168
|
async function openBrowser(url) {
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1169
|
+
const [command, args] = browserCommand(url);
|
|
1170
|
+
try {
|
|
1171
|
+
await exec(command, args);
|
|
1172
|
+
} catch (error) {
|
|
1173
|
+
const code = error.code;
|
|
1174
|
+
if (code === "ENOENT")
|
|
1175
|
+
throw new Error(`${command} is not installed`, { cause: error });
|
|
1176
|
+
if (typeof code === "number")
|
|
1177
|
+
throw new Error(`${command} exited with code ${code}`, { cause: error });
|
|
1178
|
+
throw error;
|
|
1179
|
+
}
|
|
1143
1180
|
}
|
|
1144
1181
|
|
|
1145
1182
|
// ../../packages/tonbo-cli-core/dist/src/api.js
|
|
@@ -1162,7 +1199,7 @@ var ManagementApi = class {
|
|
|
1162
1199
|
page(operationId, key, { limit, cursor } = {}, query = {}) {
|
|
1163
1200
|
return this.request(operationId, key, { query: { ...query, limit, cursor } });
|
|
1164
1201
|
}
|
|
1165
|
-
async request(operationId, key, { params = {}, query = {}, body, idempotencyKey, ifMatch }) {
|
|
1202
|
+
async request(operationId, key, { params = {}, query = {}, body, idempotencyKey, ifMatch }, seen) {
|
|
1166
1203
|
const operation = managementOperations[operationId];
|
|
1167
1204
|
const path2 = operation.path.replace(/\{([^}]+)\}/g, (_, name) => {
|
|
1168
1205
|
const value = params[name];
|
|
@@ -1190,7 +1227,12 @@ var ManagementApi = class {
|
|
|
1190
1227
|
const queryString = search.toString();
|
|
1191
1228
|
const url = `${this.apiOrigin}${managementApiBasePath}${path2}${queryString ? `?${queryString}` : ""}`;
|
|
1192
1229
|
try {
|
|
1193
|
-
|
|
1230
|
+
const fetcher = seen ? async (input, init) => {
|
|
1231
|
+
const response = await this.fetcher(input, init);
|
|
1232
|
+
seen(response);
|
|
1233
|
+
return response;
|
|
1234
|
+
} : this.fetcher;
|
|
1235
|
+
return await requestJson(fetcher, url, {
|
|
1194
1236
|
method: operation.method,
|
|
1195
1237
|
headers,
|
|
1196
1238
|
...body === void 0 ? {} : { body: JSON.stringify(body) }
|
|
@@ -1210,6 +1252,9 @@ var ManagementApi = class {
|
|
|
1210
1252
|
listMembers(key, page) {
|
|
1211
1253
|
return this.page("listMembers", key, page);
|
|
1212
1254
|
}
|
|
1255
|
+
listGitWorkspaces(key, page) {
|
|
1256
|
+
return this.page("listGitWorkspaces", key, page);
|
|
1257
|
+
}
|
|
1213
1258
|
updateMember(key, subject, change) {
|
|
1214
1259
|
return this.call("updateMember", key, { params: { subject }, body: change });
|
|
1215
1260
|
}
|
|
@@ -1334,13 +1379,18 @@ var ManagementApi = class {
|
|
|
1334
1379
|
sleepMachine(key, name) {
|
|
1335
1380
|
return this.call("sleepMachine", key, { params: { name } });
|
|
1336
1381
|
}
|
|
1337
|
-
/** The Machine being deleted
|
|
1338
|
-
*
|
|
1382
|
+
/** The Machine being deleted or the device forgotten, with its kind; the
|
|
1383
|
+
* Machine is null when the API answered 204, whose `X-Tonbo-Machine-Kind`
|
|
1384
|
+
* says whether a name was released (`managed`: its create never reached its
|
|
1385
|
+
* region) or a device was forgotten (`byoc`: its region held no row). */
|
|
1339
1386
|
async deleteMachine(key, name) {
|
|
1340
|
-
const
|
|
1341
|
-
|
|
1387
|
+
const headers = { kind: null };
|
|
1388
|
+
const answer = await this.request("deleteMachine", key, { params: { name } }, (response) => {
|
|
1389
|
+
headers.kind = response.headers.get("x-tonbo-machine-kind");
|
|
1342
1390
|
});
|
|
1343
|
-
|
|
1391
|
+
if (answer?.data)
|
|
1392
|
+
return { machine: answer.data, kind: answer.data.kind };
|
|
1393
|
+
return { machine: null, kind: headers.kind === "byoc" ? "byoc" : "managed" };
|
|
1344
1394
|
}
|
|
1345
1395
|
/** The whole answer: the shapes and the default one. */
|
|
1346
1396
|
listMachineShapes(key) {
|
|
@@ -1433,6 +1483,16 @@ var LOGIN_PROGRESS_MESSAGES = {
|
|
|
1433
1483
|
}
|
|
1434
1484
|
};
|
|
1435
1485
|
function reportLoginProgress(progress, event) {
|
|
1486
|
+
if (event.status === "authorization-url") {
|
|
1487
|
+
progress.note(`Open this URL to approve the login: ${event.url}`);
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
if (event.status === "failed") {
|
|
1491
|
+
const port = event.callbackPort;
|
|
1492
|
+
progress.fail(`Could not open a browser: ${event.reason.replace(/\.$/, "")}.`);
|
|
1493
|
+
progress.note(`Open the URL above in a browser that can reach 127.0.0.1:${port} on this machine. From another computer, forward that port first: \`ssh -L ${port}:127.0.0.1:${port} <this host>\` or your VM's port forwarding both work.`);
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1436
1496
|
const messages = LOGIN_PROGRESS_MESSAGES[event.step];
|
|
1437
1497
|
if (event.status === "started")
|
|
1438
1498
|
progress.start(messages.started);
|
|
@@ -1518,6 +1578,17 @@ var TerminalProgress = class {
|
|
|
1518
1578
|
fail(message = this.activeMessage) {
|
|
1519
1579
|
this.finish("!!", message);
|
|
1520
1580
|
}
|
|
1581
|
+
note(message) {
|
|
1582
|
+
if (this.stream.isTTY && this.activeMessage) {
|
|
1583
|
+
this.stream.write(`\r${"".padEnd(this.lastWidth)}\r${message}
|
|
1584
|
+
`);
|
|
1585
|
+
this.lastWidth = 0;
|
|
1586
|
+
this.render(`[${FRAMES[this.frame]}] ${this.activeMessage}`);
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
this.stream.write(`${message}
|
|
1590
|
+
`);
|
|
1591
|
+
}
|
|
1521
1592
|
finish(marker, message) {
|
|
1522
1593
|
if (this.timer)
|
|
1523
1594
|
clearInterval(this.timer);
|
|
@@ -1653,6 +1724,29 @@ async function memberListCommand(deps, options = {}) {
|
|
|
1653
1724
|
) : "No members."
|
|
1654
1725
|
);
|
|
1655
1726
|
}
|
|
1727
|
+
async function workspaceListCommand(deps, options = {}) {
|
|
1728
|
+
const page = await deps.api.listGitWorkspaces(await deps.auth.apiKey(), {
|
|
1729
|
+
limit: options.limit,
|
|
1730
|
+
cursor: options.cursor
|
|
1731
|
+
});
|
|
1732
|
+
printPage(
|
|
1733
|
+
deps,
|
|
1734
|
+
options.json,
|
|
1735
|
+
page,
|
|
1736
|
+
(workspaces) => workspaces.length ? table(
|
|
1737
|
+
["ID", "NAME", "DEFAULT", "REGION", "GIT ORIGIN", "STATE", "CREATED"],
|
|
1738
|
+
workspaces.map((workspace) => [
|
|
1739
|
+
workspace.id,
|
|
1740
|
+
workspace.name ?? "",
|
|
1741
|
+
workspace.default ? "yes" : "",
|
|
1742
|
+
workspace.region,
|
|
1743
|
+
`${workspace.git_origin}/${workspace.ws_hex}`,
|
|
1744
|
+
workspace.state,
|
|
1745
|
+
workspace.created_at
|
|
1746
|
+
])
|
|
1747
|
+
) : "No git workspaces."
|
|
1748
|
+
);
|
|
1749
|
+
}
|
|
1656
1750
|
async function resolveMemberSubject(deps, key, member) {
|
|
1657
1751
|
if (member.startsWith("agent.")) return member;
|
|
1658
1752
|
const subjects = /* @__PURE__ */ new Set();
|
|
@@ -2011,7 +2105,7 @@ async function databaseCreateCommand(deps, name, options = {}) {
|
|
|
2011
2105
|
}
|
|
2012
2106
|
function databaseNextStep(name) {
|
|
2013
2107
|
return [
|
|
2014
|
-
"Next: npm i @tonbo/sqlite-ursula@0.11.2",
|
|
2108
|
+
"Next: npm i --save-exact @tonbo/sqlite-ursula@0.11.2",
|
|
2015
2109
|
` open(${JSON.stringify(name)}) from "@tonbo/sqlite-ursula/tonbo"`
|
|
2016
2110
|
].join("\n");
|
|
2017
2111
|
}
|
|
@@ -2249,7 +2343,16 @@ var MACHINE_ERROR_HINTS = {
|
|
|
2249
2343
|
snapshot_would_be_discarded: "Pass --discard-snapshot to apply the change at the next boot, which then starts fresh.",
|
|
2250
2344
|
database_limit_reached: "Each Machine counts one database: delete a Machine or a database (`tonbo database list`) first.",
|
|
2251
2345
|
idempotency_conflict: "That idempotency key belongs to another request: leave out --idempotency-key to create a new Machine.",
|
|
2252
|
-
create_rate_limited: "Too many Machines were created in the last hour; try again later."
|
|
2346
|
+
create_rate_limited: "Too many Machines were created in the last hour; try again later.",
|
|
2347
|
+
image_pulling: "The Machine's image is still being pulled; the request was not run. Try again shortly; `tonbo machine show` reports the image's state.",
|
|
2348
|
+
image_failed: "The Machine's image could not be pulled; `tonbo machine show` says why. Fix the image, then name it again with `tonbo machine update --image`.",
|
|
2349
|
+
machine_kind_mismatch: "That Machine is one of your computers: `tonbo machine show` and `tonbo machine delete` serve it, and nothing else does."
|
|
2350
|
+
};
|
|
2351
|
+
var MACHINE_IMAGE_FAILURE_HINTS = {
|
|
2352
|
+
image_unavailable: "The registry no longer serves that image publicly, or served other bytes for it: publish it again, or use another image",
|
|
2353
|
+
image_platform_unsupported: "Machines run linux/arm64: publish the image for that platform (for example `docker buildx build --platform linux/arm64`)",
|
|
2354
|
+
image_too_large: "Make the image smaller (fewer or smaller layers)",
|
|
2355
|
+
image_unsupported: "Rebuild the image without the entry the message names"
|
|
2253
2356
|
};
|
|
2254
2357
|
function explainMachineError(error) {
|
|
2255
2358
|
if (!(error instanceof HttpError) || error.code === null) return error;
|
|
@@ -2280,8 +2383,27 @@ function machineState(machine) {
|
|
|
2280
2383
|
return machine.failure ? `${machine.state}: ${machine.failure.code}: ${machine.failure.message}` : machine.state;
|
|
2281
2384
|
}
|
|
2282
2385
|
function imageText(machine) {
|
|
2283
|
-
const { reference, digest } = machine.image;
|
|
2284
|
-
return `${reference} (
|
|
2386
|
+
const { reference, repository, digest, state } = machine.image;
|
|
2387
|
+
if (digest === null) return `${reference} (platform image)`;
|
|
2388
|
+
const pulled = repository === null ? digest : `${repository}@${digest}`;
|
|
2389
|
+
const pulling = state === "resolved" && (machine.state === "creating" || machine.state === "active");
|
|
2390
|
+
return `${reference} (${pulled}, ${pulling ? "pulling" : state})`;
|
|
2391
|
+
}
|
|
2392
|
+
function imageNextStep(machine, hint) {
|
|
2393
|
+
if (machine.state === "deleting") return null;
|
|
2394
|
+
if (machine.state === "failed")
|
|
2395
|
+
return `${hint}. A failed Machine does not change: delete it with \`tonbo machine delete\` and create another that names the fixed image.`;
|
|
2396
|
+
return `${hint}, then name it again with \`tonbo machine update --image\`.`;
|
|
2397
|
+
}
|
|
2398
|
+
function imageFailureFields(machine) {
|
|
2399
|
+
const { failure } = machine.image;
|
|
2400
|
+
if (!failure) return [];
|
|
2401
|
+
const hint = MACHINE_IMAGE_FAILURE_HINTS[failure.code];
|
|
2402
|
+
const next = hint ? imageNextStep(machine, hint) : null;
|
|
2403
|
+
return [
|
|
2404
|
+
["Image failure", `${failure.code}: ${failure.message}`],
|
|
2405
|
+
...next ? [["Next step", next]] : []
|
|
2406
|
+
];
|
|
2285
2407
|
}
|
|
2286
2408
|
function machineFields(machine) {
|
|
2287
2409
|
return fields([
|
|
@@ -2292,6 +2414,7 @@ function machineFields(machine) {
|
|
|
2292
2414
|
["Power", machine.status.power],
|
|
2293
2415
|
["Shape", machine.shape],
|
|
2294
2416
|
["Image", imageText(machine)],
|
|
2417
|
+
...imageFailureFields(machine),
|
|
2295
2418
|
["Command", machine.command ? machine.command.join(" ") : "the image's own"],
|
|
2296
2419
|
["Ports", machinePortsText(machine)],
|
|
2297
2420
|
["HTTPS", machine.hostnames.https],
|
|
@@ -2306,6 +2429,32 @@ function machineFields(machine) {
|
|
|
2306
2429
|
["Updated", machine.updated_at]
|
|
2307
2430
|
]);
|
|
2308
2431
|
}
|
|
2432
|
+
function keyStateText(machine) {
|
|
2433
|
+
const state = machine.byoc.key_state;
|
|
2434
|
+
if (state === "active") return state;
|
|
2435
|
+
return `${state}: this computer is signed out; \`yield login\` on it signs it in again`;
|
|
2436
|
+
}
|
|
2437
|
+
function deviceFields(machine) {
|
|
2438
|
+
const { byoc } = machine;
|
|
2439
|
+
return fields([
|
|
2440
|
+
["Name", machine.name],
|
|
2441
|
+
["ID", machine.id],
|
|
2442
|
+
["Kind", "byoc (one of your computers)"],
|
|
2443
|
+
["Region", machine.region],
|
|
2444
|
+
...byoc.hostname === void 0 ? [] : [["Hostname", byoc.hostname]],
|
|
2445
|
+
...byoc.device_id === void 0 ? [] : [["Device ID", byoc.device_id]],
|
|
2446
|
+
["System", `${byoc.os}/${byoc.arch}`],
|
|
2447
|
+
["Client", `${byoc.client} ${byoc.client_version}`],
|
|
2448
|
+
["Harnesses", byoc.harnesses.join(", ") || "none"],
|
|
2449
|
+
["Last seen", byoc.last_seen],
|
|
2450
|
+
["Key", keyStateText(machine)],
|
|
2451
|
+
["Created", machine.created_at],
|
|
2452
|
+
["Updated", machine.updated_at]
|
|
2453
|
+
]);
|
|
2454
|
+
}
|
|
2455
|
+
function anyMachineFields(machine) {
|
|
2456
|
+
return machine.kind === "byoc" ? deviceFields(machine) : machineFields(machine);
|
|
2457
|
+
}
|
|
2309
2458
|
function parsePorts(specs, current) {
|
|
2310
2459
|
const accesses = machinePortAccess;
|
|
2311
2460
|
return specs.map((spec) => {
|
|
@@ -2350,23 +2499,41 @@ async function machineListCommand(deps, options = {}) {
|
|
|
2350
2499
|
limit: options.limit,
|
|
2351
2500
|
cursor: options.cursor
|
|
2352
2501
|
});
|
|
2353
|
-
printPage(
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
(
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2502
|
+
printPage(deps, options.json, page, (machines) => {
|
|
2503
|
+
const managed = machines.filter((machine) => machine.kind !== "byoc");
|
|
2504
|
+
const devices = machines.filter((machine) => machine.kind === "byoc");
|
|
2505
|
+
const tables = [];
|
|
2506
|
+
if (managed.length)
|
|
2507
|
+
tables.push(
|
|
2508
|
+
table(
|
|
2509
|
+
["NAME", "REGION", "STATE", "POWER", "SHAPE", "IMAGE", "HTTPS"],
|
|
2510
|
+
managed.map((machine) => [
|
|
2511
|
+
machine.name,
|
|
2512
|
+
machine.region,
|
|
2513
|
+
machine.state,
|
|
2514
|
+
machine.status.power,
|
|
2515
|
+
machine.shape,
|
|
2516
|
+
machine.image.reference,
|
|
2517
|
+
machine.hostnames.https
|
|
2518
|
+
])
|
|
2519
|
+
)
|
|
2520
|
+
);
|
|
2521
|
+
if (devices.length)
|
|
2522
|
+
tables.push(
|
|
2523
|
+
table(
|
|
2524
|
+
["COMPUTER", "REGION", "SYSTEM", "HARNESSES", "LAST SEEN", "KEY"],
|
|
2525
|
+
devices.map((machine) => [
|
|
2526
|
+
machine.name,
|
|
2527
|
+
machine.region,
|
|
2528
|
+
`${machine.byoc.os}/${machine.byoc.arch}`,
|
|
2529
|
+
machine.byoc.harnesses.join(",") || "none",
|
|
2530
|
+
machine.byoc.last_seen,
|
|
2531
|
+
machine.byoc.key_state
|
|
2532
|
+
])
|
|
2533
|
+
)
|
|
2534
|
+
);
|
|
2535
|
+
return tables.length ? tables.join("\n\n") : "No Machines.";
|
|
2536
|
+
});
|
|
2370
2537
|
if (page.unavailable_regions.length)
|
|
2371
2538
|
deps.notice(
|
|
2372
2539
|
`Could not read the Machines in ${page.unavailable_regions.join(", ")}; they are missing from this list.`
|
|
@@ -2413,7 +2580,7 @@ async function machineCreateCommand(deps, name, command, options = {}) {
|
|
|
2413
2580
|
async function machineShowCommand(deps, name, options = {}) {
|
|
2414
2581
|
await machineCommand(async () => {
|
|
2415
2582
|
const machine = await deps.api.getMachine(await deps.auth.apiKey(), name);
|
|
2416
|
-
print(deps, options.json, machine,
|
|
2583
|
+
print(deps, options.json, machine, anyMachineFields(machine));
|
|
2417
2584
|
});
|
|
2418
2585
|
}
|
|
2419
2586
|
async function machineUpdateCommand(deps, name, command, options = {}) {
|
|
@@ -2423,7 +2590,12 @@ async function machineUpdateCommand(deps, name, command, options = {}) {
|
|
|
2423
2590
|
if (options.clearPorts && (options.port?.length || options.defaultPort !== void 0))
|
|
2424
2591
|
throw new Error("Give --port or --clear-ports, not both.");
|
|
2425
2592
|
const key = await deps.auth.apiKey();
|
|
2426
|
-
const
|
|
2593
|
+
const read = await deps.api.getMachine(key, name);
|
|
2594
|
+
if (read.kind === "byoc")
|
|
2595
|
+
throw new Error(
|
|
2596
|
+
`${name} is one of your computers; only \`tonbo machine show\` and \`tonbo machine delete\` serve it.`
|
|
2597
|
+
);
|
|
2598
|
+
const current = read;
|
|
2427
2599
|
const change = {
|
|
2428
2600
|
image: options.image,
|
|
2429
2601
|
shape: options.shape,
|
|
@@ -2478,24 +2650,42 @@ async function machineSleepCommand(deps, name, options = {}) {
|
|
|
2478
2650
|
}
|
|
2479
2651
|
async function machineDeleteCommand(deps, name, options = {}) {
|
|
2480
2652
|
await machineCommand(async () => {
|
|
2653
|
+
const key = await deps.auth.apiKey();
|
|
2481
2654
|
await confirm(
|
|
2482
2655
|
deps,
|
|
2483
2656
|
options.yes,
|
|
2484
2657
|
`delete Machine ${name}`,
|
|
2485
|
-
() =>
|
|
2486
|
-
`Delete Machine ${name} with its disk, database and logs? This cannot be undone. Type the Machine's name to confirm:`
|
|
2487
|
-
),
|
|
2658
|
+
() => deleteQuestion(deps, key, name),
|
|
2488
2659
|
name
|
|
2489
2660
|
);
|
|
2490
|
-
const machine = await deps.api.deleteMachine(
|
|
2491
|
-
print(
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
);
|
|
2661
|
+
const { machine, kind } = await deps.api.deleteMachine(key, name);
|
|
2662
|
+
print(deps, options.json, machine, deletedText(name, machine, kind));
|
|
2663
|
+
if (machine?.kind === "byoc" && machine.byoc.key_state === "active")
|
|
2664
|
+
deps.notice(
|
|
2665
|
+
`Its key is still active, so ${name} registers again at its next start. Run \`yield logout\` on it, or revoke its key with \`tonbo api-key revoke\`, then forget it again.`
|
|
2666
|
+
);
|
|
2497
2667
|
});
|
|
2498
2668
|
}
|
|
2669
|
+
async function deleteQuestion(deps, key, name) {
|
|
2670
|
+
let kind = null;
|
|
2671
|
+
try {
|
|
2672
|
+
kind = (await deps.api.getMachine(key, name)).kind;
|
|
2673
|
+
} catch (error) {
|
|
2674
|
+
if (!(error instanceof HttpError && error.status === 404)) throw error;
|
|
2675
|
+
}
|
|
2676
|
+
if (kind === "byoc")
|
|
2677
|
+
return `Forget your computer ${name}? It disappears from your Machines; its key is not revoked. Type its name to confirm:`;
|
|
2678
|
+
if (kind === "managed")
|
|
2679
|
+
return `Delete Machine ${name} with its disk, database and logs? This cannot be undone. Type the Machine's name to confirm:`;
|
|
2680
|
+
return `Delete Machine ${name}? Type its name to confirm:`;
|
|
2681
|
+
}
|
|
2682
|
+
function deletedText(name, machine, kind) {
|
|
2683
|
+
if (kind === "byoc")
|
|
2684
|
+
return `Forgot your computer ${machine?.name ?? name}; its name is free once the forget completes.`;
|
|
2685
|
+
if (machine)
|
|
2686
|
+
return `Machine ${machine.name} is being deleted; its name is free for a new Machine.`;
|
|
2687
|
+
return `Released the name ${name}; no Machine had been created under it.`;
|
|
2688
|
+
}
|
|
2499
2689
|
async function machineShapesCommand(deps, options = {}) {
|
|
2500
2690
|
await machineCommand(async () => {
|
|
2501
2691
|
const shapes = await deps.api.listMachineShapes(await deps.auth.apiKey());
|
|
@@ -2635,6 +2825,12 @@ function createProgram(dependencies = createDependencies) {
|
|
|
2635
2825
|
program.command("region").description("regions new resources can be created in").command("list").description("list the regions"),
|
|
2636
2826
|
"listRegions"
|
|
2637
2827
|
).action(async (options) => regionListCommand(dependencies(), options));
|
|
2828
|
+
operationOptions(
|
|
2829
|
+
program.command("workspace").description("the account's git workspaces").command("list").description("list the git workspaces, the default first"),
|
|
2830
|
+
"listGitWorkspaces"
|
|
2831
|
+
).action(
|
|
2832
|
+
async (options) => workspaceListCommand(dependencies(), options)
|
|
2833
|
+
);
|
|
2638
2834
|
const member = program.command("member").description("the people and Agents of the account");
|
|
2639
2835
|
operationOptions(
|
|
2640
2836
|
member.command("list").description("list the members and their roles"),
|
|
@@ -2859,7 +3055,7 @@ function createProgram(dependencies = createDependencies) {
|
|
|
2859
3055
|
);
|
|
2860
3056
|
const machine = program.command("machine").description("the account's Machines");
|
|
2861
3057
|
operationOptions(
|
|
2862
|
-
machine.command("list").description("list the Machines in every region"),
|
|
3058
|
+
machine.command("list").description("list the Machines in every region, and your computers in a personal account"),
|
|
2863
3059
|
"listMachines"
|
|
2864
3060
|
).action(
|
|
2865
3061
|
async (options) => machineListCommand(dependencies(), options)
|
|
@@ -2885,7 +3081,7 @@ function createProgram(dependencies = createDependencies) {
|
|
|
2885
3081
|
async (name, command, options) => machineCreateCommand(dependencies(), name, command, options)
|
|
2886
3082
|
);
|
|
2887
3083
|
operationOptions(
|
|
2888
|
-
machine.command("show <name>").description("show a Machine, its hostnames and its state"),
|
|
3084
|
+
machine.command("show <name>").description("show a Machine, its hostnames and its state, or one of your computers"),
|
|
2889
3085
|
"getMachine"
|
|
2890
3086
|
).action(
|
|
2891
3087
|
async (name, options) => machineShowCommand(dependencies(), name, options)
|
|
@@ -2912,7 +3108,9 @@ function createProgram(dependencies = createDependencies) {
|
|
|
2912
3108
|
async (name, options) => machineSleepCommand(dependencies(), name, options)
|
|
2913
3109
|
);
|
|
2914
3110
|
operationOptions(
|
|
2915
|
-
machine.command("delete <name>").description(
|
|
3111
|
+
machine.command("delete <name>").description(
|
|
3112
|
+
"delete a Machine with its disk, database and logs, or forget one of your computers"
|
|
3113
|
+
).option("--yes", "delete without asking; required when stdin is not a terminal"),
|
|
2916
3114
|
"deleteMachine"
|
|
2917
3115
|
).action(
|
|
2918
3116
|
async (name, options) => machineDeleteCommand(dependencies(), name, options)
|