@tonbo/cli 0.10.0 → 0.11.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 +8 -176
- package/dist/bin/tonbo.js +1457 -129
- package/package.json +2 -1
package/dist/bin/tonbo.js
CHANGED
|
@@ -372,6 +372,13 @@ var managementOperations = {
|
|
|
372
372
|
"idempotencyKey": false,
|
|
373
373
|
"cli": "tonbo member list"
|
|
374
374
|
},
|
|
375
|
+
"listMemberDevices": {
|
|
376
|
+
"method": "GET",
|
|
377
|
+
"path": "/members/devices",
|
|
378
|
+
"pathParams": [],
|
|
379
|
+
"query": [],
|
|
380
|
+
"idempotencyKey": false
|
|
381
|
+
},
|
|
375
382
|
"updateMember": {
|
|
376
383
|
"method": "PATCH",
|
|
377
384
|
"path": "/members/{subject}",
|
|
@@ -488,6 +495,36 @@ var managementOperations = {
|
|
|
488
495
|
"idempotencyKey": false,
|
|
489
496
|
"cli": "tonbo usage show"
|
|
490
497
|
},
|
|
498
|
+
"getUsageSeries": {
|
|
499
|
+
"method": "GET",
|
|
500
|
+
"path": "/usage/series",
|
|
501
|
+
"pathParams": [],
|
|
502
|
+
"query": [
|
|
503
|
+
"days",
|
|
504
|
+
"hours",
|
|
505
|
+
"month",
|
|
506
|
+
"compare"
|
|
507
|
+
],
|
|
508
|
+
"idempotencyKey": false,
|
|
509
|
+
"cli": "tonbo usage series"
|
|
510
|
+
},
|
|
511
|
+
"getUsageToday": {
|
|
512
|
+
"method": "GET",
|
|
513
|
+
"path": "/usage/today",
|
|
514
|
+
"pathParams": [],
|
|
515
|
+
"query": [],
|
|
516
|
+
"idempotencyKey": false,
|
|
517
|
+
"cli": "tonbo usage today"
|
|
518
|
+
},
|
|
519
|
+
"getSessionsStore": {
|
|
520
|
+
"method": "GET",
|
|
521
|
+
"path": "/sessions/store",
|
|
522
|
+
"pathParams": [],
|
|
523
|
+
"query": [
|
|
524
|
+
"capture"
|
|
525
|
+
],
|
|
526
|
+
"idempotencyKey": false
|
|
527
|
+
},
|
|
491
528
|
"listGitWorkspaces": {
|
|
492
529
|
"method": "GET",
|
|
493
530
|
"path": "/git/workspaces",
|
|
@@ -499,12 +536,242 @@ var managementOperations = {
|
|
|
499
536
|
"idempotencyKey": false,
|
|
500
537
|
"cli": "tonbo workspace list"
|
|
501
538
|
},
|
|
539
|
+
"listProjectBindings": {
|
|
540
|
+
"method": "GET",
|
|
541
|
+
"path": "/projects/bindings",
|
|
542
|
+
"pathParams": [],
|
|
543
|
+
"query": [
|
|
544
|
+
"device_id",
|
|
545
|
+
"cursor",
|
|
546
|
+
"limit"
|
|
547
|
+
],
|
|
548
|
+
"idempotencyKey": false
|
|
549
|
+
},
|
|
550
|
+
"connectProjectFolder": {
|
|
551
|
+
"method": "POST",
|
|
552
|
+
"path": "/projects/connect",
|
|
553
|
+
"pathParams": [],
|
|
554
|
+
"query": [],
|
|
555
|
+
"idempotencyKey": false
|
|
556
|
+
},
|
|
557
|
+
"disconnectProjectFolder": {
|
|
558
|
+
"method": "DELETE",
|
|
559
|
+
"path": "/projects/bindings/{binding_id}",
|
|
560
|
+
"pathParams": [
|
|
561
|
+
"binding_id"
|
|
562
|
+
],
|
|
563
|
+
"query": [],
|
|
564
|
+
"idempotencyKey": false
|
|
565
|
+
},
|
|
566
|
+
"latestPersonalHandoffResult": {
|
|
567
|
+
"method": "GET",
|
|
568
|
+
"path": "/yield/sessions/{sessionId}/latest-result",
|
|
569
|
+
"pathParams": [
|
|
570
|
+
"sessionId"
|
|
571
|
+
],
|
|
572
|
+
"query": [],
|
|
573
|
+
"idempotencyKey": false
|
|
574
|
+
},
|
|
575
|
+
"listHandoffs": {
|
|
576
|
+
"method": "GET",
|
|
577
|
+
"path": "/yield/handoffs",
|
|
578
|
+
"pathParams": [],
|
|
579
|
+
"query": [],
|
|
580
|
+
"idempotencyKey": false
|
|
581
|
+
},
|
|
582
|
+
"createPersonalHandoff": {
|
|
583
|
+
"method": "POST",
|
|
584
|
+
"path": "/yield/handoffs",
|
|
585
|
+
"pathParams": [],
|
|
586
|
+
"query": [],
|
|
587
|
+
"idempotencyKey": false
|
|
588
|
+
},
|
|
589
|
+
"listHandoffEvents": {
|
|
590
|
+
"method": "GET",
|
|
591
|
+
"path": "/yield/handoffs/events",
|
|
592
|
+
"pathParams": [],
|
|
593
|
+
"query": [
|
|
594
|
+
"after",
|
|
595
|
+
"wait"
|
|
596
|
+
],
|
|
597
|
+
"idempotencyKey": false
|
|
598
|
+
},
|
|
599
|
+
"listTeamHandoffs": {
|
|
600
|
+
"method": "GET",
|
|
601
|
+
"path": "/yield/handoffs/team",
|
|
602
|
+
"pathParams": [],
|
|
603
|
+
"query": [],
|
|
604
|
+
"idempotencyKey": false
|
|
605
|
+
},
|
|
606
|
+
"showPersonalHandoff": {
|
|
607
|
+
"method": "GET",
|
|
608
|
+
"path": "/yield/handoffs/{id}",
|
|
609
|
+
"pathParams": [
|
|
610
|
+
"id"
|
|
611
|
+
],
|
|
612
|
+
"query": [],
|
|
613
|
+
"idempotencyKey": false
|
|
614
|
+
},
|
|
615
|
+
"takePersonalHandoff": {
|
|
616
|
+
"method": "POST",
|
|
617
|
+
"path": "/yield/handoffs/{id}/take",
|
|
618
|
+
"pathParams": [
|
|
619
|
+
"id"
|
|
620
|
+
],
|
|
621
|
+
"query": [],
|
|
622
|
+
"idempotencyKey": false
|
|
623
|
+
},
|
|
624
|
+
"markPersonalHandoffRestored": {
|
|
625
|
+
"method": "POST",
|
|
626
|
+
"path": "/yield/handoffs/{id}/restored",
|
|
627
|
+
"pathParams": [
|
|
628
|
+
"id"
|
|
629
|
+
],
|
|
630
|
+
"query": [],
|
|
631
|
+
"idempotencyKey": false
|
|
632
|
+
},
|
|
633
|
+
"startPersonalHandoff": {
|
|
634
|
+
"method": "POST",
|
|
635
|
+
"path": "/yield/handoffs/{id}/started",
|
|
636
|
+
"pathParams": [
|
|
637
|
+
"id"
|
|
638
|
+
],
|
|
639
|
+
"query": [],
|
|
640
|
+
"idempotencyKey": false
|
|
641
|
+
},
|
|
642
|
+
"needHumanPersonalHandoff": {
|
|
643
|
+
"method": "POST",
|
|
644
|
+
"path": "/yield/handoffs/{id}/need-human",
|
|
645
|
+
"pathParams": [
|
|
646
|
+
"id"
|
|
647
|
+
],
|
|
648
|
+
"query": [],
|
|
649
|
+
"idempotencyKey": false
|
|
650
|
+
},
|
|
651
|
+
"cancelPersonalHandoff": {
|
|
652
|
+
"method": "POST",
|
|
653
|
+
"path": "/yield/handoffs/{id}/cancel",
|
|
654
|
+
"pathParams": [
|
|
655
|
+
"id"
|
|
656
|
+
],
|
|
657
|
+
"query": [],
|
|
658
|
+
"idempotencyKey": false
|
|
659
|
+
},
|
|
660
|
+
"renewPersonalHandoffSource": {
|
|
661
|
+
"method": "POST",
|
|
662
|
+
"path": "/yield/handoffs/{id}/renew-source",
|
|
663
|
+
"pathParams": [
|
|
664
|
+
"id"
|
|
665
|
+
],
|
|
666
|
+
"query": [],
|
|
667
|
+
"idempotencyKey": false
|
|
668
|
+
},
|
|
669
|
+
"finishPersonalHandoff": {
|
|
670
|
+
"method": "POST",
|
|
671
|
+
"path": "/yield/handoffs/{id}/done",
|
|
672
|
+
"pathParams": [
|
|
673
|
+
"id"
|
|
674
|
+
],
|
|
675
|
+
"query": [],
|
|
676
|
+
"idempotencyKey": false
|
|
677
|
+
},
|
|
678
|
+
"pullPersonalHandoff": {
|
|
679
|
+
"method": "GET",
|
|
680
|
+
"path": "/yield/handoffs/{id}/result",
|
|
681
|
+
"pathParams": [
|
|
682
|
+
"id"
|
|
683
|
+
],
|
|
684
|
+
"query": [],
|
|
685
|
+
"idempotencyKey": false
|
|
686
|
+
},
|
|
687
|
+
"markPersonalHandoffPulled": {
|
|
688
|
+
"method": "POST",
|
|
689
|
+
"path": "/yield/handoffs/{id}/pulled",
|
|
690
|
+
"pathParams": [
|
|
691
|
+
"id"
|
|
692
|
+
],
|
|
693
|
+
"query": [],
|
|
694
|
+
"idempotencyKey": false
|
|
695
|
+
},
|
|
696
|
+
"renewPersonalHandoffResult": {
|
|
697
|
+
"method": "POST",
|
|
698
|
+
"path": "/yield/handoffs/{id}/renew-result",
|
|
699
|
+
"pathParams": [
|
|
700
|
+
"id"
|
|
701
|
+
],
|
|
702
|
+
"query": [],
|
|
703
|
+
"idempotencyKey": false
|
|
704
|
+
},
|
|
705
|
+
"listYieldAccounts": {
|
|
706
|
+
"method": "GET",
|
|
707
|
+
"path": "/yield/accounts",
|
|
708
|
+
"pathParams": [],
|
|
709
|
+
"query": [],
|
|
710
|
+
"idempotencyKey": false
|
|
711
|
+
},
|
|
712
|
+
"createYieldOrganization": {
|
|
713
|
+
"method": "POST",
|
|
714
|
+
"path": "/yield/organizations",
|
|
715
|
+
"pathParams": [],
|
|
716
|
+
"query": [],
|
|
717
|
+
"idempotencyKey": false
|
|
718
|
+
},
|
|
719
|
+
"listYieldInvitations": {
|
|
720
|
+
"method": "GET",
|
|
721
|
+
"path": "/yield/invitations",
|
|
722
|
+
"pathParams": [],
|
|
723
|
+
"query": [],
|
|
724
|
+
"idempotencyKey": false
|
|
725
|
+
},
|
|
726
|
+
"createYieldInvitation": {
|
|
727
|
+
"method": "POST",
|
|
728
|
+
"path": "/yield/invitations",
|
|
729
|
+
"pathParams": [],
|
|
730
|
+
"query": [],
|
|
731
|
+
"idempotencyKey": false
|
|
732
|
+
},
|
|
733
|
+
"revokeYieldInvitation": {
|
|
734
|
+
"method": "DELETE",
|
|
735
|
+
"path": "/yield/invitations/{id}",
|
|
736
|
+
"pathParams": [
|
|
737
|
+
"id"
|
|
738
|
+
],
|
|
739
|
+
"query": [],
|
|
740
|
+
"idempotencyKey": false
|
|
741
|
+
},
|
|
742
|
+
"recordYieldMetric": {
|
|
743
|
+
"method": "POST",
|
|
744
|
+
"path": "/yield/metrics",
|
|
745
|
+
"pathParams": [],
|
|
746
|
+
"query": [],
|
|
747
|
+
"idempotencyKey": false
|
|
748
|
+
},
|
|
749
|
+
"readYieldTurnExcerpt": {
|
|
750
|
+
"method": "GET",
|
|
751
|
+
"path": "/yield/excerpts",
|
|
752
|
+
"pathParams": [],
|
|
753
|
+
"query": [
|
|
754
|
+
"author",
|
|
755
|
+
"session_id",
|
|
756
|
+
"turn",
|
|
757
|
+
"path"
|
|
758
|
+
],
|
|
759
|
+
"idempotencyKey": false
|
|
760
|
+
},
|
|
761
|
+
"publishYieldTurnExcerpt": {
|
|
762
|
+
"method": "POST",
|
|
763
|
+
"path": "/yield/excerpts",
|
|
764
|
+
"pathParams": [],
|
|
765
|
+
"query": [],
|
|
766
|
+
"idempotencyKey": false
|
|
767
|
+
},
|
|
502
768
|
"listDatabases": {
|
|
503
769
|
"method": "GET",
|
|
504
770
|
"path": "/databases",
|
|
505
771
|
"pathParams": [],
|
|
506
772
|
"query": [
|
|
507
773
|
"prefix",
|
|
774
|
+
"include",
|
|
508
775
|
"cursor",
|
|
509
776
|
"limit"
|
|
510
777
|
],
|
|
@@ -549,6 +816,20 @@ var managementOperations = {
|
|
|
549
816
|
"idempotencyKey": false,
|
|
550
817
|
"cli": "tonbo database delete"
|
|
551
818
|
},
|
|
819
|
+
"getDatabaseActivity": {
|
|
820
|
+
"method": "GET",
|
|
821
|
+
"path": "/databases/{name}/usage",
|
|
822
|
+
"pathParams": [
|
|
823
|
+
"name"
|
|
824
|
+
],
|
|
825
|
+
"query": [
|
|
826
|
+
"granularity",
|
|
827
|
+
"from",
|
|
828
|
+
"to"
|
|
829
|
+
],
|
|
830
|
+
"idempotencyKey": false,
|
|
831
|
+
"cli": "tonbo database activity"
|
|
832
|
+
},
|
|
552
833
|
"listDatabaseUsage": {
|
|
553
834
|
"method": "GET",
|
|
554
835
|
"path": "/usage/databases",
|
|
@@ -644,12 +925,49 @@ var managementOperations = {
|
|
|
644
925
|
"idempotencyKey": false,
|
|
645
926
|
"cli": "tonbo machine shapes"
|
|
646
927
|
},
|
|
928
|
+
"listVolumes": {
|
|
929
|
+
"method": "GET",
|
|
930
|
+
"path": "/volumes",
|
|
931
|
+
"pathParams": [],
|
|
932
|
+
"query": [],
|
|
933
|
+
"idempotencyKey": false,
|
|
934
|
+
"cli": "tonbo volume list"
|
|
935
|
+
},
|
|
936
|
+
"createVolume": {
|
|
937
|
+
"method": "POST",
|
|
938
|
+
"path": "/volumes",
|
|
939
|
+
"pathParams": [],
|
|
940
|
+
"query": [],
|
|
941
|
+
"idempotencyKey": true,
|
|
942
|
+
"cli": "tonbo volume create"
|
|
943
|
+
},
|
|
944
|
+
"getVolume": {
|
|
945
|
+
"method": "GET",
|
|
946
|
+
"path": "/volumes/{name}",
|
|
947
|
+
"pathParams": [
|
|
948
|
+
"name"
|
|
949
|
+
],
|
|
950
|
+
"query": [],
|
|
951
|
+
"idempotencyKey": false,
|
|
952
|
+
"cli": "tonbo volume show"
|
|
953
|
+
},
|
|
954
|
+
"deleteVolume": {
|
|
955
|
+
"method": "DELETE",
|
|
956
|
+
"path": "/volumes/{name}",
|
|
957
|
+
"pathParams": [
|
|
958
|
+
"name"
|
|
959
|
+
],
|
|
960
|
+
"query": [],
|
|
961
|
+
"idempotencyKey": false,
|
|
962
|
+
"cli": "tonbo volume delete"
|
|
963
|
+
},
|
|
647
964
|
"listStreams": {
|
|
648
965
|
"method": "GET",
|
|
649
966
|
"path": "/streams",
|
|
650
967
|
"pathParams": [],
|
|
651
968
|
"query": [
|
|
652
969
|
"prefix",
|
|
970
|
+
"include",
|
|
653
971
|
"cursor",
|
|
654
972
|
"limit"
|
|
655
973
|
],
|
|
@@ -694,6 +1012,20 @@ var managementOperations = {
|
|
|
694
1012
|
"idempotencyKey": false,
|
|
695
1013
|
"cli": "tonbo stream delete"
|
|
696
1014
|
},
|
|
1015
|
+
"getStreamActivity": {
|
|
1016
|
+
"method": "GET",
|
|
1017
|
+
"path": "/streams/{name}/usage",
|
|
1018
|
+
"pathParams": [
|
|
1019
|
+
"name"
|
|
1020
|
+
],
|
|
1021
|
+
"query": [
|
|
1022
|
+
"granularity",
|
|
1023
|
+
"from",
|
|
1024
|
+
"to"
|
|
1025
|
+
],
|
|
1026
|
+
"idempotencyKey": false,
|
|
1027
|
+
"cli": "tonbo stream activity"
|
|
1028
|
+
},
|
|
697
1029
|
"listStreamUsage": {
|
|
698
1030
|
"method": "GET",
|
|
699
1031
|
"path": "/usage/streams",
|
|
@@ -1196,6 +1528,12 @@ async function assertPrivateRegularFile(filename) {
|
|
|
1196
1528
|
var exec = promisify(execFile);
|
|
1197
1529
|
var DEFAULT_LOGIN_HINT = credentialsContract.reader.login_hint;
|
|
1198
1530
|
var LOOPBACK_HOST = "127.0.0.1";
|
|
1531
|
+
var BrowserUnavailableError = class extends Error {
|
|
1532
|
+
};
|
|
1533
|
+
function prefersDeviceLogin(env = process.env) {
|
|
1534
|
+
const ssh = Boolean(env.SSH_CONNECTION || env.SSH_CLIENT || env.SSH_TTY);
|
|
1535
|
+
return ssh && !env.DISPLAY && !env.WAYLAND_DISPLAY;
|
|
1536
|
+
}
|
|
1199
1537
|
function newLoginSecret() {
|
|
1200
1538
|
return randomBytes(32).toString("base64url");
|
|
1201
1539
|
}
|
|
@@ -1210,13 +1548,17 @@ var AuthClient = class {
|
|
|
1210
1548
|
browserOpener;
|
|
1211
1549
|
hostname;
|
|
1212
1550
|
loginHint;
|
|
1213
|
-
|
|
1551
|
+
entryLine;
|
|
1552
|
+
env;
|
|
1553
|
+
constructor(credentials, fetcher, loginOrigin2 = "https://tonbo.dev", browserOpener = openBrowser, hostname = loginHostname(), loginHint = DEFAULT_LOGIN_HINT, entryLine = "cli", env = process.env) {
|
|
1214
1554
|
this.credentials = credentials;
|
|
1215
1555
|
this.fetcher = fetcher;
|
|
1216
1556
|
this.loginOrigin = loginOrigin2;
|
|
1217
1557
|
this.browserOpener = browserOpener;
|
|
1218
1558
|
this.hostname = hostname;
|
|
1219
1559
|
this.loginHint = loginHint;
|
|
1560
|
+
this.entryLine = entryLine;
|
|
1561
|
+
this.env = env;
|
|
1220
1562
|
}
|
|
1221
1563
|
/** The personal key requests are made with: TONBO_API_KEY, or the saved
|
|
1222
1564
|
* login while it has not expired. */
|
|
@@ -1236,12 +1578,16 @@ var AuthClient = class {
|
|
|
1236
1578
|
* a free port, then the one-time code is redeemed, with this device's login
|
|
1237
1579
|
* secret, for the account's login key, which replaces this device's earlier
|
|
1238
1580
|
* login key there ("CLI on <hostname>" in a personal account, `Yield CLI ·
|
|
1239
|
-
* <8 hex>` in any other).
|
|
1240
|
-
*
|
|
1241
|
-
*
|
|
1581
|
+
* <8 hex>` in any other). With `device`, in an SSH session without a
|
|
1582
|
+
* display, or when no browser can be opened, the same PKCE challenge goes
|
|
1583
|
+
* to a device-code login instead: the person enters a short code at
|
|
1584
|
+
* /cli/device in any browser and the CLI polls for the key. The key joins
|
|
1585
|
+
* the file's `accounts`; it becomes the top level too unless `current` is
|
|
1586
|
+
* false (a login made only to publish into an organization). Answers that
|
|
1587
|
+
* account's login.
|
|
1242
1588
|
*/
|
|
1243
1589
|
async login(progress = () => {
|
|
1244
|
-
}, account, { current = true } = {}) {
|
|
1590
|
+
}, account, { current = true, device } = {}) {
|
|
1245
1591
|
const existing = await this.credentials.load().catch((error) => {
|
|
1246
1592
|
if (error instanceof UnusableLoginError)
|
|
1247
1593
|
return null;
|
|
@@ -1250,6 +1596,30 @@ var AuthClient = class {
|
|
|
1250
1596
|
const loginSecret = existing?.login_secret ?? newLoginSecret();
|
|
1251
1597
|
const verifier = randomBytes(48).toString("base64url");
|
|
1252
1598
|
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
|
1599
|
+
const redeemed = device ?? prefersDeviceLogin(this.env) ? await this.deviceLogin(challenge, verifier, loginSecret, account, progress) : await this.browserLogin(challenge, verifier, loginSecret, account, progress).catch((error) => {
|
|
1600
|
+
if (!(error instanceof BrowserUnavailableError))
|
|
1601
|
+
throw error;
|
|
1602
|
+
progress({ status: "failed", step: "browser", reason: error.message });
|
|
1603
|
+
return this.deviceLogin(challenge, verifier, loginSecret, account, progress);
|
|
1604
|
+
});
|
|
1605
|
+
const login = {
|
|
1606
|
+
account_id: redeemed.account_id,
|
|
1607
|
+
key_id: redeemed.api_key.id,
|
|
1608
|
+
key: redeemed.key,
|
|
1609
|
+
expires_at: redeemed.api_key.expires_at,
|
|
1610
|
+
kind: redeemed.account_kind ?? null,
|
|
1611
|
+
name: redeemed.account_name ?? null
|
|
1612
|
+
};
|
|
1613
|
+
progress({ status: "started", step: "credential-store" });
|
|
1614
|
+
const latest = await this.credentials.load().catch(() => existing);
|
|
1615
|
+
await this.credentials.save(withAccountLogin(latest, login, {
|
|
1616
|
+
current,
|
|
1617
|
+
loginSecret: latest?.login_secret ?? loginSecret
|
|
1618
|
+
}));
|
|
1619
|
+
progress({ status: "completed", step: "credential-store" });
|
|
1620
|
+
return login;
|
|
1621
|
+
}
|
|
1622
|
+
async browserLogin(challenge, verifier, loginSecret, account, progress) {
|
|
1253
1623
|
const state = randomBytes(24).toString("base64url");
|
|
1254
1624
|
const code = await this.listenForCode(state, (redirectUri) => {
|
|
1255
1625
|
const authorization = new URL("/cli/login", this.loginOrigin);
|
|
@@ -1259,6 +1629,7 @@ var AuthClient = class {
|
|
|
1259
1629
|
redirect_uri: redirectUri,
|
|
1260
1630
|
state,
|
|
1261
1631
|
hostname: this.hostname,
|
|
1632
|
+
from: this.entryLine,
|
|
1262
1633
|
...account ? { account } : {}
|
|
1263
1634
|
}).toString();
|
|
1264
1635
|
return authorization.toString();
|
|
@@ -1271,34 +1642,85 @@ var AuthClient = class {
|
|
|
1271
1642
|
body: JSON.stringify({ code, code_verifier: verifier, login_secret: loginSecret })
|
|
1272
1643
|
});
|
|
1273
1644
|
progress({ status: "completed", step: "key-redemption" });
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1645
|
+
return redeemed;
|
|
1646
|
+
}
|
|
1647
|
+
/**
|
|
1648
|
+
* The device-code login (RFC 8628's shape): the PKCE challenge and the
|
|
1649
|
+
* hostname buy a device code and a user code; the person approves the user
|
|
1650
|
+
* code at /cli/device, seeing the hostname; the CLI polls with the device
|
|
1651
|
+
* code, its verifier and the login secret, all in POST bodies, and the
|
|
1652
|
+
* approved poll answers the key once. A denied or expired code ends it.
|
|
1653
|
+
*/
|
|
1654
|
+
async deviceLogin(challenge, verifier, loginSecret, account, progress) {
|
|
1655
|
+
progress({ status: "started", step: "device-code" });
|
|
1656
|
+
const started = await requestJson(this.fetcher, new URL("/api/cli/login/device", this.loginOrigin).toString(), {
|
|
1657
|
+
method: "POST",
|
|
1658
|
+
headers: { "content-type": "application/json" },
|
|
1659
|
+
body: JSON.stringify({
|
|
1660
|
+
code_challenge: challenge,
|
|
1661
|
+
code_challenge_method: "S256",
|
|
1662
|
+
hostname: this.hostname
|
|
1663
|
+
})
|
|
1664
|
+
});
|
|
1665
|
+
progress({ status: "completed", step: "device-code" });
|
|
1666
|
+
const page = new URL("/cli/device", this.loginOrigin);
|
|
1667
|
+
const complete = new URL(page);
|
|
1668
|
+
complete.search = new URLSearchParams({
|
|
1669
|
+
code: started.user_code,
|
|
1670
|
+
from: this.entryLine,
|
|
1671
|
+
...account ? { account } : {}
|
|
1672
|
+
}).toString();
|
|
1673
|
+
progress({
|
|
1674
|
+
status: "device-code",
|
|
1675
|
+
step: "device-authorization",
|
|
1676
|
+
userCode: started.user_code,
|
|
1677
|
+
url: page.toString(),
|
|
1678
|
+
completeUrl: complete.toString(),
|
|
1679
|
+
expiresIn: started.expires_in
|
|
1680
|
+
});
|
|
1681
|
+
progress({ status: "started", step: "device-authorization" });
|
|
1682
|
+
const expired = new Error(`The login code expired. Run \`${this.loginHint}\` again.`);
|
|
1683
|
+
const deadline = Date.now() + started.expires_in * 1e3;
|
|
1684
|
+
let interval = started.interval;
|
|
1685
|
+
for (; ; ) {
|
|
1686
|
+
await new Promise((resolve) => setTimeout(resolve, interval * 1e3));
|
|
1687
|
+
try {
|
|
1688
|
+
const redeemed = await requestJson(this.fetcher, new URL("/api/cli/login/device/token", this.loginOrigin).toString(), {
|
|
1689
|
+
method: "POST",
|
|
1690
|
+
headers: { "content-type": "application/json" },
|
|
1691
|
+
body: JSON.stringify({
|
|
1692
|
+
device_code: started.device_code,
|
|
1693
|
+
code_verifier: verifier,
|
|
1694
|
+
login_secret: loginSecret
|
|
1695
|
+
})
|
|
1696
|
+
});
|
|
1697
|
+
progress({ status: "completed", step: "device-authorization" });
|
|
1698
|
+
return redeemed;
|
|
1699
|
+
} catch (error) {
|
|
1700
|
+
const code = error instanceof HttpError ? error.code : null;
|
|
1701
|
+
if (code === "slow_down")
|
|
1702
|
+
interval += 5;
|
|
1703
|
+
else if (code === "access_denied")
|
|
1704
|
+
throw new Error("The login was not approved (access_denied).");
|
|
1705
|
+
else if (code === "expired_token")
|
|
1706
|
+
throw expired;
|
|
1707
|
+
else if (code !== "authorization_pending" && !transient(error))
|
|
1708
|
+
throw error;
|
|
1709
|
+
if (Date.now() >= deadline)
|
|
1710
|
+
throw expired;
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1290
1713
|
}
|
|
1291
1714
|
listenForCode(expectedState, authorizationUrl, progress) {
|
|
1292
1715
|
return new Promise((resolve, reject) => {
|
|
1293
1716
|
let settled = false;
|
|
1294
1717
|
const sockets = /* @__PURE__ */ new Set();
|
|
1295
1718
|
let authorizationStarted = false;
|
|
1296
|
-
const startAuthorization = (
|
|
1719
|
+
const startAuthorization = () => {
|
|
1297
1720
|
if (authorizationStarted)
|
|
1298
1721
|
return;
|
|
1299
1722
|
authorizationStarted = true;
|
|
1300
|
-
|
|
1301
|
-
progress({ status: "completed", step: "browser" });
|
|
1723
|
+
progress({ status: "completed", step: "browser" });
|
|
1302
1724
|
progress({ status: "started", step: "browser-authorization" });
|
|
1303
1725
|
};
|
|
1304
1726
|
const finish = (result, completedResponseSocket) => {
|
|
@@ -1368,18 +1790,17 @@ var AuthClient = class {
|
|
|
1368
1790
|
void Promise.resolve().then(() => this.browserOpener(url)).then(() => startAuthorization(), (error) => {
|
|
1369
1791
|
if (settled || authorizationStarted)
|
|
1370
1792
|
return;
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
step: "browser",
|
|
1374
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
1375
|
-
callbackPort: port
|
|
1793
|
+
finish({
|
|
1794
|
+
error: new BrowserUnavailableError(error instanceof Error ? error.message : String(error))
|
|
1376
1795
|
});
|
|
1377
|
-
startAuthorization(false);
|
|
1378
1796
|
});
|
|
1379
1797
|
});
|
|
1380
1798
|
});
|
|
1381
1799
|
}
|
|
1382
1800
|
};
|
|
1801
|
+
function transient(error) {
|
|
1802
|
+
return error instanceof HttpError ? error.status === 429 || error.status >= 500 : error instanceof TypeError;
|
|
1803
|
+
}
|
|
1383
1804
|
function respond(response, outcome, finished) {
|
|
1384
1805
|
const page = callbackResponse(outcome);
|
|
1385
1806
|
if (finished)
|
|
@@ -1555,6 +1976,10 @@ var ManagementApi = class {
|
|
|
1555
1976
|
deleteDatabase(key, name) {
|
|
1556
1977
|
return this.call("deleteDatabase", key, { params: { name } });
|
|
1557
1978
|
}
|
|
1979
|
+
/** A database's writes per hour or day and its size at each step's end. */
|
|
1980
|
+
getDatabaseActivity(key, name, query = {}) {
|
|
1981
|
+
return this.call("getDatabaseActivity", key, { params: { name }, query });
|
|
1982
|
+
}
|
|
1558
1983
|
/** The whole usage page: its rows, `other`, prices and checkpoint. */
|
|
1559
1984
|
listDatabaseUsage(key, page, query = {}) {
|
|
1560
1985
|
return this.page("listDatabaseUsage", key, page, query);
|
|
@@ -1579,6 +2004,10 @@ var ManagementApi = class {
|
|
|
1579
2004
|
deleteStream(key, name) {
|
|
1580
2005
|
return this.call("deleteStream", key, { params: { name } });
|
|
1581
2006
|
}
|
|
2007
|
+
/** A stream's appends per hour or day and its size at each step's end. */
|
|
2008
|
+
getStreamActivity(key, name, query = {}) {
|
|
2009
|
+
return this.call("getStreamActivity", key, { params: { name }, query });
|
|
2010
|
+
}
|
|
1582
2011
|
/** The whole usage page: its rows, `other`, prices and checkpoint. */
|
|
1583
2012
|
listStreamUsage(key, page, query = {}) {
|
|
1584
2013
|
return this.page("listStreamUsage", key, page, query);
|
|
@@ -1586,6 +2015,13 @@ var ManagementApi = class {
|
|
|
1586
2015
|
getUsage(key, period) {
|
|
1587
2016
|
return this.call("getUsage", key, { query: { period } });
|
|
1588
2017
|
}
|
|
2018
|
+
getUsageSeries(key, query = {}) {
|
|
2019
|
+
return this.call("getUsageSeries", key, { query });
|
|
2020
|
+
}
|
|
2021
|
+
/** The account's writes of yesterday and today, hourly, per lane. */
|
|
2022
|
+
getUsageToday(key) {
|
|
2023
|
+
return this.call("getUsageToday", key);
|
|
2024
|
+
}
|
|
1589
2025
|
listMachines(key, page) {
|
|
1590
2026
|
return this.page("listMachines", key, page);
|
|
1591
2027
|
}
|
|
@@ -1715,6 +2151,22 @@ var ManagementApi = class {
|
|
|
1715
2151
|
listMachineShapes(key) {
|
|
1716
2152
|
return this.request("listMachineShapes", key, {});
|
|
1717
2153
|
}
|
|
2154
|
+
/** Every Volume of the account, and the regions that could not be read. */
|
|
2155
|
+
listVolumes(key) {
|
|
2156
|
+
return this.request("listVolumes", key, {});
|
|
2157
|
+
}
|
|
2158
|
+
/** A replay with the same idempotency key answers the Volume the first
|
|
2159
|
+
* request created, so an unconfirmed create names the key to retry with. */
|
|
2160
|
+
createVolume(key, request, idempotencyKey) {
|
|
2161
|
+
return this.call("createVolume", key, { body: request, idempotencyKey }).catch(unconfirmed("Volume creation", idempotencyKey, "the same name, region and size"));
|
|
2162
|
+
}
|
|
2163
|
+
getVolume(key, name) {
|
|
2164
|
+
return this.call("getVolume", key, { params: { name } });
|
|
2165
|
+
}
|
|
2166
|
+
/** The Volume being deleted; refused (409) while it is bound. */
|
|
2167
|
+
deleteVolume(key, name) {
|
|
2168
|
+
return this.call("deleteVolume", key, { params: { name } });
|
|
2169
|
+
}
|
|
1718
2170
|
};
|
|
1719
2171
|
var CREATE_ATTEMPTS = 3;
|
|
1720
2172
|
var CREATE_RETRY_MAX_WAIT_SECONDS = 30;
|
|
@@ -1775,7 +2227,7 @@ function when(timestamp, absent = "never") {
|
|
|
1775
2227
|
// ../../packages/tonbo-cli-core/dist/src/login.js
|
|
1776
2228
|
async function loginCommand(deps, options = {}) {
|
|
1777
2229
|
try {
|
|
1778
|
-
const login = await deps.auth.login((event) => reportLoginProgress(deps.progress, event), options.account, { current: options.current });
|
|
2230
|
+
const login = await deps.auth.login((event) => reportLoginProgress(deps.progress, event), options.account, { current: options.current, device: options.device });
|
|
1779
2231
|
deps.output(`Logged in to account ${login.name ?? login.account_id}.${login.expires_at ? ` The login expires ${login.expires_at}.` : ""}`);
|
|
1780
2232
|
return login;
|
|
1781
2233
|
} catch (error) {
|
|
@@ -1804,6 +2256,14 @@ var LOGIN_PROGRESS_MESSAGES = {
|
|
|
1804
2256
|
started: "Receiving the CLI key",
|
|
1805
2257
|
completed: "CLI key received"
|
|
1806
2258
|
},
|
|
2259
|
+
"device-code": {
|
|
2260
|
+
started: "Requesting a login code",
|
|
2261
|
+
completed: "Login code received"
|
|
2262
|
+
},
|
|
2263
|
+
"device-authorization": {
|
|
2264
|
+
started: "Waiting for approval in a browser",
|
|
2265
|
+
completed: "Login approved"
|
|
2266
|
+
},
|
|
1807
2267
|
"credential-store": {
|
|
1808
2268
|
started: "Saving login",
|
|
1809
2269
|
completed: "Login saved"
|
|
@@ -1815,9 +2275,13 @@ function reportLoginProgress(progress, event) {
|
|
|
1815
2275
|
return;
|
|
1816
2276
|
}
|
|
1817
2277
|
if (event.status === "failed") {
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
2278
|
+
progress.fail(`Could not open a browser: ${event.reason.replace(/\.$/, "")}. Approve with a code instead.`);
|
|
2279
|
+
return;
|
|
2280
|
+
}
|
|
2281
|
+
if (event.status === "device-code") {
|
|
2282
|
+
const minutes = Math.max(1, Math.round(event.expiresIn / 60));
|
|
2283
|
+
progress.note(`To approve this login, open ${event.url} in any browser and enter the code ${event.userCode}. It expires in ${minutes} minutes.`);
|
|
2284
|
+
progress.note(`Or open ${event.completeUrl}`);
|
|
1821
2285
|
return;
|
|
1822
2286
|
}
|
|
1823
2287
|
const messages = LOGIN_PROGRESS_MESSAGES[event.step];
|
|
@@ -2438,8 +2902,8 @@ async function databaseCreateCommand(deps, name, options = {}) {
|
|
|
2438
2902
|
}
|
|
2439
2903
|
function databaseNextStep(name) {
|
|
2440
2904
|
return [
|
|
2441
|
-
"Next: npm i --save-exact @tonbo/sqlite-ursula@0.11.
|
|
2442
|
-
`
|
|
2905
|
+
"Next: npm i --save-exact @tonbo/sdk@1.0.0 @tonbo/sqlite-ursula@0.11.4",
|
|
2906
|
+
` openDatabase(${JSON.stringify(name)}) from "@tonbo/sdk/database"`
|
|
2443
2907
|
].join("\n");
|
|
2444
2908
|
}
|
|
2445
2909
|
async function databaseShowCommand(deps, name, options = {}) {
|
|
@@ -2523,11 +2987,48 @@ async function databaseUsageCommand(deps, options = {}) {
|
|
|
2523
2987
|
);
|
|
2524
2988
|
printResourceUsage(deps, options.json, usage, "database");
|
|
2525
2989
|
}
|
|
2526
|
-
function
|
|
2527
|
-
return
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2990
|
+
function stepValue(value) {
|
|
2991
|
+
return value === null ? "" : String(value);
|
|
2992
|
+
}
|
|
2993
|
+
function printActivity(deps, json, activity, noun) {
|
|
2994
|
+
print(
|
|
2995
|
+
deps,
|
|
2996
|
+
json,
|
|
2997
|
+
activity,
|
|
2998
|
+
[
|
|
2999
|
+
`${noun} ${activity.name} in ${activity.region}, ${activity.granularity === "day" ? "daily" : "hourly"} from ${activity.from} to ${activity.to} (UTC; metered up to ${when(activity.as_of, "not yet metered")}; last write ${when(activity.last_write_at)})`,
|
|
3000
|
+
table(
|
|
3001
|
+
["START", "WRITE UNITS", "APPENDED", "RECORDS", "READS", "LIVE READS", "LOG", "SNAPSHOT"],
|
|
3002
|
+
activity.points.map((point) => [
|
|
3003
|
+
point.start,
|
|
3004
|
+
stepValue(point.write_units),
|
|
3005
|
+
point.append_bytes === null ? "" : bytes(point.append_bytes),
|
|
3006
|
+
stepValue(point.records),
|
|
3007
|
+
stepValue(point.reads),
|
|
3008
|
+
stepValue(point.live_reads),
|
|
3009
|
+
point.log_bytes === null ? "" : bytes(point.log_bytes),
|
|
3010
|
+
point.snapshot_bytes === null ? "" : bytes(point.snapshot_bytes)
|
|
3011
|
+
])
|
|
3012
|
+
)
|
|
3013
|
+
].join("\n")
|
|
3014
|
+
);
|
|
3015
|
+
}
|
|
3016
|
+
function activityQuery(options) {
|
|
3017
|
+
return { granularity: options.granularity, from: options.from, to: options.to };
|
|
3018
|
+
}
|
|
3019
|
+
async function databaseActivityCommand(deps, name, options = {}) {
|
|
3020
|
+
const activity = await deps.api.getDatabaseActivity(
|
|
3021
|
+
await deps.auth.apiKey(),
|
|
3022
|
+
name,
|
|
3023
|
+
activityQuery(options)
|
|
3024
|
+
);
|
|
3025
|
+
printActivity(deps, options.json, activity, "Database");
|
|
3026
|
+
}
|
|
3027
|
+
function streamFields(stream) {
|
|
3028
|
+
return fields([
|
|
3029
|
+
["Name", stream.name],
|
|
3030
|
+
["Region", stream.region],
|
|
3031
|
+
["State", stream.state],
|
|
2531
3032
|
["Content type", stream.content_type],
|
|
2532
3033
|
["Expires", stream.expires_at ?? "never"],
|
|
2533
3034
|
["Read access", readAccess(stream)],
|
|
@@ -2636,6 +3137,83 @@ async function streamUsageCommand(deps, options = {}) {
|
|
|
2636
3137
|
);
|
|
2637
3138
|
printResourceUsage(deps, options.json, usage, "stream");
|
|
2638
3139
|
}
|
|
3140
|
+
async function usageSeriesCommand(deps, options = {}) {
|
|
3141
|
+
const series = await deps.api.getUsageSeries(await deps.auth.apiKey(), {
|
|
3142
|
+
days: options.days,
|
|
3143
|
+
hours: options.hours,
|
|
3144
|
+
month: options.month,
|
|
3145
|
+
compare: options.compare ? "previous" : void 0
|
|
3146
|
+
});
|
|
3147
|
+
const at = (start) => series.granularity === "hour" ? start.slice(0, 16).replace("T", " ") : start.slice(0, 10);
|
|
3148
|
+
const month = series.month_to_date;
|
|
3149
|
+
const lines = [
|
|
3150
|
+
`Spend from ${at(series.start)} to ${at(series.end)} (UTC, ${series.currency.toUpperCase()})`,
|
|
3151
|
+
table(
|
|
3152
|
+
[
|
|
3153
|
+
series.granularity === "hour" ? "HOUR" : "DAY",
|
|
3154
|
+
"LIST",
|
|
3155
|
+
"ALLOWANCE",
|
|
3156
|
+
"CREDIT",
|
|
3157
|
+
"UNPAID",
|
|
3158
|
+
"CHARGED"
|
|
3159
|
+
],
|
|
3160
|
+
series.points.map((point) => [
|
|
3161
|
+
at(point.start),
|
|
3162
|
+
usd(point.list_micros),
|
|
3163
|
+
usd(point.allowance_micros),
|
|
3164
|
+
usd(point.credit_micros),
|
|
3165
|
+
usd(point.outstanding_micros),
|
|
3166
|
+
usd(point.charged_micros)
|
|
3167
|
+
])
|
|
3168
|
+
),
|
|
3169
|
+
`Total: ${usd(series.totals.list_micros)} at list price, ${usd(series.totals.charged_micros)} charged beyond the allowance`
|
|
3170
|
+
];
|
|
3171
|
+
if (series.previous) {
|
|
3172
|
+
const before = series.previous.totals.list_micros;
|
|
3173
|
+
const change = before ? ` (${series.totals.list_micros >= before ? "+" : ""}${Math.round((series.totals.list_micros - before) / before * 100)}%)` : "";
|
|
3174
|
+
lines.push(
|
|
3175
|
+
`Before: ${usd(before)} at list price from ${at(series.previous.start)} to ${at(series.previous.end)}${change}`
|
|
3176
|
+
);
|
|
3177
|
+
}
|
|
3178
|
+
lines.push(
|
|
3179
|
+
`${month.month} so far: ${usd(month.list_micros)} at list price, ${usd(month.allowance_micros)} of the ${usd(month.allowance_granted_micros)} allowance used, ${usd(month.charged_micros)} charged`
|
|
3180
|
+
);
|
|
3181
|
+
if (month.projection)
|
|
3182
|
+
lines.push(
|
|
3183
|
+
`At the rate of its ${month.projection.basis_days} complete days: ${usd(month.projection.list_micros)} at list price, ${usd(month.projection.charged_micros)} charged`
|
|
3184
|
+
);
|
|
3185
|
+
print(deps, options.json, series, lines.join("\n"));
|
|
3186
|
+
}
|
|
3187
|
+
async function streamActivityCommand(deps, name, options = {}) {
|
|
3188
|
+
const activity = await deps.api.getStreamActivity(
|
|
3189
|
+
await deps.auth.apiKey(),
|
|
3190
|
+
name,
|
|
3191
|
+
activityQuery(options)
|
|
3192
|
+
);
|
|
3193
|
+
printActivity(deps, options.json, activity, "Stream");
|
|
3194
|
+
}
|
|
3195
|
+
async function usageTodayCommand(deps, options = {}) {
|
|
3196
|
+
const today = await deps.api.getUsageToday(await deps.auth.apiKey());
|
|
3197
|
+
const { databases, streams, other } = today.series;
|
|
3198
|
+
print(
|
|
3199
|
+
deps,
|
|
3200
|
+
options.json,
|
|
3201
|
+
today,
|
|
3202
|
+
[
|
|
3203
|
+
`Write units per hour of yesterday and today (UTC; metered up to ${when(today.as_of, "not yet metered")})`,
|
|
3204
|
+
table(
|
|
3205
|
+
["HOUR", "DATABASES", "STREAMS", "OTHER"],
|
|
3206
|
+
databases.map((point, index) => [
|
|
3207
|
+
point.start,
|
|
3208
|
+
stepValue(point.write_units),
|
|
3209
|
+
stepValue(streams[index]?.write_units ?? null),
|
|
3210
|
+
stepValue(other[index]?.write_units ?? null)
|
|
3211
|
+
])
|
|
3212
|
+
)
|
|
3213
|
+
].join("\n")
|
|
3214
|
+
);
|
|
3215
|
+
noticeUnavailable(deps, today.unavailable_regions, "usage");
|
|
3216
|
+
}
|
|
2639
3217
|
async function usageShowCommand(deps, options = {}) {
|
|
2640
3218
|
const usage = await deps.api.getUsage(await deps.auth.apiKey(), options.period);
|
|
2641
3219
|
print(
|
|
@@ -2660,8 +3238,143 @@ async function usageShowCommand(deps, options = {}) {
|
|
|
2660
3238
|
);
|
|
2661
3239
|
}
|
|
2662
3240
|
|
|
3241
|
+
// src/help.ts
|
|
3242
|
+
import { Help } from "commander";
|
|
3243
|
+
|
|
2663
3244
|
// src/machine.ts
|
|
3245
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
3246
|
+
|
|
3247
|
+
// src/volume.ts
|
|
2664
3248
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
3249
|
+
var VOLUME_DURABILITY_LINK = "https://tonbo.dev/docs/machines/guarantees/data#volume";
|
|
3250
|
+
var VOLUME_ERROR_HINTS = {
|
|
3251
|
+
payment_required: "Add credit at <billing>, then create the Volume again.",
|
|
3252
|
+
region_not_offered: "Volumes are not offered to this account in that region yet; nothing was created. Name a region where they are offered.",
|
|
3253
|
+
region_unavailable: "A region cannot be reached right now; nothing was created or changed. Try again shortly."
|
|
3254
|
+
};
|
|
3255
|
+
async function explainVolumeError(deps, error) {
|
|
3256
|
+
if (!(error instanceof HttpError) || error.code === null) return error;
|
|
3257
|
+
const hint = VOLUME_ERROR_HINTS[error.code];
|
|
3258
|
+
if (!hint) return explainMachineError(error);
|
|
3259
|
+
const text = hint.includes("<billing>") ? hint.replaceAll("<billing>", await billingUrl(deps)) : hint;
|
|
3260
|
+
return new HttpError(`${error.message}
|
|
3261
|
+
${text}`, error.status, error.body, error.requestId);
|
|
3262
|
+
}
|
|
3263
|
+
async function volumeCommand(deps, run) {
|
|
3264
|
+
try {
|
|
3265
|
+
return await run();
|
|
3266
|
+
} catch (error) {
|
|
3267
|
+
throw await explainVolumeError(deps, error);
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
3270
|
+
function boundText(volume) {
|
|
3271
|
+
if (!volume.machine) return "none";
|
|
3272
|
+
return `${volume.machine.name}${volume.state === "attached" ? " (attached)" : ""}`;
|
|
3273
|
+
}
|
|
3274
|
+
function volumeFields(volume) {
|
|
3275
|
+
return fields([
|
|
3276
|
+
["Name", volume.name],
|
|
3277
|
+
["ID", volume.id],
|
|
3278
|
+
["Region", volume.region],
|
|
3279
|
+
["State", volume.state],
|
|
3280
|
+
["Machine", boundText(volume)],
|
|
3281
|
+
["Size", `${volume.size_gib} GiB`],
|
|
3282
|
+
["Live bytes", `${bytes(volume.live_bytes)} (billed)`],
|
|
3283
|
+
["Last commit", volume.last_commit_at ?? "none yet"],
|
|
3284
|
+
["Durability", VOLUME_DURABILITY_LINK],
|
|
3285
|
+
["Created", volume.created_at]
|
|
3286
|
+
]);
|
|
3287
|
+
}
|
|
3288
|
+
function parseSizeGib(value) {
|
|
3289
|
+
const match = /^([0-9]+)(?:\s*gi?b?)?$/i.exec(value.trim());
|
|
3290
|
+
if (!match) throw new Error(`--size ${value}: give a whole number of GiB, such as 20.`);
|
|
3291
|
+
return Number(match[1]);
|
|
3292
|
+
}
|
|
3293
|
+
async function volumeListCommand(deps, options = {}) {
|
|
3294
|
+
await volumeCommand(deps, async () => {
|
|
3295
|
+
const list = await deps.api.listVolumes(await deps.auth.apiKey());
|
|
3296
|
+
print(
|
|
3297
|
+
deps,
|
|
3298
|
+
options.json,
|
|
3299
|
+
list.data,
|
|
3300
|
+
list.data.length ? table(
|
|
3301
|
+
["NAME", "REGION", "STATE", "MACHINE", "SIZE", "LIVE BYTES"],
|
|
3302
|
+
list.data.map((volume) => [
|
|
3303
|
+
volume.name,
|
|
3304
|
+
volume.region,
|
|
3305
|
+
volume.state,
|
|
3306
|
+
volume.machine?.name ?? "-",
|
|
3307
|
+
`${volume.size_gib} GiB`,
|
|
3308
|
+
bytes(volume.live_bytes)
|
|
3309
|
+
])
|
|
3310
|
+
) : "No Volumes."
|
|
3311
|
+
);
|
|
3312
|
+
if (list.unavailable_regions.length)
|
|
3313
|
+
deps.notice(
|
|
3314
|
+
`Could not read the Volumes in ${list.unavailable_regions.join(", ")}; they are missing from this list.`
|
|
3315
|
+
);
|
|
3316
|
+
});
|
|
3317
|
+
}
|
|
3318
|
+
async function volumeShowCommand(deps, name, options = {}) {
|
|
3319
|
+
await volumeCommand(deps, async () => {
|
|
3320
|
+
const volume = await deps.api.getVolume(await deps.auth.apiKey(), name);
|
|
3321
|
+
print(deps, options.json, volume, volumeFields(volume));
|
|
3322
|
+
});
|
|
3323
|
+
}
|
|
3324
|
+
async function volumeCreateCommand(deps, name, options = {}) {
|
|
3325
|
+
await volumeCommand(deps, async () => {
|
|
3326
|
+
if (options.size === void 0) throw new Error("Pass --size <GiB>.");
|
|
3327
|
+
const sizeGib = parseSizeGib(options.size);
|
|
3328
|
+
const key = await deps.auth.apiKey();
|
|
3329
|
+
const region = options.region ?? (await deps.api.listRegions(key)).find((entry) => entry.default)?.id;
|
|
3330
|
+
if (!region) throw new Error("The account has no default region: pass --region.");
|
|
3331
|
+
const volume = await deps.api.createVolume(
|
|
3332
|
+
key,
|
|
3333
|
+
{ name, region, size_gib: sizeGib },
|
|
3334
|
+
options.idempotencyKey ?? randomUUID3()
|
|
3335
|
+
);
|
|
3336
|
+
print(
|
|
3337
|
+
deps,
|
|
3338
|
+
options.json,
|
|
3339
|
+
volume,
|
|
3340
|
+
[
|
|
3341
|
+
`Created Volume ${volume.name} in ${volume.region} (${volume.size_gib} GiB; billed by live bytes, not size).`,
|
|
3342
|
+
`Mount it on a Machine of ${volume.region} with \`tonbo machine update <machine> --volume ${volume.name}:/data\`; it attaches at the Machine's next boot.`,
|
|
3343
|
+
volumeFields(volume)
|
|
3344
|
+
].join("\n")
|
|
3345
|
+
);
|
|
3346
|
+
});
|
|
3347
|
+
}
|
|
3348
|
+
async function volumeDeleteCommand(deps, name, options = {}) {
|
|
3349
|
+
await volumeCommand(deps, async () => {
|
|
3350
|
+
const key = await deps.auth.apiKey();
|
|
3351
|
+
await confirm(
|
|
3352
|
+
deps,
|
|
3353
|
+
options.yes,
|
|
3354
|
+
`delete Volume ${name}`,
|
|
3355
|
+
() => Promise.resolve(
|
|
3356
|
+
`Delete Volume ${name} and every byte on it? There are no Volume snapshots or backups, so this cannot be undone. Type the Volume's name to confirm:`
|
|
3357
|
+
),
|
|
3358
|
+
name
|
|
3359
|
+
);
|
|
3360
|
+
const volume = await deps.api.deleteVolume(key, name);
|
|
3361
|
+
print(
|
|
3362
|
+
deps,
|
|
3363
|
+
options.json,
|
|
3364
|
+
volume,
|
|
3365
|
+
`Volume ${volume.name} is being deleted; its name is free for a new Volume.`
|
|
3366
|
+
);
|
|
3367
|
+
});
|
|
3368
|
+
}
|
|
3369
|
+
async function resolveVolumeBinding(deps, key, spec) {
|
|
3370
|
+
const split = spec.indexOf(":");
|
|
3371
|
+
if (split < 1 || split === spec.length - 1)
|
|
3372
|
+
throw new Error(`--volume ${spec}: give <name>:<path>, such as data:/data.`);
|
|
3373
|
+
const volume = await deps.api.getVolume(key, spec.slice(0, split));
|
|
3374
|
+
return { binding: { volume: volume.id, path: spec.slice(split + 1) }, region: volume.region };
|
|
3375
|
+
}
|
|
3376
|
+
|
|
3377
|
+
// src/machine.ts
|
|
2665
3378
|
var MACHINE_ERROR_HINTS = {
|
|
2666
3379
|
region_not_offered: "Machines are not offered in that region yet; nothing was created or changed. Name a region where they are offered.",
|
|
2667
3380
|
region_unavailable: "That region cannot be reached right now; nothing was created or changed. Try again shortly.",
|
|
@@ -2670,7 +3383,6 @@ var MACHINE_ERROR_HINTS = {
|
|
|
2670
3383
|
hostname_too_long: "<name>-<account> must fit one hostname label; choose a shorter name.",
|
|
2671
3384
|
account_name_unclaimed: "Claim a Tonbo ID for the account on the console's Profile page, then run the command again.",
|
|
2672
3385
|
image_platform_unsupported: "Machines run linux/arm64: publish the image for that platform (for example `docker buildx build --platform linux/arm64`).",
|
|
2673
|
-
image_not_offered: "Only the platform image runs until image pulls ship: create or update the Machine with `--image tonbo.dev/platform`.",
|
|
2674
3386
|
shape_not_offered: "`tonbo machine shapes` lists the shapes a Machine may name now.",
|
|
2675
3387
|
port_reserved: "Serve the application on another port.",
|
|
2676
3388
|
precondition_required: "The API needs the Machine's ETag for this change; `tonbo machine show` prints it, and the command sends the ETag it reads unless --if-match names one.",
|
|
@@ -2678,11 +3390,13 @@ var MACHINE_ERROR_HINTS = {
|
|
|
2678
3390
|
machine_failed: "A Machine whose creation failed does not change: `tonbo machine show` says why; delete it with `tonbo machine delete` and create it again.",
|
|
2679
3391
|
snapshot_would_be_discarded: "Pass --discard-snapshot to apply the change at the next boot, which then starts fresh.",
|
|
2680
3392
|
database_limit_reached: "Each Machine counts one database: delete a Machine or a database (`tonbo database list`) first.",
|
|
3393
|
+
machine_limit_reached: "The account holds as many Machines as it may: delete one you no longer need (`tonbo machine list`), or ask for more.",
|
|
2681
3394
|
idempotency_conflict: "That idempotency key belongs to another request: leave out --idempotency-key to create a new Machine.",
|
|
2682
3395
|
create_rate_limited: "Too many Machines were created in the last hour; try again later.",
|
|
2683
3396
|
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.",
|
|
2684
3397
|
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`.",
|
|
2685
|
-
machine_kind_mismatch: "That Machine is one of your computers: `tonbo machine show` and `tonbo machine delete` serve it, and nothing else does."
|
|
3398
|
+
machine_kind_mismatch: "That Machine is one of your computers: `tonbo machine show` and `tonbo machine delete` serve it, and nothing else does.",
|
|
3399
|
+
volumes_not_offered: "Volumes are not offered to this account in the Machine's region yet; nothing was created or changed."
|
|
2686
3400
|
};
|
|
2687
3401
|
var MACHINE_IMAGE_FAILURE_HINTS = {
|
|
2688
3402
|
image_unavailable: "The registry no longer serves that image publicly, or served other bytes for it: publish it again, or use another image",
|
|
@@ -2691,9 +3405,9 @@ var MACHINE_IMAGE_FAILURE_HINTS = {
|
|
|
2691
3405
|
image_unsupported: "Rebuild the image without the entry the message names"
|
|
2692
3406
|
};
|
|
2693
3407
|
var MACHINE_CREATION_FAILURE_HINTS = {
|
|
2694
|
-
database_creation_failed: "The Machine's database could not be created; the message names the stream and the code. If the code is payment_required, add credit
|
|
3408
|
+
database_creation_failed: "The Machine's database could not be created; the message names the stream and the code. If the code is payment_required, add credit at <billing> first; then delete the Machine and create it again.",
|
|
2695
3409
|
database_limit_reached: "Each Machine counts one database: delete a Machine or a database (`tonbo database list`), then delete this Machine and create it again.",
|
|
2696
|
-
payment_required: "The account's credit is exhausted: add credit
|
|
3410
|
+
payment_required: "The account's credit is exhausted: add credit at <billing>, then delete the Machine and create it again.",
|
|
2697
3411
|
...Object.fromEntries(
|
|
2698
3412
|
Object.entries(MACHINE_IMAGE_FAILURE_HINTS).map(([code, hint]) => [
|
|
2699
3413
|
code,
|
|
@@ -2701,10 +3415,10 @@ var MACHINE_CREATION_FAILURE_HINTS = {
|
|
|
2701
3415
|
])
|
|
2702
3416
|
)
|
|
2703
3417
|
};
|
|
2704
|
-
function creationFailureText(machine) {
|
|
3418
|
+
function creationFailureText(machine, billing) {
|
|
2705
3419
|
const { failure } = machine;
|
|
2706
3420
|
if (!failure) return { reason: "", hint: "" };
|
|
2707
|
-
const hint = MACHINE_CREATION_FAILURE_HINTS[failure.code];
|
|
3421
|
+
const hint = MACHINE_CREATION_FAILURE_HINTS[failure.code]?.replaceAll("<billing>", billing);
|
|
2708
3422
|
return { reason: ` (${failure.code}: ${failure.message})`, hint: hint ? `
|
|
2709
3423
|
${hint}` : "" };
|
|
2710
3424
|
}
|
|
@@ -2719,6 +3433,18 @@ function explainMachineError(error) {
|
|
|
2719
3433
|
return new HttpError(`${error.message}
|
|
2720
3434
|
${hint}`, error.status, error.body, error.requestId);
|
|
2721
3435
|
}
|
|
3436
|
+
var BILLING_URL = "https://tonbo.dev/{account}/billing";
|
|
3437
|
+
var BILLING_FALLBACK_URL = "https://tonbo.dev/billing";
|
|
3438
|
+
async function billingUrl(deps) {
|
|
3439
|
+
if (process.env.TONBO_API_KEY?.trim()) return BILLING_FALLBACK_URL;
|
|
3440
|
+
try {
|
|
3441
|
+
const login = await deps.credentials.load();
|
|
3442
|
+
const name = login ? accountLogins(login).find((entry) => entry.account_id === login.account_id)?.name : null;
|
|
3443
|
+
return name ? BILLING_URL.replace("{account}", encodeURIComponent(name)) : BILLING_FALLBACK_URL;
|
|
3444
|
+
} catch {
|
|
3445
|
+
return BILLING_FALLBACK_URL;
|
|
3446
|
+
}
|
|
3447
|
+
}
|
|
2722
3448
|
async function machineCommand(run) {
|
|
2723
3449
|
try {
|
|
2724
3450
|
return await run();
|
|
@@ -2774,18 +3500,18 @@ function activationFields(machine) {
|
|
|
2774
3500
|
return fields2;
|
|
2775
3501
|
}
|
|
2776
3502
|
var MACHINE_WAKE_BLOCKED_HINTS = {
|
|
2777
|
-
payment_required: "The account's credit does not cover a wake: add credit
|
|
2778
|
-
standing: "The account's Streams standing is not solvent: settle the account
|
|
3503
|
+
payment_required: "The account's credit does not cover a wake: add credit at <billing>, and the next request wakes it.",
|
|
3504
|
+
standing: "The account's Streams standing is not solvent: settle the account at <billing>, and the next request wakes it.",
|
|
2779
3505
|
meter_lag: "Tonbo could not confirm the account's usage in time; the next request tries again.",
|
|
2780
3506
|
crash_backoff: "Its recent wakes failed; see `tonbo machine activations <name>`.",
|
|
2781
3507
|
capacity: "No host had room for its shape; the next request tries again.",
|
|
2782
3508
|
region_closed: "Machines are closed in its region for now; the next request after they reopen wakes it."
|
|
2783
3509
|
};
|
|
2784
|
-
function conditionText(name, condition) {
|
|
3510
|
+
function conditionText(name, condition, billing) {
|
|
2785
3511
|
switch (condition.type) {
|
|
2786
3512
|
case "wake_blocked": {
|
|
2787
3513
|
const retry = condition.retry_at ? `, retry after ${condition.retry_at}` : "";
|
|
2788
|
-
const hint = MACHINE_WAKE_BLOCKED_HINTS[condition.reason]?.replaceAll("<name>", name);
|
|
3514
|
+
const hint = MACHINE_WAKE_BLOCKED_HINTS[condition.reason]?.replaceAll("<name>", name).replaceAll("<billing>", billing);
|
|
2789
3515
|
return `wake blocked (${condition.reason.replaceAll("_", " ")}) since ${condition.since}${retry}.${hint ? ` ${hint}` : ""}`;
|
|
2790
3516
|
}
|
|
2791
3517
|
case "creation_delayed":
|
|
@@ -2794,14 +3520,16 @@ function conditionText(name, condition) {
|
|
|
2794
3520
|
return `its host is unreachable since ${condition.since}`;
|
|
2795
3521
|
case "egress_blocked":
|
|
2796
3522
|
return `outbound access blocked for the ${condition.scope} since ${condition.since}${condition.until ? ` until ${condition.until}` : ""}`;
|
|
3523
|
+
case "volume_purged":
|
|
3524
|
+
return `its Volume ${condition.volume} was purged (${condition.reason.replaceAll("_", " ")}) since ${condition.since}; its data is gone and it boots without it. Bind another with \`tonbo machine update ${name} --volume <volume>:<path>\`, or none with \`--no-volume\`.`;
|
|
2797
3525
|
default:
|
|
2798
3526
|
return JSON.stringify(condition);
|
|
2799
3527
|
}
|
|
2800
3528
|
}
|
|
2801
|
-
function conditionFields(machine) {
|
|
3529
|
+
function conditionFields(machine, billing) {
|
|
2802
3530
|
return (machine.status.conditions ?? []).map((condition) => [
|
|
2803
3531
|
"Condition",
|
|
2804
|
-
conditionText(machine.name, condition)
|
|
3532
|
+
conditionText(machine.name, condition, billing)
|
|
2805
3533
|
]);
|
|
2806
3534
|
}
|
|
2807
3535
|
function endText(end) {
|
|
@@ -2841,7 +3569,7 @@ function imageFailureFields(machine) {
|
|
|
2841
3569
|
...next ? [["Next step", next]] : []
|
|
2842
3570
|
];
|
|
2843
3571
|
}
|
|
2844
|
-
function machineFields(machine, etag = null, lastEnd = null) {
|
|
3572
|
+
function machineFields(machine, billing, etag = null, lastEnd = null) {
|
|
2845
3573
|
return fields([
|
|
2846
3574
|
["Name", machine.name],
|
|
2847
3575
|
["ID", machine.id],
|
|
@@ -2850,7 +3578,7 @@ function machineFields(machine, etag = null, lastEnd = null) {
|
|
|
2850
3578
|
["Power", powerText(machine)],
|
|
2851
3579
|
...activationFields(machine),
|
|
2852
3580
|
...lastEnd ? [["Last end", endText(lastEnd)]] : [],
|
|
2853
|
-
...conditionFields(machine),
|
|
3581
|
+
...conditionFields(machine, billing),
|
|
2854
3582
|
["Sleeps", sleepText(machine)],
|
|
2855
3583
|
["Shape", machine.shape],
|
|
2856
3584
|
["Image", imageText(machine)],
|
|
@@ -2863,6 +3591,8 @@ function machineFields(machine, etag = null, lastEnd = null) {
|
|
|
2863
3591
|
["SSH", `${machine.hostnames.ssh} (not served yet)`],
|
|
2864
3592
|
["Env", Object.keys(machine.env).sort().join(", ") || "none"],
|
|
2865
3593
|
["Stop grace", `${machine.stop_grace_seconds} s`],
|
|
3594
|
+
["Volume", volumeText(machine)],
|
|
3595
|
+
...deleteFields(machine),
|
|
2866
3596
|
["Database", `${machine.database.name} (${machine.database.url})`],
|
|
2867
3597
|
["Logs", machine.logs.url],
|
|
2868
3598
|
["Durability", DURABILITY_LINK],
|
|
@@ -2873,8 +3603,23 @@ function machineFields(machine, etag = null, lastEnd = null) {
|
|
|
2873
3603
|
["Updated", machine.updated_at]
|
|
2874
3604
|
]);
|
|
2875
3605
|
}
|
|
2876
|
-
function
|
|
2877
|
-
|
|
3606
|
+
function deleteFields(machine) {
|
|
3607
|
+
const seconds = machine.delete_after_seconds;
|
|
3608
|
+
if (seconds === void 0 || seconds === null) return [];
|
|
3609
|
+
const at = machine.status.delete_at;
|
|
3610
|
+
return [
|
|
3611
|
+
[
|
|
3612
|
+
"Deletes",
|
|
3613
|
+
at ? `at ${at}, after ${seconds} s asleep` : `${seconds} s after this activation ends`
|
|
3614
|
+
]
|
|
3615
|
+
];
|
|
3616
|
+
}
|
|
3617
|
+
function volumeText(machine) {
|
|
3618
|
+
const binding = machine.volumes?.[0];
|
|
3619
|
+
return binding ? `${binding.volume} at ${binding.path}` : "none";
|
|
3620
|
+
}
|
|
3621
|
+
function machineText(machine, billing, etag = null, lastEnd = null) {
|
|
3622
|
+
return [machineFields(machine, billing, etag, lastEnd), ...privatePortLines(machine)].join("\n");
|
|
2878
3623
|
}
|
|
2879
3624
|
function keyStateText(machine) {
|
|
2880
3625
|
const state = machine.byoc.key_state;
|
|
@@ -2899,8 +3644,8 @@ function deviceFields(machine) {
|
|
|
2899
3644
|
["Updated", machine.updated_at]
|
|
2900
3645
|
]);
|
|
2901
3646
|
}
|
|
2902
|
-
function anyMachineText(machine, etag = null, lastEnd = null) {
|
|
2903
|
-
return machine.kind === "byoc" ? deviceFields(machine) : machineText(machine, etag, lastEnd);
|
|
3647
|
+
function anyMachineText(machine, billing, etag = null, lastEnd = null) {
|
|
3648
|
+
return machine.kind === "byoc" ? deviceFields(machine) : machineText(machine, billing, etag, lastEnd);
|
|
2904
3649
|
}
|
|
2905
3650
|
function parsePorts(specs) {
|
|
2906
3651
|
const accesses = machinePortAccess;
|
|
@@ -2969,6 +3714,10 @@ async function waitForMachine(deps, key, name, firstWait, every, timeout, settle
|
|
|
2969
3714
|
wait = every;
|
|
2970
3715
|
}
|
|
2971
3716
|
}
|
|
3717
|
+
function deleteAfterSeconds(option) {
|
|
3718
|
+
if (typeof option === "number") return option;
|
|
3719
|
+
return option === false ? null : void 0;
|
|
3720
|
+
}
|
|
2972
3721
|
async function machineListCommand(deps, options = {}) {
|
|
2973
3722
|
await machineCommand(async () => {
|
|
2974
3723
|
const page = await deps.api.listMachines(await deps.auth.apiKey(), {
|
|
@@ -3064,9 +3813,10 @@ async function waitForCreate(deps, key, machine, name, firstWait, timeout, json)
|
|
|
3064
3813
|
timeout,
|
|
3065
3814
|
(read) => read.state !== "creating"
|
|
3066
3815
|
);
|
|
3816
|
+
const billing = await billingUrl(deps);
|
|
3067
3817
|
if (!settled) {
|
|
3068
3818
|
const known = last ?? machine;
|
|
3069
|
-
print(deps, json, known, machineFields(known));
|
|
3819
|
+
print(deps, json, known, machineFields(known, billing));
|
|
3070
3820
|
throw new Error(
|
|
3071
3821
|
`Machine ${machine.name} is still creating after ${waited} s; nothing was undone. \`tonbo machine show ${machine.name}\` reports it active when it is ready.`
|
|
3072
3822
|
);
|
|
@@ -3079,14 +3829,14 @@ async function waitForCreate(deps, key, machine, name, firstWait, timeout, json)
|
|
|
3079
3829
|
settled,
|
|
3080
3830
|
[
|
|
3081
3831
|
`Created ${settled.name} in ${settled.region} (active after ${waited} s${pulled}).`,
|
|
3082
|
-
machineText(settled)
|
|
3832
|
+
machineText(settled, billing)
|
|
3083
3833
|
].join("\n")
|
|
3084
3834
|
);
|
|
3085
3835
|
return;
|
|
3086
3836
|
}
|
|
3087
|
-
print(deps, json, settled, machineFields(settled));
|
|
3837
|
+
print(deps, json, settled, machineFields(settled, billing));
|
|
3088
3838
|
if (settled.state === "failed") {
|
|
3089
|
-
const { reason, hint } = creationFailureText(settled);
|
|
3839
|
+
const { reason, hint } = creationFailureText(settled, billing);
|
|
3090
3840
|
throw new Error(
|
|
3091
3841
|
`Machine ${settled.name} failed to be created${reason}; delete it with \`tonbo machine delete ${settled.name}\` and create it again.${hint}`
|
|
3092
3842
|
);
|
|
@@ -3100,6 +3850,8 @@ async function machineCreateCommand(deps, name, command, options = {}) {
|
|
|
3100
3850
|
const env = parseEnv(options.env ?? []);
|
|
3101
3851
|
const ports = withDefault(parsePorts(options.port ?? []), options.defaultPort);
|
|
3102
3852
|
const key = await deps.auth.apiKey();
|
|
3853
|
+
const bound = typeof options.volume === "string" ? await resolveVolumeBinding(deps, key, options.volume) : null;
|
|
3854
|
+
const region = options.region ?? bound?.region;
|
|
3103
3855
|
const shape = options.shape ?? (await deps.api.listMachineShapes(key)).default;
|
|
3104
3856
|
let created;
|
|
3105
3857
|
try {
|
|
@@ -3109,15 +3861,17 @@ async function machineCreateCommand(deps, name, command, options = {}) {
|
|
|
3109
3861
|
name,
|
|
3110
3862
|
image: options.image,
|
|
3111
3863
|
shape,
|
|
3112
|
-
region
|
|
3864
|
+
region,
|
|
3113
3865
|
command: command.length ? command : void 0,
|
|
3114
3866
|
ports: ports.length ? ports : void 0,
|
|
3115
3867
|
env: Object.keys(env).length ? env : void 0,
|
|
3116
3868
|
sleep_after_seconds: options.sleepAfter,
|
|
3117
3869
|
idle_action: options.idleAction,
|
|
3118
|
-
stop_grace_seconds: options.stopGrace
|
|
3870
|
+
stop_grace_seconds: options.stopGrace,
|
|
3871
|
+
...bound ? { volumes: [bound.binding] } : {},
|
|
3872
|
+
delete_after_seconds: deleteAfterSeconds(options.deleteAfter)
|
|
3119
3873
|
},
|
|
3120
|
-
options.idempotencyKey ??
|
|
3874
|
+
options.idempotencyKey ?? randomUUID4()
|
|
3121
3875
|
);
|
|
3122
3876
|
} catch (error) {
|
|
3123
3877
|
if (error instanceof HttpError && error.code === "name_taken")
|
|
@@ -3127,8 +3881,9 @@ async function machineCreateCommand(deps, name, command, options = {}) {
|
|
|
3127
3881
|
const { machine } = created;
|
|
3128
3882
|
specWarnings(deps, machine.ports, Object.keys(env));
|
|
3129
3883
|
if (machine.state === "failed") {
|
|
3130
|
-
|
|
3131
|
-
|
|
3884
|
+
const billing = await billingUrl(deps);
|
|
3885
|
+
print(deps, options.json, machine, machineFields(machine, billing));
|
|
3886
|
+
const { reason, hint } = creationFailureText(machine, billing);
|
|
3132
3887
|
throw new Error(
|
|
3133
3888
|
`Machine ${machine.name} failed to be created${reason}; delete it with \`tonbo machine delete ${machine.name}\` and create it again.${hint}`
|
|
3134
3889
|
);
|
|
@@ -3143,7 +3898,12 @@ async function machineCreateCommand(deps, name, command, options = {}) {
|
|
|
3143
3898
|
timeout,
|
|
3144
3899
|
options.json
|
|
3145
3900
|
);
|
|
3146
|
-
print(
|
|
3901
|
+
print(
|
|
3902
|
+
deps,
|
|
3903
|
+
options.json,
|
|
3904
|
+
machine,
|
|
3905
|
+
[createdText(machine), machineText(machine, await billingUrl(deps))].join("\n")
|
|
3906
|
+
);
|
|
3147
3907
|
});
|
|
3148
3908
|
}
|
|
3149
3909
|
async function machineShowCommand(deps, name, options = {}) {
|
|
@@ -3151,7 +3911,12 @@ async function machineShowCommand(deps, name, options = {}) {
|
|
|
3151
3911
|
const key = await deps.auth.apiKey();
|
|
3152
3912
|
const { machine, etag } = await deps.api.getMachineWithEtag(key, name);
|
|
3153
3913
|
const lastEnd = options.json || machine.kind === "byoc" || machine.state !== "active" ? null : await lastActivationEnd(deps, key, machine.name);
|
|
3154
|
-
print(
|
|
3914
|
+
print(
|
|
3915
|
+
deps,
|
|
3916
|
+
options.json,
|
|
3917
|
+
machine,
|
|
3918
|
+
anyMachineText(machine, await billingUrl(deps), etag, lastEnd)
|
|
3919
|
+
);
|
|
3155
3920
|
});
|
|
3156
3921
|
}
|
|
3157
3922
|
async function lastActivationEnd(deps, key, name) {
|
|
@@ -3194,7 +3959,11 @@ function appliesText(name, change) {
|
|
|
3194
3959
|
const lines = [];
|
|
3195
3960
|
if (change.ports !== void 0 || change.sleep_after_seconds !== void 0)
|
|
3196
3961
|
lines.push("Ports and sleep-after apply now.");
|
|
3197
|
-
|
|
3962
|
+
if (change.delete_after_seconds !== void 0)
|
|
3963
|
+
lines.push(
|
|
3964
|
+
change.delete_after_seconds === null ? "The Machine is no longer deleted for being asleep." : "Delete-after applies now; `tonbo machine show` says when."
|
|
3965
|
+
);
|
|
3966
|
+
const later = [change.image, change.command, change.shape, change.env, change.volumes].some(
|
|
3198
3967
|
(field) => field !== void 0
|
|
3199
3968
|
);
|
|
3200
3969
|
if (later) {
|
|
@@ -3226,7 +3995,8 @@ async function machineUpdateCommand(deps, name, command, options = {}) {
|
|
|
3226
3995
|
command: options.imageCommand ? null : command.length ? command : void 0,
|
|
3227
3996
|
sleep_after_seconds: options.sleepAfter,
|
|
3228
3997
|
idle_action: options.idleAction,
|
|
3229
|
-
stop_grace_seconds: options.stopGrace
|
|
3998
|
+
stop_grace_seconds: options.stopGrace,
|
|
3999
|
+
delete_after_seconds: deleteAfterSeconds(options.deleteAfter)
|
|
3230
4000
|
};
|
|
3231
4001
|
if (options.clearPorts) change.ports = [];
|
|
3232
4002
|
else if (options.port?.length) {
|
|
@@ -3242,6 +4012,9 @@ async function machineUpdateCommand(deps, name, command, options = {}) {
|
|
|
3242
4012
|
for (const variable of options.unsetEnv ?? []) delete env[variable];
|
|
3243
4013
|
change.env = env;
|
|
3244
4014
|
}
|
|
4015
|
+
if (options.volume === false) change.volumes = [];
|
|
4016
|
+
else if (typeof options.volume === "string")
|
|
4017
|
+
change.volumes = [(await resolveVolumeBinding(deps, key, options.volume)).binding];
|
|
3245
4018
|
if (options.discardSnapshot) change.discard_snapshot = true;
|
|
3246
4019
|
if (Object.values(change).every((value) => value === void 0))
|
|
3247
4020
|
throw new Error("Nothing to change: pass at least one option or a command.");
|
|
@@ -3254,7 +4027,7 @@ async function machineUpdateCommand(deps, name, command, options = {}) {
|
|
|
3254
4027
|
[
|
|
3255
4028
|
`Updated Machine ${machine.name} to revision ${machine.revision}.`,
|
|
3256
4029
|
...appliesText(machine.name, change),
|
|
3257
|
-
machineText(machine)
|
|
4030
|
+
machineText(machine, await billingUrl(deps))
|
|
3258
4031
|
].join("\n")
|
|
3259
4032
|
);
|
|
3260
4033
|
});
|
|
@@ -3374,6 +4147,530 @@ async function machineShapesCommand(deps, options = {}) {
|
|
|
3374
4147
|
});
|
|
3375
4148
|
}
|
|
3376
4149
|
|
|
4150
|
+
// src/help.ts
|
|
4151
|
+
var ENVIRONMENT = [
|
|
4152
|
+
{
|
|
4153
|
+
names: ["TONBO_API_KEY"],
|
|
4154
|
+
description: "A personal key (`tbo_\u2026`) every command uses instead of the saved login, for non-interactive use such as CI. `tonbo logout` acts only on the saved login."
|
|
4155
|
+
},
|
|
4156
|
+
{
|
|
4157
|
+
names: ["TONBO_API_ORIGIN"],
|
|
4158
|
+
description: "The Management API origin, `https://api.tonbo.dev` when unset. It must be https, or http on a loopback address. `tonbo login` opens the login page on the same origin without its `api.` label."
|
|
4159
|
+
},
|
|
4160
|
+
{
|
|
4161
|
+
names: ["XDG_CONFIG_HOME"],
|
|
4162
|
+
description: "Where the login file lives: `$XDG_CONFIG_HOME/tonbo/credentials.json` when it is set and not empty, else `$HOME/.config/tonbo/credentials.json`."
|
|
4163
|
+
},
|
|
4164
|
+
{
|
|
4165
|
+
names: ["SSH_CONNECTION", "SSH_CLIENT", "SSH_TTY"],
|
|
4166
|
+
description: "Any of them marks an SSH session: without `DISPLAY` or `WAYLAND_DISPLAY` too, `tonbo login` approves with a device code instead of opening a browser here."
|
|
4167
|
+
},
|
|
4168
|
+
{
|
|
4169
|
+
names: ["DISPLAY", "WAYLAND_DISPLAY"],
|
|
4170
|
+
description: "Outside macOS and Windows, `tonbo login` opens the browser with `xdg-open`, which needs a graphical session; with neither set it approves with a device code."
|
|
4171
|
+
}
|
|
4172
|
+
];
|
|
4173
|
+
var TOPICS = [
|
|
4174
|
+
{
|
|
4175
|
+
id: "output",
|
|
4176
|
+
title: "Output and --json",
|
|
4177
|
+
text: [
|
|
4178
|
+
"Every command prints an aligned table or a list of fields for people. With `--json` it prints the answer's `data` exactly as the API returned it: one JSON value on stdout and nothing else there. Notices, next steps and warnings go to stderr. A command that prints the whole answer instead, because the fields beside `data` belong with the rows, says so."
|
|
4179
|
+
]
|
|
4180
|
+
},
|
|
4181
|
+
{
|
|
4182
|
+
id: "pagination",
|
|
4183
|
+
title: "Lists and pages",
|
|
4184
|
+
text: [
|
|
4185
|
+
"A list prints one page. When there is another, the CLI writes `More results: run again with --cursor <cursor>` to stderr, so `--json` output stays the API's `data`; run the same command with that `--cursor` for the next page. `--limit` asks for up to that many items, 1 to 200; the API answers 50 when it is not given, and a page may hold fewer."
|
|
4186
|
+
]
|
|
4187
|
+
},
|
|
4188
|
+
{
|
|
4189
|
+
id: "exit-status",
|
|
4190
|
+
title: "Exit status",
|
|
4191
|
+
text: [
|
|
4192
|
+
"0 when the command did what it says, and for `--help` and `--version`.",
|
|
4193
|
+
"1 on any failure: the API refused the request, an argument or flag is wrong, a destructive command was not confirmed, or a `--wait` ended without the state it waited for (a failure, or its `--timeout`). A refusal prints the problem's detail, the fields a validation error names and the request's `X-Request-Id` to stderr; an invalid or expired key adds `Run tonbo login`."
|
|
4194
|
+
]
|
|
4195
|
+
},
|
|
4196
|
+
{
|
|
4197
|
+
id: "credentials",
|
|
4198
|
+
title: "Keys and the login file",
|
|
4199
|
+
text: [
|
|
4200
|
+
"Every command but `login` and `logout` sends a personal key as its Bearer credential: `TONBO_API_KEY` when it is set, else the key `tonbo login` saved. What a command may do is what that key's member may do in the account, as narrowed by the key; the CLI never decides it.",
|
|
4201
|
+
"The login is saved at `${XDG_CONFIG_HOME:-$HOME/.config}/tonbo/credentials.json`, which `yield` shares: either CLI's `login` signs both in, and either `logout` signs both out. The CLI replaces the file atomically with owner-only permissions and refuses a symlink or a file other users can read. A file an earlier release wrote is never migrated; run `tonbo login`."
|
|
4202
|
+
]
|
|
4203
|
+
},
|
|
4204
|
+
{
|
|
4205
|
+
id: "retries",
|
|
4206
|
+
title: "Retried creations",
|
|
4207
|
+
text: [
|
|
4208
|
+
"`agent create`, `bucket create`, `database create`, `stream create`, `machine create` and `volume create` send an `Idempotency-Key`: a new UUID unless `--idempotency-key` names one. When a creation is not confirmed (the network failed or the server answered 5xx), the CLI prints its key; run the command again with that key and the same arguments, and it returns what the first request created instead of creating a second one. A key reused with other arguments is refused with `idempotency_conflict`.",
|
|
4209
|
+
"`api-key create` takes no idempotency key, because a key's secret is never stored and a replay could not return it; a live key's name is unique, so a blind retry is refused with 409 rather than creating a second key. Every other command reads, or sets a resource to the state it names, so running it again does no more than the first run did; a repeated delete or revoke may be refused because the first one already removed it."
|
|
4210
|
+
]
|
|
4211
|
+
},
|
|
4212
|
+
{
|
|
4213
|
+
id: "confirmations",
|
|
4214
|
+
title: "Destructive commands",
|
|
4215
|
+
text: [
|
|
4216
|
+
"`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."
|
|
4217
|
+
]
|
|
4218
|
+
}
|
|
4219
|
+
];
|
|
4220
|
+
var LIMIT_DEFAULT = "50, the API's default";
|
|
4221
|
+
var PERIOD_DEFAULT = "the current month";
|
|
4222
|
+
var DATA_REGION_DEFAULT = "the region `tonbo region list` marks as the database default";
|
|
4223
|
+
var ACTIVITY_DEFAULTS = {
|
|
4224
|
+
"--granularity": "hour",
|
|
4225
|
+
"--from": "24 hours (or 30 days for `day`) before --to",
|
|
4226
|
+
"--to": "the end of the current hour (or day)"
|
|
4227
|
+
};
|
|
4228
|
+
var USAGE_DEFAULTS = {
|
|
4229
|
+
"--period": PERIOD_DEFAULT,
|
|
4230
|
+
"--sort": "list_value",
|
|
4231
|
+
"--limit": LIMIT_DEFAULT
|
|
4232
|
+
};
|
|
4233
|
+
var EXPIRY_DEFAULTS = { "--expires-at": "never", "--expires-in": "never" };
|
|
4234
|
+
var NAME = (noun) => ({ name: `the ${noun}'s name` });
|
|
4235
|
+
var COMMAND_HELP = {
|
|
4236
|
+
"tonbo login": {
|
|
4237
|
+
about: [
|
|
4238
|
+
"Prints the login URL and opens the browser at `https://tonbo.dev/cli/login`, where you pick the account and approve a personal API key named `CLI on <hostname>`. The browser returns a one-time code to a local callback on `127.0.0.1`, and the CLI redeems it with its PKCE verifier for the key.",
|
|
4239
|
+
"The key acts as you, with your role in that account. It expires after six months and replaces the key an earlier login on the same computer created; there is no refresh, so an expired or revoked key asks you to run `tonbo login` again. `tonbo` acts in the account of the last login: log in again to switch. The login file keeps one key per account of this computer beside it, for `yield`; a key in an account other than your personal one is named `Yield CLI \xB7 <8 hex>` instead of after the host.",
|
|
4240
|
+
"When the browser is on another computer, the login uses a device code instead: with `--device`, in an SSH session without a display, or when no browser can be opened here. The CLI prints a short code and `https://tonbo.dev/cli/device`; open it in any browser where you are signed in, enter the code, check the computer name it shows, pick the account and approve. The CLI polls and saves the same key. The code works once and expires after ten minutes."
|
|
4241
|
+
],
|
|
4242
|
+
examples: ["tonbo login", "tonbo login --device"],
|
|
4243
|
+
defaults: { "--account": "your personal account" }
|
|
4244
|
+
},
|
|
4245
|
+
"tonbo logout": {
|
|
4246
|
+
about: [
|
|
4247
|
+
"Revokes every account's key in the login file, each with its own credential, and removes the file, which signs `yield` out too. A key that is already revoked or expired is skipped. If another revocation fails, the file is still removed, and the CLI prints the key's id and exits 1, so you can revoke it under API keys in the console. `TONBO_API_KEY` is never touched."
|
|
4248
|
+
],
|
|
4249
|
+
examples: ["tonbo logout"]
|
|
4250
|
+
},
|
|
4251
|
+
"tonbo account show": {
|
|
4252
|
+
about: [
|
|
4253
|
+
"Prints the account you are logged in to: its id, name and kind, your member subject and role in it, and its limits."
|
|
4254
|
+
],
|
|
4255
|
+
examples: ["tonbo account show --json"]
|
|
4256
|
+
},
|
|
4257
|
+
"tonbo region list": {
|
|
4258
|
+
about: [
|
|
4259
|
+
"Shows each region new resources can be created in, with its data-plane origin, and marks the default region for new databases and streams."
|
|
4260
|
+
],
|
|
4261
|
+
examples: ["tonbo region list"]
|
|
4262
|
+
},
|
|
4263
|
+
"tonbo workspace list": {
|
|
4264
|
+
about: [
|
|
4265
|
+
"Lists the account's git workspaces, the default first: each one's id, name, whether it is the default, its region and where it is served (`<git origin>/<ws_hex>`, the prefix of every repository URL in it). Every account has a default workspace from its creation. Listing needs only a role on the account and admits nothing; every git token is still admitted when it is minted."
|
|
4266
|
+
],
|
|
4267
|
+
examples: ["tonbo workspace list"],
|
|
4268
|
+
defaults: { "--limit": LIMIT_DEFAULT }
|
|
4269
|
+
},
|
|
4270
|
+
"tonbo member list": {
|
|
4271
|
+
about: ["Lists the account's people and Agents with their kind, subject, name and role."],
|
|
4272
|
+
examples: ["tonbo member list --limit 20"],
|
|
4273
|
+
defaults: { "--limit": LIMIT_DEFAULT }
|
|
4274
|
+
},
|
|
4275
|
+
"tonbo member update": {
|
|
4276
|
+
about: [
|
|
4277
|
+
"Gives a member another role (`owner`, `admin` or `member`; only an owner grants `owner`), renames an Agent with `--name`, or both at once. Give at least one of them.",
|
|
4278
|
+
"`<member>` is a member subject: a person's user id, or `agent.<agent id>` for an Agent. An Agent's bare id or its name works too: the CLI looks it up in `tonbo member list` and uses it only when exactly one member matches."
|
|
4279
|
+
],
|
|
4280
|
+
examples: ["tonbo member update pi-live --role admin"],
|
|
4281
|
+
arguments: { member: "a member subject, or an Agent's id or name" }
|
|
4282
|
+
},
|
|
4283
|
+
"tonbo agent create": {
|
|
4284
|
+
about: [
|
|
4285
|
+
"Creates an Agent identity with a role. Every Agent has a home region, chosen when it is created and never changed. Without `--region`, Tonbo uses the only region that accepts new Agents; when several do, an interactive `agent create` asks which one, and a request without `--region` from a script, or with `--json`, is refused.",
|
|
4286
|
+
"Create a key for the Agent with `tonbo api-key create --member <name>`."
|
|
4287
|
+
],
|
|
4288
|
+
examples: ["tonbo agent create my-agent"],
|
|
4289
|
+
arguments: NAME("Agent"),
|
|
4290
|
+
defaults: {
|
|
4291
|
+
"--role": "member",
|
|
4292
|
+
"--region": "the only region that accepts new Agents",
|
|
4293
|
+
"--idempotency-key": "a new UUID"
|
|
4294
|
+
}
|
|
4295
|
+
},
|
|
4296
|
+
"tonbo api-key list": {
|
|
4297
|
+
about: [
|
|
4298
|
+
"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."
|
|
4299
|
+
],
|
|
4300
|
+
examples: ["tonbo api-key list"],
|
|
4301
|
+
defaults: { "--limit": LIMIT_DEFAULT }
|
|
4302
|
+
},
|
|
4303
|
+
"tonbo api-key create": {
|
|
4304
|
+
about: [
|
|
4305
|
+
"Creates a personal key for you, or with `--member` a key of an Agent (its subject, id or name; owners and admins only). `--read-only`, and either `--bucket` or any of `--database`, `--database-prefix`, `--stream` and `--stream-prefix` (repeat each for several), narrow the key for its whole life; a key narrowed to databases and streams reaches both, and `api-key list` and `api-key show` print what each key reaches.",
|
|
4306
|
+
"The secret is printed once, in this answer only, with a warning on stderr: store it then, because no command can show it again. Without `--json` the answer also carries a ready-to-paste `TONBO_API_KEY=<secret>` line for your application's environment, and stderr notes that `TONBO_API_KEY` in your own shell overrides your `tonbo login`, for this CLI too. stderr never carries the secret."
|
|
4307
|
+
],
|
|
4308
|
+
examples: [
|
|
4309
|
+
"tonbo api-key create ci-reader --member pi-live --read-only --database-prefix run- --expires-in 6_months"
|
|
4310
|
+
],
|
|
4311
|
+
arguments: { name: "the key's name, unique among the account's live keys" },
|
|
4312
|
+
defaults: { "--member": "yourself", "--expires-in": "never" }
|
|
4313
|
+
},
|
|
4314
|
+
"tonbo api-key show": {
|
|
4315
|
+
about: ["Prints one key's fields, as `api-key list` does; never its secret."],
|
|
4316
|
+
examples: ["tonbo api-key show 70000000-0000-4000-8000-000000000007"],
|
|
4317
|
+
arguments: { "key-id": "the key's id, as `tonbo api-key list` prints it" }
|
|
4318
|
+
},
|
|
4319
|
+
"tonbo api-key revoke": {
|
|
4320
|
+
about: [
|
|
4321
|
+
"Revokes the key and every token minted from it. At a terminal it asks you to type `yes` first."
|
|
4322
|
+
],
|
|
4323
|
+
examples: ["tonbo api-key revoke 70000000-0000-4000-8000-000000000007 --yes"],
|
|
4324
|
+
arguments: { "key-id": "the key's id, as `tonbo api-key list` prints it" }
|
|
4325
|
+
},
|
|
4326
|
+
"tonbo bucket list": {
|
|
4327
|
+
about: [
|
|
4328
|
+
"Prints each bucket's region, state, read access and URL; a stream in it lives at `{url}/{stream}`."
|
|
4329
|
+
],
|
|
4330
|
+
examples: ["tonbo bucket list"],
|
|
4331
|
+
defaults: { "--limit": LIMIT_DEFAULT }
|
|
4332
|
+
},
|
|
4333
|
+
"tonbo bucket create": {
|
|
4334
|
+
about: ["Creates a bucket in `--region` and prints its fields with its URL."],
|
|
4335
|
+
examples: ["tonbo bucket create events"],
|
|
4336
|
+
arguments: NAME("bucket"),
|
|
4337
|
+
defaults: { "--region": "the account's default region", "--idempotency-key": "a new UUID" }
|
|
4338
|
+
},
|
|
4339
|
+
"tonbo bucket show": {
|
|
4340
|
+
about: ["Prints the bucket's region, state, read access and URL."],
|
|
4341
|
+
examples: ["tonbo bucket show events"],
|
|
4342
|
+
arguments: NAME("bucket")
|
|
4343
|
+
},
|
|
4344
|
+
"tonbo bucket update": {
|
|
4345
|
+
about: [
|
|
4346
|
+
"Publishes the bucket for anonymous reads with `--public`, or takes it private with `--private`."
|
|
4347
|
+
],
|
|
4348
|
+
examples: ["tonbo bucket update events --public"],
|
|
4349
|
+
arguments: NAME("bucket")
|
|
4350
|
+
},
|
|
4351
|
+
"tonbo bucket delete": {
|
|
4352
|
+
about: [
|
|
4353
|
+
"Deletes the bucket and every stream in it; the bucket's name is never used again. At a terminal it asks you to type the bucket's name first."
|
|
4354
|
+
],
|
|
4355
|
+
examples: ["tonbo bucket delete events --yes"],
|
|
4356
|
+
arguments: NAME("bucket")
|
|
4357
|
+
},
|
|
4358
|
+
"tonbo database list": {
|
|
4359
|
+
about: [
|
|
4360
|
+
"Lists the account's databases in every region with their kind, region, state, read access, last write, stored size and URL. A region that cannot be read is named on stderr, and its sizes show as unknown."
|
|
4361
|
+
],
|
|
4362
|
+
examples: ["tonbo database list --prefix run-"],
|
|
4363
|
+
defaults: { "--limit": LIMIT_DEFAULT }
|
|
4364
|
+
},
|
|
4365
|
+
"tonbo database create": {
|
|
4366
|
+
about: [
|
|
4367
|
+
'Creates a database in `--region` and prints its fields with its URL. Without `--json` it then writes the next step to stderr: the packages to install and `openDatabase("<name>")` from `@tonbo/sdk/database`, which opens the database by name with your key.',
|
|
4368
|
+
"`--expires-at` (an RFC 3339 time, at least 60 seconds after the creation) or `--expires-in` (`90s` or more, such as `30m`, `24h` or `7d`, counted from now) gives the database an expiry fixed for its life: once it passes, Tonbo deletes the database within a minute, as `database delete` would, and its name is free again. `--expires-in` is counted from when the command runs, so it never goes with `--idempotency-key`: an unconfirmed creation with an expiry prints its key and the absolute time it sent, and the retry passes that time with `--expires-at`. A Machine's database never expires."
|
|
4369
|
+
],
|
|
4370
|
+
examples: ["tonbo database create scratch --expires-in 24h"],
|
|
4371
|
+
arguments: { name: "the database's name, unique among the account's databases" },
|
|
4372
|
+
defaults: {
|
|
4373
|
+
"--region": DATA_REGION_DEFAULT,
|
|
4374
|
+
...EXPIRY_DEFAULTS,
|
|
4375
|
+
"--idempotency-key": "a new UUID"
|
|
4376
|
+
}
|
|
4377
|
+
},
|
|
4378
|
+
"tonbo database show": {
|
|
4379
|
+
about: [
|
|
4380
|
+
"Prints the database's kind, region, state, read access, URL, expiry, incarnation, last write, stored size, and when and by whom it was created."
|
|
4381
|
+
],
|
|
4382
|
+
examples: ["tonbo database show notes"],
|
|
4383
|
+
arguments: NAME("database")
|
|
4384
|
+
},
|
|
4385
|
+
"tonbo database update": {
|
|
4386
|
+
about: [
|
|
4387
|
+
"Publishes the database for anonymous reads with `--public`, or takes it private with `--private`. Owners and admins only."
|
|
4388
|
+
],
|
|
4389
|
+
examples: ["tonbo database update notes --public"],
|
|
4390
|
+
arguments: NAME("database")
|
|
4391
|
+
},
|
|
4392
|
+
"tonbo database delete": {
|
|
4393
|
+
about: [
|
|
4394
|
+
"Deletes the database and everything in it; its name is free again. At a terminal it asks you to type the database's name first."
|
|
4395
|
+
],
|
|
4396
|
+
examples: ["tonbo database delete scratch --yes"],
|
|
4397
|
+
arguments: NAME("database")
|
|
4398
|
+
},
|
|
4399
|
+
"tonbo database activity": {
|
|
4400
|
+
about: [
|
|
4401
|
+
"Prints one row per UTC hour or day of the window: the write units, bytes appended, records, successful reads and live (long-poll or SSE) reads of that step, and the log and snapshot size at its end. A step that has not started, or a size before the meter's first sample, is empty. The window is at most 31 days."
|
|
4402
|
+
],
|
|
4403
|
+
examples: ["tonbo database activity notes --granularity day"],
|
|
4404
|
+
arguments: NAME("database"),
|
|
4405
|
+
defaults: ACTIVITY_DEFAULTS
|
|
4406
|
+
},
|
|
4407
|
+
"tonbo database usage": {
|
|
4408
|
+
about: [
|
|
4409
|
+
"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`."
|
|
4410
|
+
],
|
|
4411
|
+
examples: ["tonbo database usage --period 2026-10 --sort name"],
|
|
4412
|
+
defaults: USAGE_DEFAULTS,
|
|
4413
|
+
json: "answer"
|
|
4414
|
+
},
|
|
4415
|
+
"tonbo stream list": {
|
|
4416
|
+
about: [
|
|
4417
|
+
"Lists the account's raw durable streams in every region with their region, state, content type, read access, last write, stored size and URL. A region that cannot be read is named on stderr."
|
|
4418
|
+
],
|
|
4419
|
+
examples: ["tonbo stream list --prefix run-"],
|
|
4420
|
+
defaults: { "--limit": LIMIT_DEFAULT }
|
|
4421
|
+
},
|
|
4422
|
+
"tonbo stream create": {
|
|
4423
|
+
about: [
|
|
4424
|
+
"Creates a raw durable stream in `--region` with the content type every append must send (one lowercase `type/subtype` without parameters), and prints its fields with its URL on the region's Streams origin. Without `--json` it then writes the next step to stderr: append with `POST` to the URL and read with `GET` from `?offset=-1`, with any Durable Streams client.",
|
|
4425
|
+
"A stream and a database may share a name: each kind has its own names, limit and usage. `--expires-at` and `--expires-in` give the stream an expiry fixed for its life, by the same rules as `database create`'s: once it passes, Tonbo deletes the stream within a minute and its name is free again. A key narrowed to streams (`api-key create --stream` or `--stream-prefix`) reaches only those streams."
|
|
4426
|
+
],
|
|
4427
|
+
examples: ["tonbo stream create events --content-type application/json"],
|
|
4428
|
+
arguments: { name: "the stream's name, unique among the account's streams" },
|
|
4429
|
+
defaults: {
|
|
4430
|
+
"--region": DATA_REGION_DEFAULT,
|
|
4431
|
+
"--content-type": "application/octet-stream",
|
|
4432
|
+
...EXPIRY_DEFAULTS,
|
|
4433
|
+
"--idempotency-key": "a new UUID"
|
|
4434
|
+
}
|
|
4435
|
+
},
|
|
4436
|
+
"tonbo stream show": {
|
|
4437
|
+
about: [
|
|
4438
|
+
"Prints the stream's region, state, content type, expiry, read access, URL, incarnation, last write, stored size, and when and by whom it was created."
|
|
4439
|
+
],
|
|
4440
|
+
examples: ["tonbo stream show events"],
|
|
4441
|
+
arguments: NAME("stream")
|
|
4442
|
+
},
|
|
4443
|
+
"tonbo stream update": {
|
|
4444
|
+
about: [
|
|
4445
|
+
"Publishes the stream for anonymous reads with `--public`, or takes it private with `--private`. Owners and admins only."
|
|
4446
|
+
],
|
|
4447
|
+
examples: ["tonbo stream update events --private"],
|
|
4448
|
+
arguments: NAME("stream")
|
|
4449
|
+
},
|
|
4450
|
+
"tonbo stream delete": {
|
|
4451
|
+
about: [
|
|
4452
|
+
"Deletes the stream and everything in it; its name is free again. At a terminal it asks you to type the stream's name first."
|
|
4453
|
+
],
|
|
4454
|
+
examples: ["tonbo stream delete run-42 --yes"],
|
|
4455
|
+
arguments: NAME("stream")
|
|
4456
|
+
},
|
|
4457
|
+
"tonbo stream activity": {
|
|
4458
|
+
about: [
|
|
4459
|
+
"Prints one row per UTC hour or day of the window: the write units, bytes appended, records, successful reads and live (long-poll or SSE) reads of that step, and the log and snapshot size at its end. A step that has not started, or a size before the meter's first sample, is empty. The window is at most 31 days."
|
|
4460
|
+
],
|
|
4461
|
+
examples: ["tonbo stream activity events --from 2026-11-01T00:00:00Z"],
|
|
4462
|
+
arguments: NAME("stream"),
|
|
4463
|
+
defaults: ACTIVITY_DEFAULTS
|
|
4464
|
+
},
|
|
4465
|
+
"tonbo stream usage": {
|
|
4466
|
+
about: [
|
|
4467
|
+
"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`."
|
|
4468
|
+
],
|
|
4469
|
+
examples: ["tonbo stream usage --names events,run-42"],
|
|
4470
|
+
defaults: USAGE_DEFAULTS,
|
|
4471
|
+
json: "answer"
|
|
4472
|
+
},
|
|
4473
|
+
"tonbo machine list": {
|
|
4474
|
+
about: [
|
|
4475
|
+
"Lists the account's Machines in every region with their region, state, power, shape, image, HTTPS hostname and conditions. In a personal account it also lists your own computers, which `yield login` registers, in a second table with their system, agent harnesses, when they last reported and the state of their login key. An organization lists Machines only."
|
|
4476
|
+
],
|
|
4477
|
+
examples: ["tonbo machine list"],
|
|
4478
|
+
defaults: { "--limit": LIMIT_DEFAULT }
|
|
4479
|
+
},
|
|
4480
|
+
"tonbo machine create": {
|
|
4481
|
+
about: [
|
|
4482
|
+
"Creates a Machine, a VM booted from an image in one region with its own database. 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`.",
|
|
4483
|
+
"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.",
|
|
4484
|
+
"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."
|
|
4485
|
+
],
|
|
4486
|
+
examples: ["tonbo machine create web --image nginx:1.27 --port 80:public --wait"],
|
|
4487
|
+
arguments: {
|
|
4488
|
+
name: "the Machine's name, part of its hostname",
|
|
4489
|
+
command: "the command that replaces the image's CMD, after `--`"
|
|
4490
|
+
},
|
|
4491
|
+
defaults: {
|
|
4492
|
+
"--shape": "the shape `tonbo machine shapes` marks as the default",
|
|
4493
|
+
"--default-port": "the first --port",
|
|
4494
|
+
"--region": "the --volume's region, else the account's default region",
|
|
4495
|
+
"--idempotency-key": "a new UUID",
|
|
4496
|
+
"--timeout": String(DEFAULT_WAIT_SECONDS),
|
|
4497
|
+
"--delete-after": "never"
|
|
4498
|
+
}
|
|
4499
|
+
},
|
|
4500
|
+
"tonbo machine show": {
|
|
4501
|
+
about: [
|
|
4502
|
+
"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, its database and logs URLs, 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>`.",
|
|
4503
|
+
"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."
|
|
4504
|
+
],
|
|
4505
|
+
examples: ["tonbo machine show web"],
|
|
4506
|
+
arguments: NAME("Machine")
|
|
4507
|
+
},
|
|
4508
|
+
"tonbo machine update": {
|
|
4509
|
+
about: [
|
|
4510
|
+
"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.",
|
|
4511
|
+
"Ports and `--sleep-after` apply at once. The image, command, shape and env apply at the next boot, and the command says how to apply them now: `tonbo machine sleep`, after which the next request boots the new spec. Naming the same tag again resolves its digest again.",
|
|
4512
|
+
"`--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."
|
|
4513
|
+
],
|
|
4514
|
+
examples: ["tonbo machine update web --env MODE=staging --sleep-after 300"],
|
|
4515
|
+
arguments: {
|
|
4516
|
+
name: "the Machine's name",
|
|
4517
|
+
command: "the command that replaces the Machine's command at the next boot, after `--`"
|
|
4518
|
+
},
|
|
4519
|
+
defaults: {
|
|
4520
|
+
"--default-port": "the current default while --port still lists it, else the first --port",
|
|
4521
|
+
"--if-match": "the ETag the command reads"
|
|
4522
|
+
}
|
|
4523
|
+
},
|
|
4524
|
+
"tonbo machine sleep": {
|
|
4525
|
+
about: [
|
|
4526
|
+
"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."
|
|
4527
|
+
],
|
|
4528
|
+
examples: ["tonbo machine sleep web --wait"],
|
|
4529
|
+
arguments: NAME("Machine"),
|
|
4530
|
+
defaults: { "--timeout": String(DEFAULT_WAIT_SECONDS) }
|
|
4531
|
+
},
|
|
4532
|
+
"tonbo machine delete": {
|
|
4533
|
+
about: [
|
|
4534
|
+
"Deletes the Machine with its disk, database and logs; 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.",
|
|
4535
|
+
"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."
|
|
4536
|
+
],
|
|
4537
|
+
examples: ["tonbo machine delete web --yes"],
|
|
4538
|
+
arguments: NAME("Machine"),
|
|
4539
|
+
defaults: { "--if-match": "the ETag read when asking for confirmation" }
|
|
4540
|
+
},
|
|
4541
|
+
"tonbo machine activations": {
|
|
4542
|
+
about: [
|
|
4543
|
+
"Lists the Machine's activations, newest first: the wake, what woke it, the spec revision and shape it ran, when it started and how it ended (its state, a reason such as `idle_early`, `workload_exited` with the exit code, or a refusal such as `payment_required`), or `live`."
|
|
4544
|
+
],
|
|
4545
|
+
examples: ["tonbo machine activations web --limit 10"],
|
|
4546
|
+
arguments: NAME("Machine"),
|
|
4547
|
+
defaults: { "--limit": LIMIT_DEFAULT }
|
|
4548
|
+
},
|
|
4549
|
+
"tonbo machine shapes": {
|
|
4550
|
+
about: [
|
|
4551
|
+
"Lists the shapes a Machine may have, each with its vCPUs, memory, scratch disk, whether it is offered and can suspend, and its list price for an hour at full use, and marks the default."
|
|
4552
|
+
],
|
|
4553
|
+
examples: ["tonbo machine shapes"],
|
|
4554
|
+
json: "answer"
|
|
4555
|
+
},
|
|
4556
|
+
"tonbo volume list": {
|
|
4557
|
+
about: [
|
|
4558
|
+
"Lists the account's Volumes in every region with each one's state, the Machine bound to it, its size and its live bytes."
|
|
4559
|
+
],
|
|
4560
|
+
examples: ["tonbo volume list"]
|
|
4561
|
+
},
|
|
4562
|
+
"tonbo volume create": {
|
|
4563
|
+
about: [
|
|
4564
|
+
"Creates a Volume of `--size` GiB in one region. It is billed by its live bytes, not its size. Mount it on a Machine of the same region with `tonbo machine update <machine> --volume <name>:<path>`; it attaches at the Machine's next boot."
|
|
4565
|
+
],
|
|
4566
|
+
examples: ["tonbo volume create data --size 20"],
|
|
4567
|
+
arguments: NAME("Volume"),
|
|
4568
|
+
defaults: { "--region": "the account's default region", "--idempotency-key": "a new UUID" }
|
|
4569
|
+
},
|
|
4570
|
+
"tonbo volume show": {
|
|
4571
|
+
about: [
|
|
4572
|
+
"Prints the Volume's region, state, the Machine it is bound to, its size, live bytes, last commit and a link to what it keeps."
|
|
4573
|
+
],
|
|
4574
|
+
examples: ["tonbo volume show data"],
|
|
4575
|
+
arguments: NAME("Volume")
|
|
4576
|
+
},
|
|
4577
|
+
"tonbo volume delete": {
|
|
4578
|
+
about: [
|
|
4579
|
+
"Deletes the Volume and every byte on it; there are no Volume snapshots or backups. It is refused while a Machine has it. At a terminal it asks you to type the Volume's name first."
|
|
4580
|
+
],
|
|
4581
|
+
examples: ["tonbo volume delete data --yes"],
|
|
4582
|
+
arguments: NAME("Volume")
|
|
4583
|
+
},
|
|
4584
|
+
"tonbo usage show": {
|
|
4585
|
+
about: [
|
|
4586
|
+
"Prints the account's rated usage of one calendar month in UTC: one line per product, region, dimension and model with its quantity and amount, and the total, in US dollars."
|
|
4587
|
+
],
|
|
4588
|
+
examples: ["tonbo usage show --period 2026-10"],
|
|
4589
|
+
defaults: { "--period": PERIOD_DEFAULT }
|
|
4590
|
+
},
|
|
4591
|
+
"tonbo usage series": {
|
|
4592
|
+
about: [
|
|
4593
|
+
"Prints the account's spend one line per UTC day of the window, or one line per hour with `--hours`: at list price, what the monthly allowance and the credit balance paid, what is still unpaid, and what it cost beyond the allowance. Then the window's total, the month so far with its allowance and, after three complete days, the month at their run rate. Give at most one of `--days`, `--hours` and `--month`.",
|
|
4594
|
+
"`--compare` adds the window before, of the same length, and the change."
|
|
4595
|
+
],
|
|
4596
|
+
examples: ["tonbo usage series --days 7 --compare"],
|
|
4597
|
+
defaults: { "--days": "30" }
|
|
4598
|
+
},
|
|
4599
|
+
"tonbo usage today": {
|
|
4600
|
+
about: [
|
|
4601
|
+
"Prints the account's write units per UTC hour of yesterday and today, in three columns: databases, streams and other."
|
|
4602
|
+
],
|
|
4603
|
+
examples: ["tonbo usage today --json"]
|
|
4604
|
+
}
|
|
4605
|
+
};
|
|
4606
|
+
var WIDTH = 80;
|
|
4607
|
+
function wrap(text, indent) {
|
|
4608
|
+
const lines = [];
|
|
4609
|
+
let line = "";
|
|
4610
|
+
for (const word of text.split(/\s+/)) {
|
|
4611
|
+
if (line && indent.length + line.length + 1 + word.length > WIDTH) {
|
|
4612
|
+
lines.push(`${indent}${line}`);
|
|
4613
|
+
line = word;
|
|
4614
|
+
} else line = line ? `${line} ${word}` : word;
|
|
4615
|
+
}
|
|
4616
|
+
if (line) lines.push(`${indent}${line}`);
|
|
4617
|
+
return lines.join("\n");
|
|
4618
|
+
}
|
|
4619
|
+
var paragraphs = (texts) => texts.map((text) => wrap(text, " ")).join("\n\n");
|
|
4620
|
+
function commandHelpText(help) {
|
|
4621
|
+
return [
|
|
4622
|
+
"",
|
|
4623
|
+
"Details:",
|
|
4624
|
+
paragraphs(help.about),
|
|
4625
|
+
"",
|
|
4626
|
+
help.examples.length === 1 ? "Example:" : "Examples:",
|
|
4627
|
+
help.examples.map((example) => ` $ ${example}`).join("\n")
|
|
4628
|
+
].join("\n");
|
|
4629
|
+
}
|
|
4630
|
+
function programHelpText() {
|
|
4631
|
+
return [
|
|
4632
|
+
...TOPICS.flatMap((topic) => ["", `${topic.title}:`, paragraphs(topic.text)]),
|
|
4633
|
+
"",
|
|
4634
|
+
"Environment:",
|
|
4635
|
+
ENVIRONMENT.map(
|
|
4636
|
+
(variable) => ` ${variable.names.join(", ")}
|
|
4637
|
+
${wrap(variable.description, " ")}`
|
|
4638
|
+
).join("\n")
|
|
4639
|
+
].join("\n");
|
|
4640
|
+
}
|
|
4641
|
+
function optionDescription(option) {
|
|
4642
|
+
const text = Help.prototype.optionDescription.call(this, option);
|
|
4643
|
+
return option.defaultValue === void 0 && option.defaultValueDescription ? `${text} (default: ${option.defaultValueDescription})` : text;
|
|
4644
|
+
}
|
|
4645
|
+
function applyHelp(program) {
|
|
4646
|
+
const named = /* @__PURE__ */ new Set();
|
|
4647
|
+
const walk = (command, words) => {
|
|
4648
|
+
const path2 = words.join(" ");
|
|
4649
|
+
command.configureHelp({ ...command.configureHelp(), optionDescription });
|
|
4650
|
+
const help = COMMAND_HELP[path2];
|
|
4651
|
+
if (help) {
|
|
4652
|
+
named.add(path2);
|
|
4653
|
+
for (const [name, text] of Object.entries(help.arguments ?? {})) {
|
|
4654
|
+
const argument = command.registeredArguments.find((entry) => entry.name() === name);
|
|
4655
|
+
if (!argument) throw new Error(`${path2} has no argument ${name}`);
|
|
4656
|
+
argument.description = text;
|
|
4657
|
+
}
|
|
4658
|
+
for (const [flag, text] of Object.entries(help.defaults ?? {})) {
|
|
4659
|
+
const option = command.options.find((entry) => entry.long === flag);
|
|
4660
|
+
if (!option) throw new Error(`${path2} has no option ${flag}`);
|
|
4661
|
+
option.defaultValueDescription = text;
|
|
4662
|
+
}
|
|
4663
|
+
command.addHelpText("after", commandHelpText(help));
|
|
4664
|
+
}
|
|
4665
|
+
for (const child of command.commands) walk(child, [...words, child.name()]);
|
|
4666
|
+
};
|
|
4667
|
+
walk(program, [program.name()]);
|
|
4668
|
+
for (const path2 of Object.keys(COMMAND_HELP))
|
|
4669
|
+
if (!named.has(path2)) throw new Error(`The help of ${path2} names no command`);
|
|
4670
|
+
program.addHelpText("after", programHelpText());
|
|
4671
|
+
return program;
|
|
4672
|
+
}
|
|
4673
|
+
|
|
3377
4674
|
// src/prompt.ts
|
|
3378
4675
|
import select from "@inquirer/select";
|
|
3379
4676
|
import { createInterface } from "node:readline/promises";
|
|
@@ -3434,7 +4731,7 @@ function operationOptions(command, operationId) {
|
|
|
3434
4731
|
command.option("--cursor <cursor>", "continue from the cursor a previous page printed");
|
|
3435
4732
|
return command.option("--json", "print the API data as JSON");
|
|
3436
4733
|
}
|
|
3437
|
-
var ifMatchHelp = (action) => `${action} only the Machine at the ETag \`tonbo machine show\` printed (with or without its quotes)
|
|
4734
|
+
var ifMatchHelp = (action) => `${action} only the Machine at the ETag \`tonbo machine show\` printed (with or without its quotes)`;
|
|
3438
4735
|
var repeated = (value, previous = []) => [...previous, value];
|
|
3439
4736
|
var roleOption = (description) => new Option("--role <role>", description).choices(accountRoles);
|
|
3440
4737
|
function machineSpecOptions(command, update) {
|
|
@@ -3445,7 +4742,7 @@ function machineSpecOptions(command, update) {
|
|
|
3445
4742
|
repeated
|
|
3446
4743
|
).option(
|
|
3447
4744
|
"--default-port <port>",
|
|
3448
|
-
|
|
4745
|
+
"the port served at the Machine's hostname without a port prefix",
|
|
3449
4746
|
parseWhole
|
|
3450
4747
|
).option(
|
|
3451
4748
|
"--env <KEY=VALUE>",
|
|
@@ -3463,6 +4760,13 @@ function machineSpecOptions(command, update) {
|
|
|
3463
4760
|
"--stop-grace <seconds>",
|
|
3464
4761
|
"the seconds between SIGTERM and SIGKILL on a planned stop",
|
|
3465
4762
|
parseWhole
|
|
4763
|
+
).option(
|
|
4764
|
+
"--volume <name:path>",
|
|
4765
|
+
`mount the Volume <name> (see tonbo volume list) at <path>; a Machine has at most one, of its own region${update ? "; applies at the next boot" : ""}`
|
|
4766
|
+
).option(
|
|
4767
|
+
"--delete-after <seconds>",
|
|
4768
|
+
`delete the Machine once it has been asleep, never woken or failed this long (3600 to 7776000)${update ? "; applies at once, from its last activation's end" : ""}`,
|
|
4769
|
+
parseWhole
|
|
3466
4770
|
);
|
|
3467
4771
|
}
|
|
3468
4772
|
function parseExpiresIn(value) {
|
|
@@ -3481,7 +4785,10 @@ function parseUsageSort(value) {
|
|
|
3481
4785
|
}
|
|
3482
4786
|
function createProgram(dependencies = createDependencies) {
|
|
3483
4787
|
const program = new Command().name("tonbo").description("Sign in to Tonbo and manage account resources.").version(packageVersion.version);
|
|
3484
|
-
program.command("login").description("sign in through the browser and save a personal key for this computer").option("--account <name>", "the account to log in to
|
|
4788
|
+
program.command("login").description("sign in through the browser and save a personal key for this computer").option("--account <name>", "the account to log in to").option(
|
|
4789
|
+
"--device",
|
|
4790
|
+
"approve with a code in a browser on any computer, as over SSH without a display"
|
|
4791
|
+
).action(async (options) => {
|
|
3485
4792
|
await loginCommand(dependencies(), options);
|
|
3486
4793
|
});
|
|
3487
4794
|
program.command("logout").description("revoke this computer's key and remove the saved login").action(async () => logoutCommand(dependencies()));
|
|
@@ -3516,7 +4823,7 @@ function createProgram(dependencies = createDependencies) {
|
|
|
3516
4823
|
async (subject, options) => memberUpdateCommand(dependencies(), subject, options)
|
|
3517
4824
|
);
|
|
3518
4825
|
operationOptions(
|
|
3519
|
-
program.command("agent").description("manage the Agents of your account").command("create <name>").description("create an agent").addOption(roleOption("the Agent's role
|
|
4826
|
+
program.command("agent").description("manage the Agents of your account").command("create <name>").description("create an agent").addOption(roleOption("the Agent's role")).option(
|
|
3520
4827
|
"--region <region>",
|
|
3521
4828
|
"permanent home region (see tonbo region list); required once several regions accept new agents"
|
|
3522
4829
|
).option(
|
|
@@ -3535,10 +4842,7 @@ function createProgram(dependencies = createDependencies) {
|
|
|
3535
4842
|
async (options) => apiKeyListCommand(dependencies(), options)
|
|
3536
4843
|
);
|
|
3537
4844
|
operationOptions(
|
|
3538
|
-
apiKey.command("create <name>").description("create a key and print its secret, once").option(
|
|
3539
|
-
"--member <member>",
|
|
3540
|
-
"an Agent (subject, id or name) to create the key for; yourself by default"
|
|
3541
|
-
).option("--read-only", "narrow the key to read-only actions, for its whole life").option(
|
|
4845
|
+
apiKey.command("create <name>").description("create a key and print its secret, once").option("--member <member>", "an Agent (subject, id or name) to create the key for").option("--read-only", "narrow the key to read-only actions, for its whole life").option(
|
|
3542
4846
|
"--bucket <name>",
|
|
3543
4847
|
"narrow the key to this bucket, for its whole life; repeat for several",
|
|
3544
4848
|
repeated
|
|
@@ -3559,9 +4863,7 @@ function createProgram(dependencies = createDependencies) {
|
|
|
3559
4863
|
"narrow the key to the raw streams whose name starts with this, for its whole life; repeat for several; never with --bucket",
|
|
3560
4864
|
repeated
|
|
3561
4865
|
).addOption(
|
|
3562
|
-
new Option("--expires-in <period>", "when the key expires
|
|
3563
|
-
apiKeyExpirations
|
|
3564
|
-
)
|
|
4866
|
+
new Option("--expires-in <period>", "when the key expires").choices(apiKeyExpirations)
|
|
3565
4867
|
),
|
|
3566
4868
|
"createApiKey"
|
|
3567
4869
|
).action(
|
|
@@ -3581,10 +4883,7 @@ function createProgram(dependencies = createDependencies) {
|
|
|
3581
4883
|
async (options) => bucketListCommand(dependencies(), options)
|
|
3582
4884
|
);
|
|
3583
4885
|
operationOptions(
|
|
3584
|
-
bucket.command("create <name>").description("create a bucket").option(
|
|
3585
|
-
"--region <region>",
|
|
3586
|
-
"the bucket's region (see tonbo region list); the account's default region by default"
|
|
3587
|
-
).option(
|
|
4886
|
+
bucket.command("create <name>").description("create a bucket").option("--region <region>", "the bucket's region (see tonbo region list)").option(
|
|
3588
4887
|
"--idempotency-key <key>",
|
|
3589
4888
|
"reuse the key of an unconfirmed creation to get the bucket it created"
|
|
3590
4889
|
),
|
|
@@ -3615,18 +4914,15 @@ function createProgram(dependencies = createDependencies) {
|
|
|
3615
4914
|
async (options) => databaseListCommand(dependencies(), options)
|
|
3616
4915
|
);
|
|
3617
4916
|
operationOptions(
|
|
3618
|
-
database.command("create <name>").description("create a database; its region is fixed for its life").option(
|
|
3619
|
-
"--region <region>",
|
|
3620
|
-
"the database's region (see tonbo region list); the database default region by default"
|
|
3621
|
-
).addOption(
|
|
4917
|
+
database.command("create <name>").description("create a database; its region is fixed for its life").option("--region <region>", "the database's region (see tonbo region list)").addOption(
|
|
3622
4918
|
new Option(
|
|
3623
4919
|
"--expires-at <time>",
|
|
3624
|
-
"delete the database after this RFC 3339 time, at least 60 seconds after its creation, such as 2026-11-01T00:00:00Z
|
|
4920
|
+
"delete the database after this RFC 3339 time, at least 60 seconds after its creation, such as 2026-11-01T00:00:00Z"
|
|
3625
4921
|
).conflicts("expiresIn")
|
|
3626
4922
|
).addOption(
|
|
3627
4923
|
new Option(
|
|
3628
4924
|
"--expires-in <duration>",
|
|
3629
|
-
"delete the database this long after its creation, at least 90s: such as 90s, 30m, 24h or 7d; never
|
|
4925
|
+
"delete the database this long after its creation, at least 90s: such as 90s, 30m, 24h or 7d; never with --idempotency-key"
|
|
3630
4926
|
).argParser(parseExpiresIn).conflicts("expiresAt")
|
|
3631
4927
|
).option(
|
|
3632
4928
|
"--idempotency-key <key>",
|
|
@@ -3655,10 +4951,15 @@ function createProgram(dependencies = createDependencies) {
|
|
|
3655
4951
|
async (name, options) => databaseDeleteCommand(dependencies(), name, options)
|
|
3656
4952
|
);
|
|
3657
4953
|
operationOptions(
|
|
3658
|
-
database.command("
|
|
3659
|
-
"--
|
|
3660
|
-
|
|
3661
|
-
|
|
4954
|
+
database.command("activity <name>").description("show a database's writes per hour or day and its size at each step's end").addOption(
|
|
4955
|
+
new Option("--granularity <step>", "one row per UTC hour or day").choices(["hour", "day"])
|
|
4956
|
+
).option("--from <time>", "the window's start, an RFC 3339 time on a UTC hour (or day)").option("--to <time>", "the window's end, exclusive, at most 31 days after --from"),
|
|
4957
|
+
"getDatabaseActivity"
|
|
4958
|
+
).action(
|
|
4959
|
+
async (name, options) => databaseActivityCommand(dependencies(), name, options)
|
|
4960
|
+
);
|
|
4961
|
+
operationOptions(
|
|
4962
|
+
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"),
|
|
3662
4963
|
"listDatabaseUsage"
|
|
3663
4964
|
).action(
|
|
3664
4965
|
async (options) => databaseUsageCommand(dependencies(), options)
|
|
@@ -3671,21 +4972,18 @@ function createProgram(dependencies = createDependencies) {
|
|
|
3671
4972
|
async (options) => streamListCommand(dependencies(), options)
|
|
3672
4973
|
);
|
|
3673
4974
|
operationOptions(
|
|
3674
|
-
stream.command("create <name>").description("create a stream; its region and content type are fixed for its life").option(
|
|
3675
|
-
"--region <region>",
|
|
3676
|
-
"the stream's region (see tonbo region list); the database default region by default"
|
|
3677
|
-
).option(
|
|
4975
|
+
stream.command("create <name>").description("create a stream; its region and content type are fixed for its life").option("--region <region>", "the stream's region (see tonbo region list)").option(
|
|
3678
4976
|
"--content-type <type>",
|
|
3679
|
-
"the media type every append sends, such as application/json
|
|
4977
|
+
"the media type every append sends, such as application/json"
|
|
3680
4978
|
).addOption(
|
|
3681
4979
|
new Option(
|
|
3682
4980
|
"--expires-at <time>",
|
|
3683
|
-
"delete the stream after this RFC 3339 time, at least 60 seconds after its creation, such as 2026-11-01T00:00:00Z
|
|
4981
|
+
"delete the stream after this RFC 3339 time, at least 60 seconds after its creation, such as 2026-11-01T00:00:00Z"
|
|
3684
4982
|
).conflicts("expiresIn")
|
|
3685
4983
|
).addOption(
|
|
3686
4984
|
new Option(
|
|
3687
4985
|
"--expires-in <duration>",
|
|
3688
|
-
"delete the stream this long after its creation, at least 90s: such as 90s, 30m, 24h or 7d; never
|
|
4986
|
+
"delete the stream this long after its creation, at least 90s: such as 90s, 30m, 24h or 7d; never with --idempotency-key"
|
|
3689
4987
|
).argParser(parseExpiresIn).conflicts("expiresAt")
|
|
3690
4988
|
).option(
|
|
3691
4989
|
"--idempotency-key <key>",
|
|
@@ -3714,10 +5012,15 @@ function createProgram(dependencies = createDependencies) {
|
|
|
3714
5012
|
async (name, options) => streamDeleteCommand(dependencies(), name, options)
|
|
3715
5013
|
);
|
|
3716
5014
|
operationOptions(
|
|
3717
|
-
stream.command("
|
|
3718
|
-
"--
|
|
3719
|
-
|
|
3720
|
-
|
|
5015
|
+
stream.command("activity <name>").description("show a stream's writes per hour or day and its size at each step's end").addOption(
|
|
5016
|
+
new Option("--granularity <step>", "one row per UTC hour or day").choices(["hour", "day"])
|
|
5017
|
+
).option("--from <time>", "the window's start, an RFC 3339 time on a UTC hour (or day)").option("--to <time>", "the window's end, exclusive, at most 31 days after --from"),
|
|
5018
|
+
"getStreamActivity"
|
|
5019
|
+
).action(
|
|
5020
|
+
async (name, options) => streamActivityCommand(dependencies(), name, options)
|
|
5021
|
+
);
|
|
5022
|
+
operationOptions(
|
|
5023
|
+
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"),
|
|
3721
5024
|
"listStreamUsage"
|
|
3722
5025
|
).action(
|
|
3723
5026
|
async (options) => streamUsageCommand(dependencies(), options)
|
|
@@ -3735,20 +5038,13 @@ function createProgram(dependencies = createDependencies) {
|
|
|
3735
5038
|
"create a Machine from an image; [command...] after -- replaces the image's CMD"
|
|
3736
5039
|
).requiredOption(
|
|
3737
5040
|
"--image <reference>",
|
|
3738
|
-
"the image the Machine boots:
|
|
3739
|
-
).option(
|
|
3740
|
-
"--region <region>",
|
|
3741
|
-
"the Machine's region, fixed for its life; the account's default region by default"
|
|
3742
|
-
),
|
|
5041
|
+
"the image the Machine boots: a public OCI image with a linux/arm64 variant (nginx:1.27, ghcr.io/org/app@sha256:...), or tonbo.dev/platform"
|
|
5042
|
+
).option("--region <region>", "the Machine's region, fixed for its life"),
|
|
3743
5043
|
false
|
|
3744
5044
|
).option(
|
|
3745
5045
|
"--idempotency-key <key>",
|
|
3746
5046
|
"reuse the key of an unconfirmed creation to get the Machine it created"
|
|
3747
|
-
).option("--wait", "wait until the Machine is active, or say why it failed").option(
|
|
3748
|
-
"--timeout <seconds>",
|
|
3749
|
-
`how long --wait waits; ${DEFAULT_WAIT_SECONDS} by default`,
|
|
3750
|
-
parseWhole
|
|
3751
|
-
),
|
|
5047
|
+
).option("--wait", "wait until the Machine is active, or say why it failed").option("--timeout <seconds>", "how long --wait waits, in seconds", parseWhole),
|
|
3752
5048
|
"createMachine"
|
|
3753
5049
|
).action(
|
|
3754
5050
|
async (name, command, options) => machineCreateCommand(dependencies(), name, command, options)
|
|
@@ -3763,7 +5059,7 @@ function createProgram(dependencies = createDependencies) {
|
|
|
3763
5059
|
machineSpecOptions(
|
|
3764
5060
|
machine.command("update <name> [command...]").description(
|
|
3765
5061
|
"change a Machine; [command...] after -- replaces its command at the next boot"
|
|
3766
|
-
).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"),
|
|
5062
|
+
).option("--image <reference>", "a new image, named as for create; applies at the next boot").option("--image-command", "run the image's own CMD again at the next boot").option("--unset-env <KEY>", "remove an environment variable; repeat for several", repeated).option("--clear-ports", "remove every port").option("--if-match <etag>", ifMatchHelp("change")).option("--discard-snapshot", "apply a change that discards the Machine's snapshot").option("--no-volume", "remove the Machine's Volume at the next boot; the Volume is kept").option("--no-delete-after", "never delete the Machine for being asleep"),
|
|
3767
5063
|
true
|
|
3768
5064
|
),
|
|
3769
5065
|
"updateMachine"
|
|
@@ -3771,11 +5067,7 @@ function createProgram(dependencies = createDependencies) {
|
|
|
3771
5067
|
async (name, command, options) => machineUpdateCommand(dependencies(), name, command, options)
|
|
3772
5068
|
);
|
|
3773
5069
|
operationOptions(
|
|
3774
|
-
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(
|
|
3775
|
-
"--timeout <seconds>",
|
|
3776
|
-
`how long --wait waits; ${DEFAULT_WAIT_SECONDS} by default`,
|
|
3777
|
-
parseWhole
|
|
3778
|
-
),
|
|
5070
|
+
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),
|
|
3779
5071
|
"sleepMachine"
|
|
3780
5072
|
).action(
|
|
3781
5073
|
async (name, options) => machineSleepCommand(dependencies(), name, options)
|
|
@@ -3798,16 +5090,52 @@ function createProgram(dependencies = createDependencies) {
|
|
|
3798
5090
|
machine.command("shapes").description("list the shapes a Machine may have"),
|
|
3799
5091
|
"listMachineShapes"
|
|
3800
5092
|
).action(async (options) => machineShapesCommand(dependencies(), options));
|
|
5093
|
+
const volume = program.command("volume").description("the account's Volumes: durable disks a Machine of the same region mounts");
|
|
5094
|
+
operationOptions(
|
|
5095
|
+
volume.command("list").description("list the Volumes in every region"),
|
|
5096
|
+
"listVolumes"
|
|
5097
|
+
).action(async (options) => volumeListCommand(dependencies(), options));
|
|
3801
5098
|
operationOptions(
|
|
3802
|
-
|
|
3803
|
-
"--
|
|
3804
|
-
"the
|
|
5099
|
+
volume.command("create <name>").description("create a Volume; billed by its live bytes, not its size").requiredOption("--size <GiB>", "the Volume's size in GiB, 1 to 100, fixed for its life").option("--region <region>", "the Volume's region, fixed for its life").option(
|
|
5100
|
+
"--idempotency-key <key>",
|
|
5101
|
+
"reuse the key of an unconfirmed creation to get the Volume it created"
|
|
3805
5102
|
),
|
|
5103
|
+
"createVolume"
|
|
5104
|
+
).action(
|
|
5105
|
+
async (name, options) => volumeCreateCommand(dependencies(), name, options)
|
|
5106
|
+
);
|
|
5107
|
+
operationOptions(
|
|
5108
|
+
volume.command("show <name>").description("show a Volume, the Machine it is bound to and its live bytes"),
|
|
5109
|
+
"getVolume"
|
|
5110
|
+
).action(
|
|
5111
|
+
async (name, options) => volumeShowCommand(dependencies(), name, options)
|
|
5112
|
+
);
|
|
5113
|
+
operationOptions(
|
|
5114
|
+
volume.command("delete <name>").description("delete a Volume and its data; refused while a Machine has it").option("--yes", "delete without asking; required when stdin is not a terminal"),
|
|
5115
|
+
"deleteVolume"
|
|
5116
|
+
).action(
|
|
5117
|
+
async (name, options) => volumeDeleteCommand(dependencies(), name, options)
|
|
5118
|
+
);
|
|
5119
|
+
const usage = program.command("usage").description("the account's rated usage");
|
|
5120
|
+
operationOptions(
|
|
5121
|
+
usage.command("show").description("show one month's rated usage").option("--period <month>", "the calendar month in UTC, as YYYY-MM"),
|
|
3806
5122
|
"getUsage"
|
|
3807
5123
|
).action(
|
|
3808
5124
|
async (options) => usageShowCommand(dependencies(), options)
|
|
3809
5125
|
);
|
|
3810
|
-
|
|
5126
|
+
operationOptions(
|
|
5127
|
+
usage.command("series").description("show the account's spend day by day, or hour by hour").option("--days <count>", "the last 1 to 92 UTC days, today included", parseWhole).option("--hours <count>", "the last 1 to 48 hours, one line per hour", parseWhole).option("--month <month>", "one calendar month in UTC, as YYYY-MM").option("--compare", "also show the window before, and the change"),
|
|
5128
|
+
"getUsageSeries"
|
|
5129
|
+
).action(
|
|
5130
|
+
async (options) => usageSeriesCommand(dependencies(), options)
|
|
5131
|
+
);
|
|
5132
|
+
operationOptions(
|
|
5133
|
+
usage.command("today").description(
|
|
5134
|
+
"show the account's write units per hour of yesterday and today, by databases, streams and other"
|
|
5135
|
+
),
|
|
5136
|
+
"getUsageToday"
|
|
5137
|
+
).action(async (options) => usageTodayCommand(dependencies(), options));
|
|
5138
|
+
return applyHelp(program);
|
|
3811
5139
|
}
|
|
3812
5140
|
|
|
3813
5141
|
// src/main.ts
|