@tonbo/cli 0.17.2 → 0.18.0

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