@tonbo/cli 0.14.0 → 0.15.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.
Files changed (2) hide show
  1. package/dist/bin/tonbo.js +1377 -74
  2. package/package.json +1 -1
package/dist/bin/tonbo.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/app.ts
4
4
  import { Command, InvalidArgumentError, Option } from "commander";
5
- import { readFileSync } from "node:fs";
5
+ import { readFileSync as readFileSync2 } from "node:fs";
6
6
 
7
7
  // ../../packages/tonbo-cli-core/dist/src/generated/contracts.js
8
8
  var agentNameContract = {
@@ -574,6 +574,13 @@ var managementOperations = {
574
574
  "query": [],
575
575
  "idempotencyKey": false
576
576
  },
577
+ "leaveYieldOrganization": {
578
+ "method": "POST",
579
+ "path": "/yield/leave",
580
+ "pathParams": [],
581
+ "query": [],
582
+ "idempotencyKey": false
583
+ },
577
584
  "recordYieldMetric": {
578
585
  "method": "POST",
579
586
  "path": "/yield/metrics",
@@ -600,6 +607,13 @@ var managementOperations = {
600
607
  "query": [],
601
608
  "idempotencyKey": false
602
609
  },
610
+ "createYieldPickup": {
611
+ "method": "POST",
612
+ "path": "/yield/pickups",
613
+ "pathParams": [],
614
+ "query": [],
615
+ "idempotencyKey": false
616
+ },
603
617
  "listYieldQuestions": {
604
618
  "method": "GET",
605
619
  "path": "/yield/questions",
@@ -794,6 +808,16 @@ var managementOperations = {
794
808
  "idempotencyKey": false,
795
809
  "cli": "tonbo machine sleep"
796
810
  },
811
+ "wakeMachine": {
812
+ "method": "POST",
813
+ "path": "/machines/{name}/wake",
814
+ "pathParams": [
815
+ "name"
816
+ ],
817
+ "query": [],
818
+ "idempotencyKey": false,
819
+ "cli": "tonbo machine wake"
820
+ },
797
821
  "listMachineActivations": {
798
822
  "method": "GET",
799
823
  "path": "/machines/{name}/activations",
@@ -815,6 +839,38 @@ var managementOperations = {
815
839
  "idempotencyKey": false,
816
840
  "cli": "tonbo machine shapes"
817
841
  },
842
+ "listMachineSecrets": {
843
+ "method": "GET",
844
+ "path": "/machines/{name}/secrets",
845
+ "pathParams": [
846
+ "name"
847
+ ],
848
+ "query": [],
849
+ "idempotencyKey": false,
850
+ "cli": "tonbo machine secret list"
851
+ },
852
+ "setMachineSecret": {
853
+ "method": "PUT",
854
+ "path": "/machines/{name}/secrets/{key}",
855
+ "pathParams": [
856
+ "name",
857
+ "key"
858
+ ],
859
+ "query": [],
860
+ "idempotencyKey": false,
861
+ "cli": "tonbo machine secret set"
862
+ },
863
+ "unsetMachineSecret": {
864
+ "method": "DELETE",
865
+ "path": "/machines/{name}/secrets/{key}",
866
+ "pathParams": [
867
+ "name",
868
+ "key"
869
+ ],
870
+ "query": [],
871
+ "idempotencyKey": false,
872
+ "cli": "tonbo machine secret unset"
873
+ },
818
874
  "execMachineCommand": {
819
875
  "method": "POST",
820
876
  "path": "/machines/{name}/exec",
@@ -892,6 +948,101 @@ var managementOperations = {
892
948
  "cli": "tonbo machine file list",
893
949
  "cell": true
894
950
  },
951
+ "listMachineProcesses": {
952
+ "method": "GET",
953
+ "path": "/machines/{name}/processes",
954
+ "pathParams": [
955
+ "name"
956
+ ],
957
+ "query": [],
958
+ "idempotencyKey": false,
959
+ "cli": "tonbo machine ps",
960
+ "cell": true
961
+ },
962
+ "startMachineProcess": {
963
+ "method": "POST",
964
+ "path": "/machines/{name}/processes",
965
+ "pathParams": [
966
+ "name"
967
+ ],
968
+ "query": [],
969
+ "idempotencyKey": true,
970
+ "cli": "tonbo machine exec --detach",
971
+ "cell": true
972
+ },
973
+ "getMachineProcess": {
974
+ "method": "GET",
975
+ "path": "/machines/{name}/processes/{process}",
976
+ "pathParams": [
977
+ "name",
978
+ "process"
979
+ ],
980
+ "query": [],
981
+ "idempotencyKey": false,
982
+ "cli": "tonbo machine wait",
983
+ "cell": true
984
+ },
985
+ "killMachineProcess": {
986
+ "method": "DELETE",
987
+ "path": "/machines/{name}/processes/{process}",
988
+ "pathParams": [
989
+ "name",
990
+ "process"
991
+ ],
992
+ "query": [],
993
+ "idempotencyKey": false,
994
+ "cli": "tonbo machine kill",
995
+ "cell": true
996
+ },
997
+ "readMachineProcessOutput": {
998
+ "method": "GET",
999
+ "path": "/machines/{name}/processes/{process}/output",
1000
+ "pathParams": [
1001
+ "name",
1002
+ "process"
1003
+ ],
1004
+ "query": [
1005
+ "offset"
1006
+ ],
1007
+ "idempotencyKey": false,
1008
+ "cli": "tonbo machine output",
1009
+ "cell": true
1010
+ },
1011
+ "listMachineHolds": {
1012
+ "method": "GET",
1013
+ "path": "/machines/{name}/holds",
1014
+ "pathParams": [
1015
+ "name"
1016
+ ],
1017
+ "query": [],
1018
+ "idempotencyKey": false,
1019
+ "cli": "tonbo machine hold ls",
1020
+ "cell": true
1021
+ },
1022
+ "setMachineHold": {
1023
+ "method": "PUT",
1024
+ "path": "/machines/{name}/holds/{hold}",
1025
+ "pathParams": [
1026
+ "name",
1027
+ "hold"
1028
+ ],
1029
+ "query": [],
1030
+ "idempotencyKey": false,
1031
+ "cli": "tonbo machine hold set",
1032
+ "cell": true
1033
+ },
1034
+ "releaseMachineHold": {
1035
+ "method": "DELETE",
1036
+ "path": "/machines/{name}/holds/{hold}",
1037
+ "pathParams": [
1038
+ "name",
1039
+ "hold"
1040
+ ],
1041
+ "query": [],
1042
+ "idempotencyKey": false,
1043
+ "cli": "tonbo machine hold rm",
1044
+ "cell": true
1045
+ },
895
1046
  "issueSshCertificate": {
896
1047
  "method": "POST",
897
1048
  "path": "/ssh/certificates",
@@ -1027,6 +1178,182 @@ var managementOperations = {
1027
1178
  "cli": "tonbo stream usage"
1028
1179
  }
1029
1180
  };
1181
+ 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
+ "getMachineActivation": {
1219
+ "method": "GET",
1220
+ "path": "/machines/{name}/activations/{activation}",
1221
+ "pathParams": [
1222
+ "name",
1223
+ "activation"
1224
+ ],
1225
+ "query": [
1226
+ "cursor",
1227
+ "limit"
1228
+ ],
1229
+ "idempotencyKey": false,
1230
+ "cli": "tonbo machine activation show"
1231
+ },
1232
+ "getMachineUsage": {
1233
+ "method": "GET",
1234
+ "path": "/machines/{name}/usage",
1235
+ "pathParams": [
1236
+ "name"
1237
+ ],
1238
+ "query": [
1239
+ "from",
1240
+ "to",
1241
+ "cursor",
1242
+ "limit"
1243
+ ],
1244
+ "idempotencyKey": false,
1245
+ "cli": "tonbo machine usage"
1246
+ },
1247
+ "setMachineProcessDeadline": {
1248
+ "method": "PUT",
1249
+ "path": "/machines/{machine}/processes/{process}/deadline",
1250
+ "pathParams": [
1251
+ "machine",
1252
+ "process"
1253
+ ],
1254
+ "query": [],
1255
+ "idempotencyKey": false,
1256
+ "cli": "tonbo machine extend",
1257
+ "cell": true
1258
+ },
1259
+ "signalMachineProcess": {
1260
+ "method": "POST",
1261
+ "path": "/machines/{machine}/processes/{process}/signal",
1262
+ "pathParams": [
1263
+ "machine",
1264
+ "process"
1265
+ ],
1266
+ "query": [],
1267
+ "idempotencyKey": false,
1268
+ "cli": "tonbo machine signal",
1269
+ "cell": true
1270
+ },
1271
+ "createMachineDirectory": {
1272
+ "method": "PUT",
1273
+ "path": "/machines/{machine}/files/directory",
1274
+ "pathParams": [
1275
+ "machine"
1276
+ ],
1277
+ "query": [
1278
+ "path",
1279
+ "parents",
1280
+ "mode",
1281
+ "user"
1282
+ ],
1283
+ "idempotencyKey": true,
1284
+ "cli": "tonbo machine file mkdir",
1285
+ "cell": true
1286
+ },
1287
+ "renameMachineFile": {
1288
+ "method": "POST",
1289
+ "path": "/machines/{machine}/files/rename",
1290
+ "pathParams": [
1291
+ "machine"
1292
+ ],
1293
+ "query": [
1294
+ "user"
1295
+ ],
1296
+ "idempotencyKey": true,
1297
+ "cli": "tonbo machine file mv",
1298
+ "cell": true
1299
+ },
1300
+ "getMachineFileArchive": {
1301
+ "method": "GET",
1302
+ "path": "/machines/{machine}/files/archive",
1303
+ "pathParams": [
1304
+ "machine"
1305
+ ],
1306
+ "query": [
1307
+ "path",
1308
+ "format",
1309
+ "user"
1310
+ ],
1311
+ "idempotencyKey": false,
1312
+ "cli": "tonbo machine file archive",
1313
+ "cell": true
1314
+ },
1315
+ "readMachineLogs": {
1316
+ "method": "GET",
1317
+ "path": "/machines/{machine}/logs",
1318
+ "pathParams": [
1319
+ "machine"
1320
+ ],
1321
+ "query": [
1322
+ "activation",
1323
+ "cursor",
1324
+ "since",
1325
+ "limit",
1326
+ "follow"
1327
+ ],
1328
+ "idempotencyKey": false,
1329
+ "cli": "tonbo machine logs",
1330
+ "cell": true
1331
+ },
1332
+ "listSshCertificates": {
1333
+ "method": "GET",
1334
+ "path": "/ssh/certificates",
1335
+ "pathParams": [],
1336
+ "query": [
1337
+ "member",
1338
+ "cursor",
1339
+ "limit"
1340
+ ],
1341
+ "idempotencyKey": false,
1342
+ "cli": "tonbo ssh certificate list",
1343
+ "cell": true
1344
+ },
1345
+ "revokeSshCertificate": {
1346
+ "method": "DELETE",
1347
+ "path": "/ssh/certificates/{certificate}",
1348
+ "pathParams": [
1349
+ "certificate"
1350
+ ],
1351
+ "query": [],
1352
+ "idempotencyKey": false,
1353
+ "cli": "tonbo ssh certificate revoke",
1354
+ "cell": true
1355
+ }
1356
+ };
1030
1357
  var accountRoles = [
1031
1358
  "owner",
1032
1359
  "admin",
@@ -1641,9 +1968,15 @@ var machinePortAccess = [
1641
1968
  var machineTimings = {
1642
1969
  "pollAfterSeconds": 2,
1643
1970
  "sleepPollAfterSeconds": 5,
1971
+ "wakePollAfterSeconds": 2,
1644
1972
  "wakeHoldSeconds": 25,
1645
1973
  "earlyReclaimSeconds": 120
1646
1974
  };
1975
+ var machineSecretLimits = {
1976
+ "perMachine": 64,
1977
+ "valueMaxBytes": 65536,
1978
+ "totalMaxBytes": 262144
1979
+ };
1647
1980
 
1648
1981
  // ../../packages/tonbo-cli-core/dist/src/bearer.js
1649
1982
  var MANAGEMENT_AUDIENCE = cliSessionContract.audiences.management;
@@ -1708,6 +2041,9 @@ async function requestJson(fetcher, url, init = {}) {
1708
2041
 
1709
2042
  // ../../packages/tonbo-cli-core/dist/src/api.js
1710
2043
  var DEFAULT_API_ORIGIN = "https://api.tonbo.dev";
2044
+ function managementOperation(operationId) {
2045
+ return operationId in managementOperations ? managementOperations[operationId] : plannedManagementOperations[operationId];
2046
+ }
1711
2047
  var DEFAULT_LOGIN_HINT2 = credentialsContract.reader.login_hint;
1712
2048
  var ExchangeRefusal = class extends HttpError {
1713
2049
  };
@@ -1752,7 +2088,7 @@ var ManagementApi = class {
1752
2088
  }
1753
2089
  }
1754
2090
  async requestWith(operationId, key, { params = {}, query = {}, body, idempotencyKey, ifMatch }, seen) {
1755
- const operation = managementOperations[operationId];
2091
+ const operation = managementOperation(operationId);
1756
2092
  const path4 = operation.path.replace(/\{([^}]+)\}/g, (_, name) => {
1757
2093
  const value = params[name];
1758
2094
  if (value === void 0)
@@ -1818,6 +2154,27 @@ var ManagementApi = class {
1818
2154
  idempotencyKey
1819
2155
  }).catch(unconfirmed("Agent creation", idempotencyKey, "the same name, role and region"));
1820
2156
  }
2157
+ /** The account's budgets (planned: 404 until budgets are served). */
2158
+ listBudgets(key) {
2159
+ return this.call("listBudgets", key);
2160
+ }
2161
+ getBudget(key, budgetId) {
2162
+ return this.call("getBudget", key, { params: { budget_id: budgetId } });
2163
+ }
2164
+ /** A replay with the same idempotency key returns the budget the first
2165
+ * request created. */
2166
+ createBudget(key, request, idempotencyKey) {
2167
+ return this.call("createBudget", key, {
2168
+ body: withoutUndefined(request),
2169
+ idempotencyKey
2170
+ }).catch(unconfirmed("Budget creation", idempotencyKey, "the same name, limit and members"));
2171
+ }
2172
+ changeBudget(key, budgetId, change) {
2173
+ return this.call("changeBudget", key, {
2174
+ params: { budget_id: budgetId },
2175
+ body: withoutUndefined(change)
2176
+ });
2177
+ }
1821
2178
  listApiKeys(key, page) {
1822
2179
  return this.page("listApiKeys", key, page);
1823
2180
  }
@@ -1923,6 +2280,21 @@ var ManagementApi = class {
1923
2280
  listMachines(key, page) {
1924
2281
  return this.page("listMachines", key, page);
1925
2282
  }
2283
+ async listMachineSecrets(key, name) {
2284
+ const answer = await this.request("listMachineSecrets", key, {
2285
+ params: { name }
2286
+ });
2287
+ return answer.data;
2288
+ }
2289
+ setMachineSecret(key, name, secretKey, value) {
2290
+ return this.call("setMachineSecret", key, {
2291
+ params: { name, key: secretKey },
2292
+ body: value
2293
+ });
2294
+ }
2295
+ unsetMachineSecret(key, name, secretKey) {
2296
+ return this.request("unsetMachineSecret", key, { params: { name, key: secretKey } });
2297
+ }
1926
2298
  /**
1927
2299
  * A replay with the same idempotency key returns the Machine the first
1928
2300
  * request created, so a transport failure or a 5xx answer is retried here
@@ -2022,6 +2394,18 @@ var ManagementApi = class {
2022
2394
  });
2023
2395
  return { machine: answer.data, retryAfterSeconds: retryAfterSeconds(headers.retryAfter) };
2024
2396
  }
2397
+ /** With `etag`, only the Machine that ETag names, at that revision, is
2398
+ * woken (machine-v2.json `etag.if_match.wake` is optional). A 202 carries
2399
+ * the `Retry-After` to read the Machine again after (machine-v2.json
2400
+ * `wake.poll_after_seconds`); a 200 (already awake) carries none. A
2401
+ * refused wake throws the API's problem. */
2402
+ async wakeMachine(key, name, etag) {
2403
+ const headers = { retryAfter: null };
2404
+ const answer = await this.request("wakeMachine", key, { params: { name }, ...etag ? { ifMatch: etag } : {} }, (response) => {
2405
+ headers.retryAfter = response.headers.get("retry-after");
2406
+ });
2407
+ return { machine: answer.data, retryAfterSeconds: retryAfterSeconds(headers.retryAfter) };
2408
+ }
2025
2409
  /** The Machine being deleted or the device forgotten, with its kind; the
2026
2410
  * Machine is null when the API answered 204, whose `X-Tonbo-Machine-Kind`
2027
2411
  * says whether a name was released (`managed`: its create never reached its
@@ -2092,11 +2476,42 @@ import os4 from "node:os";
2092
2476
  import { promisify as promisify2 } from "node:util";
2093
2477
 
2094
2478
  // ../../packages/tonbo-cli-core/dist/src/callback-page.js
2095
- var WORDMARK = [
2096
- "\u2588\u2588\u2588\u2580\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2580\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588",
2097
- "\u2588\u2588\u2584 \u2584\u2588\u2588\u2580\u2584 \u2588\u2588 \u2584\u2580\u2588\u2588 \u2584 \u2588\u2588 \u2584\u2580\u2588",
2098
- "\u2588\u2588\u2588\u2584\u2584\u2588\u2588\u2584\u2584\u2588\u2588\u2588\u2584\u2588\u2584\u2588\u2588\u2584\u2584\u2588\u2588\u2588\u2588\u2584\u2584\u2588"
2099
- ].join("\n");
2479
+ var LOGO_PIXELS = [
2480
+ [0, 5],
2481
+ [5, 0],
2482
+ [5, 5],
2483
+ [5, 10],
2484
+ [5, 15],
2485
+ [10, 5],
2486
+ [10, 15],
2487
+ [21, 10],
2488
+ [21, 15],
2489
+ [26, 5],
2490
+ [26, 15],
2491
+ [31, 5],
2492
+ [31, 10],
2493
+ [42, 5],
2494
+ [42, 10],
2495
+ [42, 15],
2496
+ [47, 5],
2497
+ [52, 10],
2498
+ [52, 15],
2499
+ [63, 0],
2500
+ [63, 5],
2501
+ [63, 10],
2502
+ [63, 15],
2503
+ [68, 5],
2504
+ [68, 15],
2505
+ [73, 5],
2506
+ [73, 10],
2507
+ [84, 5],
2508
+ [84, 10],
2509
+ [89, 5],
2510
+ [89, 15],
2511
+ [94, 10],
2512
+ [94, 15]
2513
+ ];
2514
+ var LOGO = `<svg class="wordmark" aria-hidden="true" viewBox="0 0 99 20" width="99" height="20"><g fill="currentColor" shape-rendering="crispEdges">${LOGO_PIXELS.map(([x, y]) => `<rect x="${x}" y="${y}" width="5" height="5"/>`).join("")}</g></svg>`;
2100
2515
  var OUTCOMES = {
2101
2516
  complete: {
2102
2517
  status: 200,
@@ -2133,7 +2548,7 @@ function renderCallbackPage(outcome) {
2133
2548
  --ink: #1d1b17;
2134
2549
  --rule: #c9c5ba;
2135
2550
  --quiet: #5c564a;
2136
- --signal: #d64a03;
2551
+ --signal: #e5673e;
2137
2552
  }
2138
2553
  @media (prefers-color-scheme: dark) {
2139
2554
  :root {
@@ -2141,7 +2556,7 @@ function renderCallbackPage(outcome) {
2141
2556
  --ink: #f3f2ee;
2142
2557
  --rule: #3a362e;
2143
2558
  --quiet: #a49e8f;
2144
- --signal: #f08b4b;
2559
+ --signal: #e5673e;
2145
2560
  }
2146
2561
  }
2147
2562
  * { box-sizing: border-box; }
@@ -2157,21 +2572,9 @@ function renderCallbackPage(outcome) {
2157
2572
  -webkit-font-smoothing: antialiased;
2158
2573
  }
2159
2574
  header { padding: 28px 0; }
2160
- /* Same metrics as .auth-wordmark in the app: the glyphs are the ground
2161
- and the letters are the gaps, so the 8px/10px/scaleY(0.8) combination is
2162
- what makes the rows meet without seams. Iosevka is not available here,
2163
- so it falls back to the platform monospace. */
2164
- pre.wordmark {
2165
- margin: 0;
2166
- font-family: 'Iosevka', ui-monospace, 'SFMono-Regular', Menlo, Consolas, monospace;
2167
- font-size: 8px;
2168
- font-weight: 400;
2169
- line-height: 10px;
2170
- letter-spacing: 0;
2171
- white-space: pre;
2575
+ svg.wordmark {
2576
+ display: block;
2172
2577
  color: var(--signal);
2173
- transform: scaleY(0.8);
2174
- transform-origin: left top;
2175
2578
  }
2176
2579
  main {
2177
2580
  display: flex;
@@ -2202,7 +2605,7 @@ function renderCallbackPage(outcome) {
2202
2605
  </style>
2203
2606
  </head>
2204
2607
  <body>
2205
- <header><pre class="wordmark" aria-hidden="true">${WORDMARK}</pre></header>
2608
+ <header>${LOGO}</header>
2206
2609
  <main>
2207
2610
  <h1>${title}</h1>
2208
2611
  <p>${body}</p>
@@ -2611,6 +3014,14 @@ var SessionClient = class _SessionClient {
2611
3014
  }
2612
3015
  return bearer;
2613
3016
  }
3017
+ /** Drops every token of `accountId` this process keeps, and its bearer. */
3018
+ forget(accountId) {
3019
+ for (const id of [...this.tokens.keys()])
3020
+ if (id.endsWith(` ${accountId}`))
3021
+ this.tokens.delete(id);
3022
+ this.bearers.delete(accountId);
3023
+ this.workspaces.delete(accountId);
3024
+ }
2614
3025
  /** A token of `accountId` for `audience`, kept until a minute before it expires. */
2615
3026
  async token(audience, accountId) {
2616
3027
  const id = `${audience} ${accountId}`;
@@ -2825,6 +3236,29 @@ var LoginStore = class {
2825
3236
  }
2826
3237
  return accounts;
2827
3238
  }
3239
+ /** Forgets an account the person no longer belongs to (they left it, or
3240
+ * were removed): the login's cache of names is read again without it, it
3241
+ * is no longer the default account, and this process's tokens of it are
3242
+ * dropped. The CLI login itself stays: it still reaches every other
3243
+ * account. Without a CLI login there is nothing to forget. */
3244
+ async forgetAccount(accountId) {
3245
+ const client = await this.session().catch(() => null);
3246
+ if (!client)
3247
+ return;
3248
+ await this.refreshAccounts(client).catch(() => void 0);
3249
+ const latest = await this.sessions.load();
3250
+ if (!latest || latest.session.id !== client.stored.session.id)
3251
+ return;
3252
+ const { [accountId]: _forgotten, ...accounts } = latest.accounts ?? {};
3253
+ const saved = {
3254
+ ...latest,
3255
+ accounts,
3256
+ default_account_id: latest.default_account_id === accountId ? null : latest.default_account_id ?? null
3257
+ };
3258
+ await this.sessions.save(saved);
3259
+ client.stored = saved;
3260
+ client.forget(accountId);
3261
+ }
2828
3262
  /** The id of the account `nameOrId` names, looked up again once when the cache misses. */
2829
3263
  async resolve(client, nameOrId) {
2830
3264
  if (UUID2.test(nameOrId))
@@ -3552,7 +3986,7 @@ var TerminalProgress = class {
3552
3986
  };
3553
3987
 
3554
3988
  // src/commands.ts
3555
- import { randomUUID as randomUUID3 } from "node:crypto";
3989
+ import { randomUUID as randomUUID4 } from "node:crypto";
3556
3990
 
3557
3991
  // src/agent-name.ts
3558
3992
  var AGENT_NAME_RULE = agentNameContract.$defs.name;
@@ -3567,22 +4001,225 @@ function validateAgentName(name) {
3567
4001
  }
3568
4002
  }
3569
4003
 
3570
- // src/commands.ts
3571
- function print(deps, json, data, human) {
3572
- deps.output(json ? JSON.stringify(data) : human);
3573
- }
3574
- function printPage(deps, json, page, human) {
3575
- print(deps, json, page.data, human(page.data));
3576
- if (page.next_cursor) deps.notice(`More results: run again with --cursor ${page.next_cursor}`);
4004
+ // src/budget.ts
4005
+ import { randomUUID as randomUUID3 } from "node:crypto";
4006
+ var BUDGETS_NOT_AVAILABLE = "Budgets are not available yet.";
4007
+ function parseDollars(value) {
4008
+ const match = /^\$?([0-9]+)(?:\.([0-9]{1,2}))?$/.exec(value.trim());
4009
+ if (!match) throw new Error(`${value} is not an amount in dollars, such as $50 or 12.50.`);
4010
+ return Number(match[1]) * 1e6 + Number((match[2] ?? "").padEnd(2, "0")) * 1e4;
4011
+ }
4012
+ var isAmount = (value) => /^\$?[0-9]+(?:\.[0-9]{1,2})?$/.test(value.trim());
4013
+ function unavailable(error) {
4014
+ if (error instanceof HttpError && error.status === 404 && error.code !== "budget_not_found")
4015
+ return new HttpError(BUDGETS_NOT_AVAILABLE, 404, error.body, error.requestId);
4016
+ return error;
4017
+ }
4018
+ async function budgets(deps, key) {
4019
+ return deps.api.listBudgets(key).catch((error) => {
4020
+ throw unavailable(error);
4021
+ });
3577
4022
  }
3578
- async function confirm(deps, yes, action, question, expected) {
3579
- if (yes) return;
3580
- if (!deps.interactive())
3581
- throw new Error(`Refusing to ${action} without confirmation: pass --yes.`);
3582
- if (await deps.ask(await question()) !== expected)
3583
- throw new Error("Cancelled; nothing was changed.");
4023
+ async function resolveBudget(deps, key, budget) {
4024
+ const all = await budgets(deps, key);
4025
+ const found = all.find((entry) => entry.id === budget || entry.name === budget);
4026
+ if (!found) throw new Error(`No budget of this account is ${budget}. See \`tonbo budget list\`.`);
4027
+ return found;
4028
+ }
4029
+ function usedText(budget) {
4030
+ if (budget.limit_micros === void 0) return "\u2014";
4031
+ const used = (budget.settled_micros ?? 0) + (budget.committed_micros ?? 0);
4032
+ return `${usd(used)} of ${usd(budget.limit_micros)}`;
4033
+ }
4034
+ function budgetFields(budget) {
4035
+ const rows = [
4036
+ ["ID", budget.id],
4037
+ ["Name", budget.name ?? "\u2014"],
4038
+ ["Scope", budget.scope],
4039
+ ["Stage", budget.stage]
4040
+ ];
4041
+ if (budget.limit_micros !== void 0) {
4042
+ rows.push(["Limit", usd(budget.limit_micros)]);
4043
+ rows.push(["Charged", usd(budget.settled_micros ?? 0)]);
4044
+ rows.push(["Committed", usd(budget.committed_micros ?? 0)]);
4045
+ }
4046
+ if (budget.principals?.length) rows.push(["Covers", budget.principals.join(", ")]);
4047
+ if (budget.write_pace)
4048
+ rows.push([
4049
+ "Writes paced",
4050
+ `${budget.write_pace.units_per_second}/s (floor ${budget.write_pace.floor_units_per_second}/s)`
4051
+ ]);
4052
+ rows.push(["Resets", budget.resets_at]);
4053
+ rows.push(["Raise at", budget.raise_url]);
4054
+ return fields(rows);
3584
4055
  }
3585
- async function regionListCommand(deps, options = {}) {
4056
+ async function budgetListCommand(deps, options) {
4057
+ const all = await budgets(deps, await deps.auth.apiKey());
4058
+ print(
4059
+ deps,
4060
+ options.json,
4061
+ all,
4062
+ all.length ? table(
4063
+ ["ID", "NAME", "SCOPE", "STAGE", "USED", "RESETS"],
4064
+ all.map((budget) => [
4065
+ budget.id,
4066
+ budget.name ?? "\u2014",
4067
+ budget.scope,
4068
+ budget.stage,
4069
+ usedText(budget),
4070
+ budget.resets_at
4071
+ ])
4072
+ ) : "No budgets. An owner or admin sets the account budget in the console; `tonbo budget add` sets one for you or an Agent you created."
4073
+ );
4074
+ }
4075
+ async function budgetShowCommand(deps, budget, options) {
4076
+ const found = await resolveBudget(deps, await deps.auth.apiKey(), budget);
4077
+ print(deps, options.json, found, budgetFields(found));
4078
+ }
4079
+ async function sendToConsole(deps, url, why) {
4080
+ deps.notice(`${why} An owner or admin makes it in the console: ${url}`);
4081
+ if (deps.interactive() && deps.openUrl) await deps.openUrl(url).catch(() => void 0);
4082
+ }
4083
+ async function consoleOr(deps, error) {
4084
+ if (error instanceof HttpError && error.code === "budget_raise_in_console") {
4085
+ const body = error.body;
4086
+ if (typeof body?.raise_url === "string") {
4087
+ await sendToConsole(deps, body.raise_url, "Only the console makes this change.");
4088
+ throw new HttpError(error.message, error.status, error.body, error.requestId);
4089
+ }
4090
+ }
4091
+ throw unavailable(error);
4092
+ }
4093
+ async function budgetAddCommand(deps, name, options) {
4094
+ const key = await deps.auth.apiKey();
4095
+ const limit = parseDollars(options.limit);
4096
+ 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));
4098
+ print(
4099
+ deps,
4100
+ options.json,
4101
+ created,
4102
+ `Created budget ${created.name ?? name}: ${usd(limit)} a month, covering ${principals.join(", ")}.`
4103
+ );
4104
+ }
4105
+ async function budgetSetCommand(deps, budget, options) {
4106
+ if (options.limit === void 0 && !options.add?.length)
4107
+ throw new Error("Nothing to change: pass --limit, --add or both.");
4108
+ const key = await deps.auth.apiKey();
4109
+ const found = await resolveBudget(deps, key, budget);
4110
+ const limit = options.limit === void 0 ? void 0 : parseDollars(options.limit);
4111
+ if (limit !== void 0 && (found.limit_micros === void 0 || limit >= found.limit_micros)) {
4112
+ await sendToConsole(
4113
+ deps,
4114
+ found.raise_url,
4115
+ found.limit_micros === void 0 ? "This budget is not yours to change here." : `${usd(limit)} is not below the budget's ${usd(found.limit_micros)}: the CLI only lowers a budget.`
4116
+ );
4117
+ throw new Error("The budget was not changed.");
4118
+ }
4119
+ const add = options.add?.length ? await Promise.all(options.add.map((member) => resolveMemberSubject(deps, key, member))) : void 0;
4120
+ const changed = await deps.api.changeBudget(key, found.id, { limit_micros: limit, add_principals: add }).catch((error) => consoleOr(deps, error));
4121
+ print(deps, options.json, changed, budgetFields(changed));
4122
+ }
4123
+ async function attachBudget(deps, key, subject, value, name, created) {
4124
+ try {
4125
+ if (isAmount(value)) {
4126
+ const limit = parseDollars(value);
4127
+ const budget = await deps.api.createBudget(
4128
+ key,
4129
+ { scope: "principals", name, limit_micros: limit, principals: [subject] },
4130
+ randomUUID3()
4131
+ ).catch((error) => consoleOr(deps, error));
4132
+ deps.notice(`Budget ${budget.name ?? name}: ${usd(limit)} a month for ${subject}.`);
4133
+ return;
4134
+ }
4135
+ const found = await resolveBudget(deps, key, value);
4136
+ await deps.api.changeBudget(key, found.id, { add_principals: [subject] }).catch((error) => consoleOr(deps, error));
4137
+ deps.notice(`Budget ${found.name ?? found.id} now covers ${subject}.`);
4138
+ } 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}`);
4141
+ }
4142
+ }
4143
+ function accountBudgetFields(account) {
4144
+ const budgets2 = account.budgets;
4145
+ if (!budgets2) return [];
4146
+ const line = (budget) => budget ? `${budget.name ?? budget.id} \xB7 ${budget.stage} \xB7 ${usedText(budget)} \xB7 resets ${budget.resets_at}` : "none";
4147
+ const rows = [
4148
+ ["Account budget", line(budgets2.account)],
4149
+ ["Your budget", line(budgets2.caller)]
4150
+ ];
4151
+ const pace = budgets2.account?.write_pace;
4152
+ if (pace)
4153
+ rows.push([
4154
+ "Writes paced",
4155
+ `${pace.units_per_second}/s across regions (floor ${pace.floor_units_per_second}/s)`
4156
+ ]);
4157
+ return rows;
4158
+ }
4159
+ function refusalBudget(body) {
4160
+ const budget = body?.budget;
4161
+ if (!budget || typeof budget !== "object") return null;
4162
+ const record = budget;
4163
+ return typeof record.id === "string" && typeof record.raise_url === "string" ? record : null;
4164
+ }
4165
+ function budgetHint(error) {
4166
+ const budget = refusalBudget(error.body);
4167
+ const name = budget ? budget.name ?? budget.id : "a budget";
4168
+ switch (error.code) {
4169
+ case "budget_exhausted":
4170
+ return `The budget ${name} is used up${budget?.retry_at ? ` until ${budget.retry_at}` : ""}: stop, and try again only after then. A person can raise it in the console${budget ? ` at ${budget.raise_url}` : ""}.`;
4171
+ case "budget_slowdown":
4172
+ return `The budget ${name} is close to its limit, so only a few of its Machines may be awake at once: sleep another of its Machines${budget?.largest_shape_that_fits ? `, or set this one's shape to ${budget.largest_shape_that_fits}` : ""}, then wake it again. A person can raise it${budget ? ` at ${budget.raise_url}` : " in the console"}.`;
4173
+ case "budget_scope":
4174
+ return "Your budget confines you to the Machines you created: work on one of those, or ask a person to.";
4175
+ case "budget_raise_in_console": {
4176
+ const url = error.body?.raise_url;
4177
+ return `Only the console makes this change${typeof url === "string" ? `: ${url}` : "."}`;
4178
+ }
4179
+ default:
4180
+ return null;
4181
+ }
4182
+ }
4183
+ function budgetHeaderLine(header) {
4184
+ const values = /* @__PURE__ */ new Map();
4185
+ for (const member of header.split(",")) {
4186
+ const [name, ...rest] = member.trim().split("=");
4187
+ if (!name) continue;
4188
+ values.set(name.trim(), rest.join("=").trim().replace(/^"|"$/g, ""));
4189
+ }
4190
+ const used = values.get("used");
4191
+ const percent = used === void 0 ? "" : `, ${Math.round(Number(used) * 100)}% used`;
4192
+ return `Budget: ${values.get("scope") === "account" ? "the account budget" : "your budget"} is at ${values.get("stage") ?? "notice"}${percent}; it resets ${values.get("resets") ?? "on the 1st"}.`;
4193
+ }
4194
+ function noticingBudget(fetcher, notice) {
4195
+ let said = false;
4196
+ return async (input, init) => {
4197
+ const response = await fetcher(input, init);
4198
+ const header = response.headers.get("tonbo-budget");
4199
+ if (header && !said) {
4200
+ said = true;
4201
+ notice(budgetHeaderLine(header));
4202
+ }
4203
+ return response;
4204
+ };
4205
+ }
4206
+
4207
+ // src/commands.ts
4208
+ function print(deps, json, data, human) {
4209
+ deps.output(json ? JSON.stringify(data) : human);
4210
+ }
4211
+ function printPage(deps, json, page, human) {
4212
+ print(deps, json, page.data, human(page.data));
4213
+ if (page.next_cursor) deps.notice(`More results: run again with --cursor ${page.next_cursor}`);
4214
+ }
4215
+ async function confirm(deps, yes, action, question, expected) {
4216
+ if (yes) return;
4217
+ if (!deps.interactive())
4218
+ throw new Error(`Refusing to ${action} without confirmation: pass --yes.`);
4219
+ if (await deps.ask(await question()) !== expected)
4220
+ throw new Error("Cancelled; nothing was changed.");
4221
+ }
4222
+ async function regionListCommand(deps, options = {}) {
3586
4223
  const regions = await deps.api.listRegions(await deps.auth.apiKey());
3587
4224
  print(
3588
4225
  deps,
@@ -3609,7 +4246,7 @@ async function agentCreateCommand(deps, name, options = {}) {
3609
4246
  const agent = await deps.api.createAgent(
3610
4247
  key,
3611
4248
  { name, region, ...options.role === void 0 ? {} : { role: options.role } },
3612
- options.idempotencyKey ?? randomUUID3()
4249
+ options.idempotencyKey ?? randomUUID4()
3613
4250
  );
3614
4251
  print(
3615
4252
  deps,
@@ -3622,6 +4259,15 @@ async function agentCreateCommand(deps, name, options = {}) {
3622
4259
  `Region: ${agent.region}`
3623
4260
  ].join("\n")
3624
4261
  );
4262
+ if (options.budget !== void 0)
4263
+ await attachBudget(
4264
+ deps,
4265
+ key,
4266
+ `agent.${agent.id}`,
4267
+ options.budget,
4268
+ agent.name,
4269
+ `The Agent ${agent.name}`
4270
+ );
3625
4271
  }
3626
4272
  async function accountShowCommand(deps, options = {}) {
3627
4273
  const account = await deps.api.getAccount(await deps.auth.apiKey());
@@ -3635,7 +4281,8 @@ async function accountShowCommand(deps, options = {}) {
3635
4281
  ["Kind", account.kind],
3636
4282
  ["Your role", account.member.role],
3637
4283
  ["Your subject", account.member.subject],
3638
- ["Bucket limit", String(account.limits.buckets)]
4284
+ ["Bucket limit", String(account.limits.buckets)],
4285
+ ...accountBudgetFields(account)
3639
4286
  ])
3640
4287
  );
3641
4288
  }
@@ -3840,6 +4487,15 @@ async function apiKeyCreateCommand(deps, name, options = {}) {
3840
4487
  deps.notice(
3841
4488
  "Do not set TONBO_API_KEY in your own shell: it overrides your `tonbo login`, for this CLI too."
3842
4489
  );
4490
+ if (options.budget !== void 0)
4491
+ await attachBudget(
4492
+ deps,
4493
+ key,
4494
+ created.member,
4495
+ options.budget,
4496
+ created.name,
4497
+ `The API key ${created.name}`
4498
+ );
3843
4499
  }
3844
4500
  async function apiKeyShowCommand(deps, keyId, options = {}) {
3845
4501
  const apiKey = await deps.api.getApiKey(await deps.auth.apiKey(), keyId);
@@ -3898,7 +4554,7 @@ async function bucketCreateCommand(deps, name, options = {}) {
3898
4554
  const bucket = await deps.api.createBucket(
3899
4555
  await deps.auth.apiKey(),
3900
4556
  { name, region: options.region },
3901
- options.idempotencyKey ?? randomUUID3()
4557
+ options.idempotencyKey ?? randomUUID4()
3902
4558
  );
3903
4559
  print(
3904
4560
  deps,
@@ -4029,7 +4685,7 @@ async function databaseCreateCommand(deps, name, options = {}) {
4029
4685
  const database = await deps.api.createDatabase(
4030
4686
  await deps.auth.apiKey(),
4031
4687
  { name, region: options.region, expires_at: expiresAt },
4032
- options.idempotencyKey ?? randomUUID3()
4688
+ options.idempotencyKey ?? randomUUID4()
4033
4689
  );
4034
4690
  print(
4035
4691
  deps,
@@ -4217,7 +4873,7 @@ async function streamCreateCommand(deps, name, options = {}) {
4217
4873
  content_type: options.contentType,
4218
4874
  expires_at: expiresAt
4219
4875
  },
4220
- options.idempotencyKey ?? randomUUID3()
4876
+ options.idempotencyKey ?? randomUUID4()
4221
4877
  );
4222
4878
  print(
4223
4879
  deps,
@@ -4441,10 +5097,11 @@ async function accountUseCommand(deps, account, options = {}) {
4441
5097
  import { Help } from "commander";
4442
5098
 
4443
5099
  // src/machine.ts
4444
- import { randomUUID as randomUUID5 } from "node:crypto";
5100
+ import { randomUUID as randomUUID6 } from "node:crypto";
5101
+ import { readFileSync } from "node:fs";
4445
5102
 
4446
5103
  // src/volume.ts
4447
- import { randomUUID as randomUUID4 } from "node:crypto";
5104
+ import { randomUUID as randomUUID5 } from "node:crypto";
4448
5105
  var VOLUME_DURABILITY_LINK = "https://tonbo.dev/docs/machines/guarantees/data#volume";
4449
5106
  var VOLUME_ERROR_HINTS = {
4450
5107
  payment_required: "Add credit at <billing>, then create the Volume again.",
@@ -4479,7 +5136,19 @@ function volumeFields(volume) {
4479
5136
  ["Machine", boundText(volume)],
4480
5137
  ["Size", `${volume.size_gib} GiB`],
4481
5138
  ["Live bytes", `${bytes(volume.live_bytes)} (billed)`],
4482
- ["Last commit", volume.last_commit_at ?? "none yet"],
5139
+ ["Last meter report", volume.last_commit_at ?? "none yet"],
5140
+ [
5141
+ "Restored to",
5142
+ volume.restored_to ? `${volume.restored_to.committed_at} (generation ${volume.restored_to.data_generation})` : "none"
5143
+ ],
5144
+ [
5145
+ "Backed up to",
5146
+ volume.backed_up_to ? volume.backed_up_to.committed_at ? `${volume.backed_up_to.committed_at} (generation ${volume.backed_up_to.data_generation}; ${volume.backed_up_to.dirty_bytes} bytes pending copy)` : `empty snapshot (generation ${volume.backed_up_to.data_generation}; ${volume.backed_up_to.dirty_bytes} bytes pending copy)` : "unknown"
5147
+ ],
5148
+ [
5149
+ "Cold GETs",
5150
+ volume.cold ? `${volume.cold.demand_gets} demand, ${volume.cold.hydrate_gets} hydration` : "none"
5151
+ ],
4483
5152
  ["Durability", VOLUME_DURABILITY_LINK],
4484
5153
  ["Created", volume.created_at]
4485
5154
  ]);
@@ -4530,7 +5199,7 @@ async function volumeCreateCommand(deps, name, options = {}) {
4530
5199
  const volume = await deps.api.createVolume(
4531
5200
  key,
4532
5201
  { name, region, size_gib: sizeGib },
4533
- options.idempotencyKey ?? randomUUID4()
5202
+ options.idempotencyKey ?? randomUUID5()
4534
5203
  );
4535
5204
  print(
4536
5205
  deps,
@@ -4595,7 +5264,10 @@ var MACHINE_ERROR_HINTS = {
4595
5264
  image_pulling: "The Machine's image is still being pulled; the request was not run. Try again shortly; `tonbo machine show` reports the image's state.",
4596
5265
  image_failed: "The Machine's image could not be pulled; `tonbo machine show` says why. Fix the image, then name it again with `tonbo machine update --image`.",
4597
5266
  machine_kind_mismatch: "That Machine is one of your computers: `tonbo machine show` and `tonbo machine delete` serve it, and nothing else does.",
4598
- volumes_not_offered: "Volumes are not offered to this account in the Machine's region yet; nothing was created or changed."
5267
+ volumes_not_offered: "Volumes are not offered to this account in the Machine's region yet; nothing was created or changed.",
5268
+ secret_env_conflict: "Use another secret key or remove the non-secret env variable with that name.",
5269
+ secret_limit: "Reduce the number or byte size of this Machine's secrets.",
5270
+ wake_unavailable: "The wake was refused for now; nothing woke. Wake it again after a while; `tonbo machine show` says why it is blocked."
4599
5271
  };
4600
5272
  var MACHINE_IMAGE_FAILURE_HINTS = {
4601
5273
  image_unavailable: "The registry no longer serves that image publicly, or served other bytes for it: publish it again, or use another image",
@@ -4627,7 +5299,7 @@ function ifMatchHeader(value) {
4627
5299
  }
4628
5300
  function explainMachineError(error) {
4629
5301
  if (!(error instanceof HttpError) || error.code === null) return error;
4630
- const hint = MACHINE_ERROR_HINTS[error.code];
5302
+ const hint = budgetHint(error) ?? MACHINE_ERROR_HINTS[error.code];
4631
5303
  if (!hint) return error;
4632
5304
  return new HttpError(`${error.message}
4633
5305
  ${hint}`, error.status, error.body, error.requestId);
@@ -4657,7 +5329,13 @@ function portText(port) {
4657
5329
  function machinePortsText(machine) {
4658
5330
  return machine.ports.length ? machine.ports.map(portText).join(", ") : "none";
4659
5331
  }
4660
- var { earlyReclaimSeconds, pollAfterSeconds, sleepPollAfterSeconds, wakeHoldSeconds } = machineTimings;
5332
+ var {
5333
+ earlyReclaimSeconds,
5334
+ pollAfterSeconds,
5335
+ sleepPollAfterSeconds,
5336
+ wakePollAfterSeconds,
5337
+ wakeHoldSeconds
5338
+ } = machineTimings;
4661
5339
  function machineState(machine) {
4662
5340
  if (machine.failure)
4663
5341
  return `${machine.state}: ${machine.failure.code}: ${machine.failure.message}`;
@@ -4704,7 +5382,9 @@ var MACHINE_WAKE_BLOCKED_HINTS = {
4704
5382
  meter_lag: "Tonbo could not confirm the account's usage in time; the next request tries again.",
4705
5383
  crash_backoff: "Its recent wakes failed; see `tonbo machine activations <name>`.",
4706
5384
  capacity: "No host had room for its shape; the next request tries again.",
4707
- region_closed: "Machines are closed in its region for now; the next request after they reopen wakes it."
5385
+ region_closed: "Machines are closed in its region for now; the next request after they reopen wakes it.",
5386
+ budget_slowdown: "Its budget is close to running out, and this wake would pass the budget's slow-down rate: sleep another Machine of the budget, or lower this one's shape, then wake it again. Machines already awake keep running.",
5387
+ budget_exhausted: "Its budget is used up for the month: it wakes after the budget resets, or once a person raises it in the console."
4708
5388
  };
4709
5389
  var MACHINE_ACCOUNT_EGRESS_BLOCKED_HINT = "If this is the free egress cap: 10 GiB a month without purchased credit; buy credit at <billing> to lift it.";
4710
5390
  function conditionText(name, condition, billing) {
@@ -4885,7 +5565,7 @@ function specWarnings(deps, _ports, envKeys) {
4885
5565
  const secret = envKeys.filter((key) => SECRET_LOOKING.test(key)).sort();
4886
5566
  if (secret.length)
4887
5567
  deps.notice(
4888
- `${secret.join(", ")} ${secret.length === 1 ? "looks" : "look"} secret: env values are readable by every member through the API; Machine secrets come later.`
5568
+ `${secret.join(", ")} ${secret.length === 1 ? "looks" : "look"} secret: env values are readable by every member through the API; use Machine secrets for private values.`
4889
5569
  );
4890
5570
  }
4891
5571
  function waitTimeout(options) {
@@ -4912,6 +5592,41 @@ async function waitForMachine(deps, key, name, firstWait, every, timeout, settle
4912
5592
  wait = every;
4913
5593
  }
4914
5594
  }
5595
+ function secretFromFile(key, file, delivery) {
5596
+ if (!key || !file) throw new Error("Pass a secret as KEY=FILE.");
5597
+ const bytes2 = readFileSync(file === "-" ? 0 : file);
5598
+ if (bytes2.byteLength > machineSecretLimits.valueMaxBytes)
5599
+ throw new Error(`Secret ${key} exceeds the ${machineSecretLimits.valueMaxBytes}-byte limit.`);
5600
+ if (delivery === "file") return { delivery, value_base64: bytes2.toString("base64") };
5601
+ let value;
5602
+ try {
5603
+ value = new TextDecoder("utf-8", { fatal: true }).decode(bytes2);
5604
+ } catch {
5605
+ throw new Error(`Secret ${key} is not UTF-8; use --secret-file for binary bytes.`);
5606
+ }
5607
+ return { delivery, value };
5608
+ }
5609
+ function createSecrets(options) {
5610
+ const values = {};
5611
+ let total = 0;
5612
+ for (const [delivery, entries] of [
5613
+ ["env", options.secretEnv],
5614
+ ["file", options.secretFile]
5615
+ ]) {
5616
+ for (const entry of entries ?? []) {
5617
+ const separator = entry.indexOf("=");
5618
+ if (separator < 1) throw new Error("Pass a secret as KEY=FILE.");
5619
+ const key = entry.slice(0, separator);
5620
+ if (Object.hasOwn(values, key)) throw new Error(`Secret ${key} was named twice.`);
5621
+ const secret = secretFromFile(key, entry.slice(separator + 1), delivery);
5622
+ total += "value" in secret ? Buffer.byteLength(secret.value, "utf8") : Buffer.from(secret.value_base64, "base64").byteLength;
5623
+ if (Object.keys(values).length >= machineSecretLimits.perMachine || total > machineSecretLimits.totalMaxBytes)
5624
+ throw new Error("Machine secrets exceed their count or total byte limit.");
5625
+ values[key] = secret;
5626
+ }
5627
+ }
5628
+ return values;
5629
+ }
4915
5630
  function deleteAfterSeconds(option) {
4916
5631
  if (typeof option === "number") return option;
4917
5632
  return option === false ? null : void 0;
@@ -4964,6 +5679,50 @@ async function machineListCommand(deps, options = {}) {
4964
5679
  );
4965
5680
  });
4966
5681
  }
5682
+ async function machineSecretListCommand(deps, name, options = {}) {
5683
+ await machineCommand(async () => {
5684
+ const secrets = await deps.api.listMachineSecrets(await deps.auth.apiKey(), name);
5685
+ print(
5686
+ deps,
5687
+ options.json,
5688
+ secrets,
5689
+ secrets.length ? table(
5690
+ ["KEY", "DELIVERY", "VERSION", "UPDATED", "BY"],
5691
+ secrets.map((secret) => [
5692
+ secret.key,
5693
+ secret.delivery,
5694
+ String(secret.version),
5695
+ secret.updated_at,
5696
+ secret.updated_by.sub
5697
+ ])
5698
+ ) : `Machine ${name} has no secrets.`
5699
+ );
5700
+ });
5701
+ }
5702
+ async function machineSecretSetCommand(deps, name, secretKey, options) {
5703
+ await machineCommand(async () => {
5704
+ const entry = await deps.api.setMachineSecret(
5705
+ await deps.auth.apiKey(),
5706
+ name,
5707
+ secretKey,
5708
+ secretFromFile(secretKey, options.from, options.delivery ?? "env")
5709
+ );
5710
+ print(
5711
+ deps,
5712
+ options.json,
5713
+ entry,
5714
+ `Set ${entry.key} on Machine ${name} (version ${entry.version}; applies at the next activation).`
5715
+ );
5716
+ });
5717
+ }
5718
+ async function machineSecretUnsetCommand(deps, name, secretKey) {
5719
+ await machineCommand(async () => {
5720
+ await deps.api.unsetMachineSecret(await deps.auth.apiKey(), name, secretKey);
5721
+ deps.output(
5722
+ `Removed ${secretKey} from Machine ${name}; the next activation will not receive it.`
5723
+ );
5724
+ });
5725
+ }
4967
5726
  function conditionsCell(machine) {
4968
5727
  const conditions = machine.status.conditions ?? [];
4969
5728
  if (!conditions.length) return "-";
@@ -5046,6 +5805,7 @@ async function machineCreateCommand(deps, name, command, options = {}) {
5046
5805
  if (!options.image) throw new Error("Pass --image <reference>.");
5047
5806
  const timeout = waitTimeout(options);
5048
5807
  const env = parseEnv(options.env ?? []);
5808
+ const secrets = createSecrets(options);
5049
5809
  const ports = withDefault(parsePorts(options.port ?? []), options.defaultPort);
5050
5810
  const key = await deps.auth.apiKey();
5051
5811
  const bound = typeof options.volume === "string" ? await resolveVolumeBinding(deps, key, options.volume) : null;
@@ -5068,6 +5828,7 @@ async function machineCreateCommand(deps, name, command, options = {}) {
5068
5828
  command: command.length ? command : void 0,
5069
5829
  ports: ports.length ? ports : void 0,
5070
5830
  env: Object.keys(env).length ? env : void 0,
5831
+ ...Object.keys(secrets).length ? { secrets } : {},
5071
5832
  sleep_after_seconds: options.sleepAfter,
5072
5833
  idle_action: options.idleAction,
5073
5834
  stop_grace_seconds: options.stopGrace,
@@ -5076,7 +5837,7 @@ async function machineCreateCommand(deps, name, command, options = {}) {
5076
5837
  ...database ? { database: database.name } : {},
5077
5838
  ...options.database === false ? { database: null } : {}
5078
5839
  },
5079
- options.idempotencyKey ?? randomUUID5()
5840
+ options.idempotencyKey ?? randomUUID6()
5080
5841
  );
5081
5842
  } catch (error) {
5082
5843
  if (error instanceof HttpError && error.code === "name_taken")
@@ -5116,11 +5877,16 @@ async function machineShowCommand(deps, name, options = {}) {
5116
5877
  const key = await deps.auth.apiKey();
5117
5878
  const { machine, etag } = await deps.api.getMachineWithEtag(key, name);
5118
5879
  const lastEnd = options.json || machine.kind === "byoc" || machine.state !== "active" ? null : await lastActivationEnd(deps, key, machine.name);
5880
+ const processes = options.json || machine.kind === "byoc" || machine.state !== "active" || !deps.processes ? null : await deps.processes(key, machine.region, machine.name);
5881
+ const processLines = processes ? [
5882
+ ...processes.running.map((line) => `Background process: ${line}`),
5883
+ ...processes.last ? [`Last background process: ${processes.last}`] : []
5884
+ ] : [];
5119
5885
  print(
5120
5886
  deps,
5121
5887
  options.json,
5122
5888
  machine,
5123
- anyMachineText(machine, await billingUrl(deps), etag, lastEnd)
5889
+ [anyMachineText(machine, await billingUrl(deps), etag, lastEnd), ...processLines].join("\n")
5124
5890
  );
5125
5891
  });
5126
5892
  }
@@ -5168,9 +5934,14 @@ function appliesText(name, change) {
5168
5934
  lines.push(
5169
5935
  change.delete_after_seconds === null ? "The Machine is no longer deleted for being asleep." : "Delete-after applies now; `tonbo machine show` says when."
5170
5936
  );
5171
- const later = [change.image, change.command, change.shape, change.env, change.volumes].some(
5172
- (field) => field !== void 0
5173
- );
5937
+ const later = [
5938
+ change.image,
5939
+ change.command,
5940
+ change.shape,
5941
+ change.env,
5942
+ change.volumes,
5943
+ change.database
5944
+ ].some((field) => field !== void 0);
5174
5945
  if (later) {
5175
5946
  lines.push(
5176
5947
  `The change applies at the next activation. To apply it now, run \`tonbo machine sleep ${name}\`; the next request boots the new spec.`
@@ -5220,6 +5991,8 @@ async function machineUpdateCommand(deps, name, command, options = {}) {
5220
5991
  if (options.volume === false) change.volumes = [];
5221
5992
  else if (typeof options.volume === "string")
5222
5993
  change.volumes = [(await resolveVolumeBinding(deps, key, options.volume)).binding];
5994
+ if (options.database === false) change.database = null;
5995
+ else if (typeof options.database === "string") change.database = options.database;
5223
5996
  if (options.discardSnapshot) change.discard_snapshot = true;
5224
5997
  if (Object.values(change).every((value) => value === void 0))
5225
5998
  throw new Error("Nothing to change: pass at least one option or a command.");
@@ -5281,6 +6054,42 @@ async function machineSleepCommand(deps, name, options = {}) {
5281
6054
  );
5282
6055
  });
5283
6056
  }
6057
+ function wokenText(machine, waking) {
6058
+ if (!waking) return `Machine ${machine.name} is awake.`;
6059
+ return `Machine ${machine.name} is waking; \`tonbo machine show ${machine.name}\` says when it is awake.`;
6060
+ }
6061
+ async function machineWakeCommand(deps, name, options = {}) {
6062
+ await machineCommand(async () => {
6063
+ const timeout = waitTimeout(options);
6064
+ const key = await deps.auth.apiKey();
6065
+ const { machine, retryAfterSeconds: retryAfterSeconds2 } = await deps.api.wakeMachine(
6066
+ key,
6067
+ name,
6068
+ ifMatchHeader(options.ifMatch)
6069
+ );
6070
+ const waking = retryAfterSeconds2 !== null;
6071
+ if (!options.wait || !waking) {
6072
+ print(deps, options.json, machine, wokenText(machine, waking));
6073
+ return;
6074
+ }
6075
+ const { machine: awake, waited } = await waitForMachine(
6076
+ deps,
6077
+ key,
6078
+ name,
6079
+ retryAfterSeconds2 ?? wakePollAfterSeconds,
6080
+ wakePollAfterSeconds,
6081
+ timeout,
6082
+ (read) => read.status.power === "awake" || read.state !== "active"
6083
+ );
6084
+ if (!awake)
6085
+ throw new Error(
6086
+ `Machine ${name} is not awake after ${waited} s; \`tonbo machine show ${name}\` says why.`
6087
+ );
6088
+ if (awake.status.power !== "awake")
6089
+ throw new Error(`Machine ${name} is ${awake.state}; it did not wake.`);
6090
+ print(deps, options.json, awake, `Machine ${awake.name} is awake (after ${waited} s).`);
6091
+ });
6092
+ }
5284
6093
  async function machineDeleteCommand(deps, name, options = {}) {
5285
6094
  await machineCommand(async () => {
5286
6095
  const key = await deps.auth.apiKey();
@@ -5526,6 +6335,34 @@ var COMMAND_HELP = {
5526
6335
  "--idempotency-key": "a new UUID"
5527
6336
  }
5528
6337
  },
6338
+ "tonbo budget list": {
6339
+ about: [
6340
+ "Lists the account's monthly spend budgets, the account budget first, with each one's stage and what it has used of its limit this UTC month. You see a budget's figures with `usage.read`, or when it covers you; of any other budget only its stage, reset and console link."
6341
+ ],
6342
+ examples: ["tonbo budget list"]
6343
+ },
6344
+ "tonbo budget show": {
6345
+ about: [
6346
+ "Prints one budget: its limit, what was charged and what is committed this month, whom it covers, its stage, when it resets (00:00 UTC on the 1st) and its page in the console."
6347
+ ],
6348
+ examples: ["tonbo budget show pi-agent"],
6349
+ arguments: { budget: "the budget's name or id (`bdg_\u2026`)" }
6350
+ },
6351
+ "tonbo budget add": {
6352
+ about: [
6353
+ "Creates a budget over you, or over Agents you created, which counts the compute and outbound data of the Machines they create. Only the console sets the account budget or covers another member."
6354
+ ],
6355
+ examples: ["tonbo budget add pi-agent --limit 50 --member pi-agent"],
6356
+ arguments: { name: "the budget's name, unique in the account" },
6357
+ defaults: { "--member": "yourself" }
6358
+ },
6359
+ "tonbo budget set": {
6360
+ about: [
6361
+ "Lowers the budget that covers you, or adds Agents you created to it. Raising a limit, renaming, removing and every change to another budget are the console's: the command prints the budget's page and opens it instead."
6362
+ ],
6363
+ examples: ["tonbo budget set pi-agent --limit 25"],
6364
+ arguments: { budget: "the budget's name or id (`bdg_\u2026`)" }
6365
+ },
5529
6366
  "tonbo api-key list": {
5530
6367
  about: [
5531
6368
  "Shows your keys, or every key of the account for an owner or admin, with the last four characters of each secret, whether it is read-only, what it reaches of buckets, databases and streams, and when it expires and was last used."
@@ -5713,7 +6550,7 @@ var COMMAND_HELP = {
5713
6550
  "tonbo machine create": {
5714
6551
  about: [
5715
6552
  "Creates a Machine, a VM booted from an image in one region. It has no database of its own: `--database <name>` binds one of the account's databases of its region, which it then reaches at `TONBO_DATABASE_URL`, and a database binds one Machine at a time; its files can last on a Volume (`--volume`), and its program can open any other database by URL with a key. The image is a public OCI image with a linux/arm64 variant, from Docker Hub or another public registry (`nginx:1.27`, `ghcr.io/org/app:v1`, or one pinned by digest), or `tonbo.dev/platform`, the platform image. A registry reachable only over IPv6, one named by an IP address, and one that redirects its manifests are refused. The CLI resolves a registry image to its linux/arm64 digest, and the image is pulled before the Machine leaves `creating`.",
5716
- "The Machine is served at `https://<name>-<account>.tonbo.sh` on its default port. `--port <port>` serves a private port, which takes a member's token, and `--port <port>:public` an open one, which needs the `machines.public` capability. A command after `--` replaces the image's CMD. The CLI warns on stderr when the Machine has no port, since nothing can wake it then, and when an `--env` key looks secret: env values are readable by every member through the API.",
6553
+ "The Machine is served at `https://<name>-<account>.tonbo.sh` on its default port. `--port <port>` serves a private port, which takes a member's token, and `--port <port>:public` an open one, which needs the `machines.public` capability. A command after `--` replaces the image's CMD. `--secret-env KEY=FILE` and `--secret-file KEY=FILE` read initial secrets from local files and store them with the Machine before its first boot; their values are never returned. The CLI warns on stderr when the Machine has no port, since nothing can wake it then, and when an `--env` key looks secret: env values are readable by every member through the API.",
5717
6554
  "The answer is the Machine while it is `creating`. With `--wait` the CLI reads it until it is `active` or `failed`, or `--timeout` passes: a failure prints what to do and exits 1, and a timeout says it is still creating and undoes nothing. A creation the network or a 5xx answer did not confirm is sent at most three times with the same idempotency key, each retry after its `Retry-After` (at most 30 s) or a short backoff; a `name_taken` refusal says which Machine holds the name and how old it is, in case it is yours from a lost reply. In a region not open for Machines the creation is refused with `region_not_offered`, and nothing is created."
5718
6555
  ],
5719
6556
  examples: ["tonbo machine create web --image nginx:1.27 --port 80:public --wait"],
@@ -5738,10 +6575,32 @@ var COMMAND_HELP = {
5738
6575
  examples: ["tonbo machine show web"],
5739
6576
  arguments: NAME("Machine")
5740
6577
  },
6578
+ "tonbo machine secret list": {
6579
+ about: [
6580
+ "Lists a Machine's secret keys, delivery, version and last writer. The values are never returned."
6581
+ ],
6582
+ examples: ["tonbo machine secret list web"],
6583
+ arguments: { name: "the Machine whose secrets to list" }
6584
+ },
6585
+ "tonbo machine secret set": {
6586
+ about: [
6587
+ "Reads the secret from a local file, or standard input with `--from -`, without putting its value in shell history. `env` delivers UTF-8 text as an environment variable; `file` delivers arbitrary bytes under `$TONBO_SECRETS_DIR`. The new value applies at the next activation."
6588
+ ],
6589
+ examples: ["tonbo machine secret set web API_KEY --from ./api-key.txt"],
6590
+ arguments: { name: "the Machine to change", key: "the secret's environment or file name" },
6591
+ defaults: { "--delivery": "env" }
6592
+ },
6593
+ "tonbo machine secret unset": {
6594
+ about: [
6595
+ "Removes a Machine secret whether or not it exists. A running activation keeps its existing value until the next activation."
6596
+ ],
6597
+ examples: ["tonbo machine secret unset web API_KEY"],
6598
+ arguments: { name: "the Machine to change", key: "the secret to remove" }
6599
+ },
5741
6600
  "tonbo machine update": {
5742
6601
  about: [
5743
6602
  "Changes only what its options name. It reads the Machine and sends the ETag it read, or `--if-match`'s, so a change someone made in between is refused with `precondition_failed` rather than overwritten; a Machine whose creation failed is refused with `machine_failed`. `--env` and `--unset-env` change single variables; `--port` replaces every port, and the current default port stays the default while it is still listed.",
5744
- "Ports and `--sleep-after` apply at once. The image, command, shape and env apply at the next boot, and the command says how to apply them now: `tonbo machine sleep`, after which the next request boots the new spec. Naming the same tag again resolves its digest again.",
6603
+ "Ports and `--sleep-after` apply at once. The image, command, shape, env, Volume and database apply at the next boot, and the command says how to apply them now: `tonbo machine sleep`, after which the next request boots the new spec. Naming the same tag again resolves its digest again. `--database <name>` binds another database of the account in the Machine's region, which no other Machine binds, and `--no-database` binds none; the database it leaves is kept. A Machine created with its own database keeps it.",
5745
6604
  "`--delete-after` applies at once and keeps the clock, which counts from the end of the Machine's last activation (or its creation), so a lower value can delete a Machine that has already slept that long; `--no-delete-after` turns it off. `tonbo machine show` prints when it will be deleted."
5746
6605
  ],
5747
6606
  examples: ["tonbo machine update web --env MODE=staging --sleep-after 300"],
@@ -5757,11 +6616,13 @@ var COMMAND_HELP = {
5757
6616
  "tonbo machine exec": {
5758
6617
  about: [
5759
6618
  "Runs one command in the Machine's running activation through its region's cell origin, waking the Machine first when it is asleep, and streams its stdout and stderr here as they come; a call that woke the Machine prints its hourly ceiling on stderr. The command runs without a shell, as the image's User, and lives exactly as long as the call: there is no terminal and no streamed stdin, and `--stdin` sends at most 1 MiB read before the call.",
5760
- "The CLI exits with the command's status: 128 plus the signal's number when a signal ended it, 124 at `--timeout`, 125 when it was refused, killed or passed 64 MiB of output, 126 or 127 when it could not run, and 255 when the connection ended first and the outcome is unknown. `--json` prints the answer's document instead, with the first and last 512 KiB of each stream."
6619
+ "The CLI exits with the command's status: 128 plus the signal's number when a signal ended it, 124 at `--timeout`, 125 when it was refused, killed or passed 64 MiB of output, 126 or 127 when it could not run, and 255 when the connection ended first and the outcome is unknown. `--json` prints the answer's document instead, with the first and last 512 KiB of each stream.",
6620
+ "`--detach` starts the command in the background instead, prints its process id and returns at once: it runs whatever happens to this call, keeps the Machine awake until it ends, and is ended at `--timeout` (default 3600, at most 21600). Read it with `tonbo machine output`, wait for it with `tonbo machine wait`, list them with `tonbo machine ps` and end one with `tonbo machine kill`. A budget or funding stop, or a sleep you ask for, still ends it."
5761
6621
  ],
5762
6622
  examples: [
5763
6623
  "tonbo machine exec web -- ls -la /app",
5764
- "tonbo machine exec web --timeout 60 -- npm test"
6624
+ "tonbo machine exec web --timeout 60 -- npm test",
6625
+ "tonbo machine exec web --detach -- npm run build"
5765
6626
  ],
5766
6627
  arguments: {
5767
6628
  name: "the Machine's name",
@@ -5769,6 +6630,37 @@ var COMMAND_HELP = {
5769
6630
  },
5770
6631
  defaults: { "--timeout": "600", "--cwd": "the image's WorkingDir" }
5771
6632
  },
6633
+ "tonbo machine ps": {
6634
+ about: [
6635
+ "Lists the Machine's background processes, newest first: those running and those that ended in the last 24 hours, with how each ended. It never wakes the Machine."
6636
+ ],
6637
+ examples: ["tonbo machine ps web"],
6638
+ arguments: { name: "the Machine's name" }
6639
+ },
6640
+ "tonbo machine wait": {
6641
+ about: [
6642
+ "Waits until a background process ends, prints how it ended and exits with its status as `tonbo machine exec` would: its exit code, 128 plus a signal's number, 124 at its timeout, 125 when it was killed and 255 when its outcome is unknown. It never wakes the Machine."
6643
+ ],
6644
+ examples: ["tonbo machine wait web 019a1f2e5a6b7c8d9e0f1a2b3c4d5e6f"],
6645
+ arguments: { name: "the Machine's name", process: "the process id `exec --detach` printed" }
6646
+ },
6647
+ "tonbo machine output": {
6648
+ about: [
6649
+ "Prints a background process's stdout and stderr together, as it wrote them, from the last 16 MiB the Machine keeps. `--follow` keeps printing until the process ends. The output is gone once the Machine sleeps. It never wakes the Machine."
6650
+ ],
6651
+ examples: [
6652
+ "tonbo machine output web 019a1f2e5a6b7c8d9e0f1a2b3c4d5e6f",
6653
+ "tonbo machine output web 019a1f2e5a6b7c8d9e0f1a2b3c4d5e6f --follow"
6654
+ ],
6655
+ arguments: { name: "the Machine's name", process: "the process id `exec --detach` printed" }
6656
+ },
6657
+ "tonbo machine kill": {
6658
+ about: [
6659
+ "Ends a background process: SIGTERM to everything it started, then a kill 5 seconds later. It never wakes the Machine."
6660
+ ],
6661
+ examples: ["tonbo machine kill web 019a1f2e5a6b7c8d9e0f1a2b3c4d5e6f"],
6662
+ arguments: { name: "the Machine's name", process: "the process id `exec --detach` printed" }
6663
+ },
5772
6664
  "tonbo machine file read": {
5773
6665
  about: [
5774
6666
  "Prints one regular file of the Machine, waking it when it is asleep. A file is gone when the Machine sleeps, so a read that woke it sees only its image."
@@ -5807,6 +6699,30 @@ var COMMAND_HELP = {
5807
6699
  examples: ["tonbo machine file delete web /app/tmp --recursive"],
5808
6700
  arguments: { name: "the Machine's name", path: "the absolute path in the Machine" }
5809
6701
  },
6702
+ "tonbo machine hold set": {
6703
+ about: [
6704
+ "Keeps the Machine awake until at least now plus `--for` (1 hour by default, at most 1 hour). Running it again only moves the end later, so a script can repeat it while its work runs. It never wakes an asleep Machine, and it ends when the Machine sleeps, stops or runs out of credit or budget. An awake Machine is billed as usual. A program inside the Machine sets its own holds on `/run/tonbo-hold.sock`, with no key."
6705
+ ],
6706
+ examples: ["tonbo machine hold set web build --for 30m"],
6707
+ arguments: {
6708
+ name: "the Machine's name",
6709
+ hold: "a name you choose for the hold, such as build"
6710
+ }
6711
+ },
6712
+ "tonbo machine hold rm": {
6713
+ about: [
6714
+ "Releases one hold set from outside the Machine, whether or not it existed. The Machine sleeps `sleep_after` seconds later unless something else keeps it awake."
6715
+ ],
6716
+ examples: ["tonbo machine hold rm web build"],
6717
+ arguments: { name: "the Machine's name", hold: "the hold's name" }
6718
+ },
6719
+ "tonbo machine hold ls": {
6720
+ about: [
6721
+ "Lists the holds of the Machine's current activation, those set from outside and those a program inside set, with when each lapses. An asleep Machine has none."
6722
+ ],
6723
+ examples: ["tonbo machine hold ls web"],
6724
+ arguments: { name: "the Machine's name" }
6725
+ },
5810
6726
  "tonbo machine sleep": {
5811
6727
  about: [
5812
6728
  "Puts an awake Machine to sleep now, and says so when it is already asleep or still creating; its next request boots it on its latest spec. With `--wait` the CLI reads the Machine until it is asleep or `--timeout` passes."
@@ -5815,6 +6731,14 @@ var COMMAND_HELP = {
5815
6731
  arguments: NAME("Machine"),
5816
6732
  defaults: { "--timeout": String(DEFAULT_WAIT_SECONDS) }
5817
6733
  },
6734
+ "tonbo machine wake": {
6735
+ about: [
6736
+ "Wakes an asleep Machine now, without a request to it, and says so when it is already awake. The wake passes the same checks as any other and is billed the same: it needs credit, room on a host and a budget that allows it, and a refused wake says why. With `--wait` the CLI reads the Machine until it is awake or `--timeout` passes."
6737
+ ],
6738
+ examples: ["tonbo machine wake web --wait"],
6739
+ arguments: NAME("Machine"),
6740
+ defaults: { "--timeout": String(DEFAULT_WAIT_SECONDS) }
6741
+ },
5818
6742
  "tonbo machine delete": {
5819
6743
  about: [
5820
6744
  "Deletes the Machine with its disk and logs; a Volume or database it binds stays. Its name is free for a new Machine at once. At a terminal it asks you to type the Machine's name first, and sends the ETag it read then.",
@@ -5993,7 +6917,7 @@ function applyHelp(program) {
5993
6917
 
5994
6918
  // src/machine-cell.ts
5995
6919
  import { readFile as readFile3, writeFile } from "node:fs/promises";
5996
- import { randomUUID as randomUUID6 } from "node:crypto";
6920
+ import { randomUUID as randomUUID7 } from "node:crypto";
5997
6921
  var EXEC_EXIT_CODES = {
5998
6922
  timeout: 124,
5999
6923
  output_limit: 125,
@@ -6146,6 +7070,191 @@ async function machineExecCommand(deps, name, command, options = {}) {
6146
7070
  deps.notice(`The command ended: ${ended.reason}.`);
6147
7071
  exitWith(deps, execExitCode(ended));
6148
7072
  }
7073
+ function processEndText(process2) {
7074
+ const end = process2.end;
7075
+ if (!end) return "-";
7076
+ if (end.reason === "exited") return `exited ${end.exit_code ?? 0}`;
7077
+ if (end.reason === "signaled") return `signaled ${end.signal ?? ""}`.trim();
7078
+ return end.reason;
7079
+ }
7080
+ function processExitCode(process2) {
7081
+ const end = process2.end;
7082
+ if (!end) return 0;
7083
+ if (end.reason === "exited" || end.reason === "signaled" || end.reason === "timeout")
7084
+ return execExitCode({ reason: end.reason, exit_code: end.exit_code, signal: end.signal });
7085
+ return end.reason === "lost" ? EXEC_EXIT_CODES.lost : EXEC_EXIT_CODES.killed;
7086
+ }
7087
+ async function machineProcessStartCommand(deps, name, command, options = {}) {
7088
+ if (command.length === 0)
7089
+ throw new Error(
7090
+ "Name the command after --, such as `tonbo machine exec web --detach -- npm run build`."
7091
+ );
7092
+ const body = { command };
7093
+ if (options.cwd) body.cwd = options.cwd;
7094
+ if (options.env?.length) body.env = parseEnv2(options.env);
7095
+ if (options.timeout !== void 0) body.timeout_seconds = options.timeout;
7096
+ if (options.expectActivation) body.expect_activation = options.expectActivation;
7097
+ try {
7098
+ const key = await deps.auth.apiKey();
7099
+ const region = await regionOf(deps, key, name);
7100
+ if (options.stdin) body.stdin = Buffer.from(await readAllStdin(deps)).toString("base64");
7101
+ const response = await cellApi(deps).fetch("startMachineProcess", key, region, {
7102
+ params: { name },
7103
+ headers: {
7104
+ "content-type": "application/json",
7105
+ "idempotency-key": options.idempotencyKey ?? randomUUID7()
7106
+ },
7107
+ body: JSON.stringify(body)
7108
+ });
7109
+ const { data } = await response.json();
7110
+ if (response.headers.get("tonbo-woke") === "true") {
7111
+ const ceiling = response.headers.get("tonbo-ceiling-usd-micros-per-hour");
7112
+ noticeWake(
7113
+ deps,
7114
+ {
7115
+ activation: data.activation,
7116
+ woke: true,
7117
+ ceiling_usd_micros_per_hour: ceiling === null ? null : Number(ceiling)
7118
+ },
7119
+ name
7120
+ );
7121
+ }
7122
+ print(
7123
+ deps,
7124
+ options.json,
7125
+ data,
7126
+ response.status === 200 ? `${data.process} (already started under this key, ${data.state})` : data.process
7127
+ );
7128
+ if (!options.json && response.status !== 200)
7129
+ deps.notice(
7130
+ `Started; it keeps ${name} awake until it ends or at ${data.deadline}. Read it with \`tonbo machine output ${name} ${data.process}\`, wait for it with \`tonbo machine wait ${name} ${data.process}\`.`
7131
+ );
7132
+ } catch (error) {
7133
+ throw explainMachineError(error);
7134
+ }
7135
+ }
7136
+ async function readProcess(deps, key, region, name, id) {
7137
+ return cellApi(deps).json("getMachineProcess", key, region, {
7138
+ params: { name, process: id }
7139
+ });
7140
+ }
7141
+ function processFields(process2) {
7142
+ return fields([
7143
+ ["process", process2.process],
7144
+ ["state", process2.state],
7145
+ ["command", (process2.argv_preview ?? []).join(" ") || "-"],
7146
+ ["started", process2.started_at],
7147
+ ["deadline", process2.deadline],
7148
+ ["ended", process2.ended_at ?? "-"],
7149
+ ["end", processEndText(process2)],
7150
+ ["activation", process2.activation]
7151
+ ]);
7152
+ }
7153
+ async function machineProcessListCommand(deps, name, options = {}) {
7154
+ try {
7155
+ const key = await deps.auth.apiKey();
7156
+ const region = await regionOf(deps, key, name);
7157
+ const processes = await cellApi(deps).json(
7158
+ "listMachineProcesses",
7159
+ key,
7160
+ region,
7161
+ {
7162
+ params: { name }
7163
+ }
7164
+ );
7165
+ print(
7166
+ deps,
7167
+ options.json,
7168
+ processes,
7169
+ processes.length === 0 ? `${name} has no background processes.` : table(
7170
+ ["PROCESS", "STATE", "STARTED", "END", "COMMAND"],
7171
+ processes.map((process2) => [
7172
+ process2.process,
7173
+ process2.state,
7174
+ process2.started_at,
7175
+ processEndText(process2),
7176
+ (process2.argv_preview ?? []).join(" ")
7177
+ ])
7178
+ )
7179
+ );
7180
+ } catch (error) {
7181
+ throw explainMachineError(error);
7182
+ }
7183
+ }
7184
+ async function machineProcessWaitCommand(deps, name, id, options = {}) {
7185
+ const sleep = options.sleep ?? ((ms) => new Promise((done) => setTimeout(done, ms)));
7186
+ try {
7187
+ const key = await deps.auth.apiKey();
7188
+ const region = await regionOf(deps, key, name);
7189
+ let process2 = await readProcess(deps, key, region, name, id);
7190
+ while (process2.state === "running") {
7191
+ await sleep(2e3);
7192
+ process2 = await readProcess(deps, key, region, name, id);
7193
+ }
7194
+ print(deps, options.json, process2, processFields(process2));
7195
+ exitWith(deps, processExitCode(process2));
7196
+ } catch (error) {
7197
+ throw explainMachineError(error);
7198
+ }
7199
+ }
7200
+ async function machineProcessOutputCommand(deps, name, id, options = {}) {
7201
+ const sleep = options.sleep ?? ((ms) => new Promise((done) => setTimeout(done, ms)));
7202
+ try {
7203
+ const key = await deps.auth.apiKey();
7204
+ const region = await regionOf(deps, key, name);
7205
+ let offset = options.offset ?? 0;
7206
+ for (; ; ) {
7207
+ const response = await cellApi(deps).fetch("readMachineProcessOutput", key, region, {
7208
+ params: { name, process: id },
7209
+ query: { offset: String(offset) }
7210
+ });
7211
+ const bytes2 = new Uint8Array(await response.arrayBuffer());
7212
+ const start = Number(response.headers.get("tonbo-output-start") ?? offset);
7213
+ if (start > offset)
7214
+ deps.notice(`(${start - offset} bytes before offset ${start} were dropped from the ring)`);
7215
+ if (bytes2.length) writeBytes(deps, "stdout", bytes2);
7216
+ offset = Number(response.headers.get("tonbo-output-next-offset") ?? start + bytes2.length);
7217
+ const ended = response.headers.get("tonbo-process-state") === "ended";
7218
+ if (start + bytes2.length < offset) continue;
7219
+ if (!options.follow || ended) break;
7220
+ await sleep(1e3);
7221
+ }
7222
+ } catch (error) {
7223
+ throw explainMachineError(error);
7224
+ }
7225
+ }
7226
+ async function machineProcessKillCommand(deps, name, id, options = {}) {
7227
+ try {
7228
+ const key = await deps.auth.apiKey();
7229
+ const region = await regionOf(deps, key, name);
7230
+ const process2 = await cellApi(deps).json("killMachineProcess", key, region, {
7231
+ params: { name, process: id }
7232
+ });
7233
+ print(
7234
+ deps,
7235
+ options.json,
7236
+ process2,
7237
+ process2.state === "ended" ? `${id} had already ended: ${processEndText(process2)}.` : `Killing ${id}; it ends within a few seconds.`
7238
+ );
7239
+ } catch (error) {
7240
+ throw explainMachineError(error);
7241
+ }
7242
+ }
7243
+ async function machineProcessSummary(cell, key, region, name) {
7244
+ try {
7245
+ const processes = await cell.json("listMachineProcesses", key, region, {
7246
+ params: { name }
7247
+ });
7248
+ const line = (process2) => `${process2.process} ${(process2.argv_preview ?? []).join(" ")} (${process2.state === "running" ? `until ${process2.deadline}` : processEndText(process2)})`;
7249
+ const last = processes.find((process2) => process2.state === "ended");
7250
+ return {
7251
+ running: processes.filter((process2) => process2.state === "running").map(line),
7252
+ last: last ? line(last) : null
7253
+ };
7254
+ } catch {
7255
+ return null;
7256
+ }
7257
+ }
6149
7258
  async function machineFileReadCommand(deps, name, path4, options = {}) {
6150
7259
  try {
6151
7260
  const key = await deps.auth.apiKey();
@@ -6176,7 +7285,7 @@ async function machineFileWriteCommand(deps, name, path4, options = {}) {
6176
7285
  "if-none-match": options.create ? "*" : void 0,
6177
7286
  "tonbo-file-mode": options.mode,
6178
7287
  "tonbo-expect-activation": options.expectActivation,
6179
- "idempotency-key": randomUUID6()
7288
+ "idempotency-key": randomUUID7()
6180
7289
  },
6181
7290
  body: new Uint8Array(bytes2)
6182
7291
  });
@@ -6259,7 +7368,7 @@ async function machineFileDeleteCommand(deps, name, path4, options = {}) {
6259
7368
  query: { path: path4, recursive: options.recursive ? "true" : void 0 },
6260
7369
  headers: {
6261
7370
  "tonbo-expect-activation": options.expectActivation,
6262
- "idempotency-key": randomUUID6()
7371
+ "idempotency-key": randomUUID7()
6263
7372
  }
6264
7373
  });
6265
7374
  print(deps, options.json, { path: path4 }, `Deleted ${path4}.`);
@@ -6267,6 +7376,76 @@ async function machineFileDeleteCommand(deps, name, path4, options = {}) {
6267
7376
  throw explainMachineError(error);
6268
7377
  }
6269
7378
  }
7379
+ var HOLD_MAX_SECONDS = 3600;
7380
+ function parseHoldDuration(value) {
7381
+ const match = /^(\d+)\s*([smh]?)$/.exec(value.trim());
7382
+ const unit = { "": 1, s: 1, m: 60, h: 3600 }[match?.[2] ?? ""] ?? 1;
7383
+ const seconds = match ? Number(match[1]) * unit : Number.NaN;
7384
+ if (!Number.isSafeInteger(seconds) || seconds < 1 || seconds > HOLD_MAX_SECONDS)
7385
+ throw new Error(`--for ${value} is not between 1 second and 1 hour, such as 90s, 45m or 1h.`);
7386
+ return seconds;
7387
+ }
7388
+ async function machineHoldSetCommand(deps, name, hold, options = {}) {
7389
+ try {
7390
+ const key = await deps.auth.apiKey();
7391
+ const region = await regionOf(deps, key, name);
7392
+ const placed = await cellApi(deps).json("setMachineHold", key, region, {
7393
+ params: { name, hold },
7394
+ headers: {
7395
+ "content-type": "application/json",
7396
+ "tonbo-expect-activation": options.expectActivation
7397
+ },
7398
+ body: JSON.stringify({ seconds: options.for ?? HOLD_MAX_SECONDS })
7399
+ });
7400
+ print(
7401
+ deps,
7402
+ options.json,
7403
+ placed,
7404
+ `${name} stays awake until ${placed.until} (hold ${placed.hold}). Run this again before then to extend it; \`tonbo machine hold rm ${name} ${placed.hold}\` lets it sleep. An awake Machine is billed as usual.`
7405
+ );
7406
+ } catch (error) {
7407
+ throw explainMachineError(error);
7408
+ }
7409
+ }
7410
+ async function machineHoldRmCommand(deps, name, hold, options = {}) {
7411
+ try {
7412
+ const key = await deps.auth.apiKey();
7413
+ const region = await regionOf(deps, key, name);
7414
+ await cellApi(deps).fetch("releaseMachineHold", key, region, { params: { name, hold } });
7415
+ print(
7416
+ deps,
7417
+ options.json,
7418
+ { hold },
7419
+ `Released ${hold}. ${name} sleeps once nothing else keeps it awake.`
7420
+ );
7421
+ } catch (error) {
7422
+ throw explainMachineError(error);
7423
+ }
7424
+ }
7425
+ async function machineHoldLsCommand(deps, name, options = {}) {
7426
+ try {
7427
+ const key = await deps.auth.apiKey();
7428
+ const region = await regionOf(deps, key, name);
7429
+ const holds = await cellApi(deps).json("listMachineHolds", key, region, {
7430
+ params: { name }
7431
+ });
7432
+ print(
7433
+ deps,
7434
+ options.json,
7435
+ holds,
7436
+ holds.length === 0 ? `${name} has no holds.` : table(
7437
+ ["HOLD", "SET FROM", "UNTIL"],
7438
+ holds.map((entry) => [
7439
+ entry.hold,
7440
+ entry.source === "guest" ? "inside the Machine" : "outside (API or CLI)",
7441
+ entry.until
7442
+ ])
7443
+ )
7444
+ );
7445
+ } catch (error) {
7446
+ throw explainMachineError(error);
7447
+ }
7448
+ }
6270
7449
 
6271
7450
  // src/prompt.ts
6272
7451
  import select from "@inquirer/select";
@@ -6526,7 +7705,7 @@ async function sshCertificateIssueCommand(deps, options) {
6526
7705
 
6527
7706
  // src/app.ts
6528
7707
  var packageVersion = JSON.parse(
6529
- readFileSync(new URL("../../package.json", import.meta.url), "utf8")
7708
+ readFileSync2(new URL("../../package.json", import.meta.url), "utf8")
6530
7709
  );
6531
7710
  function createDependencies(selection = {}) {
6532
7711
  const api = apiOrigin();
@@ -6537,8 +7716,9 @@ function createDependencies(selection = {}) {
6537
7716
  loginOrigin: loginOrigin(api),
6538
7717
  account
6539
7718
  });
7719
+ const notice = (text) => console.error(text);
6540
7720
  return {
6541
- api: new ManagementApi(fetch, api),
7721
+ api: new ManagementApi(noticingBudget(fetch, notice), api),
6542
7722
  ask: terminalAsk,
6543
7723
  auth: new AuthClient(
6544
7724
  credentials,
@@ -6554,7 +7734,9 @@ function createDependencies(selection = {}) {
6554
7734
  credentials,
6555
7735
  interactive: () => process.stdin.isTTY === true && process.stderr.isTTY === true,
6556
7736
  output: (text) => console.log(text),
6557
- notice: (text) => console.error(text),
7737
+ notice,
7738
+ openUrl: openBrowser,
7739
+ processes: (key, region, name) => machineProcessSummary(new CellApi(fetch), key, region, name),
6558
7740
  progress: new TerminalProgress(process.stderr),
6559
7741
  select: terminalSelect
6560
7742
  };
@@ -6568,7 +7750,7 @@ function parseWhole(value) {
6568
7750
  return Number(value);
6569
7751
  }
6570
7752
  function operationOptions(command, operationId) {
6571
- const query = managementOperations[operationId].query;
7753
+ const query = managementOperation(operationId).query;
6572
7754
  if (query.includes("limit"))
6573
7755
  command.option("--limit <count>", "the most items to return", parseLimit);
6574
7756
  if (query.includes("cursor"))
@@ -6577,6 +7759,10 @@ function operationOptions(command, operationId) {
6577
7759
  }
6578
7760
  var ifMatchHelp = (action) => `${action} only the Machine at the ETag \`tonbo machine show\` printed (with or without its quotes)`;
6579
7761
  var repeated = (value, previous = []) => [...previous, value];
7762
+ var budgetOption = (whom) => new Option(
7763
+ "--budget <budget>",
7764
+ `put ${whom} under a monthly budget: an amount in dollars such as 50 creates one, a budget's name adds to it`
7765
+ ).hideHelp();
6580
7766
  var roleOption = (description) => new Option("--role <role>", description).choices(accountRoles);
6581
7767
  function machineSpecOptions(command, update) {
6582
7768
  const later = update ? "; applies at the next boot" : "";
@@ -6684,7 +7870,7 @@ function createProgram(makeDependencies = createDependencies) {
6684
7870
  ).option(
6685
7871
  "--idempotency-key <key>",
6686
7872
  "reuse the key of an unconfirmed creation to get the Agent it created"
6687
- ),
7873
+ ).addOption(budgetOption("the Agent")),
6688
7874
  "createAgent"
6689
7875
  ).action(
6690
7876
  async (name, options) => agentCreateCommand(dependencies(), name, options)
@@ -6719,7 +7905,7 @@ function createProgram(makeDependencies = createDependencies) {
6719
7905
  repeated
6720
7906
  ).addOption(
6721
7907
  new Option("--expires-in <period>", "when the key expires").choices(apiKeyExpirations)
6722
- ),
7908
+ ).addOption(budgetOption("the key's member")),
6723
7909
  "createApiKey"
6724
7910
  ).action(
6725
7911
  async (name, options) => apiKeyCreateCommand(dependencies(), name, options)
@@ -6733,6 +7919,37 @@ function createProgram(makeDependencies = createDependencies) {
6733
7919
  ).action(
6734
7920
  async (keyId, options) => apiKeyRevokeCommand(dependencies(), keyId, options)
6735
7921
  );
7922
+ const budget = program.command("budget", { hidden: true }).description("the account's monthly spend budgets");
7923
+ operationOptions(
7924
+ budget.command("list").description("list the budgets, the account budget first"),
7925
+ "listBudgets"
7926
+ ).action(async (options) => budgetListCommand(dependencies(), options));
7927
+ operationOptions(
7928
+ budget.command("show <budget>").description("show one budget, by its name or id"),
7929
+ "getBudget"
7930
+ ).action(
7931
+ async (name, options) => budgetShowCommand(dependencies(), name, options)
7932
+ );
7933
+ operationOptions(
7934
+ budget.command("add <name>").description("create a budget for you or Agents you created").requiredOption("--limit <dollars>", "the monthly limit in dollars, such as 50").option(
7935
+ "--member <member>",
7936
+ "a member to cover: you, or an Agent you created (subject, id or name); repeat for several",
7937
+ repeated
7938
+ ),
7939
+ "createBudget"
7940
+ ).action(
7941
+ async (name, options) => budgetAddCommand(dependencies(), name, options)
7942
+ );
7943
+ operationOptions(
7944
+ budget.command("set <budget>").description("lower the budget that covers you, or add Agents you created to it").option("--limit <dollars>", "a lower monthly limit; a raise opens the console").option(
7945
+ "--add <member>",
7946
+ "an Agent you created to cover too (subject, id or name); repeat for several",
7947
+ repeated
7948
+ ),
7949
+ "changeBudget"
7950
+ ).action(
7951
+ async (name, options) => budgetSetCommand(dependencies(), name, options)
7952
+ );
6736
7953
  const bucket = program.command("bucket").description("the account's Streams buckets");
6737
7954
  operationOptions(bucket.command("list").description("list the buckets"), "listBuckets").action(
6738
7955
  async (options) => bucketListCommand(dependencies(), options)
@@ -6900,6 +8117,10 @@ function createProgram(makeDependencies = createDependencies) {
6900
8117
  ).option("--no-database", "bind no database (the default)"),
6901
8118
  false
6902
8119
  ).option(
8120
+ "--secret-env <KEY=FILE>",
8121
+ "read a UTF-8 environment secret from FILE; repeat",
8122
+ repeated
8123
+ ).option("--secret-file <KEY=FILE>", "read binary file secret from FILE; repeat", repeated).option(
6903
8124
  "--idempotency-key <key>",
6904
8125
  "reuse the key of an unconfirmed creation to get the Machine it created"
6905
8126
  ).option("--wait", "wait until the Machine is active, or say why it failed").option("--timeout <seconds>", "how long --wait waits, in seconds", parseWhole),
@@ -6913,11 +8134,37 @@ function createProgram(makeDependencies = createDependencies) {
6913
8134
  ).action(
6914
8135
  async (name, options) => machineShowCommand(dependencies(), name, options)
6915
8136
  );
8137
+ const machineSecret = machine.command("secret").description("manage a Machine's write-only secrets");
8138
+ operationOptions(
8139
+ machineSecret.command("list <name>").description("list secret keys and delivery, never values"),
8140
+ "listMachineSecrets"
8141
+ ).action(
8142
+ async (name, options) => machineSecretListCommand(dependencies(), name, options)
8143
+ );
8144
+ operationOptions(
8145
+ machineSecret.command("set <name> <key>").description(
8146
+ "set a secret from a local file or standard input; applies at the next activation"
8147
+ ).requiredOption("--from <file>", "read the value from FILE, or - for standard input").addOption(
8148
+ new Option("--delivery <mode>", "env or file (binary)").choices(["env", "file"]).default("env")
8149
+ ),
8150
+ "setMachineSecret"
8151
+ ).action(
8152
+ async (name, key, options) => machineSecretSetCommand(dependencies(), name, key, options)
8153
+ );
8154
+ operationOptions(
8155
+ machineSecret.command("unset <name> <key>").description("remove a secret at the next activation"),
8156
+ "unsetMachineSecret"
8157
+ ).action(
8158
+ async (name, key) => machineSecretUnsetCommand(dependencies(), name, key)
8159
+ );
6916
8160
  operationOptions(
6917
8161
  machineSpecOptions(
6918
8162
  machine.command("update <name> [command...]").description(
6919
8163
  "change a Machine; [command...] after -- replaces its command at the next boot"
6920
- ).option("--image <reference>", "a new image, named as for create; applies at the next boot").option("--image-command", "run the image's own CMD again at the next boot").option("--unset-env <KEY>", "remove an environment variable; repeat for several", repeated).option("--clear-ports", "remove every port").option("--if-match <etag>", ifMatchHelp("change")).option("--discard-snapshot", "apply a change that discards the Machine's snapshot").option("--no-volume", "remove the Machine's Volume at the next boot; the Volume is kept").option("--no-delete-after", "never delete the Machine for being asleep"),
8164
+ ).option("--image <reference>", "a new image, named as for create; applies at the next boot").option("--image-command", "run the image's own CMD again at the next boot").option("--unset-env <KEY>", "remove an environment variable; repeat for several", repeated).option("--clear-ports", "remove every port").option("--if-match <etag>", ifMatchHelp("change")).option("--discard-snapshot", "apply a change that discards the Machine's snapshot").option("--no-volume", "remove the Machine's Volume at the next boot; the Volume is kept").option(
8165
+ "--database <name>",
8166
+ "bind the account's database <name> of the Machine's region instead; applies at the next wake"
8167
+ ).option("--no-database", "bind no database from the next wake; the database is kept").option("--no-delete-after", "never delete the Machine for being asleep"),
6921
8168
  true
6922
8169
  ),
6923
8170
  "updateMachine"
@@ -6927,13 +8174,16 @@ function createProgram(makeDependencies = createDependencies) {
6927
8174
  operationOptions(
6928
8175
  machine.command("exec <name> [command...]").description(
6929
8176
  "run a command in a Machine, waking it first, and print its output; exits with its status"
8177
+ ).option(
8178
+ "--detach",
8179
+ "start it in the background and print its id at once; it keeps the Machine awake until it ends or its timeout"
6930
8180
  ).option("--cwd <path>", "the working directory, an absolute path").option(
6931
8181
  "--env <KEY=VALUE>",
6932
8182
  "set an environment variable for the command; repeat for several",
6933
8183
  repeated
6934
8184
  ).option(
6935
8185
  "--timeout <seconds>",
6936
- "kill the command after this many seconds (default 600, at most 3600)",
8186
+ "kill the command after this many seconds (default 600, at most 3600; with --detach default 3600, at most 21600)",
6937
8187
  parseWhole
6938
8188
  ).option("--stdin", "send this process's stdin to the command (at most 1 MiB)").option(
6939
8189
  "--expect-activation <activation>",
@@ -6944,7 +8194,31 @@ function createProgram(makeDependencies = createDependencies) {
6944
8194
  ),
6945
8195
  "execMachineCommand"
6946
8196
  ).action(
6947
- async (name, command, options) => machineExecCommand(dependencies(), name, command, options)
8197
+ async (name, command, options) => options.detach ? machineProcessStartCommand(dependencies(), name, command, options) : machineExecCommand(dependencies(), name, command, options)
8198
+ );
8199
+ operationOptions(
8200
+ machine.command("ps <name>").description("list a Machine's background processes, running and recently ended"),
8201
+ "listMachineProcesses"
8202
+ ).action(
8203
+ async (name, options) => machineProcessListCommand(dependencies(), name, options)
8204
+ );
8205
+ operationOptions(
8206
+ machine.command("wait <name> <process>").description("wait for a background process to end; exits with its status"),
8207
+ "getMachineProcess"
8208
+ ).action(
8209
+ async (name, id, options) => machineProcessWaitCommand(dependencies(), name, id, options)
8210
+ );
8211
+ operationOptions(
8212
+ machine.command("output <name> <process>").description("print a background process's output, stdout and stderr together").option("--offset <bytes>", "start at this byte offset", parseWhole).option("--follow", "keep printing new output until the process ends"),
8213
+ "readMachineProcessOutput"
8214
+ ).action(
8215
+ async (name, id, options) => machineProcessOutputCommand(dependencies(), name, id, options)
8216
+ );
8217
+ operationOptions(
8218
+ machine.command("kill <name> <process>").description("end a background process: SIGTERM, then a kill after 5 seconds"),
8219
+ "killMachineProcess"
8220
+ ).action(
8221
+ async (name, id, options) => machineProcessKillCommand(dependencies(), name, id, options)
6948
8222
  );
6949
8223
  const file = machine.command("file").description("read, write, stat, list and delete a Machine's files");
6950
8224
  operationOptions(
@@ -6977,12 +8251,41 @@ function createProgram(makeDependencies = createDependencies) {
6977
8251
  ).action(
6978
8252
  async (name, path4, options) => machineFileDeleteCommand(dependencies(), name, path4, options)
6979
8253
  );
8254
+ const hold = machine.command("hold").description("keep a Machine awake for work that has no open connection");
8255
+ operationOptions(
8256
+ hold.command("set <name> <hold>").description("keep a Machine awake until a hold lapses; never wakes it").option(
8257
+ "--for <duration>",
8258
+ "how long from now, such as 90s, 45m or 1h (at most 1h)",
8259
+ parseHoldDuration
8260
+ ).option("--expect-activation <activation>", "hold only this activation"),
8261
+ "setMachineHold"
8262
+ ).action(
8263
+ async (name, holdName, options) => machineHoldSetCommand(dependencies(), name, holdName, options)
8264
+ );
8265
+ operationOptions(
8266
+ hold.command("rm <name> <hold>").description("release a hold set from outside the Machine"),
8267
+ "releaseMachineHold"
8268
+ ).action(
8269
+ async (name, holdName, options) => machineHoldRmCommand(dependencies(), name, holdName, options)
8270
+ );
8271
+ operationOptions(
8272
+ hold.command("ls <name>").description("list what holds a Machine awake"),
8273
+ "listMachineHolds"
8274
+ ).action(
8275
+ async (name, options) => machineHoldLsCommand(dependencies(), name, options)
8276
+ );
6980
8277
  operationOptions(
6981
8278
  machine.command("sleep <name>").description("put a Machine to sleep now; its next request boots it on its latest spec").option("--if-match <etag>", ifMatchHelp("put to sleep")).option("--wait", "wait until the Machine is asleep").option("--timeout <seconds>", "how long --wait waits, in seconds", parseWhole),
6982
8279
  "sleepMachine"
6983
8280
  ).action(
6984
8281
  async (name, options) => machineSleepCommand(dependencies(), name, options)
6985
8282
  );
8283
+ operationOptions(
8284
+ machine.command("wake <name>").description("wake an asleep Machine now, without a request to it").option("--if-match <etag>", ifMatchHelp("wake")).option("--wait", "wait until the Machine is awake").option("--timeout <seconds>", "how long --wait waits, in seconds", parseWhole),
8285
+ "wakeMachine"
8286
+ ).action(
8287
+ async (name, options) => machineWakeCommand(dependencies(), name, options)
8288
+ );
6986
8289
  operationOptions(
6987
8290
  machine.command("delete <name>").description("delete a Machine with its disk and logs, or forget one of your computers").option("--yes", "delete without asking; required when stdin is not a terminal").option("--if-match <etag>", ifMatchHelp("delete")),
6988
8291
  "deleteMachine"