@tonbo/cli 0.17.1 → 0.17.3
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 +460 -57
- 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 = {
|
|
@@ -1773,6 +1772,102 @@ var loginContract = {
|
|
|
1773
1772
|
]
|
|
1774
1773
|
}
|
|
1775
1774
|
};
|
|
1775
|
+
var cliVersionsContract = {
|
|
1776
|
+
"version": 1,
|
|
1777
|
+
"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.",
|
|
1778
|
+
"package": "@tonbo/cli",
|
|
1779
|
+
"latest": {
|
|
1780
|
+
"source": "cli/tonbo/package.json",
|
|
1781
|
+
"field": "version",
|
|
1782
|
+
"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."
|
|
1783
|
+
},
|
|
1784
|
+
"minimum": "0.12.0",
|
|
1785
|
+
"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.",
|
|
1786
|
+
"headers": {
|
|
1787
|
+
"client": "Tonbo-CLI-Version",
|
|
1788
|
+
"latest": "Tonbo-CLI-Latest",
|
|
1789
|
+
"minimum": "Tonbo-CLI-Minimum"
|
|
1790
|
+
},
|
|
1791
|
+
"release_pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",
|
|
1792
|
+
"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.",
|
|
1793
|
+
"order_examples": [
|
|
1794
|
+
[
|
|
1795
|
+
"0.17.2",
|
|
1796
|
+
"0.17.3",
|
|
1797
|
+
-1
|
|
1798
|
+
],
|
|
1799
|
+
[
|
|
1800
|
+
"0.17.3",
|
|
1801
|
+
"0.17.3",
|
|
1802
|
+
0
|
|
1803
|
+
],
|
|
1804
|
+
[
|
|
1805
|
+
"0.18.0",
|
|
1806
|
+
"0.17.9",
|
|
1807
|
+
1
|
|
1808
|
+
],
|
|
1809
|
+
[
|
|
1810
|
+
"0.9.0",
|
|
1811
|
+
"0.12.0",
|
|
1812
|
+
-1
|
|
1813
|
+
],
|
|
1814
|
+
[
|
|
1815
|
+
"1.0.0",
|
|
1816
|
+
"0.99.99",
|
|
1817
|
+
1
|
|
1818
|
+
],
|
|
1819
|
+
[
|
|
1820
|
+
"0.18.0-beta.1",
|
|
1821
|
+
"0.18.0",
|
|
1822
|
+
-1
|
|
1823
|
+
],
|
|
1824
|
+
[
|
|
1825
|
+
"0.18.0-beta.2",
|
|
1826
|
+
"0.18.0-beta.1",
|
|
1827
|
+
1
|
|
1828
|
+
],
|
|
1829
|
+
[
|
|
1830
|
+
"0.17",
|
|
1831
|
+
"0.17.0",
|
|
1832
|
+
null
|
|
1833
|
+
],
|
|
1834
|
+
[
|
|
1835
|
+
"v0.17.2",
|
|
1836
|
+
"0.17.2",
|
|
1837
|
+
null
|
|
1838
|
+
],
|
|
1839
|
+
[
|
|
1840
|
+
"0.17.2 ",
|
|
1841
|
+
"0.17.2",
|
|
1842
|
+
null
|
|
1843
|
+
],
|
|
1844
|
+
[
|
|
1845
|
+
"",
|
|
1846
|
+
"0.12.0",
|
|
1847
|
+
null
|
|
1848
|
+
]
|
|
1849
|
+
],
|
|
1850
|
+
"refusal": {
|
|
1851
|
+
"code": "cli_upgrade_required",
|
|
1852
|
+
"registry": "contracts/management/problems-v1.json"
|
|
1853
|
+
},
|
|
1854
|
+
"update_command": "npm install --global @tonbo/cli@latest",
|
|
1855
|
+
"notice": {
|
|
1856
|
+
"line": "tonbo {latest} is available (you have {current}): run tonbo update",
|
|
1857
|
+
"stream": "stderr",
|
|
1858
|
+
"interval_hours": 24,
|
|
1859
|
+
"state": {
|
|
1860
|
+
"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.",
|
|
1861
|
+
"file": "update-notice.json"
|
|
1862
|
+
},
|
|
1863
|
+
"silent_when": [
|
|
1864
|
+
"the command line has --json",
|
|
1865
|
+
"neither stdout nor stderr is a terminal",
|
|
1866
|
+
"CI is set to anything but empty, 0 or false",
|
|
1867
|
+
"TONBO_NO_UPDATE_NOTICE is set to anything but empty or 0"
|
|
1868
|
+
]
|
|
1869
|
+
}
|
|
1870
|
+
};
|
|
1776
1871
|
var cliSessionContract = {
|
|
1777
1872
|
"prefix": "tbs_",
|
|
1778
1873
|
"grant": "cli_session",
|
|
@@ -2036,7 +2131,7 @@ var ManagementApi = class {
|
|
|
2036
2131
|
}
|
|
2037
2132
|
async requestWith(operationId, key, { params = {}, query = {}, body, idempotencyKey, ifMatch }, seen) {
|
|
2038
2133
|
const operation = managementOperation(operationId);
|
|
2039
|
-
const
|
|
2134
|
+
const path6 = operation.path.replace(/\{([^}]+)\}/g, (_, name) => {
|
|
2040
2135
|
const value = params[name];
|
|
2041
2136
|
if (value === void 0)
|
|
2042
2137
|
throw new Error(`${operationId} needs the path parameter ${name}.`);
|
|
@@ -2060,7 +2155,7 @@ var ManagementApi = class {
|
|
|
2060
2155
|
if (ifMatch)
|
|
2061
2156
|
headers["if-match"] = ifMatch;
|
|
2062
2157
|
const queryString = search.toString();
|
|
2063
|
-
const url = `${this.apiOrigin}${managementApiBasePath}${
|
|
2158
|
+
const url = `${this.apiOrigin}${managementApiBasePath}${path6}${queryString ? `?${queryString}` : ""}`;
|
|
2064
2159
|
try {
|
|
2065
2160
|
const fetcher = seen ? async (input, init) => {
|
|
2066
2161
|
const response = await this.fetcher(input, init);
|
|
@@ -3867,7 +3962,7 @@ var CellApi = class {
|
|
|
3867
3962
|
}
|
|
3868
3963
|
url(operationId, region, request) {
|
|
3869
3964
|
const operation = managementOperations[operationId];
|
|
3870
|
-
const
|
|
3965
|
+
const path6 = operation.path.replace(/\{([^}]+)\}/g, (_, name) => {
|
|
3871
3966
|
const value = request.params[name];
|
|
3872
3967
|
if (value === void 0)
|
|
3873
3968
|
throw new Error(`${operationId} needs the path parameter ${name}.`);
|
|
@@ -3878,7 +3973,7 @@ var CellApi = class {
|
|
|
3878
3973
|
if (value !== void 0)
|
|
3879
3974
|
search.set(name, value);
|
|
3880
3975
|
const query = search.toString();
|
|
3881
|
-
return `${cellOrigin(region)}${managementApiBasePath}${
|
|
3976
|
+
return `${cellOrigin(region)}${managementApiBasePath}${path6}${query ? `?${query}` : ""}`;
|
|
3882
3977
|
}
|
|
3883
3978
|
async init(operationId, key, request) {
|
|
3884
3979
|
const headers = {
|
|
@@ -5571,17 +5666,23 @@ var MACHINE_WAKE_BLOCKED_HINTS = {
|
|
|
5571
5666
|
standing: "The account's Streams standing is not solvent: settle the account at <billing>, and the next request wakes it.",
|
|
5572
5667
|
meter_lag: "Tonbo could not confirm the account's usage in time; the next request tries again.",
|
|
5573
5668
|
crash_backoff: "Its recent wakes failed; see `tonbo machine activations <name>`.",
|
|
5574
|
-
capacity: "No host had room for its shape
|
|
5669
|
+
capacity: "No host had room for its shape: Tonbo is adding a host, <minutes>, and the next request after that wakes it.",
|
|
5575
5670
|
region_closed: "Machines are closed in its region for now; the next request after they reopen wakes it.",
|
|
5576
5671
|
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
5672
|
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
5673
|
};
|
|
5579
5674
|
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.";
|
|
5675
|
+
function minutesUntil(retryAt, now = Date.now()) {
|
|
5676
|
+
const at = retryAt ? Date.parse(retryAt) : Number.NaN;
|
|
5677
|
+
if (!Number.isFinite(at) || at <= now) return "which takes a few minutes";
|
|
5678
|
+
const minutes = Math.ceil((at - now) / 6e4);
|
|
5679
|
+
return `about ${minutes} minute${minutes === 1 ? "" : "s"}`;
|
|
5680
|
+
}
|
|
5580
5681
|
function conditionText(name, condition, billing) {
|
|
5581
5682
|
switch (condition.type) {
|
|
5582
5683
|
case "wake_blocked": {
|
|
5583
5684
|
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);
|
|
5685
|
+
const hint = MACHINE_WAKE_BLOCKED_HINTS[condition.reason]?.replaceAll("<name>", name).replaceAll("<billing>", billing).replaceAll("<minutes>", minutesUntil(condition.retry_at));
|
|
5585
5686
|
return `wake blocked (${condition.reason.replaceAll("_", " ")}) since ${condition.since}${retry}.${hint ? ` ${hint}` : ""}`;
|
|
5586
5687
|
}
|
|
5587
5688
|
case "creation_delayed":
|
|
@@ -5688,7 +5789,10 @@ function awakeFields(machine) {
|
|
|
5688
5789
|
const held = awake.held_by.length > 0;
|
|
5689
5790
|
const sleeps = awake.sleeps_at_earliest ? `${awake.sleeps_at_earliest} (${awake.sleeps_by === "idle_early" ? `idle_early: quiet for ${duration(earlyReclaimSeconds)}` : `sleep_after: idle for ${duration(machine.sleep_after_seconds)}`})` : held ? "not while anything holds it" : "not known yet";
|
|
5690
5791
|
return [
|
|
5691
|
-
[
|
|
5792
|
+
[
|
|
5793
|
+
"Held by",
|
|
5794
|
+
held ? awake.held_by.map(holderText).join(", ") : `nothing${awake.sleeps_at_earliest ? `, so it sleeps at ${awake.sleeps_at_earliest}` : ""}; a detached process (tonbo machine exec ${machine.name} --detach -- <command>) or a hold (tonbo machine hold set ${machine.name} <hold> --for 15m) keeps it awake`
|
|
5795
|
+
],
|
|
5692
5796
|
...awake.idle_since ? [["Idle since", awake.idle_since]] : [],
|
|
5693
5797
|
["Quiet", awake.quiet.passes ? "yes" : `no: ${awake.quiet.reason ?? "measuring"}`],
|
|
5694
5798
|
["Sleeps at", sleeps],
|
|
@@ -6393,7 +6497,15 @@ var ENVIRONMENT = [
|
|
|
6393
6497
|
},
|
|
6394
6498
|
{
|
|
6395
6499
|
names: ["XDG_CONFIG_HOME"],
|
|
6396
|
-
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`."
|
|
6500
|
+
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."
|
|
6501
|
+
},
|
|
6502
|
+
{
|
|
6503
|
+
names: ["TONBO_NO_UPDATE_NOTICE"],
|
|
6504
|
+
description: "Set to `1` (anything but empty or `0`), the CLI never prints that a newer release is available."
|
|
6505
|
+
},
|
|
6506
|
+
{
|
|
6507
|
+
names: ["CI"],
|
|
6508
|
+
description: "Set to anything but empty, `0` or `false`, as CI services set it, the CLI never prints that a newer release is available."
|
|
6397
6509
|
},
|
|
6398
6510
|
{
|
|
6399
6511
|
names: ["XDG_STATE_HOME"],
|
|
@@ -6448,6 +6560,14 @@ var TOPICS = [
|
|
|
6448
6560
|
"`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."
|
|
6449
6561
|
]
|
|
6450
6562
|
},
|
|
6563
|
+
{
|
|
6564
|
+
id: "updates",
|
|
6565
|
+
title: "Updates",
|
|
6566
|
+
text: [
|
|
6567
|
+
"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.",
|
|
6568
|
+
"`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."
|
|
6569
|
+
]
|
|
6570
|
+
},
|
|
6451
6571
|
{
|
|
6452
6572
|
id: "confirmations",
|
|
6453
6573
|
title: "Destructive commands",
|
|
@@ -6486,6 +6606,13 @@ var COMMAND_HELP = {
|
|
|
6486
6606
|
],
|
|
6487
6607
|
examples: ["tonbo logout"]
|
|
6488
6608
|
},
|
|
6609
|
+
"tonbo update": {
|
|
6610
|
+
about: [
|
|
6611
|
+
"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.",
|
|
6612
|
+
"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."
|
|
6613
|
+
],
|
|
6614
|
+
examples: ["tonbo update", "tonbo update --check", "tonbo update --check --json"]
|
|
6615
|
+
},
|
|
6489
6616
|
"tonbo account show": {
|
|
6490
6617
|
about: [
|
|
6491
6618
|
"Prints the account commands act in: its id, name and kind, your member subject and role in it, and its limits."
|
|
@@ -6825,7 +6952,7 @@ var COMMAND_HELP = {
|
|
|
6825
6952
|
},
|
|
6826
6953
|
"tonbo machine exec": {
|
|
6827
6954
|
about: [
|
|
6828
|
-
"Runs one command in the Machine's running activation through its region's cell origin, waking the Machine first when it is asleep, and streams its stdout and stderr here as they come; a call that woke the Machine prints its hourly ceiling on stderr. The command runs without a shell, as the image's User, and lives exactly as long as the call:
|
|
6955
|
+
"Runs one command in the Machine's running activation through its region's cell origin, waking the Machine first when it is asleep, and streams its stdout and stderr here as they come; a call that woke the Machine prints its hourly ceiling on stderr. The command runs without a shell, as the image's User, and lives exactly as long as the call: what it starts in the background, with `nohup`, `setsid` or `&`, is ended when it returns, and the CLI then says how many it ended. There is no terminal and no streamed stdin, and `--stdin` sends at most 1 MiB read before the call.",
|
|
6829
6956
|
"The CLI exits with the command's status: 128 plus the signal's number when a signal ended it, 124 at `--timeout`, 125 when it was refused, killed or passed 64 MiB of output, 126 or 127 when it could not run, and 255 when the connection ended first and the outcome is unknown. `--json` prints the answer's document instead, with the first and last 512 KiB of each stream.",
|
|
6830
6957
|
"`--detach` starts the command in the background instead, prints its process id and returns at once: it runs whatever happens to this call, keeps the Machine awake until it ends, and is ended at `--timeout` (default 3600, at most 21600). Read it with `tonbo machine output`, wait for it with `tonbo machine wait`, list them with `tonbo machine ps` and end one with `tonbo machine kill`. A budget or funding stop, or a sleep you ask for, still ends it."
|
|
6831
6958
|
],
|
|
@@ -7099,19 +7226,19 @@ function optionDescription(option) {
|
|
|
7099
7226
|
function applyHelp(program) {
|
|
7100
7227
|
const named = /* @__PURE__ */ new Set();
|
|
7101
7228
|
const walk = (command, words2) => {
|
|
7102
|
-
const
|
|
7229
|
+
const path6 = words2.join(" ");
|
|
7103
7230
|
command.configureHelp({ ...command.configureHelp(), optionDescription });
|
|
7104
|
-
const help = COMMAND_HELP[
|
|
7231
|
+
const help = COMMAND_HELP[path6];
|
|
7105
7232
|
if (help) {
|
|
7106
|
-
named.add(
|
|
7233
|
+
named.add(path6);
|
|
7107
7234
|
for (const [name, text] of Object.entries(help.arguments ?? {})) {
|
|
7108
7235
|
const argument = command.registeredArguments.find((entry) => entry.name() === name);
|
|
7109
|
-
if (!argument) throw new Error(`${
|
|
7236
|
+
if (!argument) throw new Error(`${path6} has no argument ${name}`);
|
|
7110
7237
|
argument.description = text;
|
|
7111
7238
|
}
|
|
7112
7239
|
for (const [flag, text] of Object.entries(help.defaults ?? {})) {
|
|
7113
7240
|
const option = command.options.find((entry) => entry.long === flag);
|
|
7114
|
-
if (!option) throw new Error(`${
|
|
7241
|
+
if (!option) throw new Error(`${path6} has no option ${flag}`);
|
|
7115
7242
|
option.defaultValueDescription = text;
|
|
7116
7243
|
}
|
|
7117
7244
|
command.addHelpText("after", commandHelpText(help));
|
|
@@ -7119,8 +7246,8 @@ function applyHelp(program) {
|
|
|
7119
7246
|
for (const child of command.commands) walk(child, [...words2, child.name()]);
|
|
7120
7247
|
};
|
|
7121
7248
|
walk(program, [program.name()]);
|
|
7122
|
-
for (const
|
|
7123
|
-
if (!named.has(
|
|
7249
|
+
for (const path6 of Object.keys(COMMAND_HELP))
|
|
7250
|
+
if (!named.has(path6)) throw new Error(`The help of ${path6} names no command`);
|
|
7124
7251
|
program.addHelpText("after", programHelpText());
|
|
7125
7252
|
return program;
|
|
7126
7253
|
}
|
|
@@ -7215,9 +7342,30 @@ function parseEnv2(pairs) {
|
|
|
7215
7342
|
}
|
|
7216
7343
|
return env;
|
|
7217
7344
|
}
|
|
7345
|
+
var BACKGROUNDERS = /* @__PURE__ */ new Set(["nohup", "setsid"]);
|
|
7346
|
+
var SHELLS = /* @__PURE__ */ new Set(["sh", "bash", "zsh", "dash", "ash", "ksh", "mksh"]);
|
|
7347
|
+
function backgroundsWork(command) {
|
|
7348
|
+
const base = (arg) => arg.slice(arg.lastIndexOf("/") + 1);
|
|
7349
|
+
if (command.some((arg) => BACKGROUNDERS.has(base(arg)))) return true;
|
|
7350
|
+
const shell = command.findIndex((arg) => SHELLS.has(base(arg)));
|
|
7351
|
+
if (shell < 0) return false;
|
|
7352
|
+
const flag = command.findIndex((arg, at) => at > shell && /^-[a-zA-Z]*c[a-zA-Z]*$/.test(arg));
|
|
7353
|
+
const script = flag < 0 ? void 0 : command[flag + 1]?.trimEnd();
|
|
7354
|
+
if (script === void 0) return false;
|
|
7355
|
+
return script.endsWith("&") && !script.endsWith("&&") || /\b(nohup|disown|setsid)\b/.test(script);
|
|
7356
|
+
}
|
|
7357
|
+
var detachWay = (name) => `To keep work running after the call, use: tonbo machine exec ${name} --detach -- <command>`;
|
|
7358
|
+
function backgroundEndedNotice(name, count) {
|
|
7359
|
+
const processes = count === 1 ? "1 background process" : `${count} background processes`;
|
|
7360
|
+
return `${processes} this command started ${count === 1 ? "was" : "were"} ended when it returned. ${detachWay(name)}`;
|
|
7361
|
+
}
|
|
7362
|
+
function backgroundsWorkNotice(name, detach) {
|
|
7363
|
+
return detach ? `--detach already keeps the command running after the call, and what it puts in the background with nohup or & is ended when the command itself returns. Run the work in the foreground: tonbo machine exec ${name} --detach -- <command>` : `This command puts work in the background, which is ended when the command returns. ${detachWay(name)}`;
|
|
7364
|
+
}
|
|
7218
7365
|
async function machineExecCommand(deps, name, command, options = {}) {
|
|
7219
7366
|
if (command.length === 0)
|
|
7220
7367
|
throw new Error("Name the command after --, such as `tonbo machine exec web -- ls /`.");
|
|
7368
|
+
if (backgroundsWork(command)) deps.notice(backgroundsWorkNotice(name, false));
|
|
7221
7369
|
const key = await deps.auth.apiKey();
|
|
7222
7370
|
const region = await regionOf(deps, key, name);
|
|
7223
7371
|
const body = { command };
|
|
@@ -7244,6 +7392,7 @@ async function machineExecCommand(deps, name, command, options = {}) {
|
|
|
7244
7392
|
if (options.json) {
|
|
7245
7393
|
const answer = await response.json();
|
|
7246
7394
|
print(deps, true, answer.data, "");
|
|
7395
|
+
noticeEnded(deps, name, answer.data);
|
|
7247
7396
|
exitWith(deps, answer.data.reason ? execExitCode(answer.data) : 0);
|
|
7248
7397
|
return;
|
|
7249
7398
|
}
|
|
@@ -7276,10 +7425,17 @@ async function machineExecCommand(deps, name, command, options = {}) {
|
|
|
7276
7425
|
}
|
|
7277
7426
|
line(buffered);
|
|
7278
7427
|
const ended = end ?? { reason: "lost", exit_code: null, signal: null };
|
|
7279
|
-
if (ended.reason
|
|
7428
|
+
if (ended.reason === "timeout") deps.notice(`The command ended: timeout. ${detachWay(name)}`);
|
|
7429
|
+
else if (ended.reason !== "exited" && ended.reason !== "signaled")
|
|
7280
7430
|
deps.notice(`The command ended: ${ended.reason}.`);
|
|
7431
|
+
noticeEnded(deps, name, ended);
|
|
7281
7432
|
exitWith(deps, execExitCode(ended));
|
|
7282
7433
|
}
|
|
7434
|
+
function noticeEnded(deps, name, end) {
|
|
7435
|
+
const count = end.killed_processes ?? 0;
|
|
7436
|
+
if (count > 0 && (end.reason === "exited" || end.reason === "signaled"))
|
|
7437
|
+
deps.notice(backgroundEndedNotice(name, count));
|
|
7438
|
+
}
|
|
7283
7439
|
function processEndText(process2) {
|
|
7284
7440
|
const end = process2.end;
|
|
7285
7441
|
if (!end) return "-";
|
|
@@ -7299,6 +7455,7 @@ async function machineProcessStartCommand(deps, name, command, options = {}) {
|
|
|
7299
7455
|
throw new Error(
|
|
7300
7456
|
"Name the command after --, such as `tonbo machine exec web --detach -- npm run build`."
|
|
7301
7457
|
);
|
|
7458
|
+
if (backgroundsWork(command)) deps.notice(backgroundsWorkNotice(name, true));
|
|
7302
7459
|
const body = { command };
|
|
7303
7460
|
if (options.cwd) body.cwd = options.cwd;
|
|
7304
7461
|
if (options.env?.length) body.env = parseEnv2(options.env);
|
|
@@ -7465,13 +7622,13 @@ async function machineProcessSummary(cell, key, region, name) {
|
|
|
7465
7622
|
return null;
|
|
7466
7623
|
}
|
|
7467
7624
|
}
|
|
7468
|
-
async function machineFileReadCommand(deps, name,
|
|
7625
|
+
async function machineFileReadCommand(deps, name, path6, options = {}) {
|
|
7469
7626
|
try {
|
|
7470
7627
|
const key = await deps.auth.apiKey();
|
|
7471
7628
|
const region = await regionOf(deps, key, name);
|
|
7472
7629
|
const response = await cellApi(deps).fetch("readMachineFile", key, region, {
|
|
7473
7630
|
params: { name },
|
|
7474
|
-
query: { path:
|
|
7631
|
+
query: { path: path6 },
|
|
7475
7632
|
headers: { range: options.range, "tonbo-expect-activation": options.expectActivation }
|
|
7476
7633
|
});
|
|
7477
7634
|
const bytes2 = new Uint8Array(await response.arrayBuffer());
|
|
@@ -7481,14 +7638,14 @@ async function machineFileReadCommand(deps, name, path5, options = {}) {
|
|
|
7481
7638
|
throw explainMachineError(error);
|
|
7482
7639
|
}
|
|
7483
7640
|
}
|
|
7484
|
-
async function machineFileWriteCommand(deps, name,
|
|
7641
|
+
async function machineFileWriteCommand(deps, name, path6, options = {}) {
|
|
7485
7642
|
try {
|
|
7486
7643
|
const bytes2 = options.from ? new Uint8Array(await readFile4(options.from)) : await readAllStdin(deps);
|
|
7487
7644
|
const key = await deps.auth.apiKey();
|
|
7488
7645
|
const region = await regionOf(deps, key, name);
|
|
7489
7646
|
const written = await cellApi(deps).json("writeMachineFile", key, region, {
|
|
7490
7647
|
params: { name },
|
|
7491
|
-
query: { path:
|
|
7648
|
+
query: { path: path6 },
|
|
7492
7649
|
headers: {
|
|
7493
7650
|
"content-type": "application/octet-stream",
|
|
7494
7651
|
"if-match": options.ifMatch,
|
|
@@ -7510,7 +7667,7 @@ async function machineFileWriteCommand(deps, name, path5, options = {}) {
|
|
|
7510
7667
|
throw explainMachineError(error);
|
|
7511
7668
|
}
|
|
7512
7669
|
}
|
|
7513
|
-
async function machineFileStatCommand(deps, name,
|
|
7670
|
+
async function machineFileStatCommand(deps, name, path6, options = {}) {
|
|
7514
7671
|
try {
|
|
7515
7672
|
const key = await deps.auth.apiKey();
|
|
7516
7673
|
const region = await regionOf(deps, key, name);
|
|
@@ -7520,7 +7677,7 @@ async function machineFileStatCommand(deps, name, path5, options = {}) {
|
|
|
7520
7677
|
region,
|
|
7521
7678
|
{
|
|
7522
7679
|
params: { name },
|
|
7523
|
-
query: { path:
|
|
7680
|
+
query: { path: path6 },
|
|
7524
7681
|
headers: { "tonbo-expect-activation": options.expectActivation }
|
|
7525
7682
|
}
|
|
7526
7683
|
);
|
|
@@ -7539,13 +7696,13 @@ async function machineFileStatCommand(deps, name, path5, options = {}) {
|
|
|
7539
7696
|
throw explainMachineError(error);
|
|
7540
7697
|
}
|
|
7541
7698
|
}
|
|
7542
|
-
async function machineFileListCommand(deps, name,
|
|
7699
|
+
async function machineFileListCommand(deps, name, path6, options = {}) {
|
|
7543
7700
|
try {
|
|
7544
7701
|
const key = await deps.auth.apiKey();
|
|
7545
7702
|
const region = await regionOf(deps, key, name);
|
|
7546
7703
|
const listing = await cellApi(deps).json("listMachineFiles", key, region, {
|
|
7547
7704
|
params: { name },
|
|
7548
|
-
query: { path:
|
|
7705
|
+
query: { path: path6 },
|
|
7549
7706
|
headers: { "tonbo-expect-activation": options.expectActivation }
|
|
7550
7707
|
});
|
|
7551
7708
|
noticeWake(deps, listing.machine, name);
|
|
@@ -7569,19 +7726,19 @@ async function machineFileListCommand(deps, name, path5, options = {}) {
|
|
|
7569
7726
|
throw explainMachineError(error);
|
|
7570
7727
|
}
|
|
7571
7728
|
}
|
|
7572
|
-
async function machineFileDeleteCommand(deps, name,
|
|
7729
|
+
async function machineFileDeleteCommand(deps, name, path6, options = {}) {
|
|
7573
7730
|
try {
|
|
7574
7731
|
const key = await deps.auth.apiKey();
|
|
7575
7732
|
const region = await regionOf(deps, key, name);
|
|
7576
7733
|
await cellApi(deps).fetch("deleteMachineFile", key, region, {
|
|
7577
7734
|
params: { name },
|
|
7578
|
-
query: { path:
|
|
7735
|
+
query: { path: path6, recursive: options.recursive ? "true" : void 0 },
|
|
7579
7736
|
headers: {
|
|
7580
7737
|
"tonbo-expect-activation": options.expectActivation,
|
|
7581
7738
|
"idempotency-key": randomUUID8()
|
|
7582
7739
|
}
|
|
7583
7740
|
});
|
|
7584
|
-
print(deps, options.json, { path:
|
|
7741
|
+
print(deps, options.json, { path: path6 }, `Deleted ${path6}.`);
|
|
7585
7742
|
} catch (error) {
|
|
7586
7743
|
throw explainMachineError(error);
|
|
7587
7744
|
}
|
|
@@ -7697,8 +7854,8 @@ var LEGACY_BEGIN = "# >>> tonbo project ssh >>>";
|
|
|
7697
7854
|
var LEGACY_END = "# <<< tonbo project ssh <<<";
|
|
7698
7855
|
var PROXY_COMMAND = /^\s*proxycommand(\s|=)/i;
|
|
7699
7856
|
var SSH_PROXY = /(^|[\s'"/])ssh-proxy(\s|['"]|$)/;
|
|
7700
|
-
function shown(
|
|
7701
|
-
return
|
|
7857
|
+
function shown(path6, home) {
|
|
7858
|
+
return path6.startsWith(home + sep) ? `~${path6.slice(home.length)}` : path6;
|
|
7702
7859
|
}
|
|
7703
7860
|
function words(text) {
|
|
7704
7861
|
const found = [];
|
|
@@ -7714,15 +7871,15 @@ async function expand(pattern) {
|
|
|
7714
7871
|
let paths = [sep];
|
|
7715
7872
|
for (const part of parts) {
|
|
7716
7873
|
if (!/[*?]/.test(part)) {
|
|
7717
|
-
paths = paths.map((
|
|
7874
|
+
paths = paths.map((path6) => join(path6, part));
|
|
7718
7875
|
continue;
|
|
7719
7876
|
}
|
|
7720
7877
|
const matcher = wildcard(part);
|
|
7721
7878
|
const next = [];
|
|
7722
|
-
for (const
|
|
7723
|
-
const names = await readdir(
|
|
7879
|
+
for (const path6 of paths) {
|
|
7880
|
+
const names = await readdir(path6).catch(() => []);
|
|
7724
7881
|
for (const name of names.sort())
|
|
7725
|
-
if (matcher.test(name) && !name.startsWith(".")) next.push(join(
|
|
7882
|
+
if (matcher.test(name) && !name.startsWith(".")) next.push(join(path6, name));
|
|
7726
7883
|
}
|
|
7727
7884
|
paths = next;
|
|
7728
7885
|
}
|
|
@@ -7732,18 +7889,18 @@ async function scan(home) {
|
|
|
7732
7889
|
const sshDir = join(home, ".ssh");
|
|
7733
7890
|
const files = [];
|
|
7734
7891
|
const seen = /* @__PURE__ */ new Set();
|
|
7735
|
-
const visit = async (
|
|
7736
|
-
if (depth > 16 || seen.has(
|
|
7737
|
-
seen.add(
|
|
7892
|
+
const visit = async (path6, depth) => {
|
|
7893
|
+
if (depth > 16 || seen.has(path6) || !path6.startsWith(sshDir + sep)) return;
|
|
7894
|
+
seen.add(path6);
|
|
7738
7895
|
let text;
|
|
7739
7896
|
try {
|
|
7740
|
-
if (!(await stat2(
|
|
7741
|
-
text = await readFile5(
|
|
7897
|
+
if (!(await stat2(path6)).isFile()) return;
|
|
7898
|
+
text = await readFile5(path6, "utf8");
|
|
7742
7899
|
} catch {
|
|
7743
7900
|
return;
|
|
7744
7901
|
}
|
|
7745
7902
|
const lines = text.split("\n");
|
|
7746
|
-
const file = { path:
|
|
7903
|
+
const file = { path: path6, lines, blocks: [], handEdited: [] };
|
|
7747
7904
|
const includes = [];
|
|
7748
7905
|
for (let index = 0; index < lines.length; index++) {
|
|
7749
7906
|
if (lines[index].trim() === LEGACY_BEGIN) {
|
|
@@ -7882,10 +8039,10 @@ async function ensureKey(directory) {
|
|
|
7882
8039
|
}
|
|
7883
8040
|
return key;
|
|
7884
8041
|
}
|
|
7885
|
-
async function writeAtomic(
|
|
7886
|
-
const temporary = `${
|
|
8042
|
+
async function writeAtomic(path6, data) {
|
|
8043
|
+
const temporary = `${path6}.${process.pid}.tmp`;
|
|
7887
8044
|
await writeFile3(temporary, data, { mode: 384 });
|
|
7888
|
-
await rename5(temporary,
|
|
8045
|
+
await rename5(temporary, path6);
|
|
7889
8046
|
}
|
|
7890
8047
|
function certificatePath(directory, user) {
|
|
7891
8048
|
return join2(directory, `${user}-cert.pub`);
|
|
@@ -8089,33 +8246,255 @@ async function sshCertificateIssueCommand(deps, options) {
|
|
|
8089
8246
|
print(deps, options.json, data, data.certificate);
|
|
8090
8247
|
}
|
|
8091
8248
|
|
|
8249
|
+
// src/update.ts
|
|
8250
|
+
import { execFile as execFile3, spawn as spawn2 } from "node:child_process";
|
|
8251
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
8252
|
+
import { mkdir as mkdir5, readFile as readFile7, realpath as realpath2, rename as rename6, writeFile as writeFile4 } from "node:fs/promises";
|
|
8253
|
+
import path5 from "node:path";
|
|
8254
|
+
import { fileURLToPath } from "node:url";
|
|
8255
|
+
import { promisify as promisify3 } from "node:util";
|
|
8256
|
+
var contract = cliVersionsContract;
|
|
8257
|
+
var CLI_VERSION = JSON.parse(readFileSync2(new URL("../../package.json", import.meta.url), "utf8")).version;
|
|
8258
|
+
var RELEASE = new RegExp(contract.release_pattern);
|
|
8259
|
+
function compareReleases(a, b) {
|
|
8260
|
+
const left = RELEASE.exec(a);
|
|
8261
|
+
const right = RELEASE.exec(b);
|
|
8262
|
+
if (!left || !right) return null;
|
|
8263
|
+
for (let at = 1; at <= 3; at++) {
|
|
8264
|
+
const difference = Number(left[at]) - Number(right[at]);
|
|
8265
|
+
if (difference !== 0) return Math.sign(difference);
|
|
8266
|
+
}
|
|
8267
|
+
const [leftPre, rightPre] = [left[4], right[4]];
|
|
8268
|
+
if (leftPre === rightPre) return 0;
|
|
8269
|
+
if (leftPre === void 0) return 1;
|
|
8270
|
+
if (rightPre === void 0) return -1;
|
|
8271
|
+
return leftPre < rightPre ? -1 : 1;
|
|
8272
|
+
}
|
|
8273
|
+
function latestOf(headers) {
|
|
8274
|
+
const latest = headers.get(contract.headers.latest)?.trim() ?? "";
|
|
8275
|
+
return RELEASE.test(latest) ? latest : null;
|
|
8276
|
+
}
|
|
8277
|
+
function noticeLine(current, latest) {
|
|
8278
|
+
if (latest === null || (compareReleases(current, latest) ?? 0) >= 0) return null;
|
|
8279
|
+
return contract.notice.line.replace("{latest}", latest).replace("{current}", current);
|
|
8280
|
+
}
|
|
8281
|
+
var ReleaseWatch = class {
|
|
8282
|
+
constructor(current = CLI_VERSION, refuse = (message) => {
|
|
8283
|
+
process.stderr.write(`${message}
|
|
8284
|
+
`);
|
|
8285
|
+
process.exit(1);
|
|
8286
|
+
}) {
|
|
8287
|
+
this.current = current;
|
|
8288
|
+
this.refuse = refuse;
|
|
8289
|
+
}
|
|
8290
|
+
latest = null;
|
|
8291
|
+
fetch(fetcher) {
|
|
8292
|
+
return async (input, init) => {
|
|
8293
|
+
const headers = new Headers(
|
|
8294
|
+
init?.headers ?? (input instanceof Request ? input.headers : void 0)
|
|
8295
|
+
);
|
|
8296
|
+
headers.set(contract.headers.client, this.current);
|
|
8297
|
+
const response = await fetcher(input, { ...init, headers });
|
|
8298
|
+
this.latest = latestOf(response.headers) ?? this.latest;
|
|
8299
|
+
if (response.status === 410) {
|
|
8300
|
+
const body = await response.clone().json().catch(() => null);
|
|
8301
|
+
if (body?.code === contract.refusal.code) {
|
|
8302
|
+
const reason2 = [body.detail, body.error].find(
|
|
8303
|
+
(value) => typeof value === "string"
|
|
8304
|
+
);
|
|
8305
|
+
this.refuse(
|
|
8306
|
+
reason2 ?? `This tonbo (${this.current}) is no longer supported. Run \`tonbo update\` (or \`${contract.update_command}\`).`
|
|
8307
|
+
);
|
|
8308
|
+
}
|
|
8309
|
+
}
|
|
8310
|
+
return response;
|
|
8311
|
+
};
|
|
8312
|
+
}
|
|
8313
|
+
};
|
|
8314
|
+
var set = (value) => !!value && value !== "0";
|
|
8315
|
+
function noticeAllowed({ env, argv, stdoutIsTTY, stderrIsTTY }) {
|
|
8316
|
+
if (set(env.TONBO_NO_UPDATE_NOTICE)) return false;
|
|
8317
|
+
if (set(env.CI) && env.CI?.toLowerCase() !== "false") return false;
|
|
8318
|
+
if (argv.includes("--json")) return false;
|
|
8319
|
+
return stdoutIsTTY || stderrIsTTY;
|
|
8320
|
+
}
|
|
8321
|
+
function noticeStatePath(env = process.env) {
|
|
8322
|
+
return path5.join(path5.dirname(defaultLoginPath(env)), contract.notice.state.file);
|
|
8323
|
+
}
|
|
8324
|
+
var INTERVAL_MS = contract.notice.interval_hours * 60 * 60 * 1e3;
|
|
8325
|
+
function noticeDue(state, now) {
|
|
8326
|
+
const at = state && typeof state === "object" ? Date.parse(String(state.notified_at)) : Number.NaN;
|
|
8327
|
+
return !Number.isFinite(at) || at > now || now - at >= INTERVAL_MS;
|
|
8328
|
+
}
|
|
8329
|
+
async function showUpdateNotice(watch, {
|
|
8330
|
+
context,
|
|
8331
|
+
statePath,
|
|
8332
|
+
now = Date.now(),
|
|
8333
|
+
write = (line) => process.stderr.write(`${line}
|
|
8334
|
+
`)
|
|
8335
|
+
}) {
|
|
8336
|
+
const line = noticeLine(watch.current, watch.latest);
|
|
8337
|
+
if (line === null || !noticeAllowed(context)) return;
|
|
8338
|
+
const state = await readFile7(statePath, "utf8").then((text) => JSON.parse(text)).catch(() => null);
|
|
8339
|
+
if (!noticeDue(state, now)) return;
|
|
8340
|
+
write(line);
|
|
8341
|
+
try {
|
|
8342
|
+
await mkdir5(path5.dirname(statePath), { recursive: true, mode: 448 });
|
|
8343
|
+
const temporary = `${statePath}.${process.pid}.tmp`;
|
|
8344
|
+
await writeFile4(
|
|
8345
|
+
temporary,
|
|
8346
|
+
`${JSON.stringify({ version: 1, notified_at: new Date(now).toISOString() })}
|
|
8347
|
+
`,
|
|
8348
|
+
{ mode: 384 }
|
|
8349
|
+
);
|
|
8350
|
+
await rename6(temporary, statePath);
|
|
8351
|
+
} catch {
|
|
8352
|
+
}
|
|
8353
|
+
}
|
|
8354
|
+
function installMethod(location) {
|
|
8355
|
+
const at = location.replaceAll("\\", "/").toLowerCase();
|
|
8356
|
+
if (at.includes("/_npx/") || at.includes("/bunx-") || at.includes("/pnpm/dlx/")) return "npx";
|
|
8357
|
+
if (at.includes("/pnpm/global/") || at.includes("/global/") && at.includes("/.pnpm/"))
|
|
8358
|
+
return "pnpm";
|
|
8359
|
+
if (at.includes("/install/global/node_modules/")) return "bun";
|
|
8360
|
+
if (at.includes("/yarn/global/") || at.includes("/yarn/data/global/")) return "yarn";
|
|
8361
|
+
if (/\/(lib|npm|nodejs)\/node_modules\/@tonbo\/cli\//.test(at)) return "npm";
|
|
8362
|
+
return "standalone";
|
|
8363
|
+
}
|
|
8364
|
+
var PACKAGE = `${contract.package}@latest`;
|
|
8365
|
+
var UPDATE_COMMANDS = {
|
|
8366
|
+
npm: ["npm", "install", "--global", PACKAGE],
|
|
8367
|
+
pnpm: ["pnpm", "add", "--global", PACKAGE],
|
|
8368
|
+
bun: ["bun", "add", "--global", PACKAGE],
|
|
8369
|
+
yarn: ["yarn", "global", "add", PACKAGE]
|
|
8370
|
+
};
|
|
8371
|
+
function updateCommandLine(method) {
|
|
8372
|
+
if (method === "npx") return `npx ${PACKAGE}`;
|
|
8373
|
+
if (method === "standalone") return contract.update_command;
|
|
8374
|
+
return UPDATE_COMMANDS[method].join(" ");
|
|
8375
|
+
}
|
|
8376
|
+
async function readGlobalRoot(manager) {
|
|
8377
|
+
try {
|
|
8378
|
+
const { stdout } = await promisify3(execFile3)(manager, ["root", "-g"], {
|
|
8379
|
+
shell: process.platform === "win32",
|
|
8380
|
+
timeout: 15e3
|
|
8381
|
+
});
|
|
8382
|
+
return await realpath2(stdout.trim());
|
|
8383
|
+
} catch {
|
|
8384
|
+
return null;
|
|
8385
|
+
}
|
|
8386
|
+
}
|
|
8387
|
+
function isInside(location, directory) {
|
|
8388
|
+
const normal = (value) => value.replaceAll("\\", "/").replace(/\/+$/, "").toLowerCase();
|
|
8389
|
+
return normal(location).startsWith(`${normal(directory)}/`);
|
|
8390
|
+
}
|
|
8391
|
+
async function confirmedInstallMethod(location, globalRoot) {
|
|
8392
|
+
const method = installMethod(location);
|
|
8393
|
+
if (method !== "npm" && method !== "pnpm") return method;
|
|
8394
|
+
const root = await globalRoot(method);
|
|
8395
|
+
if (root === null) return "standalone";
|
|
8396
|
+
return isInside(location, method === "pnpm" ? path5.dirname(root) : root) ? method : "standalone";
|
|
8397
|
+
}
|
|
8398
|
+
async function readLatest(origin, fetcher = fetch) {
|
|
8399
|
+
try {
|
|
8400
|
+
const response = await fetcher(`${origin}/v1/openapi.json`, {
|
|
8401
|
+
method: "HEAD",
|
|
8402
|
+
signal: AbortSignal.timeout(1e4)
|
|
8403
|
+
});
|
|
8404
|
+
return latestOf(response.headers);
|
|
8405
|
+
} catch {
|
|
8406
|
+
return null;
|
|
8407
|
+
}
|
|
8408
|
+
}
|
|
8409
|
+
function runCommand(command) {
|
|
8410
|
+
return new Promise((resolve2, reject) => {
|
|
8411
|
+
const child = spawn2(command[0], command.slice(1), {
|
|
8412
|
+
stdio: "inherit",
|
|
8413
|
+
// npm, pnpm and yarn are .cmd scripts on Windows, which only a shell runs.
|
|
8414
|
+
shell: process.platform === "win32"
|
|
8415
|
+
});
|
|
8416
|
+
child.on("error", reject);
|
|
8417
|
+
child.on("exit", (code, signal) => resolve2(code ?? (signal ? 1 : 0)));
|
|
8418
|
+
});
|
|
8419
|
+
}
|
|
8420
|
+
var SCRIPT_LOCATION = fileURLToPath(import.meta.url);
|
|
8421
|
+
async function updateCommand(deps, options) {
|
|
8422
|
+
if (options.json && !options.check) throw new Error("--json goes with --check.");
|
|
8423
|
+
const latest = await deps.latest();
|
|
8424
|
+
const method = await confirmedInstallMethod(deps.location, deps.globalRoot);
|
|
8425
|
+
const command = updateCommandLine(method);
|
|
8426
|
+
const order = latest === null ? null : compareReleases(deps.current, latest);
|
|
8427
|
+
const available = order !== null && order < 0;
|
|
8428
|
+
if (options.check) {
|
|
8429
|
+
if (options.json) {
|
|
8430
|
+
deps.output(
|
|
8431
|
+
JSON.stringify({
|
|
8432
|
+
current: deps.current,
|
|
8433
|
+
latest,
|
|
8434
|
+
update_available: available,
|
|
8435
|
+
install: method,
|
|
8436
|
+
command
|
|
8437
|
+
})
|
|
8438
|
+
);
|
|
8439
|
+
} else if (latest === null) {
|
|
8440
|
+
deps.output(`tonbo ${deps.current}; the latest release could not be read from Tonbo.`);
|
|
8441
|
+
} else if (available) {
|
|
8442
|
+
deps.output(`tonbo ${deps.current}; ${latest} is available: run tonbo update`);
|
|
8443
|
+
} else {
|
|
8444
|
+
deps.output(`tonbo ${deps.current} is the latest release.`);
|
|
8445
|
+
}
|
|
8446
|
+
return;
|
|
8447
|
+
}
|
|
8448
|
+
if (latest !== null && !available) {
|
|
8449
|
+
deps.output(`tonbo ${deps.current} is the latest release.`);
|
|
8450
|
+
return;
|
|
8451
|
+
}
|
|
8452
|
+
if (method === "npx") {
|
|
8453
|
+
deps.notice(
|
|
8454
|
+
`This tonbo runs through a package runner, which runs the release you name each time: run \`${command}\`.`
|
|
8455
|
+
);
|
|
8456
|
+
return;
|
|
8457
|
+
}
|
|
8458
|
+
if (method === "standalone") {
|
|
8459
|
+
deps.notice(
|
|
8460
|
+
`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.`
|
|
8461
|
+
);
|
|
8462
|
+
process.exitCode = 1;
|
|
8463
|
+
return;
|
|
8464
|
+
}
|
|
8465
|
+
deps.notice(`Running: ${command}`);
|
|
8466
|
+
const status = await deps.run(UPDATE_COMMANDS[method]);
|
|
8467
|
+
if (status !== 0)
|
|
8468
|
+
throw new Error(`${command} exited with status ${status}; tonbo was not updated.`);
|
|
8469
|
+
deps.output(`Updated tonbo${latest ? ` to ${latest}` : ""}: run tonbo --version to check.`);
|
|
8470
|
+
}
|
|
8471
|
+
|
|
8092
8472
|
// src/app.ts
|
|
8093
|
-
var
|
|
8094
|
-
readFileSync2(new URL("../../package.json", import.meta.url), "utf8")
|
|
8095
|
-
);
|
|
8473
|
+
var releaseWatch = new ReleaseWatch();
|
|
8096
8474
|
function createDependencies(selection = {}) {
|
|
8097
8475
|
const api = apiOrigin();
|
|
8476
|
+
const fetcher = releaseWatch.fetch(fetch);
|
|
8098
8477
|
const account = selection.account?.trim() || process.env.TONBO_ACCOUNT?.trim() || void 0;
|
|
8099
8478
|
const credentials = new LoginStore({
|
|
8100
|
-
fetcher
|
|
8479
|
+
fetcher,
|
|
8101
8480
|
apiOrigin: api,
|
|
8102
8481
|
loginOrigin: loginOrigin(api),
|
|
8103
8482
|
account
|
|
8104
8483
|
});
|
|
8105
8484
|
const notice = (text) => console.error(text);
|
|
8106
8485
|
return {
|
|
8107
|
-
api: new ManagementApi(noticingBudget(
|
|
8486
|
+
api: new ManagementApi(noticingBudget(fetcher, notice), api),
|
|
8108
8487
|
ask: terminalAsk,
|
|
8109
8488
|
auth: new AuthClient(
|
|
8110
8489
|
credentials,
|
|
8111
|
-
|
|
8490
|
+
fetcher,
|
|
8112
8491
|
loginOrigin(api),
|
|
8113
8492
|
void 0,
|
|
8114
8493
|
void 0,
|
|
8115
8494
|
void 0,
|
|
8116
8495
|
void 0,
|
|
8117
8496
|
process.env,
|
|
8118
|
-
{ client: "tonbo", clientVersion:
|
|
8497
|
+
{ client: "tonbo", clientVersion: CLI_VERSION }
|
|
8119
8498
|
),
|
|
8120
8499
|
credentials,
|
|
8121
8500
|
interactive: () => process.stdin.isTTY === true && process.stderr.isTTY === true,
|
|
@@ -8200,7 +8579,7 @@ function parseUsageSort(value) {
|
|
|
8200
8579
|
}
|
|
8201
8580
|
}
|
|
8202
8581
|
function createProgram(makeDependencies = createDependencies) {
|
|
8203
|
-
const program = new Command().name("tonbo").description("Sign in to Tonbo and manage account resources.").version(
|
|
8582
|
+
const program = new Command().name("tonbo").description("Sign in to Tonbo and manage account resources.").version(CLI_VERSION).option(
|
|
8204
8583
|
"--account <account>",
|
|
8205
8584
|
"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"
|
|
8206
8585
|
);
|
|
@@ -8214,6 +8593,20 @@ function createProgram(makeDependencies = createDependencies) {
|
|
|
8214
8593
|
const account2 = program.opts().account;
|
|
8215
8594
|
await loginCommand(dependencies(), { ...options, account: account2 });
|
|
8216
8595
|
});
|
|
8596
|
+
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(
|
|
8597
|
+
async (options) => updateCommand(
|
|
8598
|
+
{
|
|
8599
|
+
current: CLI_VERSION,
|
|
8600
|
+
latest: () => readLatest(apiOrigin()),
|
|
8601
|
+
location: SCRIPT_LOCATION,
|
|
8602
|
+
globalRoot: readGlobalRoot,
|
|
8603
|
+
run: runCommand,
|
|
8604
|
+
output: (text) => console.log(text),
|
|
8605
|
+
notice: (text) => console.error(text)
|
|
8606
|
+
},
|
|
8607
|
+
options
|
|
8608
|
+
)
|
|
8609
|
+
);
|
|
8217
8610
|
program.command("logout").description(
|
|
8218
8611
|
"end this computer's CLI login and remove the saved login; Yield's capture here stops until the next login"
|
|
8219
8612
|
).action(async () => logoutCommand(dependencies()));
|
|
@@ -8613,31 +9006,31 @@ function createProgram(makeDependencies = createDependencies) {
|
|
|
8613
9006
|
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"),
|
|
8614
9007
|
"readMachineFile"
|
|
8615
9008
|
).action(
|
|
8616
|
-
async (name,
|
|
9009
|
+
async (name, path6, options) => machineFileReadCommand(dependencies(), name, path6, options)
|
|
8617
9010
|
);
|
|
8618
9011
|
operationOptions(
|
|
8619
9012
|
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"),
|
|
8620
9013
|
"writeMachineFile"
|
|
8621
9014
|
).action(
|
|
8622
|
-
async (name,
|
|
9015
|
+
async (name, path6, options) => machineFileWriteCommand(dependencies(), name, path6, options)
|
|
8623
9016
|
);
|
|
8624
9017
|
operationOptions(
|
|
8625
9018
|
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"),
|
|
8626
9019
|
"statMachineFile"
|
|
8627
9020
|
).action(
|
|
8628
|
-
async (name,
|
|
9021
|
+
async (name, path6, options) => machineFileStatCommand(dependencies(), name, path6, options)
|
|
8629
9022
|
);
|
|
8630
9023
|
operationOptions(
|
|
8631
9024
|
file.command("list <name> <path>").description("list a directory of a Machine").option("--expect-activation <activation>", "list only in this activation"),
|
|
8632
9025
|
"listMachineFiles"
|
|
8633
9026
|
).action(
|
|
8634
|
-
async (name,
|
|
9027
|
+
async (name, path6, options) => machineFileListCommand(dependencies(), name, path6, options)
|
|
8635
9028
|
);
|
|
8636
9029
|
operationOptions(
|
|
8637
9030
|
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"),
|
|
8638
9031
|
"deleteMachineFile"
|
|
8639
9032
|
).action(
|
|
8640
|
-
async (name,
|
|
9033
|
+
async (name, path6, options) => machineFileDeleteCommand(dependencies(), name, path6, options)
|
|
8641
9034
|
);
|
|
8642
9035
|
const hold = machine.command("hold").description("keep a Machine awake for work that has no open connection");
|
|
8643
9036
|
operationOptions(
|
|
@@ -8774,4 +9167,14 @@ try {
|
|
|
8774
9167
|
} catch (error) {
|
|
8775
9168
|
console.error(error instanceof Error ? error.message : error);
|
|
8776
9169
|
if (!process.exitCode) process.exitCode = 1;
|
|
9170
|
+
} finally {
|
|
9171
|
+
await showUpdateNotice(releaseWatch, {
|
|
9172
|
+
context: {
|
|
9173
|
+
env: process.env,
|
|
9174
|
+
argv: process.argv,
|
|
9175
|
+
stdoutIsTTY: process.stdout.isTTY === true,
|
|
9176
|
+
stderrIsTTY: process.stderr.isTTY === true
|
|
9177
|
+
},
|
|
9178
|
+
statePath: noticeStatePath()
|
|
9179
|
+
}).catch(() => void 0);
|
|
8777
9180
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tonbo/cli",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.3",
|
|
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": {
|