@tonbo/cli 0.15.0 → 0.17.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",
@@ -344,7 +380,8 @@ var managementOperations = {
344
380
  "path": "/sessions/store",
345
381
  "pathParams": [],
346
382
  "query": [
347
- "capture"
383
+ "capture",
384
+ "project"
348
385
  ],
349
386
  "idempotencyKey": false
350
387
  },
@@ -365,6 +402,7 @@ var managementOperations = {
365
402
  "pathParams": [],
366
403
  "query": [
367
404
  "origin",
405
+ "scope",
368
406
  "cursor",
369
407
  "limit"
370
408
  ],
@@ -397,142 +435,30 @@ var managementOperations = {
397
435
  "query": [],
398
436
  "idempotencyKey": false
399
437
  },
400
- "latestPersonalHandoffResult": {
438
+ "listProjectPeople": {
401
439
  "method": "GET",
402
- "path": "/yield/sessions/{sessionId}/latest-result",
440
+ "path": "/projects/{project_id}/people",
403
441
  "pathParams": [
404
- "sessionId"
442
+ "project_id"
405
443
  ],
406
444
  "query": [],
407
445
  "idempotencyKey": false
408
446
  },
409
- "listHandoffs": {
410
- "method": "GET",
411
- "path": "/yield/handoffs",
412
- "pathParams": [],
413
- "query": [],
414
- "idempotencyKey": false
415
- },
416
- "createPersonalHandoff": {
447
+ "inviteProjectPerson": {
417
448
  "method": "POST",
418
- "path": "/yield/handoffs",
419
- "pathParams": [],
420
- "query": [],
421
- "idempotencyKey": false
422
- },
423
- "listHandoffEvents": {
424
- "method": "GET",
425
- "path": "/yield/handoffs/events",
426
- "pathParams": [],
427
- "query": [
428
- "after",
429
- "wait",
430
- "include"
431
- ],
432
- "idempotencyKey": false
433
- },
434
- "listTeamHandoffs": {
435
- "method": "GET",
436
- "path": "/yield/handoffs/team",
437
- "pathParams": [],
438
- "query": [],
439
- "idempotencyKey": false
440
- },
441
- "showPersonalHandoff": {
442
- "method": "GET",
443
- "path": "/yield/handoffs/{id}",
444
- "pathParams": [
445
- "id"
446
- ],
447
- "query": [],
448
- "idempotencyKey": false
449
- },
450
- "takePersonalHandoff": {
451
- "method": "POST",
452
- "path": "/yield/handoffs/{id}/take",
453
- "pathParams": [
454
- "id"
455
- ],
456
- "query": [],
457
- "idempotencyKey": false
458
- },
459
- "markPersonalHandoffRestored": {
460
- "method": "POST",
461
- "path": "/yield/handoffs/{id}/restored",
462
- "pathParams": [
463
- "id"
464
- ],
465
- "query": [],
466
- "idempotencyKey": false
467
- },
468
- "startPersonalHandoff": {
469
- "method": "POST",
470
- "path": "/yield/handoffs/{id}/started",
449
+ "path": "/projects/{project_id}/people",
471
450
  "pathParams": [
472
- "id"
451
+ "project_id"
473
452
  ],
474
453
  "query": [],
475
454
  "idempotencyKey": false
476
455
  },
477
- "needHumanPersonalHandoff": {
478
- "method": "POST",
479
- "path": "/yield/handoffs/{id}/need-human",
480
- "pathParams": [
481
- "id"
482
- ],
483
- "query": [],
484
- "idempotencyKey": false
485
- },
486
- "cancelPersonalHandoff": {
487
- "method": "POST",
488
- "path": "/yield/handoffs/{id}/cancel",
489
- "pathParams": [
490
- "id"
491
- ],
492
- "query": [],
493
- "idempotencyKey": false
494
- },
495
- "renewPersonalHandoffSource": {
496
- "method": "POST",
497
- "path": "/yield/handoffs/{id}/renew-source",
498
- "pathParams": [
499
- "id"
500
- ],
501
- "query": [],
502
- "idempotencyKey": false
503
- },
504
- "finishPersonalHandoff": {
505
- "method": "POST",
506
- "path": "/yield/handoffs/{id}/done",
507
- "pathParams": [
508
- "id"
509
- ],
510
- "query": [],
511
- "idempotencyKey": false
512
- },
513
- "pullPersonalHandoff": {
514
- "method": "GET",
515
- "path": "/yield/handoffs/{id}/result",
516
- "pathParams": [
517
- "id"
518
- ],
519
- "query": [],
520
- "idempotencyKey": false
521
- },
522
- "markPersonalHandoffPulled": {
523
- "method": "POST",
524
- "path": "/yield/handoffs/{id}/pulled",
525
- "pathParams": [
526
- "id"
527
- ],
528
- "query": [],
529
- "idempotencyKey": false
530
- },
531
- "renewPersonalHandoffResult": {
532
- "method": "POST",
533
- "path": "/yield/handoffs/{id}/renew-result",
456
+ "removeProjectPerson": {
457
+ "method": "DELETE",
458
+ "path": "/projects/{project_id}/people/{person}",
534
459
  "pathParams": [
535
- "id"
460
+ "project_id",
461
+ "person"
536
462
  ],
537
463
  "query": [],
538
464
  "idempotencyKey": false
@@ -607,68 +533,6 @@ var managementOperations = {
607
533
  "query": [],
608
534
  "idempotencyKey": false
609
535
  },
610
- "createYieldPickup": {
611
- "method": "POST",
612
- "path": "/yield/pickups",
613
- "pathParams": [],
614
- "query": [],
615
- "idempotencyKey": false
616
- },
617
- "listYieldQuestions": {
618
- "method": "GET",
619
- "path": "/yield/questions",
620
- "pathParams": [],
621
- "query": [
622
- "team",
623
- "limit"
624
- ],
625
- "idempotencyKey": false
626
- },
627
- "createYieldQuestion": {
628
- "method": "POST",
629
- "path": "/yield/questions",
630
- "pathParams": [],
631
- "query": [],
632
- "idempotencyKey": false
633
- },
634
- "showYieldQuestion": {
635
- "method": "GET",
636
- "path": "/yield/questions/{id}",
637
- "pathParams": [
638
- "id"
639
- ],
640
- "query": [],
641
- "idempotencyKey": false
642
- },
643
- "replyYieldQuestion": {
644
- "method": "POST",
645
- "path": "/yield/questions/{id}/replies",
646
- "pathParams": [
647
- "id"
648
- ],
649
- "query": [],
650
- "idempotencyKey": false
651
- },
652
- "closeYieldQuestion": {
653
- "method": "POST",
654
- "path": "/yield/questions/{id}/close",
655
- "pathParams": [
656
- "id"
657
- ],
658
- "query": [],
659
- "idempotencyKey": false
660
- },
661
- "listYieldDecisions": {
662
- "method": "GET",
663
- "path": "/yield/decisions",
664
- "pathParams": [],
665
- "query": [
666
- "project",
667
- "path",
668
- "limit"
669
- ],
670
- "idempotencyKey": false
671
- },
672
536
  "listDatabases": {
673
537
  "method": "GET",
674
538
  "path": "/databases",
@@ -1179,42 +1043,6 @@ var managementOperations = {
1179
1043
  }
1180
1044
  };
1181
1045
  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
1046
  "getMachineActivation": {
1219
1047
  "method": "GET",
1220
1048
  "path": "/machines/{name}/activations/{activation}",
@@ -1983,19 +1811,29 @@ var MANAGEMENT_AUDIENCE = cliSessionContract.audiences.management;
1983
1811
  function isAccountBearer(bearer) {
1984
1812
  return typeof bearer !== "string";
1985
1813
  }
1814
+ function retriesAfterRefusal(bearer, audience, code) {
1815
+ if (!isAccountBearer(bearer))
1816
+ return false;
1817
+ if (bearer.kind === "key")
1818
+ return bearer.refused(audience, code);
1819
+ bearer.invalidate(audience);
1820
+ return true;
1821
+ }
1986
1822
  function managementBearer(bearer) {
1987
1823
  return isAccountBearer(bearer) ? bearer.token(MANAGEMENT_AUDIENCE) : Promise.resolve(bearer);
1988
1824
  }
1989
1825
 
1990
1826
  // ../../packages/tonbo-cli-core/dist/src/access-token.js
1991
1827
  var DEFAULT_LOGIN_HINT = credentialsContract.reader.login_hint;
1828
+ var STREAMS_AUDIENCE = "https://streams.tonbo.dev";
1992
1829
  var GIT_AUDIENCE = "https://git.tonbo.dev";
1830
+ var REFRESH_MARGIN_MS = 6e4;
1993
1831
  var AccessTokenError = class extends Error {
1994
1832
  status;
1995
1833
  retryAfterMs;
1996
1834
  code;
1997
- constructor(message, status, retryAfterMs = null, code = null) {
1998
- super(message);
1835
+ constructor(message, status, retryAfterMs = null, code = null, options) {
1836
+ super(message, options);
1999
1837
  this.status = status;
2000
1838
  this.retryAfterMs = retryAfterMs;
2001
1839
  this.code = code;
@@ -2004,6 +1842,116 @@ var AccessTokenError = class extends Error {
2004
1842
  return this.status === null || this.status === 429 || this.status >= 500;
2005
1843
  }
2006
1844
  };
1845
+ var AccessTokens = class {
1846
+ fetcher;
1847
+ userOrigin;
1848
+ audience;
1849
+ loginHint;
1850
+ now;
1851
+ cached = null;
1852
+ /** The account bearer the last token was of, which `invalidate` forgets. */
1853
+ last = null;
1854
+ constructor(fetcher, userOrigin, audience = STREAMS_AUDIENCE, loginHint = DEFAULT_LOGIN_HINT, now = () => Date.now()) {
1855
+ this.fetcher = fetcher;
1856
+ this.userOrigin = userOrigin;
1857
+ this.audience = audience;
1858
+ this.loginHint = loginHint;
1859
+ this.now = now;
1860
+ }
1861
+ async token(key) {
1862
+ if (isAccountBearer(key)) {
1863
+ this.last = key;
1864
+ return key.token(this.audience);
1865
+ }
1866
+ if (this.cached && this.cached.key === key && this.cached.refreshAt > this.now())
1867
+ return this.cached.token;
1868
+ const issuedAt = this.now();
1869
+ const what = this.audience === GIT_AUDIENCE ? "git" : this.audience === MANAGEMENT_AUDIENCE ? "Management" : "Streams";
1870
+ let response;
1871
+ try {
1872
+ response = await this.fetcher(`${this.userOrigin}/api/iam/token`, {
1873
+ method: "POST",
1874
+ redirect: "manual",
1875
+ signal: AbortSignal.timeout(3e4),
1876
+ headers: { authorization: `Bearer ${key}`, "content-type": "application/json" },
1877
+ body: JSON.stringify({ audience: this.audience })
1878
+ });
1879
+ } catch (error) {
1880
+ throw new AccessTokenError(`The ${what} token exchange failed: ${error instanceof Error ? error.message : String(error)}`, null, null, null, { cause: error });
1881
+ }
1882
+ if (!response.ok) {
1883
+ const problem = await response.json().catch(() => null);
1884
+ const retryAfter = Number(response.headers.get("retry-after") ?? "");
1885
+ 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);
1886
+ }
1887
+ const body = await response.json().catch(() => null);
1888
+ if (typeof body?.access_token !== "string" || !body.access_token || typeof body.expires_in !== "number" || !(body.expires_in > 0))
1889
+ throw new AccessTokenError(`The ${what} token exchange answered no token.`, response.status);
1890
+ this.cached = {
1891
+ key,
1892
+ token: body.access_token,
1893
+ refreshAt: issuedAt + Math.max(0, body.expires_in * 1e3 - REFRESH_MARGIN_MS)
1894
+ };
1895
+ return body.access_token;
1896
+ }
1897
+ /** Forgets the token, after the resource server refused it. */
1898
+ invalidate() {
1899
+ this.cached = null;
1900
+ this.last?.invalidate(this.audience);
1901
+ }
1902
+ /** Sends a request with the token; a 401 drops the token and sends once
1903
+ * more with a newly exchanged one, whose answer is final. */
1904
+ async send(key, request) {
1905
+ const response = await request(await this.token(key));
1906
+ if (response.status !== 401)
1907
+ return response;
1908
+ await response.body?.cancel();
1909
+ if (isAccountBearer(key))
1910
+ key.invalidate(this.audience);
1911
+ else
1912
+ this.invalidate();
1913
+ return request(await this.token(key));
1914
+ }
1915
+ };
1916
+ function apiKeyBearer(key, fetcher, userOrigin, now = () => Date.now()) {
1917
+ const tokens = /* @__PURE__ */ new Map();
1918
+ let exchanging = false;
1919
+ const tokensOf = (audience) => {
1920
+ let found = tokens.get(audience);
1921
+ if (!found) {
1922
+ found = new AccessTokens(fetcher, userOrigin, audience, void 0, now);
1923
+ tokens.set(audience, found);
1924
+ }
1925
+ return found;
1926
+ };
1927
+ return {
1928
+ kind: "key",
1929
+ async token(audience) {
1930
+ if (audience === MANAGEMENT_AUDIENCE && !exchanging)
1931
+ return key;
1932
+ try {
1933
+ return await tokensOf(audience).token(key);
1934
+ } catch (error) {
1935
+ if (!(error instanceof AccessTokenError) || error.status !== 401)
1936
+ throw error;
1937
+ 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);
1938
+ }
1939
+ },
1940
+ invalidate(audience) {
1941
+ tokens.get(audience)?.invalidate();
1942
+ },
1943
+ refused(audience, code) {
1944
+ if (audience === MANAGEMENT_AUDIENCE && !exchanging) {
1945
+ if (code !== "exchange_required")
1946
+ return false;
1947
+ exchanging = true;
1948
+ return true;
1949
+ }
1950
+ tokens.get(audience)?.invalidate();
1951
+ return true;
1952
+ }
1953
+ };
1954
+ }
2007
1955
 
2008
1956
  // ../../packages/tonbo-cli-core/dist/src/http.js
2009
1957
  var HttpError = class extends Error {
@@ -2029,11 +1977,11 @@ async function requestJson(fetcher, url, init = {}) {
2029
1977
  if (response.ok)
2030
1978
  return body;
2031
1979
  const record = body && typeof body === "object" ? body : {};
2032
- const reason = [record.detail, record.title, record.error].find((value) => typeof value === "string" && value.length > 0);
1980
+ const reason2 = [record.detail, record.title, record.error].find((value) => typeof value === "string" && value.length > 0);
2033
1981
  const requestId = response.headers.get("x-request-id") ?? (typeof record.request_id === "string" ? record.request_id : null);
2034
1982
  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
1983
  const message = [
2036
- reason ?? `Request failed with HTTP ${response.status}.`,
1984
+ reason2 ?? `Request failed with HTTP ${response.status}.`,
2037
1985
  ...issues.length ? [`(${issues.join(", ")})`] : []
2038
1986
  ].join(" ");
2039
1987
  throw new HttpError(requestId ? `${message} (request ${requestId})` : message, response.status, body, requestId);
@@ -2051,9 +1999,9 @@ async function presented(key) {
2051
1999
  try {
2052
2000
  return await managementBearer(key);
2053
2001
  } catch (error) {
2054
- if (!(error instanceof AccessTokenError))
2002
+ if (!(error instanceof AccessTokenError) || error.status === null)
2055
2003
  throw error;
2056
- throw new ExchangeRefusal(error.message, error.status ?? 503, { code: error.code }, null);
2004
+ throw new ExchangeRefusal(error.message, error.status, { code: error.code }, null);
2057
2005
  }
2058
2006
  }
2059
2007
  var ManagementApi = class {
@@ -2081,15 +2029,14 @@ var ManagementApi = class {
2081
2029
  try {
2082
2030
  return await attempt();
2083
2031
  } catch (error) {
2084
- if (!isAccountBearer(key) || !(error instanceof HttpError) || error.status !== 401 || error instanceof ExchangeRefusal)
2032
+ if (!(error instanceof HttpError) || error.status !== 401 || error instanceof ExchangeRefusal || !retriesAfterRefusal(key, MANAGEMENT_AUDIENCE, error.code))
2085
2033
  throw error;
2086
- key.invalidate(MANAGEMENT_AUDIENCE);
2087
2034
  return attempt();
2088
2035
  }
2089
2036
  }
2090
2037
  async requestWith(operationId, key, { params = {}, query = {}, body, idempotencyKey, ifMatch }, seen) {
2091
2038
  const operation = managementOperation(operationId);
2092
- const path4 = operation.path.replace(/\{([^}]+)\}/g, (_, name) => {
2039
+ const path5 = operation.path.replace(/\{([^}]+)\}/g, (_, name) => {
2093
2040
  const value = params[name];
2094
2041
  if (value === void 0)
2095
2042
  throw new Error(`${operationId} needs the path parameter ${name}.`);
@@ -2113,7 +2060,7 @@ var ManagementApi = class {
2113
2060
  if (ifMatch)
2114
2061
  headers["if-match"] = ifMatch;
2115
2062
  const queryString = search.toString();
2116
- const url = `${this.apiOrigin}${managementApiBasePath}${path4}${queryString ? `?${queryString}` : ""}`;
2063
+ const url = `${this.apiOrigin}${managementApiBasePath}${path5}${queryString ? `?${queryString}` : ""}`;
2117
2064
  try {
2118
2065
  const fetcher = seen ? async (input, init) => {
2119
2066
  const response = await this.fetcher(input, init);
@@ -2781,17 +2728,20 @@ function accountLogins(login) {
2781
2728
  });
2782
2729
  return entries;
2783
2730
  }
2731
+ function personalLogin(login) {
2732
+ return accountLogins(login).find((entry) => entry.kind === "user") ?? null;
2733
+ }
2784
2734
  async function preparePrivateDirectory(directory) {
2785
- const stat = await lstat(directory);
2786
- if (stat.isSymbolicLink() || !stat.isDirectory())
2735
+ const stat2 = await lstat(directory);
2736
+ if (stat2.isSymbolicLink() || !stat2.isDirectory())
2787
2737
  throw new Error("Tonbo config directory must be a real directory.");
2788
2738
  if (process.platform !== "win32")
2789
2739
  await chmod(directory, DIRECTORY_MODE);
2790
2740
  }
2791
2741
  async function assertExistingDestinationIsSafe(filename) {
2792
2742
  try {
2793
- const stat = await lstat(filename);
2794
- if (stat.isSymbolicLink() || !stat.isFile())
2743
+ const stat2 = await lstat(filename);
2744
+ if (stat2.isSymbolicLink() || !stat2.isFile())
2795
2745
  throw new Error("Tonbo credential path must be a regular file.");
2796
2746
  } catch (error) {
2797
2747
  if (error.code !== "ENOENT")
@@ -2799,17 +2749,17 @@ async function assertExistingDestinationIsSafe(filename) {
2799
2749
  }
2800
2750
  }
2801
2751
  async function assertPrivateRegularFile(filename) {
2802
- const stat = await lstat(filename);
2803
- if (stat.isSymbolicLink() || !stat.isFile())
2752
+ const stat2 = await lstat(filename);
2753
+ if (stat2.isSymbolicLink() || !stat2.isFile())
2804
2754
  throw new Error("Tonbo credential path must be a regular file.");
2805
- if (process.platform !== "win32" && (stat.mode & REFUSED_MODE_BITS) !== 0)
2755
+ if (process.platform !== "win32" && (stat2.mode & REFUSED_MODE_BITS) !== 0)
2806
2756
  throw new Error(`Tonbo credential file must have mode ${credentialsContract.permissions.file_mode}.`);
2807
2757
  }
2808
2758
 
2809
2759
  // ../../packages/tonbo-cli-core/dist/src/device.js
2810
2760
  import { execFile } from "node:child_process";
2811
2761
  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";
2762
+ import { link, mkdir as mkdir2, open as open2, readFile as readFile2, rename as rename2, rm as rm2, stat } from "node:fs/promises";
2813
2763
  import os2 from "node:os";
2814
2764
  import path2 from "node:path";
2815
2765
  import { promisify } from "node:util";
@@ -2836,6 +2786,14 @@ async function readMachineId(platform = process.platform) {
2836
2786
  function machineIdHash(machineId) {
2837
2787
  return createHash("sha256").update(machineId).digest("hex");
2838
2788
  }
2789
+ var NO_HARD_LINKS = /* @__PURE__ */ new Set(["EPERM", "ENOTSUP", "EOPNOTSUPP", "ENOSYS", "EMLINK", "EXDEV"]);
2790
+ var CREATE_POLL_MS = 50;
2791
+ var CREATE_STALE_MS = 5e3;
2792
+ var exists = (file) => stat(file).then(() => true, (error) => {
2793
+ if (error.code === "ENOENT")
2794
+ return false;
2795
+ throw error;
2796
+ });
2839
2797
  var DeviceStore = class {
2840
2798
  filename;
2841
2799
  machineId;
@@ -2913,13 +2871,49 @@ var DeviceStore = class {
2913
2871
  await link(temporary, this.filename);
2914
2872
  return true;
2915
2873
  } catch (error) {
2916
- if (!replace && error.code === "EEXIST")
2874
+ const code = error.code;
2875
+ if (!replace && code === "EEXIST")
2917
2876
  return false;
2877
+ if (!replace && code && NO_HARD_LINKS.has(code))
2878
+ return await this.create(temporary);
2918
2879
  throw error;
2919
2880
  } finally {
2920
2881
  await rm2(temporary, { force: true });
2921
2882
  }
2922
2883
  }
2884
+ /** The first record on a filesystem without hard links (some FUSE and
2885
+ * network volumes), from the complete, synced `temporary`: renamed into
2886
+ * place, so the file is never seen empty or partial, while holding a lock
2887
+ * directory made exclusively, so of two first runs at once only one
2888
+ * writes it and the other waits for it. A lock left by a run that died
2889
+ * holding it is taken over after a while. */
2890
+ async create(temporary) {
2891
+ const lock = `${this.filename}.lock`;
2892
+ for (let waited = 0; ; waited += CREATE_POLL_MS) {
2893
+ try {
2894
+ await mkdir2(lock, { mode: 448 });
2895
+ break;
2896
+ } catch (error) {
2897
+ if (error.code !== "EEXIST")
2898
+ throw error;
2899
+ }
2900
+ if (await exists(this.filename))
2901
+ return false;
2902
+ if (waited >= CREATE_STALE_MS) {
2903
+ await rm2(lock, { recursive: true, force: true });
2904
+ waited = -CREATE_POLL_MS;
2905
+ } else
2906
+ await new Promise((resolve) => setTimeout(resolve, CREATE_POLL_MS));
2907
+ }
2908
+ try {
2909
+ if (await exists(this.filename))
2910
+ return false;
2911
+ await rename2(temporary, this.filename);
2912
+ return true;
2913
+ } finally {
2914
+ await rm2(lock, { recursive: true, force: true });
2915
+ }
2916
+ }
2923
2917
  };
2924
2918
 
2925
2919
  // ../../packages/tonbo-cli-core/dist/src/session.js
@@ -2964,7 +2958,7 @@ var FileSessionStore = class {
2964
2958
  await removePrivateFile(this.filename);
2965
2959
  }
2966
2960
  };
2967
- var REFRESH_MARGIN_MS = 6e4;
2961
+ var REFRESH_MARGIN_MS2 = 6e4;
2968
2962
  var clients = /* @__PURE__ */ new WeakMap();
2969
2963
  var SessionClient = class _SessionClient {
2970
2964
  stored;
@@ -3059,7 +3053,7 @@ var SessionClient = class _SessionClient {
3059
3053
  }, what, accountId);
3060
3054
  this.tokens.set(`${audience} ${accountId}`, {
3061
3055
  token: body.access_token,
3062
- refreshAt: issuedAt + Math.max(0, body.expires_in * 1e3 - REFRESH_MARGIN_MS)
3056
+ refreshAt: issuedAt + Math.max(0, body.expires_in * 1e3 - REFRESH_MARGIN_MS2)
3063
3057
  });
3064
3058
  return body.access_token;
3065
3059
  }
@@ -3077,7 +3071,7 @@ var SessionClient = class _SessionClient {
3077
3071
  body: JSON.stringify(fields2)
3078
3072
  });
3079
3073
  } catch (error) {
3080
- throw new AccessTokenError(`The ${what} token exchange failed: ${error instanceof Error ? error.message : String(error)}`, null);
3074
+ throw new AccessTokenError(`The ${what} token exchange failed: ${error instanceof Error ? error.message : String(error)}`, null, null, null, { cause: error });
3081
3075
  }
3082
3076
  if (!response.ok)
3083
3077
  throw await this.refusal(response, what, accountId);
@@ -3118,7 +3112,7 @@ var SessionClient = class _SessionClient {
3118
3112
  headers: { authorization: `Bearer ${token}`, accept: "application/json" }
3119
3113
  });
3120
3114
  } catch (error) {
3121
- throw new AccessTokenError(`Reading ${this.accountName(accountId)}'s git workspaces failed: ${error instanceof Error ? error.message : String(error)}`, null);
3115
+ throw new AccessTokenError(`Reading ${this.accountName(accountId)}'s git workspaces failed: ${error instanceof Error ? error.message : String(error)}`, null, null, null, { cause: error });
3122
3116
  }
3123
3117
  const body = await response.json().catch(() => null);
3124
3118
  if (!response.ok)
@@ -3143,7 +3137,7 @@ var SessionClient = class _SessionClient {
3143
3137
  }
3144
3138
  });
3145
3139
  } catch (error) {
3146
- throw new AccessTokenError(`Reading your accounts failed: ${error instanceof Error ? error.message : String(error)}`, null);
3140
+ throw new AccessTokenError(`Reading your accounts failed: ${error instanceof Error ? error.message : String(error)}`, null, null, null, { cause: error });
3147
3141
  }
3148
3142
  if (!response.ok)
3149
3143
  throw await this.refusal(response, "session", "");
@@ -3225,17 +3219,23 @@ var LoginStore = class {
3225
3219
  await this.sessions.remove();
3226
3220
  await this.legacy.remove();
3227
3221
  }
3228
- /** The person's accounts now, saved as the login's cache of names. */
3222
+ /** The person's accounts now, saved as the login's cache of names. `client`
3223
+ * holds them before the save, so when the file cannot be written (an
3224
+ * agent's sandbox) `current(client)` still answers with them. */
3229
3225
  async refreshAccounts(client) {
3230
3226
  const accounts = await client.accounts();
3231
3227
  const latest = await this.sessions.load() ?? client.stored;
3232
3228
  if (latest.session.id === client.stored.session.id) {
3233
3229
  const saved = withAccounts(latest, accounts);
3234
- await this.sessions.save(saved);
3235
3230
  client.stored = saved;
3231
+ await this.sessions.save(saved);
3236
3232
  }
3237
3233
  return accounts;
3238
3234
  }
3235
+ /** The logins `client` holds in this process, without reading the file again. */
3236
+ current(client) {
3237
+ return this.view(client);
3238
+ }
3239
3239
  /** Forgets an account the person no longer belongs to (they left it, or
3240
3240
  * were removed): the login's cache of names is read again without it, it
3241
3241
  * is no longer the default account, and this process's tokens of it are
@@ -3316,11 +3316,21 @@ var LoginStore = class {
3316
3316
  const named = this.options.account;
3317
3317
  if (!named)
3318
3318
  return;
3319
- const response = await this.fetcher(`${this.options.apiOrigin}${managementApiBasePath}${managementOperations.getAccount.path}`, {
3319
+ const read = async () => this.fetcher(`${this.options.apiOrigin}${managementApiBasePath}${managementOperations.getAccount.path}`, {
3320
3320
  redirect: "manual",
3321
3321
  signal: AbortSignal.timeout(3e4),
3322
- headers: { authorization: `Bearer ${key}`, accept: "application/json" }
3322
+ headers: {
3323
+ authorization: `Bearer ${await managementBearer(key)}`,
3324
+ accept: "application/json"
3325
+ }
3323
3326
  });
3327
+ let response = await read();
3328
+ if (response.status === 401) {
3329
+ const refusal = await response.json().catch(() => null);
3330
+ const code = typeof refusal?.code === "string" ? refusal.code : null;
3331
+ if (retriesAfterRefusal(key, MANAGEMENT_AUDIENCE, code))
3332
+ response = await read();
3333
+ }
3324
3334
  const body = await response.json().catch(() => null);
3325
3335
  if (!response.ok)
3326
3336
  throw new Error(`TONBO_API_KEY was refused (HTTP ${response.status}).`);
@@ -3353,9 +3363,11 @@ var DEFAULT_LOGIN_HINT3 = credentialsContract.reader.login_hint;
3353
3363
  var LOOPBACK_HOST = "127.0.0.1";
3354
3364
  var BrowserUnavailableError = class extends Error {
3355
3365
  };
3356
- function prefersDeviceLogin(env = process.env) {
3366
+ function prefersDeviceLogin(env = process.env, platform = process.platform) {
3367
+ if (env.DISPLAY || env.WAYLAND_DISPLAY)
3368
+ return false;
3357
3369
  const ssh = Boolean(env.SSH_CONNECTION || env.SSH_CLIENT || env.SSH_TTY);
3358
- return ssh && !env.DISPLAY && !env.WAYLAND_DISPLAY;
3370
+ return ssh || platform !== "darwin" && platform !== "win32";
3359
3371
  }
3360
3372
  function loginHostname(hostname = os4.hostname()) {
3361
3373
  const cleaned = hostname.replace(/[^A-Za-z0-9._-]/g, "-").replace(/^[^A-Za-z0-9]+/, "").slice(0, 64);
@@ -3374,7 +3386,8 @@ var AuthClient = class {
3374
3386
  entryLine;
3375
3387
  env;
3376
3388
  facts;
3377
- keyChecked = null;
3389
+ /** TONBO_API_KEY's bearer, made once per key so its tokens and its exchange outlive a call. */
3390
+ injectedBearer = null;
3378
3391
  constructor(credentials, fetcher, loginOrigin2 = "https://tonbo.dev", browserOpener = openBrowser, hostname = loginHostname(), loginHint = DEFAULT_LOGIN_HINT3, entryLine = "cli", env = process.env, facts = {}) {
3379
3392
  this.credentials = credentials;
3380
3393
  this.fetcher = fetcher;
@@ -3388,18 +3401,25 @@ var AuthClient = class {
3388
3401
  }
3389
3402
  /**
3390
3403
  * 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.
3404
+ * own account any account the command named must be), presented as the
3405
+ * Tonbo SDK presents a key (apiKeyBearer), or the CLI login's bearer of the
3406
+ * named account, else of its default, else of the personal account; with
3407
+ * only a legacy login, that file's key.
3394
3408
  */
3395
3409
  async apiKey() {
3396
3410
  const injected = this.env.TONBO_API_KEY?.trim();
3397
3411
  if (injected) {
3398
- if (this.keyChecked !== injected) {
3399
- await this.credentials.checkKeyAccount(injected);
3400
- this.keyChecked = injected;
3412
+ if (this.injectedBearer?.key !== injected)
3413
+ this.injectedBearer = {
3414
+ key: injected,
3415
+ bearer: apiKeyBearer(injected, this.fetcher, this.loginOrigin),
3416
+ checked: false
3417
+ };
3418
+ if (!this.injectedBearer.checked) {
3419
+ await this.credentials.checkKeyAccount(this.injectedBearer.bearer);
3420
+ this.injectedBearer.checked = true;
3401
3421
  }
3402
- return injected;
3422
+ return this.injectedBearer.bearer;
3403
3423
  }
3404
3424
  const login = await this.credentials.load();
3405
3425
  if (!login)
@@ -3412,17 +3432,20 @@ var AuthClient = class {
3412
3432
  * Logs this computer in (ADR-0018): the browser handoff of /cli/login, PKCE
3413
3433
  * S256 with a loopback redirect on a free port, asks for a session
3414
3434
  * (`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
3435
+ * account. With `device`, in an SSH session or on Linux without a display, or when no
3416
3436
  * browser can be opened, the same PKCE challenge goes to a device-code
3417
3437
  * login instead: the person enters a short code at /cli/device in any
3418
3438
  * browser and the CLI polls. The redeemed session, tied to this
3419
3439
  * computer's device id, is saved as `login.json` and replaces this
3420
3440
  * computer's earlier session; the legacy `credentials.json` is left as it
3421
3441
  * 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.
3442
+ * `current` is false. With `openBrowser` false the browser login opens no
3443
+ * browser: the person opens the `authorization-url` they are shown, as
3444
+ * when an agent shows it with what the approval consents to. Answers the
3445
+ * login of the account commands act in.
3423
3446
  */
3424
3447
  async login(progress = () => {
3425
- }, account, { current = true, device } = {}) {
3448
+ }, account, { current = true, device, openBrowser: openBrowser2 = true } = {}) {
3426
3449
  const deviceId = await (this.facts.devices ?? new DeviceStore()).deviceId();
3427
3450
  const verifier = randomBytes(48).toString("base64url");
3428
3451
  const challenge = createHash3("sha256").update(verifier).digest("base64url");
@@ -3433,7 +3456,7 @@ var AuthClient = class {
3433
3456
  ...clientFact(this.facts.clientVersion) ? { client_version: clientFact(this.facts.clientVersion) } : {},
3434
3457
  ...clientFact(process.platform) ? { os: process.platform } : {}
3435
3458
  };
3436
- const redeemed = device ?? prefersDeviceLogin(this.env) ? await this.deviceLogin(challenge, verifier, redeem, progress) : await this.browserLogin(challenge, verifier, redeem, progress).catch((error) => {
3459
+ const redeemed = device ?? prefersDeviceLogin(this.env) ? await this.deviceLogin(challenge, verifier, redeem, progress) : await this.browserLogin(challenge, verifier, redeem, progress, openBrowser2).catch((error) => {
3437
3460
  if (!(error instanceof BrowserUnavailableError))
3438
3461
  throw error;
3439
3462
  progress({ status: "failed", step: "browser", reason: error.message });
@@ -3469,7 +3492,7 @@ var AuthClient = class {
3469
3492
  throw new Error("The saved login could not be read back.");
3470
3493
  return login;
3471
3494
  }
3472
- async browserLogin(challenge, verifier, redeem, progress) {
3495
+ async browserLogin(challenge, verifier, redeem, progress, openBrowser2) {
3473
3496
  const state = randomBytes(24).toString("base64url");
3474
3497
  const code = await this.listenForCode(state, (redirectUri) => {
3475
3498
  const authorization = new URL("/cli/login", this.loginOrigin);
@@ -3483,7 +3506,7 @@ var AuthClient = class {
3483
3506
  [cliSessionContract.grantParameter]: cliSessionContract.grant
3484
3507
  }).toString();
3485
3508
  return authorization.toString();
3486
- }, progress);
3509
+ }, progress, openBrowser2);
3487
3510
  progress({ status: "started", step: "key-redemption" });
3488
3511
  const redeemed = await requestJson(this.fetcher, new URL("/api/cli/login/token", this.loginOrigin).toString(), {
3489
3512
  method: "POST",
@@ -3561,7 +3584,7 @@ var AuthClient = class {
3561
3584
  }
3562
3585
  }
3563
3586
  }
3564
- listenForCode(expectedState, authorizationUrl, progress) {
3587
+ listenForCode(expectedState, authorizationUrl, progress, openBrowser2) {
3565
3588
  return new Promise((resolve, reject) => {
3566
3589
  let settled = false;
3567
3590
  const sockets = /* @__PURE__ */ new Set();
@@ -3636,6 +3659,8 @@ var AuthClient = class {
3636
3659
  progress({ status: "completed", step: "callback-server" });
3637
3660
  const url = authorizationUrl(`http://${LOOPBACK_HOST}:${port}/callback`);
3638
3661
  progress({ status: "authorization-url", step: "browser", url });
3662
+ if (!openBrowser2)
3663
+ return;
3639
3664
  progress({ status: "started", step: "browser" });
3640
3665
  void Promise.resolve().then(() => this.browserOpener(url)).then(() => startAuthorization(), (error) => {
3641
3666
  if (settled || authorizationStarted)
@@ -3681,6 +3706,150 @@ async function openBrowser(url) {
3681
3706
  }
3682
3707
  }
3683
3708
 
3709
+ // ../../packages/tonbo-cli-core/dist/src/capture-filter.js
3710
+ import { randomUUID as randomUUID3 } from "node:crypto";
3711
+ import { existsSync } from "node:fs";
3712
+ import { mkdir as mkdir3, open as open3, readFile as readFile3, rename as rename3, rm as rm3 } from "node:fs/promises";
3713
+ import os5 from "node:os";
3714
+ import path4 from "node:path";
3715
+ var KEPT_WINDOWS = 200;
3716
+ function yieldStateDirectory(env = process.env, home = os5.homedir()) {
3717
+ return path4.join(env.XDG_STATE_HOME || path4.join(home, ".local", "state"), "tonbo-yield");
3718
+ }
3719
+ function emptyFilter() {
3720
+ return { version: 1, pause: null, past: [], excluded: [] };
3721
+ }
3722
+ function activePause(file, now) {
3723
+ const pause = file.pause;
3724
+ return pause && (pause.until === null || now < pause.until) ? pause : null;
3725
+ }
3726
+ function pauseForLogout(file, now, person) {
3727
+ const current = activePause(file, now);
3728
+ if (current?.logout)
3729
+ return file;
3730
+ const after = current ? { after: current.until } : {};
3731
+ if (current)
3732
+ current.until = now;
3733
+ settle(file, now);
3734
+ file.pause = { since: now, until: null, logout: { ...after, person } };
3735
+ return file;
3736
+ }
3737
+ function endLogoutPause(file, now, person) {
3738
+ const current = activePause(file, now);
3739
+ if (!current?.logout)
3740
+ return file;
3741
+ const { after, person: before } = current.logout;
3742
+ if (person === null || before == null || before !== person)
3743
+ current.since = 0;
3744
+ current.until = now;
3745
+ settle(file, now);
3746
+ if (after === null || after !== void 0 && after > now)
3747
+ file.pause = { since: now, until: after };
3748
+ return file;
3749
+ }
3750
+ function settle(file, now) {
3751
+ if (file.pause && !activePause(file, now)) {
3752
+ file.past = [...file.past, file.pause].slice(-KEPT_WINDOWS);
3753
+ file.pause = null;
3754
+ }
3755
+ }
3756
+ async function durableWrite(filename, body) {
3757
+ const parent = path4.dirname(filename);
3758
+ const created = await mkdir3(parent, { recursive: true, mode: 448 });
3759
+ if (created) {
3760
+ const state = await open3(path4.dirname(parent), "r");
3761
+ try {
3762
+ await state.sync();
3763
+ } finally {
3764
+ await state.close();
3765
+ }
3766
+ }
3767
+ const temporary = path4.join(parent, `.${randomUUID3()}.tmp`);
3768
+ const file = await open3(temporary, "wx", 384);
3769
+ try {
3770
+ await file.writeFile(JSON.stringify(body));
3771
+ await file.sync();
3772
+ } finally {
3773
+ await file.close();
3774
+ }
3775
+ try {
3776
+ await rename3(temporary, filename);
3777
+ const dir = await open3(parent, "r");
3778
+ try {
3779
+ await dir.sync();
3780
+ } finally {
3781
+ await dir.close();
3782
+ }
3783
+ } catch (error) {
3784
+ await rm3(temporary, { force: true });
3785
+ throw error;
3786
+ }
3787
+ }
3788
+ var CaptureFilterStore = class {
3789
+ filename;
3790
+ constructor(filename = path4.join(yieldStateDirectory(), "capture-filter.json")) {
3791
+ this.filename = filename;
3792
+ }
3793
+ /** A missing file filters nothing; an unreadable one throws, and the
3794
+ * daemon then captures nothing (fail closed). */
3795
+ async load() {
3796
+ let body;
3797
+ try {
3798
+ body = await readFile3(this.filename, "utf8");
3799
+ } catch (error) {
3800
+ if (error.code === "ENOENT")
3801
+ return emptyFilter();
3802
+ throw error;
3803
+ }
3804
+ const parsed = JSON.parse(body);
3805
+ const window = (value) => !!value && Number.isFinite(value.since) && (value.until === null || Number.isFinite(value.until));
3806
+ 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)))
3807
+ throw new Error(`${this.filename} is not a Yield capture filter file.`);
3808
+ return parsed;
3809
+ }
3810
+ async save(file) {
3811
+ await durableWrite(this.filename, file);
3812
+ }
3813
+ async update(change) {
3814
+ const file = await this.load();
3815
+ change(file);
3816
+ await this.save(file);
3817
+ return file;
3818
+ }
3819
+ };
3820
+ var CaptureNotStoppedError = class extends Error {
3821
+ };
3822
+ var reason = (error) => error instanceof Error ? error.message : String(error);
3823
+ async function stopCaptureForLogout(person, store = new CaptureFilterStore(), now = Date.now()) {
3824
+ if (!existsSync(path4.dirname(store.filename)))
3825
+ return null;
3826
+ let file;
3827
+ try {
3828
+ file = await store.load();
3829
+ } catch (error) {
3830
+ return `Yield's capture stays stopped on this computer: ${reason(error)}`;
3831
+ }
3832
+ pauseForLogout(file, now, person);
3833
+ try {
3834
+ await store.save(file);
3835
+ } catch (error) {
3836
+ 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 });
3837
+ }
3838
+ return null;
3839
+ }
3840
+ async function resumeCaptureAfterLogin(person, store = new CaptureFilterStore(), now = Date.now()) {
3841
+ try {
3842
+ const file = await store.load();
3843
+ if (!activePause(file, now)?.logout)
3844
+ return null;
3845
+ endLogoutPause(file, now, await person().catch(() => null));
3846
+ await store.save(file);
3847
+ return null;
3848
+ } catch (error) {
3849
+ return `Yield's capture stays stopped on this computer: ${reason(error)}`;
3850
+ }
3851
+ }
3852
+
3684
3853
  // ../../packages/tonbo-cli-core/dist/src/cell.js
3685
3854
  function cellOrigin(region, configured = process.env.TONBO_CELL_API_ORIGIN) {
3686
3855
  if (!/^[a-z0-9-]+$/.test(region))
@@ -3698,7 +3867,7 @@ var CellApi = class {
3698
3867
  }
3699
3868
  url(operationId, region, request) {
3700
3869
  const operation = managementOperations[operationId];
3701
- const path4 = operation.path.replace(/\{([^}]+)\}/g, (_, name) => {
3870
+ const path5 = operation.path.replace(/\{([^}]+)\}/g, (_, name) => {
3702
3871
  const value = request.params[name];
3703
3872
  if (value === void 0)
3704
3873
  throw new Error(`${operationId} needs the path parameter ${name}.`);
@@ -3709,7 +3878,7 @@ var CellApi = class {
3709
3878
  if (value !== void 0)
3710
3879
  search.set(name, value);
3711
3880
  const query = search.toString();
3712
- return `${cellOrigin(region)}${managementApiBasePath}${path4}${query ? `?${query}` : ""}`;
3881
+ return `${cellOrigin(region)}${managementApiBasePath}${path5}${query ? `?${query}` : ""}`;
3713
3882
  }
3714
3883
  async init(operationId, key, request) {
3715
3884
  const headers = {
@@ -3787,14 +3956,22 @@ function when(timestamp, absent = "never") {
3787
3956
  }
3788
3957
 
3789
3958
  // ../../packages/tonbo-cli-core/dist/src/login.js
3959
+ function captureAfterLogin(deps) {
3960
+ return resumeCaptureAfterLogin(async () => {
3961
+ const file = await deps.credentials.load();
3962
+ return file ? personalLogin(file)?.account_id ?? null : null;
3963
+ }, deps.captureFilter);
3964
+ }
3790
3965
  async function loginCommand(deps, options = {}) {
3791
3966
  try {
3792
3967
  const login = await deps.auth.login((event) => reportLoginProgress(deps.progress, event), options.account, { current: options.current, device: options.device });
3968
+ const stopped = await captureAfterLogin(deps);
3793
3969
  const file = await deps.credentials.load();
3794
3970
  const names = file ? accountLogins(file).map((entry) => entry.kind === "user" ? `${entry.name ?? "you"} (personal)` : entry.name ?? entry.account_id) : [];
3795
3971
  deps.output([
3796
3972
  `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.` : ""}`
3973
+ `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.` : ""}`,
3974
+ ...stopped ? [stopped] : []
3798
3975
  ].join("\n"));
3799
3976
  return login;
3800
3977
  } catch (error) {
@@ -3866,11 +4043,13 @@ async function logoutCommand(deps) {
3866
4043
  failures.push(`${what}: ${failure.message}`);
3867
4044
  };
3868
4045
  let session = null;
4046
+ let unusable = false;
3869
4047
  try {
3870
4048
  session = await deps.credentials.session({ anyDevice: true });
3871
4049
  } catch (error) {
3872
4050
  if (!(error instanceof UnusableLoginError))
3873
4051
  throw error;
4052
+ unusable = true;
3874
4053
  }
3875
4054
  let legacy = null;
3876
4055
  try {
@@ -3878,6 +4057,13 @@ async function logoutCommand(deps) {
3878
4057
  } catch (error) {
3879
4058
  if (!(error instanceof UnusableLoginError))
3880
4059
  throw error;
4060
+ unusable = true;
4061
+ }
4062
+ if (session || legacy || unusable) {
4063
+ const person = session?.stored.personal_account_id ?? (legacy && personalLogin(legacy)?.account_id) ?? null;
4064
+ const stopped = await stopCaptureForLogout(person, deps.captureFilter);
4065
+ if (stopped)
4066
+ deps.output(stopped);
3881
4067
  }
3882
4068
  if (session)
3883
4069
  await session.logout().catch((error) => failed("the CLI login", error));
@@ -3986,7 +4172,7 @@ var TerminalProgress = class {
3986
4172
  };
3987
4173
 
3988
4174
  // src/commands.ts
3989
- import { randomUUID as randomUUID4 } from "node:crypto";
4175
+ import { randomUUID as randomUUID5 } from "node:crypto";
3990
4176
 
3991
4177
  // src/agent-name.ts
3992
4178
  var AGENT_NAME_RULE = agentNameContract.$defs.name;
@@ -4002,7 +4188,7 @@ function validateAgentName(name) {
4002
4188
  }
4003
4189
 
4004
4190
  // src/budget.ts
4005
- import { randomUUID as randomUUID3 } from "node:crypto";
4191
+ import { randomUUID as randomUUID4 } from "node:crypto";
4006
4192
  var BUDGETS_NOT_AVAILABLE = "Budgets are not available yet.";
4007
4193
  function parseDollars(value) {
4008
4194
  const match = /^\$?([0-9]+)(?:\.([0-9]{1,2}))?$/.exec(value.trim());
@@ -4094,7 +4280,7 @@ async function budgetAddCommand(deps, name, options) {
4094
4280
  const key = await deps.auth.apiKey();
4095
4281
  const limit = parseDollars(options.limit);
4096
4282
  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));
4283
+ const created = await deps.api.createBudget(key, { scope: "principals", name, limit_micros: limit, principals }, randomUUID4()).catch((error) => consoleOr(deps, error));
4098
4284
  print(
4099
4285
  deps,
4100
4286
  options.json,
@@ -4127,7 +4313,7 @@ async function attachBudget(deps, key, subject, value, name, created) {
4127
4313
  const budget = await deps.api.createBudget(
4128
4314
  key,
4129
4315
  { scope: "principals", name, limit_micros: limit, principals: [subject] },
4130
- randomUUID3()
4316
+ randomUUID4()
4131
4317
  ).catch((error) => consoleOr(deps, error));
4132
4318
  deps.notice(`Budget ${budget.name ?? name}: ${usd(limit)} a month for ${subject}.`);
4133
4319
  return;
@@ -4136,8 +4322,8 @@ async function attachBudget(deps, key, subject, value, name, created) {
4136
4322
  await deps.api.changeBudget(key, found.id, { add_principals: [subject] }).catch((error) => consoleOr(deps, error));
4137
4323
  deps.notice(`Budget ${found.name ?? found.id} now covers ${subject}.`);
4138
4324
  } 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}`);
4325
+ const reason2 = error instanceof Error ? error.message : String(error);
4326
+ throw new Error(`${created} was created, but its budget was not set: ${reason2}`);
4141
4327
  }
4142
4328
  }
4143
4329
  function accountBudgetFields(account) {
@@ -4246,7 +4432,7 @@ async function agentCreateCommand(deps, name, options = {}) {
4246
4432
  const agent = await deps.api.createAgent(
4247
4433
  key,
4248
4434
  { name, region, ...options.role === void 0 ? {} : { role: options.role } },
4249
- options.idempotencyKey ?? randomUUID4()
4435
+ options.idempotencyKey ?? randomUUID5()
4250
4436
  );
4251
4437
  print(
4252
4438
  deps,
@@ -4554,7 +4740,7 @@ async function bucketCreateCommand(deps, name, options = {}) {
4554
4740
  const bucket = await deps.api.createBucket(
4555
4741
  await deps.auth.apiKey(),
4556
4742
  { name, region: options.region },
4557
- options.idempotencyKey ?? randomUUID4()
4743
+ options.idempotencyKey ?? randomUUID5()
4558
4744
  );
4559
4745
  print(
4560
4746
  deps,
@@ -4685,7 +4871,7 @@ async function databaseCreateCommand(deps, name, options = {}) {
4685
4871
  const database = await deps.api.createDatabase(
4686
4872
  await deps.auth.apiKey(),
4687
4873
  { name, region: options.region, expires_at: expiresAt },
4688
- options.idempotencyKey ?? randomUUID4()
4874
+ options.idempotencyKey ?? randomUUID5()
4689
4875
  );
4690
4876
  print(
4691
4877
  deps,
@@ -4873,7 +5059,7 @@ async function streamCreateCommand(deps, name, options = {}) {
4873
5059
  content_type: options.contentType,
4874
5060
  expires_at: expiresAt
4875
5061
  },
4876
- options.idempotencyKey ?? randomUUID4()
5062
+ options.idempotencyKey ?? randomUUID5()
4877
5063
  );
4878
5064
  print(
4879
5065
  deps,
@@ -5097,11 +5283,11 @@ async function accountUseCommand(deps, account, options = {}) {
5097
5283
  import { Help } from "commander";
5098
5284
 
5099
5285
  // src/machine.ts
5100
- import { randomUUID as randomUUID6 } from "node:crypto";
5286
+ import { randomUUID as randomUUID7 } from "node:crypto";
5101
5287
  import { readFileSync } from "node:fs";
5102
5288
 
5103
5289
  // src/volume.ts
5104
- import { randomUUID as randomUUID5 } from "node:crypto";
5290
+ import { randomUUID as randomUUID6 } from "node:crypto";
5105
5291
  var VOLUME_DURABILITY_LINK = "https://tonbo.dev/docs/machines/guarantees/data#volume";
5106
5292
  var VOLUME_ERROR_HINTS = {
5107
5293
  payment_required: "Add credit at <billing>, then create the Volume again.",
@@ -5199,7 +5385,7 @@ async function volumeCreateCommand(deps, name, options = {}) {
5199
5385
  const volume = await deps.api.createVolume(
5200
5386
  key,
5201
5387
  { name, region, size_gib: sizeGib },
5202
- options.idempotencyKey ?? randomUUID5()
5388
+ options.idempotencyKey ?? randomUUID6()
5203
5389
  );
5204
5390
  print(
5205
5391
  deps,
@@ -5355,10 +5541,10 @@ function duration(seconds) {
5355
5541
  return seconds % 60 === 0 && seconds >= 120 ? `${seconds / 60} min` : `${seconds} s`;
5356
5542
  }
5357
5543
  function sleepText(machine) {
5358
- const { action, reason } = machine.status.sleep;
5544
+ const { action, reason: reason2 } = machine.status.sleep;
5359
5545
  const when2 = `after about ${duration(earlyReclaimSeconds)} idle (early) or at most ${machine.sleep_after_seconds} s without a connection`;
5360
5546
  if (action !== "release") return `${when2}; it then ${action}s`;
5361
- return `${when2}; the disk resets at every sleep${reason === "suspend_unavailable" ? " until suspend ships" : ""}`;
5547
+ return `${when2}; the disk resets at every sleep${reason2 === "suspend_unavailable" ? " until suspend ships" : ""}`;
5362
5548
  }
5363
5549
  function activationFields(machine) {
5364
5550
  const { activation } = machine;
@@ -5458,6 +5644,7 @@ function machineFields(machine, billing, etag = null, lastEnd = null) {
5458
5644
  ["Region", machine.region],
5459
5645
  ["State", machineState(machine)],
5460
5646
  ["Power", powerText(machine)],
5647
+ ...awakeFields(machine),
5461
5648
  ...activationFields(machine),
5462
5649
  ...lastEnd ? [["Last end", endText(lastEnd)]] : [],
5463
5650
  ...conditionFields(machine, billing),
@@ -5485,6 +5672,25 @@ function machineFields(machine, billing, etag = null, lastEnd = null) {
5485
5672
  ["Updated", machine.updated_at]
5486
5673
  ]);
5487
5674
  }
5675
+ function holderText(hold) {
5676
+ const until = hold.until ? ` until ${hold.until}` : "";
5677
+ return hold.kind === "edge_outage" ? `edge_outage${until}` : `${hold.kind} ${hold.count}${until}`;
5678
+ }
5679
+ function awakeFields(machine) {
5680
+ if (machine.state !== "active" || machine.status.power !== "awake") return [];
5681
+ const awake = machine.status.awake;
5682
+ if (awake === void 0) return [];
5683
+ if (awake === null) return [["Held by", "not reported yet; read it again in a few seconds"]];
5684
+ const held = awake.held_by.length > 0;
5685
+ const sleeps = awake.sleeps_at_earliest ? `${awake.sleeps_at_earliest} (${awake.sleeps_by === "idle_early" ? `idle_early: quiet for ${duration(earlyReclaimSeconds)}` : `sleep_after: idle for ${duration(machine.sleep_after_seconds)}`})` : held ? "not while anything holds it" : "not known yet";
5686
+ return [
5687
+ ["Held by", held ? awake.held_by.map(holderText).join(", ") : "nothing"],
5688
+ ...awake.idle_since ? [["Idle since", awake.idle_since]] : [],
5689
+ ["Quiet", awake.quiet.passes ? "yes" : `no: ${awake.quiet.reason ?? "measuring"}`],
5690
+ ["Sleeps at", sleeps],
5691
+ ["Judged at", awake.as_of]
5692
+ ];
5693
+ }
5488
5694
  function deleteFields(machine) {
5489
5695
  const seconds = machine.delete_after_seconds;
5490
5696
  if (seconds === void 0 || seconds === null) return [];
@@ -5793,9 +5999,9 @@ async function waitForCreate(deps, key, machine, name, firstWait, timeout, json)
5793
5999
  }
5794
6000
  print(deps, json, settled, machineFields(settled, billing));
5795
6001
  if (settled.state === "failed") {
5796
- const { reason, hint } = creationFailureText(settled, billing);
6002
+ const { reason: reason2, hint } = creationFailureText(settled, billing);
5797
6003
  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}`
6004
+ `Machine ${settled.name} failed to be created${reason2}; delete it with \`tonbo machine delete ${settled.name}\` and create it again.${hint}`
5799
6005
  );
5800
6006
  }
5801
6007
  throw new Error(`Machine ${settled.name} is ${settled.state}; it was not created.`);
@@ -5837,7 +6043,7 @@ async function machineCreateCommand(deps, name, command, options = {}) {
5837
6043
  ...database ? { database: database.name } : {},
5838
6044
  ...options.database === false ? { database: null } : {}
5839
6045
  },
5840
- options.idempotencyKey ?? randomUUID6()
6046
+ options.idempotencyKey ?? randomUUID7()
5841
6047
  );
5842
6048
  } catch (error) {
5843
6049
  if (error instanceof HttpError && error.code === "name_taken")
@@ -5849,9 +6055,9 @@ async function machineCreateCommand(deps, name, command, options = {}) {
5849
6055
  if (machine.state === "failed") {
5850
6056
  const billing = await billingUrl(deps);
5851
6057
  print(deps, options.json, machine, machineFields(machine, billing));
5852
- const { reason, hint } = creationFailureText(machine, billing);
6058
+ const { reason: reason2, hint } = creationFailureText(machine, billing);
5853
6059
  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}`
6060
+ `Machine ${machine.name} failed to be created${reason2}; delete it with \`tonbo machine delete ${machine.name}\` and create it again.${hint}`
5855
6061
  );
5856
6062
  }
5857
6063
  if (options.wait && machine.state === "creating")
@@ -5895,9 +6101,9 @@ async function lastActivationEnd(deps, key, name) {
5895
6101
  const page = await deps.api.listMachineActivations(key, name, { limit: 2 });
5896
6102
  return page.data.find((entry) => entry.end !== null)?.end ?? null;
5897
6103
  } catch (error) {
5898
- if (!(error instanceof HttpError)) throw error;
6104
+ if (!(error instanceof HttpError || error instanceof AccessTokenError)) throw error;
5899
6105
  deps.notice(
5900
- `Could not read how its last activation ended (${error.status}); \`tonbo machine activations ${name}\` lists them.`
6106
+ `Could not read how its last activation ended (${error.status ?? "no answer"}); \`tonbo machine activations ${name}\` lists them.`
5901
6107
  );
5902
6108
  return null;
5903
6109
  }
@@ -6226,7 +6432,7 @@ var TOPICS = [
6226
6432
  title: "The login, accounts and keys",
6227
6433
  text: [
6228
6434
  "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.",
6435
+ "`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
6436
  "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
6437
  ]
6232
6438
  },
@@ -6272,7 +6478,7 @@ var COMMAND_HELP = {
6272
6478
  },
6273
6479
  "tonbo logout": {
6274
6480
  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."
6481
+ "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
6482
  ],
6277
6483
  examples: ["tonbo logout"]
6278
6484
  },
@@ -6889,19 +7095,19 @@ function optionDescription(option) {
6889
7095
  function applyHelp(program) {
6890
7096
  const named = /* @__PURE__ */ new Set();
6891
7097
  const walk = (command, words) => {
6892
- const path4 = words.join(" ");
7098
+ const path5 = words.join(" ");
6893
7099
  command.configureHelp({ ...command.configureHelp(), optionDescription });
6894
- const help = COMMAND_HELP[path4];
7100
+ const help = COMMAND_HELP[path5];
6895
7101
  if (help) {
6896
- named.add(path4);
7102
+ named.add(path5);
6897
7103
  for (const [name, text] of Object.entries(help.arguments ?? {})) {
6898
7104
  const argument = command.registeredArguments.find((entry) => entry.name() === name);
6899
- if (!argument) throw new Error(`${path4} has no argument ${name}`);
7105
+ if (!argument) throw new Error(`${path5} has no argument ${name}`);
6900
7106
  argument.description = text;
6901
7107
  }
6902
7108
  for (const [flag, text] of Object.entries(help.defaults ?? {})) {
6903
7109
  const option = command.options.find((entry) => entry.long === flag);
6904
- if (!option) throw new Error(`${path4} has no option ${flag}`);
7110
+ if (!option) throw new Error(`${path5} has no option ${flag}`);
6905
7111
  option.defaultValueDescription = text;
6906
7112
  }
6907
7113
  command.addHelpText("after", commandHelpText(help));
@@ -6909,15 +7115,15 @@ function applyHelp(program) {
6909
7115
  for (const child of command.commands) walk(child, [...words, child.name()]);
6910
7116
  };
6911
7117
  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`);
7118
+ for (const path5 of Object.keys(COMMAND_HELP))
7119
+ if (!named.has(path5)) throw new Error(`The help of ${path5} names no command`);
6914
7120
  program.addHelpText("after", programHelpText());
6915
7121
  return program;
6916
7122
  }
6917
7123
 
6918
7124
  // src/machine-cell.ts
6919
- import { readFile as readFile3, writeFile } from "node:fs/promises";
6920
- import { randomUUID as randomUUID7 } from "node:crypto";
7125
+ import { readFile as readFile4, writeFile } from "node:fs/promises";
7126
+ import { randomUUID as randomUUID8 } from "node:crypto";
6921
7127
  var EXEC_EXIT_CODES = {
6922
7128
  timeout: 124,
6923
7129
  output_limit: 125,
@@ -7102,7 +7308,7 @@ async function machineProcessStartCommand(deps, name, command, options = {}) {
7102
7308
  params: { name },
7103
7309
  headers: {
7104
7310
  "content-type": "application/json",
7105
- "idempotency-key": options.idempotencyKey ?? randomUUID7()
7311
+ "idempotency-key": options.idempotencyKey ?? randomUUID8()
7106
7312
  },
7107
7313
  body: JSON.stringify(body)
7108
7314
  });
@@ -7255,13 +7461,13 @@ async function machineProcessSummary(cell, key, region, name) {
7255
7461
  return null;
7256
7462
  }
7257
7463
  }
7258
- async function machineFileReadCommand(deps, name, path4, options = {}) {
7464
+ async function machineFileReadCommand(deps, name, path5, options = {}) {
7259
7465
  try {
7260
7466
  const key = await deps.auth.apiKey();
7261
7467
  const region = await regionOf(deps, key, name);
7262
7468
  const response = await cellApi(deps).fetch("readMachineFile", key, region, {
7263
7469
  params: { name },
7264
- query: { path: path4 },
7470
+ query: { path: path5 },
7265
7471
  headers: { range: options.range, "tonbo-expect-activation": options.expectActivation }
7266
7472
  });
7267
7473
  const bytes2 = new Uint8Array(await response.arrayBuffer());
@@ -7271,21 +7477,21 @@ async function machineFileReadCommand(deps, name, path4, options = {}) {
7271
7477
  throw explainMachineError(error);
7272
7478
  }
7273
7479
  }
7274
- async function machineFileWriteCommand(deps, name, path4, options = {}) {
7480
+ async function machineFileWriteCommand(deps, name, path5, options = {}) {
7275
7481
  try {
7276
- const bytes2 = options.from ? new Uint8Array(await readFile3(options.from)) : await readAllStdin(deps);
7482
+ const bytes2 = options.from ? new Uint8Array(await readFile4(options.from)) : await readAllStdin(deps);
7277
7483
  const key = await deps.auth.apiKey();
7278
7484
  const region = await regionOf(deps, key, name);
7279
7485
  const written = await cellApi(deps).json("writeMachineFile", key, region, {
7280
7486
  params: { name },
7281
- query: { path: path4 },
7487
+ query: { path: path5 },
7282
7488
  headers: {
7283
7489
  "content-type": "application/octet-stream",
7284
7490
  "if-match": options.ifMatch,
7285
7491
  "if-none-match": options.create ? "*" : void 0,
7286
7492
  "tonbo-file-mode": options.mode,
7287
7493
  "tonbo-expect-activation": options.expectActivation,
7288
- "idempotency-key": randomUUID7()
7494
+ "idempotency-key": randomUUID8()
7289
7495
  },
7290
7496
  body: new Uint8Array(bytes2)
7291
7497
  });
@@ -7300,28 +7506,28 @@ async function machineFileWriteCommand(deps, name, path4, options = {}) {
7300
7506
  throw explainMachineError(error);
7301
7507
  }
7302
7508
  }
7303
- async function machineFileStatCommand(deps, name, path4, options = {}) {
7509
+ async function machineFileStatCommand(deps, name, path5, options = {}) {
7304
7510
  try {
7305
7511
  const key = await deps.auth.apiKey();
7306
7512
  const region = await regionOf(deps, key, name);
7307
- const stat = await cellApi(deps).json(
7513
+ const stat2 = await cellApi(deps).json(
7308
7514
  "statMachineFile",
7309
7515
  key,
7310
7516
  region,
7311
7517
  {
7312
7518
  params: { name },
7313
- query: { path: path4 },
7519
+ query: { path: path5 },
7314
7520
  headers: { "tonbo-expect-activation": options.expectActivation }
7315
7521
  }
7316
7522
  );
7317
- noticeWake(deps, stat.machine, name);
7523
+ noticeWake(deps, stat2.machine, name);
7318
7524
  print(
7319
7525
  deps,
7320
7526
  options.json,
7321
- stat,
7527
+ stat2,
7322
7528
  fields(
7323
7529
  ["path", "type", "size", "mode", "uid", "gid", "mtime", "etag", "target", "durability"].map(
7324
- (field) => [field, cellText(stat[field])]
7530
+ (field) => [field, cellText(stat2[field])]
7325
7531
  )
7326
7532
  )
7327
7533
  );
@@ -7329,13 +7535,13 @@ async function machineFileStatCommand(deps, name, path4, options = {}) {
7329
7535
  throw explainMachineError(error);
7330
7536
  }
7331
7537
  }
7332
- async function machineFileListCommand(deps, name, path4, options = {}) {
7538
+ async function machineFileListCommand(deps, name, path5, options = {}) {
7333
7539
  try {
7334
7540
  const key = await deps.auth.apiKey();
7335
7541
  const region = await regionOf(deps, key, name);
7336
7542
  const listing = await cellApi(deps).json("listMachineFiles", key, region, {
7337
7543
  params: { name },
7338
- query: { path: path4 },
7544
+ query: { path: path5 },
7339
7545
  headers: { "tonbo-expect-activation": options.expectActivation }
7340
7546
  });
7341
7547
  noticeWake(deps, listing.machine, name);
@@ -7359,19 +7565,19 @@ async function machineFileListCommand(deps, name, path4, options = {}) {
7359
7565
  throw explainMachineError(error);
7360
7566
  }
7361
7567
  }
7362
- async function machineFileDeleteCommand(deps, name, path4, options = {}) {
7568
+ async function machineFileDeleteCommand(deps, name, path5, options = {}) {
7363
7569
  try {
7364
7570
  const key = await deps.auth.apiKey();
7365
7571
  const region = await regionOf(deps, key, name);
7366
7572
  await cellApi(deps).fetch("deleteMachineFile", key, region, {
7367
7573
  params: { name },
7368
- query: { path: path4, recursive: options.recursive ? "true" : void 0 },
7574
+ query: { path: path5, recursive: options.recursive ? "true" : void 0 },
7369
7575
  headers: {
7370
7576
  "tonbo-expect-activation": options.expectActivation,
7371
- "idempotency-key": randomUUID7()
7577
+ "idempotency-key": randomUUID8()
7372
7578
  }
7373
7579
  });
7374
- print(deps, options.json, { path: path4 }, `Deleted ${path4}.`);
7580
+ print(deps, options.json, { path: path5 }, `Deleted ${path5}.`);
7375
7581
  } catch (error) {
7376
7582
  throw explainMachineError(error);
7377
7583
  }
@@ -7474,8 +7680,8 @@ async function terminalAsk(question) {
7474
7680
 
7475
7681
  // src/ssh.ts
7476
7682
  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";
7683
+ import { existsSync as existsSync2 } from "node:fs";
7684
+ import { chmod as chmod2, mkdir as mkdir4, readFile as readFile5, readdir, rename as rename4, writeFile as writeFile2 } from "node:fs/promises";
7479
7685
  import { homedir } from "node:os";
7480
7686
  import { join } from "node:path";
7481
7687
  function sshDirectory(env = process.env) {
@@ -7499,9 +7705,9 @@ function cellOrigin2(host) {
7499
7705
  }
7500
7706
  async function ensureKey(directory) {
7501
7707
  const key = join(directory, "id_ed25519");
7502
- await mkdir3(directory, { recursive: true, mode: 448 });
7708
+ await mkdir4(directory, { recursive: true, mode: 448 });
7503
7709
  await chmod2(directory, 448);
7504
- if (!existsSync(key)) {
7710
+ if (!existsSync2(key)) {
7505
7711
  const made = spawnSync(
7506
7712
  "ssh-keygen",
7507
7713
  ["-q", "-t", "ed25519", "-N", "", "-C", "tonbo", "-f", key],
@@ -7517,10 +7723,10 @@ async function ensureKey(directory) {
7517
7723
  }
7518
7724
  return key;
7519
7725
  }
7520
- async function writeAtomic(path4, data) {
7521
- const temporary = `${path4}.${process.pid}.tmp`;
7726
+ async function writeAtomic(path5, data) {
7727
+ const temporary = `${path5}.${process.pid}.tmp`;
7522
7728
  await writeFile2(temporary, data, { mode: 384 });
7523
- await rename3(temporary, path4);
7729
+ await rename4(temporary, path5);
7524
7730
  }
7525
7731
  function certificatePath(directory, user) {
7526
7732
  return join(directory, `${user}-cert.pub`);
@@ -7533,7 +7739,7 @@ function hostCAPath(directory, host) {
7533
7739
  }
7534
7740
  async function trustHostCA(directory, host, lines) {
7535
7741
  const cells = join(directory, "host-ca");
7536
- await mkdir3(cells, { recursive: true, mode: 448 });
7742
+ await mkdir4(cells, { recursive: true, mode: 448 });
7537
7743
  await writeAtomic(hostCAPath(directory, host), `${lines.join("\n")}
7538
7744
  `);
7539
7745
  const trusted = /* @__PURE__ */ new Set();
@@ -7541,7 +7747,7 @@ async function trustHostCA(directory, host, lines) {
7541
7747
  if (name.endsWith(".tmp")) {
7542
7748
  continue;
7543
7749
  }
7544
- for (const line of (await readFile4(join(cells, name), "utf8")).split("\n")) {
7750
+ for (const line of (await readFile5(join(cells, name), "utf8")).split("\n")) {
7545
7751
  if (line.trim() !== "") {
7546
7752
  trusted.add(line.trim());
7547
7753
  }
@@ -7553,7 +7759,7 @@ async function trustHostCA(directory, host, lines) {
7553
7759
  async function heldUntil(directory, user) {
7554
7760
  try {
7555
7761
  const sidecar = JSON.parse(
7556
- await readFile4(`${certificatePath(directory, user)}.json`, "utf8")
7762
+ await readFile5(`${certificatePath(directory, user)}.json`, "utf8")
7557
7763
  );
7558
7764
  return sidecar.valid_before ? Date.parse(sidecar.valid_before) : 0;
7559
7765
  } catch {
@@ -7572,11 +7778,9 @@ async function cellRequest(bearer, method, url, body) {
7572
7778
  },
7573
7779
  body: body === void 0 ? void 0 : JSON.stringify(body)
7574
7780
  });
7575
- if (response.status === 401 && attempt === 0 && isAccountBearer(bearer)) {
7576
- bearer.invalidate(MANAGEMENT_AUDIENCE);
7577
- continue;
7578
- }
7579
7781
  const answer = await response.json().catch(() => null);
7782
+ if (response.status === 401 && attempt === 0 && retriesAfterRefusal(bearer, MANAGEMENT_AUDIENCE, answer?.code ?? null))
7783
+ continue;
7580
7784
  if (!response.ok) {
7581
7785
  throw new Error(
7582
7786
  `${url} answered ${response.status}${answer?.code ? ` ${answer.code}` : ""}${answer?.detail ? `: ${answer.detail}` : ""}`
@@ -7589,11 +7793,11 @@ async function refreshCertificate(bearer, destination, now = Date.now, directory
7589
7793
  const key = await ensureKey(directory);
7590
7794
  const certificate = certificatePath(directory, destination.user);
7591
7795
  const knownHosts = join(directory, "known_hosts");
7592
- if (await heldUntil(directory, destination.user) - now() > RENEW_BEFORE_MS && existsSync(hostCAPath(directory, destination.host)) && existsSync(knownHosts)) {
7796
+ if (await heldUntil(directory, destination.user) - now() > RENEW_BEFORE_MS && existsSync2(hostCAPath(directory, destination.host)) && existsSync2(knownHosts)) {
7593
7797
  return { key, certificate, knownHosts };
7594
7798
  }
7595
7799
  const origin = cellOrigin2(destination.host);
7596
- const publicKey = (await readFile4(`${key}.pub`, "utf8")).trim();
7800
+ const publicKey = (await readFile5(`${key}.pub`, "utf8")).trim();
7597
7801
  const issued = await cellRequest(bearer, "POST", `${origin}/v1/ssh/certificates`, {
7598
7802
  public_key: publicKey,
7599
7803
  ttl_seconds: REQUESTED_TTL_SECONDS
@@ -7685,7 +7889,7 @@ async function sshHostCaCommand(deps, options) {
7685
7889
  print(deps, options.json, data, data.known_hosts.join("\n"));
7686
7890
  }
7687
7891
  async function sshCertificateIssueCommand(deps, options) {
7688
- const publicKey = (await readFile4(options.publicKey, "utf8")).trim();
7892
+ const publicKey = (await readFile5(options.publicKey, "utf8")).trim();
7689
7893
  const key = await deps.auth.apiKey();
7690
7894
  const data = await (deps.cell ?? new CellApi(fetch)).json(
7691
7895
  "issueSshCertificate",
@@ -7762,7 +7966,7 @@ var repeated = (value, previous = []) => [...previous, value];
7762
7966
  var budgetOption = (whom) => new Option(
7763
7967
  "--budget <budget>",
7764
7968
  `put ${whom} under a monthly budget: an amount in dollars such as 50 creates one, a budget's name adds to it`
7765
- ).hideHelp();
7969
+ );
7766
7970
  var roleOption = (description) => new Option("--role <role>", description).choices(accountRoles);
7767
7971
  function machineSpecOptions(command, update) {
7768
7972
  const later = update ? "; applies at the next boot" : "";
@@ -7828,7 +8032,9 @@ function createProgram(makeDependencies = createDependencies) {
7828
8032
  const account2 = program.opts().account;
7829
8033
  await loginCommand(dependencies(), { ...options, account: account2 });
7830
8034
  });
7831
- program.command("logout").description("end this computer's CLI login and remove the saved login").action(async () => logoutCommand(dependencies()));
8035
+ program.command("logout").description(
8036
+ "end this computer's CLI login and remove the saved login; Yield's capture here stops until the next login"
8037
+ ).action(async () => logoutCommand(dependencies()));
7832
8038
  const account = program.command("account").description("the accounts you belong to and the one commands act in");
7833
8039
  operationOptions(
7834
8040
  account.command("show").description("show the account, your role in it and its limits"),
@@ -7919,7 +8125,7 @@ function createProgram(makeDependencies = createDependencies) {
7919
8125
  ).action(
7920
8126
  async (keyId, options) => apiKeyRevokeCommand(dependencies(), keyId, options)
7921
8127
  );
7922
- const budget = program.command("budget", { hidden: true }).description("the account's monthly spend budgets");
8128
+ const budget = program.command("budget").description("the account's monthly spend budgets");
7923
8129
  operationOptions(
7924
8130
  budget.command("list").description("list the budgets, the account budget first"),
7925
8131
  "listBudgets"
@@ -8225,31 +8431,31 @@ function createProgram(makeDependencies = createDependencies) {
8225
8431
  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
8432
  "readMachineFile"
8227
8433
  ).action(
8228
- async (name, path4, options) => machineFileReadCommand(dependencies(), name, path4, options)
8434
+ async (name, path5, options) => machineFileReadCommand(dependencies(), name, path5, options)
8229
8435
  );
8230
8436
  operationOptions(
8231
8437
  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
8438
  "writeMachineFile"
8233
8439
  ).action(
8234
- async (name, path4, options) => machineFileWriteCommand(dependencies(), name, path4, options)
8440
+ async (name, path5, options) => machineFileWriteCommand(dependencies(), name, path5, options)
8235
8441
  );
8236
8442
  operationOptions(
8237
8443
  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
8444
  "statMachineFile"
8239
8445
  ).action(
8240
- async (name, path4, options) => machineFileStatCommand(dependencies(), name, path4, options)
8446
+ async (name, path5, options) => machineFileStatCommand(dependencies(), name, path5, options)
8241
8447
  );
8242
8448
  operationOptions(
8243
8449
  file.command("list <name> <path>").description("list a directory of a Machine").option("--expect-activation <activation>", "list only in this activation"),
8244
8450
  "listMachineFiles"
8245
8451
  ).action(
8246
- async (name, path4, options) => machineFileListCommand(dependencies(), name, path4, options)
8452
+ async (name, path5, options) => machineFileListCommand(dependencies(), name, path5, options)
8247
8453
  );
8248
8454
  operationOptions(
8249
8455
  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
8456
  "deleteMachineFile"
8251
8457
  ).action(
8252
- async (name, path4, options) => machineFileDeleteCommand(dependencies(), name, path4, options)
8458
+ async (name, path5, options) => machineFileDeleteCommand(dependencies(), name, path5, options)
8253
8459
  );
8254
8460
  const hold = machine.command("hold").description("keep a Machine awake for work that has no open connection");
8255
8461
  operationOptions(