@tonbo/cli 0.7.1 → 0.7.2
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 +2 -2
- package/dist/bin/tonbo.js +91 -16
- 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
|
|
|
@@ -132,7 +132,7 @@ tonbo database show notes
|
|
|
132
132
|
`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
133
|
|
|
134
134
|
```text
|
|
135
|
-
Next: npm i @tonbo/sqlite-ursula@0.11.2
|
|
135
|
+
Next: npm i --save-exact @tonbo/sqlite-ursula@0.11.2
|
|
136
136
|
open("notes") from "@tonbo/sqlite-ursula/tonbo"
|
|
137
137
|
```
|
|
138
138
|
|
package/dist/bin/tonbo.js
CHANGED
|
@@ -1025,7 +1025,7 @@ var AuthClient = class {
|
|
|
1025
1025
|
hostname: this.hostname,
|
|
1026
1026
|
...account ? { account } : {}
|
|
1027
1027
|
}).toString();
|
|
1028
|
-
return
|
|
1028
|
+
return authorization.toString();
|
|
1029
1029
|
}, progress);
|
|
1030
1030
|
progress({ status: "started", step: "key-redemption" });
|
|
1031
1031
|
const redeemed = await requestJson(this.fetcher, new URL("/api/cli/login/token", this.loginOrigin).toString(), {
|
|
@@ -1046,16 +1046,17 @@ var AuthClient = class {
|
|
|
1046
1046
|
progress({ status: "completed", step: "credential-store" });
|
|
1047
1047
|
return login;
|
|
1048
1048
|
}
|
|
1049
|
-
listenForCode(expectedState,
|
|
1049
|
+
listenForCode(expectedState, authorizationUrl, progress) {
|
|
1050
1050
|
return new Promise((resolve, reject) => {
|
|
1051
1051
|
let settled = false;
|
|
1052
1052
|
const sockets = /* @__PURE__ */ new Set();
|
|
1053
1053
|
let authorizationStarted = false;
|
|
1054
|
-
const startAuthorization = () => {
|
|
1054
|
+
const startAuthorization = (browserOpened = true) => {
|
|
1055
1055
|
if (authorizationStarted)
|
|
1056
1056
|
return;
|
|
1057
1057
|
authorizationStarted = true;
|
|
1058
|
-
|
|
1058
|
+
if (browserOpened)
|
|
1059
|
+
progress({ status: "completed", step: "browser" });
|
|
1059
1060
|
progress({ status: "started", step: "browser-authorization" });
|
|
1060
1061
|
};
|
|
1061
1062
|
const finish = (result, completedResponseSocket) => {
|
|
@@ -1119,10 +1120,20 @@ var AuthClient = class {
|
|
|
1119
1120
|
server.listen(0, LOOPBACK_HOST, () => {
|
|
1120
1121
|
const { port } = server.address();
|
|
1121
1122
|
progress({ status: "completed", step: "callback-server" });
|
|
1123
|
+
const url = authorizationUrl(`http://${LOOPBACK_HOST}:${port}/callback`);
|
|
1124
|
+
progress({ status: "authorization-url", step: "browser", url });
|
|
1122
1125
|
progress({ status: "started", step: "browser" });
|
|
1123
|
-
void
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
+
void Promise.resolve().then(() => this.browserOpener(url)).then(() => startAuthorization(), (error) => {
|
|
1127
|
+
if (settled || authorizationStarted)
|
|
1128
|
+
return;
|
|
1129
|
+
progress({
|
|
1130
|
+
status: "failed",
|
|
1131
|
+
step: "browser",
|
|
1132
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1133
|
+
callbackPort: port
|
|
1134
|
+
});
|
|
1135
|
+
startAuthorization(false);
|
|
1136
|
+
});
|
|
1126
1137
|
});
|
|
1127
1138
|
});
|
|
1128
1139
|
}
|
|
@@ -1134,12 +1145,27 @@ function respond(response, outcome, finished) {
|
|
|
1134
1145
|
response.writeHead(page.status, { ...page.headers, connection: "close" });
|
|
1135
1146
|
response.end(page.body);
|
|
1136
1147
|
}
|
|
1148
|
+
function browserCommand(url, platform = process.platform, env = process.env) {
|
|
1149
|
+
if (platform === "darwin")
|
|
1150
|
+
return ["open", [url]];
|
|
1151
|
+
if (platform === "win32")
|
|
1152
|
+
return ["rundll32", ["url.dll,FileProtocolHandler", url]];
|
|
1153
|
+
if (!env.DISPLAY && !env.WAYLAND_DISPLAY)
|
|
1154
|
+
throw new Error("there is no graphical session (DISPLAY and WAYLAND_DISPLAY are unset)");
|
|
1155
|
+
return ["xdg-open", [url]];
|
|
1156
|
+
}
|
|
1137
1157
|
async function openBrowser(url) {
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1158
|
+
const [command, args] = browserCommand(url);
|
|
1159
|
+
try {
|
|
1160
|
+
await exec(command, args);
|
|
1161
|
+
} catch (error) {
|
|
1162
|
+
const code = error.code;
|
|
1163
|
+
if (code === "ENOENT")
|
|
1164
|
+
throw new Error(`${command} is not installed`, { cause: error });
|
|
1165
|
+
if (typeof code === "number")
|
|
1166
|
+
throw new Error(`${command} exited with code ${code}`, { cause: error });
|
|
1167
|
+
throw error;
|
|
1168
|
+
}
|
|
1143
1169
|
}
|
|
1144
1170
|
|
|
1145
1171
|
// ../../packages/tonbo-cli-core/dist/src/api.js
|
|
@@ -1433,6 +1459,16 @@ var LOGIN_PROGRESS_MESSAGES = {
|
|
|
1433
1459
|
}
|
|
1434
1460
|
};
|
|
1435
1461
|
function reportLoginProgress(progress, event) {
|
|
1462
|
+
if (event.status === "authorization-url") {
|
|
1463
|
+
progress.note(`Open this URL to approve the login: ${event.url}`);
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
if (event.status === "failed") {
|
|
1467
|
+
const port = event.callbackPort;
|
|
1468
|
+
progress.fail(`Could not open a browser: ${event.reason.replace(/\.$/, "")}.`);
|
|
1469
|
+
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.`);
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1436
1472
|
const messages = LOGIN_PROGRESS_MESSAGES[event.step];
|
|
1437
1473
|
if (event.status === "started")
|
|
1438
1474
|
progress.start(messages.started);
|
|
@@ -1518,6 +1554,17 @@ var TerminalProgress = class {
|
|
|
1518
1554
|
fail(message = this.activeMessage) {
|
|
1519
1555
|
this.finish("!!", message);
|
|
1520
1556
|
}
|
|
1557
|
+
note(message) {
|
|
1558
|
+
if (this.stream.isTTY && this.activeMessage) {
|
|
1559
|
+
this.stream.write(`\r${"".padEnd(this.lastWidth)}\r${message}
|
|
1560
|
+
`);
|
|
1561
|
+
this.lastWidth = 0;
|
|
1562
|
+
this.render(`[${FRAMES[this.frame]}] ${this.activeMessage}`);
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
this.stream.write(`${message}
|
|
1566
|
+
`);
|
|
1567
|
+
}
|
|
1521
1568
|
finish(marker, message) {
|
|
1522
1569
|
if (this.timer)
|
|
1523
1570
|
clearInterval(this.timer);
|
|
@@ -2011,7 +2058,7 @@ async function databaseCreateCommand(deps, name, options = {}) {
|
|
|
2011
2058
|
}
|
|
2012
2059
|
function databaseNextStep(name) {
|
|
2013
2060
|
return [
|
|
2014
|
-
"Next: npm i @tonbo/sqlite-ursula@0.11.2",
|
|
2061
|
+
"Next: npm i --save-exact @tonbo/sqlite-ursula@0.11.2",
|
|
2015
2062
|
` open(${JSON.stringify(name)}) from "@tonbo/sqlite-ursula/tonbo"`
|
|
2016
2063
|
].join("\n");
|
|
2017
2064
|
}
|
|
@@ -2249,7 +2296,15 @@ var MACHINE_ERROR_HINTS = {
|
|
|
2249
2296
|
snapshot_would_be_discarded: "Pass --discard-snapshot to apply the change at the next boot, which then starts fresh.",
|
|
2250
2297
|
database_limit_reached: "Each Machine counts one database: delete a Machine or a database (`tonbo database list`) first.",
|
|
2251
2298
|
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."
|
|
2299
|
+
create_rate_limited: "Too many Machines were created in the last hour; try again later.",
|
|
2300
|
+
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.",
|
|
2301
|
+
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`."
|
|
2302
|
+
};
|
|
2303
|
+
var MACHINE_IMAGE_FAILURE_HINTS = {
|
|
2304
|
+
image_unavailable: "The registry no longer serves that image publicly, or served other bytes for it: publish it again, or use another image",
|
|
2305
|
+
image_platform_unsupported: "Machines run linux/arm64: publish the image for that platform (for example `docker buildx build --platform linux/arm64`)",
|
|
2306
|
+
image_too_large: "Make the image smaller (fewer or smaller layers)",
|
|
2307
|
+
image_unsupported: "Rebuild the image without the entry the message names"
|
|
2253
2308
|
};
|
|
2254
2309
|
function explainMachineError(error) {
|
|
2255
2310
|
if (!(error instanceof HttpError) || error.code === null) return error;
|
|
@@ -2280,8 +2335,27 @@ function machineState(machine) {
|
|
|
2280
2335
|
return machine.failure ? `${machine.state}: ${machine.failure.code}: ${machine.failure.message}` : machine.state;
|
|
2281
2336
|
}
|
|
2282
2337
|
function imageText(machine) {
|
|
2283
|
-
const { reference, digest } = machine.image;
|
|
2284
|
-
return `${reference} (
|
|
2338
|
+
const { reference, repository, digest, state } = machine.image;
|
|
2339
|
+
if (digest === null) return `${reference} (platform image)`;
|
|
2340
|
+
const pulled = repository === null ? digest : `${repository}@${digest}`;
|
|
2341
|
+
const pulling = state === "resolved" && (machine.state === "creating" || machine.state === "active");
|
|
2342
|
+
return `${reference} (${pulled}, ${pulling ? "pulling" : state})`;
|
|
2343
|
+
}
|
|
2344
|
+
function imageNextStep(machine, hint) {
|
|
2345
|
+
if (machine.state === "deleting") return null;
|
|
2346
|
+
if (machine.state === "failed")
|
|
2347
|
+
return `${hint}. A failed Machine does not change: delete it with \`tonbo machine delete\` and create another that names the fixed image.`;
|
|
2348
|
+
return `${hint}, then name it again with \`tonbo machine update --image\`.`;
|
|
2349
|
+
}
|
|
2350
|
+
function imageFailureFields(machine) {
|
|
2351
|
+
const { failure } = machine.image;
|
|
2352
|
+
if (!failure) return [];
|
|
2353
|
+
const hint = MACHINE_IMAGE_FAILURE_HINTS[failure.code];
|
|
2354
|
+
const next = hint ? imageNextStep(machine, hint) : null;
|
|
2355
|
+
return [
|
|
2356
|
+
["Image failure", `${failure.code}: ${failure.message}`],
|
|
2357
|
+
...next ? [["Next step", next]] : []
|
|
2358
|
+
];
|
|
2285
2359
|
}
|
|
2286
2360
|
function machineFields(machine) {
|
|
2287
2361
|
return fields([
|
|
@@ -2292,6 +2366,7 @@ function machineFields(machine) {
|
|
|
2292
2366
|
["Power", machine.status.power],
|
|
2293
2367
|
["Shape", machine.shape],
|
|
2294
2368
|
["Image", imageText(machine)],
|
|
2369
|
+
...imageFailureFields(machine),
|
|
2295
2370
|
["Command", machine.command ? machine.command.join(" ") : "the image's own"],
|
|
2296
2371
|
["Ports", machinePortsText(machine)],
|
|
2297
2372
|
["HTTPS", machine.hostnames.https],
|