@groundfloorcloud/cli 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +84 -13
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -535,12 +535,14 @@ async function logoutCommand() {
|
|
|
535
535
|
|
|
536
536
|
// src/cp.ts
|
|
537
537
|
var CpError = class extends Error {
|
|
538
|
-
constructor(status, message) {
|
|
538
|
+
constructor(status, message, reason = null) {
|
|
539
539
|
super(message);
|
|
540
540
|
this.status = status;
|
|
541
|
+
this.reason = reason;
|
|
541
542
|
this.name = "CpError";
|
|
542
543
|
}
|
|
543
544
|
status;
|
|
545
|
+
reason;
|
|
544
546
|
};
|
|
545
547
|
function formatCpErrorDetail(detail, fallback) {
|
|
546
548
|
if (typeof detail === "string" && detail.trim()) return detail.trim();
|
|
@@ -560,6 +562,40 @@ function formatCpErrorDetail(detail, fallback) {
|
|
|
560
562
|
}
|
|
561
563
|
return fallback;
|
|
562
564
|
}
|
|
565
|
+
function parseRetryAfter(value, now = Date.now()) {
|
|
566
|
+
if (!value) return null;
|
|
567
|
+
const trimmed = value.trim();
|
|
568
|
+
if (/^\d+$/.test(trimmed)) return Number(trimmed);
|
|
569
|
+
const at = Date.parse(trimmed);
|
|
570
|
+
if (Number.isNaN(at)) return null;
|
|
571
|
+
return Math.max(0, Math.ceil((at - now) / 1e3));
|
|
572
|
+
}
|
|
573
|
+
function formatWait(seconds) {
|
|
574
|
+
if (seconds < 60) return `${seconds} second${seconds === 1 ? "" : "s"}`;
|
|
575
|
+
const minutes = Math.ceil(seconds / 60);
|
|
576
|
+
if (minutes < 60) return `${minutes} minute${minutes === 1 ? "" : "s"}`;
|
|
577
|
+
const hours = Math.ceil(minutes / 60);
|
|
578
|
+
return `${hours} hour${hours === 1 ? "" : "s"}`;
|
|
579
|
+
}
|
|
580
|
+
var REASON_HEADER = "x-groundfloor-reason";
|
|
581
|
+
function formatCpErrorMessage(status, detail, retryAfterSeconds, reason = null) {
|
|
582
|
+
let message = `${status} ${detail}`;
|
|
583
|
+
const wait = retryAfterSeconds !== null ? `try again in ${formatWait(retryAfterSeconds)}` : "try again later";
|
|
584
|
+
if (status === 429 && reason === "sending_limited") {
|
|
585
|
+
message += ` (workspace sign-in email limit reached: ${wait}, or give access with a copy-link or temporary password instead)`;
|
|
586
|
+
} else if (status === 429) {
|
|
587
|
+
message += ` (rate limited: ${wait})`;
|
|
588
|
+
} else if (status === 423 && reason === "sending_paused") {
|
|
589
|
+
message += " (sign-in email is paused: only Groundfloor support can resume it; meanwhile give access with a copy-link or temporary password, or switch to your own email server)";
|
|
590
|
+
} else if (status === 423 && !/support/i.test(detail)) {
|
|
591
|
+
message += " (locked: Groundfloor has paused or frozen this; contact Groundfloor support)";
|
|
592
|
+
}
|
|
593
|
+
return message;
|
|
594
|
+
}
|
|
595
|
+
function reasonHeader(res) {
|
|
596
|
+
const value = res.headers?.get?.(REASON_HEADER);
|
|
597
|
+
return value ? value.trim() : null;
|
|
598
|
+
}
|
|
563
599
|
async function parseError(res) {
|
|
564
600
|
let detail = res.statusText;
|
|
565
601
|
try {
|
|
@@ -569,7 +605,12 @@ async function parseError(res) {
|
|
|
569
605
|
}
|
|
570
606
|
} catch {
|
|
571
607
|
}
|
|
572
|
-
return
|
|
608
|
+
return formatCpErrorMessage(
|
|
609
|
+
res.status,
|
|
610
|
+
detail,
|
|
611
|
+
parseRetryAfter(res.headers?.get?.("retry-after")),
|
|
612
|
+
reasonHeader(res)
|
|
613
|
+
);
|
|
573
614
|
}
|
|
574
615
|
async function cpGet(apiUrl, path11) {
|
|
575
616
|
const token = await getValidAccessToken();
|
|
@@ -580,7 +621,7 @@ async function cpGet(apiUrl, path11) {
|
|
|
580
621
|
}
|
|
581
622
|
});
|
|
582
623
|
if (!res.ok) {
|
|
583
|
-
throw new CpError(res.status, await parseError(res));
|
|
624
|
+
throw new CpError(res.status, await parseError(res), reasonHeader(res));
|
|
584
625
|
}
|
|
585
626
|
return await res.json();
|
|
586
627
|
}
|
|
@@ -596,7 +637,7 @@ async function cpPostJson(apiUrl, path11, body) {
|
|
|
596
637
|
body: JSON.stringify(body)
|
|
597
638
|
});
|
|
598
639
|
if (!res.ok) {
|
|
599
|
-
throw new CpError(res.status, await parseError(res));
|
|
640
|
+
throw new CpError(res.status, await parseError(res), reasonHeader(res));
|
|
600
641
|
}
|
|
601
642
|
return await res.json();
|
|
602
643
|
}
|
|
@@ -607,7 +648,7 @@ async function cpDelete(apiUrl, path11) {
|
|
|
607
648
|
headers: { Authorization: `Bearer ${token}`, Accept: "application/json" }
|
|
608
649
|
});
|
|
609
650
|
if (!res.ok) {
|
|
610
|
-
throw new CpError(res.status, await parseError(res));
|
|
651
|
+
throw new CpError(res.status, await parseError(res), reasonHeader(res));
|
|
611
652
|
}
|
|
612
653
|
}
|
|
613
654
|
async function cpPutJson(apiUrl, path11, body) {
|
|
@@ -622,7 +663,7 @@ async function cpPutJson(apiUrl, path11, body) {
|
|
|
622
663
|
body: JSON.stringify(body)
|
|
623
664
|
});
|
|
624
665
|
if (!res.ok) {
|
|
625
|
-
throw new CpError(res.status, await parseError(res));
|
|
666
|
+
throw new CpError(res.status, await parseError(res), reasonHeader(res));
|
|
626
667
|
}
|
|
627
668
|
return await res.json();
|
|
628
669
|
}
|
|
@@ -638,7 +679,7 @@ async function cpPatchJson(apiUrl, path11, body) {
|
|
|
638
679
|
body: JSON.stringify(body)
|
|
639
680
|
});
|
|
640
681
|
if (!res.ok) {
|
|
641
|
-
throw new CpError(res.status, await parseError(res));
|
|
682
|
+
throw new CpError(res.status, await parseError(res), reasonHeader(res));
|
|
642
683
|
}
|
|
643
684
|
return await res.json();
|
|
644
685
|
}
|
|
@@ -654,7 +695,7 @@ async function cpPutBytes(apiUrl, path11, body, contentType) {
|
|
|
654
695
|
body
|
|
655
696
|
});
|
|
656
697
|
if (!res.ok) {
|
|
657
|
-
throw new CpError(res.status, await parseError(res));
|
|
698
|
+
throw new CpError(res.status, await parseError(res), reasonHeader(res));
|
|
658
699
|
}
|
|
659
700
|
}
|
|
660
701
|
function queryString(params) {
|
|
@@ -710,7 +751,7 @@ async function uploadZip(apiUrl, workspaceId, coderunnerId, zip, description) {
|
|
|
710
751
|
}
|
|
711
752
|
);
|
|
712
753
|
if (!res.ok) {
|
|
713
|
-
throw new CpError(res.status, await parseError(res));
|
|
754
|
+
throw new CpError(res.status, await parseError(res), reasonHeader(res));
|
|
714
755
|
}
|
|
715
756
|
return await res.json();
|
|
716
757
|
}
|
|
@@ -748,7 +789,7 @@ async function getDeployment(apiUrl, workspaceId, coderunnerId) {
|
|
|
748
789
|
);
|
|
749
790
|
if (res.status === 204) return null;
|
|
750
791
|
if (!res.ok) {
|
|
751
|
-
throw new CpError(res.status, await parseError(res));
|
|
792
|
+
throw new CpError(res.status, await parseError(res), reasonHeader(res));
|
|
752
793
|
}
|
|
753
794
|
return await res.json();
|
|
754
795
|
}
|
|
@@ -768,7 +809,7 @@ async function getDeploymentById(apiUrl, workspaceId, coderunnerId, deploymentId
|
|
|
768
809
|
}
|
|
769
810
|
);
|
|
770
811
|
if (!res.ok) {
|
|
771
|
-
throw new CpError(res.status, await parseError(res));
|
|
812
|
+
throw new CpError(res.status, await parseError(res), reasonHeader(res));
|
|
772
813
|
}
|
|
773
814
|
return await res.json();
|
|
774
815
|
}
|
|
@@ -1034,7 +1075,7 @@ async function mintM2mToken(apiUrl, workspaceId, clientId, clientSecret) {
|
|
|
1034
1075
|
}
|
|
1035
1076
|
);
|
|
1036
1077
|
if (!res.ok) {
|
|
1037
|
-
throw new CpError(res.status, await parseError(res));
|
|
1078
|
+
throw new CpError(res.status, await parseError(res), reasonHeader(res));
|
|
1038
1079
|
}
|
|
1039
1080
|
return await res.json();
|
|
1040
1081
|
}
|
|
@@ -1075,7 +1116,7 @@ async function controlPlaneRequest(apiUrl, method, path11, body) {
|
|
|
1075
1116
|
});
|
|
1076
1117
|
if (res.status === 204) return void 0;
|
|
1077
1118
|
if (!res.ok) {
|
|
1078
|
-
throw new CpError(res.status, await parseError(res));
|
|
1119
|
+
throw new CpError(res.status, await parseError(res), reasonHeader(res));
|
|
1079
1120
|
}
|
|
1080
1121
|
const text = await res.text();
|
|
1081
1122
|
if (!text) return void 0;
|
|
@@ -3807,6 +3848,33 @@ function commanderExitCode(err) {
|
|
|
3807
3848
|
return 0;
|
|
3808
3849
|
}
|
|
3809
3850
|
|
|
3851
|
+
// src/email-delivery.ts
|
|
3852
|
+
var REASONS = {
|
|
3853
|
+
smtp_not_configured: "no email server is set up for this workspace's sign-in email",
|
|
3854
|
+
send_failed: "the email server refused or failed to send it",
|
|
3855
|
+
delivery_unconfirmed: "the email server timed out; the email may still arrive",
|
|
3856
|
+
not_requested: "sending was turned off for this request (send_email: false)",
|
|
3857
|
+
sending_limited: "the workspace or this recipient reached the sign-in email limit (200 per hour and 1,000 per day per workspace; 5 per hour and 10 per day per recipient); try again later, or share a copy-link or temporary password instead",
|
|
3858
|
+
sending_paused: "Groundfloor paused this workspace's sign-in email after too many bounces or spam reports; contact Groundfloor support, or share a copy-link or temporary password instead",
|
|
3859
|
+
recipient_suppressed: "this address bounced or reported spam, so Groundfloor will not email it; check the address, or clear it from the workspace suppression list (GET/DELETE /v1/workspaces/{workspace_id}/mail/suppressions; for a spam report only Groundfloor support can also clear it at the mail provider)"
|
|
3860
|
+
};
|
|
3861
|
+
function describeEmailSkippedReason(reason) {
|
|
3862
|
+
return REASONS[reason] ?? reason;
|
|
3863
|
+
}
|
|
3864
|
+
function emailDeliveryNote(data) {
|
|
3865
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return null;
|
|
3866
|
+
const obj = data;
|
|
3867
|
+
if (!("email_sent" in obj) || obj.email_sent === true) return null;
|
|
3868
|
+
const reason = typeof obj.email_skipped_reason === "string" && obj.email_skipped_reason ? obj.email_skipped_reason : null;
|
|
3869
|
+
if (obj.email_sent === false) {
|
|
3870
|
+
return reason ? `Note: the email was not sent (${reason}): ${describeEmailSkippedReason(reason)}.` : "Note: the email was not sent.";
|
|
3871
|
+
}
|
|
3872
|
+
if (obj.email_sent === null || obj.email_sent === void 0) {
|
|
3873
|
+
return reason ? `Note: email delivery is unknown (${reason}): ${describeEmailSkippedReason(reason)}.` : null;
|
|
3874
|
+
}
|
|
3875
|
+
return null;
|
|
3876
|
+
}
|
|
3877
|
+
|
|
3810
3878
|
// src/commands/api.ts
|
|
3811
3879
|
var METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
|
|
3812
3880
|
async function apiCommand(opts) {
|
|
@@ -3829,6 +3897,9 @@ async function apiCommand(opts) {
|
|
|
3829
3897
|
return;
|
|
3830
3898
|
}
|
|
3831
3899
|
process.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
3900
|
+
`);
|
|
3901
|
+
const note = emailDeliveryNote(data);
|
|
3902
|
+
if (note) process.stderr.write(`${note}
|
|
3832
3903
|
`);
|
|
3833
3904
|
}
|
|
3834
3905
|
|
package/package.json
CHANGED