@tonbo/cli 0.18.0 → 0.20.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
@@ -26,7 +26,7 @@ The login file, `login.json`, and the device id it is tied to are shared with [`
26
26
 
27
27
  ## Commands and the Management API
28
28
 
29
- Every command other than `login` and `logout` is named by one operation of the [Management API](../../contracts/management/openapi-v1.yaml) through its `x-tonbo-cli` and calls that operation, plus reads of the same resource (a `--wait`, the Machine a `name_taken` refusal names, the activations `machine show` reads for the last end, or the Volume a `machine create` or `update --volume` names, read for its id and region); `tonbo machine logs`, once it ships, is the named Streams exception, reading the stream at the Machine's `logs.url`. The command set and the operations tagged with a command are the same set, which `test/commands.test.ts` checks in both directions. The CLI resolves each request from the operation table of the internal package [`@tonbo/cli-core`](../../packages/tonbo-cli-core) (`src/generated/contracts.ts`, which `pnpm --filter @tonbo/cli-core generate:contracts` derives from the contract). `login`, `logout`, `account list` and `account use` are the named exception: they obtain, choose among and discard what the other commands present as their Bearer credential. What a command may do is what your role in the account it acts in allows; the CLI never decides it.
29
+ Every command other than `login` and `logout` is named by one operation of the [Management API](../../contracts/management/openapi-v1.yaml) through its `x-tonbo-cli` and calls that operation, plus reads of the same resource (a `--wait`, the Machine a `name_taken` refusal names, the activations `machine show` reads for the last end, or the Volume a `machine create` or `update --volume` names, read for its id and region); `tonbo machine logs`, once it ships, is the named Streams exception, reading the stream at the Machine's `logs.url`. `tonbo volume move` is the one command that sends another command's operation: it reads the Volume and both Machines and sends the target's `updateMachine` with `move_from` (the `machine create --move-from` of an existing Machine, docs/design/machines-volume-move.md). Apart from it, the command set and the operations tagged with a command are the same set, which `test/commands.test.ts` checks in both directions. The CLI resolves each request from the operation table of the internal package [`@tonbo/cli-core`](../../packages/tonbo-cli-core) (`src/generated/contracts.ts`, which `pnpm --filter @tonbo/cli-core generate:contracts` derives from the contract). `login`, `logout`, `account list` and `account use` are the named exception: they obtain, choose among and discard what the other commands present as their Bearer credential. What a command may do is what your role in the account it acts in allows; the CLI never decides it.
30
30
 
31
31
  The help lives with the commands: [`src/app.ts`](src/app.ts) defines each command, its arguments and flags with their one-line descriptions, and [`src/help.ts`](src/help.ts) holds the long help, each flag's default, the examples, the shared rules and the environment. [`commands.json`](commands.json) is all of it as `--help` prints it, which the docs' CLI reference renders; `test/commands.test.ts` holds it to the program, and holds every runnable command to its details and example and the environment list to the variables the code reads, so after changing a command run `pnpm --filter @tonbo/cli commands`.
32
32
 
package/dist/bin/tonbo.js CHANGED
@@ -364,6 +364,20 @@ var managementOperations = {
364
364
  "idempotencyKey": false,
365
365
  "cli": "tonbo usage series"
366
366
  },
367
+ "getVolumeMetrics": {
368
+ "method": "GET",
369
+ "path": "/metrics/volumes",
370
+ "pathParams": [],
371
+ "query": [
372
+ "series",
373
+ "window",
374
+ "granularity",
375
+ "from",
376
+ "to"
377
+ ],
378
+ "idempotencyKey": false,
379
+ "cli": "tonbo metrics volumes"
380
+ },
367
381
  "getUsageToday": {
368
382
  "method": "GET",
369
383
  "path": "/usage/today",
@@ -414,6 +428,7 @@ var managementOperations = {
414
428
  "pathParams": [],
415
429
  "query": [
416
430
  "device_id",
431
+ "project",
417
432
  "cursor",
418
433
  "limit"
419
434
  ],
@@ -2316,6 +2331,10 @@ var ManagementApi = class {
2316
2331
  getUsageSeries(key, query = {}) {
2317
2332
  return this.call("getUsageSeries", key, { query });
2318
2333
  }
2334
+ /** The account's Volume metrics over a window, such as what they stored each day. */
2335
+ getVolumeMetrics(key, query = {}) {
2336
+ return this.call("getVolumeMetrics", key, { query });
2337
+ }
2319
2338
  /** The account's writes of yesterday and today, hourly, per lane. */
2320
2339
  getUsageToday(key) {
2321
2340
  return this.call("getUsageToday", key);
@@ -2363,7 +2382,8 @@ var ManagementApi = class {
2363
2382
  return {
2364
2383
  machine: answer.data,
2365
2384
  location: headers.location,
2366
- retryAfterSeconds: retryAfterSeconds(headers.retryAfter)
2385
+ retryAfterSeconds: retryAfterSeconds(headers.retryAfter),
2386
+ volumeMove: answer.volume_move ?? null
2367
2387
  };
2368
2388
  } catch (error) {
2369
2389
  if (error instanceof HttpError && error.status < 500)
@@ -2426,6 +2446,13 @@ var ManagementApi = class {
2426
2446
  ifMatch: etag
2427
2447
  });
2428
2448
  }
2449
+ /** An update whose binding names `move_from`: the Machine, and what the
2450
+ * move did, which the answer carries beside it
2451
+ * (docs/design/machines-volume-move.md). */
2452
+ async moveVolume(key, name, etag, change) {
2453
+ const answer = await this.request("updateMachine", key, { params: { name }, body: withoutUndefined(change), ifMatch: etag });
2454
+ return { machine: answer.data, volumeMove: answer.volume_move ?? null };
2455
+ }
2429
2456
  /** With `etag`, only the Machine that ETag names, at that revision, is put
2430
2457
  * to sleep (machine-v2.json `etag.if_match.sleep` is optional). A 202
2431
2458
  * carries the `Retry-After` to read the Machine again after
@@ -2811,7 +2838,8 @@ function accountLogins(login) {
2811
2838
  ...entry,
2812
2839
  account_id,
2813
2840
  kind: entry.kind ?? null,
2814
- name: entry.name ?? null
2841
+ name: entry.name ?? null,
2842
+ role: entry.role ?? null
2815
2843
  }));
2816
2844
  if (!entries.some((entry) => entry.account_id === login.account_id))
2817
2845
  entries.unshift({
@@ -2820,12 +2848,18 @@ function accountLogins(login) {
2820
2848
  key: login.key,
2821
2849
  expires_at: login.expires_at,
2822
2850
  kind: null,
2823
- name: null
2851
+ name: null,
2852
+ role: null
2824
2853
  });
2825
2854
  return entries;
2826
2855
  }
2827
2856
  function personalLogin(login) {
2828
- return accountLogins(login).find((entry) => entry.kind === "user") ?? null;
2857
+ const logins = accountLogins(login);
2858
+ const personal = login.session?.personalAccountId;
2859
+ return (personal ? logins.find((entry) => entry.account_id === personal) : void 0) ?? logins.find((entry) => entry.kind === "user" && !isGuestLogin(entry)) ?? null;
2860
+ }
2861
+ function isGuestLogin(login) {
2862
+ return login.role === "collaborator";
2829
2863
  }
2830
2864
  async function preparePrivateDirectory(directory) {
2831
2865
  const stat3 = await lstat(directory);
@@ -3379,7 +3413,8 @@ var LoginStore = class {
3379
3413
  key: client.bearer(id),
3380
3414
  expires_at: current.session.expires_at,
3381
3415
  kind: current.accounts?.[id]?.kind ?? (id === current.personal_account_id ? "user" : null),
3382
- name: current.accounts?.[id]?.name ?? null
3416
+ name: current.accounts?.[id]?.name ?? null,
3417
+ role: current.accounts?.[id]?.role ?? null
3383
3418
  });
3384
3419
  accounts[current.personal_account_id] = entry(current.personal_account_id);
3385
3420
  for (const id of Object.keys(current.accounts ?? {}))
@@ -5264,6 +5299,37 @@ async function usageSeriesCommand(deps, options = {}) {
5264
5299
  );
5265
5300
  print(deps, options.json, series, lines.join("\n"));
5266
5301
  }
5302
+ async function metricsVolumesCommand(deps, options = {}) {
5303
+ const metrics = await deps.api.getVolumeMetrics(await deps.auth.apiKey(), {
5304
+ series: "stored_bytes",
5305
+ window: options.window,
5306
+ granularity: options.from || options.to ? "day" : void 0,
5307
+ from: options.from,
5308
+ to: options.to
5309
+ });
5310
+ const points = metrics.total.points;
5311
+ const lines = [
5312
+ `What the Volumes stored from ${metrics.from.slice(0, 10)} to ${metrics.to.slice(0, 10)} (UTC, average per day)`,
5313
+ table(
5314
+ ["DAY", "STORED", ""],
5315
+ points.map((point) => [
5316
+ point.start.slice(0, 10),
5317
+ bytes(point.stored_bytes),
5318
+ point.partial ? "so far" : ""
5319
+ ])
5320
+ )
5321
+ ];
5322
+ const complete = points.filter((point) => !point.partial);
5323
+ if (complete.length > 1) {
5324
+ const change = complete[complete.length - 1].stored_bytes - complete[0].stored_bytes;
5325
+ lines.push(
5326
+ `Change over its complete days: ${change < 0 ? "-" : "+"}${bytes(Math.abs(change))}`
5327
+ );
5328
+ }
5329
+ if (metrics.unavailable_regions.length > 0)
5330
+ lines.push(`Not read: ${metrics.unavailable_regions.join(", ")}`);
5331
+ print(deps, options.json, metrics, lines.join("\n"));
5332
+ }
5267
5333
  async function streamActivityCommand(deps, name, options = {}) {
5268
5334
  const activity = await deps.api.getStreamActivity(
5269
5335
  await deps.auth.apiKey(),
@@ -5520,6 +5586,57 @@ async function volumeDeleteCommand(deps, name, options = {}) {
5520
5586
  );
5521
5587
  });
5522
5588
  }
5589
+ function movedText(move, volume, target) {
5590
+ return [
5591
+ `Moved Volume ${volume} from ${move.from} to ${target}; it attaches at ${target}'s next boot, and ${move.from} boots without it.`,
5592
+ ...move.replaced ? [
5593
+ `${target}'s Volume ${move.replaced.name} was unbound and kept; bind it again with \`tonbo machine update <machine> --volume ${move.replaced.name}:<path>\`.`
5594
+ ] : []
5595
+ ];
5596
+ }
5597
+ async function volumeMoveCommand(deps, name, options = {}) {
5598
+ await volumeCommand(deps, async () => {
5599
+ if (!options.to) throw new Error("Pass --to <machine>.");
5600
+ const to = options.to;
5601
+ const key = await deps.auth.apiKey();
5602
+ const volume = await deps.api.getVolume(key, name);
5603
+ const from = volume.machine?.name ?? null;
5604
+ if (from === to && options.path === void 0) {
5605
+ print(
5606
+ deps,
5607
+ options.json,
5608
+ volume,
5609
+ `Volume ${volume.name} is already on ${to}; nothing to move.`
5610
+ );
5611
+ return;
5612
+ }
5613
+ const { machine: target, etag } = await deps.api.getMachineWithEtag(key, to);
5614
+ if (target.kind === "byoc")
5615
+ throw new Error(
5616
+ `${to} is one of your computers; a Volume moves only to a Machine Tonbo runs.`
5617
+ );
5618
+ if (etag === null) throw new Error(`The API answered ${to} without an ETag.`);
5619
+ const binding = { volume: volume.id };
5620
+ let path6 = options.path;
5621
+ if (from !== null && from !== to) {
5622
+ const { machine: source, etag: sourceTag } = await deps.api.getMachineWithEtag(key, from);
5623
+ const held = source.kind === "managed" ? source.volumes?.find((entry) => entry.volume === volume.id) : void 0;
5624
+ path6 ??= held?.path;
5625
+ binding.move_from = from;
5626
+ if (sourceTag !== null) binding.move_from_etag = sourceTag;
5627
+ }
5628
+ if (path6 !== void 0) binding.path = path6;
5629
+ const { machine, volumeMove: move } = await deps.api.moveVolume(key, to, etag, {
5630
+ volumes: [binding]
5631
+ });
5632
+ print(
5633
+ deps,
5634
+ options.json,
5635
+ machine,
5636
+ move ? movedText(move, volume.name, machine.name).join("\n") : `Bound Volume ${volume.name} to ${machine.name}${path6 === void 0 ? " as its whole disk" : ` at ${path6}`}; it attaches at the next boot.`
5637
+ );
5638
+ });
5639
+ }
5523
5640
  function parseVolumeSpec(spec) {
5524
5641
  const split = spec.indexOf(":");
5525
5642
  if (split === -1 && spec.length > 0) return { name: spec };
@@ -5564,6 +5681,10 @@ var MACHINE_ERROR_HINTS = {
5564
5681
  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
5682
  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
5683
  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.",
5684
+ volume_attached: "The Machine the message names is awake with the Volume attached; nothing was changed. Put it to sleep with `tonbo machine sleep <machine>`, then run the command again.",
5685
+ volume_holder_changed: "The Volume is not on the Machine you named; nothing was changed. `tonbo volume show <volume>` says which Machine has it: name that one, or use `tonbo volume move`.",
5686
+ volume_image_mismatch: "The Volume is a Machine's whole disk, made against one image; it moves only to a Machine on that same image. Nothing was changed.",
5687
+ volume_rebuild_pending: "The Volume's Rebuild has not finished; move it once it has. Nothing was changed.",
5567
5688
  secret_env_conflict: "Use another secret key or remove the non-secret env variable with that name.",
5568
5689
  secret_limit: "Reduce the number or byte size of this Machine's secrets.",
5569
5690
  wake_unavailable: "The wake was refused for now; nothing woke. Wake it again after a while; `tonbo machine show` says why it is blocked."
@@ -6132,12 +6253,22 @@ async function waitForCreate(deps, key, machine, name, firstWait, timeout, json)
6132
6253
  async function machineCreateCommand(deps, name, command, options = {}) {
6133
6254
  await machineCommand(async () => {
6134
6255
  if (!options.image) throw new Error("Pass --image <reference>.");
6256
+ if (options.moveFrom !== void 0 && typeof options.volume !== "string")
6257
+ throw new Error("--move-from names the Machine that holds the --volume: pass --volume too.");
6135
6258
  const timeout = waitTimeout(options);
6136
6259
  const env = parseEnv(options.env ?? []);
6137
6260
  const secrets = createSecrets(options);
6138
6261
  const ports = withDefault(parsePorts(options.port ?? []), options.defaultPort);
6139
6262
  const key = await deps.auth.apiKey();
6140
6263
  const bound = typeof options.volume === "string" ? await resolveVolumeBinding(deps, key, options.volume) : null;
6264
+ if (bound && options.moveFrom !== void 0) {
6265
+ const { etag } = await deps.api.getMachineWithEtag(key, options.moveFrom);
6266
+ bound.binding = {
6267
+ ...bound.binding,
6268
+ move_from: options.moveFrom,
6269
+ ...etag === null ? {} : { move_from_etag: etag }
6270
+ };
6271
+ }
6141
6272
  const database = typeof options.database === "string" ? await deps.api.getDatabase(key, options.database) : null;
6142
6273
  if (database && bound && database.region !== bound.region && !options.region)
6143
6274
  throw new Error(
@@ -6197,7 +6328,11 @@ async function machineCreateCommand(deps, name, command, options = {}) {
6197
6328
  deps,
6198
6329
  options.json,
6199
6330
  machine,
6200
- [createdText(machine), machineText(machine, await billingUrl(deps))].join("\n")
6331
+ [
6332
+ createdText(machine),
6333
+ ...created.volumeMove ? movedText(created.volumeMove, options.volume, machine.name) : [],
6334
+ machineText(machine, await billingUrl(deps))
6335
+ ].join("\n")
6201
6336
  );
6202
6337
  });
6203
6338
  }
@@ -6903,9 +7038,13 @@ var COMMAND_HELP = {
6903
7038
  about: [
6904
7039
  "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`.",
6905
7040
  "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.",
7041
+ "`--move-from <machine>` takes the `--volume` from the Machine that has it now, in the same write as the create: that Machine's binding is removed (its ETag changes) and it boots without the Volume. It is refused while that Machine is awake with the Volume attached (`volume_attached`: `tonbo machine sleep <machine>` first), or when another Machine has the Volume (`volume_holder_changed`); either way nothing is created or changed. Without it, a Volume another Machine has is refused.",
6906
7042
  "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."
6907
7043
  ],
6908
- examples: ["tonbo machine create web --image nginx:1.27 --port 80:public --wait"],
7044
+ examples: [
7045
+ "tonbo machine create web --image nginx:1.27 --port 80:public --wait",
7046
+ "tonbo machine create agent --image nginx:1.27 --volume data:/workspace --move-from worker"
7047
+ ],
6909
7048
  arguments: {
6910
7049
  name: "the Machine's name, part of its hostname",
6911
7050
  command: "the command that replaces the image's CMD, after `--`"
@@ -7170,6 +7309,15 @@ var COMMAND_HELP = {
7170
7309
  examples: ["tonbo volume show data"],
7171
7310
  arguments: NAME("Volume")
7172
7311
  },
7312
+ "tonbo volume move": {
7313
+ about: [
7314
+ "Moves the Volume to the Machine `--to`, taking it from the Machine that has it now in one write: the Volume is unbound there (that Machine's ETag changes and it boots without it) and bound to `--to`, which may be awake and gets it at its next boot. The CLI reads both Machines' ETags first, so a Machine changed since is refused, never overwritten. Without `--path` the Volume keeps the path it has now, or stays a whole disk; a whole disk moves only to a Machine on the same image.",
7315
+ 'The Machine that has it must be asleep: an awake one is refused with `volume_attached`, and nothing changes; `tonbo machine sleep <machine>` first. When `--to` already has another Volume, that one is unbound and kept, and the CLI names it. The REST equivalent is `PATCH /v1/machines/<to>` with `If-Match: <its ETag>` and `{"volumes": [{"volume": "<id>", "path": "<path>", "move_from": "<machine>", "move_from_etag": "<its ETag>"}]}`.'
7316
+ ],
7317
+ examples: ["tonbo volume move data --to agent --path /workspace"],
7318
+ arguments: NAME("Volume"),
7319
+ defaults: { "--path": "the path the Volume has now, or none (a whole disk)" }
7320
+ },
7173
7321
  "tonbo volume delete": {
7174
7322
  about: [
7175
7323
  "Deletes the Volume and every byte on it; there are no Volume snapshots or backups. It is refused while a Machine has it. At a terminal it asks you to type the Volume's name first."
@@ -7192,6 +7340,14 @@ var COMMAND_HELP = {
7192
7340
  examples: ["tonbo usage series --days 7 --compare"],
7193
7341
  defaults: { "--days": "30" }
7194
7342
  },
7343
+ "tonbo metrics volumes": {
7344
+ about: [
7345
+ "Prints what the account's Volumes stored, one line per UTC day of the window: the average bytes their files held that day, compressed, summed over every region, which is what they are billed for. Today, and a day not fully rated yet, is marked `so far`. Then the change between the window's first and last complete days.",
7346
+ "Give `--window`, or `--from` and `--to` (at most 92 days); the last 30 days when neither is given."
7347
+ ],
7348
+ examples: ["tonbo metrics volumes --window 7d"],
7349
+ defaults: { "--window": "30d" }
7350
+ },
7195
7351
  "tonbo usage today": {
7196
7352
  about: [
7197
7353
  "Prints the account's write units per UTC hour of yesterday and today, in three columns: databases, streams and other."
@@ -8917,6 +9073,9 @@ function createProgram(makeDependencies = createDependencies) {
8917
9073
  "read a UTF-8 environment secret from FILE; repeat",
8918
9074
  repeated
8919
9075
  ).option("--secret-file <KEY=FILE>", "read binary file secret from FILE; repeat", repeated).option(
9076
+ "--move-from <machine>",
9077
+ "take the --volume from the asleep Machine <machine> that has it, in the same write; that Machine boots without it"
9078
+ ).option(
8920
9079
  "--idempotency-key <key>",
8921
9080
  "reuse the key of an unconfirmed creation to get the Machine it created"
8922
9081
  ).option("--wait", "wait until the Machine is active, or say why it failed").option("--timeout <seconds>", "how long --wait waits, in seconds", parseWhole),
@@ -9118,6 +9277,17 @@ function createProgram(makeDependencies = createDependencies) {
9118
9277
  ).action(
9119
9278
  async (name, options) => volumeShowCommand(dependencies(), name, options)
9120
9279
  );
9280
+ operationOptions(
9281
+ volume.command("move <name>").description(
9282
+ "move a Volume to another Machine of its region, taking it from the asleep Machine that has it in the same write"
9283
+ ).requiredOption("--to <machine>", "the Machine that gets the Volume; it may be awake").option(
9284
+ "--path <path>",
9285
+ "mount it at <path> there; without it, it keeps the path it has now, or stays a whole disk"
9286
+ ),
9287
+ "updateMachine"
9288
+ ).action(
9289
+ async (name, options) => volumeMoveCommand(dependencies(), name, options)
9290
+ );
9121
9291
  operationOptions(
9122
9292
  volume.command("delete <name>").description("delete a Volume and its data; refused while a Machine has it").option("--yes", "delete without asking; required when stdin is not a terminal"),
9123
9293
  "deleteVolume"
@@ -9143,6 +9313,13 @@ function createProgram(makeDependencies = createDependencies) {
9143
9313
  ),
9144
9314
  "getUsageToday"
9145
9315
  ).action(async (options) => usageTodayCommand(dependencies(), options));
9316
+ const metrics = program.command("metrics").description("show how the account's resources are used over time");
9317
+ operationOptions(
9318
+ metrics.command("volumes").description("show what the account's Volumes stored, day by day").option("--window <window>", "the last 7d, 30d or 90d, today included").option("--from <day>", "with --to, the first UTC day, as YYYY-MM-DD").option("--to <day>", "with --from, the UTC day the window ends before, as YYYY-MM-DD"),
9319
+ "getVolumeMetrics"
9320
+ ).action(
9321
+ async (options) => metricsVolumesCommand(dependencies(), options)
9322
+ );
9146
9323
  const ssh = program.command("ssh").description("open a terminal on a Machine, or run a command there, over SSH").argument("<machine>", "the Machine's name").argument("[command...]", "a command to run instead of a shell, after --").action(async (name, command) => sshCommand(dependencies(), name, command));
9147
9324
  ssh.command("config").description(
9148
9325
  "print the ssh_config block that lets ssh, scp, sftp and Remote-SSH reach every Machine"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tonbo/cli",
3
- "version": "0.18.0",
3
+ "version": "0.20.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": {