@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/README.md +1 -1
- package/dist/bin/tonbo.js +553 -347
- package/package.json +2 -2
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
|
-
"
|
|
438
|
+
"listProjectPeople": {
|
|
401
439
|
"method": "GET",
|
|
402
|
-
"path": "/
|
|
440
|
+
"path": "/projects/{project_id}/people",
|
|
403
441
|
"pathParams": [
|
|
404
|
-
"
|
|
442
|
+
"project_id"
|
|
405
443
|
],
|
|
406
444
|
"query": [],
|
|
407
445
|
"idempotencyKey": false
|
|
408
446
|
},
|
|
409
|
-
"
|
|
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": "/
|
|
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
|
-
"
|
|
451
|
+
"project_id"
|
|
473
452
|
],
|
|
474
453
|
"query": [],
|
|
475
454
|
"idempotencyKey": false
|
|
476
455
|
},
|
|
477
|
-
"
|
|
478
|
-
"method": "
|
|
479
|
-
"path": "/
|
|
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
|
-
"
|
|
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
|
|
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
|
-
|
|
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
|
|
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 (!
|
|
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
|
|
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}${
|
|
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
|
|
2786
|
-
if (
|
|
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
|
|
2794
|
-
if (
|
|
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
|
|
2803
|
-
if (
|
|
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" && (
|
|
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
|
-
|
|
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
|
|
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 -
|
|
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
|
|
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: {
|
|
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
|
|
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
|
-
|
|
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),
|
|
3392
|
-
*
|
|
3393
|
-
* account
|
|
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.
|
|
3399
|
-
|
|
3400
|
-
|
|
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
|
|
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.
|
|
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
|
|
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}${
|
|
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
|
|
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
|
|
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 },
|
|
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
|
-
|
|
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
|
|
4140
|
-
throw new Error(`${created} was created, but its budget was not set: ${
|
|
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 ??
|
|
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 ??
|
|
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 ??
|
|
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 ??
|
|
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
|
|
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
|
|
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 ??
|
|
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${
|
|
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${
|
|
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 ??
|
|
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${
|
|
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
|
|
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
|
|
7098
|
+
const path5 = words.join(" ");
|
|
6893
7099
|
command.configureHelp({ ...command.configureHelp(), optionDescription });
|
|
6894
|
-
const help = COMMAND_HELP[
|
|
7100
|
+
const help = COMMAND_HELP[path5];
|
|
6895
7101
|
if (help) {
|
|
6896
|
-
named.add(
|
|
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(`${
|
|
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(`${
|
|
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
|
|
6913
|
-
if (!named.has(
|
|
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
|
|
6920
|
-
import { randomUUID as
|
|
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 ??
|
|
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,
|
|
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:
|
|
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,
|
|
7480
|
+
async function machineFileWriteCommand(deps, name, path5, options = {}) {
|
|
7275
7481
|
try {
|
|
7276
|
-
const bytes2 = options.from ? new Uint8Array(await
|
|
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:
|
|
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":
|
|
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,
|
|
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
|
|
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:
|
|
7519
|
+
query: { path: path5 },
|
|
7314
7520
|
headers: { "tonbo-expect-activation": options.expectActivation }
|
|
7315
7521
|
}
|
|
7316
7522
|
);
|
|
7317
|
-
noticeWake(deps,
|
|
7523
|
+
noticeWake(deps, stat2.machine, name);
|
|
7318
7524
|
print(
|
|
7319
7525
|
deps,
|
|
7320
7526
|
options.json,
|
|
7321
|
-
|
|
7527
|
+
stat2,
|
|
7322
7528
|
fields(
|
|
7323
7529
|
["path", "type", "size", "mode", "uid", "gid", "mtime", "etag", "target", "durability"].map(
|
|
7324
|
-
(field) => [field, cellText(
|
|
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,
|
|
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:
|
|
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,
|
|
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:
|
|
7574
|
+
query: { path: path5, recursive: options.recursive ? "true" : void 0 },
|
|
7369
7575
|
headers: {
|
|
7370
7576
|
"tonbo-expect-activation": options.expectActivation,
|
|
7371
|
-
"idempotency-key":
|
|
7577
|
+
"idempotency-key": randomUUID8()
|
|
7372
7578
|
}
|
|
7373
7579
|
});
|
|
7374
|
-
print(deps, options.json, { path:
|
|
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
|
|
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
|
|
7708
|
+
await mkdir4(directory, { recursive: true, mode: 448 });
|
|
7503
7709
|
await chmod2(directory, 448);
|
|
7504
|
-
if (!
|
|
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(
|
|
7521
|
-
const temporary = `${
|
|
7726
|
+
async function writeAtomic(path5, data) {
|
|
7727
|
+
const temporary = `${path5}.${process.pid}.tmp`;
|
|
7522
7728
|
await writeFile2(temporary, data, { mode: 384 });
|
|
7523
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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 &&
|
|
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
|
|
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
|
|
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
|
-
)
|
|
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(
|
|
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"
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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(
|