@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.
Files changed (3) hide show
  1. package/README.md +32 -12
  2. package/dist/bin/tonbo.js +387 -55
  3. 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 `--database-prefix` (repeat each for several), narrow the key for its whole life; `--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.
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 ghcr.io/acme/web:1.4 --port 8080:public --env MODE=prod
139
- tonbo machine create worker --image python:3.13 -- python -m worker
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 a public OCI image with a linux/arm64 variant, 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.
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 the image to its linux/arm64 digest 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.
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 narrowedByDatabase(apiKey) {
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 narrowedByDatabase(apiKey) ? "none" : "all";
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
- if (narrowedByDatabase(apiKey))
1623
- return [...apiKey.databases, ...apiKey.database_prefixes.map((prefix) => `${prefix}*`)].join(
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 databaseAccess(database) {
1815
- return database.public_read ? "public" : "private";
1912
+ function readAccess(resource) {
1913
+ return resource.public_read ? "public" : "private";
1816
1914
  }
1817
- function databaseSize(database) {
1818
- const { log, snapshot } = database.stored_bytes;
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", databaseAccess(database)],
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", databaseSize(database)],
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
- databaseAccess(database),
1961
+ readAccess(database),
1863
1962
  when(database.last_write_at),
1864
- databaseSize(database),
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 databaseUsageSort(value) {
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
- async function databaseUsageCommand(deps, options = {}) {
1935
- const usage = await deps.api.listDatabaseUsage(
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
- `Database 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")})`,
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
- ) : "No database usage.",
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", `${machine.image.reference} (${machine.image.digest})`],
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
- print(
2155
- deps,
2156
- options.json,
2157
- machine,
2158
- [
2159
- machine.state === "active" ? `Created Machine ${machine.name}.` : `Machine ${machine.name} is being created; \`tonbo machine show ${machine.name}\` reports it active when it is ready.`,
2160
- machineFields(machine)
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 a public linux/arm64 image; [command...] after -- replaces the image's CMD"
2542
- ).requiredOption("--image <reference>", "the public OCI image the Machine boots").option(
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tonbo/cli",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "The Tonbo CLI: sign in and manage account resources from the command line.",
5
5
  "homepage": "https://tonbo.dev",
6
6
  "bugs": {