@tonbo/cli 0.9.0 → 0.10.0
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 +10 -6
- package/dist/bin/tonbo.js +376 -47
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,9 +14,9 @@ tonbo login [--account <name>]
|
|
|
14
14
|
tonbo logout
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
`login` prints the login URL and opens the browser at `https://tonbo.dev/cli/login`, where you pick the account (your personal account unless `--account` names another) and approve a personal API key named `CLI on <hostname>`. The browser returns a one-time code to a local callback on `127.0.0.1`, and the CLI redeems it with its PKCE verifier for the key. The key acts as you with your role in that account, expires after six months and replaces the key an earlier login on the same host created; there is no refresh, so an expired or revoked key asks you to run `tonbo login` again.
|
|
17
|
+
`login` prints the login URL and opens the browser at `https://tonbo.dev/cli/login`, where you pick the account (your personal account unless `--account` names another) and approve a personal API key named `CLI on <hostname>`. The browser returns a one-time code to a local callback on `127.0.0.1`, and the CLI redeems it with its PKCE verifier for the key. The key acts as you with your role in that account, expires after six months and replaces the key an earlier login on the same host created; there is no refresh, so an expired or revoked key asks you to run `tonbo login` again. `tonbo` acts in the account of the last login: log in again to switch accounts. The file keeps one login key per account of the computer beside it, for `yield` ([`credentials-v1.json`](../../contracts/cli/credentials-v1.json) `accounts`); a key in a non-personal account is named `Yield CLI · <8 hex>` instead of after the host. When no browser can be opened (an SSH session, a server without a desktop), the CLI keeps waiting: open the printed URL in a browser that can reach the callback's `127.0.0.1:<port>` on that machine, through `ssh -L <port>:127.0.0.1:<port>` or the VM's port forwarding.
|
|
18
18
|
|
|
19
|
-
The login and the credentials file are shared with [`yield`](../yield) through `@tonbo/cli-core`: either CLI's `login` signs both in, and either `logout` signs both out. The login is saved at `${XDG_CONFIG_HOME:-$HOME/.config}/tonbo/credentials.json` as `{version, account_id, key_id, key, expires_at}
|
|
19
|
+
The login and the credentials file are shared with [`yield`](../yield) through `@tonbo/cli-core`: either CLI's `login` signs both in, and either `logout` signs both out, revoking every account's key of the file. The login is saved at `${XDG_CONFIG_HOME:-$HOME/.config}/tonbo/credentials.json` as `{version, account_id, key_id, key, expires_at}`, with `accounts` and `login_secret` beside them. The CLI atomically replaces that file with owner-only permissions and refuses a symlink or a file other users can read. A file written by an earlier CLI release is not migrated; run `tonbo login`.
|
|
20
20
|
|
|
21
21
|
`logout` revokes the saved key through the Management API and removes the file. If the revocation fails, the file is still removed and the CLI reports the key id, so you can revoke it under API keys in the console.
|
|
22
22
|
|
|
@@ -27,7 +27,7 @@ The login and the credentials file are shared with [`yield`](../yield) through `
|
|
|
27
27
|
|
|
28
28
|
## Commands and the Management API
|
|
29
29
|
|
|
30
|
-
Every command other than `login` and `logout` is named by one operation of the [Management API](../../contracts/management/openapi-v1.yaml) through its `x-tonbo-cli` and calls that operation, plus reads of the same resource (a `--wait`,
|
|
30
|
+
Every command other than `login` and `logout` is named by one operation of the [Management API](../../contracts/management/openapi-v1.yaml) through its `x-tonbo-cli` and calls that operation, plus reads of the same resource (a `--wait`, the Machine a `name_taken` refusal names, or the activations `machine show` reads for the last end); `tonbo machine logs`, once it ships, is the named Streams exception, reading the stream at the Machine's `logs.url`. The command set and the operations tagged with a command are the same set, which `test/commands.test.ts` checks in both directions. The CLI resolves each request from the operation table of the internal package [`@tonbo/cli-core`](../../packages/tonbo-cli-core) (`src/generated/contracts.ts`, which `pnpm --filter @tonbo/cli-core generate:contracts` derives from the contract). `login` and `logout` are the named exception: they obtain and discard the key the other commands present as their Bearer credential. What a command may do is what your role in the logged-in account allows; the CLI never decides it.
|
|
31
31
|
|
|
32
32
|
| Command | Operation | Request |
|
|
33
33
|
| --- | --- | --- |
|
|
@@ -65,6 +65,7 @@ Every command other than `login` and `logout` is named by one operation of the [
|
|
|
65
65
|
| `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] [--if-match <etag>] [-- <command>…]` | `updateMachine` | `PATCH /v1/machines/{name}` |
|
|
66
66
|
| `tonbo machine sleep <name> [--if-match <etag>] [--wait [--timeout <seconds>]]` | `sleepMachine` | `POST /v1/machines/{name}/sleep` |
|
|
67
67
|
| `tonbo machine delete <name> [--yes] [--if-match <etag>]` | `deleteMachine` | `DELETE /v1/machines/{name}` |
|
|
68
|
+
| `tonbo machine activations <name> [--limit <n>] [--cursor <c>]` | `listMachineActivations` | `GET /v1/machines/{name}/activations` |
|
|
68
69
|
| `tonbo machine shapes` | `listMachineShapes` | `GET /v1/machine-shapes` |
|
|
69
70
|
|
|
70
71
|
### Output and errors
|
|
@@ -166,10 +167,11 @@ tonbo machine create web --image tonbo.dev/platform --port 8080:public --env MOD
|
|
|
166
167
|
tonbo machine create worker --image tonbo.dev/platform -- sh -c 'while true; do date; sleep 60; done'
|
|
167
168
|
tonbo machine update web --env MODE=staging --sleep-after 300
|
|
168
169
|
tonbo machine sleep web
|
|
170
|
+
tonbo machine activations web
|
|
169
171
|
tonbo machine delete web
|
|
170
172
|
```
|
|
171
173
|
|
|
172
|
-
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, `machine create`, `update`, `sleep` and `delete` are refused with `region_not_offered`, and the CLI says that nothing was created or changed; `machine list`, `show` and `shapes` still answer. A region that cannot be reached is refused with `region_unavailable`,
|
|
174
|
+
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, `machine create`, `update`, `sleep` and `delete` are refused with `region_not_offered`, and the CLI says that nothing was created or changed; `machine list`, `show` and `shapes` still answer. A region that cannot be reached is refused with `region_unavailable`, whose `Retry-After` says when a retry may clear it.
|
|
173
175
|
|
|
174
176
|
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)`.
|
|
175
177
|
|
|
@@ -177,9 +179,11 @@ The image is `tonbo.dev/platform`, the platform image each node release ships, o
|
|
|
177
179
|
|
|
178
180
|
`machine create` and `machine update` warn on stderr when the Machine ends up with no port (nothing can wake it until exec and SSH arrive) and when an `--env` key looks secret (`*_KEY`, `*_TOKEN`, `*SECRET*`, `*PASSWORD*`): env values are readable by every member through the API, and Machine secrets come later. When the default port is private, `machine create` and `machine show` print the `curl` call that reaches it with a personal key, and say that on a private port Tonbo authenticates the caller, so the app sees neither `Authorization` nor who called; an app with its own users serves a public port and does its own auth.
|
|
179
181
|
|
|
180
|
-
`machine show` prints the state and what it means (an `active` Machine that is asleep boots on its first request, which the edge holds while it boots), the live activation and conditions when there are any, when the Machine sleeps and that its disk resets at every sleep until suspend ships, the database and logs URLs, a link to the durability text (the CLI prints the link only, never the text or a paraphrase of it), and the
|
|
182
|
+
`machine show` prints the state and what it means (an `active` Machine that is asleep boots on its first request, which the edge holds while it boots), the live activation and conditions when there are any, when the Machine sleeps and that its disk resets at every sleep until suspend ships, the database and logs URLs, a link to the durability text (the CLI prints the link only, never the text or a paraphrase of it), the ETag and who created the Machine. Each condition comes with what to do: a refused wake (`wake_blocked`) names its reason, when it was first refused and, for a crash backoff, when a retry may pass; `creation_delayed` says the creation is taking longer than usual and goes on. For an `active` Machine, `machine show` also reads its newest activations and prints how the last one ended (`Last end`), and when the live activation was admitted at an older revision than the Machine's it prints `Spec N, admitted at M` and says that ports and sleep-after already apply and that a changed image, command, shape or env applies after `tonbo machine sleep <name>`, since every spec change advances the revision. `machine show` prints image states as the API answers them: `pulling`, `ready`, `failed` or `not_pulled`. `machine list` prints each Machine's conditions in its last column.
|
|
181
183
|
|
|
182
|
-
`machine
|
|
184
|
+
`machine activations` lists a Machine's activations, newest first: the wake, what woke it, the spec revision and shape it ran, when it started and how it ended (its state, the reason such as `idle_early`, `workload_exited` with the exit code, or a refusal such as `payment_required`), or `live`.
|
|
185
|
+
|
|
186
|
+
`machine update` changes only what its options name. It reads the Machine and sends the ETag it read, or `--if-match`'s, so a change someone made in between, or another Machine that took the name, is refused with `precondition_failed` rather than overwritten; a Machine whose creation failed is refused with `machine_failed`. `--env` and `--unset-env` change single variables; `--port` replaces every port, 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 apply at the next activation, and the command says how to apply them now: `machine sleep`, after which the next request boots the new spec; naming the same tag again re-resolves its digest. `machine sleep` puts an awake Machine to sleep now, and says so when it is already asleep or still creating; with `--wait` it reads the Machine after the answer's `Retry-After`, and again every `sleep.poll_after_seconds`, until it is asleep or `--timeout` (300 s by default) passes. `machine delete` sends the ETag it read when it asked for confirmation, and `machine sleep` and `machine delete` send `--if-match`'s when given. `--if-match` takes the ETag `machine show` prints, with or without its double quotes: a value with neither a leading `"` nor `W/"` is quoted again, since a shell drops the quotes of a pasted tag. `machine shapes` prints each shape's list price for an hour at full use. `machine delete` deletes the Machine with its disk, database and logs, and its name is free for a new Machine at once.
|
|
183
187
|
|
|
184
188
|
In a personal account, `machine list`, `machine show` and `machine delete` also cover your own computers, which `yield login` registers (Machines of kind `byoc`, which run nothing Tonbo operates). They are served even while Machines are not open in any region. `machine list` prints them in a second table with their system, agent harnesses, when they last reported and the state of their login key; `machine show` adds the hostname and device id, which only you see. `machine delete` forgets a computer: it disappears from the list at once and its name is free once the forget completes. Forgetting does not revoke the computer's key, so a computer whose key is still active registers again at its next start; the command says so and suggests `yield logout` on it or `tonbo api-key revoke` first. An organization lists managed Machines only.
|
|
185
189
|
|
package/dist/bin/tonbo.js
CHANGED
|
@@ -202,6 +202,7 @@ var agentNameContract = {
|
|
|
202
202
|
"artifacts",
|
|
203
203
|
"auth",
|
|
204
204
|
"inference",
|
|
205
|
+
"lb-check",
|
|
205
206
|
"network-health",
|
|
206
207
|
"sandbox-control",
|
|
207
208
|
"status",
|
|
@@ -221,6 +222,7 @@ var agentNameContract = {
|
|
|
221
222
|
"artifacts.tonbo.sh",
|
|
222
223
|
"auth.tonbo.sh",
|
|
223
224
|
"inference.tonbo.sh",
|
|
225
|
+
"lb-check.tonbo.sh",
|
|
224
226
|
"network-health.tonbo.sh",
|
|
225
227
|
"sandbox-control.tonbo.sh",
|
|
226
228
|
"status.tonbo.sh",
|
|
@@ -621,6 +623,19 @@ var managementOperations = {
|
|
|
621
623
|
"idempotencyKey": false,
|
|
622
624
|
"cli": "tonbo machine sleep"
|
|
623
625
|
},
|
|
626
|
+
"listMachineActivations": {
|
|
627
|
+
"method": "GET",
|
|
628
|
+
"path": "/machines/{name}/activations",
|
|
629
|
+
"pathParams": [
|
|
630
|
+
"name"
|
|
631
|
+
],
|
|
632
|
+
"query": [
|
|
633
|
+
"cursor",
|
|
634
|
+
"limit"
|
|
635
|
+
],
|
|
636
|
+
"idempotencyKey": false,
|
|
637
|
+
"cli": "tonbo machine activations"
|
|
638
|
+
},
|
|
624
639
|
"listMachineShapes": {
|
|
625
640
|
"method": "GET",
|
|
626
641
|
"path": "/machine-shapes",
|
|
@@ -709,7 +724,7 @@ var apiKeyExpirations = [
|
|
|
709
724
|
];
|
|
710
725
|
var credentialsContract = {
|
|
711
726
|
"version": 1,
|
|
712
|
-
"description": "The login `tonbo login` (and `yield login`) saves: a personal key of one account. @tonbo/cli-core writes and reads it;
|
|
727
|
+
"description": "The login `tonbo login` (and `yield login`) saves: a personal key of one account at the top level, the login made last, and, beside it, every account's login of this device in `accounts` with the device's `login_secret` (ADR-0016, \"Credentials\", PR 4b-ii). @tonbo/cli-core writes and reads it; the Tonbo SDK (@tonbo/sdk and tonbo-sdk, contracts/sdk/v1.json) reads it as its last key source, and every other reader follows `reader` and reads the top level alone.",
|
|
713
728
|
"path": {
|
|
714
729
|
"description": "`${XDG_CONFIG_HOME}/tonbo/credentials.json` when XDG_CONFIG_HOME is set and not empty, else `${HOME}/.config/tonbo/credentials.json`.",
|
|
715
730
|
"base_env": "XDG_CONFIG_HOME",
|
|
@@ -747,6 +762,13 @@ var credentialsContract = {
|
|
|
747
762
|
"expired": "A login whose `expires_at` is at or before now is expired: ask the person to run `tonbo login`. `expires_at: null` never expires.",
|
|
748
763
|
"login_hint": "tonbo login"
|
|
749
764
|
},
|
|
765
|
+
"accounts": {
|
|
766
|
+
"description": "One CLI login key per account (ADR-0016 R7 (a)): a daemon that publishes into an organization holds that organization's key beside the personal account's, since a key reaches only its own account. The fields are additive and optional, so a version 1 reader that knows only the top level keeps working, and the format stays version 1.",
|
|
767
|
+
"top_level": "The login made last, by `tonbo login` or `yield login`: the account `tonbo` acts in. A login made only to publish into an organization (`yield connect --account`) is added to `accounts` and leaves the top level alone.",
|
|
768
|
+
"entries": "`accounts` maps an account id to that account's login on this device, the top level's account included. `kind` and `name` are the redemption's answer; a file written before them, or by a server that does not answer them, has the top level alone, whose kind a client asks the API for.",
|
|
769
|
+
"personal": "The entry whose kind is `user` is the person's personal account's login: it alone captures raw sessions and registers the device (ADR-0016 W-10, W-14).",
|
|
770
|
+
"login_secret": "One random per-device secret, 32 bytes base64url, made at the first login and kept: it is sent only in the redeem POST, beside the PKCE verifier, never in the /cli/login URL, and User Center keeps only the suffix it derives from it, in an organization key's name `Yield CLI \xB7 <8 hex>`. `tonbo` and `yield` share it through this file, and so share one key per account. A lost file sends a new secret and gets a new suffix; the old key stays until it is revoked or idles out."
|
|
771
|
+
},
|
|
750
772
|
"current_version": 1,
|
|
751
773
|
"schema": {
|
|
752
774
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
@@ -787,6 +809,59 @@ var credentialsContract = {
|
|
|
787
809
|
],
|
|
788
810
|
"format": "date-time",
|
|
789
811
|
"description": "When the key expires, ISO 8601; null for a key that never expires."
|
|
812
|
+
},
|
|
813
|
+
"login_secret": {
|
|
814
|
+
"type": "string",
|
|
815
|
+
"pattern": "^[A-Za-z0-9_-]{43}$",
|
|
816
|
+
"description": "This device's login secret (`accounts.login_secret`)."
|
|
817
|
+
},
|
|
818
|
+
"accounts": {
|
|
819
|
+
"type": "object",
|
|
820
|
+
"description": "Every account's login of this device, keyed by the account id (`accounts.entries`).",
|
|
821
|
+
"additionalProperties": {
|
|
822
|
+
"type": "object",
|
|
823
|
+
"required": [
|
|
824
|
+
"key_id",
|
|
825
|
+
"key",
|
|
826
|
+
"expires_at"
|
|
827
|
+
],
|
|
828
|
+
"properties": {
|
|
829
|
+
"key_id": {
|
|
830
|
+
"type": "string",
|
|
831
|
+
"minLength": 1,
|
|
832
|
+
"description": "The key's id."
|
|
833
|
+
},
|
|
834
|
+
"key": {
|
|
835
|
+
"type": "string",
|
|
836
|
+
"pattern": "^tbo_",
|
|
837
|
+
"description": "The personal key's secret."
|
|
838
|
+
},
|
|
839
|
+
"expires_at": {
|
|
840
|
+
"type": [
|
|
841
|
+
"string",
|
|
842
|
+
"null"
|
|
843
|
+
],
|
|
844
|
+
"format": "date-time",
|
|
845
|
+
"description": "When the key expires; null for never."
|
|
846
|
+
},
|
|
847
|
+
"kind": {
|
|
848
|
+
"type": [
|
|
849
|
+
"string",
|
|
850
|
+
"null"
|
|
851
|
+
],
|
|
852
|
+
"pattern": "^(user|organization)$",
|
|
853
|
+
"description": "The account's kind, `user` for the personal account; null when unknown."
|
|
854
|
+
},
|
|
855
|
+
"name": {
|
|
856
|
+
"type": [
|
|
857
|
+
"string",
|
|
858
|
+
"null"
|
|
859
|
+
],
|
|
860
|
+
"minLength": 1,
|
|
861
|
+
"description": "The account's name, which `--account` takes; null when unknown."
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
790
865
|
}
|
|
791
866
|
}
|
|
792
867
|
},
|
|
@@ -805,6 +880,30 @@ var credentialsContract = {
|
|
|
805
880
|
"key_id": "20000000-0000-4000-8000-000000000002",
|
|
806
881
|
"key": "tbo_secret",
|
|
807
882
|
"expires_at": null
|
|
883
|
+
},
|
|
884
|
+
{
|
|
885
|
+
"version": 1,
|
|
886
|
+
"account_id": "10000000-0000-4000-8000-000000000001",
|
|
887
|
+
"key_id": "20000000-0000-4000-8000-000000000002",
|
|
888
|
+
"key": "tbo_secret",
|
|
889
|
+
"expires_at": null,
|
|
890
|
+
"login_secret": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
|
891
|
+
"accounts": {
|
|
892
|
+
"10000000-0000-4000-8000-000000000001": {
|
|
893
|
+
"key_id": "20000000-0000-4000-8000-000000000002",
|
|
894
|
+
"key": "tbo_secret",
|
|
895
|
+
"expires_at": null,
|
|
896
|
+
"kind": "user",
|
|
897
|
+
"name": "sam"
|
|
898
|
+
},
|
|
899
|
+
"10000000-0000-4000-8000-0000000000a1": {
|
|
900
|
+
"key_id": "20000000-0000-4000-8000-0000000000a2",
|
|
901
|
+
"key": "tbo_org",
|
|
902
|
+
"expires_at": "2027-04-24T00:00:00.000Z",
|
|
903
|
+
"kind": "organization",
|
|
904
|
+
"name": "acme"
|
|
905
|
+
}
|
|
906
|
+
}
|
|
808
907
|
}
|
|
809
908
|
],
|
|
810
909
|
"unusable": [
|
|
@@ -846,6 +945,51 @@ var credentialsContract = {
|
|
|
846
945
|
"account_id": "10000000-0000-4000-8000-000000000001",
|
|
847
946
|
"key_id": "20000000-0000-4000-8000-000000000002",
|
|
848
947
|
"key": "tbo_secret"
|
|
948
|
+
},
|
|
949
|
+
{
|
|
950
|
+
"version": 1,
|
|
951
|
+
"account_id": "10000000-0000-4000-8000-000000000001",
|
|
952
|
+
"key_id": "20000000-0000-4000-8000-000000000002",
|
|
953
|
+
"key": "tbo_secret",
|
|
954
|
+
"expires_at": null,
|
|
955
|
+
"accounts": {
|
|
956
|
+
"10000000-0000-4000-8000-0000000000a1": {
|
|
957
|
+
"key_id": "k",
|
|
958
|
+
"key": "not-a-personal-key",
|
|
959
|
+
"expires_at": null
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
},
|
|
963
|
+
{
|
|
964
|
+
"version": 1,
|
|
965
|
+
"account_id": "10000000-0000-4000-8000-000000000001",
|
|
966
|
+
"key_id": "20000000-0000-4000-8000-000000000002",
|
|
967
|
+
"key": "tbo_secret",
|
|
968
|
+
"expires_at": null,
|
|
969
|
+
"accounts": {
|
|
970
|
+
"10000000-0000-4000-8000-0000000000a1": {
|
|
971
|
+
"key_id": "k",
|
|
972
|
+
"key": "tbo_org",
|
|
973
|
+
"expires_at": null,
|
|
974
|
+
"kind": "team"
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
},
|
|
978
|
+
{
|
|
979
|
+
"version": 1,
|
|
980
|
+
"account_id": "10000000-0000-4000-8000-000000000001",
|
|
981
|
+
"key_id": "20000000-0000-4000-8000-000000000002",
|
|
982
|
+
"key": "tbo_secret",
|
|
983
|
+
"expires_at": null,
|
|
984
|
+
"accounts": []
|
|
985
|
+
},
|
|
986
|
+
{
|
|
987
|
+
"version": 1,
|
|
988
|
+
"account_id": "10000000-0000-4000-8000-000000000001",
|
|
989
|
+
"key_id": "20000000-0000-4000-8000-000000000002",
|
|
990
|
+
"key": "tbo_secret",
|
|
991
|
+
"expires_at": null,
|
|
992
|
+
"login_secret": "in-a-url"
|
|
849
993
|
}
|
|
850
994
|
]
|
|
851
995
|
}
|
|
@@ -860,6 +1004,7 @@ var machinePortAccess = [
|
|
|
860
1004
|
];
|
|
861
1005
|
var machineTimings = {
|
|
862
1006
|
"pollAfterSeconds": 2,
|
|
1007
|
+
"sleepPollAfterSeconds": 5,
|
|
863
1008
|
"wakeHoldSeconds": 25,
|
|
864
1009
|
"earlyReclaimSeconds": 120
|
|
865
1010
|
};
|
|
@@ -929,13 +1074,37 @@ var FileCredentialStore = class {
|
|
|
929
1074
|
await rm(this.filename, { force: true });
|
|
930
1075
|
}
|
|
931
1076
|
};
|
|
932
|
-
var RULE_KEYWORDS = /* @__PURE__ */ new Set([
|
|
1077
|
+
var RULE_KEYWORDS = /* @__PURE__ */ new Set([
|
|
1078
|
+
"const",
|
|
1079
|
+
"type",
|
|
1080
|
+
"minLength",
|
|
1081
|
+
"pattern",
|
|
1082
|
+
"format",
|
|
1083
|
+
"description",
|
|
1084
|
+
"additionalProperties"
|
|
1085
|
+
]);
|
|
1086
|
+
function isPlainObject(value) {
|
|
1087
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
1088
|
+
}
|
|
1089
|
+
function matchesObject(value, rule) {
|
|
1090
|
+
if (rule.type !== "object" || !isPlainObject(value))
|
|
1091
|
+
return false;
|
|
1092
|
+
if (!rule.required.every((name) => Object.hasOwn(value, name)))
|
|
1093
|
+
return false;
|
|
1094
|
+
return Object.entries(rule.properties).every(([name, inner]) => !Object.hasOwn(value, name) || matchesRule(value[name], inner));
|
|
1095
|
+
}
|
|
933
1096
|
function matchesRule(value, rule) {
|
|
934
1097
|
if (Object.keys(rule).some((keyword) => !RULE_KEYWORDS.has(keyword)))
|
|
935
1098
|
return false;
|
|
936
1099
|
if ("const" in rule)
|
|
937
1100
|
return value === rule.const;
|
|
938
1101
|
const types = typeof rule.type === "string" ? [rule.type] : rule.type ?? [];
|
|
1102
|
+
if (types.includes("object")) {
|
|
1103
|
+
if (!isPlainObject(value) || !rule.additionalProperties)
|
|
1104
|
+
return false;
|
|
1105
|
+
const entries = rule.additionalProperties;
|
|
1106
|
+
return Object.entries(value).every(([key, entry]) => key.length > 0 && matchesObject(entry, entries));
|
|
1107
|
+
}
|
|
939
1108
|
if (value === null)
|
|
940
1109
|
return types.includes("null");
|
|
941
1110
|
if (typeof value !== "string" || !types.includes("string"))
|
|
@@ -949,7 +1118,7 @@ function matchesRule(value, rule) {
|
|
|
949
1118
|
return true;
|
|
950
1119
|
}
|
|
951
1120
|
function isStoredLogin(value) {
|
|
952
|
-
if (!
|
|
1121
|
+
if (!isPlainObject(value))
|
|
953
1122
|
return false;
|
|
954
1123
|
const login = value;
|
|
955
1124
|
const schema = credentialsContract.schema;
|
|
@@ -960,6 +1129,44 @@ function isStoredLogin(value) {
|
|
|
960
1129
|
const properties = schema.properties;
|
|
961
1130
|
return Object.entries(properties).every(([name, rule]) => !Object.hasOwn(login, name) || matchesRule(login[name], rule));
|
|
962
1131
|
}
|
|
1132
|
+
function accountLogins(login) {
|
|
1133
|
+
const entries = Object.entries(login.accounts ?? {}).map(([account_id, entry]) => ({
|
|
1134
|
+
...entry,
|
|
1135
|
+
account_id,
|
|
1136
|
+
kind: entry.kind ?? null,
|
|
1137
|
+
name: entry.name ?? null
|
|
1138
|
+
}));
|
|
1139
|
+
if (!entries.some((entry) => entry.account_id === login.account_id))
|
|
1140
|
+
entries.unshift({
|
|
1141
|
+
account_id: login.account_id,
|
|
1142
|
+
key_id: login.key_id,
|
|
1143
|
+
key: login.key,
|
|
1144
|
+
expires_at: login.expires_at,
|
|
1145
|
+
kind: null,
|
|
1146
|
+
name: null
|
|
1147
|
+
});
|
|
1148
|
+
return entries;
|
|
1149
|
+
}
|
|
1150
|
+
function withAccountLogin(file, entry, options) {
|
|
1151
|
+
const accounts = {};
|
|
1152
|
+
if (file)
|
|
1153
|
+
for (const known of accountLogins(file)) {
|
|
1154
|
+
const { account_id: account_id2, ...rest2 } = known;
|
|
1155
|
+
accounts[account_id2] = rest2;
|
|
1156
|
+
}
|
|
1157
|
+
const { account_id, ...rest } = entry;
|
|
1158
|
+
accounts[account_id] = rest;
|
|
1159
|
+
const top = options.current || !file ? entry : file;
|
|
1160
|
+
return {
|
|
1161
|
+
version: 1,
|
|
1162
|
+
account_id: top.account_id,
|
|
1163
|
+
key_id: top.key_id,
|
|
1164
|
+
key: top.key,
|
|
1165
|
+
expires_at: top.expires_at,
|
|
1166
|
+
login_secret: options.loginSecret,
|
|
1167
|
+
accounts
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
963
1170
|
async function preparePrivateDirectory(directory) {
|
|
964
1171
|
const stat = await lstat(directory);
|
|
965
1172
|
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
@@ -989,6 +1196,9 @@ async function assertPrivateRegularFile(filename) {
|
|
|
989
1196
|
var exec = promisify(execFile);
|
|
990
1197
|
var DEFAULT_LOGIN_HINT = credentialsContract.reader.login_hint;
|
|
991
1198
|
var LOOPBACK_HOST = "127.0.0.1";
|
|
1199
|
+
function newLoginSecret() {
|
|
1200
|
+
return randomBytes(32).toString("base64url");
|
|
1201
|
+
}
|
|
992
1202
|
function loginHostname(hostname = os2.hostname()) {
|
|
993
1203
|
const cleaned = hostname.replace(/[^A-Za-z0-9._-]/g, "-").replace(/^[^A-Za-z0-9]+/, "").slice(0, 64);
|
|
994
1204
|
return cleaned || "computer";
|
|
@@ -1023,11 +1233,21 @@ var AuthClient = class {
|
|
|
1023
1233
|
}
|
|
1024
1234
|
/**
|
|
1025
1235
|
* The browser handoff of /cli/login: PKCE S256 with a loopback redirect on
|
|
1026
|
-
* a free port, then the one-time code is redeemed
|
|
1027
|
-
*
|
|
1236
|
+
* a free port, then the one-time code is redeemed, with this device's login
|
|
1237
|
+
* secret, for the account's login key, which replaces this device's earlier
|
|
1238
|
+
* login key there ("CLI on <hostname>" in a personal account, `Yield CLI ·
|
|
1239
|
+
* <8 hex>` in any other). The key joins the file's `accounts`; it becomes
|
|
1240
|
+
* the top level too unless `current` is false (a login made only to publish
|
|
1241
|
+
* into an organization). Answers that account's login.
|
|
1028
1242
|
*/
|
|
1029
1243
|
async login(progress = () => {
|
|
1030
|
-
}, account) {
|
|
1244
|
+
}, account, { current = true } = {}) {
|
|
1245
|
+
const existing = await this.credentials.load().catch((error) => {
|
|
1246
|
+
if (error instanceof UnusableLoginError)
|
|
1247
|
+
return null;
|
|
1248
|
+
throw error;
|
|
1249
|
+
});
|
|
1250
|
+
const loginSecret = existing?.login_secret ?? newLoginSecret();
|
|
1031
1251
|
const verifier = randomBytes(48).toString("base64url");
|
|
1032
1252
|
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
|
1033
1253
|
const state = randomBytes(24).toString("base64url");
|
|
@@ -1047,18 +1267,24 @@ var AuthClient = class {
|
|
|
1047
1267
|
const redeemed = await requestJson(this.fetcher, new URL("/api/cli/login/token", this.loginOrigin).toString(), {
|
|
1048
1268
|
method: "POST",
|
|
1049
1269
|
headers: { "content-type": "application/json" },
|
|
1050
|
-
|
|
1270
|
+
// The secret goes only here, in the POST body, never in the URL.
|
|
1271
|
+
body: JSON.stringify({ code, code_verifier: verifier, login_secret: loginSecret })
|
|
1051
1272
|
});
|
|
1052
1273
|
progress({ status: "completed", step: "key-redemption" });
|
|
1053
1274
|
const login = {
|
|
1054
|
-
version: 1,
|
|
1055
1275
|
account_id: redeemed.account_id,
|
|
1056
1276
|
key_id: redeemed.api_key.id,
|
|
1057
1277
|
key: redeemed.key,
|
|
1058
|
-
expires_at: redeemed.api_key.expires_at
|
|
1278
|
+
expires_at: redeemed.api_key.expires_at,
|
|
1279
|
+
kind: redeemed.account_kind ?? null,
|
|
1280
|
+
name: redeemed.account_name ?? null
|
|
1059
1281
|
};
|
|
1060
1282
|
progress({ status: "started", step: "credential-store" });
|
|
1061
|
-
await this.credentials.
|
|
1283
|
+
const latest = await this.credentials.load().catch(() => existing);
|
|
1284
|
+
await this.credentials.save(withAccountLogin(latest, login, {
|
|
1285
|
+
current,
|
|
1286
|
+
loginSecret: latest?.login_secret ?? loginSecret
|
|
1287
|
+
}));
|
|
1062
1288
|
progress({ status: "completed", step: "credential-store" });
|
|
1063
1289
|
return login;
|
|
1064
1290
|
}
|
|
@@ -1452,12 +1678,15 @@ var ManagementApi = class {
|
|
|
1452
1678
|
});
|
|
1453
1679
|
}
|
|
1454
1680
|
/** With `etag`, only the Machine that ETag names, at that revision, is put
|
|
1455
|
-
* to sleep (machine-v2.json `etag.if_match.sleep` is optional).
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1681
|
+
* to sleep (machine-v2.json `etag.if_match.sleep` is optional). A 202
|
|
1682
|
+
* carries the `Retry-After` to read the Machine again after
|
|
1683
|
+
* (machine-v2.json `sleep.poll_after_seconds`); a 200 carries none. */
|
|
1684
|
+
async sleepMachine(key, name, etag) {
|
|
1685
|
+
const headers = { retryAfter: null };
|
|
1686
|
+
const answer = await this.request("sleepMachine", key, { params: { name }, ...etag ? { ifMatch: etag } : {} }, (response) => {
|
|
1687
|
+
headers.retryAfter = response.headers.get("retry-after");
|
|
1460
1688
|
});
|
|
1689
|
+
return { machine: answer.data, retryAfterSeconds: retryAfterSeconds(headers.retryAfter) };
|
|
1461
1690
|
}
|
|
1462
1691
|
/** The Machine being deleted or the device forgotten, with its kind; the
|
|
1463
1692
|
* Machine is null when the API answered 204, whose `X-Tonbo-Machine-Kind`
|
|
@@ -1474,6 +1703,14 @@ var ManagementApi = class {
|
|
|
1474
1703
|
return { machine: answer.data, kind: answer.data.kind };
|
|
1475
1704
|
return { machine: null, kind: headers.kind === "byoc" ? "byoc" : "managed" };
|
|
1476
1705
|
}
|
|
1706
|
+
/** One page of a managed Machine's activations, newest first; the cursor
|
|
1707
|
+
* pages that Machine only. */
|
|
1708
|
+
listMachineActivations(key, name, { limit, cursor } = {}) {
|
|
1709
|
+
return this.request("listMachineActivations", key, {
|
|
1710
|
+
params: { name },
|
|
1711
|
+
query: { limit, cursor }
|
|
1712
|
+
});
|
|
1713
|
+
}
|
|
1477
1714
|
/** The whole answer: the shapes and the default one. */
|
|
1478
1715
|
listMachineShapes(key) {
|
|
1479
1716
|
return this.request("listMachineShapes", key, {});
|
|
@@ -1538,8 +1775,9 @@ function when(timestamp, absent = "never") {
|
|
|
1538
1775
|
// ../../packages/tonbo-cli-core/dist/src/login.js
|
|
1539
1776
|
async function loginCommand(deps, options = {}) {
|
|
1540
1777
|
try {
|
|
1541
|
-
const login = await deps.auth.login((event) => reportLoginProgress(deps.progress, event), options.account);
|
|
1542
|
-
deps.output(`Logged in to account ${login.account_id}.${login.expires_at ? ` The login expires ${login.expires_at}.` : ""}`);
|
|
1778
|
+
const login = await deps.auth.login((event) => reportLoginProgress(deps.progress, event), options.account, { current: options.current });
|
|
1779
|
+
deps.output(`Logged in to account ${login.name ?? login.account_id}.${login.expires_at ? ` The login expires ${login.expires_at}.` : ""}`);
|
|
1780
|
+
return login;
|
|
1543
1781
|
} catch (error) {
|
|
1544
1782
|
deps.progress.fail();
|
|
1545
1783
|
throw error;
|
|
@@ -1603,17 +1841,23 @@ async function logoutCommand(deps) {
|
|
|
1603
1841
|
deps.output("Not logged in.");
|
|
1604
1842
|
return;
|
|
1605
1843
|
}
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1844
|
+
const failures = [];
|
|
1845
|
+
let first;
|
|
1846
|
+
for (const entry of accountLogins(login)) {
|
|
1847
|
+
try {
|
|
1848
|
+
await deps.api.revokeApiKey(entry.key, entry.key_id);
|
|
1849
|
+
} catch (error) {
|
|
1850
|
+
if (error instanceof HttpError && (error.status === 401 || error.status === 404))
|
|
1851
|
+
continue;
|
|
1852
|
+
const failure = error instanceof Error ? error : new Error("The revocation failed.");
|
|
1853
|
+
first ??= failure;
|
|
1854
|
+
failures.push(`${entry.key_id}: ${failure.message}`);
|
|
1855
|
+
}
|
|
1612
1856
|
}
|
|
1613
1857
|
await deps.credentials.remove();
|
|
1614
|
-
if (
|
|
1615
|
-
throw new Error(`Removed the saved login, but could not revoke its key ${
|
|
1616
|
-
deps.output("Logged out. The CLI key is revoked.");
|
|
1858
|
+
if (failures.length)
|
|
1859
|
+
throw new Error(`Removed the saved login, but could not revoke ${failures.length === 1 ? "its key" : "these keys"} ${failures.join("; ")} Revoke ${failures.length === 1 ? "it" : "them"} under API keys in the Tonbo console.`, { cause: first });
|
|
1860
|
+
deps.output(login.accounts && Object.keys(login.accounts).length > 1 ? "Logged out. Every account's CLI key on this computer is revoked." : "Logged out. The CLI key is revoked.");
|
|
1617
1861
|
}
|
|
1618
1862
|
|
|
1619
1863
|
// ../../packages/tonbo-cli-core/dist/src/origin.js
|
|
@@ -2488,7 +2732,7 @@ function portText(port) {
|
|
|
2488
2732
|
function machinePortsText(machine) {
|
|
2489
2733
|
return machine.ports.length ? machine.ports.map(portText).join(", ") : "none";
|
|
2490
2734
|
}
|
|
2491
|
-
var { earlyReclaimSeconds, pollAfterSeconds, wakeHoldSeconds } = machineTimings;
|
|
2735
|
+
var { earlyReclaimSeconds, pollAfterSeconds, sleepPollAfterSeconds, wakeHoldSeconds } = machineTimings;
|
|
2492
2736
|
function machineState(machine) {
|
|
2493
2737
|
if (machine.failure)
|
|
2494
2738
|
return `${machine.state}: ${machine.failure.code}: ${machine.failure.message}`;
|
|
@@ -2516,17 +2760,36 @@ function sleepText(machine) {
|
|
|
2516
2760
|
function activationFields(machine) {
|
|
2517
2761
|
const { activation } = machine;
|
|
2518
2762
|
if (!activation) return [];
|
|
2519
|
-
|
|
2763
|
+
const fields2 = [
|
|
2520
2764
|
[
|
|
2521
2765
|
"Activation",
|
|
2522
2766
|
`${activation.state} since ${activation.started_at} (wake ${activation.wake_gen}, from ${activation.source})`
|
|
2523
2767
|
]
|
|
2524
2768
|
];
|
|
2525
|
-
|
|
2526
|
-
|
|
2769
|
+
if (typeof activation.revision === "number" && activation.revision !== machine.revision)
|
|
2770
|
+
fields2.push([
|
|
2771
|
+
"Pending",
|
|
2772
|
+
`Spec ${machine.revision}, admitted at ${activation.revision}: ports and sleep-after already apply; if the image, command, shape or env changed, run \`tonbo machine sleep ${machine.name}\` to apply it`
|
|
2773
|
+
]);
|
|
2774
|
+
return fields2;
|
|
2775
|
+
}
|
|
2776
|
+
var MACHINE_WAKE_BLOCKED_HINTS = {
|
|
2777
|
+
payment_required: "The account's credit does not cover a wake: add credit, and the next request wakes it.",
|
|
2778
|
+
standing: "The account's Streams standing is not solvent: settle the account, and the next request wakes it.",
|
|
2779
|
+
meter_lag: "Tonbo could not confirm the account's usage in time; the next request tries again.",
|
|
2780
|
+
crash_backoff: "Its recent wakes failed; see `tonbo machine activations <name>`.",
|
|
2781
|
+
capacity: "No host had room for its shape; the next request tries again.",
|
|
2782
|
+
region_closed: "Machines are closed in its region for now; the next request after they reopen wakes it."
|
|
2783
|
+
};
|
|
2784
|
+
function conditionText(name, condition) {
|
|
2527
2785
|
switch (condition.type) {
|
|
2528
|
-
case "wake_blocked":
|
|
2529
|
-
|
|
2786
|
+
case "wake_blocked": {
|
|
2787
|
+
const retry = condition.retry_at ? `, retry after ${condition.retry_at}` : "";
|
|
2788
|
+
const hint = MACHINE_WAKE_BLOCKED_HINTS[condition.reason]?.replaceAll("<name>", name);
|
|
2789
|
+
return `wake blocked (${condition.reason.replaceAll("_", " ")}) since ${condition.since}${retry}.${hint ? ` ${hint}` : ""}`;
|
|
2790
|
+
}
|
|
2791
|
+
case "creation_delayed":
|
|
2792
|
+
return `still creating since ${condition.since}, longer than usual; Tonbo's operators are alerted, and creation goes on until it is active or failed.`;
|
|
2530
2793
|
case "node_unreachable":
|
|
2531
2794
|
return `its host is unreachable since ${condition.since}`;
|
|
2532
2795
|
case "egress_blocked":
|
|
@@ -2538,9 +2801,13 @@ function conditionText(condition) {
|
|
|
2538
2801
|
function conditionFields(machine) {
|
|
2539
2802
|
return (machine.status.conditions ?? []).map((condition) => [
|
|
2540
2803
|
"Condition",
|
|
2541
|
-
conditionText(condition)
|
|
2804
|
+
conditionText(machine.name, condition)
|
|
2542
2805
|
]);
|
|
2543
2806
|
}
|
|
2807
|
+
function endText(end) {
|
|
2808
|
+
const status = end.exit_code !== null ? `, exit code ${end.exit_code}` : end.signal !== null ? `, signal ${end.signal}` : "";
|
|
2809
|
+
return `${end.state} (${end.reason.replaceAll("_", " ")}${status}) at ${end.at}`;
|
|
2810
|
+
}
|
|
2544
2811
|
var DURABILITY_LINK = "https://tonbo.dev/docs/machines/guarantees#sleep";
|
|
2545
2812
|
var PRIVATE_PORTS_LINK = "https://tonbo.dev/docs/machines/requests#private-ports";
|
|
2546
2813
|
function privatePortLines(machine) {
|
|
@@ -2574,7 +2841,7 @@ function imageFailureFields(machine) {
|
|
|
2574
2841
|
...next ? [["Next step", next]] : []
|
|
2575
2842
|
];
|
|
2576
2843
|
}
|
|
2577
|
-
function machineFields(machine, etag = null) {
|
|
2844
|
+
function machineFields(machine, etag = null, lastEnd = null) {
|
|
2578
2845
|
return fields([
|
|
2579
2846
|
["Name", machine.name],
|
|
2580
2847
|
["ID", machine.id],
|
|
@@ -2582,6 +2849,7 @@ function machineFields(machine, etag = null) {
|
|
|
2582
2849
|
["State", machineState(machine)],
|
|
2583
2850
|
["Power", powerText(machine)],
|
|
2584
2851
|
...activationFields(machine),
|
|
2852
|
+
...lastEnd ? [["Last end", endText(lastEnd)]] : [],
|
|
2585
2853
|
...conditionFields(machine),
|
|
2586
2854
|
["Sleeps", sleepText(machine)],
|
|
2587
2855
|
["Shape", machine.shape],
|
|
@@ -2600,12 +2868,13 @@ function machineFields(machine, etag = null) {
|
|
|
2600
2868
|
["Durability", DURABILITY_LINK],
|
|
2601
2869
|
["Revision", String(machine.revision)],
|
|
2602
2870
|
...etag ? [["ETag", etag]] : [],
|
|
2871
|
+
...machine.created_by ? [["Created by", machine.created_by]] : [],
|
|
2603
2872
|
["Created", machine.created_at],
|
|
2604
2873
|
["Updated", machine.updated_at]
|
|
2605
2874
|
]);
|
|
2606
2875
|
}
|
|
2607
|
-
function machineText(machine, etag = null) {
|
|
2608
|
-
return [machineFields(machine, etag), ...privatePortLines(machine)].join("\n");
|
|
2876
|
+
function machineText(machine, etag = null, lastEnd = null) {
|
|
2877
|
+
return [machineFields(machine, etag, lastEnd), ...privatePortLines(machine)].join("\n");
|
|
2609
2878
|
}
|
|
2610
2879
|
function keyStateText(machine) {
|
|
2611
2880
|
const state = machine.byoc.key_state;
|
|
@@ -2630,8 +2899,8 @@ function deviceFields(machine) {
|
|
|
2630
2899
|
["Updated", machine.updated_at]
|
|
2631
2900
|
]);
|
|
2632
2901
|
}
|
|
2633
|
-
function anyMachineText(machine, etag = null) {
|
|
2634
|
-
return machine.kind === "byoc" ? deviceFields(machine) : machineText(machine, etag);
|
|
2902
|
+
function anyMachineText(machine, etag = null, lastEnd = null) {
|
|
2903
|
+
return machine.kind === "byoc" ? deviceFields(machine) : machineText(machine, etag, lastEnd);
|
|
2635
2904
|
}
|
|
2636
2905
|
function parsePorts(specs) {
|
|
2637
2906
|
const accesses = machinePortAccess;
|
|
@@ -2681,7 +2950,7 @@ function waitTimeout(options) {
|
|
|
2681
2950
|
return options.timeout ?? DEFAULT_WAIT_SECONDS;
|
|
2682
2951
|
}
|
|
2683
2952
|
var DEFAULT_WAIT_SECONDS = 300;
|
|
2684
|
-
async function waitForMachine(deps, key, name, firstWait, timeout, settled) {
|
|
2953
|
+
async function waitForMachine(deps, key, name, firstWait, every, timeout, settled) {
|
|
2685
2954
|
const now = deps.now ?? Date.now;
|
|
2686
2955
|
const pause = deps.pause ?? ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)));
|
|
2687
2956
|
const start = now();
|
|
@@ -2697,7 +2966,7 @@ async function waitForMachine(deps, key, name, firstWait, timeout, settled) {
|
|
|
2697
2966
|
if (settled(machine))
|
|
2698
2967
|
return { machine, last: machine, waited: Math.round((now() - start) / 1e3) };
|
|
2699
2968
|
last = machine;
|
|
2700
|
-
wait =
|
|
2969
|
+
wait = every;
|
|
2701
2970
|
}
|
|
2702
2971
|
}
|
|
2703
2972
|
async function machineListCommand(deps, options = {}) {
|
|
@@ -2713,7 +2982,7 @@ async function machineListCommand(deps, options = {}) {
|
|
|
2713
2982
|
if (managed.length)
|
|
2714
2983
|
tables.push(
|
|
2715
2984
|
table(
|
|
2716
|
-
["NAME", "REGION", "STATE", "POWER", "SHAPE", "IMAGE", "HTTPS"],
|
|
2985
|
+
["NAME", "REGION", "STATE", "POWER", "SHAPE", "IMAGE", "HTTPS", "CONDITIONS"],
|
|
2717
2986
|
managed.map((machine) => [
|
|
2718
2987
|
machine.name,
|
|
2719
2988
|
machine.region,
|
|
@@ -2721,7 +2990,8 @@ async function machineListCommand(deps, options = {}) {
|
|
|
2721
2990
|
machine.status.power,
|
|
2722
2991
|
machine.shape,
|
|
2723
2992
|
machine.image.reference,
|
|
2724
|
-
machine.hostnames.https
|
|
2993
|
+
machine.hostnames.https,
|
|
2994
|
+
conditionsCell(machine)
|
|
2725
2995
|
])
|
|
2726
2996
|
)
|
|
2727
2997
|
);
|
|
@@ -2747,6 +3017,13 @@ async function machineListCommand(deps, options = {}) {
|
|
|
2747
3017
|
);
|
|
2748
3018
|
});
|
|
2749
3019
|
}
|
|
3020
|
+
function conditionsCell(machine) {
|
|
3021
|
+
const conditions = machine.status.conditions ?? [];
|
|
3022
|
+
if (!conditions.length) return "-";
|
|
3023
|
+
return conditions.map(
|
|
3024
|
+
(condition) => condition.type === "wake_blocked" ? `wake_blocked:${condition.reason}` : condition.type
|
|
3025
|
+
).join(",");
|
|
3026
|
+
}
|
|
2750
3027
|
function createdText(machine) {
|
|
2751
3028
|
if (machine.state === "creating")
|
|
2752
3029
|
return `Machine ${machine.name} is being created; \`tonbo machine show ${machine.name}\` reports it active when it is ready, or create with --wait.`;
|
|
@@ -2783,6 +3060,7 @@ async function waitForCreate(deps, key, machine, name, firstWait, timeout, json)
|
|
|
2783
3060
|
key,
|
|
2784
3061
|
name,
|
|
2785
3062
|
firstWait,
|
|
3063
|
+
pollAfterSeconds,
|
|
2786
3064
|
timeout,
|
|
2787
3065
|
(read) => read.state !== "creating"
|
|
2788
3066
|
);
|
|
@@ -2870,8 +3148,46 @@ async function machineCreateCommand(deps, name, command, options = {}) {
|
|
|
2870
3148
|
}
|
|
2871
3149
|
async function machineShowCommand(deps, name, options = {}) {
|
|
2872
3150
|
await machineCommand(async () => {
|
|
2873
|
-
const
|
|
2874
|
-
|
|
3151
|
+
const key = await deps.auth.apiKey();
|
|
3152
|
+
const { machine, etag } = await deps.api.getMachineWithEtag(key, name);
|
|
3153
|
+
const lastEnd = options.json || machine.kind === "byoc" || machine.state !== "active" ? null : await lastActivationEnd(deps, key, machine.name);
|
|
3154
|
+
print(deps, options.json, machine, anyMachineText(machine, etag, lastEnd));
|
|
3155
|
+
});
|
|
3156
|
+
}
|
|
3157
|
+
async function lastActivationEnd(deps, key, name) {
|
|
3158
|
+
try {
|
|
3159
|
+
const page = await deps.api.listMachineActivations(key, name, { limit: 2 });
|
|
3160
|
+
return page.data.find((entry) => entry.end !== null)?.end ?? null;
|
|
3161
|
+
} catch (error) {
|
|
3162
|
+
if (!(error instanceof HttpError)) throw error;
|
|
3163
|
+
deps.notice(
|
|
3164
|
+
`Could not read how its last activation ended (${error.status}); \`tonbo machine activations ${name}\` lists them.`
|
|
3165
|
+
);
|
|
3166
|
+
return null;
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
async function machineActivationsCommand(deps, name, options = {}) {
|
|
3170
|
+
await machineCommand(async () => {
|
|
3171
|
+
const page = await deps.api.listMachineActivations(await deps.auth.apiKey(), name, {
|
|
3172
|
+
limit: options.limit,
|
|
3173
|
+
cursor: options.cursor
|
|
3174
|
+
});
|
|
3175
|
+
printPage(
|
|
3176
|
+
deps,
|
|
3177
|
+
options.json,
|
|
3178
|
+
page,
|
|
3179
|
+
(activations) => activations.length ? table(
|
|
3180
|
+
["WAKE", "SOURCE", "REVISION", "SHAPE", "STARTED", "END"],
|
|
3181
|
+
activations.map((entry) => [
|
|
3182
|
+
String(entry.wake_gen),
|
|
3183
|
+
entry.source,
|
|
3184
|
+
String(entry.revision),
|
|
3185
|
+
entry.shape,
|
|
3186
|
+
entry.started_at,
|
|
3187
|
+
entry.end ? endText(entry.end) : "live"
|
|
3188
|
+
])
|
|
3189
|
+
) : `Machine ${name} has not been woken yet.`
|
|
3190
|
+
);
|
|
2875
3191
|
});
|
|
2876
3192
|
}
|
|
2877
3193
|
function appliesText(name, change) {
|
|
@@ -2954,7 +3270,11 @@ async function machineSleepCommand(deps, name, options = {}) {
|
|
|
2954
3270
|
await machineCommand(async () => {
|
|
2955
3271
|
const timeout = waitTimeout(options);
|
|
2956
3272
|
const key = await deps.auth.apiKey();
|
|
2957
|
-
const machine = await deps.api.sleepMachine(
|
|
3273
|
+
const { machine, retryAfterSeconds: retryAfterSeconds2 } = await deps.api.sleepMachine(
|
|
3274
|
+
key,
|
|
3275
|
+
name,
|
|
3276
|
+
ifMatchHeader(options.ifMatch)
|
|
3277
|
+
);
|
|
2958
3278
|
const going = options.wait && machine.state === "active" && machine.status.power !== "asleep";
|
|
2959
3279
|
if (!going) {
|
|
2960
3280
|
print(deps, options.json, machine, sleptText(machine));
|
|
@@ -2964,7 +3284,8 @@ async function machineSleepCommand(deps, name, options = {}) {
|
|
|
2964
3284
|
deps,
|
|
2965
3285
|
key,
|
|
2966
3286
|
name,
|
|
2967
|
-
|
|
3287
|
+
retryAfterSeconds2 ?? sleepPollAfterSeconds,
|
|
3288
|
+
sleepPollAfterSeconds,
|
|
2968
3289
|
timeout,
|
|
2969
3290
|
(read) => read.status.power === "asleep" || read.state !== "active"
|
|
2970
3291
|
);
|
|
@@ -3160,7 +3481,9 @@ function parseUsageSort(value) {
|
|
|
3160
3481
|
}
|
|
3161
3482
|
function createProgram(dependencies = createDependencies) {
|
|
3162
3483
|
const program = new Command().name("tonbo").description("Sign in to Tonbo and manage account resources.").version(packageVersion.version);
|
|
3163
|
-
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) =>
|
|
3484
|
+
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) => {
|
|
3485
|
+
await loginCommand(dependencies(), options);
|
|
3486
|
+
});
|
|
3164
3487
|
program.command("logout").description("revoke this computer's key and remove the saved login").action(async () => logoutCommand(dependencies()));
|
|
3165
3488
|
const account = program.command("account").description("the account you are logged in to");
|
|
3166
3489
|
operationOptions(
|
|
@@ -3465,6 +3788,12 @@ function createProgram(dependencies = createDependencies) {
|
|
|
3465
3788
|
).action(
|
|
3466
3789
|
async (name, options) => machineDeleteCommand(dependencies(), name, options)
|
|
3467
3790
|
);
|
|
3791
|
+
operationOptions(
|
|
3792
|
+
machine.command("activations <name>").description("list a Machine's activations, newest first, and how each ended"),
|
|
3793
|
+
"listMachineActivations"
|
|
3794
|
+
).action(
|
|
3795
|
+
async (name, options) => machineActivationsCommand(dependencies(), name, options)
|
|
3796
|
+
);
|
|
3468
3797
|
operationOptions(
|
|
3469
3798
|
machine.command("shapes").description("list the shapes a Machine may have"),
|
|
3470
3799
|
"listMachineShapes"
|