@tonbo/cli 0.6.1 → 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 +55 -9
- package/dist/bin/tonbo.js +861 -43
- 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,26 +46,39 @@ 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
|
-
| `tonbo database usage [--period <YYYY-MM>] [--sort list_value\|name] [--prefix <prefix>] [--limit <n>] [--cursor <c>]` | `listDatabaseUsage` | `GET /v1/usage/databases` |
|
|
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` |
|
|
61
|
+
| `tonbo machine list [--limit <n>] [--cursor <c>]` | `listMachines` | `GET /v1/machines` |
|
|
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` |
|
|
63
|
+
| `tonbo machine show <name>` | `getMachine` | `GET /v1/machines/{name}` |
|
|
64
|
+
| `tonbo machine update <name> [--image <reference>] [--image-command] [--shape <shape>] [--port <port[:access]>]… [--clear-ports] [--default-port <port>] [--env <KEY=VALUE>]… [--unset-env <KEY>]… [--sleep-after <seconds>] [--idle-action <action>] [--stop-grace <seconds>] [--discard-snapshot] [--revision <n>] [-- <command>…]` | `updateMachine` | `PATCH /v1/machines/{name}` |
|
|
65
|
+
| `tonbo machine sleep <name>` | `sleepMachine` | `POST /v1/machines/{name}/sleep` |
|
|
66
|
+
| `tonbo machine delete <name> [--yes]` | `deleteMachine` | `DELETE /v1/machines/{name}` |
|
|
67
|
+
| `tonbo machine shapes` | `listMachineShapes` | `GET /v1/machine-shapes` |
|
|
55
68
|
|
|
56
69
|
### Output and errors
|
|
57
70
|
|
|
58
|
-
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`
|
|
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).
|
|
59
72
|
|
|
60
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`.
|
|
61
74
|
|
|
62
75
|
### Retried creations
|
|
63
76
|
|
|
64
|
-
`agent create`, `bucket create` and `
|
|
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.
|
|
65
78
|
|
|
66
79
|
### Destructive commands
|
|
67
80
|
|
|
68
|
-
`api-key revoke`, `bucket delete` and `
|
|
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.
|
|
69
82
|
|
|
70
83
|
## Account
|
|
71
84
|
|
|
@@ -96,7 +109,7 @@ tonbo api-key create ci-reader --member pi-live --read-only --bucket events --ex
|
|
|
96
109
|
tonbo api-key revoke 70000000-0000-4000-8000-000000000007
|
|
97
110
|
```
|
|
98
111
|
|
|
99
|
-
`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.
|
|
100
113
|
|
|
101
114
|
## Buckets
|
|
102
115
|
|
|
@@ -112,18 +125,51 @@ tonbo bucket delete events
|
|
|
112
125
|
|
|
113
126
|
```console
|
|
114
127
|
tonbo database create notes
|
|
128
|
+
tonbo database create scratch --expires-in 24h
|
|
115
129
|
tonbo database show notes
|
|
116
130
|
```
|
|
117
131
|
|
|
118
|
-
`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:
|
|
119
133
|
|
|
120
134
|
```text
|
|
121
|
-
Next: npm i @tonbo/sqlite-ursula
|
|
135
|
+
Next: npm i @tonbo/sqlite-ursula@0.11.2
|
|
122
136
|
open("notes") from "@tonbo/sqlite-ursula/tonbo"
|
|
123
137
|
```
|
|
124
138
|
|
|
125
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.
|
|
126
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
|
+
|
|
152
|
+
## Machines
|
|
153
|
+
|
|
154
|
+
```console
|
|
155
|
+
tonbo machine shapes
|
|
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'
|
|
158
|
+
tonbo machine update web --env MODE=staging --sleep-after 300
|
|
159
|
+
tonbo machine sleep web
|
|
160
|
+
tonbo machine delete web
|
|
161
|
+
```
|
|
162
|
+
|
|
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)`.
|
|
166
|
+
|
|
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.
|
|
168
|
+
|
|
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.
|
|
170
|
+
|
|
171
|
+
A refusal of machine-v2's `errors` prints the server's detail and then what to do next, for example `machine shapes` for `shape_unavailable` or a shorter name for `hostname_too_long`.
|
|
172
|
+
|
|
127
173
|
## Usage
|
|
128
174
|
|
|
129
175
|
`usage show` prints the account's rated usage of one calendar month in UTC, the current one unless `--period YYYY-MM` names another: one line per product, region, dimension and model with its quantity and amount, and the total, in US dollars.
|
package/dist/bin/tonbo.js
CHANGED
|
@@ -544,11 +544,144 @@ var managementOperations = {
|
|
|
544
544
|
"period",
|
|
545
545
|
"sort",
|
|
546
546
|
"prefix",
|
|
547
|
+
"names",
|
|
547
548
|
"cursor",
|
|
548
549
|
"limit"
|
|
549
550
|
],
|
|
550
551
|
"idempotencyKey": false,
|
|
551
552
|
"cli": "tonbo database usage"
|
|
553
|
+
},
|
|
554
|
+
"listMachines": {
|
|
555
|
+
"method": "GET",
|
|
556
|
+
"path": "/machines",
|
|
557
|
+
"pathParams": [],
|
|
558
|
+
"query": [
|
|
559
|
+
"cursor",
|
|
560
|
+
"limit"
|
|
561
|
+
],
|
|
562
|
+
"idempotencyKey": false,
|
|
563
|
+
"cli": "tonbo machine list"
|
|
564
|
+
},
|
|
565
|
+
"createMachine": {
|
|
566
|
+
"method": "POST",
|
|
567
|
+
"path": "/machines",
|
|
568
|
+
"pathParams": [],
|
|
569
|
+
"query": [],
|
|
570
|
+
"idempotencyKey": true,
|
|
571
|
+
"cli": "tonbo machine create"
|
|
572
|
+
},
|
|
573
|
+
"getMachine": {
|
|
574
|
+
"method": "GET",
|
|
575
|
+
"path": "/machines/{name}",
|
|
576
|
+
"pathParams": [
|
|
577
|
+
"name"
|
|
578
|
+
],
|
|
579
|
+
"query": [],
|
|
580
|
+
"idempotencyKey": false,
|
|
581
|
+
"cli": "tonbo machine show"
|
|
582
|
+
},
|
|
583
|
+
"updateMachine": {
|
|
584
|
+
"method": "PATCH",
|
|
585
|
+
"path": "/machines/{name}",
|
|
586
|
+
"pathParams": [
|
|
587
|
+
"name"
|
|
588
|
+
],
|
|
589
|
+
"query": [],
|
|
590
|
+
"idempotencyKey": false,
|
|
591
|
+
"cli": "tonbo machine update"
|
|
592
|
+
},
|
|
593
|
+
"deleteMachine": {
|
|
594
|
+
"method": "DELETE",
|
|
595
|
+
"path": "/machines/{name}",
|
|
596
|
+
"pathParams": [
|
|
597
|
+
"name"
|
|
598
|
+
],
|
|
599
|
+
"query": [],
|
|
600
|
+
"idempotencyKey": false,
|
|
601
|
+
"cli": "tonbo machine delete"
|
|
602
|
+
},
|
|
603
|
+
"sleepMachine": {
|
|
604
|
+
"method": "POST",
|
|
605
|
+
"path": "/machines/{name}/sleep",
|
|
606
|
+
"pathParams": [
|
|
607
|
+
"name"
|
|
608
|
+
],
|
|
609
|
+
"query": [],
|
|
610
|
+
"idempotencyKey": false,
|
|
611
|
+
"cli": "tonbo machine sleep"
|
|
612
|
+
},
|
|
613
|
+
"listMachineShapes": {
|
|
614
|
+
"method": "GET",
|
|
615
|
+
"path": "/machine-shapes",
|
|
616
|
+
"pathParams": [],
|
|
617
|
+
"query": [],
|
|
618
|
+
"idempotencyKey": false,
|
|
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"
|
|
552
685
|
}
|
|
553
686
|
};
|
|
554
687
|
var accountRoles = [
|
|
@@ -706,6 +839,14 @@ var credentialsContract = {
|
|
|
706
839
|
]
|
|
707
840
|
}
|
|
708
841
|
};
|
|
842
|
+
var machineIdleActions = [
|
|
843
|
+
"suspend",
|
|
844
|
+
"release"
|
|
845
|
+
];
|
|
846
|
+
var machinePortAccess = [
|
|
847
|
+
"private",
|
|
848
|
+
"public"
|
|
849
|
+
];
|
|
709
850
|
|
|
710
851
|
// ../../packages/tonbo-cli-core/dist/src/credentials.js
|
|
711
852
|
var DIRECTORY_MODE = Number.parseInt(credentialsContract.permissions.directory_mode, 8);
|
|
@@ -1021,7 +1162,7 @@ var ManagementApi = class {
|
|
|
1021
1162
|
page(operationId, key, { limit, cursor } = {}, query = {}) {
|
|
1022
1163
|
return this.request(operationId, key, { query: { ...query, limit, cursor } });
|
|
1023
1164
|
}
|
|
1024
|
-
async request(operationId, key, { params = {}, query = {}, body, idempotencyKey }) {
|
|
1165
|
+
async request(operationId, key, { params = {}, query = {}, body, idempotencyKey, ifMatch }) {
|
|
1025
1166
|
const operation = managementOperations[operationId];
|
|
1026
1167
|
const path2 = operation.path.replace(/\{([^}]+)\}/g, (_, name) => {
|
|
1027
1168
|
const value = params[name];
|
|
@@ -1044,6 +1185,8 @@ var ManagementApi = class {
|
|
|
1044
1185
|
headers["content-type"] = "application/json";
|
|
1045
1186
|
if (idempotencyKey)
|
|
1046
1187
|
headers["idempotency-key"] = idempotencyKey;
|
|
1188
|
+
if (ifMatch)
|
|
1189
|
+
headers["if-match"] = ifMatch;
|
|
1047
1190
|
const queryString = search.toString();
|
|
1048
1191
|
const url = `${this.apiOrigin}${managementApiBasePath}${path2}${queryString ? `?${queryString}` : ""}`;
|
|
1049
1192
|
try {
|
|
@@ -1123,7 +1266,7 @@ var ManagementApi = class {
|
|
|
1123
1266
|
return this.call("createDatabase", key, {
|
|
1124
1267
|
body: withoutUndefined(request),
|
|
1125
1268
|
idempotencyKey
|
|
1126
|
-
}).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"));
|
|
1127
1270
|
}
|
|
1128
1271
|
getDatabase(key, name) {
|
|
1129
1272
|
return this.call("getDatabase", key, { params: { name } });
|
|
@@ -1138,9 +1281,71 @@ var ManagementApi = class {
|
|
|
1138
1281
|
listDatabaseUsage(key, page, query = {}) {
|
|
1139
1282
|
return this.page("listDatabaseUsage", key, page, query);
|
|
1140
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
|
+
}
|
|
1141
1308
|
getUsage(key, period) {
|
|
1142
1309
|
return this.call("getUsage", key, { query: { period } });
|
|
1143
1310
|
}
|
|
1311
|
+
listMachines(key, page) {
|
|
1312
|
+
return this.page("listMachines", key, page);
|
|
1313
|
+
}
|
|
1314
|
+
/** A replay with the same idempotency key returns the Machine the first
|
|
1315
|
+
* request created. */
|
|
1316
|
+
createMachine(key, request, idempotencyKey) {
|
|
1317
|
+
return this.call("createMachine", key, {
|
|
1318
|
+
body: withoutUndefined(request),
|
|
1319
|
+
idempotencyKey
|
|
1320
|
+
}).catch(unconfirmed("Machine creation", idempotencyKey, "the same arguments"));
|
|
1321
|
+
}
|
|
1322
|
+
getMachine(key, name) {
|
|
1323
|
+
return this.call("getMachine", key, { params: { name } });
|
|
1324
|
+
}
|
|
1325
|
+
/** Written only at `revision` (machine-v2 requires If-Match, the Machine's
|
|
1326
|
+
* ETag); a stale one is refused with `revision_mismatch`. */
|
|
1327
|
+
updateMachine(key, name, revision, change) {
|
|
1328
|
+
return this.call("updateMachine", key, {
|
|
1329
|
+
params: { name },
|
|
1330
|
+
body: withoutUndefined(change),
|
|
1331
|
+
ifMatch: `"${revision}"`
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
sleepMachine(key, name) {
|
|
1335
|
+
return this.call("sleepMachine", key, { params: { name } });
|
|
1336
|
+
}
|
|
1337
|
+
/** The Machine being deleted, or null when the API answered 204: the
|
|
1338
|
+
* name's create never reached its region, and only the name was released. */
|
|
1339
|
+
async deleteMachine(key, name) {
|
|
1340
|
+
const answer = await this.request("deleteMachine", key, {
|
|
1341
|
+
params: { name }
|
|
1342
|
+
});
|
|
1343
|
+
return answer?.data ?? null;
|
|
1344
|
+
}
|
|
1345
|
+
/** The whole answer: the shapes and the default one. */
|
|
1346
|
+
listMachineShapes(key) {
|
|
1347
|
+
return this.request("listMachineShapes", key, {});
|
|
1348
|
+
}
|
|
1144
1349
|
};
|
|
1145
1350
|
function withoutUndefined(value) {
|
|
1146
1351
|
return Object.fromEntries(Object.entries(value).filter(([, field]) => field !== void 0));
|
|
@@ -1495,19 +1700,23 @@ async function memberUpdateCommand(deps, member, options = {}) {
|
|
|
1495
1700
|
function keyAccess(apiKey) {
|
|
1496
1701
|
return apiKey.read_only ? "read-only" : "read-write";
|
|
1497
1702
|
}
|
|
1498
|
-
function
|
|
1499
|
-
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;
|
|
1500
1705
|
}
|
|
1501
1706
|
function keyBuckets(apiKey) {
|
|
1502
1707
|
if (apiKey.buckets.length) return apiKey.buckets.join(", ");
|
|
1503
|
-
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";
|
|
1504
1714
|
}
|
|
1505
1715
|
function keyDatabases(apiKey) {
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
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);
|
|
1511
1720
|
}
|
|
1512
1721
|
function apiKeyFields(apiKey) {
|
|
1513
1722
|
return fields([
|
|
@@ -1518,6 +1727,7 @@ function apiKeyFields(apiKey) {
|
|
|
1518
1727
|
["Access", keyAccess(apiKey)],
|
|
1519
1728
|
["Buckets", keyBuckets(apiKey)],
|
|
1520
1729
|
["Databases", keyDatabases(apiKey)],
|
|
1730
|
+
["Streams", keyStreams(apiKey)],
|
|
1521
1731
|
["Expires", when(apiKey.expires_at)],
|
|
1522
1732
|
["Last used", when(apiKey.last_used_at)],
|
|
1523
1733
|
["Created", apiKey.created_at]
|
|
@@ -1541,6 +1751,7 @@ async function apiKeyListCommand(deps, options = {}) {
|
|
|
1541
1751
|
"ACCESS",
|
|
1542
1752
|
"BUCKETS",
|
|
1543
1753
|
"DATABASES",
|
|
1754
|
+
"STREAMS",
|
|
1544
1755
|
"EXPIRES",
|
|
1545
1756
|
"LAST USED"
|
|
1546
1757
|
],
|
|
@@ -1552,6 +1763,7 @@ async function apiKeyListCommand(deps, options = {}) {
|
|
|
1552
1763
|
keyAccess(apiKey),
|
|
1553
1764
|
keyBuckets(apiKey),
|
|
1554
1765
|
keyDatabases(apiKey),
|
|
1766
|
+
keyStreams(apiKey),
|
|
1555
1767
|
when(apiKey.expires_at),
|
|
1556
1768
|
when(apiKey.last_used_at)
|
|
1557
1769
|
])
|
|
@@ -1568,6 +1780,8 @@ async function apiKeyCreateCommand(deps, name, options = {}) {
|
|
|
1568
1780
|
buckets: options.bucket,
|
|
1569
1781
|
databases: options.database,
|
|
1570
1782
|
database_prefixes: options.databasePrefix,
|
|
1783
|
+
streams: options.stream,
|
|
1784
|
+
stream_prefixes: options.streamPrefix,
|
|
1571
1785
|
expires_in: options.expiresIn
|
|
1572
1786
|
});
|
|
1573
1787
|
print(
|
|
@@ -1695,11 +1909,11 @@ async function bucketDeleteCommand(deps, name, options = {}) {
|
|
|
1695
1909
|
`Bucket ${bucket.name} is ${bucket.state}; its streams are being purged.`
|
|
1696
1910
|
);
|
|
1697
1911
|
}
|
|
1698
|
-
function
|
|
1699
|
-
return
|
|
1912
|
+
function readAccess(resource) {
|
|
1913
|
+
return resource.public_read ? "public" : "private";
|
|
1700
1914
|
}
|
|
1701
|
-
function
|
|
1702
|
-
const { log, snapshot } =
|
|
1915
|
+
function storedSize(resource) {
|
|
1916
|
+
const { log, snapshot } = resource.stored_bytes;
|
|
1703
1917
|
if (log === null && snapshot === null) return "unknown";
|
|
1704
1918
|
return `${bytes(log)} log, ${bytes(snapshot, "no")} snapshot`;
|
|
1705
1919
|
}
|
|
@@ -1712,11 +1926,12 @@ function databaseFields(database) {
|
|
|
1712
1926
|
["Kind", databaseOwner(database)],
|
|
1713
1927
|
["Region", database.region],
|
|
1714
1928
|
["State", database.state],
|
|
1715
|
-
["Read access",
|
|
1929
|
+
["Read access", readAccess(database)],
|
|
1716
1930
|
["URL", database.url],
|
|
1931
|
+
["Expires", database.expires_at ?? "never"],
|
|
1717
1932
|
["Incarnation", database.incarnation ?? "none yet"],
|
|
1718
1933
|
["Last write", when(database.last_write_at)],
|
|
1719
|
-
["Stored",
|
|
1934
|
+
["Stored", storedSize(database)],
|
|
1720
1935
|
["Created", `${database.created_at} by ${database.created_by}`]
|
|
1721
1936
|
]);
|
|
1722
1937
|
}
|
|
@@ -1743,19 +1958,44 @@ async function databaseListCommand(deps, options = {}) {
|
|
|
1743
1958
|
databaseOwner(database),
|
|
1744
1959
|
database.region,
|
|
1745
1960
|
database.state,
|
|
1746
|
-
|
|
1961
|
+
readAccess(database),
|
|
1747
1962
|
when(database.last_write_at),
|
|
1748
|
-
|
|
1963
|
+
storedSize(database),
|
|
1749
1964
|
database.url
|
|
1750
1965
|
])
|
|
1751
1966
|
) : "No databases."
|
|
1752
1967
|
);
|
|
1753
1968
|
noticeUnavailable(deps, page.unavailable_regions, "sizes");
|
|
1754
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
|
+
}
|
|
1755
1994
|
async function databaseCreateCommand(deps, name, options = {}) {
|
|
1995
|
+
const expiresAt = creationExpiry("database", options);
|
|
1756
1996
|
const database = await deps.api.createDatabase(
|
|
1757
1997
|
await deps.auth.apiKey(),
|
|
1758
|
-
{ name, region: options.region },
|
|
1998
|
+
{ name, region: options.region, expires_at: expiresAt },
|
|
1759
1999
|
options.idempotencyKey ?? randomUUID2()
|
|
1760
2000
|
);
|
|
1761
2001
|
print(
|
|
@@ -1771,7 +2011,7 @@ async function databaseCreateCommand(deps, name, options = {}) {
|
|
|
1771
2011
|
}
|
|
1772
2012
|
function databaseNextStep(name) {
|
|
1773
2013
|
return [
|
|
1774
|
-
"Next: npm i @tonbo/sqlite-ursula",
|
|
2014
|
+
"Next: npm i @tonbo/sqlite-ursula@0.11.2",
|
|
1775
2015
|
` open(${JSON.stringify(name)}) from "@tonbo/sqlite-ursula/tonbo"`
|
|
1776
2016
|
].join("\n");
|
|
1777
2017
|
}
|
|
@@ -1810,22 +2050,17 @@ async function databaseDeleteCommand(deps, name, options = {}) {
|
|
|
1810
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.`
|
|
1811
2051
|
);
|
|
1812
2052
|
}
|
|
1813
|
-
function
|
|
2053
|
+
function usageSort(value) {
|
|
1814
2054
|
if (value === "-list_value" || value === "list_value") return "-list_value";
|
|
1815
2055
|
if (value === "name") return "name";
|
|
1816
2056
|
throw new Error("--sort is list_value or name.");
|
|
1817
2057
|
}
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
await deps.auth.apiKey(),
|
|
1821
|
-
{ limit: options.limit, cursor: options.cursor },
|
|
1822
|
-
{ period: options.period, sort: options.sort, prefix: options.prefix }
|
|
1823
|
-
);
|
|
1824
|
-
if (options.json) deps.output(JSON.stringify(usage));
|
|
2058
|
+
function printResourceUsage(deps, json, usage, noun) {
|
|
2059
|
+
if (json) deps.output(JSON.stringify(usage));
|
|
1825
2060
|
else
|
|
1826
2061
|
deps.output(
|
|
1827
2062
|
[
|
|
1828
|
-
|
|
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")})`,
|
|
1829
2064
|
usage.data.length ? table(
|
|
1830
2065
|
[
|
|
1831
2066
|
"NAME",
|
|
@@ -1845,7 +2080,7 @@ async function databaseUsageCommand(deps, options = {}) {
|
|
|
1845
2080
|
row.snapshot_gib_months ?? "",
|
|
1846
2081
|
usd(row.list_value_micros)
|
|
1847
2082
|
])
|
|
1848
|
-
) :
|
|
2083
|
+
) : `No ${noun} usage.`,
|
|
1849
2084
|
`Other metered usage: ${usd(usage.other.list_value_micros)}`,
|
|
1850
2085
|
"Charges, after the allowance and credit, are in `tonbo usage show`."
|
|
1851
2086
|
].join("\n")
|
|
@@ -1853,6 +2088,127 @@ async function databaseUsageCommand(deps, options = {}) {
|
|
|
1853
2088
|
if (usage.next_cursor) deps.notice(`More results: run again with --cursor ${usage.next_cursor}`);
|
|
1854
2089
|
noticeUnavailable(deps, usage.unavailable_regions, "usage");
|
|
1855
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
|
+
}
|
|
1856
2212
|
async function usageShowCommand(deps, options = {}) {
|
|
1857
2213
|
const usage = await deps.api.getUsage(await deps.auth.apiKey(), options.period);
|
|
1858
2214
|
print(
|
|
@@ -1877,6 +2233,291 @@ async function usageShowCommand(deps, options = {}) {
|
|
|
1877
2233
|
);
|
|
1878
2234
|
}
|
|
1879
2235
|
|
|
2236
|
+
// src/machine.ts
|
|
2237
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
2238
|
+
var MACHINE_ERROR_HINTS = {
|
|
2239
|
+
region_unavailable: "Machines are not open there yet, or that region cannot be reached from here; nothing was created or changed.",
|
|
2240
|
+
name_taken: "Choose another name; `tonbo machine list` shows the account's Machines.",
|
|
2241
|
+
hostname_taken: "The hostname <name>-<account> is held by another Machine or account; choose another name.",
|
|
2242
|
+
hostname_too_long: "<name>-<account> must fit one hostname label; choose a shorter name.",
|
|
2243
|
+
account_name_unclaimed: "Claim a Tonbo ID for the account on the console's Profile page, then run the command again.",
|
|
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`.",
|
|
2246
|
+
shape_unavailable: "`tonbo machine shapes` lists the shapes a Machine may name now.",
|
|
2247
|
+
port_reserved: "Serve the application on another port.",
|
|
2248
|
+
revision_mismatch: "The Machine changed since it was read; `tonbo machine show` it and run the update again.",
|
|
2249
|
+
snapshot_would_be_discarded: "Pass --discard-snapshot to apply the change at the next boot, which then starts fresh.",
|
|
2250
|
+
database_limit_reached: "Each Machine counts one database: delete a Machine or a database (`tonbo database list`) first.",
|
|
2251
|
+
idempotency_conflict: "That idempotency key belongs to another request: leave out --idempotency-key to create a new Machine.",
|
|
2252
|
+
create_rate_limited: "Too many Machines were created in the last hour; try again later."
|
|
2253
|
+
};
|
|
2254
|
+
function explainMachineError(error) {
|
|
2255
|
+
if (!(error instanceof HttpError) || error.code === null) return error;
|
|
2256
|
+
const hint = MACHINE_ERROR_HINTS[error.code];
|
|
2257
|
+
if (!hint) return error;
|
|
2258
|
+
return new HttpError(`${error.message}
|
|
2259
|
+
${hint}`, error.status, error.body, error.requestId);
|
|
2260
|
+
}
|
|
2261
|
+
async function machineCommand(run) {
|
|
2262
|
+
try {
|
|
2263
|
+
return await run();
|
|
2264
|
+
} catch (error) {
|
|
2265
|
+
throw explainMachineError(error);
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
function portText(port) {
|
|
2269
|
+
return `${port.port} ${port.access}${port.default ? " (default)" : ""}`;
|
|
2270
|
+
}
|
|
2271
|
+
function machinePortsText(machine) {
|
|
2272
|
+
return machine.ports.length ? machine.ports.map(portText).join(", ") : "none";
|
|
2273
|
+
}
|
|
2274
|
+
function idleActionText(machine) {
|
|
2275
|
+
const { action, reason } = machine.status.sleep;
|
|
2276
|
+
if (action === machine.idle_action) return action;
|
|
2277
|
+
return `${action} (${machine.idle_action} requested; ${reason.replaceAll("_", " ")})`;
|
|
2278
|
+
}
|
|
2279
|
+
function machineState(machine) {
|
|
2280
|
+
return machine.failure ? `${machine.state}: ${machine.failure.code}: ${machine.failure.message}` : machine.state;
|
|
2281
|
+
}
|
|
2282
|
+
function imageText(machine) {
|
|
2283
|
+
const { reference, digest } = machine.image;
|
|
2284
|
+
return `${reference} (${digest ?? "platform image"})`;
|
|
2285
|
+
}
|
|
2286
|
+
function machineFields(machine) {
|
|
2287
|
+
return fields([
|
|
2288
|
+
["Name", machine.name],
|
|
2289
|
+
["ID", machine.id],
|
|
2290
|
+
["Region", machine.region],
|
|
2291
|
+
["State", machineState(machine)],
|
|
2292
|
+
["Power", machine.status.power],
|
|
2293
|
+
["Shape", machine.shape],
|
|
2294
|
+
["Image", imageText(machine)],
|
|
2295
|
+
["Command", machine.command ? machine.command.join(" ") : "the image's own"],
|
|
2296
|
+
["Ports", machinePortsText(machine)],
|
|
2297
|
+
["HTTPS", machine.hostnames.https],
|
|
2298
|
+
["SSH", machine.hostnames.ssh],
|
|
2299
|
+
["Env", Object.keys(machine.env).sort().join(", ") || "none"],
|
|
2300
|
+
["Sleep after", `${machine.sleep_after_seconds}s without a connection`],
|
|
2301
|
+
["Idle action", idleActionText(machine)],
|
|
2302
|
+
["Stop grace", `${machine.stop_grace_seconds}s`],
|
|
2303
|
+
["Database", machine.database.name],
|
|
2304
|
+
["Revision", String(machine.revision)],
|
|
2305
|
+
["Created", machine.created_at],
|
|
2306
|
+
["Updated", machine.updated_at]
|
|
2307
|
+
]);
|
|
2308
|
+
}
|
|
2309
|
+
function parsePorts(specs, current) {
|
|
2310
|
+
const accesses = machinePortAccess;
|
|
2311
|
+
return specs.map((spec) => {
|
|
2312
|
+
const match = /^([0-9]{1,5})(?::([a-z]+))?$/.exec(spec);
|
|
2313
|
+
const access = match?.[2] ?? "private";
|
|
2314
|
+
if (!match || !accesses.includes(access))
|
|
2315
|
+
throw new Error(
|
|
2316
|
+
`--port ${spec}: give <port> or <port>:<access>, access ${accesses.join(" or ")}.`
|
|
2317
|
+
);
|
|
2318
|
+
const port = Number(match[1]);
|
|
2319
|
+
const before = current.find((entry) => entry.port === port && entry.access === access);
|
|
2320
|
+
return {
|
|
2321
|
+
port,
|
|
2322
|
+
access,
|
|
2323
|
+
default: false,
|
|
2324
|
+
...before?.public_limits === void 0 ? {} : { public_limits: before.public_limits }
|
|
2325
|
+
};
|
|
2326
|
+
});
|
|
2327
|
+
}
|
|
2328
|
+
function withDefault(ports, defaultPort) {
|
|
2329
|
+
if (ports.length === 0) {
|
|
2330
|
+
if (defaultPort !== void 0) throw new Error("--default-port needs a port to choose.");
|
|
2331
|
+
return ports;
|
|
2332
|
+
}
|
|
2333
|
+
const chosen = defaultPort ?? ports[0].port;
|
|
2334
|
+
if (!ports.some((entry) => entry.port === chosen))
|
|
2335
|
+
throw new Error(`--default-port ${chosen} is not one of the Machine's ports.`);
|
|
2336
|
+
return ports.map((entry) => ({ ...entry, default: entry.port === chosen }));
|
|
2337
|
+
}
|
|
2338
|
+
function parseEnv(pairs) {
|
|
2339
|
+
const env = {};
|
|
2340
|
+
for (const pair of pairs) {
|
|
2341
|
+
const split = pair.indexOf("=");
|
|
2342
|
+
if (split < 1) throw new Error(`--env ${pair}: give KEY=VALUE.`);
|
|
2343
|
+
env[pair.slice(0, split)] = pair.slice(split + 1);
|
|
2344
|
+
}
|
|
2345
|
+
return env;
|
|
2346
|
+
}
|
|
2347
|
+
async function machineListCommand(deps, options = {}) {
|
|
2348
|
+
await machineCommand(async () => {
|
|
2349
|
+
const page = await deps.api.listMachines(await deps.auth.apiKey(), {
|
|
2350
|
+
limit: options.limit,
|
|
2351
|
+
cursor: options.cursor
|
|
2352
|
+
});
|
|
2353
|
+
printPage(
|
|
2354
|
+
deps,
|
|
2355
|
+
options.json,
|
|
2356
|
+
page,
|
|
2357
|
+
(machines) => machines.length ? table(
|
|
2358
|
+
["NAME", "REGION", "STATE", "POWER", "SHAPE", "IMAGE", "HTTPS"],
|
|
2359
|
+
machines.map((machine) => [
|
|
2360
|
+
machine.name,
|
|
2361
|
+
machine.region,
|
|
2362
|
+
machine.state,
|
|
2363
|
+
machine.status.power,
|
|
2364
|
+
machine.shape,
|
|
2365
|
+
machine.image.reference,
|
|
2366
|
+
machine.hostnames.https
|
|
2367
|
+
])
|
|
2368
|
+
) : "No Machines."
|
|
2369
|
+
);
|
|
2370
|
+
if (page.unavailable_regions.length)
|
|
2371
|
+
deps.notice(
|
|
2372
|
+
`Could not read the Machines in ${page.unavailable_regions.join(", ")}; they are missing from this list.`
|
|
2373
|
+
);
|
|
2374
|
+
});
|
|
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
|
+
}
|
|
2381
|
+
async function machineCreateCommand(deps, name, command, options = {}) {
|
|
2382
|
+
await machineCommand(async () => {
|
|
2383
|
+
if (!options.image) throw new Error("Pass --image <reference>.");
|
|
2384
|
+
const env = parseEnv(options.env ?? []);
|
|
2385
|
+
const ports = withDefault(parsePorts(options.port ?? [], []), options.defaultPort);
|
|
2386
|
+
const key = await deps.auth.apiKey();
|
|
2387
|
+
const shape = options.shape ?? (await deps.api.listMachineShapes(key)).default;
|
|
2388
|
+
const machine = await deps.api.createMachine(
|
|
2389
|
+
key,
|
|
2390
|
+
{
|
|
2391
|
+
name,
|
|
2392
|
+
image: options.image,
|
|
2393
|
+
shape,
|
|
2394
|
+
region: options.region,
|
|
2395
|
+
command: command.length ? command : void 0,
|
|
2396
|
+
ports: ports.length ? ports : void 0,
|
|
2397
|
+
env: Object.keys(env).length ? env : void 0,
|
|
2398
|
+
sleep_after_seconds: options.sleepAfter,
|
|
2399
|
+
idle_action: options.idleAction,
|
|
2400
|
+
stop_grace_seconds: options.stopGrace
|
|
2401
|
+
},
|
|
2402
|
+
options.idempotencyKey ?? randomUUID3()
|
|
2403
|
+
);
|
|
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"));
|
|
2411
|
+
});
|
|
2412
|
+
}
|
|
2413
|
+
async function machineShowCommand(deps, name, options = {}) {
|
|
2414
|
+
await machineCommand(async () => {
|
|
2415
|
+
const machine = await deps.api.getMachine(await deps.auth.apiKey(), name);
|
|
2416
|
+
print(deps, options.json, machine, machineFields(machine));
|
|
2417
|
+
});
|
|
2418
|
+
}
|
|
2419
|
+
async function machineUpdateCommand(deps, name, command, options = {}) {
|
|
2420
|
+
await machineCommand(async () => {
|
|
2421
|
+
if (command.length && options.imageCommand)
|
|
2422
|
+
throw new Error("Give a command or --image-command, not both.");
|
|
2423
|
+
if (options.clearPorts && (options.port?.length || options.defaultPort !== void 0))
|
|
2424
|
+
throw new Error("Give --port or --clear-ports, not both.");
|
|
2425
|
+
const key = await deps.auth.apiKey();
|
|
2426
|
+
const current = await deps.api.getMachine(key, name);
|
|
2427
|
+
const change = {
|
|
2428
|
+
image: options.image,
|
|
2429
|
+
shape: options.shape,
|
|
2430
|
+
command: options.imageCommand ? null : command.length ? command : void 0,
|
|
2431
|
+
sleep_after_seconds: options.sleepAfter,
|
|
2432
|
+
idle_action: options.idleAction,
|
|
2433
|
+
stop_grace_seconds: options.stopGrace
|
|
2434
|
+
};
|
|
2435
|
+
if (options.clearPorts) change.ports = [];
|
|
2436
|
+
else if (options.port?.length) {
|
|
2437
|
+
const ports = parsePorts(options.port, current.ports);
|
|
2438
|
+
const kept = current.ports.find((entry) => entry.default)?.port;
|
|
2439
|
+
const defaultPort = options.defaultPort ?? (ports.some((entry) => entry.port === kept) ? kept : void 0);
|
|
2440
|
+
change.ports = withDefault(ports, defaultPort);
|
|
2441
|
+
} else if (options.defaultPort !== void 0)
|
|
2442
|
+
change.ports = withDefault(current.ports, options.defaultPort);
|
|
2443
|
+
if (options.env?.length || options.unsetEnv?.length) {
|
|
2444
|
+
const env = { ...current.env, ...parseEnv(options.env ?? []) };
|
|
2445
|
+
for (const variable of options.unsetEnv ?? []) delete env[variable];
|
|
2446
|
+
change.env = env;
|
|
2447
|
+
}
|
|
2448
|
+
if (options.discardSnapshot) change.discard_snapshot = true;
|
|
2449
|
+
if (Object.values(change).every((value) => value === void 0))
|
|
2450
|
+
throw new Error("Nothing to change: pass at least one option or a command.");
|
|
2451
|
+
const machine = await deps.api.updateMachine(
|
|
2452
|
+
key,
|
|
2453
|
+
name,
|
|
2454
|
+
options.revision ?? current.revision,
|
|
2455
|
+
change
|
|
2456
|
+
);
|
|
2457
|
+
print(
|
|
2458
|
+
deps,
|
|
2459
|
+
options.json,
|
|
2460
|
+
machine,
|
|
2461
|
+
[
|
|
2462
|
+
`Updated Machine ${machine.name} to revision ${machine.revision}. Ports and sleep-after apply now; image, command, shape and env at its next boot.`,
|
|
2463
|
+
machineFields(machine)
|
|
2464
|
+
].join("\n")
|
|
2465
|
+
);
|
|
2466
|
+
});
|
|
2467
|
+
}
|
|
2468
|
+
async function machineSleepCommand(deps, name, options = {}) {
|
|
2469
|
+
await machineCommand(async () => {
|
|
2470
|
+
const machine = await deps.api.sleepMachine(await deps.auth.apiKey(), name);
|
|
2471
|
+
print(
|
|
2472
|
+
deps,
|
|
2473
|
+
options.json,
|
|
2474
|
+
machine,
|
|
2475
|
+
`Machine ${machine.name} is going to sleep (${machine.status.sleep.action}); its next connection wakes it.`
|
|
2476
|
+
);
|
|
2477
|
+
});
|
|
2478
|
+
}
|
|
2479
|
+
async function machineDeleteCommand(deps, name, options = {}) {
|
|
2480
|
+
await machineCommand(async () => {
|
|
2481
|
+
await confirm(
|
|
2482
|
+
deps,
|
|
2483
|
+
options.yes,
|
|
2484
|
+
`delete Machine ${name}`,
|
|
2485
|
+
() => Promise.resolve(
|
|
2486
|
+
`Delete Machine ${name} with its disk, database and logs? This cannot be undone. Type the Machine's name to confirm:`
|
|
2487
|
+
),
|
|
2488
|
+
name
|
|
2489
|
+
);
|
|
2490
|
+
const machine = await deps.api.deleteMachine(await deps.auth.apiKey(), name);
|
|
2491
|
+
print(
|
|
2492
|
+
deps,
|
|
2493
|
+
options.json,
|
|
2494
|
+
machine,
|
|
2495
|
+
machine ? `Machine ${machine.name} is being deleted; its name is free for a new Machine.` : `Released the name ${name}; no Machine had been created under it.`
|
|
2496
|
+
);
|
|
2497
|
+
});
|
|
2498
|
+
}
|
|
2499
|
+
async function machineShapesCommand(deps, options = {}) {
|
|
2500
|
+
await machineCommand(async () => {
|
|
2501
|
+
const shapes = await deps.api.listMachineShapes(await deps.auth.apiKey());
|
|
2502
|
+
print(
|
|
2503
|
+
deps,
|
|
2504
|
+
options.json,
|
|
2505
|
+
shapes,
|
|
2506
|
+
table(
|
|
2507
|
+
["SHAPE", "VCPUS", "MEMORY", "SCRATCH", "OFFERED", "SUSPENDABLE"],
|
|
2508
|
+
shapes.data.map((shape) => [
|
|
2509
|
+
shape.shape === shapes.default ? `${shape.shape} (default)` : shape.shape,
|
|
2510
|
+
String(shape.vcpus),
|
|
2511
|
+
`${shape.memory_gib} GiB`,
|
|
2512
|
+
`${shape.scratch_gib} GiB`,
|
|
2513
|
+
shape.offered ? "yes" : "no",
|
|
2514
|
+
shape.suspendable ? "yes" : "no"
|
|
2515
|
+
])
|
|
2516
|
+
)
|
|
2517
|
+
);
|
|
2518
|
+
});
|
|
2519
|
+
}
|
|
2520
|
+
|
|
1880
2521
|
// src/prompt.ts
|
|
1881
2522
|
import select from "@inquirer/select";
|
|
1882
2523
|
import { createInterface } from "node:readline/promises";
|
|
@@ -1925,6 +2566,10 @@ function parseLimit(value) {
|
|
|
1925
2566
|
if (!/^[1-9][0-9]*$/.test(value)) throw new InvalidArgumentError("Not a positive integer.");
|
|
1926
2567
|
return Number(value);
|
|
1927
2568
|
}
|
|
2569
|
+
function parseWhole(value) {
|
|
2570
|
+
if (!/^[0-9]+$/.test(value)) throw new InvalidArgumentError("Not a whole number.");
|
|
2571
|
+
return Number(value);
|
|
2572
|
+
}
|
|
1928
2573
|
function operationOptions(command, operationId) {
|
|
1929
2574
|
const query = managementOperations[operationId].query;
|
|
1930
2575
|
if (query.includes("limit"))
|
|
@@ -1935,6 +2580,48 @@ function operationOptions(command, operationId) {
|
|
|
1935
2580
|
}
|
|
1936
2581
|
var repeated = (value, previous = []) => [...previous, value];
|
|
1937
2582
|
var roleOption = (description) => new Option("--role <role>", description).choices(accountRoles);
|
|
2583
|
+
function machineSpecOptions(command, update) {
|
|
2584
|
+
const later = update ? "; applies at the next boot" : "";
|
|
2585
|
+
return command.option("--shape <shape>", `the Machine's size (see tonbo machine shapes)${later}`).option(
|
|
2586
|
+
"--port <port[:access]>",
|
|
2587
|
+
`a port the Machine serves, private (the default) or public; repeat for several${update ? "; replaces every port" : ""}`,
|
|
2588
|
+
repeated
|
|
2589
|
+
).option(
|
|
2590
|
+
"--default-port <port>",
|
|
2591
|
+
`the port served at the Machine's hostname without a port prefix; the first --port by default${update ? ", or the current default while --port still lists it" : ""}`,
|
|
2592
|
+
parseWhole
|
|
2593
|
+
).option(
|
|
2594
|
+
"--env <KEY=VALUE>",
|
|
2595
|
+
`a non-secret environment variable; repeat for several${later}`,
|
|
2596
|
+
repeated
|
|
2597
|
+
).option(
|
|
2598
|
+
"--sleep-after <seconds>",
|
|
2599
|
+
"how long the Machine stays awake without a connection",
|
|
2600
|
+
parseWhole
|
|
2601
|
+
).addOption(
|
|
2602
|
+
new Option("--idle-action <action>", "what sleeping does to the Machine").choices(
|
|
2603
|
+
machineIdleActions
|
|
2604
|
+
)
|
|
2605
|
+
).option(
|
|
2606
|
+
"--stop-grace <seconds>",
|
|
2607
|
+
"the seconds between SIGTERM and SIGKILL on a planned stop",
|
|
2608
|
+
parseWhole
|
|
2609
|
+
);
|
|
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
|
+
}
|
|
1938
2625
|
function createProgram(dependencies = createDependencies) {
|
|
1939
2626
|
const program = new Command().name("tonbo").description("Sign in to Tonbo and manage account resources.").version(packageVersion.version);
|
|
1940
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));
|
|
@@ -1998,6 +2685,14 @@ function createProgram(dependencies = createDependencies) {
|
|
|
1998
2685
|
"--database-prefix <prefix>",
|
|
1999
2686
|
"narrow the key to the databases whose name starts with this, for its whole life; repeat for several; never with --bucket",
|
|
2000
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
|
|
2001
2696
|
).addOption(
|
|
2002
2697
|
new Option("--expires-in <period>", "when the key expires; never by default").choices(
|
|
2003
2698
|
apiKeyExpirations
|
|
@@ -2058,16 +2753,26 @@ function createProgram(dependencies = createDependencies) {
|
|
|
2058
2753
|
database.command("create <name>").description("create a database; its region is fixed for its life").option(
|
|
2059
2754
|
"--region <region>",
|
|
2060
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")
|
|
2061
2766
|
).option(
|
|
2062
2767
|
"--idempotency-key <key>",
|
|
2063
|
-
"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"
|
|
2064
2769
|
),
|
|
2065
2770
|
"createDatabase"
|
|
2066
2771
|
).action(
|
|
2067
2772
|
async (name, options) => databaseCreateCommand(dependencies(), name, options)
|
|
2068
2773
|
);
|
|
2069
2774
|
operationOptions(
|
|
2070
|
-
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"),
|
|
2071
2776
|
"getDatabase"
|
|
2072
2777
|
).action(
|
|
2073
2778
|
async (name, options) => databaseShowCommand(dependencies(), name, options)
|
|
@@ -2088,21 +2793,134 @@ function createProgram(dependencies = createDependencies) {
|
|
|
2088
2793
|
database.command("usage").description("each database's usage of one month at list price, before allowance and credit").option(
|
|
2089
2794
|
"--period <month>",
|
|
2090
2795
|
"the calendar month in UTC, as YYYY-MM; the current one by default"
|
|
2091
|
-
).option(
|
|
2092
|
-
"--sort <order>",
|
|
2093
|
-
"list_value (highest first, the default) or name",
|
|
2094
|
-
(value) => {
|
|
2095
|
-
try {
|
|
2096
|
-
return databaseUsageSort(value);
|
|
2097
|
-
} catch (error) {
|
|
2098
|
-
throw new InvalidArgumentError(error.message);
|
|
2099
|
-
}
|
|
2100
|
-
}
|
|
2101
|
-
).option("--prefix <prefix>", "only databases whose name starts with this"),
|
|
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"),
|
|
2102
2797
|
"listDatabaseUsage"
|
|
2103
2798
|
).action(
|
|
2104
2799
|
async (options) => databaseUsageCommand(dependencies(), options)
|
|
2105
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
|
+
);
|
|
2860
|
+
const machine = program.command("machine").description("the account's Machines");
|
|
2861
|
+
operationOptions(
|
|
2862
|
+
machine.command("list").description("list the Machines in every region"),
|
|
2863
|
+
"listMachines"
|
|
2864
|
+
).action(
|
|
2865
|
+
async (options) => machineListCommand(dependencies(), options)
|
|
2866
|
+
);
|
|
2867
|
+
operationOptions(
|
|
2868
|
+
machineSpecOptions(
|
|
2869
|
+
machine.command("create <name> [command...]").description(
|
|
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(
|
|
2875
|
+
"--region <region>",
|
|
2876
|
+
"the Machine's region, fixed for its life; the account's default region by default"
|
|
2877
|
+
),
|
|
2878
|
+
false
|
|
2879
|
+
).option(
|
|
2880
|
+
"--idempotency-key <key>",
|
|
2881
|
+
"reuse the key of an unconfirmed creation to get the Machine it created"
|
|
2882
|
+
),
|
|
2883
|
+
"createMachine"
|
|
2884
|
+
).action(
|
|
2885
|
+
async (name, command, options) => machineCreateCommand(dependencies(), name, command, options)
|
|
2886
|
+
);
|
|
2887
|
+
operationOptions(
|
|
2888
|
+
machine.command("show <name>").description("show a Machine, its hostnames and its state"),
|
|
2889
|
+
"getMachine"
|
|
2890
|
+
).action(
|
|
2891
|
+
async (name, options) => machineShowCommand(dependencies(), name, options)
|
|
2892
|
+
);
|
|
2893
|
+
operationOptions(
|
|
2894
|
+
machineSpecOptions(
|
|
2895
|
+
machine.command("update <name> [command...]").description(
|
|
2896
|
+
"change a Machine; [command...] after -- replaces its command at the next boot"
|
|
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(
|
|
2898
|
+
"--revision <revision>",
|
|
2899
|
+
"write only if the Machine is still at this revision (tonbo machine show); the revision it reads by default",
|
|
2900
|
+
parseWhole
|
|
2901
|
+
).option("--discard-snapshot", "apply a change that discards the Machine's snapshot"),
|
|
2902
|
+
true
|
|
2903
|
+
),
|
|
2904
|
+
"updateMachine"
|
|
2905
|
+
).action(
|
|
2906
|
+
async (name, command, options) => machineUpdateCommand(dependencies(), name, command, options)
|
|
2907
|
+
);
|
|
2908
|
+
operationOptions(
|
|
2909
|
+
machine.command("sleep <name>").description("put a Machine to sleep now"),
|
|
2910
|
+
"sleepMachine"
|
|
2911
|
+
).action(
|
|
2912
|
+
async (name, options) => machineSleepCommand(dependencies(), name, options)
|
|
2913
|
+
);
|
|
2914
|
+
operationOptions(
|
|
2915
|
+
machine.command("delete <name>").description("delete a Machine with its disk, database and logs").option("--yes", "delete without asking; required when stdin is not a terminal"),
|
|
2916
|
+
"deleteMachine"
|
|
2917
|
+
).action(
|
|
2918
|
+
async (name, options) => machineDeleteCommand(dependencies(), name, options)
|
|
2919
|
+
);
|
|
2920
|
+
operationOptions(
|
|
2921
|
+
machine.command("shapes").description("list the shapes a Machine may have"),
|
|
2922
|
+
"listMachineShapes"
|
|
2923
|
+
).action(async (options) => machineShapesCommand(dependencies(), options));
|
|
2106
2924
|
operationOptions(
|
|
2107
2925
|
program.command("usage").description("the account's rated usage").command("show").description("show one month's rated usage").option(
|
|
2108
2926
|
"--period <month>",
|