@tonbo/cli 0.7.0 → 0.7.1
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 +32 -12
- package/dist/bin/tonbo.js +387 -55
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -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 @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 = [
|
|
@@ -1201,7 +1266,7 @@ var ManagementApi = class {
|
|
|
1201
1266
|
return this.call("createDatabase", key, {
|
|
1202
1267
|
body: withoutUndefined(request),
|
|
1203
1268
|
idempotencyKey
|
|
1204
|
-
}).catch(unconfirmed("Database creation", idempotencyKey, "the same name and region"));
|
|
1269
|
+
}).catch(unconfirmed("Database creation", idempotencyKey, request.expires_at ? `the same name, region and --expires-at ${request.expires_at}` : "the same name and region"));
|
|
1205
1270
|
}
|
|
1206
1271
|
getDatabase(key, name) {
|
|
1207
1272
|
return this.call("getDatabase", key, { params: { name } });
|
|
@@ -1216,6 +1281,30 @@ var ManagementApi = class {
|
|
|
1216
1281
|
listDatabaseUsage(key, page, query = {}) {
|
|
1217
1282
|
return this.page("listDatabaseUsage", key, page, query);
|
|
1218
1283
|
}
|
|
1284
|
+
listStreams(key, page, prefix) {
|
|
1285
|
+
return this.page("listStreams", key, page, { prefix });
|
|
1286
|
+
}
|
|
1287
|
+
/** A replay with the same idempotency key returns the stream the first
|
|
1288
|
+
* request created. */
|
|
1289
|
+
createStream(key, request, idempotencyKey) {
|
|
1290
|
+
return this.call("createStream", key, {
|
|
1291
|
+
body: withoutUndefined(request),
|
|
1292
|
+
idempotencyKey
|
|
1293
|
+
}).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"));
|
|
1294
|
+
}
|
|
1295
|
+
getStream(key, name) {
|
|
1296
|
+
return this.call("getStream", key, { params: { name } });
|
|
1297
|
+
}
|
|
1298
|
+
updateStream(key, name, change) {
|
|
1299
|
+
return this.call("updateStream", key, { params: { name }, body: change });
|
|
1300
|
+
}
|
|
1301
|
+
deleteStream(key, name) {
|
|
1302
|
+
return this.call("deleteStream", key, { params: { name } });
|
|
1303
|
+
}
|
|
1304
|
+
/** The whole usage page: its rows, `other`, prices and checkpoint. */
|
|
1305
|
+
listStreamUsage(key, page, query = {}) {
|
|
1306
|
+
return this.page("listStreamUsage", key, page, query);
|
|
1307
|
+
}
|
|
1219
1308
|
getUsage(key, period) {
|
|
1220
1309
|
return this.call("getUsage", key, { query: { period } });
|
|
1221
1310
|
}
|
|
@@ -1611,19 +1700,23 @@ async function memberUpdateCommand(deps, member, options = {}) {
|
|
|
1611
1700
|
function keyAccess(apiKey) {
|
|
1612
1701
|
return apiKey.read_only ? "read-only" : "read-write";
|
|
1613
1702
|
}
|
|
1614
|
-
function
|
|
1615
|
-
return apiKey.databases.length + apiKey.database_prefixes.length > 0;
|
|
1703
|
+
function narrowedByName(apiKey) {
|
|
1704
|
+
return apiKey.databases.length + apiKey.database_prefixes.length + apiKey.streams.length + apiKey.stream_prefixes.length > 0;
|
|
1616
1705
|
}
|
|
1617
1706
|
function keyBuckets(apiKey) {
|
|
1618
1707
|
if (apiKey.buckets.length) return apiKey.buckets.join(", ");
|
|
1619
|
-
return
|
|
1708
|
+
return narrowedByName(apiKey) ? "none" : "all";
|
|
1709
|
+
}
|
|
1710
|
+
function keyNames(apiKey, names, prefixes) {
|
|
1711
|
+
if (names.length + prefixes.length > 0)
|
|
1712
|
+
return [...names, ...prefixes.map((prefix) => `${prefix}*`)].join(", ");
|
|
1713
|
+
return apiKey.buckets.length || narrowedByName(apiKey) ? "none" : "all";
|
|
1620
1714
|
}
|
|
1621
1715
|
function keyDatabases(apiKey) {
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
return apiKey.buckets.length ? "none" : "all";
|
|
1716
|
+
return keyNames(apiKey, apiKey.databases, apiKey.database_prefixes);
|
|
1717
|
+
}
|
|
1718
|
+
function keyStreams(apiKey) {
|
|
1719
|
+
return keyNames(apiKey, apiKey.streams, apiKey.stream_prefixes);
|
|
1627
1720
|
}
|
|
1628
1721
|
function apiKeyFields(apiKey) {
|
|
1629
1722
|
return fields([
|
|
@@ -1634,6 +1727,7 @@ function apiKeyFields(apiKey) {
|
|
|
1634
1727
|
["Access", keyAccess(apiKey)],
|
|
1635
1728
|
["Buckets", keyBuckets(apiKey)],
|
|
1636
1729
|
["Databases", keyDatabases(apiKey)],
|
|
1730
|
+
["Streams", keyStreams(apiKey)],
|
|
1637
1731
|
["Expires", when(apiKey.expires_at)],
|
|
1638
1732
|
["Last used", when(apiKey.last_used_at)],
|
|
1639
1733
|
["Created", apiKey.created_at]
|
|
@@ -1657,6 +1751,7 @@ async function apiKeyListCommand(deps, options = {}) {
|
|
|
1657
1751
|
"ACCESS",
|
|
1658
1752
|
"BUCKETS",
|
|
1659
1753
|
"DATABASES",
|
|
1754
|
+
"STREAMS",
|
|
1660
1755
|
"EXPIRES",
|
|
1661
1756
|
"LAST USED"
|
|
1662
1757
|
],
|
|
@@ -1668,6 +1763,7 @@ async function apiKeyListCommand(deps, options = {}) {
|
|
|
1668
1763
|
keyAccess(apiKey),
|
|
1669
1764
|
keyBuckets(apiKey),
|
|
1670
1765
|
keyDatabases(apiKey),
|
|
1766
|
+
keyStreams(apiKey),
|
|
1671
1767
|
when(apiKey.expires_at),
|
|
1672
1768
|
when(apiKey.last_used_at)
|
|
1673
1769
|
])
|
|
@@ -1684,6 +1780,8 @@ async function apiKeyCreateCommand(deps, name, options = {}) {
|
|
|
1684
1780
|
buckets: options.bucket,
|
|
1685
1781
|
databases: options.database,
|
|
1686
1782
|
database_prefixes: options.databasePrefix,
|
|
1783
|
+
streams: options.stream,
|
|
1784
|
+
stream_prefixes: options.streamPrefix,
|
|
1687
1785
|
expires_in: options.expiresIn
|
|
1688
1786
|
});
|
|
1689
1787
|
print(
|
|
@@ -1811,11 +1909,11 @@ async function bucketDeleteCommand(deps, name, options = {}) {
|
|
|
1811
1909
|
`Bucket ${bucket.name} is ${bucket.state}; its streams are being purged.`
|
|
1812
1910
|
);
|
|
1813
1911
|
}
|
|
1814
|
-
function
|
|
1815
|
-
return
|
|
1912
|
+
function readAccess(resource) {
|
|
1913
|
+
return resource.public_read ? "public" : "private";
|
|
1816
1914
|
}
|
|
1817
|
-
function
|
|
1818
|
-
const { log, snapshot } =
|
|
1915
|
+
function storedSize(resource) {
|
|
1916
|
+
const { log, snapshot } = resource.stored_bytes;
|
|
1819
1917
|
if (log === null && snapshot === null) return "unknown";
|
|
1820
1918
|
return `${bytes(log)} log, ${bytes(snapshot, "no")} snapshot`;
|
|
1821
1919
|
}
|
|
@@ -1828,11 +1926,12 @@ function databaseFields(database) {
|
|
|
1828
1926
|
["Kind", databaseOwner(database)],
|
|
1829
1927
|
["Region", database.region],
|
|
1830
1928
|
["State", database.state],
|
|
1831
|
-
["Read access",
|
|
1929
|
+
["Read access", readAccess(database)],
|
|
1832
1930
|
["URL", database.url],
|
|
1931
|
+
["Expires", database.expires_at ?? "never"],
|
|
1833
1932
|
["Incarnation", database.incarnation ?? "none yet"],
|
|
1834
1933
|
["Last write", when(database.last_write_at)],
|
|
1835
|
-
["Stored",
|
|
1934
|
+
["Stored", storedSize(database)],
|
|
1836
1935
|
["Created", `${database.created_at} by ${database.created_by}`]
|
|
1837
1936
|
]);
|
|
1838
1937
|
}
|
|
@@ -1859,19 +1958,44 @@ async function databaseListCommand(deps, options = {}) {
|
|
|
1859
1958
|
databaseOwner(database),
|
|
1860
1959
|
database.region,
|
|
1861
1960
|
database.state,
|
|
1862
|
-
|
|
1961
|
+
readAccess(database),
|
|
1863
1962
|
when(database.last_write_at),
|
|
1864
|
-
|
|
1963
|
+
storedSize(database),
|
|
1865
1964
|
database.url
|
|
1866
1965
|
])
|
|
1867
1966
|
) : "No databases."
|
|
1868
1967
|
);
|
|
1869
1968
|
noticeUnavailable(deps, page.unavailable_regions, "sizes");
|
|
1870
1969
|
}
|
|
1970
|
+
var EXPIRY_UNITS = { s: 1, m: 60, h: 3600, d: 86400 };
|
|
1971
|
+
var EXPIRES_IN_MIN_SECONDS = 90;
|
|
1972
|
+
function expiresInSeconds(value) {
|
|
1973
|
+
const match = /^([1-9][0-9]{0,8})([smhd])$/.exec(value.trim());
|
|
1974
|
+
if (!match)
|
|
1975
|
+
throw new Error(
|
|
1976
|
+
"Give a whole number of seconds, minutes, hours or days, such as 90s, 30m, 24h or 7d."
|
|
1977
|
+
);
|
|
1978
|
+
const seconds = Number(match[1]) * EXPIRY_UNITS[match[2]];
|
|
1979
|
+
if (seconds < EXPIRES_IN_MIN_SECONDS)
|
|
1980
|
+
throw new Error(
|
|
1981
|
+
`An expiry is at least ${EXPIRES_IN_MIN_SECONDS}s after this command creates it; give 90s or more.`
|
|
1982
|
+
);
|
|
1983
|
+
return seconds;
|
|
1984
|
+
}
|
|
1985
|
+
function creationExpiry(noun, options, now = Date.now()) {
|
|
1986
|
+
if (options.expiresIn !== void 0 && options.idempotencyKey !== void 0)
|
|
1987
|
+
throw new Error(
|
|
1988
|
+
`--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.`
|
|
1989
|
+
);
|
|
1990
|
+
if (options.expiresAt !== void 0) return options.expiresAt;
|
|
1991
|
+
if (options.expiresIn === void 0) return void 0;
|
|
1992
|
+
return new Date(now + options.expiresIn * 1e3).toISOString();
|
|
1993
|
+
}
|
|
1871
1994
|
async function databaseCreateCommand(deps, name, options = {}) {
|
|
1995
|
+
const expiresAt = creationExpiry("database", options);
|
|
1872
1996
|
const database = await deps.api.createDatabase(
|
|
1873
1997
|
await deps.auth.apiKey(),
|
|
1874
|
-
{ name, region: options.region },
|
|
1998
|
+
{ name, region: options.region, expires_at: expiresAt },
|
|
1875
1999
|
options.idempotencyKey ?? randomUUID2()
|
|
1876
2000
|
);
|
|
1877
2001
|
print(
|
|
@@ -1887,7 +2011,7 @@ async function databaseCreateCommand(deps, name, options = {}) {
|
|
|
1887
2011
|
}
|
|
1888
2012
|
function databaseNextStep(name) {
|
|
1889
2013
|
return [
|
|
1890
|
-
"Next: npm i @tonbo/sqlite-ursula",
|
|
2014
|
+
"Next: npm i @tonbo/sqlite-ursula@0.11.2",
|
|
1891
2015
|
` open(${JSON.stringify(name)}) from "@tonbo/sqlite-ursula/tonbo"`
|
|
1892
2016
|
].join("\n");
|
|
1893
2017
|
}
|
|
@@ -1926,22 +2050,17 @@ async function databaseDeleteCommand(deps, name, options = {}) {
|
|
|
1926
2050
|
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
2051
|
);
|
|
1928
2052
|
}
|
|
1929
|
-
function
|
|
2053
|
+
function usageSort(value) {
|
|
1930
2054
|
if (value === "-list_value" || value === "list_value") return "-list_value";
|
|
1931
2055
|
if (value === "name") return "name";
|
|
1932
2056
|
throw new Error("--sort is list_value or name.");
|
|
1933
2057
|
}
|
|
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));
|
|
2058
|
+
function printResourceUsage(deps, json, usage, noun) {
|
|
2059
|
+
if (json) deps.output(JSON.stringify(usage));
|
|
1941
2060
|
else
|
|
1942
2061
|
deps.output(
|
|
1943
2062
|
[
|
|
1944
|
-
|
|
2063
|
+
`${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
2064
|
usage.data.length ? table(
|
|
1946
2065
|
[
|
|
1947
2066
|
"NAME",
|
|
@@ -1961,7 +2080,7 @@ async function databaseUsageCommand(deps, options = {}) {
|
|
|
1961
2080
|
row.snapshot_gib_months ?? "",
|
|
1962
2081
|
usd(row.list_value_micros)
|
|
1963
2082
|
])
|
|
1964
|
-
) :
|
|
2083
|
+
) : `No ${noun} usage.`,
|
|
1965
2084
|
`Other metered usage: ${usd(usage.other.list_value_micros)}`,
|
|
1966
2085
|
"Charges, after the allowance and credit, are in `tonbo usage show`."
|
|
1967
2086
|
].join("\n")
|
|
@@ -1969,6 +2088,127 @@ async function databaseUsageCommand(deps, options = {}) {
|
|
|
1969
2088
|
if (usage.next_cursor) deps.notice(`More results: run again with --cursor ${usage.next_cursor}`);
|
|
1970
2089
|
noticeUnavailable(deps, usage.unavailable_regions, "usage");
|
|
1971
2090
|
}
|
|
2091
|
+
async function databaseUsageCommand(deps, options = {}) {
|
|
2092
|
+
const usage = await deps.api.listDatabaseUsage(
|
|
2093
|
+
await deps.auth.apiKey(),
|
|
2094
|
+
{ limit: options.limit, cursor: options.cursor },
|
|
2095
|
+
{ period: options.period, sort: options.sort, prefix: options.prefix, names: options.names }
|
|
2096
|
+
);
|
|
2097
|
+
printResourceUsage(deps, options.json, usage, "database");
|
|
2098
|
+
}
|
|
2099
|
+
function streamFields(stream) {
|
|
2100
|
+
return fields([
|
|
2101
|
+
["Name", stream.name],
|
|
2102
|
+
["Region", stream.region],
|
|
2103
|
+
["State", stream.state],
|
|
2104
|
+
["Content type", stream.content_type],
|
|
2105
|
+
["Expires", stream.expires_at ?? "never"],
|
|
2106
|
+
["Read access", readAccess(stream)],
|
|
2107
|
+
["URL", stream.url],
|
|
2108
|
+
["Incarnation", stream.incarnation ?? "none yet"],
|
|
2109
|
+
["Last write", when(stream.last_write_at)],
|
|
2110
|
+
["Stored", storedSize(stream)],
|
|
2111
|
+
["Created", `${stream.created_at} by ${stream.created_by}`]
|
|
2112
|
+
]);
|
|
2113
|
+
}
|
|
2114
|
+
async function streamListCommand(deps, options = {}) {
|
|
2115
|
+
const page = await deps.api.listStreams(
|
|
2116
|
+
await deps.auth.apiKey(),
|
|
2117
|
+
{ limit: options.limit, cursor: options.cursor },
|
|
2118
|
+
options.prefix
|
|
2119
|
+
);
|
|
2120
|
+
printPage(
|
|
2121
|
+
deps,
|
|
2122
|
+
options.json,
|
|
2123
|
+
page,
|
|
2124
|
+
(streams) => streams.length ? table(
|
|
2125
|
+
["NAME", "REGION", "STATE", "CONTENT TYPE", "READ ACCESS", "LAST WRITE", "STORED", "URL"],
|
|
2126
|
+
streams.map((stream) => [
|
|
2127
|
+
stream.name,
|
|
2128
|
+
stream.region,
|
|
2129
|
+
stream.state,
|
|
2130
|
+
stream.content_type,
|
|
2131
|
+
readAccess(stream),
|
|
2132
|
+
when(stream.last_write_at),
|
|
2133
|
+
storedSize(stream),
|
|
2134
|
+
stream.url
|
|
2135
|
+
])
|
|
2136
|
+
) : "No streams."
|
|
2137
|
+
);
|
|
2138
|
+
noticeUnavailable(deps, page.unavailable_regions, "sizes");
|
|
2139
|
+
}
|
|
2140
|
+
async function streamCreateCommand(deps, name, options = {}) {
|
|
2141
|
+
const expiresAt = creationExpiry("stream", options);
|
|
2142
|
+
const stream = await deps.api.createStream(
|
|
2143
|
+
await deps.auth.apiKey(),
|
|
2144
|
+
{
|
|
2145
|
+
name,
|
|
2146
|
+
region: options.region,
|
|
2147
|
+
content_type: options.contentType,
|
|
2148
|
+
expires_at: expiresAt
|
|
2149
|
+
},
|
|
2150
|
+
options.idempotencyKey ?? randomUUID2()
|
|
2151
|
+
);
|
|
2152
|
+
print(
|
|
2153
|
+
deps,
|
|
2154
|
+
options.json,
|
|
2155
|
+
stream,
|
|
2156
|
+
[
|
|
2157
|
+
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.`,
|
|
2158
|
+
streamFields(stream)
|
|
2159
|
+
].join("\n")
|
|
2160
|
+
);
|
|
2161
|
+
if (!options.json)
|
|
2162
|
+
deps.notice(
|
|
2163
|
+
[
|
|
2164
|
+
`Next: append with POST ${stream.url} and Content-Type: ${stream.content_type}`,
|
|
2165
|
+
` read from the start with GET ${stream.url}?offset=-1`
|
|
2166
|
+
].join("\n")
|
|
2167
|
+
);
|
|
2168
|
+
}
|
|
2169
|
+
async function streamShowCommand(deps, name, options = {}) {
|
|
2170
|
+
const stream = await deps.api.getStream(await deps.auth.apiKey(), name);
|
|
2171
|
+
print(deps, options.json, stream, streamFields(stream));
|
|
2172
|
+
}
|
|
2173
|
+
async function streamUpdateCommand(deps, name, options = {}) {
|
|
2174
|
+
if (options.public === options.private)
|
|
2175
|
+
throw new Error("Pass exactly one of --public and --private.");
|
|
2176
|
+
const stream = await deps.api.updateStream(await deps.auth.apiKey(), name, {
|
|
2177
|
+
public_read: options.public === true
|
|
2178
|
+
});
|
|
2179
|
+
print(
|
|
2180
|
+
deps,
|
|
2181
|
+
options.json,
|
|
2182
|
+
stream,
|
|
2183
|
+
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.`
|
|
2184
|
+
);
|
|
2185
|
+
}
|
|
2186
|
+
async function streamDeleteCommand(deps, name, options = {}) {
|
|
2187
|
+
await confirm(
|
|
2188
|
+
deps,
|
|
2189
|
+
options.yes,
|
|
2190
|
+
`delete stream ${name}`,
|
|
2191
|
+
() => Promise.resolve(
|
|
2192
|
+
`Delete stream ${name} and everything in it? This cannot be undone. Type the stream's name to confirm:`
|
|
2193
|
+
),
|
|
2194
|
+
name
|
|
2195
|
+
);
|
|
2196
|
+
const stream = await deps.api.deleteStream(await deps.auth.apiKey(), name);
|
|
2197
|
+
print(
|
|
2198
|
+
deps,
|
|
2199
|
+
options.json,
|
|
2200
|
+
stream,
|
|
2201
|
+
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.`
|
|
2202
|
+
);
|
|
2203
|
+
}
|
|
2204
|
+
async function streamUsageCommand(deps, options = {}) {
|
|
2205
|
+
const usage = await deps.api.listStreamUsage(
|
|
2206
|
+
await deps.auth.apiKey(),
|
|
2207
|
+
{ limit: options.limit, cursor: options.cursor },
|
|
2208
|
+
{ period: options.period, sort: options.sort, prefix: options.prefix, names: options.names }
|
|
2209
|
+
);
|
|
2210
|
+
printResourceUsage(deps, options.json, usage, "stream");
|
|
2211
|
+
}
|
|
1972
2212
|
async function usageShowCommand(deps, options = {}) {
|
|
1973
2213
|
const usage = await deps.api.getUsage(await deps.auth.apiKey(), options.period);
|
|
1974
2214
|
print(
|
|
@@ -2002,6 +2242,7 @@ var MACHINE_ERROR_HINTS = {
|
|
|
2002
2242
|
hostname_too_long: "<name>-<account> must fit one hostname label; choose a shorter name.",
|
|
2003
2243
|
account_name_unclaimed: "Claim a Tonbo ID for the account on the console's Profile page, then run the command again.",
|
|
2004
2244
|
image_platform_unsupported: "Machines run linux/arm64: publish the image for that platform (for example `docker buildx build --platform linux/arm64`).",
|
|
2245
|
+
image_not_offered: "Only the platform image runs until image pulls ship: create or update the Machine with `--image tonbo.dev/platform`.",
|
|
2005
2246
|
shape_unavailable: "`tonbo machine shapes` lists the shapes a Machine may name now.",
|
|
2006
2247
|
port_reserved: "Serve the application on another port.",
|
|
2007
2248
|
revision_mismatch: "The Machine changed since it was read; `tonbo machine show` it and run the update again.",
|
|
@@ -2038,6 +2279,10 @@ function idleActionText(machine) {
|
|
|
2038
2279
|
function machineState(machine) {
|
|
2039
2280
|
return machine.failure ? `${machine.state}: ${machine.failure.code}: ${machine.failure.message}` : machine.state;
|
|
2040
2281
|
}
|
|
2282
|
+
function imageText(machine) {
|
|
2283
|
+
const { reference, digest } = machine.image;
|
|
2284
|
+
return `${reference} (${digest ?? "platform image"})`;
|
|
2285
|
+
}
|
|
2041
2286
|
function machineFields(machine) {
|
|
2042
2287
|
return fields([
|
|
2043
2288
|
["Name", machine.name],
|
|
@@ -2046,7 +2291,7 @@ function machineFields(machine) {
|
|
|
2046
2291
|
["State", machineState(machine)],
|
|
2047
2292
|
["Power", machine.status.power],
|
|
2048
2293
|
["Shape", machine.shape],
|
|
2049
|
-
["Image",
|
|
2294
|
+
["Image", imageText(machine)],
|
|
2050
2295
|
["Command", machine.command ? machine.command.join(" ") : "the image's own"],
|
|
2051
2296
|
["Ports", machinePortsText(machine)],
|
|
2052
2297
|
["HTTPS", machine.hostnames.https],
|
|
@@ -2128,6 +2373,11 @@ async function machineListCommand(deps, options = {}) {
|
|
|
2128
2373
|
);
|
|
2129
2374
|
});
|
|
2130
2375
|
}
|
|
2376
|
+
function createdText(machine) {
|
|
2377
|
+
if (machine.state === "creating")
|
|
2378
|
+
return `Machine ${machine.name} is being created; \`tonbo machine show ${machine.name}\` reports it active when it is ready.`;
|
|
2379
|
+
return `Created Machine ${machine.name}.`;
|
|
2380
|
+
}
|
|
2131
2381
|
async function machineCreateCommand(deps, name, command, options = {}) {
|
|
2132
2382
|
await machineCommand(async () => {
|
|
2133
2383
|
if (!options.image) throw new Error("Pass --image <reference>.");
|
|
@@ -2151,15 +2401,13 @@ async function machineCreateCommand(deps, name, command, options = {}) {
|
|
|
2151
2401
|
},
|
|
2152
2402
|
options.idempotencyKey ?? randomUUID3()
|
|
2153
2403
|
);
|
|
2154
|
-
|
|
2155
|
-
deps,
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
].join("\n")
|
|
2162
|
-
);
|
|
2404
|
+
if (machine.state === "failed") {
|
|
2405
|
+
print(deps, options.json, machine, machineFields(machine));
|
|
2406
|
+
throw new Error(
|
|
2407
|
+
`Machine ${machine.name} failed to be created; delete it with \`tonbo machine delete ${machine.name}\` and create it again.`
|
|
2408
|
+
);
|
|
2409
|
+
}
|
|
2410
|
+
print(deps, options.json, machine, [createdText(machine), machineFields(machine)].join("\n"));
|
|
2163
2411
|
});
|
|
2164
2412
|
}
|
|
2165
2413
|
async function machineShowCommand(deps, name, options = {}) {
|
|
@@ -2360,6 +2608,20 @@ function machineSpecOptions(command, update) {
|
|
|
2360
2608
|
parseWhole
|
|
2361
2609
|
);
|
|
2362
2610
|
}
|
|
2611
|
+
function parseExpiresIn(value) {
|
|
2612
|
+
try {
|
|
2613
|
+
return expiresInSeconds(value);
|
|
2614
|
+
} catch (error) {
|
|
2615
|
+
throw new InvalidArgumentError(error.message);
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
function parseUsageSort(value) {
|
|
2619
|
+
try {
|
|
2620
|
+
return usageSort(value);
|
|
2621
|
+
} catch (error) {
|
|
2622
|
+
throw new InvalidArgumentError(error.message);
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2363
2625
|
function createProgram(dependencies = createDependencies) {
|
|
2364
2626
|
const program = new Command().name("tonbo").description("Sign in to Tonbo and manage account resources.").version(packageVersion.version);
|
|
2365
2627
|
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 +2685,14 @@ function createProgram(dependencies = createDependencies) {
|
|
|
2423
2685
|
"--database-prefix <prefix>",
|
|
2424
2686
|
"narrow the key to the databases whose name starts with this, for its whole life; repeat for several; never with --bucket",
|
|
2425
2687
|
repeated
|
|
2688
|
+
).option(
|
|
2689
|
+
"--stream <name>",
|
|
2690
|
+
"narrow the key to this raw stream, for its whole life; repeat for several; never with --bucket",
|
|
2691
|
+
repeated
|
|
2692
|
+
).option(
|
|
2693
|
+
"--stream-prefix <prefix>",
|
|
2694
|
+
"narrow the key to the raw streams whose name starts with this, for its whole life; repeat for several; never with --bucket",
|
|
2695
|
+
repeated
|
|
2426
2696
|
).addOption(
|
|
2427
2697
|
new Option("--expires-in <period>", "when the key expires; never by default").choices(
|
|
2428
2698
|
apiKeyExpirations
|
|
@@ -2483,16 +2753,26 @@ function createProgram(dependencies = createDependencies) {
|
|
|
2483
2753
|
database.command("create <name>").description("create a database; its region is fixed for its life").option(
|
|
2484
2754
|
"--region <region>",
|
|
2485
2755
|
"the database's region (see tonbo region list); the database default region by default"
|
|
2756
|
+
).addOption(
|
|
2757
|
+
new Option(
|
|
2758
|
+
"--expires-at <time>",
|
|
2759
|
+
"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"
|
|
2760
|
+
).conflicts("expiresIn")
|
|
2761
|
+
).addOption(
|
|
2762
|
+
new Option(
|
|
2763
|
+
"--expires-in <duration>",
|
|
2764
|
+
"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"
|
|
2765
|
+
).argParser(parseExpiresIn).conflicts("expiresAt")
|
|
2486
2766
|
).option(
|
|
2487
2767
|
"--idempotency-key <key>",
|
|
2488
|
-
"reuse the key of an unconfirmed creation to get the database it created"
|
|
2768
|
+
"reuse the key of an unconfirmed creation to get the database it created; with --expires-at, never --expires-in"
|
|
2489
2769
|
),
|
|
2490
2770
|
"createDatabase"
|
|
2491
2771
|
).action(
|
|
2492
2772
|
async (name, options) => databaseCreateCommand(dependencies(), name, options)
|
|
2493
2773
|
);
|
|
2494
2774
|
operationOptions(
|
|
2495
|
-
database.command("show <name>").description("show a database, its URL and size"),
|
|
2775
|
+
database.command("show <name>").description("show a database, its URL, expiry and size"),
|
|
2496
2776
|
"getDatabase"
|
|
2497
2777
|
).action(
|
|
2498
2778
|
async (name, options) => databaseShowCommand(dependencies(), name, options)
|
|
@@ -2513,21 +2793,70 @@ function createProgram(dependencies = createDependencies) {
|
|
|
2513
2793
|
database.command("usage").description("each database's usage of one month at list price, before allowance and credit").option(
|
|
2514
2794
|
"--period <month>",
|
|
2515
2795
|
"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"),
|
|
2796
|
+
).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
2797
|
"listDatabaseUsage"
|
|
2528
2798
|
).action(
|
|
2529
2799
|
async (options) => databaseUsageCommand(dependencies(), options)
|
|
2530
2800
|
);
|
|
2801
|
+
const stream = program.command("stream").description("the account's raw durable streams");
|
|
2802
|
+
operationOptions(
|
|
2803
|
+
stream.command("list").description("list the streams in every region").option("--prefix <prefix>", "only streams whose name starts with this"),
|
|
2804
|
+
"listStreams"
|
|
2805
|
+
).action(
|
|
2806
|
+
async (options) => streamListCommand(dependencies(), options)
|
|
2807
|
+
);
|
|
2808
|
+
operationOptions(
|
|
2809
|
+
stream.command("create <name>").description("create a stream; its region and content type are fixed for its life").option(
|
|
2810
|
+
"--region <region>",
|
|
2811
|
+
"the stream's region (see tonbo region list); the database default region by default"
|
|
2812
|
+
).option(
|
|
2813
|
+
"--content-type <type>",
|
|
2814
|
+
"the media type every append sends, such as application/json; application/octet-stream by default"
|
|
2815
|
+
).addOption(
|
|
2816
|
+
new Option(
|
|
2817
|
+
"--expires-at <time>",
|
|
2818
|
+
"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"
|
|
2819
|
+
).conflicts("expiresIn")
|
|
2820
|
+
).addOption(
|
|
2821
|
+
new Option(
|
|
2822
|
+
"--expires-in <duration>",
|
|
2823
|
+
"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"
|
|
2824
|
+
).argParser(parseExpiresIn).conflicts("expiresAt")
|
|
2825
|
+
).option(
|
|
2826
|
+
"--idempotency-key <key>",
|
|
2827
|
+
"reuse the key of an unconfirmed creation to get the stream it created; with --expires-at, never --expires-in"
|
|
2828
|
+
),
|
|
2829
|
+
"createStream"
|
|
2830
|
+
).action(
|
|
2831
|
+
async (name, options) => streamCreateCommand(dependencies(), name, options)
|
|
2832
|
+
);
|
|
2833
|
+
operationOptions(
|
|
2834
|
+
stream.command("show <name>").description("show a stream, its URL, content type, expiry and size"),
|
|
2835
|
+
"getStream"
|
|
2836
|
+
).action(
|
|
2837
|
+
async (name, options) => streamShowCommand(dependencies(), name, options)
|
|
2838
|
+
);
|
|
2839
|
+
operationOptions(
|
|
2840
|
+
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")),
|
|
2841
|
+
"updateStream"
|
|
2842
|
+
).action(
|
|
2843
|
+
async (name, options) => streamUpdateCommand(dependencies(), name, options)
|
|
2844
|
+
);
|
|
2845
|
+
operationOptions(
|
|
2846
|
+
stream.command("delete <name>").description("delete a stream and everything in it").option("--yes", "delete without asking; required when stdin is not a terminal"),
|
|
2847
|
+
"deleteStream"
|
|
2848
|
+
).action(
|
|
2849
|
+
async (name, options) => streamDeleteCommand(dependencies(), name, options)
|
|
2850
|
+
);
|
|
2851
|
+
operationOptions(
|
|
2852
|
+
stream.command("usage").description("each stream's usage of one month at list price, before allowance and credit").option(
|
|
2853
|
+
"--period <month>",
|
|
2854
|
+
"the calendar month in UTC, as YYYY-MM; the current one by default"
|
|
2855
|
+
).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"),
|
|
2856
|
+
"listStreamUsage"
|
|
2857
|
+
).action(
|
|
2858
|
+
async (options) => streamUsageCommand(dependencies(), options)
|
|
2859
|
+
);
|
|
2531
2860
|
const machine = program.command("machine").description("the account's Machines");
|
|
2532
2861
|
operationOptions(
|
|
2533
2862
|
machine.command("list").description("list the Machines in every region"),
|
|
@@ -2538,8 +2867,11 @@ function createProgram(dependencies = createDependencies) {
|
|
|
2538
2867
|
operationOptions(
|
|
2539
2868
|
machineSpecOptions(
|
|
2540
2869
|
machine.command("create <name> [command...]").description(
|
|
2541
|
-
"create a Machine from
|
|
2542
|
-
).requiredOption(
|
|
2870
|
+
"create a Machine from an image; [command...] after -- replaces the image's CMD"
|
|
2871
|
+
).requiredOption(
|
|
2872
|
+
"--image <reference>",
|
|
2873
|
+
"the image the Machine boots: tonbo.dev/platform, or a public linux/arm64 OCI image once registry images are offered"
|
|
2874
|
+
).option(
|
|
2543
2875
|
"--region <region>",
|
|
2544
2876
|
"the Machine's region, fixed for its life; the account's default region by default"
|
|
2545
2877
|
),
|
|
@@ -2562,7 +2894,7 @@ function createProgram(dependencies = createDependencies) {
|
|
|
2562
2894
|
machineSpecOptions(
|
|
2563
2895
|
machine.command("update <name> [command...]").description(
|
|
2564
2896
|
"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(
|
|
2897
|
+
).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
2898
|
"--revision <revision>",
|
|
2567
2899
|
"write only if the Machine is still at this revision (tonbo machine show); the revision it reads by default",
|
|
2568
2900
|
parseWhole
|