@tonbo/cli 0.22.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/bin/tonbo.js +538 -42
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ The login file, `login.json`, and the device id it is tied to are shared with [`
|
|
|
26
26
|
|
|
27
27
|
## Commands and the Management API
|
|
28
28
|
|
|
29
|
-
Every command other than `login` and `logout` is named by one operation of the [Management API](../../contracts/management/openapi-v1.yaml) through its `x-tonbo-cli` and calls that operation, plus reads of the same resource (a `--wait`, the Machine a `name_taken` refusal names, the activations `machine show` reads for the last end,
|
|
29
|
+
Every command other than `login` and `logout` is named by one operation of the [Management API](../../contracts/management/openapi-v1.yaml) through its `x-tonbo-cli` and calls that operation, plus reads of the same resource (a `--wait`, the Machine a `name_taken` refusal names, the activations `machine show` reads for the last end, the Volume a `machine create` or `update --volume` names, read for its id and region, or the Volume list `machine show` and `machine delete` read for the name of the Machine's Volume); `tonbo machine logs`, once it ships, is the named Streams exception, reading the stream at the Machine's `logs.url`. `tonbo volume move` is the one command that sends another command's operation: it reads the Volume and both Machines and sends the target's `updateMachine` with `move_from` (the `machine create --move-from` of an existing Machine, docs/design/machines-volume-move.md). Apart from it, the command set and the operations tagged with a command are the same set, which `test/commands.test.ts` checks in both directions. The CLI resolves each request from the operation table of the internal package [`@tonbo/cli-core`](../../packages/tonbo-cli-core) (`src/generated/contracts.ts`, which `pnpm --filter @tonbo/cli-core generate:contracts` derives from the contract). `login`, `logout`, `account list` and `account use` are the named exception: they obtain, choose among and discard what the other commands present as their Bearer credential. What a command may do is what your role in the account it acts in allows; the CLI never decides it.
|
|
30
30
|
|
|
31
31
|
The help lives with the commands: [`src/app.ts`](src/app.ts) defines each command, its arguments and flags with their one-line descriptions, and [`src/help.ts`](src/help.ts) holds the long help, each flag's default, the examples, the shared rules and the environment. [`commands.json`](commands.json) is all of it as `--help` prints it, which the docs' CLI reference renders; `test/commands.test.ts` holds it to the program, and holds every runnable command to its details and example and the environment list to the variables the code reads, so after changing a command run `pnpm --filter @tonbo/cli commands`.
|
|
32
32
|
|
package/dist/bin/tonbo.js
CHANGED
|
@@ -613,6 +613,22 @@ var managementOperations = {
|
|
|
613
613
|
"idempotencyKey": false,
|
|
614
614
|
"cli": "tonbo database activity"
|
|
615
615
|
},
|
|
616
|
+
"getDatabaseMetrics": {
|
|
617
|
+
"method": "GET",
|
|
618
|
+
"path": "/databases/{name}/metrics",
|
|
619
|
+
"pathParams": [
|
|
620
|
+
"name"
|
|
621
|
+
],
|
|
622
|
+
"query": [
|
|
623
|
+
"series",
|
|
624
|
+
"window",
|
|
625
|
+
"granularity",
|
|
626
|
+
"from",
|
|
627
|
+
"to"
|
|
628
|
+
],
|
|
629
|
+
"idempotencyKey": false,
|
|
630
|
+
"cli": "tonbo database metrics"
|
|
631
|
+
},
|
|
616
632
|
"listDatabaseUsage": {
|
|
617
633
|
"method": "GET",
|
|
618
634
|
"path": "/usage/databases",
|
|
@@ -673,7 +689,9 @@ var managementOperations = {
|
|
|
673
689
|
"pathParams": [
|
|
674
690
|
"name"
|
|
675
691
|
],
|
|
676
|
-
"query": [
|
|
692
|
+
"query": [
|
|
693
|
+
"delete_volume"
|
|
694
|
+
],
|
|
677
695
|
"idempotencyKey": false,
|
|
678
696
|
"cli": "tonbo machine delete"
|
|
679
697
|
},
|
|
@@ -687,6 +705,16 @@ var managementOperations = {
|
|
|
687
705
|
"idempotencyKey": false,
|
|
688
706
|
"cli": "tonbo machine sleep"
|
|
689
707
|
},
|
|
708
|
+
"rebuildMachine": {
|
|
709
|
+
"method": "POST",
|
|
710
|
+
"path": "/machines/{name}/rebuild",
|
|
711
|
+
"pathParams": [
|
|
712
|
+
"name"
|
|
713
|
+
],
|
|
714
|
+
"query": [],
|
|
715
|
+
"idempotencyKey": false,
|
|
716
|
+
"cli": "tonbo machine rebuild"
|
|
717
|
+
},
|
|
690
718
|
"wakeMachine": {
|
|
691
719
|
"method": "POST",
|
|
692
720
|
"path": "/machines/{name}/wake",
|
|
@@ -740,6 +768,34 @@ var managementOperations = {
|
|
|
740
768
|
"idempotencyKey": false,
|
|
741
769
|
"cli": "tonbo metrics machines"
|
|
742
770
|
},
|
|
771
|
+
"getAccountDatabaseMetrics": {
|
|
772
|
+
"method": "GET",
|
|
773
|
+
"path": "/metrics/databases",
|
|
774
|
+
"pathParams": [],
|
|
775
|
+
"query": [
|
|
776
|
+
"series",
|
|
777
|
+
"window",
|
|
778
|
+
"granularity",
|
|
779
|
+
"from",
|
|
780
|
+
"to"
|
|
781
|
+
],
|
|
782
|
+
"idempotencyKey": false,
|
|
783
|
+
"cli": "tonbo metrics databases"
|
|
784
|
+
},
|
|
785
|
+
"getAccountStreamMetrics": {
|
|
786
|
+
"method": "GET",
|
|
787
|
+
"path": "/metrics/streams",
|
|
788
|
+
"pathParams": [],
|
|
789
|
+
"query": [
|
|
790
|
+
"series",
|
|
791
|
+
"window",
|
|
792
|
+
"granularity",
|
|
793
|
+
"from",
|
|
794
|
+
"to"
|
|
795
|
+
],
|
|
796
|
+
"idempotencyKey": false,
|
|
797
|
+
"cli": "tonbo metrics streams"
|
|
798
|
+
},
|
|
743
799
|
"listMachineShapes": {
|
|
744
800
|
"method": "GET",
|
|
745
801
|
"path": "/machine-shapes",
|
|
@@ -1071,6 +1127,22 @@ var managementOperations = {
|
|
|
1071
1127
|
"idempotencyKey": false,
|
|
1072
1128
|
"cli": "tonbo stream activity"
|
|
1073
1129
|
},
|
|
1130
|
+
"getStreamMetrics": {
|
|
1131
|
+
"method": "GET",
|
|
1132
|
+
"path": "/streams/{name}/metrics",
|
|
1133
|
+
"pathParams": [
|
|
1134
|
+
"name"
|
|
1135
|
+
],
|
|
1136
|
+
"query": [
|
|
1137
|
+
"series",
|
|
1138
|
+
"window",
|
|
1139
|
+
"granularity",
|
|
1140
|
+
"from",
|
|
1141
|
+
"to"
|
|
1142
|
+
],
|
|
1143
|
+
"idempotencyKey": false,
|
|
1144
|
+
"cli": "tonbo stream metrics"
|
|
1145
|
+
},
|
|
1074
1146
|
"listStreamUsage": {
|
|
1075
1147
|
"method": "GET",
|
|
1076
1148
|
"path": "/usage/streams",
|
|
@@ -1941,6 +2013,10 @@ var machineTimings = {
|
|
|
1941
2013
|
"wakeHoldSeconds": 25,
|
|
1942
2014
|
"earlyReclaimSeconds": 120
|
|
1943
2015
|
};
|
|
2016
|
+
var machineRebuildWarning = [
|
|
2017
|
+
"**Rebuild deletes changes outside /home, /workspace and /root.**",
|
|
2018
|
+
"Rebuild moves this Machine to the image `<image>`. Files in /home, /workspace and /root are kept. Everything else you changed on this Machine's disk is deleted and cannot be recovered: installed packages, files you edited in /etc, global npm or pip packages, and anything in /usr, /opt or /var. To keep those changes, add them to the image's Dockerfile. The Machine sleeps now and starts on the new image at its next wake."
|
|
2019
|
+
];
|
|
1944
2020
|
var machineSecretLimits = {
|
|
1945
2021
|
"perMachine": 64,
|
|
1946
2022
|
"valueMaxBytes": 65536,
|
|
@@ -2319,10 +2395,19 @@ var ManagementApi = class {
|
|
|
2319
2395
|
deleteDatabase(key, name) {
|
|
2320
2396
|
return this.call("deleteDatabase", key, { params: { name } });
|
|
2321
2397
|
}
|
|
2322
|
-
/**
|
|
2398
|
+
/** Deprecated (`getDatabaseMetrics`): a database's writes per hour or day
|
|
2399
|
+
* and its size at each step's end, for `tonbo database activity`. */
|
|
2323
2400
|
getDatabaseActivity(key, name, query = {}) {
|
|
2324
2401
|
return this.call("getDatabaseActivity", key, { params: { name }, query });
|
|
2325
2402
|
}
|
|
2403
|
+
/** A live database's metrics per UTC hour or day. */
|
|
2404
|
+
getDatabaseMetrics(key, name, query = {}) {
|
|
2405
|
+
return this.call("getDatabaseMetrics", key, { params: { name }, query });
|
|
2406
|
+
}
|
|
2407
|
+
/** The account's databases' metrics, each and summed. */
|
|
2408
|
+
getAccountDatabaseMetrics(key, query = {}) {
|
|
2409
|
+
return this.call("getAccountDatabaseMetrics", key, { query });
|
|
2410
|
+
}
|
|
2326
2411
|
/** The whole usage page: its rows, `other`, prices and checkpoint. */
|
|
2327
2412
|
listDatabaseUsage(key, page, query = {}) {
|
|
2328
2413
|
return this.page("listDatabaseUsage", key, page, query);
|
|
@@ -2347,10 +2432,19 @@ var ManagementApi = class {
|
|
|
2347
2432
|
deleteStream(key, name) {
|
|
2348
2433
|
return this.call("deleteStream", key, { params: { name } });
|
|
2349
2434
|
}
|
|
2350
|
-
/**
|
|
2435
|
+
/** Deprecated (`getStreamMetrics`): a stream's appends per hour or day and
|
|
2436
|
+
* its size at each step's end, for `tonbo stream activity`. */
|
|
2351
2437
|
getStreamActivity(key, name, query = {}) {
|
|
2352
2438
|
return this.call("getStreamActivity", key, { params: { name }, query });
|
|
2353
2439
|
}
|
|
2440
|
+
/** A live stream's metrics per UTC hour or day. */
|
|
2441
|
+
getStreamMetrics(key, name, query = {}) {
|
|
2442
|
+
return this.call("getStreamMetrics", key, { params: { name }, query });
|
|
2443
|
+
}
|
|
2444
|
+
/** The account's streams' metrics, each and summed. */
|
|
2445
|
+
getAccountStreamMetrics(key, query = {}) {
|
|
2446
|
+
return this.call("getAccountStreamMetrics", key, { query });
|
|
2447
|
+
}
|
|
2354
2448
|
/** The whole usage page: its rows, `other`, prices and checkpoint. */
|
|
2355
2449
|
listStreamUsage(key, page, query = {}) {
|
|
2356
2450
|
return this.page("listStreamUsage", key, page, query);
|
|
@@ -2483,6 +2577,36 @@ var ManagementApi = class {
|
|
|
2483
2577
|
const answer = await this.request("updateMachine", key, { params: { name }, body: withoutUndefined(change), ifMatch: etag });
|
|
2484
2578
|
return { machine: answer.data, volumeMove: answer.volume_move ?? null };
|
|
2485
2579
|
}
|
|
2580
|
+
/** A Rebuild (machine-v2.json `rebuild`): `POST /machines/{name}/rebuild`
|
|
2581
|
+
* with `image`, or the Machine's own reference resolved again without it.
|
|
2582
|
+
* A 202 (`waiting_for_sleep`) carries the `Retry-After` to read the Machine
|
|
2583
|
+
* again after; the request is then sent again once it is asleep. With
|
|
2584
|
+
* `etag`, only the Machine that ETag names is rebuilt
|
|
2585
|
+
* (machine-v2.json `etag.if_match.rebuild` is optional). */
|
|
2586
|
+
async rebuildMachine(key, name, request, etag) {
|
|
2587
|
+
const headers = { retryAfter: null };
|
|
2588
|
+
const answer = await this.request("rebuildMachine", key, { params: { name }, body: withoutUndefined(request), ...etag ? { ifMatch: etag } : {} }, (response) => {
|
|
2589
|
+
headers.retryAfter = response.headers.get("retry-after");
|
|
2590
|
+
});
|
|
2591
|
+
return {
|
|
2592
|
+
machine: answer.data,
|
|
2593
|
+
rebuild: answer.rebuild,
|
|
2594
|
+
retryAfterSeconds: retryAfterSeconds(headers.retryAfter)
|
|
2595
|
+
};
|
|
2596
|
+
}
|
|
2597
|
+
/** An update with `rebuild: true` and `image`, which answers as
|
|
2598
|
+
* rebuildMachine does (machine-v2.json `rebuild.answers`). */
|
|
2599
|
+
async updateMachineRebuild(key, name, etag, change) {
|
|
2600
|
+
const headers = { retryAfter: null };
|
|
2601
|
+
const answer = await this.request("updateMachine", key, { params: { name }, body: withoutUndefined({ ...change, rebuild: true }), ifMatch: etag }, (response) => {
|
|
2602
|
+
headers.retryAfter = response.headers.get("retry-after");
|
|
2603
|
+
});
|
|
2604
|
+
return {
|
|
2605
|
+
machine: answer.data,
|
|
2606
|
+
rebuild: answer.rebuild,
|
|
2607
|
+
retryAfterSeconds: retryAfterSeconds(headers.retryAfter)
|
|
2608
|
+
};
|
|
2609
|
+
}
|
|
2486
2610
|
/** With `etag`, only the Machine that ETag names, at that revision, is put
|
|
2487
2611
|
* to sleep (machine-v2.json `etag.if_match.sleep` is optional). A 202
|
|
2488
2612
|
* carries the `Retry-After` to read the Machine again after
|
|
@@ -2511,14 +2635,25 @@ var ManagementApi = class {
|
|
|
2511
2635
|
* says whether a name was released (`managed`: its create never reached its
|
|
2512
2636
|
* region) or a device was forgotten (`byoc`: its region held no row). With
|
|
2513
2637
|
* `etag`, only the Machine that ETag names is deleted
|
|
2514
|
-
* (machine-v2.json `etag.if_match.delete` is optional).
|
|
2515
|
-
|
|
2638
|
+
* (machine-v2.json `etag.if_match.delete` is optional). `deleteVolume`
|
|
2639
|
+
* decides the Volume a managed Machine holds (machine-v2.json
|
|
2640
|
+
* `delete_volume`); without it the server's default applies, and a 202
|
|
2641
|
+
* says in `volume` what became of it. */
|
|
2642
|
+
async deleteMachine(key, name, etag, deleteVolume) {
|
|
2516
2643
|
const headers = { kind: null };
|
|
2517
|
-
const answer = await this.request("deleteMachine", key, {
|
|
2644
|
+
const answer = await this.request("deleteMachine", key, {
|
|
2645
|
+
params: { name },
|
|
2646
|
+
query: { delete_volume: deleteVolume === void 0 ? void 0 : String(deleteVolume) },
|
|
2647
|
+
...etag ? { ifMatch: etag } : {}
|
|
2648
|
+
}, (response) => {
|
|
2518
2649
|
headers.kind = response.headers.get("x-tonbo-machine-kind");
|
|
2519
2650
|
});
|
|
2520
2651
|
if (answer?.data)
|
|
2521
|
-
return {
|
|
2652
|
+
return {
|
|
2653
|
+
machine: answer.data,
|
|
2654
|
+
kind: answer.data.kind,
|
|
2655
|
+
...answer.volume === void 0 ? {} : { volume: answer.volume }
|
|
2656
|
+
};
|
|
2522
2657
|
return { machine: null, kind: headers.kind === "byoc" ? "byoc" : "managed" };
|
|
2523
2658
|
}
|
|
2524
2659
|
/** One page of a managed Machine's activations, newest first; the cursor
|
|
@@ -5177,6 +5312,128 @@ async function databaseActivityCommand(deps, name, options = {}) {
|
|
|
5177
5312
|
);
|
|
5178
5313
|
printActivity(deps, options.json, activity, "Database");
|
|
5179
5314
|
}
|
|
5315
|
+
var REGISTRY_COLUMNS = {
|
|
5316
|
+
stored_bytes: ["STORED", (value) => bytes(value)],
|
|
5317
|
+
log_bytes: ["LOG", (value) => bytes(value)],
|
|
5318
|
+
snapshot_bytes: ["SNAPSHOT", (value) => bytes(value)],
|
|
5319
|
+
write_units: ["WRITE UNITS", String],
|
|
5320
|
+
write_bytes: ["WRITTEN", (value) => bytes(value)],
|
|
5321
|
+
records: ["RECORDS", String],
|
|
5322
|
+
reads: ["READS", String],
|
|
5323
|
+
read_bytes: ["READ", (value) => bytes(value)],
|
|
5324
|
+
live_reads: ["LIVE READS", String],
|
|
5325
|
+
live_read_bytes: ["LIVE READ", (value) => bytes(value)],
|
|
5326
|
+
server_write_latency_p50_ms: ["WRITE P50", (value) => `${value} ms`],
|
|
5327
|
+
server_write_latency_p99_ms: ["WRITE P99", (value) => `${value} ms`],
|
|
5328
|
+
server_read_latency_p50_ms: ["READ P50", (value) => `${value} ms`],
|
|
5329
|
+
server_read_latency_p99_ms: ["READ P99", (value) => `${value} ms`]
|
|
5330
|
+
};
|
|
5331
|
+
var SERVER_LATENCY = "Latency is server latency: measured at Tonbo's servers, from request to response headers; your network adds to this.";
|
|
5332
|
+
function registryMetricsTable(granularity, points) {
|
|
5333
|
+
const series = Object.keys(REGISTRY_COLUMNS).filter(
|
|
5334
|
+
(name) => points.some((point) => name in point)
|
|
5335
|
+
);
|
|
5336
|
+
return {
|
|
5337
|
+
latency: series.some((name) => name.startsWith("server_")),
|
|
5338
|
+
text: table(
|
|
5339
|
+
[
|
|
5340
|
+
granularity === "hour" ? "HOUR (UTC)" : "DAY (UTC)",
|
|
5341
|
+
...series.map((name) => REGISTRY_COLUMNS[name][0])
|
|
5342
|
+
],
|
|
5343
|
+
points.map((point) => [
|
|
5344
|
+
granularity === "hour" ? point.start.slice(0, 16).replace("T", " ") : point.start.slice(0, 10),
|
|
5345
|
+
...series.map((name) => {
|
|
5346
|
+
const value = point[name];
|
|
5347
|
+
if (value === null || value === void 0)
|
|
5348
|
+
return Date.parse(point.start) > Date.now() ? "" : "-";
|
|
5349
|
+
return REGISTRY_COLUMNS[name][1](value);
|
|
5350
|
+
})
|
|
5351
|
+
])
|
|
5352
|
+
)
|
|
5353
|
+
};
|
|
5354
|
+
}
|
|
5355
|
+
function printRegistryMetrics(deps, noun, metrics, options) {
|
|
5356
|
+
const rendered = registryMetricsTable(metrics.granularity, metrics.points);
|
|
5357
|
+
print(
|
|
5358
|
+
deps,
|
|
5359
|
+
options.json,
|
|
5360
|
+
metrics,
|
|
5361
|
+
[
|
|
5362
|
+
`${noun} ${metrics.name} in ${metrics.region} from ${metrics.from} to ${metrics.to} (UTC; metered up to ${when(metrics.as_of, "not yet metered")})`,
|
|
5363
|
+
rendered.text,
|
|
5364
|
+
...rendered.latency ? [SERVER_LATENCY] : []
|
|
5365
|
+
].join("\n")
|
|
5366
|
+
);
|
|
5367
|
+
}
|
|
5368
|
+
async function databaseMetricsCommand(deps, name, options = {}) {
|
|
5369
|
+
const metrics = await deps.api.getDatabaseMetrics(
|
|
5370
|
+
await deps.auth.apiKey(),
|
|
5371
|
+
name,
|
|
5372
|
+
metricsQuery(options)
|
|
5373
|
+
);
|
|
5374
|
+
printRegistryMetrics(deps, "Database", metrics, options);
|
|
5375
|
+
}
|
|
5376
|
+
async function streamMetricsCommand(deps, name, options = {}) {
|
|
5377
|
+
const metrics = await deps.api.getStreamMetrics(
|
|
5378
|
+
await deps.auth.apiKey(),
|
|
5379
|
+
name,
|
|
5380
|
+
metricsQuery(options)
|
|
5381
|
+
);
|
|
5382
|
+
printRegistryMetrics(deps, "Stream", metrics, options);
|
|
5383
|
+
}
|
|
5384
|
+
function printAccountRegistryMetrics(deps, noun, metrics, options) {
|
|
5385
|
+
const rendered = registryMetricsTable(metrics.granularity, metrics.total.points);
|
|
5386
|
+
const lines = [
|
|
5387
|
+
`The account's ${noun} from ${metrics.from} to ${metrics.to} (UTC; metered up to ${when(metrics.as_of, "not yet metered")})`,
|
|
5388
|
+
rendered.text
|
|
5389
|
+
];
|
|
5390
|
+
const counted = ["write_units", "write_bytes", "records", "reads", "read_bytes"].filter((name) => metrics.total.points.some((point) => name in point));
|
|
5391
|
+
const sized = metrics.total.points.some((point) => "stored_bytes" in point);
|
|
5392
|
+
if (metrics.resources.length > 0 && (counted.length > 0 || sized)) {
|
|
5393
|
+
lines.push(
|
|
5394
|
+
"",
|
|
5395
|
+
table(
|
|
5396
|
+
[
|
|
5397
|
+
noun === "databases" ? "DATABASE" : "STREAM",
|
|
5398
|
+
"REGION",
|
|
5399
|
+
...sized ? ["STORED"] : [],
|
|
5400
|
+
...counted.map((name) => REGISTRY_COLUMNS[name][0])
|
|
5401
|
+
],
|
|
5402
|
+
metrics.resources.map((resource) => {
|
|
5403
|
+
const stored = [...resource.points].reverse().find((point) => point.stored_bytes !== null && point.stored_bytes !== void 0);
|
|
5404
|
+
return [
|
|
5405
|
+
resource.name,
|
|
5406
|
+
resource.region,
|
|
5407
|
+
...sized ? [stored ? bytes(stored.stored_bytes ?? 0) : "-"] : [],
|
|
5408
|
+
...counted.map(
|
|
5409
|
+
(name) => REGISTRY_COLUMNS[name][1](
|
|
5410
|
+
resource.points.reduce((total, point) => total + (point[name] ?? 0), 0)
|
|
5411
|
+
)
|
|
5412
|
+
)
|
|
5413
|
+
];
|
|
5414
|
+
})
|
|
5415
|
+
)
|
|
5416
|
+
);
|
|
5417
|
+
}
|
|
5418
|
+
if (rendered.latency) lines.push(SERVER_LATENCY);
|
|
5419
|
+
if (metrics.unavailable_regions.length > 0)
|
|
5420
|
+
lines.push(`Not read: ${metrics.unavailable_regions.join(", ")}`);
|
|
5421
|
+
print(deps, options.json, metrics, lines.join("\n"));
|
|
5422
|
+
}
|
|
5423
|
+
async function metricsDatabasesCommand(deps, options = {}) {
|
|
5424
|
+
const metrics = await deps.api.getAccountDatabaseMetrics(
|
|
5425
|
+
await deps.auth.apiKey(),
|
|
5426
|
+
metricsQuery(options)
|
|
5427
|
+
);
|
|
5428
|
+
printAccountRegistryMetrics(deps, "databases", metrics, options);
|
|
5429
|
+
}
|
|
5430
|
+
async function metricsStreamsCommand(deps, options = {}) {
|
|
5431
|
+
const metrics = await deps.api.getAccountStreamMetrics(
|
|
5432
|
+
await deps.auth.apiKey(),
|
|
5433
|
+
metricsQuery(options)
|
|
5434
|
+
);
|
|
5435
|
+
printAccountRegistryMetrics(deps, "streams", metrics, options);
|
|
5436
|
+
}
|
|
5180
5437
|
function streamFields(stream) {
|
|
5181
5438
|
return fields([
|
|
5182
5439
|
["Name", stream.name],
|
|
@@ -5777,12 +6034,13 @@ var MACHINE_ERROR_HINTS = {
|
|
|
5777
6034
|
machine_kind_mismatch: "That Machine is one of your computers: `tonbo machine show` and `tonbo machine delete` serve it, and nothing else does.",
|
|
5778
6035
|
volumes_not_offered: "Volumes are not offered to this account in the Machine's region yet; nothing was created or changed.",
|
|
5779
6036
|
root_mode_not_offered: "A Volume as the Machine's whole disk is not offered to this account yet; nothing was created or changed. Mount it at a path instead: --volume <name>:<path>.",
|
|
5780
|
-
rebuild_required: "This Machine's Volume is its whole disk, so its image changes only by Rebuild
|
|
5781
|
-
|
|
6037
|
+
rebuild_required: "This Machine's Volume is its whole disk, so its image changes only by Rebuild; nothing was changed. Once you have read what Rebuild deletes, run `tonbo machine rebuild <name> --image <reference>`, or the same update with --rebuild.",
|
|
6038
|
+
volume_not_whole_disk: "This Machine's Volume is mounted at a path, or it has none, so it needs no Rebuild; nothing was changed. Change its image with `tonbo machine update <name> --image <reference>`.",
|
|
6039
|
+
volume_mode_change: "A Machine's whole disk is never mounted at a path, and a Volume mounted at a path becomes a whole disk only on its own Machine; nothing was changed.",
|
|
5782
6040
|
volume_attached: "The Machine the message names is awake with the Volume attached; nothing was changed. Put it to sleep with `tonbo machine sleep <machine>`, then run the command again.",
|
|
5783
6041
|
volume_holder_changed: "The Volume is not on the Machine you named; nothing was changed. `tonbo volume show <volume>` says which Machine has it: name that one, or use `tonbo volume move`.",
|
|
5784
6042
|
volume_image_mismatch: "The Volume is a Machine's whole disk, made against one image; it moves only to a Machine on that same image. Nothing was changed.",
|
|
5785
|
-
volume_rebuild_pending: "The Volume's Rebuild
|
|
6043
|
+
volume_rebuild_pending: "The Volume's Rebuild is recorded and applies at its Machine's next wake: wake that Machine once (`tonbo machine wake <machine>`), then move the Volume. Nothing was changed.",
|
|
5786
6044
|
secret_env_conflict: "Use another secret key or remove the non-secret env variable with that name.",
|
|
5787
6045
|
secret_limit: "Reduce the number or byte size of this Machine's secrets.",
|
|
5788
6046
|
wake_unavailable: "The wake was refused for now; nothing woke. Wake it again after a while; `tonbo machine show` says why it is blocked."
|
|
@@ -5902,7 +6160,8 @@ var MACHINE_WAKE_BLOCKED_HINTS = {
|
|
|
5902
6160
|
capacity: "No host had room for its shape: Tonbo is adding a host, <minutes>, and the next request after that wakes it.",
|
|
5903
6161
|
region_closed: "Machines are closed in its region for now; the next request after they reopen wakes it.",
|
|
5904
6162
|
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.",
|
|
5905
|
-
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."
|
|
6163
|
+
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.",
|
|
6164
|
+
root_image_mismatch: "Its disk was made over another image than the Machine's: run `tonbo machine rebuild <name>` to move the disk to the Machine's image (Rebuild deletes changes outside /home, /workspace and /root)."
|
|
5906
6165
|
};
|
|
5907
6166
|
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.";
|
|
5908
6167
|
function minutesUntil(retryAt, now = Date.now()) {
|
|
@@ -5975,7 +6234,7 @@ function imageFailureFields(machine) {
|
|
|
5975
6234
|
...next ? [["Next step", next]] : []
|
|
5976
6235
|
];
|
|
5977
6236
|
}
|
|
5978
|
-
function machineFields(machine, billing, etag = null, lastEnd = null) {
|
|
6237
|
+
function machineFields(machine, billing, etag = null, lastEnd = null, volumeName2 = null) {
|
|
5979
6238
|
return fields([
|
|
5980
6239
|
["Name", machine.name],
|
|
5981
6240
|
["ID", machine.id],
|
|
@@ -5998,7 +6257,7 @@ function machineFields(machine, billing, etag = null, lastEnd = null) {
|
|
|
5998
6257
|
["SSH", `${machine.hostnames.ssh} (tonbo ssh ${machine.name})`],
|
|
5999
6258
|
["Env", Object.keys(machine.env).sort().join(", ") || "none"],
|
|
6000
6259
|
["Stop grace", `${machine.stop_grace_seconds} s`],
|
|
6001
|
-
["Volume", volumeText(machine)],
|
|
6260
|
+
["Volume", volumeText(machine, volumeName2)],
|
|
6002
6261
|
...deleteFields(machine),
|
|
6003
6262
|
["Database", machine.database ? `${machine.database.name} (${machine.database.url})` : "none"],
|
|
6004
6263
|
["Logs", machine.logs.url],
|
|
@@ -6043,13 +6302,28 @@ function deleteFields(machine) {
|
|
|
6043
6302
|
]
|
|
6044
6303
|
];
|
|
6045
6304
|
}
|
|
6046
|
-
function volumeText(machine) {
|
|
6305
|
+
function volumeText(machine, name = null) {
|
|
6047
6306
|
const binding = machine.volumes?.[0];
|
|
6048
6307
|
if (!binding) return "none";
|
|
6049
|
-
|
|
6308
|
+
if (name === null)
|
|
6309
|
+
return binding.path === void 0 ? `${binding.volume} (whole disk)` : `${binding.volume} at ${binding.path}`;
|
|
6310
|
+
return binding.path === void 0 ? `${name} (whole disk; id ${binding.volume})` : `${name} at ${binding.path} (id ${binding.volume})`;
|
|
6311
|
+
}
|
|
6312
|
+
async function volumeName(deps, key, machine) {
|
|
6313
|
+
const id = machine.volumes?.[0]?.volume;
|
|
6314
|
+
if (id === void 0) return null;
|
|
6315
|
+
try {
|
|
6316
|
+
return (await deps.api.listVolumes(key)).data.find((volume) => volume.id === id)?.name ?? null;
|
|
6317
|
+
} catch (error) {
|
|
6318
|
+
if (!(error instanceof HttpError || error instanceof AccessTokenError)) throw error;
|
|
6319
|
+
return null;
|
|
6320
|
+
}
|
|
6050
6321
|
}
|
|
6051
|
-
function machineText(machine, billing, etag = null, lastEnd = null) {
|
|
6052
|
-
return [
|
|
6322
|
+
function machineText(machine, billing, etag = null, lastEnd = null, volume = null) {
|
|
6323
|
+
return [
|
|
6324
|
+
machineFields(machine, billing, etag, lastEnd, volume),
|
|
6325
|
+
...privatePortLines(machine)
|
|
6326
|
+
].join("\n");
|
|
6053
6327
|
}
|
|
6054
6328
|
function keyStateText(machine) {
|
|
6055
6329
|
const state = machine.byoc.key_state;
|
|
@@ -6074,8 +6348,8 @@ function deviceFields(machine) {
|
|
|
6074
6348
|
["Updated", machine.updated_at]
|
|
6075
6349
|
]);
|
|
6076
6350
|
}
|
|
6077
|
-
function anyMachineText(machine, billing, etag = null, lastEnd = null) {
|
|
6078
|
-
return machine.kind === "byoc" ? deviceFields(machine) : machineText(machine, billing, etag, lastEnd);
|
|
6351
|
+
function anyMachineText(machine, billing, etag = null, lastEnd = null, volume = null) {
|
|
6352
|
+
return machine.kind === "byoc" ? deviceFields(machine) : machineText(machine, billing, etag, lastEnd, volume);
|
|
6079
6353
|
}
|
|
6080
6354
|
function parsePorts(specs) {
|
|
6081
6355
|
const accesses = machinePortAccess;
|
|
@@ -6444,11 +6718,15 @@ async function machineShowCommand(deps, name, options = {}) {
|
|
|
6444
6718
|
...processes.running.map((line) => `Background process: ${line}`),
|
|
6445
6719
|
...processes.last ? [`Last background process: ${processes.last}`] : []
|
|
6446
6720
|
] : [];
|
|
6721
|
+
const volume = options.json || machine.kind === "byoc" ? null : await volumeName(deps, key, machine);
|
|
6447
6722
|
print(
|
|
6448
6723
|
deps,
|
|
6449
6724
|
options.json,
|
|
6450
6725
|
machine,
|
|
6451
|
-
[
|
|
6726
|
+
[
|
|
6727
|
+
anyMachineText(machine, await billingUrl(deps), etag, lastEnd, volume),
|
|
6728
|
+
...processLines
|
|
6729
|
+
].join("\n")
|
|
6452
6730
|
);
|
|
6453
6731
|
});
|
|
6454
6732
|
}
|
|
@@ -6532,6 +6810,8 @@ function appliesText(name, change) {
|
|
|
6532
6810
|
}
|
|
6533
6811
|
async function machineUpdateCommand(deps, name, command, options = {}) {
|
|
6534
6812
|
await machineCommand(async () => {
|
|
6813
|
+
if (options.rebuild && options.image === void 0)
|
|
6814
|
+
throw new Error("--rebuild needs --image: it moves the Machine's whole disk to that image.");
|
|
6535
6815
|
if (command.length && options.imageCommand)
|
|
6536
6816
|
throw new Error("Give a command or --image-command, not both.");
|
|
6537
6817
|
if (options.clearPorts && (options.port?.length || options.defaultPort !== void 0))
|
|
@@ -6576,7 +6856,27 @@ async function machineUpdateCommand(deps, name, command, options = {}) {
|
|
|
6576
6856
|
if (options.discardSnapshot) change.discard_snapshot = true;
|
|
6577
6857
|
if (Object.values(change).every((value) => value === void 0))
|
|
6578
6858
|
throw new Error("Nothing to change: pass at least one option or a command.");
|
|
6579
|
-
const
|
|
6859
|
+
const rebuild = () => rebuildUntilDone(
|
|
6860
|
+
deps,
|
|
6861
|
+
key,
|
|
6862
|
+
name,
|
|
6863
|
+
DEFAULT_WAIT_SECONDS,
|
|
6864
|
+
() => deps.api.updateMachineRebuild(key, name, etag, change)
|
|
6865
|
+
);
|
|
6866
|
+
let rebuilt = options.rebuild ? await rebuild() : null;
|
|
6867
|
+
let machine = rebuilt?.machine;
|
|
6868
|
+
if (!machine)
|
|
6869
|
+
try {
|
|
6870
|
+
machine = await deps.api.updateMachine(key, name, etag, change);
|
|
6871
|
+
} catch (error) {
|
|
6872
|
+
if (!(error instanceof HttpError && error.code === "rebuild_required" && deps.interactive()))
|
|
6873
|
+
throw error;
|
|
6874
|
+
const question = `${rebuildWarning(options.image ?? current.image.reference)}
|
|
6875
|
+
This Machine's Volume is its whole disk, so its image changes only by Rebuild. Type the Machine's name to rebuild it with this change:`;
|
|
6876
|
+
if (await deps.ask(question) !== name) throw new Error("Cancelled; nothing was changed.");
|
|
6877
|
+
rebuilt = await rebuild();
|
|
6878
|
+
machine = rebuilt.machine;
|
|
6879
|
+
}
|
|
6580
6880
|
specWarnings(deps, machine.ports, Object.keys(setEnv));
|
|
6581
6881
|
print(
|
|
6582
6882
|
deps,
|
|
@@ -6584,12 +6884,80 @@ async function machineUpdateCommand(deps, name, command, options = {}) {
|
|
|
6584
6884
|
machine,
|
|
6585
6885
|
[
|
|
6586
6886
|
`Updated Machine ${machine.name} to revision ${machine.revision}.`,
|
|
6587
|
-
...appliesText(machine.name, change),
|
|
6887
|
+
...rebuilt ? [rebuiltText(machine.name, rebuilt.rebuild)] : appliesText(machine.name, change),
|
|
6588
6888
|
machineText(machine, await billingUrl(deps))
|
|
6589
6889
|
].join("\n")
|
|
6590
6890
|
);
|
|
6591
6891
|
});
|
|
6592
6892
|
}
|
|
6893
|
+
function rebuildWarning(image) {
|
|
6894
|
+
return machineRebuildWarning.map((paragraph) => paragraph.replaceAll("**", "").replaceAll("<image>", image)).join("\n");
|
|
6895
|
+
}
|
|
6896
|
+
function rebuiltText(name, answer) {
|
|
6897
|
+
const { reference, digest } = answer.image;
|
|
6898
|
+
const image = digest === null ? reference : `${reference} (${digest})`;
|
|
6899
|
+
if (answer.state === "not_needed")
|
|
6900
|
+
return `Machine ${name}'s disk was never used, so it moves to ${image} with no Rebuild.`;
|
|
6901
|
+
return `Machine ${name} rebuilds onto ${image} at its next wake; files in /home, /workspace and /root are kept.`;
|
|
6902
|
+
}
|
|
6903
|
+
async function rebuildUntilDone(deps, key, name, timeout, send) {
|
|
6904
|
+
const now = deps.now ?? Date.now;
|
|
6905
|
+
const start = now();
|
|
6906
|
+
for (; ; ) {
|
|
6907
|
+
const answer = await send();
|
|
6908
|
+
if (answer.rebuild.state !== "waiting_for_sleep") return answer;
|
|
6909
|
+
deps.notice(
|
|
6910
|
+
`Machine ${name} is awake with its disk attached, so it is going to sleep first; the Rebuild is sent again once it is asleep.`
|
|
6911
|
+
);
|
|
6912
|
+
const { machine: asleep } = await waitForMachine(
|
|
6913
|
+
deps,
|
|
6914
|
+
key,
|
|
6915
|
+
name,
|
|
6916
|
+
answer.retryAfterSeconds ?? sleepPollAfterSeconds,
|
|
6917
|
+
sleepPollAfterSeconds,
|
|
6918
|
+
timeout - (now() - start) / 1e3,
|
|
6919
|
+
(read) => read.status.power === "asleep" || read.state !== "active"
|
|
6920
|
+
);
|
|
6921
|
+
if (!asleep)
|
|
6922
|
+
throw new Error(
|
|
6923
|
+
`Machine ${name} is not asleep after ${Math.round((now() - start) / 1e3)} s, so nothing was rebuilt; run the command again once it sleeps. \`tonbo machine show ${name}\` says what keeps it awake.`
|
|
6924
|
+
);
|
|
6925
|
+
if (asleep.status.power !== "asleep")
|
|
6926
|
+
throw new Error(`Machine ${name} is ${asleep.state}; nothing was rebuilt.`);
|
|
6927
|
+
}
|
|
6928
|
+
}
|
|
6929
|
+
async function machineRebuildCommand(deps, name, options = {}) {
|
|
6930
|
+
await machineCommand(async () => {
|
|
6931
|
+
const key = await deps.auth.apiKey();
|
|
6932
|
+
const { machine: read, etag } = await deps.api.getMachineWithEtag(key, name);
|
|
6933
|
+
if (read.kind === "byoc")
|
|
6934
|
+
throw new Error(
|
|
6935
|
+
`${name} is one of your computers; only \`tonbo machine show\` and \`tonbo machine delete\` serve it.`
|
|
6936
|
+
);
|
|
6937
|
+
const image = options.image ?? read.image.reference;
|
|
6938
|
+
await confirm(
|
|
6939
|
+
deps,
|
|
6940
|
+
options.yes,
|
|
6941
|
+
`rebuild Machine ${name}`,
|
|
6942
|
+
() => Promise.resolve(`${rebuildWarning(image)}
|
|
6943
|
+
Type the Machine's name to rebuild it:`),
|
|
6944
|
+
name
|
|
6945
|
+
);
|
|
6946
|
+
const answer = await rebuildUntilDone(
|
|
6947
|
+
deps,
|
|
6948
|
+
key,
|
|
6949
|
+
name,
|
|
6950
|
+
options.timeout ?? DEFAULT_WAIT_SECONDS,
|
|
6951
|
+
() => deps.api.rebuildMachine(key, name, { image: options.image }, etag ?? void 0)
|
|
6952
|
+
);
|
|
6953
|
+
print(
|
|
6954
|
+
deps,
|
|
6955
|
+
options.json,
|
|
6956
|
+
{ data: answer.machine, rebuild: answer.rebuild },
|
|
6957
|
+
rebuiltText(name, answer.rebuild)
|
|
6958
|
+
);
|
|
6959
|
+
});
|
|
6960
|
+
}
|
|
6593
6961
|
function sleptText(machine) {
|
|
6594
6962
|
if (machine.state === "creating")
|
|
6595
6963
|
return `Machine ${machine.name} is still creating; nothing to put to sleep.`;
|
|
@@ -6672,43 +7040,61 @@ async function machineWakeCommand(deps, name, options = {}) {
|
|
|
6672
7040
|
}
|
|
6673
7041
|
async function machineDeleteCommand(deps, name, options = {}) {
|
|
6674
7042
|
await machineCommand(async () => {
|
|
7043
|
+
if (options.keepVolume && options.deleteVolume)
|
|
7044
|
+
throw new Error("Give --keep-volume or --delete-volume, not both.");
|
|
7045
|
+
const deleteVolume = options.deleteVolume ? true : options.keepVolume ? false : void 0;
|
|
6675
7046
|
const key = await deps.auth.apiKey();
|
|
6676
7047
|
const read = { etag: null };
|
|
6677
7048
|
await confirm(
|
|
6678
7049
|
deps,
|
|
6679
7050
|
options.yes,
|
|
6680
7051
|
`delete Machine ${name}`,
|
|
6681
|
-
() => deleteQuestion(deps, key, name, read),
|
|
7052
|
+
() => deleteQuestion(deps, key, name, read, deleteVolume),
|
|
6682
7053
|
name
|
|
6683
7054
|
);
|
|
6684
|
-
const { machine, kind } = await deps.api.deleteMachine(
|
|
7055
|
+
const { machine, kind, volume } = await deps.api.deleteMachine(
|
|
6685
7056
|
key,
|
|
6686
7057
|
name,
|
|
6687
|
-
ifMatchHeader(options.ifMatch) ?? read.etag ?? void 0
|
|
7058
|
+
ifMatchHeader(options.ifMatch) ?? read.etag ?? void 0,
|
|
7059
|
+
deleteVolume
|
|
7060
|
+
);
|
|
7061
|
+
print(
|
|
7062
|
+
deps,
|
|
7063
|
+
options.json,
|
|
7064
|
+
machine,
|
|
7065
|
+
[deletedText(name, machine, kind), ...volume ? [volumeFateText(volume)] : []].join("\n")
|
|
6688
7066
|
);
|
|
6689
|
-
print(deps, options.json, machine, deletedText(name, machine, kind));
|
|
6690
7067
|
if (machine?.kind === "byoc" && machine.byoc.key_state === "active")
|
|
6691
7068
|
deps.notice(
|
|
6692
7069
|
`Its key is still active, so ${name} registers again at its next start. Run \`yield logout\` on it, or revoke its key with \`tonbo api-key revoke\`, then forget it again.`
|
|
6693
7070
|
);
|
|
6694
7071
|
});
|
|
6695
7072
|
}
|
|
6696
|
-
async function deleteQuestion(deps, key, name, read) {
|
|
6697
|
-
let
|
|
6698
|
-
let ownDatabase = false;
|
|
7073
|
+
async function deleteQuestion(deps, key, name, read, deleteVolume) {
|
|
7074
|
+
let machine = null;
|
|
6699
7075
|
try {
|
|
6700
7076
|
const answer = await deps.api.getMachineWithEtag(key, name);
|
|
6701
|
-
|
|
6702
|
-
ownDatabase = answer.machine.kind === "managed" && !!answer.machine.database?.name.startsWith("m.");
|
|
7077
|
+
machine = answer.machine;
|
|
6703
7078
|
read.etag = answer.etag;
|
|
6704
7079
|
} catch (error) {
|
|
6705
7080
|
if (!(error instanceof HttpError && error.status === 404)) throw error;
|
|
6706
7081
|
}
|
|
6707
|
-
if (kind === "byoc")
|
|
7082
|
+
if (machine?.kind === "byoc")
|
|
6708
7083
|
return `Forget your computer ${name}? It disappears from your Machines; its key is not revoked. Type its name to confirm:`;
|
|
6709
|
-
if (kind
|
|
6710
|
-
|
|
6711
|
-
|
|
7084
|
+
if (machine?.kind !== "managed") return `Delete Machine ${name}? Type its name to confirm:`;
|
|
7085
|
+
const ownDatabase = !!machine.database?.name.startsWith("m.");
|
|
7086
|
+
const binding = machine.volumes?.[0];
|
|
7087
|
+
const parts = [
|
|
7088
|
+
...binding && binding.path === void 0 ? [] : ["disk"],
|
|
7089
|
+
...ownDatabase ? ["database"] : []
|
|
7090
|
+
];
|
|
7091
|
+
const what = parts.length ? `${parts.join(", ")} and logs` : "logs";
|
|
7092
|
+
const volume = binding ? `Volume ${await volumeName(deps, key, machine) ?? binding.volume}` : "";
|
|
7093
|
+
const fate = !binding ? "" : deleteVolume === true ? ` Its ${volume} is deleted with it.` : deleteVolume === false ? ` Its ${volume} is kept.` : ` Its ${volume} is deleted with it if it was made for this Machine (as the console makes a new Machine's disk), and kept otherwise; --keep-volume or --delete-volume decides.`;
|
|
7094
|
+
return `Delete Machine ${name} with its ${what}?${fate} This cannot be undone. Type the Machine's name to confirm:`;
|
|
7095
|
+
}
|
|
7096
|
+
function volumeFateText(volume) {
|
|
7097
|
+
return volume.deleted ? `Volume ${volume.name}: deleted with it.` : `Volume ${volume.name}: kept; delete it with \`tonbo volume delete ${volume.name}\`.`;
|
|
6712
7098
|
}
|
|
6713
7099
|
function deletedText(name, machine, kind) {
|
|
6714
7100
|
if (kind === "byoc")
|
|
@@ -6838,7 +7224,7 @@ var TOPICS = [
|
|
|
6838
7224
|
id: "confirmations",
|
|
6839
7225
|
title: "Destructive commands",
|
|
6840
7226
|
text: [
|
|
6841
|
-
"`api-key revoke`, `bucket delete`, `database delete`, `stream delete`, `machine delete` and `volume delete` ask before they act when stdin and stderr are a terminal: a revocation is confirmed by typing `yes`, a deletion by typing the resource's name, as in the console. Without a terminal they refuse unless `--yes` is given."
|
|
7227
|
+
"`api-key revoke`, `bucket delete`, `database delete`, `stream delete`, `machine delete`, `machine rebuild` and `volume delete` ask before they act when stdin and stderr are a terminal: a revocation is confirmed by typing `yes`, a deletion or a Rebuild by typing the resource's name, as in the console. Without a terminal they refuse unless `--yes` is given."
|
|
6842
7228
|
]
|
|
6843
7229
|
}
|
|
6844
7230
|
];
|
|
@@ -7077,6 +7463,18 @@ var COMMAND_HELP = {
|
|
|
7077
7463
|
arguments: NAME("database"),
|
|
7078
7464
|
defaults: ACTIVITY_DEFAULTS
|
|
7079
7465
|
},
|
|
7466
|
+
"tonbo database metrics": {
|
|
7467
|
+
about: [
|
|
7468
|
+
"Prints the database's metrics, one row per UTC hour or day: its stored size at the step's end (the log and snapshots it keeps), the write units, bytes and records written, the successful reads and live (long-poll or SSE) reads and the bytes they streamed, and the median and 99th percentile server latency of its appends and catch-up reads. Server latency is measured at Tonbo's servers, from request to response headers; your network adds to this. A dash is a value not measured (a size before the meter's first sample, a latency with no request); the current step is still growing.",
|
|
7469
|
+
"Give `--window`, or `--from` and `--to` with `--granularity` (at most 31 days hourly, 92 days daily; server latency is kept 8 days hourly). `--series` prints only the series it names. It carries no money: `tonbo database usage` says what it cost."
|
|
7470
|
+
],
|
|
7471
|
+
examples: [
|
|
7472
|
+
"tonbo database metrics notes --window 7d",
|
|
7473
|
+
"tonbo database metrics notes --series server_write_latency_p50_ms,server_write_latency_p99_ms"
|
|
7474
|
+
],
|
|
7475
|
+
arguments: NAME("database"),
|
|
7476
|
+
defaults: { "--window": "24h" }
|
|
7477
|
+
},
|
|
7080
7478
|
"tonbo database usage": {
|
|
7081
7479
|
about: [
|
|
7082
7480
|
"Prints each database's usage of one calendar month in UTC at list price, before the monthly allowance and credit: its write units, log and snapshot GiB-months and list value, then the account's other metered usage (its streams and everything else). The charges after allowance and credit are in `tonbo usage show`."
|
|
@@ -7135,6 +7533,18 @@ var COMMAND_HELP = {
|
|
|
7135
7533
|
arguments: NAME("stream"),
|
|
7136
7534
|
defaults: ACTIVITY_DEFAULTS
|
|
7137
7535
|
},
|
|
7536
|
+
"tonbo stream metrics": {
|
|
7537
|
+
about: [
|
|
7538
|
+
"Prints the stream's metrics, one row per UTC hour or day: its stored size at the step's end (the log and snapshots it keeps), the write units, bytes and records written, the successful reads and live (long-poll or SSE) reads and the bytes they streamed, and the median and 99th percentile server latency of its appends and catch-up reads. Server latency is measured at Tonbo's servers, from request to response headers; your network adds to this. A dash is a value not measured (a size before the meter's first sample, a latency with no request); the current step is still growing.",
|
|
7539
|
+
"Give `--window`, or `--from` and `--to` with `--granularity` (at most 31 days hourly, 92 days daily; server latency is kept 8 days hourly). `--series` prints only the series it names. It carries no money: `tonbo stream usage` says what it cost."
|
|
7540
|
+
],
|
|
7541
|
+
examples: [
|
|
7542
|
+
"tonbo stream metrics events --window 7d",
|
|
7543
|
+
"tonbo stream metrics events --series server_write_latency_p50_ms,server_write_latency_p99_ms"
|
|
7544
|
+
],
|
|
7545
|
+
arguments: NAME("stream"),
|
|
7546
|
+
defaults: { "--window": "24h" }
|
|
7547
|
+
},
|
|
7138
7548
|
"tonbo stream usage": {
|
|
7139
7549
|
about: [
|
|
7140
7550
|
"Prints each stream's usage of one calendar month in UTC at list price, before the monthly allowance and credit, then the account's other metered usage (its databases and everything else). The charges after allowance and credit are in `tonbo usage show`."
|
|
@@ -7176,7 +7586,7 @@ var COMMAND_HELP = {
|
|
|
7176
7586
|
},
|
|
7177
7587
|
"tonbo machine show": {
|
|
7178
7588
|
about: [
|
|
7179
|
-
"Prints the Machine's state and what it means (an `active` Machine that is asleep boots on its first request), its hostnames, image and whether it is pulled, the live activation and conditions with what to do about each, when it sleeps, the database it binds (or none) and its logs URL, a link to what its disk keeps, its ETag and who created it. For an `active` Machine it also reads the newest activations and prints how the last one ended. When the live activation runs an older revision than the Machine's, it says which changes already apply and that the rest apply after `tonbo machine sleep <name>`.",
|
|
7589
|
+
"Prints the Machine's state and what it means (an `active` Machine that is asleep boots on its first request), its hostnames, image and whether it is pulled, the live activation and conditions with what to do about each, when it sleeps, the database it binds (or none), its Volume by name and id as its whole disk or where it is mounted (or none), its logs URL, a link to what its disk keeps, its ETag and who created it. For an `active` Machine it also reads the newest activations and prints how the last one ended. When the live activation runs an older revision than the Machine's, it says which changes already apply and that the rest apply after `tonbo machine sleep <name>`.",
|
|
7180
7590
|
"When the default port is private, it prints the `curl` call that reaches it with a personal key. For one of your computers it prints its system, harnesses, hostname and device id, which only you see."
|
|
7181
7591
|
],
|
|
7182
7592
|
examples: ["tonbo machine show web"],
|
|
@@ -7207,10 +7617,13 @@ var COMMAND_HELP = {
|
|
|
7207
7617
|
"tonbo machine update": {
|
|
7208
7618
|
about: [
|
|
7209
7619
|
"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.",
|
|
7210
|
-
"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. A Machine whose Volume is its whole disk keeps its image: another image is refused with `rebuild_required`, since only Rebuild, which keeps /home, /workspace and /root and deletes every other change on the disk, may move it
|
|
7620
|
+
"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. A Machine whose Volume is its whole disk keeps its image: another image is refused with `rebuild_required`, since only Rebuild, which keeps /home, /workspace and /root and deletes every other change on the disk, may move it. At a terminal the CLI then prints what Rebuild deletes and, once you type the Machine's name, sends the change again as a Rebuild; `--rebuild` with `--image` sends it as one at once, as `tonbo machine rebuild` does, and on a Machine whose Volume is mounted at a path, or that has none, it is refused with `volume_not_whole_disk`. `--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.",
|
|
7211
7621
|
"`--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."
|
|
7212
7622
|
],
|
|
7213
|
-
examples: [
|
|
7623
|
+
examples: [
|
|
7624
|
+
"tonbo machine update web --env MODE=staging --sleep-after 300",
|
|
7625
|
+
"tonbo machine update agent --image ghcr.io/acme/agent:v2 --rebuild"
|
|
7626
|
+
],
|
|
7214
7627
|
arguments: {
|
|
7215
7628
|
name: "the Machine's name",
|
|
7216
7629
|
command: "the command that replaces the Machine's command at the next boot, after `--`"
|
|
@@ -7346,9 +7759,25 @@ var COMMAND_HELP = {
|
|
|
7346
7759
|
arguments: NAME("Machine"),
|
|
7347
7760
|
defaults: { "--timeout": String(DEFAULT_WAIT_SECONDS) }
|
|
7348
7761
|
},
|
|
7762
|
+
"tonbo machine rebuild": {
|
|
7763
|
+
about: [
|
|
7764
|
+
"Moves a Machine whose Volume is its whole disk to an image: `--image`, or without it the Machine's own reference resolved again, so a newer build of its tag is taken. Rebuild keeps /home, /workspace and /root and deletes every other change on the disk. At a terminal it prints the warning of what it deletes and asks you to type the Machine's name first, and sends the ETag it read then.",
|
|
7765
|
+
"An asleep Machine answers at once: the Rebuild is recorded and the disk moves to the image at the next wake, or, when the disk was never used, the image changes with no Rebuild. An awake Machine with its disk attached is put to sleep first and nothing else changes; the CLI reads it until it is asleep, for up to `--timeout`, and sends the Rebuild again. A Machine whose Volume is mounted at a path, or that has none, is refused with `volume_not_whole_disk`: change its image with `tonbo machine update --image`."
|
|
7766
|
+
],
|
|
7767
|
+
examples: [
|
|
7768
|
+
"tonbo machine rebuild agent --image ghcr.io/acme/agent:v2",
|
|
7769
|
+
"tonbo machine rebuild agent --yes"
|
|
7770
|
+
],
|
|
7771
|
+
arguments: NAME("Machine"),
|
|
7772
|
+
defaults: {
|
|
7773
|
+
"--image": "the Machine's own image reference, resolved again",
|
|
7774
|
+
"--timeout": String(DEFAULT_WAIT_SECONDS)
|
|
7775
|
+
},
|
|
7776
|
+
json: "answer"
|
|
7777
|
+
},
|
|
7349
7778
|
"tonbo machine delete": {
|
|
7350
7779
|
about: [
|
|
7351
|
-
"Deletes the Machine with its disk and logs; a
|
|
7780
|
+
"Deletes the Machine with its disk and logs; a database it binds stays. The Volume it holds is deleted with it when it was made for it, as the console makes a new Machine's disk, and kept otherwise; `--delete-volume` deletes any Volume it holds and `--keep-volume` keeps it. The command then says which. 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.",
|
|
7352
7781
|
"For one of your computers it forgets the computer: it leaves the list at once, and its name is free once the forget completes. Forgetting does not revoke the computer's key, so a computer whose key is still active registers again at its next start; run `yield logout` on it, or `tonbo api-key revoke`, first."
|
|
7353
7782
|
],
|
|
7354
7783
|
examples: ["tonbo machine delete web --yes"],
|
|
@@ -7476,6 +7905,22 @@ var COMMAND_HELP = {
|
|
|
7476
7905
|
examples: ["tonbo metrics machines --window 30d"],
|
|
7477
7906
|
defaults: { "--window": "7d" }
|
|
7478
7907
|
},
|
|
7908
|
+
"tonbo metrics databases": {
|
|
7909
|
+
about: [
|
|
7910
|
+
"Prints the account's databases per UTC hour or day, summed over every region, with the same series as `tonbo database metrics`: the total's server latency comes from every database's merged histograms, never an average of percentiles. Server latency is measured at Tonbo's servers, from request to response headers; your network adds to this. Then each database's row over the window: its stored size at the last measured step and its counts summed.",
|
|
7911
|
+
"Give `--window`, or `--from` and `--to` with `--granularity` (at most 31 days hourly, 92 days daily)."
|
|
7912
|
+
],
|
|
7913
|
+
examples: ["tonbo metrics databases --window 30d --series stored_bytes,write_units"],
|
|
7914
|
+
defaults: { "--window": "7d" }
|
|
7915
|
+
},
|
|
7916
|
+
"tonbo metrics streams": {
|
|
7917
|
+
about: [
|
|
7918
|
+
"Prints the account's streams per UTC hour or day, summed over every region, with the same series as `tonbo stream metrics`: the total's server latency comes from every stream's merged histograms, never an average of percentiles. Server latency is measured at Tonbo's servers, from request to response headers; your network adds to this. Then each stream's row over the window: its stored size at the last measured step and its counts summed.",
|
|
7919
|
+
"Give `--window`, or `--from` and `--to` with `--granularity` (at most 31 days hourly, 92 days daily)."
|
|
7920
|
+
],
|
|
7921
|
+
examples: ["tonbo metrics streams --window 30d --series stored_bytes,write_units"],
|
|
7922
|
+
defaults: { "--window": "7d" }
|
|
7923
|
+
},
|
|
7479
7924
|
"tonbo metrics volumes": {
|
|
7480
7925
|
about: [
|
|
7481
7926
|
"Prints what the account's Volumes stored, one line per UTC day of the window: the average bytes their files held that day, compressed, summed over every region, which is what they are billed for. Today, and a day not fully rated yet, is marked `so far`. Then the change between the window's first and last complete days.",
|
|
@@ -9118,6 +9563,15 @@ function createProgram(makeDependencies = createDependencies) {
|
|
|
9118
9563
|
).action(
|
|
9119
9564
|
async (name, options) => databaseActivityCommand(dependencies(), name, options)
|
|
9120
9565
|
);
|
|
9566
|
+
operationOptions(
|
|
9567
|
+
database.command("metrics <name>").description("show a database's writes, reads, stored size and server latency over time").option("--window <window>", "24h or 7d hourly, 30d or 90d daily; 24h when omitted").option(
|
|
9568
|
+
"--series <series>",
|
|
9569
|
+
"the series to print, comma-separated; every series when omitted"
|
|
9570
|
+
).option("--granularity <step>", "with --from and --to, hour or day").option("--from <time>", "with --to, the window's start, on a UTC hour or day").option("--to <time>", "with --from, the window's end, exclusive"),
|
|
9571
|
+
"getDatabaseMetrics"
|
|
9572
|
+
).action(
|
|
9573
|
+
async (name, options) => databaseMetricsCommand(dependencies(), name, options)
|
|
9574
|
+
);
|
|
9121
9575
|
operationOptions(
|
|
9122
9576
|
database.command("usage").description("each database's usage of one month at list price, before allowance and credit").option("--period <month>", "the calendar month in UTC, as YYYY-MM").option("--sort <order>", "list_value (highest first) or name", parseUsageSort).option("--prefix <prefix>", "only databases whose name starts with this").option("--names <names>", "only these databases, up to 100 names, comma-separated"),
|
|
9123
9577
|
"listDatabaseUsage"
|
|
@@ -9179,6 +9633,15 @@ function createProgram(makeDependencies = createDependencies) {
|
|
|
9179
9633
|
).action(
|
|
9180
9634
|
async (name, options) => streamActivityCommand(dependencies(), name, options)
|
|
9181
9635
|
);
|
|
9636
|
+
operationOptions(
|
|
9637
|
+
stream.command("metrics <name>").description("show a stream's writes, reads, stored size and server latency over time").option("--window <window>", "24h or 7d hourly, 30d or 90d daily; 24h when omitted").option(
|
|
9638
|
+
"--series <series>",
|
|
9639
|
+
"the series to print, comma-separated; every series when omitted"
|
|
9640
|
+
).option("--granularity <step>", "with --from and --to, hour or day").option("--from <time>", "with --to, the window's start, on a UTC hour or day").option("--to <time>", "with --from, the window's end, exclusive"),
|
|
9641
|
+
"getStreamMetrics"
|
|
9642
|
+
).action(
|
|
9643
|
+
async (name, options) => streamMetricsCommand(dependencies(), name, options)
|
|
9644
|
+
);
|
|
9182
9645
|
operationOptions(
|
|
9183
9646
|
stream.command("usage").description("each stream's usage of one month at list price, before allowance and credit").option("--period <month>", "the calendar month in UTC, as YYYY-MM").option("--sort <order>", "list_value (highest first) or name", parseUsageSort).option("--prefix <prefix>", "only streams whose name starts with this").option("--names <names>", "only these streams, up to 100 names, comma-separated"),
|
|
9184
9647
|
"listStreamUsage"
|
|
@@ -9252,7 +9715,10 @@ function createProgram(makeDependencies = createDependencies) {
|
|
|
9252
9715
|
machineSpecOptions(
|
|
9253
9716
|
machine.command("update <name> [command...]").description(
|
|
9254
9717
|
"change a Machine; [command...] after -- replaces its command at the next boot"
|
|
9255
|
-
).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(
|
|
9718
|
+
).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(
|
|
9719
|
+
"--rebuild",
|
|
9720
|
+
"with --image, move a Machine whose Volume is its whole disk to the image by Rebuild, which deletes changes outside /home, /workspace and /root"
|
|
9721
|
+
).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(
|
|
9256
9722
|
"--database <name>",
|
|
9257
9723
|
"bind the account's database <name> of the Machine's region instead; applies at the next wake"
|
|
9258
9724
|
).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"),
|
|
@@ -9378,7 +9844,19 @@ function createProgram(makeDependencies = createDependencies) {
|
|
|
9378
9844
|
async (name, options) => machineWakeCommand(dependencies(), name, options)
|
|
9379
9845
|
);
|
|
9380
9846
|
operationOptions(
|
|
9381
|
-
machine.command("
|
|
9847
|
+
machine.command("rebuild <name>").description(
|
|
9848
|
+
"move a Machine whose Volume is its whole disk to an image; deletes changes outside /home, /workspace and /root"
|
|
9849
|
+
).option("--image <reference>", "the image to move to, named as for create").option("--yes", "rebuild without asking; required when stdin is not a terminal").option(
|
|
9850
|
+
"--timeout <seconds>",
|
|
9851
|
+
"how long to wait for an awake Machine to sleep, in seconds",
|
|
9852
|
+
parseWhole
|
|
9853
|
+
),
|
|
9854
|
+
"rebuildMachine"
|
|
9855
|
+
).action(
|
|
9856
|
+
async (name, options) => machineRebuildCommand(dependencies(), name, options)
|
|
9857
|
+
);
|
|
9858
|
+
operationOptions(
|
|
9859
|
+
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")).option("--keep-volume", "keep the Machine's Volume, even one made for it").option("--delete-volume", "delete the Machine's Volume with it, even one not made for it"),
|
|
9382
9860
|
"deleteMachine"
|
|
9383
9861
|
).action(
|
|
9384
9862
|
async (name, options) => machineDeleteCommand(dependencies(), name, options)
|
|
@@ -9472,6 +9950,24 @@ function createProgram(makeDependencies = createDependencies) {
|
|
|
9472
9950
|
).option("--granularity <step>", "with --from and --to, hour or day").option("--from <time>", "with --to, the window's start, on a UTC hour or day").option("--to <time>", "with --from, the window's end, exclusive"),
|
|
9473
9951
|
"getAccountMachineMetrics"
|
|
9474
9952
|
).action(async (options) => metricsMachinesCommand(dependencies(), options));
|
|
9953
|
+
operationOptions(
|
|
9954
|
+
metrics.command("databases").description(
|
|
9955
|
+
"show the account's databases' writes, reads, stored size and server latency over time"
|
|
9956
|
+
).option("--window <window>", "24h or 7d hourly, 30d or 90d daily; 7d when omitted").option(
|
|
9957
|
+
"--series <series>",
|
|
9958
|
+
"the series to print, comma-separated; every series when omitted"
|
|
9959
|
+
).option("--granularity <step>", "with --from and --to, hour or day").option("--from <time>", "with --to, the window's start, on a UTC hour or day").option("--to <time>", "with --from, the window's end, exclusive"),
|
|
9960
|
+
"getAccountDatabaseMetrics"
|
|
9961
|
+
).action(async (options) => metricsDatabasesCommand(dependencies(), options));
|
|
9962
|
+
operationOptions(
|
|
9963
|
+
metrics.command("streams").description(
|
|
9964
|
+
"show the account's streams' writes, reads, stored size and server latency over time"
|
|
9965
|
+
).option("--window <window>", "24h or 7d hourly, 30d or 90d daily; 7d when omitted").option(
|
|
9966
|
+
"--series <series>",
|
|
9967
|
+
"the series to print, comma-separated; every series when omitted"
|
|
9968
|
+
).option("--granularity <step>", "with --from and --to, hour or day").option("--from <time>", "with --to, the window's start, on a UTC hour or day").option("--to <time>", "with --from, the window's end, exclusive"),
|
|
9969
|
+
"getAccountStreamMetrics"
|
|
9970
|
+
).action(async (options) => metricsStreamsCommand(dependencies(), options));
|
|
9475
9971
|
const ssh = program.command("ssh").description("open a terminal on a Machine, or run a command there, over SSH").argument("<machine>", "the Machine's name").argument("[command...]", "a command to run instead of a shell, after --").action(async (name, command) => sshCommand(dependencies(), name, command));
|
|
9476
9972
|
ssh.command("config").description(
|
|
9477
9973
|
"print the ssh_config block that lets ssh, scp, sftp and Remote-SSH reach every Machine"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tonbo/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "The Tonbo CLI. Compute, disks, databases and streams for AI agents: your agent picks what it needs, sets it up, and knows what it costs, from the command line.",
|
|
5
5
|
"homepage": "https://tonbo.dev",
|
|
6
6
|
"bugs": {
|