@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.
Files changed (2) hide show
  1. package/dist/index.js +84 -13
  2. 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 `${res.status} ${detail}`;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groundfloorcloud/cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Groundfloor gf CLI — sign in, deploy coderunners, publish Shell apps, and manage secrets, files, and Dataplane.",
5
5
  "type": "module",
6
6
  "bin": {