@tonbo/cli 0.7.0 → 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 +33 -13
- package/dist/bin/tonbo.js +475 -68
- 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
|
|
|
@@ -37,7 +37,7 @@ Every command other than `login` and `logout` calls one operation of the [Manage
|
|
|
37
37
|
| `tonbo member update <member> [--role <role>] [--name <name>]` | `updateMember` | `PATCH /v1/members/{subject}` |
|
|
38
38
|
| `tonbo agent create <name> [--role <role>] [--region <region>] [--idempotency-key <key>]` | `createAgent` | `POST /v1/agents` |
|
|
39
39
|
| `tonbo api-key list [--limit <n>] [--cursor <c>]` | `listApiKeys` | `GET /v1/api-keys` |
|
|
40
|
-
| `tonbo api-key create <name> [--member <member>] [--read-only] [--bucket <name>]… [--database <name>]… [--database-prefix <prefix>]… [--expires-in <period>]` | `createApiKey` | `POST /v1/api-keys` |
|
|
40
|
+
| `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` |
|
|
41
41
|
| `tonbo api-key show <key-id>` | `getApiKey` | `GET /v1/api-keys/{key_id}` |
|
|
42
42
|
| `tonbo api-key revoke <key-id> [--yes]` | `revokeApiKey` | `DELETE /v1/api-keys/{key_id}` |
|
|
43
43
|
| `tonbo bucket list [--limit <n>] [--cursor <c>]` | `listBuckets` | `GET /v1/buckets` |
|
|
@@ -46,11 +46,17 @@ Every command other than `login` and `logout` calls one operation of the [Manage
|
|
|
46
46
|
| `tonbo bucket update <name> (--public \| --private)` | `updateBucket` | `PATCH /v1/buckets/{name}` |
|
|
47
47
|
| `tonbo bucket delete <name> [--yes]` | `deleteBucket` | `DELETE /v1/buckets/{name}` |
|
|
48
48
|
| `tonbo database list [--prefix <prefix>] [--limit <n>] [--cursor <c>]` | `listDatabases` | `GET /v1/databases` |
|
|
49
|
-
| `tonbo database create <name> [--region <region>] [--idempotency-key <key>]` | `createDatabase` | `POST /v1/databases` |
|
|
49
|
+
| `tonbo database create <name> [--region <region>] [--expires-at <time> \| --expires-in <duration>] [--idempotency-key <key>]` | `createDatabase` | `POST /v1/databases` |
|
|
50
50
|
| `tonbo database show <name>` | `getDatabase` | `GET /v1/databases/{name}` |
|
|
51
51
|
| `tonbo database update <name> (--public \| --private)` | `updateDatabase` | `PATCH /v1/databases/{name}` |
|
|
52
52
|
| `tonbo database delete <name> [--yes]` | `deleteDatabase` | `DELETE /v1/databases/{name}` |
|
|
53
53
|
| `tonbo database usage [--period <YYYY-MM>] [--sort list_value\|name] [--prefix <prefix>] [--names <a,b>] [--limit <n>] [--cursor <c>]` | `listDatabaseUsage` | `GET /v1/usage/databases` |
|
|
54
|
+
| `tonbo stream list [--prefix <prefix>] [--limit <n>] [--cursor <c>]` | `listStreams` | `GET /v1/streams` |
|
|
55
|
+
| `tonbo stream create <name> [--region <region>] [--content-type <type>] [--expires-at <time> \| --expires-in <duration>] [--idempotency-key <key>]` | `createStream` | `POST /v1/streams` |
|
|
56
|
+
| `tonbo stream show <name>` | `getStream` | `GET /v1/streams/{name}` |
|
|
57
|
+
| `tonbo stream update <name> (--public \| --private)` | `updateStream` | `PATCH /v1/streams/{name}` |
|
|
58
|
+
| `tonbo stream delete <name> [--yes]` | `deleteStream` | `DELETE /v1/streams/{name}` |
|
|
59
|
+
| `tonbo stream usage [--period <YYYY-MM>] [--sort list_value\|name] [--prefix <prefix>] [--names <a,b>] [--limit <n>] [--cursor <c>]` | `listStreamUsage` | `GET /v1/usage/streams` |
|
|
54
60
|
| `tonbo usage show [--period <YYYY-MM>]` | `getUsage` | `GET /v1/usage` |
|
|
55
61
|
| `tonbo machine list [--limit <n>] [--cursor <c>]` | `listMachines` | `GET /v1/machines` |
|
|
56
62
|
| `tonbo machine create <name> --image <reference> [--shape <shape>] [--region <region>] [--port <port[:access]>]… [--default-port <port>] [--env <KEY=VALUE>]… [--sleep-after <seconds>] [--idle-action <action>] [--stop-grace <seconds>] [--idempotency-key <key>] [-- <command>…]` | `createMachine` | `POST /v1/machines` |
|
|
@@ -62,17 +68,17 @@ Every command other than `login` and `logout` calls one operation of the [Manage
|
|
|
62
68
|
|
|
63
69
|
### Output and errors
|
|
64
70
|
|
|
65
|
-
Every command prints an aligned table or a list of fields for people. With `--json` it prints the answer's `data` exactly as the API returned it, one JSON value on stdout and nothing else there. A list prints one page; when there is another, the CLI writes `More results: run again with --cursor <cursor>` to stderr, so `--json` output stays the API's `data`. `database usage --json` and `machine shapes --json` print the whole answer, since the usage's `other` row, checkpoint and prices, and the default shape, belong with the rows. `--limit` asks for up to that many items (the API's default is 50, its maximum 200).
|
|
71
|
+
Every command prints an aligned table or a list of fields for people. With `--json` it prints the answer's `data` exactly as the API returned it, one JSON value on stdout and nothing else there. A list prints one page; when there is another, the CLI writes `More results: run again with --cursor <cursor>` to stderr, so `--json` output stays the API's `data`. `database usage --json`, `stream usage --json` and `machine shapes --json` print the whole answer, since the usage's `other` row, checkpoint and prices, and the default shape, belong with the rows. `--limit` asks for up to that many items (the API's default is 50, its maximum 200).
|
|
66
72
|
|
|
67
73
|
A refusal prints the problem's detail, the fields a validation error names, and the request's `X-Request-Id` to stderr, and the command exits with status 1. An invalid or expired key adds `Run tonbo login`.
|
|
68
74
|
|
|
69
75
|
### Retried creations
|
|
70
76
|
|
|
71
|
-
`agent create`, `bucket create`, `database create` and `machine create` send an `Idempotency-Key`, a new UUID unless `--idempotency-key` names one. When a creation is not confirmed (the network failed or the server answered 5xx), the CLI prints its key; retrying with that key and the same arguments returns what the first request created instead of creating a second one. `api-key create` has no idempotency key, because a key's secret is never stored and a replay could not return it; a live key's name is unique, so a blind retry is refused with 409 rather than creating a second key.
|
|
77
|
+
`agent create`, `bucket create`, `database create`, `stream create` and `machine create` send an `Idempotency-Key`, a new UUID unless `--idempotency-key` names one. When a creation is not confirmed (the network failed or the server answered 5xx), the CLI prints its key; retrying with that key and the same arguments returns what the first request created instead of creating a second one. `api-key create` has no idempotency key, because a key's secret is never stored and a replay could not return it; a live key's name is unique, so a blind retry is refused with 409 rather than creating a second key.
|
|
72
78
|
|
|
73
79
|
### Destructive commands
|
|
74
80
|
|
|
75
|
-
`api-key revoke`, `bucket delete`, `database delete` and `machine delete` ask before they act when stdin and stderr are a terminal: a revocation is confirmed by typing `yes`, a deletion by typing the bucket's, database's or Machine's name, as in the console. Without a terminal they refuse unless `--yes` is given.
|
|
81
|
+
`api-key revoke`, `bucket delete`, `database delete`, `stream delete` and `machine delete` ask before they act when stdin and stderr are a terminal: a revocation is confirmed by typing `yes`, a deletion by typing the bucket's, database's, stream's or Machine's name, as in the console. Without a terminal they refuse unless `--yes` is given.
|
|
76
82
|
|
|
77
83
|
## Account
|
|
78
84
|
|
|
@@ -103,7 +109,7 @@ tonbo api-key create ci-reader --member pi-live --read-only --bucket events --ex
|
|
|
103
109
|
tonbo api-key revoke 70000000-0000-4000-8000-000000000007
|
|
104
110
|
```
|
|
105
111
|
|
|
106
|
-
`api-key list` shows your keys, or every key of the account for an owner or admin, with the last four characters of each secret. `api-key create` creates a personal key for you, or with `--member` a key of an Agent (its subject, id or name; owners and admins only). `--read-only`, and either `--bucket` or `--database` and `--
|
|
112
|
+
`api-key list` shows your keys, or every key of the account for an owner or admin, with the last four characters of each secret. `api-key create` creates a personal key for you, or with `--member` a key of an Agent (its subject, id or name; owners and admins only). `--read-only`, and either `--bucket` or any of `--database`, `--database-prefix`, `--stream` and `--stream-prefix` (repeat each for several), narrow the key for its whole life; a key narrowed to databases and streams reaches both, and `api-key list` and `api-key show` print what each key reaches of each; `--expires-in` is one of `15_days`, `1_month`, `6_months`, `1_year` or `never` (the default). The secret is printed once, in that answer only, with a warning on stderr: store it then, because no command can show it again. Without `--json`, the answer also carries a ready-to-paste `TONBO_API_KEY=<secret>` line on stdout, beside the secret, for your application's environment (a `.env` file or a secret store), and stderr notes that `TONBO_API_KEY` in your own shell overrides your `tonbo login`, for this CLI too. stderr never carries the secret, and `--json` prints only the answer. `api-key show` prints one key, and `api-key revoke` revokes it and every token minted from it.
|
|
107
113
|
|
|
108
114
|
## Buckets
|
|
109
115
|
|
|
@@ -119,32 +125,46 @@ tonbo bucket delete events
|
|
|
119
125
|
|
|
120
126
|
```console
|
|
121
127
|
tonbo database create notes
|
|
128
|
+
tonbo database create scratch --expires-in 24h
|
|
122
129
|
tonbo database show notes
|
|
123
130
|
```
|
|
124
131
|
|
|
125
|
-
`database create` creates a database in `--region`, or in the default region, and prints its fields with its URL. Without `--json` it then writes the next step to stderr, opening the database by name from code:
|
|
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:
|
|
126
133
|
|
|
127
134
|
```text
|
|
128
|
-
Next: npm i @tonbo/sqlite-ursula
|
|
135
|
+
Next: npm i --save-exact @tonbo/sqlite-ursula@0.11.2
|
|
129
136
|
open("notes") from "@tonbo/sqlite-ursula/tonbo"
|
|
130
137
|
```
|
|
131
138
|
|
|
132
139
|
`open("notes")` resolves the database through `GET /v1/databases/notes`. It takes its key from, in order, its `apiKey` option, then `TONBO_API_KEY`, then the `tonbo login` credentials file (`key_sources` in `contracts/cli/credentials-v1.json`). A key narrowed to databases reads the databases it reaches there, so an application can be given a narrowed key.
|
|
133
140
|
|
|
141
|
+
## Streams
|
|
142
|
+
|
|
143
|
+
```console
|
|
144
|
+
tonbo stream create events --content-type application/json
|
|
145
|
+
tonbo stream create run-42 --content-type application/json --expires-in 24h
|
|
146
|
+
tonbo stream show events
|
|
147
|
+
tonbo api-key create run-reader --read-only --stream-prefix run-
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`stream create` creates a raw durable stream in `--region`, or in the default region, with the content type every append must send (`--content-type`, `application/octet-stream` by default; one lowercase `type/subtype` without parameters), and prints its fields with its URL on the region's Streams origin. A stream and a database may share a name; each kind has its own names, limit and usage (`stream usage`, whose `other` holds the account's databases and everything else, as `database usage`'s holds its streams). Without `--json` it then writes the next step to stderr: append with `POST` to the URL and read with `GET` from `?offset=-1`, with any Durable Streams client. `--expires-at` (an RFC 3339 time, at least 60 seconds after the creation) or `--expires-in` (`90s` or more, such as `30m`, `24h` or `7d`, counted from now) gives the stream an expiry fixed for its life: once it passes, Tonbo deletes the stream within a minute, as `stream delete` would, and its name is free again; `stream show` prints it, and without either the stream never expires. `--expires-in` is counted from when the command runs, so it never goes with `--idempotency-key`: an unconfirmed creation with an expiry prints a retry hint naming its key and the absolute time it sent, and the retry passes that time with `--expires-at` (the same for `database create`). A key narrowed to streams (`api-key create --stream` or `--stream-prefix`) reaches only those streams; a key narrowed to buckets, or by name to databases alone, reaches no stream.
|
|
151
|
+
|
|
134
152
|
## Machines
|
|
135
153
|
|
|
136
154
|
```console
|
|
137
155
|
tonbo machine shapes
|
|
138
|
-
tonbo machine create web --image
|
|
139
|
-
tonbo machine create worker --image
|
|
156
|
+
tonbo machine create web --image tonbo.dev/platform --port 8080:public --env MODE=prod
|
|
157
|
+
tonbo machine create worker --image tonbo.dev/platform -- sh -c 'while true; do date; sleep 60; done'
|
|
140
158
|
tonbo machine update web --env MODE=staging --sleep-after 300
|
|
141
159
|
tonbo machine sleep web
|
|
142
160
|
tonbo machine delete web
|
|
143
161
|
```
|
|
144
162
|
|
|
145
|
-
A Machine is a VM booted from
|
|
163
|
+
A Machine is a VM booted from an image, in one region, with its own database (`contracts/machines/machine-v2.json`). Machines open by invitation: until a region is open for them, every `tonbo machine` command is refused with `region_unavailable`, and the CLI says that nothing was created or changed.
|
|
164
|
+
|
|
165
|
+
The image is `tonbo.dev/platform`, the platform image each node release ships, or, once registry images are offered, a public OCI image with a linux/arm64 variant. Until then any other reference is refused with `image_not_offered`, before anything is created or changed; `machine show` prints the platform image as `tonbo.dev/platform (platform image)`.
|
|
146
166
|
|
|
147
|
-
`machine create` resolves
|
|
167
|
+
`machine create` resolves a registry image to its linux/arm64 digest (the platform image resolves to none) and reserves the Machine's name and its hostnames: `https://<name>-<account>.<region>.tonbo.sh` for the default port, and `<name>.<account>@<region>.tonbo.sh` for SSH. Without `--shape` it takes the shape `machine shapes` marks as the default. `--port <port>` serves a private port (a member's token or SSH) and `--port <port>:public` an open one, which needs the `machines.public` capability; the first port is served at the hostname unless `--default-port` names another. A command after `--` replaces the image's CMD. The answer is the Machine while it is `creating`; `machine show` reports it `active` when it is ready.
|
|
148
168
|
|
|
149
169
|
`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.
|
|
150
170
|
|
package/dist/bin/tonbo.js
CHANGED
|
@@ -617,6 +617,71 @@ var managementOperations = {
|
|
|
617
617
|
"query": [],
|
|
618
618
|
"idempotencyKey": false,
|
|
619
619
|
"cli": "tonbo machine shapes"
|
|
620
|
+
},
|
|
621
|
+
"listStreams": {
|
|
622
|
+
"method": "GET",
|
|
623
|
+
"path": "/streams",
|
|
624
|
+
"pathParams": [],
|
|
625
|
+
"query": [
|
|
626
|
+
"prefix",
|
|
627
|
+
"cursor",
|
|
628
|
+
"limit"
|
|
629
|
+
],
|
|
630
|
+
"idempotencyKey": false,
|
|
631
|
+
"cli": "tonbo stream list"
|
|
632
|
+
},
|
|
633
|
+
"createStream": {
|
|
634
|
+
"method": "POST",
|
|
635
|
+
"path": "/streams",
|
|
636
|
+
"pathParams": [],
|
|
637
|
+
"query": [],
|
|
638
|
+
"idempotencyKey": true,
|
|
639
|
+
"cli": "tonbo stream create"
|
|
640
|
+
},
|
|
641
|
+
"getStream": {
|
|
642
|
+
"method": "GET",
|
|
643
|
+
"path": "/streams/{name}",
|
|
644
|
+
"pathParams": [
|
|
645
|
+
"name"
|
|
646
|
+
],
|
|
647
|
+
"query": [],
|
|
648
|
+
"idempotencyKey": false,
|
|
649
|
+
"cli": "tonbo stream show"
|
|
650
|
+
},
|
|
651
|
+
"updateStream": {
|
|
652
|
+
"method": "PATCH",
|
|
653
|
+
"path": "/streams/{name}",
|
|
654
|
+
"pathParams": [
|
|
655
|
+
"name"
|
|
656
|
+
],
|
|
657
|
+
"query": [],
|
|
658
|
+
"idempotencyKey": false,
|
|
659
|
+
"cli": "tonbo stream update"
|
|
660
|
+
},
|
|
661
|
+
"deleteStream": {
|
|
662
|
+
"method": "DELETE",
|
|
663
|
+
"path": "/streams/{name}",
|
|
664
|
+
"pathParams": [
|
|
665
|
+
"name"
|
|
666
|
+
],
|
|
667
|
+
"query": [],
|
|
668
|
+
"idempotencyKey": false,
|
|
669
|
+
"cli": "tonbo stream delete"
|
|
670
|
+
},
|
|
671
|
+
"listStreamUsage": {
|
|
672
|
+
"method": "GET",
|
|
673
|
+
"path": "/usage/streams",
|
|
674
|
+
"pathParams": [],
|
|
675
|
+
"query": [
|
|
676
|
+
"period",
|
|
677
|
+
"sort",
|
|
678
|
+
"prefix",
|
|
679
|
+
"names",
|
|
680
|
+
"cursor",
|
|
681
|
+
"limit"
|
|
682
|
+
],
|
|
683
|
+
"idempotencyKey": false,
|
|
684
|
+
"cli": "tonbo stream usage"
|
|
620
685
|
}
|
|
621
686
|
};
|
|
622
687
|
var accountRoles = [
|
|
@@ -960,7 +1025,7 @@ var AuthClient = class {
|
|
|
960
1025
|
hostname: this.hostname,
|
|
961
1026
|
...account ? { account } : {}
|
|
962
1027
|
}).toString();
|
|
963
|
-
return
|
|
1028
|
+
return authorization.toString();
|
|
964
1029
|
}, progress);
|
|
965
1030
|
progress({ status: "started", step: "key-redemption" });
|
|
966
1031
|
const redeemed = await requestJson(this.fetcher, new URL("/api/cli/login/token", this.loginOrigin).toString(), {
|
|
@@ -981,16 +1046,17 @@ var AuthClient = class {
|
|
|
981
1046
|
progress({ status: "completed", step: "credential-store" });
|
|
982
1047
|
return login;
|
|
983
1048
|
}
|
|
984
|
-
listenForCode(expectedState,
|
|
1049
|
+
listenForCode(expectedState, authorizationUrl, progress) {
|
|
985
1050
|
return new Promise((resolve, reject) => {
|
|
986
1051
|
let settled = false;
|
|
987
1052
|
const sockets = /* @__PURE__ */ new Set();
|
|
988
1053
|
let authorizationStarted = false;
|
|
989
|
-
const startAuthorization = () => {
|
|
1054
|
+
const startAuthorization = (browserOpened = true) => {
|
|
990
1055
|
if (authorizationStarted)
|
|
991
1056
|
return;
|
|
992
1057
|
authorizationStarted = true;
|
|
993
|
-
|
|
1058
|
+
if (browserOpened)
|
|
1059
|
+
progress({ status: "completed", step: "browser" });
|
|
994
1060
|
progress({ status: "started", step: "browser-authorization" });
|
|
995
1061
|
};
|
|
996
1062
|
const finish = (result, completedResponseSocket) => {
|
|
@@ -1054,10 +1120,20 @@ var AuthClient = class {
|
|
|
1054
1120
|
server.listen(0, LOOPBACK_HOST, () => {
|
|
1055
1121
|
const { port } = server.address();
|
|
1056
1122
|
progress({ status: "completed", step: "callback-server" });
|
|
1123
|
+
const url = authorizationUrl(`http://${LOOPBACK_HOST}:${port}/callback`);
|
|
1124
|
+
progress({ status: "authorization-url", step: "browser", url });
|
|
1057
1125
|
progress({ status: "started", step: "browser" });
|
|
1058
|
-
void
|
|
1059
|
-
|
|
1060
|
-
|
|
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
|
+
});
|
|
1061
1137
|
});
|
|
1062
1138
|
});
|
|
1063
1139
|
}
|
|
@@ -1069,12 +1145,27 @@ function respond(response, outcome, finished) {
|
|
|
1069
1145
|
response.writeHead(page.status, { ...page.headers, connection: "close" });
|
|
1070
1146
|
response.end(page.body);
|
|
1071
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
|
+
}
|
|
1072
1157
|
async function openBrowser(url) {
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
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
|
+
}
|
|
1078
1169
|
}
|
|
1079
1170
|
|
|
1080
1171
|
// ../../packages/tonbo-cli-core/dist/src/api.js
|
|
@@ -1201,7 +1292,7 @@ var ManagementApi = class {
|
|
|
1201
1292
|
return this.call("createDatabase", key, {
|
|
1202
1293
|
body: withoutUndefined(request),
|
|
1203
1294
|
idempotencyKey
|
|
1204
|
-
}).catch(unconfirmed("Database creation", idempotencyKey, "the same name and region"));
|
|
1295
|
+
}).catch(unconfirmed("Database creation", idempotencyKey, request.expires_at ? `the same name, region and --expires-at ${request.expires_at}` : "the same name and region"));
|
|
1205
1296
|
}
|
|
1206
1297
|
getDatabase(key, name) {
|
|
1207
1298
|
return this.call("getDatabase", key, { params: { name } });
|
|
@@ -1216,6 +1307,30 @@ var ManagementApi = class {
|
|
|
1216
1307
|
listDatabaseUsage(key, page, query = {}) {
|
|
1217
1308
|
return this.page("listDatabaseUsage", key, page, query);
|
|
1218
1309
|
}
|
|
1310
|
+
listStreams(key, page, prefix) {
|
|
1311
|
+
return this.page("listStreams", key, page, { prefix });
|
|
1312
|
+
}
|
|
1313
|
+
/** A replay with the same idempotency key returns the stream the first
|
|
1314
|
+
* request created. */
|
|
1315
|
+
createStream(key, request, idempotencyKey) {
|
|
1316
|
+
return this.call("createStream", key, {
|
|
1317
|
+
body: withoutUndefined(request),
|
|
1318
|
+
idempotencyKey
|
|
1319
|
+
}).catch(unconfirmed("Stream creation", idempotencyKey, request.expires_at ? `the same name, region, content type and --expires-at ${request.expires_at}` : "the same name, region and content type"));
|
|
1320
|
+
}
|
|
1321
|
+
getStream(key, name) {
|
|
1322
|
+
return this.call("getStream", key, { params: { name } });
|
|
1323
|
+
}
|
|
1324
|
+
updateStream(key, name, change) {
|
|
1325
|
+
return this.call("updateStream", key, { params: { name }, body: change });
|
|
1326
|
+
}
|
|
1327
|
+
deleteStream(key, name) {
|
|
1328
|
+
return this.call("deleteStream", key, { params: { name } });
|
|
1329
|
+
}
|
|
1330
|
+
/** The whole usage page: its rows, `other`, prices and checkpoint. */
|
|
1331
|
+
listStreamUsage(key, page, query = {}) {
|
|
1332
|
+
return this.page("listStreamUsage", key, page, query);
|
|
1333
|
+
}
|
|
1219
1334
|
getUsage(key, period) {
|
|
1220
1335
|
return this.call("getUsage", key, { query: { period } });
|
|
1221
1336
|
}
|
|
@@ -1344,6 +1459,16 @@ var LOGIN_PROGRESS_MESSAGES = {
|
|
|
1344
1459
|
}
|
|
1345
1460
|
};
|
|
1346
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
|
+
}
|
|
1347
1472
|
const messages = LOGIN_PROGRESS_MESSAGES[event.step];
|
|
1348
1473
|
if (event.status === "started")
|
|
1349
1474
|
progress.start(messages.started);
|
|
@@ -1429,6 +1554,17 @@ var TerminalProgress = class {
|
|
|
1429
1554
|
fail(message = this.activeMessage) {
|
|
1430
1555
|
this.finish("!!", message);
|
|
1431
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
|
+
}
|
|
1432
1568
|
finish(marker, message) {
|
|
1433
1569
|
if (this.timer)
|
|
1434
1570
|
clearInterval(this.timer);
|
|
@@ -1611,19 +1747,23 @@ async function memberUpdateCommand(deps, member, options = {}) {
|
|
|
1611
1747
|
function keyAccess(apiKey) {
|
|
1612
1748
|
return apiKey.read_only ? "read-only" : "read-write";
|
|
1613
1749
|
}
|
|
1614
|
-
function
|
|
1615
|
-
return apiKey.databases.length + apiKey.database_prefixes.length > 0;
|
|
1750
|
+
function narrowedByName(apiKey) {
|
|
1751
|
+
return apiKey.databases.length + apiKey.database_prefixes.length + apiKey.streams.length + apiKey.stream_prefixes.length > 0;
|
|
1616
1752
|
}
|
|
1617
1753
|
function keyBuckets(apiKey) {
|
|
1618
1754
|
if (apiKey.buckets.length) return apiKey.buckets.join(", ");
|
|
1619
|
-
return
|
|
1755
|
+
return narrowedByName(apiKey) ? "none" : "all";
|
|
1756
|
+
}
|
|
1757
|
+
function keyNames(apiKey, names, prefixes) {
|
|
1758
|
+
if (names.length + prefixes.length > 0)
|
|
1759
|
+
return [...names, ...prefixes.map((prefix) => `${prefix}*`)].join(", ");
|
|
1760
|
+
return apiKey.buckets.length || narrowedByName(apiKey) ? "none" : "all";
|
|
1620
1761
|
}
|
|
1621
1762
|
function keyDatabases(apiKey) {
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
return apiKey.buckets.length ? "none" : "all";
|
|
1763
|
+
return keyNames(apiKey, apiKey.databases, apiKey.database_prefixes);
|
|
1764
|
+
}
|
|
1765
|
+
function keyStreams(apiKey) {
|
|
1766
|
+
return keyNames(apiKey, apiKey.streams, apiKey.stream_prefixes);
|
|
1627
1767
|
}
|
|
1628
1768
|
function apiKeyFields(apiKey) {
|
|
1629
1769
|
return fields([
|
|
@@ -1634,6 +1774,7 @@ function apiKeyFields(apiKey) {
|
|
|
1634
1774
|
["Access", keyAccess(apiKey)],
|
|
1635
1775
|
["Buckets", keyBuckets(apiKey)],
|
|
1636
1776
|
["Databases", keyDatabases(apiKey)],
|
|
1777
|
+
["Streams", keyStreams(apiKey)],
|
|
1637
1778
|
["Expires", when(apiKey.expires_at)],
|
|
1638
1779
|
["Last used", when(apiKey.last_used_at)],
|
|
1639
1780
|
["Created", apiKey.created_at]
|
|
@@ -1657,6 +1798,7 @@ async function apiKeyListCommand(deps, options = {}) {
|
|
|
1657
1798
|
"ACCESS",
|
|
1658
1799
|
"BUCKETS",
|
|
1659
1800
|
"DATABASES",
|
|
1801
|
+
"STREAMS",
|
|
1660
1802
|
"EXPIRES",
|
|
1661
1803
|
"LAST USED"
|
|
1662
1804
|
],
|
|
@@ -1668,6 +1810,7 @@ async function apiKeyListCommand(deps, options = {}) {
|
|
|
1668
1810
|
keyAccess(apiKey),
|
|
1669
1811
|
keyBuckets(apiKey),
|
|
1670
1812
|
keyDatabases(apiKey),
|
|
1813
|
+
keyStreams(apiKey),
|
|
1671
1814
|
when(apiKey.expires_at),
|
|
1672
1815
|
when(apiKey.last_used_at)
|
|
1673
1816
|
])
|
|
@@ -1684,6 +1827,8 @@ async function apiKeyCreateCommand(deps, name, options = {}) {
|
|
|
1684
1827
|
buckets: options.bucket,
|
|
1685
1828
|
databases: options.database,
|
|
1686
1829
|
database_prefixes: options.databasePrefix,
|
|
1830
|
+
streams: options.stream,
|
|
1831
|
+
stream_prefixes: options.streamPrefix,
|
|
1687
1832
|
expires_in: options.expiresIn
|
|
1688
1833
|
});
|
|
1689
1834
|
print(
|
|
@@ -1811,11 +1956,11 @@ async function bucketDeleteCommand(deps, name, options = {}) {
|
|
|
1811
1956
|
`Bucket ${bucket.name} is ${bucket.state}; its streams are being purged.`
|
|
1812
1957
|
);
|
|
1813
1958
|
}
|
|
1814
|
-
function
|
|
1815
|
-
return
|
|
1959
|
+
function readAccess(resource) {
|
|
1960
|
+
return resource.public_read ? "public" : "private";
|
|
1816
1961
|
}
|
|
1817
|
-
function
|
|
1818
|
-
const { log, snapshot } =
|
|
1962
|
+
function storedSize(resource) {
|
|
1963
|
+
const { log, snapshot } = resource.stored_bytes;
|
|
1819
1964
|
if (log === null && snapshot === null) return "unknown";
|
|
1820
1965
|
return `${bytes(log)} log, ${bytes(snapshot, "no")} snapshot`;
|
|
1821
1966
|
}
|
|
@@ -1828,11 +1973,12 @@ function databaseFields(database) {
|
|
|
1828
1973
|
["Kind", databaseOwner(database)],
|
|
1829
1974
|
["Region", database.region],
|
|
1830
1975
|
["State", database.state],
|
|
1831
|
-
["Read access",
|
|
1976
|
+
["Read access", readAccess(database)],
|
|
1832
1977
|
["URL", database.url],
|
|
1978
|
+
["Expires", database.expires_at ?? "never"],
|
|
1833
1979
|
["Incarnation", database.incarnation ?? "none yet"],
|
|
1834
1980
|
["Last write", when(database.last_write_at)],
|
|
1835
|
-
["Stored",
|
|
1981
|
+
["Stored", storedSize(database)],
|
|
1836
1982
|
["Created", `${database.created_at} by ${database.created_by}`]
|
|
1837
1983
|
]);
|
|
1838
1984
|
}
|
|
@@ -1859,19 +2005,44 @@ async function databaseListCommand(deps, options = {}) {
|
|
|
1859
2005
|
databaseOwner(database),
|
|
1860
2006
|
database.region,
|
|
1861
2007
|
database.state,
|
|
1862
|
-
|
|
2008
|
+
readAccess(database),
|
|
1863
2009
|
when(database.last_write_at),
|
|
1864
|
-
|
|
2010
|
+
storedSize(database),
|
|
1865
2011
|
database.url
|
|
1866
2012
|
])
|
|
1867
2013
|
) : "No databases."
|
|
1868
2014
|
);
|
|
1869
2015
|
noticeUnavailable(deps, page.unavailable_regions, "sizes");
|
|
1870
2016
|
}
|
|
2017
|
+
var EXPIRY_UNITS = { s: 1, m: 60, h: 3600, d: 86400 };
|
|
2018
|
+
var EXPIRES_IN_MIN_SECONDS = 90;
|
|
2019
|
+
function expiresInSeconds(value) {
|
|
2020
|
+
const match = /^([1-9][0-9]{0,8})([smhd])$/.exec(value.trim());
|
|
2021
|
+
if (!match)
|
|
2022
|
+
throw new Error(
|
|
2023
|
+
"Give a whole number of seconds, minutes, hours or days, such as 90s, 30m, 24h or 7d."
|
|
2024
|
+
);
|
|
2025
|
+
const seconds = Number(match[1]) * EXPIRY_UNITS[match[2]];
|
|
2026
|
+
if (seconds < EXPIRES_IN_MIN_SECONDS)
|
|
2027
|
+
throw new Error(
|
|
2028
|
+
`An expiry is at least ${EXPIRES_IN_MIN_SECONDS}s after this command creates it; give 90s or more.`
|
|
2029
|
+
);
|
|
2030
|
+
return seconds;
|
|
2031
|
+
}
|
|
2032
|
+
function creationExpiry(noun, options, now = Date.now()) {
|
|
2033
|
+
if (options.expiresIn !== void 0 && options.idempotencyKey !== void 0)
|
|
2034
|
+
throw new Error(
|
|
2035
|
+
`--expires-in counts from now, so a retry would name another expiry; retry an unconfirmed ${noun} creation with --idempotency-key and the --expires-at time its message printed.`
|
|
2036
|
+
);
|
|
2037
|
+
if (options.expiresAt !== void 0) return options.expiresAt;
|
|
2038
|
+
if (options.expiresIn === void 0) return void 0;
|
|
2039
|
+
return new Date(now + options.expiresIn * 1e3).toISOString();
|
|
2040
|
+
}
|
|
1871
2041
|
async function databaseCreateCommand(deps, name, options = {}) {
|
|
2042
|
+
const expiresAt = creationExpiry("database", options);
|
|
1872
2043
|
const database = await deps.api.createDatabase(
|
|
1873
2044
|
await deps.auth.apiKey(),
|
|
1874
|
-
{ name, region: options.region },
|
|
2045
|
+
{ name, region: options.region, expires_at: expiresAt },
|
|
1875
2046
|
options.idempotencyKey ?? randomUUID2()
|
|
1876
2047
|
);
|
|
1877
2048
|
print(
|
|
@@ -1887,7 +2058,7 @@ async function databaseCreateCommand(deps, name, options = {}) {
|
|
|
1887
2058
|
}
|
|
1888
2059
|
function databaseNextStep(name) {
|
|
1889
2060
|
return [
|
|
1890
|
-
"Next: npm i @tonbo/sqlite-ursula",
|
|
2061
|
+
"Next: npm i --save-exact @tonbo/sqlite-ursula@0.11.2",
|
|
1891
2062
|
` open(${JSON.stringify(name)}) from "@tonbo/sqlite-ursula/tonbo"`
|
|
1892
2063
|
].join("\n");
|
|
1893
2064
|
}
|
|
@@ -1926,22 +2097,17 @@ async function databaseDeleteCommand(deps, name, options = {}) {
|
|
|
1926
2097
|
database.state === "deleted" ? `Deleted database ${database.name}; its name is free again.` : `Database ${database.name} is ${database.state}; its name is free again once it is deleted.`
|
|
1927
2098
|
);
|
|
1928
2099
|
}
|
|
1929
|
-
function
|
|
2100
|
+
function usageSort(value) {
|
|
1930
2101
|
if (value === "-list_value" || value === "list_value") return "-list_value";
|
|
1931
2102
|
if (value === "name") return "name";
|
|
1932
2103
|
throw new Error("--sort is list_value or name.");
|
|
1933
2104
|
}
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
await deps.auth.apiKey(),
|
|
1937
|
-
{ limit: options.limit, cursor: options.cursor },
|
|
1938
|
-
{ period: options.period, sort: options.sort, prefix: options.prefix, names: options.names }
|
|
1939
|
-
);
|
|
1940
|
-
if (options.json) deps.output(JSON.stringify(usage));
|
|
2105
|
+
function printResourceUsage(deps, json, usage, noun) {
|
|
2106
|
+
if (json) deps.output(JSON.stringify(usage));
|
|
1941
2107
|
else
|
|
1942
2108
|
deps.output(
|
|
1943
2109
|
[
|
|
1944
|
-
|
|
2110
|
+
`${noun === "database" ? "Database" : "Stream"} usage in ${usage.period} at list price, before the monthly allowance and credit (${usage.currency.toUpperCase()}; metered up to ${when(usage.as_of, "not yet metered")})`,
|
|
1945
2111
|
usage.data.length ? table(
|
|
1946
2112
|
[
|
|
1947
2113
|
"NAME",
|
|
@@ -1961,7 +2127,7 @@ async function databaseUsageCommand(deps, options = {}) {
|
|
|
1961
2127
|
row.snapshot_gib_months ?? "",
|
|
1962
2128
|
usd(row.list_value_micros)
|
|
1963
2129
|
])
|
|
1964
|
-
) :
|
|
2130
|
+
) : `No ${noun} usage.`,
|
|
1965
2131
|
`Other metered usage: ${usd(usage.other.list_value_micros)}`,
|
|
1966
2132
|
"Charges, after the allowance and credit, are in `tonbo usage show`."
|
|
1967
2133
|
].join("\n")
|
|
@@ -1969,6 +2135,127 @@ async function databaseUsageCommand(deps, options = {}) {
|
|
|
1969
2135
|
if (usage.next_cursor) deps.notice(`More results: run again with --cursor ${usage.next_cursor}`);
|
|
1970
2136
|
noticeUnavailable(deps, usage.unavailable_regions, "usage");
|
|
1971
2137
|
}
|
|
2138
|
+
async function databaseUsageCommand(deps, options = {}) {
|
|
2139
|
+
const usage = await deps.api.listDatabaseUsage(
|
|
2140
|
+
await deps.auth.apiKey(),
|
|
2141
|
+
{ limit: options.limit, cursor: options.cursor },
|
|
2142
|
+
{ period: options.period, sort: options.sort, prefix: options.prefix, names: options.names }
|
|
2143
|
+
);
|
|
2144
|
+
printResourceUsage(deps, options.json, usage, "database");
|
|
2145
|
+
}
|
|
2146
|
+
function streamFields(stream) {
|
|
2147
|
+
return fields([
|
|
2148
|
+
["Name", stream.name],
|
|
2149
|
+
["Region", stream.region],
|
|
2150
|
+
["State", stream.state],
|
|
2151
|
+
["Content type", stream.content_type],
|
|
2152
|
+
["Expires", stream.expires_at ?? "never"],
|
|
2153
|
+
["Read access", readAccess(stream)],
|
|
2154
|
+
["URL", stream.url],
|
|
2155
|
+
["Incarnation", stream.incarnation ?? "none yet"],
|
|
2156
|
+
["Last write", when(stream.last_write_at)],
|
|
2157
|
+
["Stored", storedSize(stream)],
|
|
2158
|
+
["Created", `${stream.created_at} by ${stream.created_by}`]
|
|
2159
|
+
]);
|
|
2160
|
+
}
|
|
2161
|
+
async function streamListCommand(deps, options = {}) {
|
|
2162
|
+
const page = await deps.api.listStreams(
|
|
2163
|
+
await deps.auth.apiKey(),
|
|
2164
|
+
{ limit: options.limit, cursor: options.cursor },
|
|
2165
|
+
options.prefix
|
|
2166
|
+
);
|
|
2167
|
+
printPage(
|
|
2168
|
+
deps,
|
|
2169
|
+
options.json,
|
|
2170
|
+
page,
|
|
2171
|
+
(streams) => streams.length ? table(
|
|
2172
|
+
["NAME", "REGION", "STATE", "CONTENT TYPE", "READ ACCESS", "LAST WRITE", "STORED", "URL"],
|
|
2173
|
+
streams.map((stream) => [
|
|
2174
|
+
stream.name,
|
|
2175
|
+
stream.region,
|
|
2176
|
+
stream.state,
|
|
2177
|
+
stream.content_type,
|
|
2178
|
+
readAccess(stream),
|
|
2179
|
+
when(stream.last_write_at),
|
|
2180
|
+
storedSize(stream),
|
|
2181
|
+
stream.url
|
|
2182
|
+
])
|
|
2183
|
+
) : "No streams."
|
|
2184
|
+
);
|
|
2185
|
+
noticeUnavailable(deps, page.unavailable_regions, "sizes");
|
|
2186
|
+
}
|
|
2187
|
+
async function streamCreateCommand(deps, name, options = {}) {
|
|
2188
|
+
const expiresAt = creationExpiry("stream", options);
|
|
2189
|
+
const stream = await deps.api.createStream(
|
|
2190
|
+
await deps.auth.apiKey(),
|
|
2191
|
+
{
|
|
2192
|
+
name,
|
|
2193
|
+
region: options.region,
|
|
2194
|
+
content_type: options.contentType,
|
|
2195
|
+
expires_at: expiresAt
|
|
2196
|
+
},
|
|
2197
|
+
options.idempotencyKey ?? randomUUID2()
|
|
2198
|
+
);
|
|
2199
|
+
print(
|
|
2200
|
+
deps,
|
|
2201
|
+
options.json,
|
|
2202
|
+
stream,
|
|
2203
|
+
[
|
|
2204
|
+
stream.state === "active" ? `Created stream ${stream.name}.` : `Stream ${stream.name} is being created; \`tonbo stream show ${stream.name}\` reports it active when it is ready.`,
|
|
2205
|
+
streamFields(stream)
|
|
2206
|
+
].join("\n")
|
|
2207
|
+
);
|
|
2208
|
+
if (!options.json)
|
|
2209
|
+
deps.notice(
|
|
2210
|
+
[
|
|
2211
|
+
`Next: append with POST ${stream.url} and Content-Type: ${stream.content_type}`,
|
|
2212
|
+
` read from the start with GET ${stream.url}?offset=-1`
|
|
2213
|
+
].join("\n")
|
|
2214
|
+
);
|
|
2215
|
+
}
|
|
2216
|
+
async function streamShowCommand(deps, name, options = {}) {
|
|
2217
|
+
const stream = await deps.api.getStream(await deps.auth.apiKey(), name);
|
|
2218
|
+
print(deps, options.json, stream, streamFields(stream));
|
|
2219
|
+
}
|
|
2220
|
+
async function streamUpdateCommand(deps, name, options = {}) {
|
|
2221
|
+
if (options.public === options.private)
|
|
2222
|
+
throw new Error("Pass exactly one of --public and --private.");
|
|
2223
|
+
const stream = await deps.api.updateStream(await deps.auth.apiKey(), name, {
|
|
2224
|
+
public_read: options.public === true
|
|
2225
|
+
});
|
|
2226
|
+
print(
|
|
2227
|
+
deps,
|
|
2228
|
+
options.json,
|
|
2229
|
+
stream,
|
|
2230
|
+
stream.public_read ? `Stream ${stream.name} is public: anyone can read it at ${stream.url}.` : `Stream ${stream.name} is private: only the account's keys can read it.`
|
|
2231
|
+
);
|
|
2232
|
+
}
|
|
2233
|
+
async function streamDeleteCommand(deps, name, options = {}) {
|
|
2234
|
+
await confirm(
|
|
2235
|
+
deps,
|
|
2236
|
+
options.yes,
|
|
2237
|
+
`delete stream ${name}`,
|
|
2238
|
+
() => Promise.resolve(
|
|
2239
|
+
`Delete stream ${name} and everything in it? This cannot be undone. Type the stream's name to confirm:`
|
|
2240
|
+
),
|
|
2241
|
+
name
|
|
2242
|
+
);
|
|
2243
|
+
const stream = await deps.api.deleteStream(await deps.auth.apiKey(), name);
|
|
2244
|
+
print(
|
|
2245
|
+
deps,
|
|
2246
|
+
options.json,
|
|
2247
|
+
stream,
|
|
2248
|
+
stream.state === "deleted" ? `Deleted stream ${stream.name}; its name is free again.` : `Stream ${stream.name} is ${stream.state}; its name is free again once it is deleted.`
|
|
2249
|
+
);
|
|
2250
|
+
}
|
|
2251
|
+
async function streamUsageCommand(deps, options = {}) {
|
|
2252
|
+
const usage = await deps.api.listStreamUsage(
|
|
2253
|
+
await deps.auth.apiKey(),
|
|
2254
|
+
{ limit: options.limit, cursor: options.cursor },
|
|
2255
|
+
{ period: options.period, sort: options.sort, prefix: options.prefix, names: options.names }
|
|
2256
|
+
);
|
|
2257
|
+
printResourceUsage(deps, options.json, usage, "stream");
|
|
2258
|
+
}
|
|
1972
2259
|
async function usageShowCommand(deps, options = {}) {
|
|
1973
2260
|
const usage = await deps.api.getUsage(await deps.auth.apiKey(), options.period);
|
|
1974
2261
|
print(
|
|
@@ -2002,13 +2289,22 @@ var MACHINE_ERROR_HINTS = {
|
|
|
2002
2289
|
hostname_too_long: "<name>-<account> must fit one hostname label; choose a shorter name.",
|
|
2003
2290
|
account_name_unclaimed: "Claim a Tonbo ID for the account on the console's Profile page, then run the command again.",
|
|
2004
2291
|
image_platform_unsupported: "Machines run linux/arm64: publish the image for that platform (for example `docker buildx build --platform linux/arm64`).",
|
|
2292
|
+
image_not_offered: "Only the platform image runs until image pulls ship: create or update the Machine with `--image tonbo.dev/platform`.",
|
|
2005
2293
|
shape_unavailable: "`tonbo machine shapes` lists the shapes a Machine may name now.",
|
|
2006
2294
|
port_reserved: "Serve the application on another port.",
|
|
2007
2295
|
revision_mismatch: "The Machine changed since it was read; `tonbo machine show` it and run the update again.",
|
|
2008
2296
|
snapshot_would_be_discarded: "Pass --discard-snapshot to apply the change at the next boot, which then starts fresh.",
|
|
2009
2297
|
database_limit_reached: "Each Machine counts one database: delete a Machine or a database (`tonbo database list`) first.",
|
|
2010
2298
|
idempotency_conflict: "That idempotency key belongs to another request: leave out --idempotency-key to create a new Machine.",
|
|
2011
|
-
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"
|
|
2012
2308
|
};
|
|
2013
2309
|
function explainMachineError(error) {
|
|
2014
2310
|
if (!(error instanceof HttpError) || error.code === null) return error;
|
|
@@ -2038,6 +2334,29 @@ function idleActionText(machine) {
|
|
|
2038
2334
|
function machineState(machine) {
|
|
2039
2335
|
return machine.failure ? `${machine.state}: ${machine.failure.code}: ${machine.failure.message}` : machine.state;
|
|
2040
2336
|
}
|
|
2337
|
+
function imageText(machine) {
|
|
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
|
+
];
|
|
2359
|
+
}
|
|
2041
2360
|
function machineFields(machine) {
|
|
2042
2361
|
return fields([
|
|
2043
2362
|
["Name", machine.name],
|
|
@@ -2046,7 +2365,8 @@ function machineFields(machine) {
|
|
|
2046
2365
|
["State", machineState(machine)],
|
|
2047
2366
|
["Power", machine.status.power],
|
|
2048
2367
|
["Shape", machine.shape],
|
|
2049
|
-
["Image",
|
|
2368
|
+
["Image", imageText(machine)],
|
|
2369
|
+
...imageFailureFields(machine),
|
|
2050
2370
|
["Command", machine.command ? machine.command.join(" ") : "the image's own"],
|
|
2051
2371
|
["Ports", machinePortsText(machine)],
|
|
2052
2372
|
["HTTPS", machine.hostnames.https],
|
|
@@ -2128,6 +2448,11 @@ async function machineListCommand(deps, options = {}) {
|
|
|
2128
2448
|
);
|
|
2129
2449
|
});
|
|
2130
2450
|
}
|
|
2451
|
+
function createdText(machine) {
|
|
2452
|
+
if (machine.state === "creating")
|
|
2453
|
+
return `Machine ${machine.name} is being created; \`tonbo machine show ${machine.name}\` reports it active when it is ready.`;
|
|
2454
|
+
return `Created Machine ${machine.name}.`;
|
|
2455
|
+
}
|
|
2131
2456
|
async function machineCreateCommand(deps, name, command, options = {}) {
|
|
2132
2457
|
await machineCommand(async () => {
|
|
2133
2458
|
if (!options.image) throw new Error("Pass --image <reference>.");
|
|
@@ -2151,15 +2476,13 @@ async function machineCreateCommand(deps, name, command, options = {}) {
|
|
|
2151
2476
|
},
|
|
2152
2477
|
options.idempotencyKey ?? randomUUID3()
|
|
2153
2478
|
);
|
|
2154
|
-
|
|
2155
|
-
deps,
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
].join("\n")
|
|
2162
|
-
);
|
|
2479
|
+
if (machine.state === "failed") {
|
|
2480
|
+
print(deps, options.json, machine, machineFields(machine));
|
|
2481
|
+
throw new Error(
|
|
2482
|
+
`Machine ${machine.name} failed to be created; delete it with \`tonbo machine delete ${machine.name}\` and create it again.`
|
|
2483
|
+
);
|
|
2484
|
+
}
|
|
2485
|
+
print(deps, options.json, machine, [createdText(machine), machineFields(machine)].join("\n"));
|
|
2163
2486
|
});
|
|
2164
2487
|
}
|
|
2165
2488
|
async function machineShowCommand(deps, name, options = {}) {
|
|
@@ -2360,6 +2683,20 @@ function machineSpecOptions(command, update) {
|
|
|
2360
2683
|
parseWhole
|
|
2361
2684
|
);
|
|
2362
2685
|
}
|
|
2686
|
+
function parseExpiresIn(value) {
|
|
2687
|
+
try {
|
|
2688
|
+
return expiresInSeconds(value);
|
|
2689
|
+
} catch (error) {
|
|
2690
|
+
throw new InvalidArgumentError(error.message);
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
function parseUsageSort(value) {
|
|
2694
|
+
try {
|
|
2695
|
+
return usageSort(value);
|
|
2696
|
+
} catch (error) {
|
|
2697
|
+
throw new InvalidArgumentError(error.message);
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2363
2700
|
function createProgram(dependencies = createDependencies) {
|
|
2364
2701
|
const program = new Command().name("tonbo").description("Sign in to Tonbo and manage account resources.").version(packageVersion.version);
|
|
2365
2702
|
program.command("login").description("sign in through the browser and save a personal key for this computer").option("--account <name>", "the account to log in to; defaults to your personal account").action(async (options) => loginCommand(dependencies(), options));
|
|
@@ -2423,6 +2760,14 @@ function createProgram(dependencies = createDependencies) {
|
|
|
2423
2760
|
"--database-prefix <prefix>",
|
|
2424
2761
|
"narrow the key to the databases whose name starts with this, for its whole life; repeat for several; never with --bucket",
|
|
2425
2762
|
repeated
|
|
2763
|
+
).option(
|
|
2764
|
+
"--stream <name>",
|
|
2765
|
+
"narrow the key to this raw stream, for its whole life; repeat for several; never with --bucket",
|
|
2766
|
+
repeated
|
|
2767
|
+
).option(
|
|
2768
|
+
"--stream-prefix <prefix>",
|
|
2769
|
+
"narrow the key to the raw streams whose name starts with this, for its whole life; repeat for several; never with --bucket",
|
|
2770
|
+
repeated
|
|
2426
2771
|
).addOption(
|
|
2427
2772
|
new Option("--expires-in <period>", "when the key expires; never by default").choices(
|
|
2428
2773
|
apiKeyExpirations
|
|
@@ -2483,16 +2828,26 @@ function createProgram(dependencies = createDependencies) {
|
|
|
2483
2828
|
database.command("create <name>").description("create a database; its region is fixed for its life").option(
|
|
2484
2829
|
"--region <region>",
|
|
2485
2830
|
"the database's region (see tonbo region list); the database default region by default"
|
|
2831
|
+
).addOption(
|
|
2832
|
+
new Option(
|
|
2833
|
+
"--expires-at <time>",
|
|
2834
|
+
"delete the database after this RFC 3339 time, at least 60 seconds after its creation, such as 2026-11-01T00:00:00Z; never by default"
|
|
2835
|
+
).conflicts("expiresIn")
|
|
2836
|
+
).addOption(
|
|
2837
|
+
new Option(
|
|
2838
|
+
"--expires-in <duration>",
|
|
2839
|
+
"delete the database this long after its creation, at least 90s: such as 90s, 30m, 24h or 7d; never by default; never with --idempotency-key"
|
|
2840
|
+
).argParser(parseExpiresIn).conflicts("expiresAt")
|
|
2486
2841
|
).option(
|
|
2487
2842
|
"--idempotency-key <key>",
|
|
2488
|
-
"reuse the key of an unconfirmed creation to get the database it created"
|
|
2843
|
+
"reuse the key of an unconfirmed creation to get the database it created; with --expires-at, never --expires-in"
|
|
2489
2844
|
),
|
|
2490
2845
|
"createDatabase"
|
|
2491
2846
|
).action(
|
|
2492
2847
|
async (name, options) => databaseCreateCommand(dependencies(), name, options)
|
|
2493
2848
|
);
|
|
2494
2849
|
operationOptions(
|
|
2495
|
-
database.command("show <name>").description("show a database, its URL and size"),
|
|
2850
|
+
database.command("show <name>").description("show a database, its URL, expiry and size"),
|
|
2496
2851
|
"getDatabase"
|
|
2497
2852
|
).action(
|
|
2498
2853
|
async (name, options) => databaseShowCommand(dependencies(), name, options)
|
|
@@ -2513,21 +2868,70 @@ function createProgram(dependencies = createDependencies) {
|
|
|
2513
2868
|
database.command("usage").description("each database's usage of one month at list price, before allowance and credit").option(
|
|
2514
2869
|
"--period <month>",
|
|
2515
2870
|
"the calendar month in UTC, as YYYY-MM; the current one by default"
|
|
2516
|
-
).option(
|
|
2517
|
-
"--sort <order>",
|
|
2518
|
-
"list_value (highest first, the default) or name",
|
|
2519
|
-
(value) => {
|
|
2520
|
-
try {
|
|
2521
|
-
return databaseUsageSort(value);
|
|
2522
|
-
} catch (error) {
|
|
2523
|
-
throw new InvalidArgumentError(error.message);
|
|
2524
|
-
}
|
|
2525
|
-
}
|
|
2526
|
-
).option("--prefix <prefix>", "only databases whose name starts with this").option("--names <names>", "only these databases, up to 100 names, comma-separated"),
|
|
2871
|
+
).option("--sort <order>", "list_value (highest first, the default) or name", parseUsageSort).option("--prefix <prefix>", "only databases whose name starts with this").option("--names <names>", "only these databases, up to 100 names, comma-separated"),
|
|
2527
2872
|
"listDatabaseUsage"
|
|
2528
2873
|
).action(
|
|
2529
2874
|
async (options) => databaseUsageCommand(dependencies(), options)
|
|
2530
2875
|
);
|
|
2876
|
+
const stream = program.command("stream").description("the account's raw durable streams");
|
|
2877
|
+
operationOptions(
|
|
2878
|
+
stream.command("list").description("list the streams in every region").option("--prefix <prefix>", "only streams whose name starts with this"),
|
|
2879
|
+
"listStreams"
|
|
2880
|
+
).action(
|
|
2881
|
+
async (options) => streamListCommand(dependencies(), options)
|
|
2882
|
+
);
|
|
2883
|
+
operationOptions(
|
|
2884
|
+
stream.command("create <name>").description("create a stream; its region and content type are fixed for its life").option(
|
|
2885
|
+
"--region <region>",
|
|
2886
|
+
"the stream's region (see tonbo region list); the database default region by default"
|
|
2887
|
+
).option(
|
|
2888
|
+
"--content-type <type>",
|
|
2889
|
+
"the media type every append sends, such as application/json; application/octet-stream by default"
|
|
2890
|
+
).addOption(
|
|
2891
|
+
new Option(
|
|
2892
|
+
"--expires-at <time>",
|
|
2893
|
+
"delete the stream after this RFC 3339 time, at least 60 seconds after its creation, such as 2026-11-01T00:00:00Z; never by default"
|
|
2894
|
+
).conflicts("expiresIn")
|
|
2895
|
+
).addOption(
|
|
2896
|
+
new Option(
|
|
2897
|
+
"--expires-in <duration>",
|
|
2898
|
+
"delete the stream this long after its creation, at least 90s: such as 90s, 30m, 24h or 7d; never by default; never with --idempotency-key"
|
|
2899
|
+
).argParser(parseExpiresIn).conflicts("expiresAt")
|
|
2900
|
+
).option(
|
|
2901
|
+
"--idempotency-key <key>",
|
|
2902
|
+
"reuse the key of an unconfirmed creation to get the stream it created; with --expires-at, never --expires-in"
|
|
2903
|
+
),
|
|
2904
|
+
"createStream"
|
|
2905
|
+
).action(
|
|
2906
|
+
async (name, options) => streamCreateCommand(dependencies(), name, options)
|
|
2907
|
+
);
|
|
2908
|
+
operationOptions(
|
|
2909
|
+
stream.command("show <name>").description("show a stream, its URL, content type, expiry and size"),
|
|
2910
|
+
"getStream"
|
|
2911
|
+
).action(
|
|
2912
|
+
async (name, options) => streamShowCommand(dependencies(), name, options)
|
|
2913
|
+
);
|
|
2914
|
+
operationOptions(
|
|
2915
|
+
stream.command("update <name>").description("publish a stream for anonymous reads, or take it private (owners and admins)").addOption(new Option("--public", "serve anonymous reads").conflicts("private")).addOption(new Option("--private", "serve only the account's keys").conflicts("public")),
|
|
2916
|
+
"updateStream"
|
|
2917
|
+
).action(
|
|
2918
|
+
async (name, options) => streamUpdateCommand(dependencies(), name, options)
|
|
2919
|
+
);
|
|
2920
|
+
operationOptions(
|
|
2921
|
+
stream.command("delete <name>").description("delete a stream and everything in it").option("--yes", "delete without asking; required when stdin is not a terminal"),
|
|
2922
|
+
"deleteStream"
|
|
2923
|
+
).action(
|
|
2924
|
+
async (name, options) => streamDeleteCommand(dependencies(), name, options)
|
|
2925
|
+
);
|
|
2926
|
+
operationOptions(
|
|
2927
|
+
stream.command("usage").description("each stream's usage of one month at list price, before allowance and credit").option(
|
|
2928
|
+
"--period <month>",
|
|
2929
|
+
"the calendar month in UTC, as YYYY-MM; the current one by default"
|
|
2930
|
+
).option("--sort <order>", "list_value (highest first, the default) or name", parseUsageSort).option("--prefix <prefix>", "only streams whose name starts with this").option("--names <names>", "only these streams, up to 100 names, comma-separated"),
|
|
2931
|
+
"listStreamUsage"
|
|
2932
|
+
).action(
|
|
2933
|
+
async (options) => streamUsageCommand(dependencies(), options)
|
|
2934
|
+
);
|
|
2531
2935
|
const machine = program.command("machine").description("the account's Machines");
|
|
2532
2936
|
operationOptions(
|
|
2533
2937
|
machine.command("list").description("list the Machines in every region"),
|
|
@@ -2538,8 +2942,11 @@ function createProgram(dependencies = createDependencies) {
|
|
|
2538
2942
|
operationOptions(
|
|
2539
2943
|
machineSpecOptions(
|
|
2540
2944
|
machine.command("create <name> [command...]").description(
|
|
2541
|
-
"create a Machine from
|
|
2542
|
-
).requiredOption(
|
|
2945
|
+
"create a Machine from an image; [command...] after -- replaces the image's CMD"
|
|
2946
|
+
).requiredOption(
|
|
2947
|
+
"--image <reference>",
|
|
2948
|
+
"the image the Machine boots: tonbo.dev/platform, or a public linux/arm64 OCI image once registry images are offered"
|
|
2949
|
+
).option(
|
|
2543
2950
|
"--region <region>",
|
|
2544
2951
|
"the Machine's region, fixed for its life; the account's default region by default"
|
|
2545
2952
|
),
|
|
@@ -2562,7 +2969,7 @@ function createProgram(dependencies = createDependencies) {
|
|
|
2562
2969
|
machineSpecOptions(
|
|
2563
2970
|
machine.command("update <name> [command...]").description(
|
|
2564
2971
|
"change a Machine; [command...] after -- replaces its command at the next boot"
|
|
2565
|
-
).option("--image <reference>", "a new image; applies at the next boot").option("--image-command", "run the image's own CMD again at the next boot").option("--unset-env <KEY>", "remove an environment variable; repeat for several", repeated).option("--clear-ports", "remove every port").option(
|
|
2972
|
+
).option("--image <reference>", "a new image, named as for create; applies at the next boot").option("--image-command", "run the image's own CMD again at the next boot").option("--unset-env <KEY>", "remove an environment variable; repeat for several", repeated).option("--clear-ports", "remove every port").option(
|
|
2566
2973
|
"--revision <revision>",
|
|
2567
2974
|
"write only if the Machine is still at this revision (tonbo machine show); the revision it reads by default",
|
|
2568
2975
|
parseWhole
|