@tonbo/cli 0.15.0 → 0.16.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/dist/bin/tonbo.js CHANGED
@@ -174,6 +174,42 @@ var managementOperations = {
174
174
  "idempotencyKey": false,
175
175
  "cli": "tonbo account show"
176
176
  },
177
+ "listBudgets": {
178
+ "method": "GET",
179
+ "path": "/budgets",
180
+ "pathParams": [],
181
+ "query": [],
182
+ "idempotencyKey": false,
183
+ "cli": "tonbo budget list"
184
+ },
185
+ "createBudget": {
186
+ "method": "POST",
187
+ "path": "/budgets",
188
+ "pathParams": [],
189
+ "query": [],
190
+ "idempotencyKey": true,
191
+ "cli": "tonbo budget add"
192
+ },
193
+ "getBudget": {
194
+ "method": "GET",
195
+ "path": "/budgets/{budget_id}",
196
+ "pathParams": [
197
+ "budget_id"
198
+ ],
199
+ "query": [],
200
+ "idempotencyKey": false,
201
+ "cli": "tonbo budget show"
202
+ },
203
+ "changeBudget": {
204
+ "method": "PATCH",
205
+ "path": "/budgets/{budget_id}",
206
+ "pathParams": [
207
+ "budget_id"
208
+ ],
209
+ "query": [],
210
+ "idempotencyKey": false,
211
+ "cli": "tonbo budget set"
212
+ },
177
213
  "listRegions": {
178
214
  "method": "GET",
179
215
  "path": "/regions",
@@ -438,6 +474,13 @@ var managementOperations = {
438
474
  "query": [],
439
475
  "idempotencyKey": false
440
476
  },
477
+ "renewHandoffHolds": {
478
+ "method": "POST",
479
+ "path": "/yield/handoffs/holds",
480
+ "pathParams": [],
481
+ "query": [],
482
+ "idempotencyKey": false
483
+ },
441
484
  "showPersonalHandoff": {
442
485
  "method": "GET",
443
486
  "path": "/yield/handoffs/{id}",
@@ -1179,42 +1222,6 @@ var managementOperations = {
1179
1222
  }
1180
1223
  };
1181
1224
  var plannedManagementOperations = {
1182
- "listBudgets": {
1183
- "method": "GET",
1184
- "path": "/budgets",
1185
- "pathParams": [],
1186
- "query": [],
1187
- "idempotencyKey": false,
1188
- "cli": "tonbo budget list"
1189
- },
1190
- "createBudget": {
1191
- "method": "POST",
1192
- "path": "/budgets",
1193
- "pathParams": [],
1194
- "query": [],
1195
- "idempotencyKey": true,
1196
- "cli": "tonbo budget add"
1197
- },
1198
- "getBudget": {
1199
- "method": "GET",
1200
- "path": "/budgets/{budget_id}",
1201
- "pathParams": [
1202
- "budget_id"
1203
- ],
1204
- "query": [],
1205
- "idempotencyKey": false,
1206
- "cli": "tonbo budget show"
1207
- },
1208
- "changeBudget": {
1209
- "method": "PATCH",
1210
- "path": "/budgets/{budget_id}",
1211
- "pathParams": [
1212
- "budget_id"
1213
- ],
1214
- "query": [],
1215
- "idempotencyKey": false,
1216
- "cli": "tonbo budget set"
1217
- },
1218
1225
  "getMachineActivation": {
1219
1226
  "method": "GET",
1220
1227
  "path": "/machines/{name}/activations/{activation}",
@@ -1983,19 +1990,29 @@ var MANAGEMENT_AUDIENCE = cliSessionContract.audiences.management;
1983
1990
  function isAccountBearer(bearer) {
1984
1991
  return typeof bearer !== "string";
1985
1992
  }
1993
+ function retriesAfterRefusal(bearer, audience, code) {
1994
+ if (!isAccountBearer(bearer))
1995
+ return false;
1996
+ if (bearer.kind === "key")
1997
+ return bearer.refused(audience, code);
1998
+ bearer.invalidate(audience);
1999
+ return true;
2000
+ }
1986
2001
  function managementBearer(bearer) {
1987
2002
  return isAccountBearer(bearer) ? bearer.token(MANAGEMENT_AUDIENCE) : Promise.resolve(bearer);
1988
2003
  }
1989
2004
 
1990
2005
  // ../../packages/tonbo-cli-core/dist/src/access-token.js
1991
2006
  var DEFAULT_LOGIN_HINT = credentialsContract.reader.login_hint;
2007
+ var STREAMS_AUDIENCE = "https://streams.tonbo.dev";
1992
2008
  var GIT_AUDIENCE = "https://git.tonbo.dev";
2009
+ var REFRESH_MARGIN_MS = 6e4;
1993
2010
  var AccessTokenError = class extends Error {
1994
2011
  status;
1995
2012
  retryAfterMs;
1996
2013
  code;
1997
- constructor(message, status, retryAfterMs = null, code = null) {
1998
- super(message);
2014
+ constructor(message, status, retryAfterMs = null, code = null, options) {
2015
+ super(message, options);
1999
2016
  this.status = status;
2000
2017
  this.retryAfterMs = retryAfterMs;
2001
2018
  this.code = code;
@@ -2004,6 +2021,116 @@ var AccessTokenError = class extends Error {
2004
2021
  return this.status === null || this.status === 429 || this.status >= 500;
2005
2022
  }
2006
2023
  };
2024
+ var AccessTokens = class {
2025
+ fetcher;
2026
+ userOrigin;
2027
+ audience;
2028
+ loginHint;
2029
+ now;
2030
+ cached = null;
2031
+ /** The account bearer the last token was of, which `invalidate` forgets. */
2032
+ last = null;
2033
+ constructor(fetcher, userOrigin, audience = STREAMS_AUDIENCE, loginHint = DEFAULT_LOGIN_HINT, now = () => Date.now()) {
2034
+ this.fetcher = fetcher;
2035
+ this.userOrigin = userOrigin;
2036
+ this.audience = audience;
2037
+ this.loginHint = loginHint;
2038
+ this.now = now;
2039
+ }
2040
+ async token(key) {
2041
+ if (isAccountBearer(key)) {
2042
+ this.last = key;
2043
+ return key.token(this.audience);
2044
+ }
2045
+ if (this.cached && this.cached.key === key && this.cached.refreshAt > this.now())
2046
+ return this.cached.token;
2047
+ const issuedAt = this.now();
2048
+ const what = this.audience === GIT_AUDIENCE ? "git" : this.audience === MANAGEMENT_AUDIENCE ? "Management" : "Streams";
2049
+ let response;
2050
+ try {
2051
+ response = await this.fetcher(`${this.userOrigin}/api/iam/token`, {
2052
+ method: "POST",
2053
+ redirect: "manual",
2054
+ signal: AbortSignal.timeout(3e4),
2055
+ headers: { authorization: `Bearer ${key}`, "content-type": "application/json" },
2056
+ body: JSON.stringify({ audience: this.audience })
2057
+ });
2058
+ } catch (error) {
2059
+ throw new AccessTokenError(`The ${what} token exchange failed: ${error instanceof Error ? error.message : String(error)}`, null, null, null, { cause: error });
2060
+ }
2061
+ if (!response.ok) {
2062
+ const problem = await response.json().catch(() => null);
2063
+ const retryAfter = Number(response.headers.get("retry-after") ?? "");
2064
+ throw new AccessTokenError(response.status === 401 ? `Your Tonbo login was refused. Run \`${this.loginHint}\`.` : `The ${what} token exchange answered ${response.status}.`, response.status, Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1e3 : null, typeof problem?.code === "string" ? problem.code : null);
2065
+ }
2066
+ const body = await response.json().catch(() => null);
2067
+ if (typeof body?.access_token !== "string" || !body.access_token || typeof body.expires_in !== "number" || !(body.expires_in > 0))
2068
+ throw new AccessTokenError(`The ${what} token exchange answered no token.`, response.status);
2069
+ this.cached = {
2070
+ key,
2071
+ token: body.access_token,
2072
+ refreshAt: issuedAt + Math.max(0, body.expires_in * 1e3 - REFRESH_MARGIN_MS)
2073
+ };
2074
+ return body.access_token;
2075
+ }
2076
+ /** Forgets the token, after the resource server refused it. */
2077
+ invalidate() {
2078
+ this.cached = null;
2079
+ this.last?.invalidate(this.audience);
2080
+ }
2081
+ /** Sends a request with the token; a 401 drops the token and sends once
2082
+ * more with a newly exchanged one, whose answer is final. */
2083
+ async send(key, request) {
2084
+ const response = await request(await this.token(key));
2085
+ if (response.status !== 401)
2086
+ return response;
2087
+ await response.body?.cancel();
2088
+ if (isAccountBearer(key))
2089
+ key.invalidate(this.audience);
2090
+ else
2091
+ this.invalidate();
2092
+ return request(await this.token(key));
2093
+ }
2094
+ };
2095
+ function apiKeyBearer(key, fetcher, userOrigin, now = () => Date.now()) {
2096
+ const tokens = /* @__PURE__ */ new Map();
2097
+ let exchanging = false;
2098
+ const tokensOf = (audience) => {
2099
+ let found = tokens.get(audience);
2100
+ if (!found) {
2101
+ found = new AccessTokens(fetcher, userOrigin, audience, void 0, now);
2102
+ tokens.set(audience, found);
2103
+ }
2104
+ return found;
2105
+ };
2106
+ return {
2107
+ kind: "key",
2108
+ async token(audience) {
2109
+ if (audience === MANAGEMENT_AUDIENCE && !exchanging)
2110
+ return key;
2111
+ try {
2112
+ return await tokensOf(audience).token(key);
2113
+ } catch (error) {
2114
+ if (!(error instanceof AccessTokenError) || error.status !== 401)
2115
+ throw error;
2116
+ throw new AccessTokenError("TONBO_API_KEY was refused (HTTP 401). Check that the key exists and is not revoked or expired.", 401, null, error.code);
2117
+ }
2118
+ },
2119
+ invalidate(audience) {
2120
+ tokens.get(audience)?.invalidate();
2121
+ },
2122
+ refused(audience, code) {
2123
+ if (audience === MANAGEMENT_AUDIENCE && !exchanging) {
2124
+ if (code !== "exchange_required")
2125
+ return false;
2126
+ exchanging = true;
2127
+ return true;
2128
+ }
2129
+ tokens.get(audience)?.invalidate();
2130
+ return true;
2131
+ }
2132
+ };
2133
+ }
2007
2134
 
2008
2135
  // ../../packages/tonbo-cli-core/dist/src/http.js
2009
2136
  var HttpError = class extends Error {
@@ -2029,11 +2156,11 @@ async function requestJson(fetcher, url, init = {}) {
2029
2156
  if (response.ok)
2030
2157
  return body;
2031
2158
  const record = body && typeof body === "object" ? body : {};
2032
- const reason = [record.detail, record.title, record.error].find((value) => typeof value === "string" && value.length > 0);
2159
+ const reason2 = [record.detail, record.title, record.error].find((value) => typeof value === "string" && value.length > 0);
2033
2160
  const requestId = response.headers.get("x-request-id") ?? (typeof record.request_id === "string" ? record.request_id : null);
2034
2161
  const issues = Array.isArray(record.errors) ? record.errors.filter((issue) => typeof issue?.field === "string" && typeof issue.code === "string").map((issue) => `${issue.field}: ${issue.code}`) : [];
2035
2162
  const message = [
2036
- reason ?? `Request failed with HTTP ${response.status}.`,
2163
+ reason2 ?? `Request failed with HTTP ${response.status}.`,
2037
2164
  ...issues.length ? [`(${issues.join(", ")})`] : []
2038
2165
  ].join(" ");
2039
2166
  throw new HttpError(requestId ? `${message} (request ${requestId})` : message, response.status, body, requestId);
@@ -2051,9 +2178,9 @@ async function presented(key) {
2051
2178
  try {
2052
2179
  return await managementBearer(key);
2053
2180
  } catch (error) {
2054
- if (!(error instanceof AccessTokenError))
2181
+ if (!(error instanceof AccessTokenError) || error.status === null)
2055
2182
  throw error;
2056
- throw new ExchangeRefusal(error.message, error.status ?? 503, { code: error.code }, null);
2183
+ throw new ExchangeRefusal(error.message, error.status, { code: error.code }, null);
2057
2184
  }
2058
2185
  }
2059
2186
  var ManagementApi = class {
@@ -2081,15 +2208,14 @@ var ManagementApi = class {
2081
2208
  try {
2082
2209
  return await attempt();
2083
2210
  } catch (error) {
2084
- if (!isAccountBearer(key) || !(error instanceof HttpError) || error.status !== 401 || error instanceof ExchangeRefusal)
2211
+ if (!(error instanceof HttpError) || error.status !== 401 || error instanceof ExchangeRefusal || !retriesAfterRefusal(key, MANAGEMENT_AUDIENCE, error.code))
2085
2212
  throw error;
2086
- key.invalidate(MANAGEMENT_AUDIENCE);
2087
2213
  return attempt();
2088
2214
  }
2089
2215
  }
2090
2216
  async requestWith(operationId, key, { params = {}, query = {}, body, idempotencyKey, ifMatch }, seen) {
2091
2217
  const operation = managementOperation(operationId);
2092
- const path4 = operation.path.replace(/\{([^}]+)\}/g, (_, name) => {
2218
+ const path5 = operation.path.replace(/\{([^}]+)\}/g, (_, name) => {
2093
2219
  const value = params[name];
2094
2220
  if (value === void 0)
2095
2221
  throw new Error(`${operationId} needs the path parameter ${name}.`);
@@ -2113,7 +2239,7 @@ var ManagementApi = class {
2113
2239
  if (ifMatch)
2114
2240
  headers["if-match"] = ifMatch;
2115
2241
  const queryString = search.toString();
2116
- const url = `${this.apiOrigin}${managementApiBasePath}${path4}${queryString ? `?${queryString}` : ""}`;
2242
+ const url = `${this.apiOrigin}${managementApiBasePath}${path5}${queryString ? `?${queryString}` : ""}`;
2117
2243
  try {
2118
2244
  const fetcher = seen ? async (input, init) => {
2119
2245
  const response = await this.fetcher(input, init);
@@ -2781,17 +2907,20 @@ function accountLogins(login) {
2781
2907
  });
2782
2908
  return entries;
2783
2909
  }
2910
+ function personalLogin(login) {
2911
+ return accountLogins(login).find((entry) => entry.kind === "user") ?? null;
2912
+ }
2784
2913
  async function preparePrivateDirectory(directory) {
2785
- const stat = await lstat(directory);
2786
- if (stat.isSymbolicLink() || !stat.isDirectory())
2914
+ const stat2 = await lstat(directory);
2915
+ if (stat2.isSymbolicLink() || !stat2.isDirectory())
2787
2916
  throw new Error("Tonbo config directory must be a real directory.");
2788
2917
  if (process.platform !== "win32")
2789
2918
  await chmod(directory, DIRECTORY_MODE);
2790
2919
  }
2791
2920
  async function assertExistingDestinationIsSafe(filename) {
2792
2921
  try {
2793
- const stat = await lstat(filename);
2794
- if (stat.isSymbolicLink() || !stat.isFile())
2922
+ const stat2 = await lstat(filename);
2923
+ if (stat2.isSymbolicLink() || !stat2.isFile())
2795
2924
  throw new Error("Tonbo credential path must be a regular file.");
2796
2925
  } catch (error) {
2797
2926
  if (error.code !== "ENOENT")
@@ -2799,17 +2928,17 @@ async function assertExistingDestinationIsSafe(filename) {
2799
2928
  }
2800
2929
  }
2801
2930
  async function assertPrivateRegularFile(filename) {
2802
- const stat = await lstat(filename);
2803
- if (stat.isSymbolicLink() || !stat.isFile())
2931
+ const stat2 = await lstat(filename);
2932
+ if (stat2.isSymbolicLink() || !stat2.isFile())
2804
2933
  throw new Error("Tonbo credential path must be a regular file.");
2805
- if (process.platform !== "win32" && (stat.mode & REFUSED_MODE_BITS) !== 0)
2934
+ if (process.platform !== "win32" && (stat2.mode & REFUSED_MODE_BITS) !== 0)
2806
2935
  throw new Error(`Tonbo credential file must have mode ${credentialsContract.permissions.file_mode}.`);
2807
2936
  }
2808
2937
 
2809
2938
  // ../../packages/tonbo-cli-core/dist/src/device.js
2810
2939
  import { execFile } from "node:child_process";
2811
2940
  import { createHash, randomUUID as randomUUID2 } from "node:crypto";
2812
- import { link, mkdir as mkdir2, open as open2, readFile as readFile2, rename as rename2, rm as rm2 } from "node:fs/promises";
2941
+ import { link, mkdir as mkdir2, open as open2, readFile as readFile2, rename as rename2, rm as rm2, stat } from "node:fs/promises";
2813
2942
  import os2 from "node:os";
2814
2943
  import path2 from "node:path";
2815
2944
  import { promisify } from "node:util";
@@ -2836,6 +2965,14 @@ async function readMachineId(platform = process.platform) {
2836
2965
  function machineIdHash(machineId) {
2837
2966
  return createHash("sha256").update(machineId).digest("hex");
2838
2967
  }
2968
+ var NO_HARD_LINKS = /* @__PURE__ */ new Set(["EPERM", "ENOTSUP", "EOPNOTSUPP", "ENOSYS", "EMLINK", "EXDEV"]);
2969
+ var CREATE_POLL_MS = 50;
2970
+ var CREATE_STALE_MS = 5e3;
2971
+ var exists = (file) => stat(file).then(() => true, (error) => {
2972
+ if (error.code === "ENOENT")
2973
+ return false;
2974
+ throw error;
2975
+ });
2839
2976
  var DeviceStore = class {
2840
2977
  filename;
2841
2978
  machineId;
@@ -2913,13 +3050,49 @@ var DeviceStore = class {
2913
3050
  await link(temporary, this.filename);
2914
3051
  return true;
2915
3052
  } catch (error) {
2916
- if (!replace && error.code === "EEXIST")
3053
+ const code = error.code;
3054
+ if (!replace && code === "EEXIST")
2917
3055
  return false;
3056
+ if (!replace && code && NO_HARD_LINKS.has(code))
3057
+ return await this.create(temporary);
2918
3058
  throw error;
2919
3059
  } finally {
2920
3060
  await rm2(temporary, { force: true });
2921
3061
  }
2922
3062
  }
3063
+ /** The first record on a filesystem without hard links (some FUSE and
3064
+ * network volumes), from the complete, synced `temporary`: renamed into
3065
+ * place, so the file is never seen empty or partial, while holding a lock
3066
+ * directory made exclusively, so of two first runs at once only one
3067
+ * writes it and the other waits for it. A lock left by a run that died
3068
+ * holding it is taken over after a while. */
3069
+ async create(temporary) {
3070
+ const lock = `${this.filename}.lock`;
3071
+ for (let waited = 0; ; waited += CREATE_POLL_MS) {
3072
+ try {
3073
+ await mkdir2(lock, { mode: 448 });
3074
+ break;
3075
+ } catch (error) {
3076
+ if (error.code !== "EEXIST")
3077
+ throw error;
3078
+ }
3079
+ if (await exists(this.filename))
3080
+ return false;
3081
+ if (waited >= CREATE_STALE_MS) {
3082
+ await rm2(lock, { recursive: true, force: true });
3083
+ waited = -CREATE_POLL_MS;
3084
+ } else
3085
+ await new Promise((resolve) => setTimeout(resolve, CREATE_POLL_MS));
3086
+ }
3087
+ try {
3088
+ if (await exists(this.filename))
3089
+ return false;
3090
+ await rename2(temporary, this.filename);
3091
+ return true;
3092
+ } finally {
3093
+ await rm2(lock, { recursive: true, force: true });
3094
+ }
3095
+ }
2923
3096
  };
2924
3097
 
2925
3098
  // ../../packages/tonbo-cli-core/dist/src/session.js
@@ -2964,7 +3137,7 @@ var FileSessionStore = class {
2964
3137
  await removePrivateFile(this.filename);
2965
3138
  }
2966
3139
  };
2967
- var REFRESH_MARGIN_MS = 6e4;
3140
+ var REFRESH_MARGIN_MS2 = 6e4;
2968
3141
  var clients = /* @__PURE__ */ new WeakMap();
2969
3142
  var SessionClient = class _SessionClient {
2970
3143
  stored;
@@ -3059,7 +3232,7 @@ var SessionClient = class _SessionClient {
3059
3232
  }, what, accountId);
3060
3233
  this.tokens.set(`${audience} ${accountId}`, {
3061
3234
  token: body.access_token,
3062
- refreshAt: issuedAt + Math.max(0, body.expires_in * 1e3 - REFRESH_MARGIN_MS)
3235
+ refreshAt: issuedAt + Math.max(0, body.expires_in * 1e3 - REFRESH_MARGIN_MS2)
3063
3236
  });
3064
3237
  return body.access_token;
3065
3238
  }
@@ -3077,7 +3250,7 @@ var SessionClient = class _SessionClient {
3077
3250
  body: JSON.stringify(fields2)
3078
3251
  });
3079
3252
  } catch (error) {
3080
- throw new AccessTokenError(`The ${what} token exchange failed: ${error instanceof Error ? error.message : String(error)}`, null);
3253
+ throw new AccessTokenError(`The ${what} token exchange failed: ${error instanceof Error ? error.message : String(error)}`, null, null, null, { cause: error });
3081
3254
  }
3082
3255
  if (!response.ok)
3083
3256
  throw await this.refusal(response, what, accountId);
@@ -3118,7 +3291,7 @@ var SessionClient = class _SessionClient {
3118
3291
  headers: { authorization: `Bearer ${token}`, accept: "application/json" }
3119
3292
  });
3120
3293
  } catch (error) {
3121
- throw new AccessTokenError(`Reading ${this.accountName(accountId)}'s git workspaces failed: ${error instanceof Error ? error.message : String(error)}`, null);
3294
+ throw new AccessTokenError(`Reading ${this.accountName(accountId)}'s git workspaces failed: ${error instanceof Error ? error.message : String(error)}`, null, null, null, { cause: error });
3122
3295
  }
3123
3296
  const body = await response.json().catch(() => null);
3124
3297
  if (!response.ok)
@@ -3143,7 +3316,7 @@ var SessionClient = class _SessionClient {
3143
3316
  }
3144
3317
  });
3145
3318
  } catch (error) {
3146
- throw new AccessTokenError(`Reading your accounts failed: ${error instanceof Error ? error.message : String(error)}`, null);
3319
+ throw new AccessTokenError(`Reading your accounts failed: ${error instanceof Error ? error.message : String(error)}`, null, null, null, { cause: error });
3147
3320
  }
3148
3321
  if (!response.ok)
3149
3322
  throw await this.refusal(response, "session", "");
@@ -3225,17 +3398,23 @@ var LoginStore = class {
3225
3398
  await this.sessions.remove();
3226
3399
  await this.legacy.remove();
3227
3400
  }
3228
- /** The person's accounts now, saved as the login's cache of names. */
3401
+ /** The person's accounts now, saved as the login's cache of names. `client`
3402
+ * holds them before the save, so when the file cannot be written (an
3403
+ * agent's sandbox) `current(client)` still answers with them. */
3229
3404
  async refreshAccounts(client) {
3230
3405
  const accounts = await client.accounts();
3231
3406
  const latest = await this.sessions.load() ?? client.stored;
3232
3407
  if (latest.session.id === client.stored.session.id) {
3233
3408
  const saved = withAccounts(latest, accounts);
3234
- await this.sessions.save(saved);
3235
3409
  client.stored = saved;
3410
+ await this.sessions.save(saved);
3236
3411
  }
3237
3412
  return accounts;
3238
3413
  }
3414
+ /** The logins `client` holds in this process, without reading the file again. */
3415
+ current(client) {
3416
+ return this.view(client);
3417
+ }
3239
3418
  /** Forgets an account the person no longer belongs to (they left it, or
3240
3419
  * were removed): the login's cache of names is read again without it, it
3241
3420
  * is no longer the default account, and this process's tokens of it are
@@ -3316,11 +3495,21 @@ var LoginStore = class {
3316
3495
  const named = this.options.account;
3317
3496
  if (!named)
3318
3497
  return;
3319
- const response = await this.fetcher(`${this.options.apiOrigin}${managementApiBasePath}${managementOperations.getAccount.path}`, {
3498
+ const read = async () => this.fetcher(`${this.options.apiOrigin}${managementApiBasePath}${managementOperations.getAccount.path}`, {
3320
3499
  redirect: "manual",
3321
3500
  signal: AbortSignal.timeout(3e4),
3322
- headers: { authorization: `Bearer ${key}`, accept: "application/json" }
3501
+ headers: {
3502
+ authorization: `Bearer ${await managementBearer(key)}`,
3503
+ accept: "application/json"
3504
+ }
3323
3505
  });
3506
+ let response = await read();
3507
+ if (response.status === 401) {
3508
+ const refusal = await response.json().catch(() => null);
3509
+ const code = typeof refusal?.code === "string" ? refusal.code : null;
3510
+ if (retriesAfterRefusal(key, MANAGEMENT_AUDIENCE, code))
3511
+ response = await read();
3512
+ }
3324
3513
  const body = await response.json().catch(() => null);
3325
3514
  if (!response.ok)
3326
3515
  throw new Error(`TONBO_API_KEY was refused (HTTP ${response.status}).`);
@@ -3353,9 +3542,11 @@ var DEFAULT_LOGIN_HINT3 = credentialsContract.reader.login_hint;
3353
3542
  var LOOPBACK_HOST = "127.0.0.1";
3354
3543
  var BrowserUnavailableError = class extends Error {
3355
3544
  };
3356
- function prefersDeviceLogin(env = process.env) {
3545
+ function prefersDeviceLogin(env = process.env, platform = process.platform) {
3546
+ if (env.DISPLAY || env.WAYLAND_DISPLAY)
3547
+ return false;
3357
3548
  const ssh = Boolean(env.SSH_CONNECTION || env.SSH_CLIENT || env.SSH_TTY);
3358
- return ssh && !env.DISPLAY && !env.WAYLAND_DISPLAY;
3549
+ return ssh || platform !== "darwin" && platform !== "win32";
3359
3550
  }
3360
3551
  function loginHostname(hostname = os4.hostname()) {
3361
3552
  const cleaned = hostname.replace(/[^A-Za-z0-9._-]/g, "-").replace(/^[^A-Za-z0-9]+/, "").slice(0, 64);
@@ -3374,7 +3565,8 @@ var AuthClient = class {
3374
3565
  entryLine;
3375
3566
  env;
3376
3567
  facts;
3377
- keyChecked = null;
3568
+ /** TONBO_API_KEY's bearer, made once per key so its tokens and its exchange outlive a call. */
3569
+ injectedBearer = null;
3378
3570
  constructor(credentials, fetcher, loginOrigin2 = "https://tonbo.dev", browserOpener = openBrowser, hostname = loginHostname(), loginHint = DEFAULT_LOGIN_HINT3, entryLine = "cli", env = process.env, facts = {}) {
3379
3571
  this.credentials = credentials;
3380
3572
  this.fetcher = fetcher;
@@ -3388,18 +3580,25 @@ var AuthClient = class {
3388
3580
  }
3389
3581
  /**
3390
3582
  * The credential of the account a command acts in: TONBO_API_KEY (whose
3391
- * own account any account the command named must be), or the CLI login's
3392
- * bearer of the named account, else of its default, else of the personal
3393
- * account; with only a legacy login, that file's key.
3583
+ * own account any account the command named must be), presented as the
3584
+ * Tonbo SDK presents a key (apiKeyBearer), or the CLI login's bearer of the
3585
+ * named account, else of its default, else of the personal account; with
3586
+ * only a legacy login, that file's key.
3394
3587
  */
3395
3588
  async apiKey() {
3396
3589
  const injected = this.env.TONBO_API_KEY?.trim();
3397
3590
  if (injected) {
3398
- if (this.keyChecked !== injected) {
3399
- await this.credentials.checkKeyAccount(injected);
3400
- this.keyChecked = injected;
3591
+ if (this.injectedBearer?.key !== injected)
3592
+ this.injectedBearer = {
3593
+ key: injected,
3594
+ bearer: apiKeyBearer(injected, this.fetcher, this.loginOrigin),
3595
+ checked: false
3596
+ };
3597
+ if (!this.injectedBearer.checked) {
3598
+ await this.credentials.checkKeyAccount(this.injectedBearer.bearer);
3599
+ this.injectedBearer.checked = true;
3401
3600
  }
3402
- return injected;
3601
+ return this.injectedBearer.bearer;
3403
3602
  }
3404
3603
  const login = await this.credentials.load();
3405
3604
  if (!login)
@@ -3412,17 +3611,20 @@ var AuthClient = class {
3412
3611
  * Logs this computer in (ADR-0018): the browser handoff of /cli/login, PKCE
3413
3612
  * S256 with a loopback redirect on a free port, asks for a session
3414
3613
  * (`grant=cli_session`); the approval names this computer and asks no
3415
- * account. With `device`, in an SSH session without a display, or when no
3614
+ * account. With `device`, in an SSH session or on Linux without a display, or when no
3416
3615
  * browser can be opened, the same PKCE challenge goes to a device-code
3417
3616
  * login instead: the person enters a short code at /cli/device in any
3418
3617
  * browser and the CLI polls. The redeemed session, tied to this
3419
3618
  * computer's device id, is saved as `login.json` and replaces this
3420
3619
  * computer's earlier session; the legacy `credentials.json` is left as it
3421
3620
  * is. With `account` (a name or id), commands act there by default unless
3422
- * `current` is false. Answers the login of the account commands act in.
3621
+ * `current` is false. With `openBrowser` false the browser login opens no
3622
+ * browser: the person opens the `authorization-url` they are shown, as
3623
+ * when an agent shows it with what the approval consents to. Answers the
3624
+ * login of the account commands act in.
3423
3625
  */
3424
3626
  async login(progress = () => {
3425
- }, account, { current = true, device } = {}) {
3627
+ }, account, { current = true, device, openBrowser: openBrowser2 = true } = {}) {
3426
3628
  const deviceId = await (this.facts.devices ?? new DeviceStore()).deviceId();
3427
3629
  const verifier = randomBytes(48).toString("base64url");
3428
3630
  const challenge = createHash3("sha256").update(verifier).digest("base64url");
@@ -3433,7 +3635,7 @@ var AuthClient = class {
3433
3635
  ...clientFact(this.facts.clientVersion) ? { client_version: clientFact(this.facts.clientVersion) } : {},
3434
3636
  ...clientFact(process.platform) ? { os: process.platform } : {}
3435
3637
  };
3436
- const redeemed = device ?? prefersDeviceLogin(this.env) ? await this.deviceLogin(challenge, verifier, redeem, progress) : await this.browserLogin(challenge, verifier, redeem, progress).catch((error) => {
3638
+ const redeemed = device ?? prefersDeviceLogin(this.env) ? await this.deviceLogin(challenge, verifier, redeem, progress) : await this.browserLogin(challenge, verifier, redeem, progress, openBrowser2).catch((error) => {
3437
3639
  if (!(error instanceof BrowserUnavailableError))
3438
3640
  throw error;
3439
3641
  progress({ status: "failed", step: "browser", reason: error.message });
@@ -3469,7 +3671,7 @@ var AuthClient = class {
3469
3671
  throw new Error("The saved login could not be read back.");
3470
3672
  return login;
3471
3673
  }
3472
- async browserLogin(challenge, verifier, redeem, progress) {
3674
+ async browserLogin(challenge, verifier, redeem, progress, openBrowser2) {
3473
3675
  const state = randomBytes(24).toString("base64url");
3474
3676
  const code = await this.listenForCode(state, (redirectUri) => {
3475
3677
  const authorization = new URL("/cli/login", this.loginOrigin);
@@ -3483,7 +3685,7 @@ var AuthClient = class {
3483
3685
  [cliSessionContract.grantParameter]: cliSessionContract.grant
3484
3686
  }).toString();
3485
3687
  return authorization.toString();
3486
- }, progress);
3688
+ }, progress, openBrowser2);
3487
3689
  progress({ status: "started", step: "key-redemption" });
3488
3690
  const redeemed = await requestJson(this.fetcher, new URL("/api/cli/login/token", this.loginOrigin).toString(), {
3489
3691
  method: "POST",
@@ -3561,7 +3763,7 @@ var AuthClient = class {
3561
3763
  }
3562
3764
  }
3563
3765
  }
3564
- listenForCode(expectedState, authorizationUrl, progress) {
3766
+ listenForCode(expectedState, authorizationUrl, progress, openBrowser2) {
3565
3767
  return new Promise((resolve, reject) => {
3566
3768
  let settled = false;
3567
3769
  const sockets = /* @__PURE__ */ new Set();
@@ -3636,6 +3838,8 @@ var AuthClient = class {
3636
3838
  progress({ status: "completed", step: "callback-server" });
3637
3839
  const url = authorizationUrl(`http://${LOOPBACK_HOST}:${port}/callback`);
3638
3840
  progress({ status: "authorization-url", step: "browser", url });
3841
+ if (!openBrowser2)
3842
+ return;
3639
3843
  progress({ status: "started", step: "browser" });
3640
3844
  void Promise.resolve().then(() => this.browserOpener(url)).then(() => startAuthorization(), (error) => {
3641
3845
  if (settled || authorizationStarted)
@@ -3681,6 +3885,150 @@ async function openBrowser(url) {
3681
3885
  }
3682
3886
  }
3683
3887
 
3888
+ // ../../packages/tonbo-cli-core/dist/src/capture-filter.js
3889
+ import { randomUUID as randomUUID3 } from "node:crypto";
3890
+ import { existsSync } from "node:fs";
3891
+ import { mkdir as mkdir3, open as open3, readFile as readFile3, rename as rename3, rm as rm3 } from "node:fs/promises";
3892
+ import os5 from "node:os";
3893
+ import path4 from "node:path";
3894
+ var KEPT_WINDOWS = 200;
3895
+ function yieldStateDirectory(env = process.env, home = os5.homedir()) {
3896
+ return path4.join(env.XDG_STATE_HOME || path4.join(home, ".local", "state"), "tonbo-yield");
3897
+ }
3898
+ function emptyFilter() {
3899
+ return { version: 1, pause: null, past: [], excluded: [] };
3900
+ }
3901
+ function activePause(file, now) {
3902
+ const pause = file.pause;
3903
+ return pause && (pause.until === null || now < pause.until) ? pause : null;
3904
+ }
3905
+ function pauseForLogout(file, now, person) {
3906
+ const current = activePause(file, now);
3907
+ if (current?.logout)
3908
+ return file;
3909
+ const after = current ? { after: current.until } : {};
3910
+ if (current)
3911
+ current.until = now;
3912
+ settle(file, now);
3913
+ file.pause = { since: now, until: null, logout: { ...after, person } };
3914
+ return file;
3915
+ }
3916
+ function endLogoutPause(file, now, person) {
3917
+ const current = activePause(file, now);
3918
+ if (!current?.logout)
3919
+ return file;
3920
+ const { after, person: before } = current.logout;
3921
+ if (person === null || before == null || before !== person)
3922
+ current.since = 0;
3923
+ current.until = now;
3924
+ settle(file, now);
3925
+ if (after === null || after !== void 0 && after > now)
3926
+ file.pause = { since: now, until: after };
3927
+ return file;
3928
+ }
3929
+ function settle(file, now) {
3930
+ if (file.pause && !activePause(file, now)) {
3931
+ file.past = [...file.past, file.pause].slice(-KEPT_WINDOWS);
3932
+ file.pause = null;
3933
+ }
3934
+ }
3935
+ async function durableWrite(filename, body) {
3936
+ const parent = path4.dirname(filename);
3937
+ const created = await mkdir3(parent, { recursive: true, mode: 448 });
3938
+ if (created) {
3939
+ const state = await open3(path4.dirname(parent), "r");
3940
+ try {
3941
+ await state.sync();
3942
+ } finally {
3943
+ await state.close();
3944
+ }
3945
+ }
3946
+ const temporary = path4.join(parent, `.${randomUUID3()}.tmp`);
3947
+ const file = await open3(temporary, "wx", 384);
3948
+ try {
3949
+ await file.writeFile(JSON.stringify(body));
3950
+ await file.sync();
3951
+ } finally {
3952
+ await file.close();
3953
+ }
3954
+ try {
3955
+ await rename3(temporary, filename);
3956
+ const dir = await open3(parent, "r");
3957
+ try {
3958
+ await dir.sync();
3959
+ } finally {
3960
+ await dir.close();
3961
+ }
3962
+ } catch (error) {
3963
+ await rm3(temporary, { force: true });
3964
+ throw error;
3965
+ }
3966
+ }
3967
+ var CaptureFilterStore = class {
3968
+ filename;
3969
+ constructor(filename = path4.join(yieldStateDirectory(), "capture-filter.json")) {
3970
+ this.filename = filename;
3971
+ }
3972
+ /** A missing file filters nothing; an unreadable one throws, and the
3973
+ * daemon then captures nothing (fail closed). */
3974
+ async load() {
3975
+ let body;
3976
+ try {
3977
+ body = await readFile3(this.filename, "utf8");
3978
+ } catch (error) {
3979
+ if (error.code === "ENOENT")
3980
+ return emptyFilter();
3981
+ throw error;
3982
+ }
3983
+ const parsed = JSON.parse(body);
3984
+ const window = (value) => !!value && Number.isFinite(value.since) && (value.until === null || Number.isFinite(value.until));
3985
+ if (parsed?.version !== 1 || !(parsed.pause === null || window(parsed.pause)) || !Array.isArray(parsed.past) || !parsed.past.every(window) || !Array.isArray(parsed.excluded) || !parsed.excluded.every((folder) => typeof folder === "string" && path4.isAbsolute(folder)))
3986
+ throw new Error(`${this.filename} is not a Yield capture filter file.`);
3987
+ return parsed;
3988
+ }
3989
+ async save(file) {
3990
+ await durableWrite(this.filename, file);
3991
+ }
3992
+ async update(change) {
3993
+ const file = await this.load();
3994
+ change(file);
3995
+ await this.save(file);
3996
+ return file;
3997
+ }
3998
+ };
3999
+ var CaptureNotStoppedError = class extends Error {
4000
+ };
4001
+ var reason = (error) => error instanceof Error ? error.message : String(error);
4002
+ async function stopCaptureForLogout(person, store = new CaptureFilterStore(), now = Date.now()) {
4003
+ if (!existsSync(path4.dirname(store.filename)))
4004
+ return null;
4005
+ let file;
4006
+ try {
4007
+ file = await store.load();
4008
+ } catch (error) {
4009
+ return `Yield's capture stays stopped on this computer: ${reason(error)}`;
4010
+ }
4011
+ pauseForLogout(file, now, person);
4012
+ try {
4013
+ await store.save(file);
4014
+ } catch (error) {
4015
+ throw new CaptureNotStoppedError(`Could not stop Yield's capture on this computer (${reason(error)}), so nothing was logged out. Run the logout again where it can write ${store.filename}, outside an agent's sandbox.`, { cause: error });
4016
+ }
4017
+ return null;
4018
+ }
4019
+ async function resumeCaptureAfterLogin(person, store = new CaptureFilterStore(), now = Date.now()) {
4020
+ try {
4021
+ const file = await store.load();
4022
+ if (!activePause(file, now)?.logout)
4023
+ return null;
4024
+ endLogoutPause(file, now, await person().catch(() => null));
4025
+ await store.save(file);
4026
+ return null;
4027
+ } catch (error) {
4028
+ return `Yield's capture stays stopped on this computer: ${reason(error)}`;
4029
+ }
4030
+ }
4031
+
3684
4032
  // ../../packages/tonbo-cli-core/dist/src/cell.js
3685
4033
  function cellOrigin(region, configured = process.env.TONBO_CELL_API_ORIGIN) {
3686
4034
  if (!/^[a-z0-9-]+$/.test(region))
@@ -3698,7 +4046,7 @@ var CellApi = class {
3698
4046
  }
3699
4047
  url(operationId, region, request) {
3700
4048
  const operation = managementOperations[operationId];
3701
- const path4 = operation.path.replace(/\{([^}]+)\}/g, (_, name) => {
4049
+ const path5 = operation.path.replace(/\{([^}]+)\}/g, (_, name) => {
3702
4050
  const value = request.params[name];
3703
4051
  if (value === void 0)
3704
4052
  throw new Error(`${operationId} needs the path parameter ${name}.`);
@@ -3709,7 +4057,7 @@ var CellApi = class {
3709
4057
  if (value !== void 0)
3710
4058
  search.set(name, value);
3711
4059
  const query = search.toString();
3712
- return `${cellOrigin(region)}${managementApiBasePath}${path4}${query ? `?${query}` : ""}`;
4060
+ return `${cellOrigin(region)}${managementApiBasePath}${path5}${query ? `?${query}` : ""}`;
3713
4061
  }
3714
4062
  async init(operationId, key, request) {
3715
4063
  const headers = {
@@ -3787,14 +4135,22 @@ function when(timestamp, absent = "never") {
3787
4135
  }
3788
4136
 
3789
4137
  // ../../packages/tonbo-cli-core/dist/src/login.js
4138
+ function captureAfterLogin(deps) {
4139
+ return resumeCaptureAfterLogin(async () => {
4140
+ const file = await deps.credentials.load();
4141
+ return file ? personalLogin(file)?.account_id ?? null : null;
4142
+ }, deps.captureFilter);
4143
+ }
3790
4144
  async function loginCommand(deps, options = {}) {
3791
4145
  try {
3792
4146
  const login = await deps.auth.login((event) => reportLoginProgress(deps.progress, event), options.account, { current: options.current, device: options.device });
4147
+ const stopped = await captureAfterLogin(deps);
3793
4148
  const file = await deps.credentials.load();
3794
4149
  const names = file ? accountLogins(file).map((entry) => entry.kind === "user" ? `${entry.name ?? "you"} (personal)` : entry.name ?? entry.account_id) : [];
3795
4150
  deps.output([
3796
4151
  `Logged in. This computer can act in every account you belong to, with your role in each${names.length ? `: ${names.join(", ")}` : ""}.`,
3797
- `Commands act in ${login.kind === "user" ? "your personal account" : login.name ?? login.account_id} unless you pass --account.${login.expires_at ? ` The login ends ${login.expires_at.slice(0, 10)}, or after 30 days unused.` : ""}`
4152
+ `Commands act in ${login.kind === "user" ? "your personal account" : login.name ?? login.account_id} unless you pass --account.${login.expires_at ? ` The login ends ${login.expires_at.slice(0, 10)}, or after 30 days unused.` : ""}`,
4153
+ ...stopped ? [stopped] : []
3798
4154
  ].join("\n"));
3799
4155
  return login;
3800
4156
  } catch (error) {
@@ -3866,11 +4222,13 @@ async function logoutCommand(deps) {
3866
4222
  failures.push(`${what}: ${failure.message}`);
3867
4223
  };
3868
4224
  let session = null;
4225
+ let unusable = false;
3869
4226
  try {
3870
4227
  session = await deps.credentials.session({ anyDevice: true });
3871
4228
  } catch (error) {
3872
4229
  if (!(error instanceof UnusableLoginError))
3873
4230
  throw error;
4231
+ unusable = true;
3874
4232
  }
3875
4233
  let legacy = null;
3876
4234
  try {
@@ -3878,6 +4236,13 @@ async function logoutCommand(deps) {
3878
4236
  } catch (error) {
3879
4237
  if (!(error instanceof UnusableLoginError))
3880
4238
  throw error;
4239
+ unusable = true;
4240
+ }
4241
+ if (session || legacy || unusable) {
4242
+ const person = session?.stored.personal_account_id ?? (legacy && personalLogin(legacy)?.account_id) ?? null;
4243
+ const stopped = await stopCaptureForLogout(person, deps.captureFilter);
4244
+ if (stopped)
4245
+ deps.output(stopped);
3881
4246
  }
3882
4247
  if (session)
3883
4248
  await session.logout().catch((error) => failed("the CLI login", error));
@@ -3986,7 +4351,7 @@ var TerminalProgress = class {
3986
4351
  };
3987
4352
 
3988
4353
  // src/commands.ts
3989
- import { randomUUID as randomUUID4 } from "node:crypto";
4354
+ import { randomUUID as randomUUID5 } from "node:crypto";
3990
4355
 
3991
4356
  // src/agent-name.ts
3992
4357
  var AGENT_NAME_RULE = agentNameContract.$defs.name;
@@ -4002,7 +4367,7 @@ function validateAgentName(name) {
4002
4367
  }
4003
4368
 
4004
4369
  // src/budget.ts
4005
- import { randomUUID as randomUUID3 } from "node:crypto";
4370
+ import { randomUUID as randomUUID4 } from "node:crypto";
4006
4371
  var BUDGETS_NOT_AVAILABLE = "Budgets are not available yet.";
4007
4372
  function parseDollars(value) {
4008
4373
  const match = /^\$?([0-9]+)(?:\.([0-9]{1,2}))?$/.exec(value.trim());
@@ -4094,7 +4459,7 @@ async function budgetAddCommand(deps, name, options) {
4094
4459
  const key = await deps.auth.apiKey();
4095
4460
  const limit = parseDollars(options.limit);
4096
4461
  const principals = options.member?.length ? await Promise.all(options.member.map((member) => resolveMemberSubject(deps, key, member))) : [(await deps.api.getAccount(key)).member.subject];
4097
- const created = await deps.api.createBudget(key, { scope: "principals", name, limit_micros: limit, principals }, randomUUID3()).catch((error) => consoleOr(deps, error));
4462
+ const created = await deps.api.createBudget(key, { scope: "principals", name, limit_micros: limit, principals }, randomUUID4()).catch((error) => consoleOr(deps, error));
4098
4463
  print(
4099
4464
  deps,
4100
4465
  options.json,
@@ -4127,7 +4492,7 @@ async function attachBudget(deps, key, subject, value, name, created) {
4127
4492
  const budget = await deps.api.createBudget(
4128
4493
  key,
4129
4494
  { scope: "principals", name, limit_micros: limit, principals: [subject] },
4130
- randomUUID3()
4495
+ randomUUID4()
4131
4496
  ).catch((error) => consoleOr(deps, error));
4132
4497
  deps.notice(`Budget ${budget.name ?? name}: ${usd(limit)} a month for ${subject}.`);
4133
4498
  return;
@@ -4136,8 +4501,8 @@ async function attachBudget(deps, key, subject, value, name, created) {
4136
4501
  await deps.api.changeBudget(key, found.id, { add_principals: [subject] }).catch((error) => consoleOr(deps, error));
4137
4502
  deps.notice(`Budget ${found.name ?? found.id} now covers ${subject}.`);
4138
4503
  } catch (error) {
4139
- const reason = error instanceof Error ? error.message : String(error);
4140
- throw new Error(`${created} was created, but its budget was not set: ${reason}`);
4504
+ const reason2 = error instanceof Error ? error.message : String(error);
4505
+ throw new Error(`${created} was created, but its budget was not set: ${reason2}`);
4141
4506
  }
4142
4507
  }
4143
4508
  function accountBudgetFields(account) {
@@ -4246,7 +4611,7 @@ async function agentCreateCommand(deps, name, options = {}) {
4246
4611
  const agent = await deps.api.createAgent(
4247
4612
  key,
4248
4613
  { name, region, ...options.role === void 0 ? {} : { role: options.role } },
4249
- options.idempotencyKey ?? randomUUID4()
4614
+ options.idempotencyKey ?? randomUUID5()
4250
4615
  );
4251
4616
  print(
4252
4617
  deps,
@@ -4554,7 +4919,7 @@ async function bucketCreateCommand(deps, name, options = {}) {
4554
4919
  const bucket = await deps.api.createBucket(
4555
4920
  await deps.auth.apiKey(),
4556
4921
  { name, region: options.region },
4557
- options.idempotencyKey ?? randomUUID4()
4922
+ options.idempotencyKey ?? randomUUID5()
4558
4923
  );
4559
4924
  print(
4560
4925
  deps,
@@ -4685,7 +5050,7 @@ async function databaseCreateCommand(deps, name, options = {}) {
4685
5050
  const database = await deps.api.createDatabase(
4686
5051
  await deps.auth.apiKey(),
4687
5052
  { name, region: options.region, expires_at: expiresAt },
4688
- options.idempotencyKey ?? randomUUID4()
5053
+ options.idempotencyKey ?? randomUUID5()
4689
5054
  );
4690
5055
  print(
4691
5056
  deps,
@@ -4873,7 +5238,7 @@ async function streamCreateCommand(deps, name, options = {}) {
4873
5238
  content_type: options.contentType,
4874
5239
  expires_at: expiresAt
4875
5240
  },
4876
- options.idempotencyKey ?? randomUUID4()
5241
+ options.idempotencyKey ?? randomUUID5()
4877
5242
  );
4878
5243
  print(
4879
5244
  deps,
@@ -5097,11 +5462,11 @@ async function accountUseCommand(deps, account, options = {}) {
5097
5462
  import { Help } from "commander";
5098
5463
 
5099
5464
  // src/machine.ts
5100
- import { randomUUID as randomUUID6 } from "node:crypto";
5465
+ import { randomUUID as randomUUID7 } from "node:crypto";
5101
5466
  import { readFileSync } from "node:fs";
5102
5467
 
5103
5468
  // src/volume.ts
5104
- import { randomUUID as randomUUID5 } from "node:crypto";
5469
+ import { randomUUID as randomUUID6 } from "node:crypto";
5105
5470
  var VOLUME_DURABILITY_LINK = "https://tonbo.dev/docs/machines/guarantees/data#volume";
5106
5471
  var VOLUME_ERROR_HINTS = {
5107
5472
  payment_required: "Add credit at <billing>, then create the Volume again.",
@@ -5199,7 +5564,7 @@ async function volumeCreateCommand(deps, name, options = {}) {
5199
5564
  const volume = await deps.api.createVolume(
5200
5565
  key,
5201
5566
  { name, region, size_gib: sizeGib },
5202
- options.idempotencyKey ?? randomUUID5()
5567
+ options.idempotencyKey ?? randomUUID6()
5203
5568
  );
5204
5569
  print(
5205
5570
  deps,
@@ -5355,10 +5720,10 @@ function duration(seconds) {
5355
5720
  return seconds % 60 === 0 && seconds >= 120 ? `${seconds / 60} min` : `${seconds} s`;
5356
5721
  }
5357
5722
  function sleepText(machine) {
5358
- const { action, reason } = machine.status.sleep;
5723
+ const { action, reason: reason2 } = machine.status.sleep;
5359
5724
  const when2 = `after about ${duration(earlyReclaimSeconds)} idle (early) or at most ${machine.sleep_after_seconds} s without a connection`;
5360
5725
  if (action !== "release") return `${when2}; it then ${action}s`;
5361
- return `${when2}; the disk resets at every sleep${reason === "suspend_unavailable" ? " until suspend ships" : ""}`;
5726
+ return `${when2}; the disk resets at every sleep${reason2 === "suspend_unavailable" ? " until suspend ships" : ""}`;
5362
5727
  }
5363
5728
  function activationFields(machine) {
5364
5729
  const { activation } = machine;
@@ -5793,9 +6158,9 @@ async function waitForCreate(deps, key, machine, name, firstWait, timeout, json)
5793
6158
  }
5794
6159
  print(deps, json, settled, machineFields(settled, billing));
5795
6160
  if (settled.state === "failed") {
5796
- const { reason, hint } = creationFailureText(settled, billing);
6161
+ const { reason: reason2, hint } = creationFailureText(settled, billing);
5797
6162
  throw new Error(
5798
- `Machine ${settled.name} failed to be created${reason}; delete it with \`tonbo machine delete ${settled.name}\` and create it again.${hint}`
6163
+ `Machine ${settled.name} failed to be created${reason2}; delete it with \`tonbo machine delete ${settled.name}\` and create it again.${hint}`
5799
6164
  );
5800
6165
  }
5801
6166
  throw new Error(`Machine ${settled.name} is ${settled.state}; it was not created.`);
@@ -5837,7 +6202,7 @@ async function machineCreateCommand(deps, name, command, options = {}) {
5837
6202
  ...database ? { database: database.name } : {},
5838
6203
  ...options.database === false ? { database: null } : {}
5839
6204
  },
5840
- options.idempotencyKey ?? randomUUID6()
6205
+ options.idempotencyKey ?? randomUUID7()
5841
6206
  );
5842
6207
  } catch (error) {
5843
6208
  if (error instanceof HttpError && error.code === "name_taken")
@@ -5849,9 +6214,9 @@ async function machineCreateCommand(deps, name, command, options = {}) {
5849
6214
  if (machine.state === "failed") {
5850
6215
  const billing = await billingUrl(deps);
5851
6216
  print(deps, options.json, machine, machineFields(machine, billing));
5852
- const { reason, hint } = creationFailureText(machine, billing);
6217
+ const { reason: reason2, hint } = creationFailureText(machine, billing);
5853
6218
  throw new Error(
5854
- `Machine ${machine.name} failed to be created${reason}; delete it with \`tonbo machine delete ${machine.name}\` and create it again.${hint}`
6219
+ `Machine ${machine.name} failed to be created${reason2}; delete it with \`tonbo machine delete ${machine.name}\` and create it again.${hint}`
5855
6220
  );
5856
6221
  }
5857
6222
  if (options.wait && machine.state === "creating")
@@ -5895,9 +6260,9 @@ async function lastActivationEnd(deps, key, name) {
5895
6260
  const page = await deps.api.listMachineActivations(key, name, { limit: 2 });
5896
6261
  return page.data.find((entry) => entry.end !== null)?.end ?? null;
5897
6262
  } catch (error) {
5898
- if (!(error instanceof HttpError)) throw error;
6263
+ if (!(error instanceof HttpError || error instanceof AccessTokenError)) throw error;
5899
6264
  deps.notice(
5900
- `Could not read how its last activation ended (${error.status}); \`tonbo machine activations ${name}\` lists them.`
6265
+ `Could not read how its last activation ended (${error.status ?? "no answer"}); \`tonbo machine activations ${name}\` lists them.`
5901
6266
  );
5902
6267
  return null;
5903
6268
  }
@@ -6226,7 +6591,7 @@ var TOPICS = [
6226
6591
  title: "The login, accounts and keys",
6227
6592
  text: [
6228
6593
  "Log in once per computer: `tonbo login` signs this computer in as you, and every account you belong to is reachable with `--account`, with your role in each. Joining an organization needs no new login, and leaving one ends this computer's access to it at once. A command acts in one account: `--account`, else `TONBO_ACCOUNT`, else the default `tonbo account use` set, else your personal account. For each request the CLI exchanges the login for a short-lived token of that account; what a command may do is what your role there allows, and the CLI never decides it.",
6229
- "`TONBO_API_KEY`, when it is set, is used instead: an API key reaches only its own account and is sent as the Bearer credential, as narrowed by the key. Agents and CI should use an Agent key or an API key of one account, never your login, which reaches every account you belong to.",
6594
+ "`TONBO_API_KEY`, when it is set, is used instead: an API key reaches only its own account, as narrowed by the key. A personal key is sent as the Bearer credential. An Agent key, which the API never accepts directly, is exchanged at `POST https://tonbo.dev/api/iam/token` for a token of at most 10 minutes, kept in memory and exchanged again shortly before it expires; neither the key nor the token is ever printed. Agents and CI should use an Agent key or an API key of one account, never your login, which reaches every account you belong to.",
6230
6595
  "The login is saved at `${XDG_CONFIG_HOME:-$HOME/.config}/tonbo/login.json`, which `yield` shares: either CLI's `login` signs both in, and either `logout` signs both out. It works only on the computer that logged in, ends after 30 days unused or 180 days at most, and is listed and revoked on the console's Sign-in sessions page. The CLI replaces the file atomically with owner-only permissions and refuses a symlink or a file other users can read."
6231
6596
  ]
6232
6597
  },
@@ -6272,7 +6637,7 @@ var COMMAND_HELP = {
6272
6637
  },
6273
6638
  "tonbo logout": {
6274
6639
  about: [
6275
- "Ends this computer's login for every account and removes the file, which signs `yield` out too. A login an earlier release saved (one key per account in `credentials.json`) has each key revoked as well; one already revoked or expired is skipped. If a revocation fails, the files are still removed, and the CLI says what it could not revoke and exits 1, so you can revoke it on the console's Sign-in sessions page or under API keys. `TONBO_API_KEY` is never touched."
6640
+ "Ends this computer's login for every account and removes the file, which signs `yield` out too. Before anything is revoked, Yield's capture on this computer stops until the next login of either CLI, and nothing written meanwhile is ever recorded; when it cannot write Yield's folder to stop it, nothing is logged out. A login an earlier release saved (one key per account in `credentials.json`) has each key revoked as well; one already revoked or expired is skipped. If a revocation fails, the files are still removed, and the CLI says what it could not revoke and exits 1, so you can revoke it on the console's Sign-in sessions page or under API keys. `TONBO_API_KEY` is never touched."
6276
6641
  ],
6277
6642
  examples: ["tonbo logout"]
6278
6643
  },
@@ -6889,19 +7254,19 @@ function optionDescription(option) {
6889
7254
  function applyHelp(program) {
6890
7255
  const named = /* @__PURE__ */ new Set();
6891
7256
  const walk = (command, words) => {
6892
- const path4 = words.join(" ");
7257
+ const path5 = words.join(" ");
6893
7258
  command.configureHelp({ ...command.configureHelp(), optionDescription });
6894
- const help = COMMAND_HELP[path4];
7259
+ const help = COMMAND_HELP[path5];
6895
7260
  if (help) {
6896
- named.add(path4);
7261
+ named.add(path5);
6897
7262
  for (const [name, text] of Object.entries(help.arguments ?? {})) {
6898
7263
  const argument = command.registeredArguments.find((entry) => entry.name() === name);
6899
- if (!argument) throw new Error(`${path4} has no argument ${name}`);
7264
+ if (!argument) throw new Error(`${path5} has no argument ${name}`);
6900
7265
  argument.description = text;
6901
7266
  }
6902
7267
  for (const [flag, text] of Object.entries(help.defaults ?? {})) {
6903
7268
  const option = command.options.find((entry) => entry.long === flag);
6904
- if (!option) throw new Error(`${path4} has no option ${flag}`);
7269
+ if (!option) throw new Error(`${path5} has no option ${flag}`);
6905
7270
  option.defaultValueDescription = text;
6906
7271
  }
6907
7272
  command.addHelpText("after", commandHelpText(help));
@@ -6909,15 +7274,15 @@ function applyHelp(program) {
6909
7274
  for (const child of command.commands) walk(child, [...words, child.name()]);
6910
7275
  };
6911
7276
  walk(program, [program.name()]);
6912
- for (const path4 of Object.keys(COMMAND_HELP))
6913
- if (!named.has(path4)) throw new Error(`The help of ${path4} names no command`);
7277
+ for (const path5 of Object.keys(COMMAND_HELP))
7278
+ if (!named.has(path5)) throw new Error(`The help of ${path5} names no command`);
6914
7279
  program.addHelpText("after", programHelpText());
6915
7280
  return program;
6916
7281
  }
6917
7282
 
6918
7283
  // src/machine-cell.ts
6919
- import { readFile as readFile3, writeFile } from "node:fs/promises";
6920
- import { randomUUID as randomUUID7 } from "node:crypto";
7284
+ import { readFile as readFile4, writeFile } from "node:fs/promises";
7285
+ import { randomUUID as randomUUID8 } from "node:crypto";
6921
7286
  var EXEC_EXIT_CODES = {
6922
7287
  timeout: 124,
6923
7288
  output_limit: 125,
@@ -7102,7 +7467,7 @@ async function machineProcessStartCommand(deps, name, command, options = {}) {
7102
7467
  params: { name },
7103
7468
  headers: {
7104
7469
  "content-type": "application/json",
7105
- "idempotency-key": options.idempotencyKey ?? randomUUID7()
7470
+ "idempotency-key": options.idempotencyKey ?? randomUUID8()
7106
7471
  },
7107
7472
  body: JSON.stringify(body)
7108
7473
  });
@@ -7255,13 +7620,13 @@ async function machineProcessSummary(cell, key, region, name) {
7255
7620
  return null;
7256
7621
  }
7257
7622
  }
7258
- async function machineFileReadCommand(deps, name, path4, options = {}) {
7623
+ async function machineFileReadCommand(deps, name, path5, options = {}) {
7259
7624
  try {
7260
7625
  const key = await deps.auth.apiKey();
7261
7626
  const region = await regionOf(deps, key, name);
7262
7627
  const response = await cellApi(deps).fetch("readMachineFile", key, region, {
7263
7628
  params: { name },
7264
- query: { path: path4 },
7629
+ query: { path: path5 },
7265
7630
  headers: { range: options.range, "tonbo-expect-activation": options.expectActivation }
7266
7631
  });
7267
7632
  const bytes2 = new Uint8Array(await response.arrayBuffer());
@@ -7271,21 +7636,21 @@ async function machineFileReadCommand(deps, name, path4, options = {}) {
7271
7636
  throw explainMachineError(error);
7272
7637
  }
7273
7638
  }
7274
- async function machineFileWriteCommand(deps, name, path4, options = {}) {
7639
+ async function machineFileWriteCommand(deps, name, path5, options = {}) {
7275
7640
  try {
7276
- const bytes2 = options.from ? new Uint8Array(await readFile3(options.from)) : await readAllStdin(deps);
7641
+ const bytes2 = options.from ? new Uint8Array(await readFile4(options.from)) : await readAllStdin(deps);
7277
7642
  const key = await deps.auth.apiKey();
7278
7643
  const region = await regionOf(deps, key, name);
7279
7644
  const written = await cellApi(deps).json("writeMachineFile", key, region, {
7280
7645
  params: { name },
7281
- query: { path: path4 },
7646
+ query: { path: path5 },
7282
7647
  headers: {
7283
7648
  "content-type": "application/octet-stream",
7284
7649
  "if-match": options.ifMatch,
7285
7650
  "if-none-match": options.create ? "*" : void 0,
7286
7651
  "tonbo-file-mode": options.mode,
7287
7652
  "tonbo-expect-activation": options.expectActivation,
7288
- "idempotency-key": randomUUID7()
7653
+ "idempotency-key": randomUUID8()
7289
7654
  },
7290
7655
  body: new Uint8Array(bytes2)
7291
7656
  });
@@ -7300,28 +7665,28 @@ async function machineFileWriteCommand(deps, name, path4, options = {}) {
7300
7665
  throw explainMachineError(error);
7301
7666
  }
7302
7667
  }
7303
- async function machineFileStatCommand(deps, name, path4, options = {}) {
7668
+ async function machineFileStatCommand(deps, name, path5, options = {}) {
7304
7669
  try {
7305
7670
  const key = await deps.auth.apiKey();
7306
7671
  const region = await regionOf(deps, key, name);
7307
- const stat = await cellApi(deps).json(
7672
+ const stat2 = await cellApi(deps).json(
7308
7673
  "statMachineFile",
7309
7674
  key,
7310
7675
  region,
7311
7676
  {
7312
7677
  params: { name },
7313
- query: { path: path4 },
7678
+ query: { path: path5 },
7314
7679
  headers: { "tonbo-expect-activation": options.expectActivation }
7315
7680
  }
7316
7681
  );
7317
- noticeWake(deps, stat.machine, name);
7682
+ noticeWake(deps, stat2.machine, name);
7318
7683
  print(
7319
7684
  deps,
7320
7685
  options.json,
7321
- stat,
7686
+ stat2,
7322
7687
  fields(
7323
7688
  ["path", "type", "size", "mode", "uid", "gid", "mtime", "etag", "target", "durability"].map(
7324
- (field) => [field, cellText(stat[field])]
7689
+ (field) => [field, cellText(stat2[field])]
7325
7690
  )
7326
7691
  )
7327
7692
  );
@@ -7329,13 +7694,13 @@ async function machineFileStatCommand(deps, name, path4, options = {}) {
7329
7694
  throw explainMachineError(error);
7330
7695
  }
7331
7696
  }
7332
- async function machineFileListCommand(deps, name, path4, options = {}) {
7697
+ async function machineFileListCommand(deps, name, path5, options = {}) {
7333
7698
  try {
7334
7699
  const key = await deps.auth.apiKey();
7335
7700
  const region = await regionOf(deps, key, name);
7336
7701
  const listing = await cellApi(deps).json("listMachineFiles", key, region, {
7337
7702
  params: { name },
7338
- query: { path: path4 },
7703
+ query: { path: path5 },
7339
7704
  headers: { "tonbo-expect-activation": options.expectActivation }
7340
7705
  });
7341
7706
  noticeWake(deps, listing.machine, name);
@@ -7359,19 +7724,19 @@ async function machineFileListCommand(deps, name, path4, options = {}) {
7359
7724
  throw explainMachineError(error);
7360
7725
  }
7361
7726
  }
7362
- async function machineFileDeleteCommand(deps, name, path4, options = {}) {
7727
+ async function machineFileDeleteCommand(deps, name, path5, options = {}) {
7363
7728
  try {
7364
7729
  const key = await deps.auth.apiKey();
7365
7730
  const region = await regionOf(deps, key, name);
7366
7731
  await cellApi(deps).fetch("deleteMachineFile", key, region, {
7367
7732
  params: { name },
7368
- query: { path: path4, recursive: options.recursive ? "true" : void 0 },
7733
+ query: { path: path5, recursive: options.recursive ? "true" : void 0 },
7369
7734
  headers: {
7370
7735
  "tonbo-expect-activation": options.expectActivation,
7371
- "idempotency-key": randomUUID7()
7736
+ "idempotency-key": randomUUID8()
7372
7737
  }
7373
7738
  });
7374
- print(deps, options.json, { path: path4 }, `Deleted ${path4}.`);
7739
+ print(deps, options.json, { path: path5 }, `Deleted ${path5}.`);
7375
7740
  } catch (error) {
7376
7741
  throw explainMachineError(error);
7377
7742
  }
@@ -7474,8 +7839,8 @@ async function terminalAsk(question) {
7474
7839
 
7475
7840
  // src/ssh.ts
7476
7841
  import { spawn, spawnSync } from "node:child_process";
7477
- import { existsSync } from "node:fs";
7478
- import { chmod as chmod2, mkdir as mkdir3, readFile as readFile4, readdir, rename as rename3, writeFile as writeFile2 } from "node:fs/promises";
7842
+ import { existsSync as existsSync2 } from "node:fs";
7843
+ import { chmod as chmod2, mkdir as mkdir4, readFile as readFile5, readdir, rename as rename4, writeFile as writeFile2 } from "node:fs/promises";
7479
7844
  import { homedir } from "node:os";
7480
7845
  import { join } from "node:path";
7481
7846
  function sshDirectory(env = process.env) {
@@ -7499,9 +7864,9 @@ function cellOrigin2(host) {
7499
7864
  }
7500
7865
  async function ensureKey(directory) {
7501
7866
  const key = join(directory, "id_ed25519");
7502
- await mkdir3(directory, { recursive: true, mode: 448 });
7867
+ await mkdir4(directory, { recursive: true, mode: 448 });
7503
7868
  await chmod2(directory, 448);
7504
- if (!existsSync(key)) {
7869
+ if (!existsSync2(key)) {
7505
7870
  const made = spawnSync(
7506
7871
  "ssh-keygen",
7507
7872
  ["-q", "-t", "ed25519", "-N", "", "-C", "tonbo", "-f", key],
@@ -7517,10 +7882,10 @@ async function ensureKey(directory) {
7517
7882
  }
7518
7883
  return key;
7519
7884
  }
7520
- async function writeAtomic(path4, data) {
7521
- const temporary = `${path4}.${process.pid}.tmp`;
7885
+ async function writeAtomic(path5, data) {
7886
+ const temporary = `${path5}.${process.pid}.tmp`;
7522
7887
  await writeFile2(temporary, data, { mode: 384 });
7523
- await rename3(temporary, path4);
7888
+ await rename4(temporary, path5);
7524
7889
  }
7525
7890
  function certificatePath(directory, user) {
7526
7891
  return join(directory, `${user}-cert.pub`);
@@ -7533,7 +7898,7 @@ function hostCAPath(directory, host) {
7533
7898
  }
7534
7899
  async function trustHostCA(directory, host, lines) {
7535
7900
  const cells = join(directory, "host-ca");
7536
- await mkdir3(cells, { recursive: true, mode: 448 });
7901
+ await mkdir4(cells, { recursive: true, mode: 448 });
7537
7902
  await writeAtomic(hostCAPath(directory, host), `${lines.join("\n")}
7538
7903
  `);
7539
7904
  const trusted = /* @__PURE__ */ new Set();
@@ -7541,7 +7906,7 @@ async function trustHostCA(directory, host, lines) {
7541
7906
  if (name.endsWith(".tmp")) {
7542
7907
  continue;
7543
7908
  }
7544
- for (const line of (await readFile4(join(cells, name), "utf8")).split("\n")) {
7909
+ for (const line of (await readFile5(join(cells, name), "utf8")).split("\n")) {
7545
7910
  if (line.trim() !== "") {
7546
7911
  trusted.add(line.trim());
7547
7912
  }
@@ -7553,7 +7918,7 @@ async function trustHostCA(directory, host, lines) {
7553
7918
  async function heldUntil(directory, user) {
7554
7919
  try {
7555
7920
  const sidecar = JSON.parse(
7556
- await readFile4(`${certificatePath(directory, user)}.json`, "utf8")
7921
+ await readFile5(`${certificatePath(directory, user)}.json`, "utf8")
7557
7922
  );
7558
7923
  return sidecar.valid_before ? Date.parse(sidecar.valid_before) : 0;
7559
7924
  } catch {
@@ -7572,11 +7937,9 @@ async function cellRequest(bearer, method, url, body) {
7572
7937
  },
7573
7938
  body: body === void 0 ? void 0 : JSON.stringify(body)
7574
7939
  });
7575
- if (response.status === 401 && attempt === 0 && isAccountBearer(bearer)) {
7576
- bearer.invalidate(MANAGEMENT_AUDIENCE);
7577
- continue;
7578
- }
7579
7940
  const answer = await response.json().catch(() => null);
7941
+ if (response.status === 401 && attempt === 0 && retriesAfterRefusal(bearer, MANAGEMENT_AUDIENCE, answer?.code ?? null))
7942
+ continue;
7580
7943
  if (!response.ok) {
7581
7944
  throw new Error(
7582
7945
  `${url} answered ${response.status}${answer?.code ? ` ${answer.code}` : ""}${answer?.detail ? `: ${answer.detail}` : ""}`
@@ -7589,11 +7952,11 @@ async function refreshCertificate(bearer, destination, now = Date.now, directory
7589
7952
  const key = await ensureKey(directory);
7590
7953
  const certificate = certificatePath(directory, destination.user);
7591
7954
  const knownHosts = join(directory, "known_hosts");
7592
- if (await heldUntil(directory, destination.user) - now() > RENEW_BEFORE_MS && existsSync(hostCAPath(directory, destination.host)) && existsSync(knownHosts)) {
7955
+ if (await heldUntil(directory, destination.user) - now() > RENEW_BEFORE_MS && existsSync2(hostCAPath(directory, destination.host)) && existsSync2(knownHosts)) {
7593
7956
  return { key, certificate, knownHosts };
7594
7957
  }
7595
7958
  const origin = cellOrigin2(destination.host);
7596
- const publicKey = (await readFile4(`${key}.pub`, "utf8")).trim();
7959
+ const publicKey = (await readFile5(`${key}.pub`, "utf8")).trim();
7597
7960
  const issued = await cellRequest(bearer, "POST", `${origin}/v1/ssh/certificates`, {
7598
7961
  public_key: publicKey,
7599
7962
  ttl_seconds: REQUESTED_TTL_SECONDS
@@ -7685,7 +8048,7 @@ async function sshHostCaCommand(deps, options) {
7685
8048
  print(deps, options.json, data, data.known_hosts.join("\n"));
7686
8049
  }
7687
8050
  async function sshCertificateIssueCommand(deps, options) {
7688
- const publicKey = (await readFile4(options.publicKey, "utf8")).trim();
8051
+ const publicKey = (await readFile5(options.publicKey, "utf8")).trim();
7689
8052
  const key = await deps.auth.apiKey();
7690
8053
  const data = await (deps.cell ?? new CellApi(fetch)).json(
7691
8054
  "issueSshCertificate",
@@ -7762,7 +8125,7 @@ var repeated = (value, previous = []) => [...previous, value];
7762
8125
  var budgetOption = (whom) => new Option(
7763
8126
  "--budget <budget>",
7764
8127
  `put ${whom} under a monthly budget: an amount in dollars such as 50 creates one, a budget's name adds to it`
7765
- ).hideHelp();
8128
+ );
7766
8129
  var roleOption = (description) => new Option("--role <role>", description).choices(accountRoles);
7767
8130
  function machineSpecOptions(command, update) {
7768
8131
  const later = update ? "; applies at the next boot" : "";
@@ -7828,7 +8191,9 @@ function createProgram(makeDependencies = createDependencies) {
7828
8191
  const account2 = program.opts().account;
7829
8192
  await loginCommand(dependencies(), { ...options, account: account2 });
7830
8193
  });
7831
- program.command("logout").description("end this computer's CLI login and remove the saved login").action(async () => logoutCommand(dependencies()));
8194
+ program.command("logout").description(
8195
+ "end this computer's CLI login and remove the saved login; Yield's capture here stops until the next login"
8196
+ ).action(async () => logoutCommand(dependencies()));
7832
8197
  const account = program.command("account").description("the accounts you belong to and the one commands act in");
7833
8198
  operationOptions(
7834
8199
  account.command("show").description("show the account, your role in it and its limits"),
@@ -7919,7 +8284,7 @@ function createProgram(makeDependencies = createDependencies) {
7919
8284
  ).action(
7920
8285
  async (keyId, options) => apiKeyRevokeCommand(dependencies(), keyId, options)
7921
8286
  );
7922
- const budget = program.command("budget", { hidden: true }).description("the account's monthly spend budgets");
8287
+ const budget = program.command("budget").description("the account's monthly spend budgets");
7923
8288
  operationOptions(
7924
8289
  budget.command("list").description("list the budgets, the account budget first"),
7925
8290
  "listBudgets"
@@ -8225,31 +8590,31 @@ function createProgram(makeDependencies = createDependencies) {
8225
8590
  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"),
8226
8591
  "readMachineFile"
8227
8592
  ).action(
8228
- async (name, path4, options) => machineFileReadCommand(dependencies(), name, path4, options)
8593
+ async (name, path5, options) => machineFileReadCommand(dependencies(), name, path5, options)
8229
8594
  );
8230
8595
  operationOptions(
8231
8596
  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"),
8232
8597
  "writeMachineFile"
8233
8598
  ).action(
8234
- async (name, path4, options) => machineFileWriteCommand(dependencies(), name, path4, options)
8599
+ async (name, path5, options) => machineFileWriteCommand(dependencies(), name, path5, options)
8235
8600
  );
8236
8601
  operationOptions(
8237
8602
  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"),
8238
8603
  "statMachineFile"
8239
8604
  ).action(
8240
- async (name, path4, options) => machineFileStatCommand(dependencies(), name, path4, options)
8605
+ async (name, path5, options) => machineFileStatCommand(dependencies(), name, path5, options)
8241
8606
  );
8242
8607
  operationOptions(
8243
8608
  file.command("list <name> <path>").description("list a directory of a Machine").option("--expect-activation <activation>", "list only in this activation"),
8244
8609
  "listMachineFiles"
8245
8610
  ).action(
8246
- async (name, path4, options) => machineFileListCommand(dependencies(), name, path4, options)
8611
+ async (name, path5, options) => machineFileListCommand(dependencies(), name, path5, options)
8247
8612
  );
8248
8613
  operationOptions(
8249
8614
  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"),
8250
8615
  "deleteMachineFile"
8251
8616
  ).action(
8252
- async (name, path4, options) => machineFileDeleteCommand(dependencies(), name, path4, options)
8617
+ async (name, path5, options) => machineFileDeleteCommand(dependencies(), name, path5, options)
8253
8618
  );
8254
8619
  const hold = machine.command("hold").description("keep a Machine awake for work that has no open connection");
8255
8620
  operationOptions(