@tonbo/cli 0.21.0 → 0.23.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 +1 -1
- package/dist/bin/tonbo.js +260 -52
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ The login file, `login.json`, and the device id it is tied to are shared with [`
|
|
|
26
26
|
|
|
27
27
|
## Commands and the Management API
|
|
28
28
|
|
|
29
|
-
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, the activations `machine show` reads for the last end,
|
|
29
|
+
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, the activations `machine show` reads for the last end, the Volume a `machine create` or `update --volume` names, read for its id and region, or the Volume list `machine show` and `machine delete` read for the name of the Machine's Volume); `tonbo machine logs`, once it ships, is the named Streams exception, reading the stream at the Machine's `logs.url`. `tonbo volume move` is the one command that sends another command's operation: it reads the Volume and both Machines and sends the target's `updateMachine` with `move_from` (the `machine create --move-from` of an existing Machine, docs/design/machines-volume-move.md). Apart from it, 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`, `logout`, `account list` and `account use` are the named exception: they obtain, choose among and discard what the other commands present as their Bearer credential. What a command may do is what your role in the account it acts in allows; the CLI never decides it.
|
|
30
30
|
|
|
31
31
|
The help lives with the commands: [`src/app.ts`](src/app.ts) defines each command, its arguments and flags with their one-line descriptions, and [`src/help.ts`](src/help.ts) holds the long help, each flag's default, the examples, the shared rules and the environment. [`commands.json`](commands.json) is all of it as `--help` prints it, which the docs' CLI reference renders; `test/commands.test.ts` holds it to the program, and holds every runnable command to its details and example and the environment list to the variables the code reads, so after changing a command run `pnpm --filter @tonbo/cli commands`.
|
|
32
32
|
|
package/dist/bin/tonbo.js
CHANGED
|
@@ -673,7 +673,9 @@ var managementOperations = {
|
|
|
673
673
|
"pathParams": [
|
|
674
674
|
"name"
|
|
675
675
|
],
|
|
676
|
-
"query": [
|
|
676
|
+
"query": [
|
|
677
|
+
"delete_volume"
|
|
678
|
+
],
|
|
677
679
|
"idempotencyKey": false,
|
|
678
680
|
"cli": "tonbo machine delete"
|
|
679
681
|
},
|
|
@@ -687,6 +689,16 @@ var managementOperations = {
|
|
|
687
689
|
"idempotencyKey": false,
|
|
688
690
|
"cli": "tonbo machine sleep"
|
|
689
691
|
},
|
|
692
|
+
"rebuildMachine": {
|
|
693
|
+
"method": "POST",
|
|
694
|
+
"path": "/machines/{name}/rebuild",
|
|
695
|
+
"pathParams": [
|
|
696
|
+
"name"
|
|
697
|
+
],
|
|
698
|
+
"query": [],
|
|
699
|
+
"idempotencyKey": false,
|
|
700
|
+
"cli": "tonbo machine rebuild"
|
|
701
|
+
},
|
|
690
702
|
"wakeMachine": {
|
|
691
703
|
"method": "POST",
|
|
692
704
|
"path": "/machines/{name}/wake",
|
|
@@ -1941,6 +1953,10 @@ var machineTimings = {
|
|
|
1941
1953
|
"wakeHoldSeconds": 25,
|
|
1942
1954
|
"earlyReclaimSeconds": 120
|
|
1943
1955
|
};
|
|
1956
|
+
var machineRebuildWarning = [
|
|
1957
|
+
"**Rebuild deletes changes outside /home, /workspace and /root.**",
|
|
1958
|
+
"Rebuild moves this Machine to the image `<image>`. Files in /home, /workspace and /root are kept. Everything else you changed on this Machine's disk is deleted and cannot be recovered: installed packages, files you edited in /etc, global npm or pip packages, and anything in /usr, /opt or /var. To keep those changes, add them to the image's Dockerfile. The Machine sleeps now and starts on the new image at its next wake."
|
|
1959
|
+
];
|
|
1944
1960
|
var machineSecretLimits = {
|
|
1945
1961
|
"perMachine": 64,
|
|
1946
1962
|
"valueMaxBytes": 65536,
|
|
@@ -2483,6 +2499,36 @@ var ManagementApi = class {
|
|
|
2483
2499
|
const answer = await this.request("updateMachine", key, { params: { name }, body: withoutUndefined(change), ifMatch: etag });
|
|
2484
2500
|
return { machine: answer.data, volumeMove: answer.volume_move ?? null };
|
|
2485
2501
|
}
|
|
2502
|
+
/** A Rebuild (machine-v2.json `rebuild`): `POST /machines/{name}/rebuild`
|
|
2503
|
+
* with `image`, or the Machine's own reference resolved again without it.
|
|
2504
|
+
* A 202 (`waiting_for_sleep`) carries the `Retry-After` to read the Machine
|
|
2505
|
+
* again after; the request is then sent again once it is asleep. With
|
|
2506
|
+
* `etag`, only the Machine that ETag names is rebuilt
|
|
2507
|
+
* (machine-v2.json `etag.if_match.rebuild` is optional). */
|
|
2508
|
+
async rebuildMachine(key, name, request, etag) {
|
|
2509
|
+
const headers = { retryAfter: null };
|
|
2510
|
+
const answer = await this.request("rebuildMachine", key, { params: { name }, body: withoutUndefined(request), ...etag ? { ifMatch: etag } : {} }, (response) => {
|
|
2511
|
+
headers.retryAfter = response.headers.get("retry-after");
|
|
2512
|
+
});
|
|
2513
|
+
return {
|
|
2514
|
+
machine: answer.data,
|
|
2515
|
+
rebuild: answer.rebuild,
|
|
2516
|
+
retryAfterSeconds: retryAfterSeconds(headers.retryAfter)
|
|
2517
|
+
};
|
|
2518
|
+
}
|
|
2519
|
+
/** An update with `rebuild: true` and `image`, which answers as
|
|
2520
|
+
* rebuildMachine does (machine-v2.json `rebuild.answers`). */
|
|
2521
|
+
async updateMachineRebuild(key, name, etag, change) {
|
|
2522
|
+
const headers = { retryAfter: null };
|
|
2523
|
+
const answer = await this.request("updateMachine", key, { params: { name }, body: withoutUndefined({ ...change, rebuild: true }), ifMatch: etag }, (response) => {
|
|
2524
|
+
headers.retryAfter = response.headers.get("retry-after");
|
|
2525
|
+
});
|
|
2526
|
+
return {
|
|
2527
|
+
machine: answer.data,
|
|
2528
|
+
rebuild: answer.rebuild,
|
|
2529
|
+
retryAfterSeconds: retryAfterSeconds(headers.retryAfter)
|
|
2530
|
+
};
|
|
2531
|
+
}
|
|
2486
2532
|
/** With `etag`, only the Machine that ETag names, at that revision, is put
|
|
2487
2533
|
* to sleep (machine-v2.json `etag.if_match.sleep` is optional). A 202
|
|
2488
2534
|
* carries the `Retry-After` to read the Machine again after
|
|
@@ -2511,14 +2557,25 @@ var ManagementApi = class {
|
|
|
2511
2557
|
* says whether a name was released (`managed`: its create never reached its
|
|
2512
2558
|
* region) or a device was forgotten (`byoc`: its region held no row). With
|
|
2513
2559
|
* `etag`, only the Machine that ETag names is deleted
|
|
2514
|
-
* (machine-v2.json `etag.if_match.delete` is optional).
|
|
2515
|
-
|
|
2560
|
+
* (machine-v2.json `etag.if_match.delete` is optional). `deleteVolume`
|
|
2561
|
+
* decides the Volume a managed Machine holds (machine-v2.json
|
|
2562
|
+
* `delete_volume`); without it the server's default applies, and a 202
|
|
2563
|
+
* says in `volume` what became of it. */
|
|
2564
|
+
async deleteMachine(key, name, etag, deleteVolume) {
|
|
2516
2565
|
const headers = { kind: null };
|
|
2517
|
-
const answer = await this.request("deleteMachine", key, {
|
|
2566
|
+
const answer = await this.request("deleteMachine", key, {
|
|
2567
|
+
params: { name },
|
|
2568
|
+
query: { delete_volume: deleteVolume === void 0 ? void 0 : String(deleteVolume) },
|
|
2569
|
+
...etag ? { ifMatch: etag } : {}
|
|
2570
|
+
}, (response) => {
|
|
2518
2571
|
headers.kind = response.headers.get("x-tonbo-machine-kind");
|
|
2519
2572
|
});
|
|
2520
2573
|
if (answer?.data)
|
|
2521
|
-
return {
|
|
2574
|
+
return {
|
|
2575
|
+
machine: answer.data,
|
|
2576
|
+
kind: answer.data.kind,
|
|
2577
|
+
...answer.volume === void 0 ? {} : { volume: answer.volume }
|
|
2578
|
+
};
|
|
2522
2579
|
return { machine: null, kind: headers.kind === "byoc" ? "byoc" : "managed" };
|
|
2523
2580
|
}
|
|
2524
2581
|
/** One page of a managed Machine's activations, newest first; the cursor
|
|
@@ -5597,18 +5654,6 @@ function volumeFields(volume) {
|
|
|
5597
5654
|
["Size", `${volume.size_gib} GiB`],
|
|
5598
5655
|
["Live bytes", `${bytes(volume.live_bytes)} (billed)`],
|
|
5599
5656
|
["Last meter report", volume.last_commit_at ?? "none yet"],
|
|
5600
|
-
[
|
|
5601
|
-
"Restored to",
|
|
5602
|
-
volume.restored_to ? `${volume.restored_to.committed_at} (generation ${volume.restored_to.data_generation})` : "none"
|
|
5603
|
-
],
|
|
5604
|
-
[
|
|
5605
|
-
"Backed up to",
|
|
5606
|
-
volume.backed_up_to ? volume.backed_up_to.committed_at ? `${volume.backed_up_to.committed_at} (generation ${volume.backed_up_to.data_generation}; ${volume.backed_up_to.dirty_bytes} bytes pending copy)` : `empty snapshot (generation ${volume.backed_up_to.data_generation}; ${volume.backed_up_to.dirty_bytes} bytes pending copy)` : "unknown"
|
|
5607
|
-
],
|
|
5608
|
-
[
|
|
5609
|
-
"Cold GETs",
|
|
5610
|
-
volume.cold ? `${volume.cold.demand_gets} demand, ${volume.cold.hydrate_gets} hydration` : "none"
|
|
5611
|
-
],
|
|
5612
5657
|
["Durability", VOLUMES_PREVIEW ? VOLUME_PREVIEW_TEXT : VOLUME_DURABILITY_LINK],
|
|
5613
5658
|
["Created", volume.created_at]
|
|
5614
5659
|
]);
|
|
@@ -5789,12 +5834,13 @@ var MACHINE_ERROR_HINTS = {
|
|
|
5789
5834
|
machine_kind_mismatch: "That Machine is one of your computers: `tonbo machine show` and `tonbo machine delete` serve it, and nothing else does.",
|
|
5790
5835
|
volumes_not_offered: "Volumes are not offered to this account in the Machine's region yet; nothing was created or changed.",
|
|
5791
5836
|
root_mode_not_offered: "A Volume as the Machine's whole disk is not offered to this account yet; nothing was created or changed. Mount it at a path instead: --volume <name>:<path>.",
|
|
5792
|
-
rebuild_required: "This Machine's Volume is its whole disk, so its image changes only by Rebuild
|
|
5793
|
-
|
|
5837
|
+
rebuild_required: "This Machine's Volume is its whole disk, so its image changes only by Rebuild; nothing was changed. Once you have read what Rebuild deletes, run `tonbo machine rebuild <name> --image <reference>`, or the same update with --rebuild.",
|
|
5838
|
+
volume_not_whole_disk: "This Machine's Volume is mounted at a path, or it has none, so it needs no Rebuild; nothing was changed. Change its image with `tonbo machine update <name> --image <reference>`.",
|
|
5839
|
+
volume_mode_change: "A Machine's whole disk is never mounted at a path, and a Volume mounted at a path becomes a whole disk only on its own Machine; nothing was changed.",
|
|
5794
5840
|
volume_attached: "The Machine the message names is awake with the Volume attached; nothing was changed. Put it to sleep with `tonbo machine sleep <machine>`, then run the command again.",
|
|
5795
5841
|
volume_holder_changed: "The Volume is not on the Machine you named; nothing was changed. `tonbo volume show <volume>` says which Machine has it: name that one, or use `tonbo volume move`.",
|
|
5796
5842
|
volume_image_mismatch: "The Volume is a Machine's whole disk, made against one image; it moves only to a Machine on that same image. Nothing was changed.",
|
|
5797
|
-
volume_rebuild_pending: "The Volume's Rebuild
|
|
5843
|
+
volume_rebuild_pending: "The Volume's Rebuild is recorded and applies at its Machine's next wake: wake that Machine once (`tonbo machine wake <machine>`), then move the Volume. Nothing was changed.",
|
|
5798
5844
|
secret_env_conflict: "Use another secret key or remove the non-secret env variable with that name.",
|
|
5799
5845
|
secret_limit: "Reduce the number or byte size of this Machine's secrets.",
|
|
5800
5846
|
wake_unavailable: "The wake was refused for now; nothing woke. Wake it again after a while; `tonbo machine show` says why it is blocked."
|
|
@@ -5914,7 +5960,8 @@ var MACHINE_WAKE_BLOCKED_HINTS = {
|
|
|
5914
5960
|
capacity: "No host had room for its shape: Tonbo is adding a host, <minutes>, and the next request after that wakes it.",
|
|
5915
5961
|
region_closed: "Machines are closed in its region for now; the next request after they reopen wakes it.",
|
|
5916
5962
|
budget_slowdown: "Its budget is close to running out, and this wake would pass the budget's slow-down rate: sleep another Machine of the budget, or lower this one's shape, then wake it again. Machines already awake keep running.",
|
|
5917
|
-
budget_exhausted: "Its budget is used up for the month: it wakes after the budget resets, or once a person raises it in the console."
|
|
5963
|
+
budget_exhausted: "Its budget is used up for the month: it wakes after the budget resets, or once a person raises it in the console.",
|
|
5964
|
+
root_image_mismatch: "Its disk was made over another image than the Machine's: run `tonbo machine rebuild <name>` to move the disk to the Machine's image (Rebuild deletes changes outside /home, /workspace and /root)."
|
|
5918
5965
|
};
|
|
5919
5966
|
var MACHINE_ACCOUNT_EGRESS_BLOCKED_HINT = "If this is the free egress cap: 10 GiB a month without purchased credit; buy credit at <billing> to lift it.";
|
|
5920
5967
|
function minutesUntil(retryAt, now = Date.now()) {
|
|
@@ -5987,7 +6034,7 @@ function imageFailureFields(machine) {
|
|
|
5987
6034
|
...next ? [["Next step", next]] : []
|
|
5988
6035
|
];
|
|
5989
6036
|
}
|
|
5990
|
-
function machineFields(machine, billing, etag = null, lastEnd = null) {
|
|
6037
|
+
function machineFields(machine, billing, etag = null, lastEnd = null, volumeName2 = null) {
|
|
5991
6038
|
return fields([
|
|
5992
6039
|
["Name", machine.name],
|
|
5993
6040
|
["ID", machine.id],
|
|
@@ -6010,7 +6057,7 @@ function machineFields(machine, billing, etag = null, lastEnd = null) {
|
|
|
6010
6057
|
["SSH", `${machine.hostnames.ssh} (tonbo ssh ${machine.name})`],
|
|
6011
6058
|
["Env", Object.keys(machine.env).sort().join(", ") || "none"],
|
|
6012
6059
|
["Stop grace", `${machine.stop_grace_seconds} s`],
|
|
6013
|
-
["Volume", volumeText(machine)],
|
|
6060
|
+
["Volume", volumeText(machine, volumeName2)],
|
|
6014
6061
|
...deleteFields(machine),
|
|
6015
6062
|
["Database", machine.database ? `${machine.database.name} (${machine.database.url})` : "none"],
|
|
6016
6063
|
["Logs", machine.logs.url],
|
|
@@ -6055,13 +6102,28 @@ function deleteFields(machine) {
|
|
|
6055
6102
|
]
|
|
6056
6103
|
];
|
|
6057
6104
|
}
|
|
6058
|
-
function volumeText(machine) {
|
|
6105
|
+
function volumeText(machine, name = null) {
|
|
6059
6106
|
const binding = machine.volumes?.[0];
|
|
6060
6107
|
if (!binding) return "none";
|
|
6061
|
-
|
|
6108
|
+
if (name === null)
|
|
6109
|
+
return binding.path === void 0 ? `${binding.volume} (whole disk)` : `${binding.volume} at ${binding.path}`;
|
|
6110
|
+
return binding.path === void 0 ? `${name} (whole disk; id ${binding.volume})` : `${name} at ${binding.path} (id ${binding.volume})`;
|
|
6062
6111
|
}
|
|
6063
|
-
function
|
|
6064
|
-
|
|
6112
|
+
async function volumeName(deps, key, machine) {
|
|
6113
|
+
const id = machine.volumes?.[0]?.volume;
|
|
6114
|
+
if (id === void 0) return null;
|
|
6115
|
+
try {
|
|
6116
|
+
return (await deps.api.listVolumes(key)).data.find((volume) => volume.id === id)?.name ?? null;
|
|
6117
|
+
} catch (error) {
|
|
6118
|
+
if (!(error instanceof HttpError || error instanceof AccessTokenError)) throw error;
|
|
6119
|
+
return null;
|
|
6120
|
+
}
|
|
6121
|
+
}
|
|
6122
|
+
function machineText(machine, billing, etag = null, lastEnd = null, volume = null) {
|
|
6123
|
+
return [
|
|
6124
|
+
machineFields(machine, billing, etag, lastEnd, volume),
|
|
6125
|
+
...privatePortLines(machine)
|
|
6126
|
+
].join("\n");
|
|
6065
6127
|
}
|
|
6066
6128
|
function keyStateText(machine) {
|
|
6067
6129
|
const state = machine.byoc.key_state;
|
|
@@ -6086,8 +6148,8 @@ function deviceFields(machine) {
|
|
|
6086
6148
|
["Updated", machine.updated_at]
|
|
6087
6149
|
]);
|
|
6088
6150
|
}
|
|
6089
|
-
function anyMachineText(machine, billing, etag = null, lastEnd = null) {
|
|
6090
|
-
return machine.kind === "byoc" ? deviceFields(machine) : machineText(machine, billing, etag, lastEnd);
|
|
6151
|
+
function anyMachineText(machine, billing, etag = null, lastEnd = null, volume = null) {
|
|
6152
|
+
return machine.kind === "byoc" ? deviceFields(machine) : machineText(machine, billing, etag, lastEnd, volume);
|
|
6091
6153
|
}
|
|
6092
6154
|
function parsePorts(specs) {
|
|
6093
6155
|
const accesses = machinePortAccess;
|
|
@@ -6456,11 +6518,15 @@ async function machineShowCommand(deps, name, options = {}) {
|
|
|
6456
6518
|
...processes.running.map((line) => `Background process: ${line}`),
|
|
6457
6519
|
...processes.last ? [`Last background process: ${processes.last}`] : []
|
|
6458
6520
|
] : [];
|
|
6521
|
+
const volume = options.json || machine.kind === "byoc" ? null : await volumeName(deps, key, machine);
|
|
6459
6522
|
print(
|
|
6460
6523
|
deps,
|
|
6461
6524
|
options.json,
|
|
6462
6525
|
machine,
|
|
6463
|
-
[
|
|
6526
|
+
[
|
|
6527
|
+
anyMachineText(machine, await billingUrl(deps), etag, lastEnd, volume),
|
|
6528
|
+
...processLines
|
|
6529
|
+
].join("\n")
|
|
6464
6530
|
);
|
|
6465
6531
|
});
|
|
6466
6532
|
}
|
|
@@ -6544,6 +6610,8 @@ function appliesText(name, change) {
|
|
|
6544
6610
|
}
|
|
6545
6611
|
async function machineUpdateCommand(deps, name, command, options = {}) {
|
|
6546
6612
|
await machineCommand(async () => {
|
|
6613
|
+
if (options.rebuild && options.image === void 0)
|
|
6614
|
+
throw new Error("--rebuild needs --image: it moves the Machine's whole disk to that image.");
|
|
6547
6615
|
if (command.length && options.imageCommand)
|
|
6548
6616
|
throw new Error("Give a command or --image-command, not both.");
|
|
6549
6617
|
if (options.clearPorts && (options.port?.length || options.defaultPort !== void 0))
|
|
@@ -6588,7 +6656,27 @@ async function machineUpdateCommand(deps, name, command, options = {}) {
|
|
|
6588
6656
|
if (options.discardSnapshot) change.discard_snapshot = true;
|
|
6589
6657
|
if (Object.values(change).every((value) => value === void 0))
|
|
6590
6658
|
throw new Error("Nothing to change: pass at least one option or a command.");
|
|
6591
|
-
const
|
|
6659
|
+
const rebuild = () => rebuildUntilDone(
|
|
6660
|
+
deps,
|
|
6661
|
+
key,
|
|
6662
|
+
name,
|
|
6663
|
+
DEFAULT_WAIT_SECONDS,
|
|
6664
|
+
() => deps.api.updateMachineRebuild(key, name, etag, change)
|
|
6665
|
+
);
|
|
6666
|
+
let rebuilt = options.rebuild ? await rebuild() : null;
|
|
6667
|
+
let machine = rebuilt?.machine;
|
|
6668
|
+
if (!machine)
|
|
6669
|
+
try {
|
|
6670
|
+
machine = await deps.api.updateMachine(key, name, etag, change);
|
|
6671
|
+
} catch (error) {
|
|
6672
|
+
if (!(error instanceof HttpError && error.code === "rebuild_required" && deps.interactive()))
|
|
6673
|
+
throw error;
|
|
6674
|
+
const question = `${rebuildWarning(options.image ?? current.image.reference)}
|
|
6675
|
+
This Machine's Volume is its whole disk, so its image changes only by Rebuild. Type the Machine's name to rebuild it with this change:`;
|
|
6676
|
+
if (await deps.ask(question) !== name) throw new Error("Cancelled; nothing was changed.");
|
|
6677
|
+
rebuilt = await rebuild();
|
|
6678
|
+
machine = rebuilt.machine;
|
|
6679
|
+
}
|
|
6592
6680
|
specWarnings(deps, machine.ports, Object.keys(setEnv));
|
|
6593
6681
|
print(
|
|
6594
6682
|
deps,
|
|
@@ -6596,12 +6684,80 @@ async function machineUpdateCommand(deps, name, command, options = {}) {
|
|
|
6596
6684
|
machine,
|
|
6597
6685
|
[
|
|
6598
6686
|
`Updated Machine ${machine.name} to revision ${machine.revision}.`,
|
|
6599
|
-
...appliesText(machine.name, change),
|
|
6687
|
+
...rebuilt ? [rebuiltText(machine.name, rebuilt.rebuild)] : appliesText(machine.name, change),
|
|
6600
6688
|
machineText(machine, await billingUrl(deps))
|
|
6601
6689
|
].join("\n")
|
|
6602
6690
|
);
|
|
6603
6691
|
});
|
|
6604
6692
|
}
|
|
6693
|
+
function rebuildWarning(image) {
|
|
6694
|
+
return machineRebuildWarning.map((paragraph) => paragraph.replaceAll("**", "").replaceAll("<image>", image)).join("\n");
|
|
6695
|
+
}
|
|
6696
|
+
function rebuiltText(name, answer) {
|
|
6697
|
+
const { reference, digest } = answer.image;
|
|
6698
|
+
const image = digest === null ? reference : `${reference} (${digest})`;
|
|
6699
|
+
if (answer.state === "not_needed")
|
|
6700
|
+
return `Machine ${name}'s disk was never used, so it moves to ${image} with no Rebuild.`;
|
|
6701
|
+
return `Machine ${name} rebuilds onto ${image} at its next wake; files in /home, /workspace and /root are kept.`;
|
|
6702
|
+
}
|
|
6703
|
+
async function rebuildUntilDone(deps, key, name, timeout, send) {
|
|
6704
|
+
const now = deps.now ?? Date.now;
|
|
6705
|
+
const start = now();
|
|
6706
|
+
for (; ; ) {
|
|
6707
|
+
const answer = await send();
|
|
6708
|
+
if (answer.rebuild.state !== "waiting_for_sleep") return answer;
|
|
6709
|
+
deps.notice(
|
|
6710
|
+
`Machine ${name} is awake with its disk attached, so it is going to sleep first; the Rebuild is sent again once it is asleep.`
|
|
6711
|
+
);
|
|
6712
|
+
const { machine: asleep } = await waitForMachine(
|
|
6713
|
+
deps,
|
|
6714
|
+
key,
|
|
6715
|
+
name,
|
|
6716
|
+
answer.retryAfterSeconds ?? sleepPollAfterSeconds,
|
|
6717
|
+
sleepPollAfterSeconds,
|
|
6718
|
+
timeout - (now() - start) / 1e3,
|
|
6719
|
+
(read) => read.status.power === "asleep" || read.state !== "active"
|
|
6720
|
+
);
|
|
6721
|
+
if (!asleep)
|
|
6722
|
+
throw new Error(
|
|
6723
|
+
`Machine ${name} is not asleep after ${Math.round((now() - start) / 1e3)} s, so nothing was rebuilt; run the command again once it sleeps. \`tonbo machine show ${name}\` says what keeps it awake.`
|
|
6724
|
+
);
|
|
6725
|
+
if (asleep.status.power !== "asleep")
|
|
6726
|
+
throw new Error(`Machine ${name} is ${asleep.state}; nothing was rebuilt.`);
|
|
6727
|
+
}
|
|
6728
|
+
}
|
|
6729
|
+
async function machineRebuildCommand(deps, name, options = {}) {
|
|
6730
|
+
await machineCommand(async () => {
|
|
6731
|
+
const key = await deps.auth.apiKey();
|
|
6732
|
+
const { machine: read, etag } = await deps.api.getMachineWithEtag(key, name);
|
|
6733
|
+
if (read.kind === "byoc")
|
|
6734
|
+
throw new Error(
|
|
6735
|
+
`${name} is one of your computers; only \`tonbo machine show\` and \`tonbo machine delete\` serve it.`
|
|
6736
|
+
);
|
|
6737
|
+
const image = options.image ?? read.image.reference;
|
|
6738
|
+
await confirm(
|
|
6739
|
+
deps,
|
|
6740
|
+
options.yes,
|
|
6741
|
+
`rebuild Machine ${name}`,
|
|
6742
|
+
() => Promise.resolve(`${rebuildWarning(image)}
|
|
6743
|
+
Type the Machine's name to rebuild it:`),
|
|
6744
|
+
name
|
|
6745
|
+
);
|
|
6746
|
+
const answer = await rebuildUntilDone(
|
|
6747
|
+
deps,
|
|
6748
|
+
key,
|
|
6749
|
+
name,
|
|
6750
|
+
options.timeout ?? DEFAULT_WAIT_SECONDS,
|
|
6751
|
+
() => deps.api.rebuildMachine(key, name, { image: options.image }, etag ?? void 0)
|
|
6752
|
+
);
|
|
6753
|
+
print(
|
|
6754
|
+
deps,
|
|
6755
|
+
options.json,
|
|
6756
|
+
{ data: answer.machine, rebuild: answer.rebuild },
|
|
6757
|
+
rebuiltText(name, answer.rebuild)
|
|
6758
|
+
);
|
|
6759
|
+
});
|
|
6760
|
+
}
|
|
6605
6761
|
function sleptText(machine) {
|
|
6606
6762
|
if (machine.state === "creating")
|
|
6607
6763
|
return `Machine ${machine.name} is still creating; nothing to put to sleep.`;
|
|
@@ -6684,43 +6840,61 @@ async function machineWakeCommand(deps, name, options = {}) {
|
|
|
6684
6840
|
}
|
|
6685
6841
|
async function machineDeleteCommand(deps, name, options = {}) {
|
|
6686
6842
|
await machineCommand(async () => {
|
|
6843
|
+
if (options.keepVolume && options.deleteVolume)
|
|
6844
|
+
throw new Error("Give --keep-volume or --delete-volume, not both.");
|
|
6845
|
+
const deleteVolume = options.deleteVolume ? true : options.keepVolume ? false : void 0;
|
|
6687
6846
|
const key = await deps.auth.apiKey();
|
|
6688
6847
|
const read = { etag: null };
|
|
6689
6848
|
await confirm(
|
|
6690
6849
|
deps,
|
|
6691
6850
|
options.yes,
|
|
6692
6851
|
`delete Machine ${name}`,
|
|
6693
|
-
() => deleteQuestion(deps, key, name, read),
|
|
6852
|
+
() => deleteQuestion(deps, key, name, read, deleteVolume),
|
|
6694
6853
|
name
|
|
6695
6854
|
);
|
|
6696
|
-
const { machine, kind } = await deps.api.deleteMachine(
|
|
6855
|
+
const { machine, kind, volume } = await deps.api.deleteMachine(
|
|
6697
6856
|
key,
|
|
6698
6857
|
name,
|
|
6699
|
-
ifMatchHeader(options.ifMatch) ?? read.etag ?? void 0
|
|
6858
|
+
ifMatchHeader(options.ifMatch) ?? read.etag ?? void 0,
|
|
6859
|
+
deleteVolume
|
|
6860
|
+
);
|
|
6861
|
+
print(
|
|
6862
|
+
deps,
|
|
6863
|
+
options.json,
|
|
6864
|
+
machine,
|
|
6865
|
+
[deletedText(name, machine, kind), ...volume ? [volumeFateText(volume)] : []].join("\n")
|
|
6700
6866
|
);
|
|
6701
|
-
print(deps, options.json, machine, deletedText(name, machine, kind));
|
|
6702
6867
|
if (machine?.kind === "byoc" && machine.byoc.key_state === "active")
|
|
6703
6868
|
deps.notice(
|
|
6704
6869
|
`Its key is still active, so ${name} registers again at its next start. Run \`yield logout\` on it, or revoke its key with \`tonbo api-key revoke\`, then forget it again.`
|
|
6705
6870
|
);
|
|
6706
6871
|
});
|
|
6707
6872
|
}
|
|
6708
|
-
async function deleteQuestion(deps, key, name, read) {
|
|
6709
|
-
let
|
|
6710
|
-
let ownDatabase = false;
|
|
6873
|
+
async function deleteQuestion(deps, key, name, read, deleteVolume) {
|
|
6874
|
+
let machine = null;
|
|
6711
6875
|
try {
|
|
6712
6876
|
const answer = await deps.api.getMachineWithEtag(key, name);
|
|
6713
|
-
|
|
6714
|
-
ownDatabase = answer.machine.kind === "managed" && !!answer.machine.database?.name.startsWith("m.");
|
|
6877
|
+
machine = answer.machine;
|
|
6715
6878
|
read.etag = answer.etag;
|
|
6716
6879
|
} catch (error) {
|
|
6717
6880
|
if (!(error instanceof HttpError && error.status === 404)) throw error;
|
|
6718
6881
|
}
|
|
6719
|
-
if (kind === "byoc")
|
|
6882
|
+
if (machine?.kind === "byoc")
|
|
6720
6883
|
return `Forget your computer ${name}? It disappears from your Machines; its key is not revoked. Type its name to confirm:`;
|
|
6721
|
-
if (kind
|
|
6722
|
-
|
|
6723
|
-
|
|
6884
|
+
if (machine?.kind !== "managed") return `Delete Machine ${name}? Type its name to confirm:`;
|
|
6885
|
+
const ownDatabase = !!machine.database?.name.startsWith("m.");
|
|
6886
|
+
const binding = machine.volumes?.[0];
|
|
6887
|
+
const parts = [
|
|
6888
|
+
...binding && binding.path === void 0 ? [] : ["disk"],
|
|
6889
|
+
...ownDatabase ? ["database"] : []
|
|
6890
|
+
];
|
|
6891
|
+
const what = parts.length ? `${parts.join(", ")} and logs` : "logs";
|
|
6892
|
+
const volume = binding ? `Volume ${await volumeName(deps, key, machine) ?? binding.volume}` : "";
|
|
6893
|
+
const fate = !binding ? "" : deleteVolume === true ? ` Its ${volume} is deleted with it.` : deleteVolume === false ? ` Its ${volume} is kept.` : ` Its ${volume} is deleted with it if it was made for this Machine (as the console makes a new Machine's disk), and kept otherwise; --keep-volume or --delete-volume decides.`;
|
|
6894
|
+
return `Delete Machine ${name} with its ${what}?${fate} This cannot be undone. Type the Machine's name to confirm:`;
|
|
6895
|
+
}
|
|
6896
|
+
function volumeFateText(volume) {
|
|
6897
|
+
return volume.deleted ? `Volume ${volume.name}: deleted with it.` : `Volume ${volume.name}: kept; delete it with \`tonbo volume delete ${volume.name}\`.`;
|
|
6724
6898
|
}
|
|
6725
6899
|
function deletedText(name, machine, kind) {
|
|
6726
6900
|
if (kind === "byoc")
|
|
@@ -6850,7 +7024,7 @@ var TOPICS = [
|
|
|
6850
7024
|
id: "confirmations",
|
|
6851
7025
|
title: "Destructive commands",
|
|
6852
7026
|
text: [
|
|
6853
|
-
"`api-key revoke`, `bucket delete`, `database delete`, `stream delete`, `machine delete` and `volume delete` ask before they act when stdin and stderr are a terminal: a revocation is confirmed by typing `yes`, a deletion by typing the resource's name, as in the console. Without a terminal they refuse unless `--yes` is given."
|
|
7027
|
+
"`api-key revoke`, `bucket delete`, `database delete`, `stream delete`, `machine delete`, `machine rebuild` and `volume delete` ask before they act when stdin and stderr are a terminal: a revocation is confirmed by typing `yes`, a deletion or a Rebuild by typing the resource's name, as in the console. Without a terminal they refuse unless `--yes` is given."
|
|
6854
7028
|
]
|
|
6855
7029
|
}
|
|
6856
7030
|
];
|
|
@@ -7188,7 +7362,7 @@ var COMMAND_HELP = {
|
|
|
7188
7362
|
},
|
|
7189
7363
|
"tonbo machine show": {
|
|
7190
7364
|
about: [
|
|
7191
|
-
"Prints the Machine's state and what it means (an `active` Machine that is asleep boots on its first request), its hostnames, image and whether it is pulled, the live activation and conditions with what to do about each, when it sleeps, the database it binds (or none) and its logs URL, a link to what its disk keeps, its ETag and who created it. For an `active` Machine it also reads the newest activations and prints how the last one ended. When the live activation runs an older revision than the Machine's, it says which changes already apply and that the rest apply after `tonbo machine sleep <name>`.",
|
|
7365
|
+
"Prints the Machine's state and what it means (an `active` Machine that is asleep boots on its first request), its hostnames, image and whether it is pulled, the live activation and conditions with what to do about each, when it sleeps, the database it binds (or none), its Volume by name and id as its whole disk or where it is mounted (or none), its logs URL, a link to what its disk keeps, its ETag and who created it. For an `active` Machine it also reads the newest activations and prints how the last one ended. When the live activation runs an older revision than the Machine's, it says which changes already apply and that the rest apply after `tonbo machine sleep <name>`.",
|
|
7192
7366
|
"When the default port is private, it prints the `curl` call that reaches it with a personal key. For one of your computers it prints its system, harnesses, hostname and device id, which only you see."
|
|
7193
7367
|
],
|
|
7194
7368
|
examples: ["tonbo machine show web"],
|
|
@@ -7219,10 +7393,13 @@ var COMMAND_HELP = {
|
|
|
7219
7393
|
"tonbo machine update": {
|
|
7220
7394
|
about: [
|
|
7221
7395
|
"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 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.",
|
|
7222
|
-
"Ports and `--sleep-after` apply at once. The image, command, shape, env, Volume and database apply at the next boot, and the command says how to apply them now: `tonbo machine sleep`, after which the next request boots the new spec. Naming the same tag again resolves its digest again. A Machine whose Volume is its whole disk keeps its image: another image is refused with `rebuild_required`, since only Rebuild, which keeps /home, /workspace and /root and deletes every other change on the disk, may move it
|
|
7396
|
+
"Ports and `--sleep-after` apply at once. The image, command, shape, env, Volume and database apply at the next boot, and the command says how to apply them now: `tonbo machine sleep`, after which the next request boots the new spec. Naming the same tag again resolves its digest again. A Machine whose Volume is its whole disk keeps its image: another image is refused with `rebuild_required`, since only Rebuild, which keeps /home, /workspace and /root and deletes every other change on the disk, may move it. At a terminal the CLI then prints what Rebuild deletes and, once you type the Machine's name, sends the change again as a Rebuild; `--rebuild` with `--image` sends it as one at once, as `tonbo machine rebuild` does, and on a Machine whose Volume is mounted at a path, or that has none, it is refused with `volume_not_whole_disk`. `--database <name>` binds another database of the account in the Machine's region, which no other Machine binds, and `--no-database` binds none; the database it leaves is kept. A Machine created with its own database keeps it.",
|
|
7223
7397
|
"`--delete-after` applies at once and keeps the clock, which counts from the end of the Machine's last activation (or its creation), so a lower value can delete a Machine that has already slept that long; `--no-delete-after` turns it off. `tonbo machine show` prints when it will be deleted."
|
|
7224
7398
|
],
|
|
7225
|
-
examples: [
|
|
7399
|
+
examples: [
|
|
7400
|
+
"tonbo machine update web --env MODE=staging --sleep-after 300",
|
|
7401
|
+
"tonbo machine update agent --image ghcr.io/acme/agent:v2 --rebuild"
|
|
7402
|
+
],
|
|
7226
7403
|
arguments: {
|
|
7227
7404
|
name: "the Machine's name",
|
|
7228
7405
|
command: "the command that replaces the Machine's command at the next boot, after `--`"
|
|
@@ -7358,9 +7535,25 @@ var COMMAND_HELP = {
|
|
|
7358
7535
|
arguments: NAME("Machine"),
|
|
7359
7536
|
defaults: { "--timeout": String(DEFAULT_WAIT_SECONDS) }
|
|
7360
7537
|
},
|
|
7538
|
+
"tonbo machine rebuild": {
|
|
7539
|
+
about: [
|
|
7540
|
+
"Moves a Machine whose Volume is its whole disk to an image: `--image`, or without it the Machine's own reference resolved again, so a newer build of its tag is taken. Rebuild keeps /home, /workspace and /root and deletes every other change on the disk. At a terminal it prints the warning of what it deletes and asks you to type the Machine's name first, and sends the ETag it read then.",
|
|
7541
|
+
"An asleep Machine answers at once: the Rebuild is recorded and the disk moves to the image at the next wake, or, when the disk was never used, the image changes with no Rebuild. An awake Machine with its disk attached is put to sleep first and nothing else changes; the CLI reads it until it is asleep, for up to `--timeout`, and sends the Rebuild again. A Machine whose Volume is mounted at a path, or that has none, is refused with `volume_not_whole_disk`: change its image with `tonbo machine update --image`."
|
|
7542
|
+
],
|
|
7543
|
+
examples: [
|
|
7544
|
+
"tonbo machine rebuild agent --image ghcr.io/acme/agent:v2",
|
|
7545
|
+
"tonbo machine rebuild agent --yes"
|
|
7546
|
+
],
|
|
7547
|
+
arguments: NAME("Machine"),
|
|
7548
|
+
defaults: {
|
|
7549
|
+
"--image": "the Machine's own image reference, resolved again",
|
|
7550
|
+
"--timeout": String(DEFAULT_WAIT_SECONDS)
|
|
7551
|
+
},
|
|
7552
|
+
json: "answer"
|
|
7553
|
+
},
|
|
7361
7554
|
"tonbo machine delete": {
|
|
7362
7555
|
about: [
|
|
7363
|
-
"Deletes the Machine with its disk and logs; a
|
|
7556
|
+
"Deletes the Machine with its disk and logs; a database it binds stays. The Volume it holds is deleted with it when it was made for it, as the console makes a new Machine's disk, and kept otherwise; `--delete-volume` deletes any Volume it holds and `--keep-volume` keeps it. The command then says which. Its name is free for a new Machine at once. At a terminal it asks you to type the Machine's name first, and sends the ETag it read then.",
|
|
7364
7557
|
"For one of your computers it forgets the computer: it leaves 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; run `yield logout` on it, or `tonbo api-key revoke`, first."
|
|
7365
7558
|
],
|
|
7366
7559
|
examples: ["tonbo machine delete web --yes"],
|
|
@@ -9264,7 +9457,10 @@ function createProgram(makeDependencies = createDependencies) {
|
|
|
9264
9457
|
machineSpecOptions(
|
|
9265
9458
|
machine.command("update <name> [command...]").description(
|
|
9266
9459
|
"change a Machine; [command...] after -- replaces its command at the next boot"
|
|
9267
|
-
).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(
|
|
9460
|
+
).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(
|
|
9461
|
+
"--rebuild",
|
|
9462
|
+
"with --image, move a Machine whose Volume is its whole disk to the image by Rebuild, which deletes changes outside /home, /workspace and /root"
|
|
9463
|
+
).option("--unset-env <KEY>", "remove an environment variable; repeat for several", repeated).option("--clear-ports", "remove every port").option("--if-match <etag>", ifMatchHelp("change")).option("--discard-snapshot", "apply a change that discards the Machine's snapshot").option("--no-volume", "remove the Machine's Volume at the next boot; the Volume is kept").option(
|
|
9268
9464
|
"--database <name>",
|
|
9269
9465
|
"bind the account's database <name> of the Machine's region instead; applies at the next wake"
|
|
9270
9466
|
).option("--no-database", "bind no database from the next wake; the database is kept").option("--no-delete-after", "never delete the Machine for being asleep"),
|
|
@@ -9390,7 +9586,19 @@ function createProgram(makeDependencies = createDependencies) {
|
|
|
9390
9586
|
async (name, options) => machineWakeCommand(dependencies(), name, options)
|
|
9391
9587
|
);
|
|
9392
9588
|
operationOptions(
|
|
9393
|
-
machine.command("
|
|
9589
|
+
machine.command("rebuild <name>").description(
|
|
9590
|
+
"move a Machine whose Volume is its whole disk to an image; deletes changes outside /home, /workspace and /root"
|
|
9591
|
+
).option("--image <reference>", "the image to move to, named as for create").option("--yes", "rebuild without asking; required when stdin is not a terminal").option(
|
|
9592
|
+
"--timeout <seconds>",
|
|
9593
|
+
"how long to wait for an awake Machine to sleep, in seconds",
|
|
9594
|
+
parseWhole
|
|
9595
|
+
),
|
|
9596
|
+
"rebuildMachine"
|
|
9597
|
+
).action(
|
|
9598
|
+
async (name, options) => machineRebuildCommand(dependencies(), name, options)
|
|
9599
|
+
);
|
|
9600
|
+
operationOptions(
|
|
9601
|
+
machine.command("delete <name>").description("delete a Machine with its disk and logs, or forget one of your computers").option("--yes", "delete without asking; required when stdin is not a terminal").option("--if-match <etag>", ifMatchHelp("delete")).option("--keep-volume", "keep the Machine's Volume, even one made for it").option("--delete-volume", "delete the Machine's Volume with it, even one not made for it"),
|
|
9394
9602
|
"deleteMachine"
|
|
9395
9603
|
).action(
|
|
9396
9604
|
async (name, options) => machineDeleteCommand(dependencies(), name, options)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tonbo/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "The Tonbo CLI. Compute, disks, databases and streams for AI agents: your agent picks what it needs, sets it up, and knows what it costs, from the command line.",
|
|
5
5
|
"homepage": "https://tonbo.dev",
|
|
6
6
|
"bugs": {
|