@tonbo/cli 0.17.2 → 0.18.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 +4 -0
- package/dist/bin/tonbo.js +448 -63
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,6 +30,10 @@ Every command other than `login` and `logout` is named by one operation of the [
|
|
|
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
|
|
|
33
|
+
## Updates
|
|
34
|
+
|
|
35
|
+
Every request to the Management API and to tonbo.dev carries the CLI's release in `Tonbo-CLI-Version`, and every answer names the latest release in `Tonbo-CLI-Latest` and the oldest supported one in `Tonbo-CLI-Minimum`, by the contract [`versions-v1.json`](../../contracts/cli/versions-v1.json): Latest is this package's `version` on main, so merging a release bump moves it with no other change, and Minimum is declared in the contract. When Latest is newer, the command ends with one line on stderr, at most once a day (`update-notice.json` beside the login), never with `--json`, without a terminal, under `CI` or with `TONBO_NO_UPDATE_NOTICE=1`. A release below Minimum is refused with 410 `cli_upgrade_required` on any request; the CLI prints the detail and exits 1. `tonbo update` runs the update of the package manager that installed it (npm, pnpm, bun or yarn), printing the command first, and only prints the command for npx or a path it cannot tell; `--check` only compares. [`src/update.ts`](src/update.ts) holds all of it.
|
|
36
|
+
|
|
33
37
|
## Related design
|
|
34
38
|
|
|
35
39
|
- [ADR-0008: Tonbo CLI onboarding and credential boundaries](../../apps/user-center/docs/decisions/adr-0008-tonbo-cli-onboarding.md)
|
package/dist/bin/tonbo.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
// src/app.ts
|
|
4
4
|
import { Command, InvalidArgumentError, Option } from "commander";
|
|
5
|
-
import { readFileSync as readFileSync2 } from "node:fs";
|
|
6
5
|
|
|
7
6
|
// ../../packages/tonbo-cli-core/dist/src/generated/contracts.js
|
|
8
7
|
var agentNameContract = {
|
|
@@ -403,6 +402,7 @@ var managementOperations = {
|
|
|
403
402
|
"query": [
|
|
404
403
|
"origin",
|
|
405
404
|
"scope",
|
|
405
|
+
"owners",
|
|
406
406
|
"cursor",
|
|
407
407
|
"limit"
|
|
408
408
|
],
|
|
@@ -1773,6 +1773,102 @@ var loginContract = {
|
|
|
1773
1773
|
]
|
|
1774
1774
|
}
|
|
1775
1775
|
};
|
|
1776
|
+
var cliVersionsContract = {
|
|
1777
|
+
"version": 1,
|
|
1778
|
+
"description": "Which `tonbo` CLI (@tonbo/cli) releases Tonbo supports, and how a CLI learns of a newer one without a request of its own. User Center answers every request to the Management API host, and every request that carries `headers.client`, with `headers.latest` and `headers.minimum`. A request whose `headers.client` is a release below `minimum` is refused with `refusal.code` (contracts/management/problems-v1.json holds its status and title), its detail naming the update; a request without the header, or with one that is not a release, is never refused for its version. @tonbo/cli sends `headers.client` on every request, prints `notice.line` when `headers.latest` is newer than itself, and on the refusal prints its detail and exits 1. @tonbo/yield does not send the header.",
|
|
1779
|
+
"package": "@tonbo/cli",
|
|
1780
|
+
"latest": {
|
|
1781
|
+
"source": "cli/tonbo/package.json",
|
|
1782
|
+
"field": "version",
|
|
1783
|
+
"description": "The release main carries when User Center is deployed. Merging a version change to main is what publishes it (tonbo-cli-release.yml); the same push redeploys User Center, whose deploy waits until npm serves that version before it ships, so `latest` is always installable and follows each release with no bump of its own."
|
|
1784
|
+
},
|
|
1785
|
+
"minimum": "0.12.0",
|
|
1786
|
+
"minimum_reason": "0.12.0 is the oldest release that can sign in: the login of 0.11 and earlier was retired when 0.12 shipped (contracts/iam/access-token-v1.json `cli_session.login.retired`). Raise it only when a change of the API leaves older releases unable to work, and in the same change say so in the CLI's changelog.",
|
|
1787
|
+
"headers": {
|
|
1788
|
+
"client": "Tonbo-CLI-Version",
|
|
1789
|
+
"latest": "Tonbo-CLI-Latest",
|
|
1790
|
+
"minimum": "Tonbo-CLI-Minimum"
|
|
1791
|
+
},
|
|
1792
|
+
"release_pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",
|
|
1793
|
+
"release_order": "Semantic Versioning 2.0.0 precedence: major, minor and patch compare as numbers, and a release with a pre-release suffix is older than the same release without one; pre-release suffixes compare as text. A value that does not match `release_pattern` is not a release and orders with nothing. `order_examples` are [a, b, the sign of a compared with b, or null when either is not a release]; every implementation runs them.",
|
|
1794
|
+
"order_examples": [
|
|
1795
|
+
[
|
|
1796
|
+
"0.17.2",
|
|
1797
|
+
"0.17.3",
|
|
1798
|
+
-1
|
|
1799
|
+
],
|
|
1800
|
+
[
|
|
1801
|
+
"0.17.3",
|
|
1802
|
+
"0.17.3",
|
|
1803
|
+
0
|
|
1804
|
+
],
|
|
1805
|
+
[
|
|
1806
|
+
"0.18.0",
|
|
1807
|
+
"0.17.9",
|
|
1808
|
+
1
|
|
1809
|
+
],
|
|
1810
|
+
[
|
|
1811
|
+
"0.9.0",
|
|
1812
|
+
"0.12.0",
|
|
1813
|
+
-1
|
|
1814
|
+
],
|
|
1815
|
+
[
|
|
1816
|
+
"1.0.0",
|
|
1817
|
+
"0.99.99",
|
|
1818
|
+
1
|
|
1819
|
+
],
|
|
1820
|
+
[
|
|
1821
|
+
"0.18.0-beta.1",
|
|
1822
|
+
"0.18.0",
|
|
1823
|
+
-1
|
|
1824
|
+
],
|
|
1825
|
+
[
|
|
1826
|
+
"0.18.0-beta.2",
|
|
1827
|
+
"0.18.0-beta.1",
|
|
1828
|
+
1
|
|
1829
|
+
],
|
|
1830
|
+
[
|
|
1831
|
+
"0.17",
|
|
1832
|
+
"0.17.0",
|
|
1833
|
+
null
|
|
1834
|
+
],
|
|
1835
|
+
[
|
|
1836
|
+
"v0.17.2",
|
|
1837
|
+
"0.17.2",
|
|
1838
|
+
null
|
|
1839
|
+
],
|
|
1840
|
+
[
|
|
1841
|
+
"0.17.2 ",
|
|
1842
|
+
"0.17.2",
|
|
1843
|
+
null
|
|
1844
|
+
],
|
|
1845
|
+
[
|
|
1846
|
+
"",
|
|
1847
|
+
"0.12.0",
|
|
1848
|
+
null
|
|
1849
|
+
]
|
|
1850
|
+
],
|
|
1851
|
+
"refusal": {
|
|
1852
|
+
"code": "cli_upgrade_required",
|
|
1853
|
+
"registry": "contracts/management/problems-v1.json"
|
|
1854
|
+
},
|
|
1855
|
+
"update_command": "npm install --global @tonbo/cli@latest",
|
|
1856
|
+
"notice": {
|
|
1857
|
+
"line": "tonbo {latest} is available (you have {current}): run tonbo update",
|
|
1858
|
+
"stream": "stderr",
|
|
1859
|
+
"interval_hours": 24,
|
|
1860
|
+
"state": {
|
|
1861
|
+
"description": "When the CLI last printed the notice: `${XDG_CONFIG_HOME}/tonbo/update-notice.json`, else `${HOME}/.config/tonbo/update-notice.json`, beside the login. A state that cannot be read counts as never; one that cannot be written never fails the command.",
|
|
1862
|
+
"file": "update-notice.json"
|
|
1863
|
+
},
|
|
1864
|
+
"silent_when": [
|
|
1865
|
+
"the command line has --json",
|
|
1866
|
+
"neither stdout nor stderr is a terminal",
|
|
1867
|
+
"CI is set to anything but empty, 0 or false",
|
|
1868
|
+
"TONBO_NO_UPDATE_NOTICE is set to anything but empty or 0"
|
|
1869
|
+
]
|
|
1870
|
+
}
|
|
1871
|
+
};
|
|
1776
1872
|
var cliSessionContract = {
|
|
1777
1873
|
"prefix": "tbs_",
|
|
1778
1874
|
"grant": "cli_session",
|
|
@@ -2036,7 +2132,7 @@ var ManagementApi = class {
|
|
|
2036
2132
|
}
|
|
2037
2133
|
async requestWith(operationId, key, { params = {}, query = {}, body, idempotencyKey, ifMatch }, seen) {
|
|
2038
2134
|
const operation = managementOperation(operationId);
|
|
2039
|
-
const
|
|
2135
|
+
const path6 = operation.path.replace(/\{([^}]+)\}/g, (_, name) => {
|
|
2040
2136
|
const value = params[name];
|
|
2041
2137
|
if (value === void 0)
|
|
2042
2138
|
throw new Error(`${operationId} needs the path parameter ${name}.`);
|
|
@@ -2060,7 +2156,7 @@ var ManagementApi = class {
|
|
|
2060
2156
|
if (ifMatch)
|
|
2061
2157
|
headers["if-match"] = ifMatch;
|
|
2062
2158
|
const queryString = search.toString();
|
|
2063
|
-
const url = `${this.apiOrigin}${managementApiBasePath}${
|
|
2159
|
+
const url = `${this.apiOrigin}${managementApiBasePath}${path6}${queryString ? `?${queryString}` : ""}`;
|
|
2064
2160
|
try {
|
|
2065
2161
|
const fetcher = seen ? async (input, init) => {
|
|
2066
2162
|
const response = await this.fetcher(input, init);
|
|
@@ -3867,7 +3963,7 @@ var CellApi = class {
|
|
|
3867
3963
|
}
|
|
3868
3964
|
url(operationId, region, request) {
|
|
3869
3965
|
const operation = managementOperations[operationId];
|
|
3870
|
-
const
|
|
3966
|
+
const path6 = operation.path.replace(/\{([^}]+)\}/g, (_, name) => {
|
|
3871
3967
|
const value = request.params[name];
|
|
3872
3968
|
if (value === void 0)
|
|
3873
3969
|
throw new Error(`${operationId} needs the path parameter ${name}.`);
|
|
@@ -3878,7 +3974,7 @@ var CellApi = class {
|
|
|
3878
3974
|
if (value !== void 0)
|
|
3879
3975
|
search.set(name, value);
|
|
3880
3976
|
const query = search.toString();
|
|
3881
|
-
return `${cellOrigin(region)}${managementApiBasePath}${
|
|
3977
|
+
return `${cellOrigin(region)}${managementApiBasePath}${path6}${query ? `?${query}` : ""}`;
|
|
3882
3978
|
}
|
|
3883
3979
|
async init(operationId, key, request) {
|
|
3884
3980
|
const headers = {
|
|
@@ -5424,12 +5520,22 @@ async function volumeDeleteCommand(deps, name, options = {}) {
|
|
|
5424
5520
|
);
|
|
5425
5521
|
});
|
|
5426
5522
|
}
|
|
5427
|
-
|
|
5523
|
+
function parseVolumeSpec(spec) {
|
|
5428
5524
|
const split = spec.indexOf(":");
|
|
5525
|
+
if (split === -1 && spec.length > 0) return { name: spec };
|
|
5429
5526
|
if (split < 1 || split === spec.length - 1)
|
|
5430
|
-
throw new Error(
|
|
5431
|
-
|
|
5432
|
-
|
|
5527
|
+
throw new Error(
|
|
5528
|
+
`--volume ${spec}: give <name> for the Machine's whole disk, or <name>:<path> to mount it at a path, such as data:/data.`
|
|
5529
|
+
);
|
|
5530
|
+
return { name: spec.slice(0, split), path: spec.slice(split + 1) };
|
|
5531
|
+
}
|
|
5532
|
+
async function resolveVolumeBinding(deps, key, spec) {
|
|
5533
|
+
const { name, path: path6 } = parseVolumeSpec(spec);
|
|
5534
|
+
const volume = await deps.api.getVolume(key, name);
|
|
5535
|
+
return {
|
|
5536
|
+
binding: path6 === void 0 ? { volume: volume.id } : { volume: volume.id, path: path6 },
|
|
5537
|
+
region: volume.region
|
|
5538
|
+
};
|
|
5433
5539
|
}
|
|
5434
5540
|
|
|
5435
5541
|
// src/machine.ts
|
|
@@ -5455,6 +5561,9 @@ var MACHINE_ERROR_HINTS = {
|
|
|
5455
5561
|
image_failed: "The Machine's image could not be pulled; `tonbo machine show` says why. Fix the image, then name it again with `tonbo machine update --image`.",
|
|
5456
5562
|
machine_kind_mismatch: "That Machine is one of your computers: `tonbo machine show` and `tonbo machine delete` serve it, and nothing else does.",
|
|
5457
5563
|
volumes_not_offered: "Volumes are not offered to this account in the Machine's region yet; nothing was created or changed.",
|
|
5564
|
+
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>.",
|
|
5565
|
+
rebuild_required: "This Machine's Volume is its whole disk, so its image changes only by Rebuild, which keeps /home, /workspace and /root and deletes every other change on the disk. Rebuild is not available yet: keep the image, or create a new Machine with the new image and a new Volume.",
|
|
5566
|
+
volume_mode_change: "The Volume cannot move between being the Machine's whole disk and being mounted at a path; nothing was changed. Bind a new Volume in the mode you want.",
|
|
5458
5567
|
secret_env_conflict: "Use another secret key or remove the non-secret env variable with that name.",
|
|
5459
5568
|
secret_limit: "Reduce the number or byte size of this Machine's secrets.",
|
|
5460
5569
|
wake_unavailable: "The wake was refused for now; nothing woke. Wake it again after a while; `tonbo machine show` says why it is blocked."
|
|
@@ -5571,17 +5680,23 @@ var MACHINE_WAKE_BLOCKED_HINTS = {
|
|
|
5571
5680
|
standing: "The account's Streams standing is not solvent: settle the account at <billing>, and the next request wakes it.",
|
|
5572
5681
|
meter_lag: "Tonbo could not confirm the account's usage in time; the next request tries again.",
|
|
5573
5682
|
crash_backoff: "Its recent wakes failed; see `tonbo machine activations <name>`.",
|
|
5574
|
-
capacity: "No host had room for its shape
|
|
5683
|
+
capacity: "No host had room for its shape: Tonbo is adding a host, <minutes>, and the next request after that wakes it.",
|
|
5575
5684
|
region_closed: "Machines are closed in its region for now; the next request after they reopen wakes it.",
|
|
5576
5685
|
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.",
|
|
5577
5686
|
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."
|
|
5578
5687
|
};
|
|
5579
5688
|
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.";
|
|
5689
|
+
function minutesUntil(retryAt, now = Date.now()) {
|
|
5690
|
+
const at = retryAt ? Date.parse(retryAt) : Number.NaN;
|
|
5691
|
+
if (!Number.isFinite(at) || at <= now) return "which takes a few minutes";
|
|
5692
|
+
const minutes = Math.ceil((at - now) / 6e4);
|
|
5693
|
+
return `about ${minutes} minute${minutes === 1 ? "" : "s"}`;
|
|
5694
|
+
}
|
|
5580
5695
|
function conditionText(name, condition, billing) {
|
|
5581
5696
|
switch (condition.type) {
|
|
5582
5697
|
case "wake_blocked": {
|
|
5583
5698
|
const retry = condition.retry_at ? `, retry after ${condition.retry_at}` : "";
|
|
5584
|
-
const hint = MACHINE_WAKE_BLOCKED_HINTS[condition.reason]?.replaceAll("<name>", name).replaceAll("<billing>", billing);
|
|
5699
|
+
const hint = MACHINE_WAKE_BLOCKED_HINTS[condition.reason]?.replaceAll("<name>", name).replaceAll("<billing>", billing).replaceAll("<minutes>", minutesUntil(condition.retry_at));
|
|
5585
5700
|
return `wake blocked (${condition.reason.replaceAll("_", " ")}) since ${condition.since}${retry}.${hint ? ` ${hint}` : ""}`;
|
|
5586
5701
|
}
|
|
5587
5702
|
case "creation_delayed":
|
|
@@ -5711,7 +5826,8 @@ function deleteFields(machine) {
|
|
|
5711
5826
|
}
|
|
5712
5827
|
function volumeText(machine) {
|
|
5713
5828
|
const binding = machine.volumes?.[0];
|
|
5714
|
-
|
|
5829
|
+
if (!binding) return "none";
|
|
5830
|
+
return binding.path === void 0 ? `${binding.volume} (whole disk)` : `${binding.volume} at ${binding.path}`;
|
|
5715
5831
|
}
|
|
5716
5832
|
function machineText(machine, billing, etag = null, lastEnd = null) {
|
|
5717
5833
|
return [machineFields(machine, billing, etag, lastEnd), ...privatePortLines(machine)].join("\n");
|
|
@@ -6396,7 +6512,15 @@ var ENVIRONMENT = [
|
|
|
6396
6512
|
},
|
|
6397
6513
|
{
|
|
6398
6514
|
names: ["XDG_CONFIG_HOME"],
|
|
6399
|
-
description: "Where the login file lives: `$XDG_CONFIG_HOME/tonbo/login.json` when it is set and not empty, else `$HOME/.config/tonbo/login.json`. A login an earlier release saved, `credentials.json` beside it, is read only when there is no `login.json`."
|
|
6515
|
+
description: "Where the login file lives: `$XDG_CONFIG_HOME/tonbo/login.json` when it is set and not empty, else `$HOME/.config/tonbo/login.json`. A login an earlier release saved, `credentials.json` beside it, is read only when there is no `login.json`. `update-notice.json` beside it records when the update notice last printed."
|
|
6516
|
+
},
|
|
6517
|
+
{
|
|
6518
|
+
names: ["TONBO_NO_UPDATE_NOTICE"],
|
|
6519
|
+
description: "Set to `1` (anything but empty or `0`), the CLI never prints that a newer release is available."
|
|
6520
|
+
},
|
|
6521
|
+
{
|
|
6522
|
+
names: ["CI"],
|
|
6523
|
+
description: "Set to anything but empty, `0` or `false`, as CI services set it, the CLI never prints that a newer release is available."
|
|
6400
6524
|
},
|
|
6401
6525
|
{
|
|
6402
6526
|
names: ["XDG_STATE_HOME"],
|
|
@@ -6451,6 +6575,14 @@ var TOPICS = [
|
|
|
6451
6575
|
"`api-key create` takes 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. Every other command reads, or sets a resource to the state it names, so running it again does no more than the first run did; a repeated delete or revoke may be refused because the first one already removed it."
|
|
6452
6576
|
]
|
|
6453
6577
|
},
|
|
6578
|
+
{
|
|
6579
|
+
id: "updates",
|
|
6580
|
+
title: "Updates",
|
|
6581
|
+
text: [
|
|
6582
|
+
"The CLI names its release in a `Tonbo-CLI-Version` header on every request to the Management API and to tonbo.dev, and every answer of the Management API names the latest release in `Tonbo-CLI-Latest` and the oldest one Tonbo supports in `Tonbo-CLI-Minimum`, so the CLI learns of a new release with no request of its own; `curl -sI https://api.tonbo.dev/v1/openapi.json` shows both headers. When the latest is newer, the command ends with `tonbo <latest> is available (you have <current>): run tonbo update` on stderr, at most once a day. It never prints with `--json`, when neither stdout nor stderr is a terminal, under `CI=true`, or with `TONBO_NO_UPDATE_NOTICE=1`. An agent that runs the CLI in a terminal sees it too, and should run `tonbo update` before it goes on.",
|
|
6583
|
+
"`tonbo update` updates the CLI with the package manager that installed it, printing the command before it runs it; nothing is ever updated in the background. A release older than `Tonbo-CLI-Minimum` is refused on every request with 410 `cli_upgrade_required`: the CLI prints the refusal, which names the update, and exits 1. `tonbo update` itself still works then."
|
|
6584
|
+
]
|
|
6585
|
+
},
|
|
6454
6586
|
{
|
|
6455
6587
|
id: "confirmations",
|
|
6456
6588
|
title: "Destructive commands",
|
|
@@ -6489,6 +6621,13 @@ var COMMAND_HELP = {
|
|
|
6489
6621
|
],
|
|
6490
6622
|
examples: ["tonbo logout"]
|
|
6491
6623
|
},
|
|
6624
|
+
"tonbo update": {
|
|
6625
|
+
about: [
|
|
6626
|
+
"Reads the latest release from the `Tonbo-CLI-Latest` header of the Management API's answer, then updates the CLI with the package manager that installed it: `npm install --global @tonbo/cli@latest`, `pnpm add --global @tonbo/cli@latest`, `bun add --global @tonbo/cli@latest` or `yarn global add @tonbo/cli@latest`. It prints the command first, then runs it in this terminal, and exits with 1 if it fails. When this release is already the latest, it says so and runs nothing.",
|
|
6627
|
+
"A CLI run through npx, `pnpm dlx` or bunx runs the release you name each time, so it prints `npx @tonbo/cli@latest` and runs nothing. When it cannot tell how the CLI was installed, as for a project's own dependency or a copy, it prints the command to run and exits 1. `--check` only prints this release and the latest one."
|
|
6628
|
+
],
|
|
6629
|
+
examples: ["tonbo update", "tonbo update --check", "tonbo update --check --json"]
|
|
6630
|
+
},
|
|
6492
6631
|
"tonbo account show": {
|
|
6493
6632
|
about: [
|
|
6494
6633
|
"Prints the account commands act in: its id, name and kind, your member subject and role in it, and its limits."
|
|
@@ -6762,7 +6901,7 @@ var COMMAND_HELP = {
|
|
|
6762
6901
|
},
|
|
6763
6902
|
"tonbo machine create": {
|
|
6764
6903
|
about: [
|
|
6765
|
-
"Creates a Machine, a VM booted from an image in one region. It has no database of its own: `--database <name>` binds one of the account's databases of its region, which it then reaches at `TONBO_DATABASE_URL`, and a database binds one Machine at a time; its
|
|
6904
|
+
"Creates a Machine, a VM booted from an image in one region. It has no database of its own: `--database <name>` binds one of the account's databases of its region, which it then reaches at `TONBO_DATABASE_URL`, and a database binds one Machine at a time; its disk can last on a Volume (`--volume <name>` keeps the Machine's whole disk on it, and `--volume <name>:<path>` mounts it at a path instead), and its program can open any other database by URL with a key. The image is a public OCI image with a linux/arm64 variant, from Docker Hub or another public registry (`nginx:1.27`, `ghcr.io/org/app:v1`, or one pinned by digest), or `tonbo.dev/platform`, the platform image. A registry reachable only over IPv6, one named by an IP address, and one that redirects its manifests are refused. The CLI resolves a registry image to its linux/arm64 digest, and the image is pulled before the Machine leaves `creating`.",
|
|
6766
6905
|
"The Machine is served at `https://<name>-<account>.tonbo.sh` on its default port. `--port <port>` serves a private port, which takes a member's token, and `--port <port>:public` an open one, which needs the `machines.public` capability. A command after `--` replaces the image's CMD. `--secret-env KEY=FILE` and `--secret-file KEY=FILE` read initial secrets from local files and store them with the Machine before its first boot; their values are never returned. The CLI warns on stderr when the Machine has no port, since nothing can wake it then, and when an `--env` key looks secret: env values are readable by every member through the API.",
|
|
6767
6906
|
"The answer is the Machine while it is `creating`. With `--wait` the CLI reads it until it is `active` or `failed`, or `--timeout` passes: a failure prints what to do and exits 1, and a timeout says it is still creating and undoes nothing. A creation the network or a 5xx answer did not confirm is sent at most three times with the same idempotency key, each retry after its `Retry-After` (at most 30 s) or a short backoff; a `name_taken` refusal says which Machine holds the name and how old it is, in case it is yours from a lost reply. In a region not open for Machines the creation is refused with `region_not_offered`, and nothing is created."
|
|
6768
6907
|
],
|
|
@@ -6813,7 +6952,7 @@ var COMMAND_HELP = {
|
|
|
6813
6952
|
"tonbo machine update": {
|
|
6814
6953
|
about: [
|
|
6815
6954
|
"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.",
|
|
6816
|
-
"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. `--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.",
|
|
6955
|
+
"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, and Rebuild is not available yet. `--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.",
|
|
6817
6956
|
"`--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."
|
|
6818
6957
|
],
|
|
6819
6958
|
examples: ["tonbo machine update web --env MODE=staging --sleep-after 300"],
|
|
@@ -7102,19 +7241,19 @@ function optionDescription(option) {
|
|
|
7102
7241
|
function applyHelp(program) {
|
|
7103
7242
|
const named = /* @__PURE__ */ new Set();
|
|
7104
7243
|
const walk = (command, words2) => {
|
|
7105
|
-
const
|
|
7244
|
+
const path6 = words2.join(" ");
|
|
7106
7245
|
command.configureHelp({ ...command.configureHelp(), optionDescription });
|
|
7107
|
-
const help = COMMAND_HELP[
|
|
7246
|
+
const help = COMMAND_HELP[path6];
|
|
7108
7247
|
if (help) {
|
|
7109
|
-
named.add(
|
|
7248
|
+
named.add(path6);
|
|
7110
7249
|
for (const [name, text] of Object.entries(help.arguments ?? {})) {
|
|
7111
7250
|
const argument = command.registeredArguments.find((entry) => entry.name() === name);
|
|
7112
|
-
if (!argument) throw new Error(`${
|
|
7251
|
+
if (!argument) throw new Error(`${path6} has no argument ${name}`);
|
|
7113
7252
|
argument.description = text;
|
|
7114
7253
|
}
|
|
7115
7254
|
for (const [flag, text] of Object.entries(help.defaults ?? {})) {
|
|
7116
7255
|
const option = command.options.find((entry) => entry.long === flag);
|
|
7117
|
-
if (!option) throw new Error(`${
|
|
7256
|
+
if (!option) throw new Error(`${path6} has no option ${flag}`);
|
|
7118
7257
|
option.defaultValueDescription = text;
|
|
7119
7258
|
}
|
|
7120
7259
|
command.addHelpText("after", commandHelpText(help));
|
|
@@ -7122,8 +7261,8 @@ function applyHelp(program) {
|
|
|
7122
7261
|
for (const child of command.commands) walk(child, [...words2, child.name()]);
|
|
7123
7262
|
};
|
|
7124
7263
|
walk(program, [program.name()]);
|
|
7125
|
-
for (const
|
|
7126
|
-
if (!named.has(
|
|
7264
|
+
for (const path6 of Object.keys(COMMAND_HELP))
|
|
7265
|
+
if (!named.has(path6)) throw new Error(`The help of ${path6} names no command`);
|
|
7127
7266
|
program.addHelpText("after", programHelpText());
|
|
7128
7267
|
return program;
|
|
7129
7268
|
}
|
|
@@ -7498,13 +7637,13 @@ async function machineProcessSummary(cell, key, region, name) {
|
|
|
7498
7637
|
return null;
|
|
7499
7638
|
}
|
|
7500
7639
|
}
|
|
7501
|
-
async function machineFileReadCommand(deps, name,
|
|
7640
|
+
async function machineFileReadCommand(deps, name, path6, options = {}) {
|
|
7502
7641
|
try {
|
|
7503
7642
|
const key = await deps.auth.apiKey();
|
|
7504
7643
|
const region = await regionOf(deps, key, name);
|
|
7505
7644
|
const response = await cellApi(deps).fetch("readMachineFile", key, region, {
|
|
7506
7645
|
params: { name },
|
|
7507
|
-
query: { path:
|
|
7646
|
+
query: { path: path6 },
|
|
7508
7647
|
headers: { range: options.range, "tonbo-expect-activation": options.expectActivation }
|
|
7509
7648
|
});
|
|
7510
7649
|
const bytes2 = new Uint8Array(await response.arrayBuffer());
|
|
@@ -7514,14 +7653,14 @@ async function machineFileReadCommand(deps, name, path5, options = {}) {
|
|
|
7514
7653
|
throw explainMachineError(error);
|
|
7515
7654
|
}
|
|
7516
7655
|
}
|
|
7517
|
-
async function machineFileWriteCommand(deps, name,
|
|
7656
|
+
async function machineFileWriteCommand(deps, name, path6, options = {}) {
|
|
7518
7657
|
try {
|
|
7519
7658
|
const bytes2 = options.from ? new Uint8Array(await readFile4(options.from)) : await readAllStdin(deps);
|
|
7520
7659
|
const key = await deps.auth.apiKey();
|
|
7521
7660
|
const region = await regionOf(deps, key, name);
|
|
7522
7661
|
const written = await cellApi(deps).json("writeMachineFile", key, region, {
|
|
7523
7662
|
params: { name },
|
|
7524
|
-
query: { path:
|
|
7663
|
+
query: { path: path6 },
|
|
7525
7664
|
headers: {
|
|
7526
7665
|
"content-type": "application/octet-stream",
|
|
7527
7666
|
"if-match": options.ifMatch,
|
|
@@ -7543,7 +7682,7 @@ async function machineFileWriteCommand(deps, name, path5, options = {}) {
|
|
|
7543
7682
|
throw explainMachineError(error);
|
|
7544
7683
|
}
|
|
7545
7684
|
}
|
|
7546
|
-
async function machineFileStatCommand(deps, name,
|
|
7685
|
+
async function machineFileStatCommand(deps, name, path6, options = {}) {
|
|
7547
7686
|
try {
|
|
7548
7687
|
const key = await deps.auth.apiKey();
|
|
7549
7688
|
const region = await regionOf(deps, key, name);
|
|
@@ -7553,7 +7692,7 @@ async function machineFileStatCommand(deps, name, path5, options = {}) {
|
|
|
7553
7692
|
region,
|
|
7554
7693
|
{
|
|
7555
7694
|
params: { name },
|
|
7556
|
-
query: { path:
|
|
7695
|
+
query: { path: path6 },
|
|
7557
7696
|
headers: { "tonbo-expect-activation": options.expectActivation }
|
|
7558
7697
|
}
|
|
7559
7698
|
);
|
|
@@ -7572,13 +7711,13 @@ async function machineFileStatCommand(deps, name, path5, options = {}) {
|
|
|
7572
7711
|
throw explainMachineError(error);
|
|
7573
7712
|
}
|
|
7574
7713
|
}
|
|
7575
|
-
async function machineFileListCommand(deps, name,
|
|
7714
|
+
async function machineFileListCommand(deps, name, path6, options = {}) {
|
|
7576
7715
|
try {
|
|
7577
7716
|
const key = await deps.auth.apiKey();
|
|
7578
7717
|
const region = await regionOf(deps, key, name);
|
|
7579
7718
|
const listing = await cellApi(deps).json("listMachineFiles", key, region, {
|
|
7580
7719
|
params: { name },
|
|
7581
|
-
query: { path:
|
|
7720
|
+
query: { path: path6 },
|
|
7582
7721
|
headers: { "tonbo-expect-activation": options.expectActivation }
|
|
7583
7722
|
});
|
|
7584
7723
|
noticeWake(deps, listing.machine, name);
|
|
@@ -7602,19 +7741,19 @@ async function machineFileListCommand(deps, name, path5, options = {}) {
|
|
|
7602
7741
|
throw explainMachineError(error);
|
|
7603
7742
|
}
|
|
7604
7743
|
}
|
|
7605
|
-
async function machineFileDeleteCommand(deps, name,
|
|
7744
|
+
async function machineFileDeleteCommand(deps, name, path6, options = {}) {
|
|
7606
7745
|
try {
|
|
7607
7746
|
const key = await deps.auth.apiKey();
|
|
7608
7747
|
const region = await regionOf(deps, key, name);
|
|
7609
7748
|
await cellApi(deps).fetch("deleteMachineFile", key, region, {
|
|
7610
7749
|
params: { name },
|
|
7611
|
-
query: { path:
|
|
7750
|
+
query: { path: path6, recursive: options.recursive ? "true" : void 0 },
|
|
7612
7751
|
headers: {
|
|
7613
7752
|
"tonbo-expect-activation": options.expectActivation,
|
|
7614
7753
|
"idempotency-key": randomUUID8()
|
|
7615
7754
|
}
|
|
7616
7755
|
});
|
|
7617
|
-
print(deps, options.json, { path:
|
|
7756
|
+
print(deps, options.json, { path: path6 }, `Deleted ${path6}.`);
|
|
7618
7757
|
} catch (error) {
|
|
7619
7758
|
throw explainMachineError(error);
|
|
7620
7759
|
}
|
|
@@ -7730,8 +7869,8 @@ var LEGACY_BEGIN = "# >>> tonbo project ssh >>>";
|
|
|
7730
7869
|
var LEGACY_END = "# <<< tonbo project ssh <<<";
|
|
7731
7870
|
var PROXY_COMMAND = /^\s*proxycommand(\s|=)/i;
|
|
7732
7871
|
var SSH_PROXY = /(^|[\s'"/])ssh-proxy(\s|['"]|$)/;
|
|
7733
|
-
function shown(
|
|
7734
|
-
return
|
|
7872
|
+
function shown(path6, home) {
|
|
7873
|
+
return path6.startsWith(home + sep) ? `~${path6.slice(home.length)}` : path6;
|
|
7735
7874
|
}
|
|
7736
7875
|
function words(text) {
|
|
7737
7876
|
const found = [];
|
|
@@ -7747,15 +7886,15 @@ async function expand(pattern) {
|
|
|
7747
7886
|
let paths = [sep];
|
|
7748
7887
|
for (const part of parts) {
|
|
7749
7888
|
if (!/[*?]/.test(part)) {
|
|
7750
|
-
paths = paths.map((
|
|
7889
|
+
paths = paths.map((path6) => join(path6, part));
|
|
7751
7890
|
continue;
|
|
7752
7891
|
}
|
|
7753
7892
|
const matcher = wildcard(part);
|
|
7754
7893
|
const next = [];
|
|
7755
|
-
for (const
|
|
7756
|
-
const names = await readdir(
|
|
7894
|
+
for (const path6 of paths) {
|
|
7895
|
+
const names = await readdir(path6).catch(() => []);
|
|
7757
7896
|
for (const name of names.sort())
|
|
7758
|
-
if (matcher.test(name) && !name.startsWith(".")) next.push(join(
|
|
7897
|
+
if (matcher.test(name) && !name.startsWith(".")) next.push(join(path6, name));
|
|
7759
7898
|
}
|
|
7760
7899
|
paths = next;
|
|
7761
7900
|
}
|
|
@@ -7765,18 +7904,18 @@ async function scan(home) {
|
|
|
7765
7904
|
const sshDir = join(home, ".ssh");
|
|
7766
7905
|
const files = [];
|
|
7767
7906
|
const seen = /* @__PURE__ */ new Set();
|
|
7768
|
-
const visit = async (
|
|
7769
|
-
if (depth > 16 || seen.has(
|
|
7770
|
-
seen.add(
|
|
7907
|
+
const visit = async (path6, depth) => {
|
|
7908
|
+
if (depth > 16 || seen.has(path6) || !path6.startsWith(sshDir + sep)) return;
|
|
7909
|
+
seen.add(path6);
|
|
7771
7910
|
let text;
|
|
7772
7911
|
try {
|
|
7773
|
-
if (!(await stat2(
|
|
7774
|
-
text = await readFile5(
|
|
7912
|
+
if (!(await stat2(path6)).isFile()) return;
|
|
7913
|
+
text = await readFile5(path6, "utf8");
|
|
7775
7914
|
} catch {
|
|
7776
7915
|
return;
|
|
7777
7916
|
}
|
|
7778
7917
|
const lines = text.split("\n");
|
|
7779
|
-
const file = { path:
|
|
7918
|
+
const file = { path: path6, lines, blocks: [], handEdited: [] };
|
|
7780
7919
|
const includes = [];
|
|
7781
7920
|
for (let index = 0; index < lines.length; index++) {
|
|
7782
7921
|
if (lines[index].trim() === LEGACY_BEGIN) {
|
|
@@ -7915,10 +8054,10 @@ async function ensureKey(directory) {
|
|
|
7915
8054
|
}
|
|
7916
8055
|
return key;
|
|
7917
8056
|
}
|
|
7918
|
-
async function writeAtomic(
|
|
7919
|
-
const temporary = `${
|
|
8057
|
+
async function writeAtomic(path6, data) {
|
|
8058
|
+
const temporary = `${path6}.${process.pid}.tmp`;
|
|
7920
8059
|
await writeFile3(temporary, data, { mode: 384 });
|
|
7921
|
-
await rename5(temporary,
|
|
8060
|
+
await rename5(temporary, path6);
|
|
7922
8061
|
}
|
|
7923
8062
|
function certificatePath(directory, user) {
|
|
7924
8063
|
return join2(directory, `${user}-cert.pub`);
|
|
@@ -8122,33 +8261,255 @@ async function sshCertificateIssueCommand(deps, options) {
|
|
|
8122
8261
|
print(deps, options.json, data, data.certificate);
|
|
8123
8262
|
}
|
|
8124
8263
|
|
|
8264
|
+
// src/update.ts
|
|
8265
|
+
import { execFile as execFile3, spawn as spawn2 } from "node:child_process";
|
|
8266
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
8267
|
+
import { mkdir as mkdir5, readFile as readFile7, realpath as realpath2, rename as rename6, writeFile as writeFile4 } from "node:fs/promises";
|
|
8268
|
+
import path5 from "node:path";
|
|
8269
|
+
import { fileURLToPath } from "node:url";
|
|
8270
|
+
import { promisify as promisify3 } from "node:util";
|
|
8271
|
+
var contract = cliVersionsContract;
|
|
8272
|
+
var CLI_VERSION = JSON.parse(readFileSync2(new URL("../../package.json", import.meta.url), "utf8")).version;
|
|
8273
|
+
var RELEASE = new RegExp(contract.release_pattern);
|
|
8274
|
+
function compareReleases(a, b) {
|
|
8275
|
+
const left = RELEASE.exec(a);
|
|
8276
|
+
const right = RELEASE.exec(b);
|
|
8277
|
+
if (!left || !right) return null;
|
|
8278
|
+
for (let at = 1; at <= 3; at++) {
|
|
8279
|
+
const difference = Number(left[at]) - Number(right[at]);
|
|
8280
|
+
if (difference !== 0) return Math.sign(difference);
|
|
8281
|
+
}
|
|
8282
|
+
const [leftPre, rightPre] = [left[4], right[4]];
|
|
8283
|
+
if (leftPre === rightPre) return 0;
|
|
8284
|
+
if (leftPre === void 0) return 1;
|
|
8285
|
+
if (rightPre === void 0) return -1;
|
|
8286
|
+
return leftPre < rightPre ? -1 : 1;
|
|
8287
|
+
}
|
|
8288
|
+
function latestOf(headers) {
|
|
8289
|
+
const latest = headers.get(contract.headers.latest)?.trim() ?? "";
|
|
8290
|
+
return RELEASE.test(latest) ? latest : null;
|
|
8291
|
+
}
|
|
8292
|
+
function noticeLine(current, latest) {
|
|
8293
|
+
if (latest === null || (compareReleases(current, latest) ?? 0) >= 0) return null;
|
|
8294
|
+
return contract.notice.line.replace("{latest}", latest).replace("{current}", current);
|
|
8295
|
+
}
|
|
8296
|
+
var ReleaseWatch = class {
|
|
8297
|
+
constructor(current = CLI_VERSION, refuse = (message) => {
|
|
8298
|
+
process.stderr.write(`${message}
|
|
8299
|
+
`);
|
|
8300
|
+
process.exit(1);
|
|
8301
|
+
}) {
|
|
8302
|
+
this.current = current;
|
|
8303
|
+
this.refuse = refuse;
|
|
8304
|
+
}
|
|
8305
|
+
latest = null;
|
|
8306
|
+
fetch(fetcher) {
|
|
8307
|
+
return async (input, init) => {
|
|
8308
|
+
const headers = new Headers(
|
|
8309
|
+
init?.headers ?? (input instanceof Request ? input.headers : void 0)
|
|
8310
|
+
);
|
|
8311
|
+
headers.set(contract.headers.client, this.current);
|
|
8312
|
+
const response = await fetcher(input, { ...init, headers });
|
|
8313
|
+
this.latest = latestOf(response.headers) ?? this.latest;
|
|
8314
|
+
if (response.status === 410) {
|
|
8315
|
+
const body = await response.clone().json().catch(() => null);
|
|
8316
|
+
if (body?.code === contract.refusal.code) {
|
|
8317
|
+
const reason2 = [body.detail, body.error].find(
|
|
8318
|
+
(value) => typeof value === "string"
|
|
8319
|
+
);
|
|
8320
|
+
this.refuse(
|
|
8321
|
+
reason2 ?? `This tonbo (${this.current}) is no longer supported. Run \`tonbo update\` (or \`${contract.update_command}\`).`
|
|
8322
|
+
);
|
|
8323
|
+
}
|
|
8324
|
+
}
|
|
8325
|
+
return response;
|
|
8326
|
+
};
|
|
8327
|
+
}
|
|
8328
|
+
};
|
|
8329
|
+
var set = (value) => !!value && value !== "0";
|
|
8330
|
+
function noticeAllowed({ env, argv, stdoutIsTTY, stderrIsTTY }) {
|
|
8331
|
+
if (set(env.TONBO_NO_UPDATE_NOTICE)) return false;
|
|
8332
|
+
if (set(env.CI) && env.CI?.toLowerCase() !== "false") return false;
|
|
8333
|
+
if (argv.includes("--json")) return false;
|
|
8334
|
+
return stdoutIsTTY || stderrIsTTY;
|
|
8335
|
+
}
|
|
8336
|
+
function noticeStatePath(env = process.env) {
|
|
8337
|
+
return path5.join(path5.dirname(defaultLoginPath(env)), contract.notice.state.file);
|
|
8338
|
+
}
|
|
8339
|
+
var INTERVAL_MS = contract.notice.interval_hours * 60 * 60 * 1e3;
|
|
8340
|
+
function noticeDue(state, now) {
|
|
8341
|
+
const at = state && typeof state === "object" ? Date.parse(String(state.notified_at)) : Number.NaN;
|
|
8342
|
+
return !Number.isFinite(at) || at > now || now - at >= INTERVAL_MS;
|
|
8343
|
+
}
|
|
8344
|
+
async function showUpdateNotice(watch, {
|
|
8345
|
+
context,
|
|
8346
|
+
statePath,
|
|
8347
|
+
now = Date.now(),
|
|
8348
|
+
write = (line) => process.stderr.write(`${line}
|
|
8349
|
+
`)
|
|
8350
|
+
}) {
|
|
8351
|
+
const line = noticeLine(watch.current, watch.latest);
|
|
8352
|
+
if (line === null || !noticeAllowed(context)) return;
|
|
8353
|
+
const state = await readFile7(statePath, "utf8").then((text) => JSON.parse(text)).catch(() => null);
|
|
8354
|
+
if (!noticeDue(state, now)) return;
|
|
8355
|
+
write(line);
|
|
8356
|
+
try {
|
|
8357
|
+
await mkdir5(path5.dirname(statePath), { recursive: true, mode: 448 });
|
|
8358
|
+
const temporary = `${statePath}.${process.pid}.tmp`;
|
|
8359
|
+
await writeFile4(
|
|
8360
|
+
temporary,
|
|
8361
|
+
`${JSON.stringify({ version: 1, notified_at: new Date(now).toISOString() })}
|
|
8362
|
+
`,
|
|
8363
|
+
{ mode: 384 }
|
|
8364
|
+
);
|
|
8365
|
+
await rename6(temporary, statePath);
|
|
8366
|
+
} catch {
|
|
8367
|
+
}
|
|
8368
|
+
}
|
|
8369
|
+
function installMethod(location) {
|
|
8370
|
+
const at = location.replaceAll("\\", "/").toLowerCase();
|
|
8371
|
+
if (at.includes("/_npx/") || at.includes("/bunx-") || at.includes("/pnpm/dlx/")) return "npx";
|
|
8372
|
+
if (at.includes("/pnpm/global/") || at.includes("/global/") && at.includes("/.pnpm/"))
|
|
8373
|
+
return "pnpm";
|
|
8374
|
+
if (at.includes("/install/global/node_modules/")) return "bun";
|
|
8375
|
+
if (at.includes("/yarn/global/") || at.includes("/yarn/data/global/")) return "yarn";
|
|
8376
|
+
if (/\/(lib|npm|nodejs)\/node_modules\/@tonbo\/cli\//.test(at)) return "npm";
|
|
8377
|
+
return "standalone";
|
|
8378
|
+
}
|
|
8379
|
+
var PACKAGE = `${contract.package}@latest`;
|
|
8380
|
+
var UPDATE_COMMANDS = {
|
|
8381
|
+
npm: ["npm", "install", "--global", PACKAGE],
|
|
8382
|
+
pnpm: ["pnpm", "add", "--global", PACKAGE],
|
|
8383
|
+
bun: ["bun", "add", "--global", PACKAGE],
|
|
8384
|
+
yarn: ["yarn", "global", "add", PACKAGE]
|
|
8385
|
+
};
|
|
8386
|
+
function updateCommandLine(method) {
|
|
8387
|
+
if (method === "npx") return `npx ${PACKAGE}`;
|
|
8388
|
+
if (method === "standalone") return contract.update_command;
|
|
8389
|
+
return UPDATE_COMMANDS[method].join(" ");
|
|
8390
|
+
}
|
|
8391
|
+
async function readGlobalRoot(manager) {
|
|
8392
|
+
try {
|
|
8393
|
+
const { stdout } = await promisify3(execFile3)(manager, ["root", "-g"], {
|
|
8394
|
+
shell: process.platform === "win32",
|
|
8395
|
+
timeout: 15e3
|
|
8396
|
+
});
|
|
8397
|
+
return await realpath2(stdout.trim());
|
|
8398
|
+
} catch {
|
|
8399
|
+
return null;
|
|
8400
|
+
}
|
|
8401
|
+
}
|
|
8402
|
+
function isInside(location, directory) {
|
|
8403
|
+
const normal = (value) => value.replaceAll("\\", "/").replace(/\/+$/, "").toLowerCase();
|
|
8404
|
+
return normal(location).startsWith(`${normal(directory)}/`);
|
|
8405
|
+
}
|
|
8406
|
+
async function confirmedInstallMethod(location, globalRoot) {
|
|
8407
|
+
const method = installMethod(location);
|
|
8408
|
+
if (method !== "npm" && method !== "pnpm") return method;
|
|
8409
|
+
const root = await globalRoot(method);
|
|
8410
|
+
if (root === null) return "standalone";
|
|
8411
|
+
return isInside(location, method === "pnpm" ? path5.dirname(root) : root) ? method : "standalone";
|
|
8412
|
+
}
|
|
8413
|
+
async function readLatest(origin, fetcher = fetch) {
|
|
8414
|
+
try {
|
|
8415
|
+
const response = await fetcher(`${origin}/v1/openapi.json`, {
|
|
8416
|
+
method: "HEAD",
|
|
8417
|
+
signal: AbortSignal.timeout(1e4)
|
|
8418
|
+
});
|
|
8419
|
+
return latestOf(response.headers);
|
|
8420
|
+
} catch {
|
|
8421
|
+
return null;
|
|
8422
|
+
}
|
|
8423
|
+
}
|
|
8424
|
+
function runCommand(command) {
|
|
8425
|
+
return new Promise((resolve2, reject) => {
|
|
8426
|
+
const child = spawn2(command[0], command.slice(1), {
|
|
8427
|
+
stdio: "inherit",
|
|
8428
|
+
// npm, pnpm and yarn are .cmd scripts on Windows, which only a shell runs.
|
|
8429
|
+
shell: process.platform === "win32"
|
|
8430
|
+
});
|
|
8431
|
+
child.on("error", reject);
|
|
8432
|
+
child.on("exit", (code, signal) => resolve2(code ?? (signal ? 1 : 0)));
|
|
8433
|
+
});
|
|
8434
|
+
}
|
|
8435
|
+
var SCRIPT_LOCATION = fileURLToPath(import.meta.url);
|
|
8436
|
+
async function updateCommand(deps, options) {
|
|
8437
|
+
if (options.json && !options.check) throw new Error("--json goes with --check.");
|
|
8438
|
+
const latest = await deps.latest();
|
|
8439
|
+
const method = await confirmedInstallMethod(deps.location, deps.globalRoot);
|
|
8440
|
+
const command = updateCommandLine(method);
|
|
8441
|
+
const order = latest === null ? null : compareReleases(deps.current, latest);
|
|
8442
|
+
const available = order !== null && order < 0;
|
|
8443
|
+
if (options.check) {
|
|
8444
|
+
if (options.json) {
|
|
8445
|
+
deps.output(
|
|
8446
|
+
JSON.stringify({
|
|
8447
|
+
current: deps.current,
|
|
8448
|
+
latest,
|
|
8449
|
+
update_available: available,
|
|
8450
|
+
install: method,
|
|
8451
|
+
command
|
|
8452
|
+
})
|
|
8453
|
+
);
|
|
8454
|
+
} else if (latest === null) {
|
|
8455
|
+
deps.output(`tonbo ${deps.current}; the latest release could not be read from Tonbo.`);
|
|
8456
|
+
} else if (available) {
|
|
8457
|
+
deps.output(`tonbo ${deps.current}; ${latest} is available: run tonbo update`);
|
|
8458
|
+
} else {
|
|
8459
|
+
deps.output(`tonbo ${deps.current} is the latest release.`);
|
|
8460
|
+
}
|
|
8461
|
+
return;
|
|
8462
|
+
}
|
|
8463
|
+
if (latest !== null && !available) {
|
|
8464
|
+
deps.output(`tonbo ${deps.current} is the latest release.`);
|
|
8465
|
+
return;
|
|
8466
|
+
}
|
|
8467
|
+
if (method === "npx") {
|
|
8468
|
+
deps.notice(
|
|
8469
|
+
`This tonbo runs through a package runner, which runs the release you name each time: run \`${command}\`.`
|
|
8470
|
+
);
|
|
8471
|
+
return;
|
|
8472
|
+
}
|
|
8473
|
+
if (method === "standalone") {
|
|
8474
|
+
deps.notice(
|
|
8475
|
+
`Could not tell how the tonbo at ${deps.location} was installed, so nothing was run. Update it with \`${command}\`, or the way you installed it.`
|
|
8476
|
+
);
|
|
8477
|
+
process.exitCode = 1;
|
|
8478
|
+
return;
|
|
8479
|
+
}
|
|
8480
|
+
deps.notice(`Running: ${command}`);
|
|
8481
|
+
const status = await deps.run(UPDATE_COMMANDS[method]);
|
|
8482
|
+
if (status !== 0)
|
|
8483
|
+
throw new Error(`${command} exited with status ${status}; tonbo was not updated.`);
|
|
8484
|
+
deps.output(`Updated tonbo${latest ? ` to ${latest}` : ""}: run tonbo --version to check.`);
|
|
8485
|
+
}
|
|
8486
|
+
|
|
8125
8487
|
// src/app.ts
|
|
8126
|
-
var
|
|
8127
|
-
readFileSync2(new URL("../../package.json", import.meta.url), "utf8")
|
|
8128
|
-
);
|
|
8488
|
+
var releaseWatch = new ReleaseWatch();
|
|
8129
8489
|
function createDependencies(selection = {}) {
|
|
8130
8490
|
const api = apiOrigin();
|
|
8491
|
+
const fetcher = releaseWatch.fetch(fetch);
|
|
8131
8492
|
const account = selection.account?.trim() || process.env.TONBO_ACCOUNT?.trim() || void 0;
|
|
8132
8493
|
const credentials = new LoginStore({
|
|
8133
|
-
fetcher
|
|
8494
|
+
fetcher,
|
|
8134
8495
|
apiOrigin: api,
|
|
8135
8496
|
loginOrigin: loginOrigin(api),
|
|
8136
8497
|
account
|
|
8137
8498
|
});
|
|
8138
8499
|
const notice = (text) => console.error(text);
|
|
8139
8500
|
return {
|
|
8140
|
-
api: new ManagementApi(noticingBudget(
|
|
8501
|
+
api: new ManagementApi(noticingBudget(fetcher, notice), api),
|
|
8141
8502
|
ask: terminalAsk,
|
|
8142
8503
|
auth: new AuthClient(
|
|
8143
8504
|
credentials,
|
|
8144
|
-
|
|
8505
|
+
fetcher,
|
|
8145
8506
|
loginOrigin(api),
|
|
8146
8507
|
void 0,
|
|
8147
8508
|
void 0,
|
|
8148
8509
|
void 0,
|
|
8149
8510
|
void 0,
|
|
8150
8511
|
process.env,
|
|
8151
|
-
{ client: "tonbo", clientVersion:
|
|
8512
|
+
{ client: "tonbo", clientVersion: CLI_VERSION }
|
|
8152
8513
|
),
|
|
8153
8514
|
credentials,
|
|
8154
8515
|
interactive: () => process.stdin.isTTY === true && process.stderr.isTTY === true,
|
|
@@ -8210,8 +8571,8 @@ function machineSpecOptions(command, update) {
|
|
|
8210
8571
|
"the seconds between SIGTERM and SIGKILL on a planned stop",
|
|
8211
8572
|
parseWhole
|
|
8212
8573
|
).option(
|
|
8213
|
-
"--volume <name:path>",
|
|
8214
|
-
`
|
|
8574
|
+
"--volume <name[:path]>",
|
|
8575
|
+
`keep the Machine's whole disk on the Volume <name> (see tonbo volume list), or with :<path> mount it at <path> instead; a Machine has at most one, of its own region${update ? "; applies at the next boot" : ""}`
|
|
8215
8576
|
).option(
|
|
8216
8577
|
"--delete-after <seconds>",
|
|
8217
8578
|
`delete the Machine once it has been asleep, never woken or failed this long (3600 to 7776000)${update ? "; applies at once, from its last activation's end" : ""}`,
|
|
@@ -8233,7 +8594,7 @@ function parseUsageSort(value) {
|
|
|
8233
8594
|
}
|
|
8234
8595
|
}
|
|
8235
8596
|
function createProgram(makeDependencies = createDependencies) {
|
|
8236
|
-
const program = new Command().name("tonbo").description("Sign in to Tonbo and manage account resources.").version(
|
|
8597
|
+
const program = new Command().name("tonbo").description("Sign in to Tonbo and manage account resources.").version(CLI_VERSION).option(
|
|
8237
8598
|
"--account <account>",
|
|
8238
8599
|
"the account to act in, by name or id: any account you belong to; else TONBO_ACCOUNT, else the default `tonbo account use` set, else your personal account"
|
|
8239
8600
|
);
|
|
@@ -8247,6 +8608,20 @@ function createProgram(makeDependencies = createDependencies) {
|
|
|
8247
8608
|
const account2 = program.opts().account;
|
|
8248
8609
|
await loginCommand(dependencies(), { ...options, account: account2 });
|
|
8249
8610
|
});
|
|
8611
|
+
program.command("update").description("update this CLI with the package manager that installed it").option("--check", "only print this release and the latest one").option("--json", "with --check, print the two releases and the update command as JSON").action(
|
|
8612
|
+
async (options) => updateCommand(
|
|
8613
|
+
{
|
|
8614
|
+
current: CLI_VERSION,
|
|
8615
|
+
latest: () => readLatest(apiOrigin()),
|
|
8616
|
+
location: SCRIPT_LOCATION,
|
|
8617
|
+
globalRoot: readGlobalRoot,
|
|
8618
|
+
run: runCommand,
|
|
8619
|
+
output: (text) => console.log(text),
|
|
8620
|
+
notice: (text) => console.error(text)
|
|
8621
|
+
},
|
|
8622
|
+
options
|
|
8623
|
+
)
|
|
8624
|
+
);
|
|
8250
8625
|
program.command("logout").description(
|
|
8251
8626
|
"end this computer's CLI login and remove the saved login; Yield's capture here stops until the next login"
|
|
8252
8627
|
).action(async () => logoutCommand(dependencies()));
|
|
@@ -8646,31 +9021,31 @@ function createProgram(makeDependencies = createDependencies) {
|
|
|
8646
9021
|
file.command("read <name> <path>").description("print a file of a Machine, waking it first").option("--output <file>", "write the bytes to this local file instead of stdout").option("--range <range>", "one byte range, such as bytes=0-1023").option("--expect-activation <activation>", "read only in this activation"),
|
|
8647
9022
|
"readMachineFile"
|
|
8648
9023
|
).action(
|
|
8649
|
-
async (name,
|
|
9024
|
+
async (name, path6, options) => machineFileReadCommand(dependencies(), name, path6, options)
|
|
8650
9025
|
);
|
|
8651
9026
|
operationOptions(
|
|
8652
9027
|
file.command("write <name> <path>").description("replace a file of a Machine with stdin or --from, atomically").option("--from <file>", "the local file to send instead of stdin").option("--mode <mode>", "the file's mode, four octal digits such as 0755").option("--if-match <etag>", "replace only the version with this ETag").option("--create", "only create the file; refuse when it exists").option("--expect-activation <activation>", "write only in this activation"),
|
|
8653
9028
|
"writeMachineFile"
|
|
8654
9029
|
).action(
|
|
8655
|
-
async (name,
|
|
9030
|
+
async (name, path6, options) => machineFileWriteCommand(dependencies(), name, path6, options)
|
|
8656
9031
|
);
|
|
8657
9032
|
operationOptions(
|
|
8658
9033
|
file.command("stat <name> <path>").description("show a path's type, size, mode, owner and ETag").option("--expect-activation <activation>", "stat only in this activation"),
|
|
8659
9034
|
"statMachineFile"
|
|
8660
9035
|
).action(
|
|
8661
|
-
async (name,
|
|
9036
|
+
async (name, path6, options) => machineFileStatCommand(dependencies(), name, path6, options)
|
|
8662
9037
|
);
|
|
8663
9038
|
operationOptions(
|
|
8664
9039
|
file.command("list <name> <path>").description("list a directory of a Machine").option("--expect-activation <activation>", "list only in this activation"),
|
|
8665
9040
|
"listMachineFiles"
|
|
8666
9041
|
).action(
|
|
8667
|
-
async (name,
|
|
9042
|
+
async (name, path6, options) => machineFileListCommand(dependencies(), name, path6, options)
|
|
8668
9043
|
);
|
|
8669
9044
|
operationOptions(
|
|
8670
9045
|
file.command("delete <name> <path>").description("delete a file, a symbolic link or an empty directory of a Machine").option("--recursive", "delete a directory and everything in it").option("--expect-activation <activation>", "delete only in this activation"),
|
|
8671
9046
|
"deleteMachineFile"
|
|
8672
9047
|
).action(
|
|
8673
|
-
async (name,
|
|
9048
|
+
async (name, path6, options) => machineFileDeleteCommand(dependencies(), name, path6, options)
|
|
8674
9049
|
);
|
|
8675
9050
|
const hold = machine.command("hold").description("keep a Machine awake for work that has no open connection");
|
|
8676
9051
|
operationOptions(
|
|
@@ -8807,4 +9182,14 @@ try {
|
|
|
8807
9182
|
} catch (error) {
|
|
8808
9183
|
console.error(error instanceof Error ? error.message : error);
|
|
8809
9184
|
if (!process.exitCode) process.exitCode = 1;
|
|
9185
|
+
} finally {
|
|
9186
|
+
await showUpdateNotice(releaseWatch, {
|
|
9187
|
+
context: {
|
|
9188
|
+
env: process.env,
|
|
9189
|
+
argv: process.argv,
|
|
9190
|
+
stdoutIsTTY: process.stdout.isTTY === true,
|
|
9191
|
+
stderrIsTTY: process.stderr.isTTY === true
|
|
9192
|
+
},
|
|
9193
|
+
statePath: noticeStatePath()
|
|
9194
|
+
}).catch(() => void 0);
|
|
8810
9195
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tonbo/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.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": {
|