@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.
- package/dist/bin/tonbo.js +1377 -74
- 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 =
|
|
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
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
]
|
|
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: #
|
|
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: #
|
|
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
|
-
|
|
2161
|
-
|
|
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
|
|
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
|
|
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/
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
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
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
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
|
|
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 ??
|
|
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 ??
|
|
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 ??
|
|
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 ??
|
|
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
|
|
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
|
|
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
|
|
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 ??
|
|
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 {
|
|
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
|
|
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 ??
|
|
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 = [
|
|
5172
|
-
|
|
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
|
|
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
|
|
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":
|
|
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":
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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(
|
|
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"
|