@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 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 path5 = operation.path.replace(/\{([^}]+)\}/g, (_, name) => {
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}${path5}${queryString ? `?${queryString}` : ""}`;
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 path5 = operation.path.replace(/\{([^}]+)\}/g, (_, name) => {
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}${path5}${query ? `?${query}` : ""}`;
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; the next request tries again.",
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
- ["Held by", held ? awake.held_by.map(holderText).join(", ") : "nothing"],
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: there is no terminal and no streamed stdin, and `--stdin` sends at most 1 MiB read before 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 path5 = words2.join(" ");
7229
+ const path6 = words2.join(" ");
7103
7230
  command.configureHelp({ ...command.configureHelp(), optionDescription });
7104
- const help = COMMAND_HELP[path5];
7231
+ const help = COMMAND_HELP[path6];
7105
7232
  if (help) {
7106
- named.add(path5);
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(`${path5} has no argument ${name}`);
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(`${path5} has no option ${flag}`);
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 path5 of Object.keys(COMMAND_HELP))
7123
- if (!named.has(path5)) throw new Error(`The help of ${path5} names no command`);
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 !== "exited" && ended.reason !== "signaled")
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, path5, options = {}) {
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: path5 },
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, path5, options = {}) {
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: path5 },
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, path5, options = {}) {
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: path5 },
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, path5, options = {}) {
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: path5 },
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, path5, options = {}) {
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: path5, recursive: options.recursive ? "true" : void 0 },
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: path5 }, `Deleted ${path5}.`);
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(path5, home) {
7701
- return path5.startsWith(home + sep) ? `~${path5.slice(home.length)}` : path5;
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((path5) => join(path5, part));
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 path5 of paths) {
7723
- const names = await readdir(path5).catch(() => []);
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(path5, name));
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 (path5, depth) => {
7736
- if (depth > 16 || seen.has(path5) || !path5.startsWith(sshDir + sep)) return;
7737
- seen.add(path5);
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(path5)).isFile()) return;
7741
- text = await readFile5(path5, "utf8");
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: path5, lines, blocks: [], handEdited: [] };
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(path5, data) {
7886
- const temporary = `${path5}.${process.pid}.tmp`;
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, path5);
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 packageVersion = JSON.parse(
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: fetch,
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(fetch, notice), api),
8486
+ api: new ManagementApi(noticingBudget(fetcher, notice), api),
8108
8487
  ask: terminalAsk,
8109
8488
  auth: new AuthClient(
8110
8489
  credentials,
8111
- fetch,
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: packageVersion.version }
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(packageVersion.version).option(
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, path5, options) => machineFileReadCommand(dependencies(), name, path5, options)
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, path5, options) => machineFileWriteCommand(dependencies(), name, path5, options)
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, path5, options) => machineFileStatCommand(dependencies(), name, path5, options)
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, path5, options) => machineFileListCommand(dependencies(), name, path5, options)
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, path5, options) => machineFileDeleteCommand(dependencies(), name, path5, options)
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.1",
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": {