@astris-labs/cli 0.1.3 → 0.1.4

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 CHANGED
@@ -97,7 +97,7 @@ Optional flags are `--build-path`, `--dockerfile`, `--port`, `--build`, `--publi
97
97
 
98
98
  `astris compose list` calls `GET /v1/compose`. Human output is one line per stack in the order the API returned: the id, the name, and the project name. An empty list prints nothing. `--json` prints one JSON object `{"compose":[...]}` of the stack rows.
99
99
 
100
- `astris compose deploy --id <id>` calls `POST /v1/compose/:id/deploy` and sends no body. Human output is the stack id, project id, name, git URL, branch, compose path, memory limit, created_at, and updated_at, one per line. `--json` prints one JSON object `{"compose":{...}}`.
100
+ `astris compose deploy --id <id>` calls `POST /v1/compose/:id/deploy` and sends no body. In the private preview the API refuses every redeploy, so the command prints the API's sentence and exits 1. Otherwise human output is the stack id, project id, name, git URL, branch, compose path, memory limit, created_at, and updated_at, one per line. `--json` prints one JSON object `{"compose":{...}}`.
101
101
 
102
102
  `astris compose start --id <id>` calls `POST /v1/compose/:id/start` and sends no body. Human output is the status the API returned, then the same stack fields as deploy, one per line. `--json` prints one JSON object `{"compose":{...},"status":"<status>"}`.
103
103
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astris-labs/cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "The Astris CLI, a command-line client of the Astris API.",
5
5
  "repository": {
6
6
  "type": "git",
package/src/client.mjs CHANGED
@@ -18,6 +18,22 @@ function swallow(promise) {
18
18
  promise.catch(() => undefined);
19
19
  }
20
20
 
21
+ function retryAfterSecondsOf(headers) {
22
+ if (!headers) return null;
23
+ let raw;
24
+ if (typeof headers.get === "function") {
25
+ raw = headers.get("retry-after");
26
+ } else if (typeof headers === "object") {
27
+ raw = headers["retry-after"] ?? headers["Retry-After"];
28
+ }
29
+ if (typeof raw !== "string") return null;
30
+ const trimmed = raw.trim();
31
+ if (!/^[0-9]+$/.test(trimmed)) return null;
32
+ const seconds = Number(trimmed);
33
+ if (seconds < 1 || seconds > 3600) return null;
34
+ return seconds;
35
+ }
36
+
21
37
  export async function astrisFetch({ key, baseUrl, path, method, body, fetchImpl }) {
22
38
  const url = baseUrl + path;
23
39
  const headers = { Authorization: `Bearer ${key}` };
@@ -54,14 +70,15 @@ export async function astrisFetch({ key, baseUrl, path, method, body, fetchImpl
54
70
  }
55
71
  swallow(reading);
56
72
  const text = await Promise.race([reading, timeout]);
73
+ const retryAfterSeconds = retryAfterSecondsOf(response?.headers);
57
74
 
58
75
  if (text === "") {
59
- return { status: response.status, body: null };
76
+ return { status: response.status, body: null, retryAfterSeconds };
60
77
  }
61
78
  try {
62
- return { status: response.status, body: JSON.parse(text) };
79
+ return { status: response.status, body: JSON.parse(text), retryAfterSeconds };
63
80
  } catch {
64
- return { status: response.status, body: null };
81
+ return { status: response.status, body: null, retryAfterSeconds };
65
82
  }
66
83
  } catch (err) {
67
84
  throw new Error(redact(failureMessage(err), key));
package/src/commands.mjs CHANGED
@@ -62,13 +62,22 @@ const HTTP_SENTENCE = new Map([
62
62
  [502, SENTENCE.platform],
63
63
  ]);
64
64
 
65
- function sentenceFor(status) {
65
+ function rateWaitSentence(seconds) {
66
+ if (typeof seconds === "number") {
67
+ return `This account asked for too many changes in a short time. Wait ${seconds} second${seconds === 1 ? "" : "s"}, then try again.`;
68
+ }
69
+ return "This account asked for too many changes in a short time. Wait a little, then try again.";
70
+ }
71
+
72
+ function sentenceFor(status, retryAfterSeconds = null) {
73
+ if (status === 429) return rateWaitSentence(retryAfterSeconds);
66
74
  return HTTP_SENTENCE.get(status) ?? SENTENCE.failed;
67
75
  }
68
76
 
69
- function errorMessage(status, body) {
77
+ function errorMessage(status, body, retryAfterSeconds = null) {
78
+ if (status === 429) return rateWaitSentence(retryAfterSeconds);
70
79
  if (isRecord(body) && typeof body.message === "string") return body.message;
71
- return sentenceFor(status);
80
+ return sentenceFor(status, retryAfterSeconds);
72
81
  }
73
82
 
74
83
  function upgradeMessage(status, body) {
@@ -78,6 +87,14 @@ function upgradeMessage(status, body) {
78
87
  return body.message;
79
88
  }
80
89
 
90
+ // T-275: the API pauses compose redeploy in the private preview and says why.
91
+ function composeUnavailableMessage(status, body) {
92
+ if (status !== 403 || !isRecord(body) || body.error !== "compose_unavailable" || typeof body.message !== "string") {
93
+ return null;
94
+ }
95
+ return body.message;
96
+ }
97
+
81
98
  function storageCreateMessage(status, body) {
82
99
  if (!isRecord(body)) return null;
83
100
  if (status === 403 && body.error === "bucket_limit") {
@@ -333,7 +350,7 @@ async function redeploy(io, key, json, appId) {
333
350
  method: "POST",
334
351
  });
335
352
  if (result.network) return fail(io, key, json, SENTENCE.platform);
336
- if (result.status !== 202) return fail(io, key, json, sentenceFor(result.status));
353
+ if (result.status !== 202) return fail(io, key, json, sentenceFor(result.status, result.retryAfterSeconds));
337
354
  const application = result.body?.application ?? {};
338
355
  const id = text(application.id) || appId;
339
356
  const name = text(application.name);
@@ -445,7 +462,7 @@ async function createThenDeploy(io, key, json, flags) {
445
462
  try {
446
463
  const listed = await call(io, key, { path: "/v1/projects", method: "GET" });
447
464
  if (listed.network) return fail(io, key, json, SENTENCE.platform, created);
448
- if (listed.status !== 200) return fail(io, key, json, sentenceFor(listed.status), created);
465
+ if (listed.status !== 200) return fail(io, key, json, sentenceFor(listed.status, listed.retryAfterSeconds), created);
449
466
  let projectId = findProjectId(listed.body, project);
450
467
  if (projectId === null) {
451
468
  const made = await call(io, key, {
@@ -456,7 +473,7 @@ async function createThenDeploy(io, key, json, flags) {
456
473
  if (made.network) return fail(io, key, json, SENTENCE.platform, created);
457
474
  const madeId = made.body?.project?.id;
458
475
  if (made.status !== 201 || typeof madeId !== "string" || madeId === "") {
459
- return fail(io, key, json, made.status === 201 ? SENTENCE.failed : sentenceFor(made.status), created);
476
+ return fail(io, key, json, made.status === 201 ? SENTENCE.failed : sentenceFor(made.status, made.retryAfterSeconds), created);
460
477
  }
461
478
  projectId = madeId;
462
479
  created.projectId = madeId;
@@ -484,7 +501,7 @@ async function createThenDeploy(io, key, json, flags) {
484
501
  io,
485
502
  key,
486
503
  json,
487
- passed ?? (madeApp.status === 201 ? SENTENCE.failed : sentenceFor(madeApp.status)),
504
+ passed ?? (madeApp.status === 201 ? SENTENCE.failed : sentenceFor(madeApp.status, madeApp.retryAfterSeconds)),
488
505
  created,
489
506
  );
490
507
  }
@@ -518,7 +535,7 @@ async function createThenDeploy(io, key, json, flags) {
518
535
  method: "POST",
519
536
  });
520
537
  if (deployed.network) return fail(io, key, json, SENTENCE.platform, created);
521
- if (deployed.status !== 202) return fail(io, key, json, sentenceFor(deployed.status), created);
538
+ if (deployed.status !== 202) return fail(io, key, json, sentenceFor(deployed.status, deployed.retryAfterSeconds), created);
522
539
  const shown = deployed.body?.application ?? application;
523
540
  const id = text(shown.id) || applicationId;
524
541
  const shownName = text(shown.name) || name;
@@ -577,7 +594,7 @@ export async function status(args, io) {
577
594
  method: "GET",
578
595
  });
579
596
  if (result.network) return fail(io, opened.key, opened.json, SENTENCE.platform);
580
- if (result.status !== 200) return fail(io, opened.key, opened.json, sentenceFor(result.status));
597
+ if (result.status !== 200) return fail(io, opened.key, opened.json, sentenceFor(result.status, result.retryAfterSeconds));
581
598
  const statusText = text(result.body?.status);
582
599
  const hostnames = hostnamesOf(result.body);
583
600
  const id = text(result.body?.application?.id) || required.appId;
@@ -603,7 +620,7 @@ async function hookUrl(io, key, json, appId) {
603
620
  method: "GET",
604
621
  });
605
622
  if (result.network) return fail(io, key, json, SENTENCE.platform);
606
- if (result.status !== 200) return fail(io, key, json, sentenceFor(result.status));
623
+ if (result.status !== 200) return fail(io, key, json, sentenceFor(result.status, result.retryAfterSeconds));
607
624
  const url = result.body?.hook?.url;
608
625
  if (typeof url !== "string") return fail(io, key, json, SENTENCE.failed);
609
626
  return succeed(io, key, json, { application_id: appId, url }, `${url}\n`);
@@ -617,7 +634,7 @@ async function envNames(io, key, json, appId) {
617
634
  method: "GET",
618
635
  });
619
636
  if (result.network) return fail(io, key, json, SENTENCE.platform);
620
- if (result.status !== 200) return fail(io, key, json, sentenceFor(result.status));
637
+ if (result.status !== 200) return fail(io, key, json, sentenceFor(result.status, result.retryAfterSeconds));
621
638
  const names = result.body?.names;
622
639
  if (!Array.isArray(names) || !names.every((name) => typeof name === "string")) {
623
640
  return fail(io, key, json, SENTENCE.failed);
@@ -645,7 +662,7 @@ export async function env(args, io) {
645
662
  body,
646
663
  });
647
664
  if (result.network) return fail(io, opened.key, opened.json, SENTENCE.platform);
648
- if (result.status !== 200) return fail(io, opened.key, opened.json, sentenceFor(result.status));
665
+ if (result.status !== 200) return fail(io, opened.key, opened.json, sentenceFor(result.status, result.retryAfterSeconds));
649
666
  return succeed(
650
667
  io,
651
668
  opened.key,
@@ -741,7 +758,7 @@ async function domainsBind(args, io, key) {
741
758
  }
742
759
  const result = await call(io, key, request);
743
760
  if (result.network) return fail(io, key, json, SENTENCE.platform);
744
- if (result.status !== 201) return fail(io, key, json, sentenceFor(result.status));
761
+ if (result.status !== 201) return fail(io, key, json, sentenceFor(result.status, result.retryAfterSeconds));
745
762
  const domain = isRecord(result.body) ? result.body.domain : undefined;
746
763
  const hostname = text(domain?.hostname);
747
764
  if (hostnameFlag === undefined) {
@@ -779,7 +796,7 @@ async function domainsVerify(args, io, key) {
779
796
  method: "POST",
780
797
  });
781
798
  if (result.network) return fail(io, key, json, SENTENCE.platform);
782
- if (result.status !== 200) return fail(io, key, json, sentenceFor(result.status));
799
+ if (result.status !== 200) return fail(io, key, json, sentenceFor(result.status, result.retryAfterSeconds));
783
800
  const body = isRecord(result.body) ? result.body : {};
784
801
  const domain = isRecord(body.domain) ? body.domain : {};
785
802
  const statusText = typeof domain.status === "string" ? domain.status : "";
@@ -812,7 +829,7 @@ export async function restart(args, io) {
812
829
  method: "POST",
813
830
  });
814
831
  if (result.network) return fail(io, opened.key, opened.json, SENTENCE.platform);
815
- if (result.status !== 200) return fail(io, opened.key, opened.json, sentenceFor(result.status));
832
+ if (result.status !== 200) return fail(io, opened.key, opened.json, sentenceFor(result.status, result.retryAfterSeconds));
816
833
  return succeed(
817
834
  io,
818
835
  opened.key,
@@ -863,7 +880,7 @@ export async function logs(args, io) {
863
880
  method: "GET",
864
881
  });
865
882
  if (result.network) return fail(io, loaded.key, json, SENTENCE.platform);
866
- if (result.status !== 200) return fail(io, loaded.key, json, sentenceFor(result.status));
883
+ if (result.status !== 200) return fail(io, loaded.key, json, sentenceFor(result.status, result.retryAfterSeconds));
867
884
  if (!isRecord(result.body) || typeof result.body.lines !== "string") {
868
885
  return fail(io, loaded.key, json, SENTENCE.failed);
869
886
  }
@@ -927,7 +944,7 @@ export async function list(args, io) {
927
944
  resource === "apps" ? "/v1/apps" : resource === "databases" ? "/v1/databases" : "/v1/projects";
928
945
  const result = await call(io, loaded.key, { path, method: "GET" });
929
946
  if (result.network) return fail(io, loaded.key, json, SENTENCE.platform);
930
- if (result.status !== 200) return fail(io, loaded.key, json, sentenceFor(result.status));
947
+ if (result.status !== 200) return fail(io, loaded.key, json, sentenceFor(result.status, result.retryAfterSeconds));
931
948
  if (resource !== "projects") {
932
949
  const human = listText(resource, result.body);
933
950
  if (human === null) return fail(io, loaded.key, json, SENTENCE.failed);
@@ -964,7 +981,7 @@ export async function usage(args, io) {
964
981
  const json = parsed.values.json === true;
965
982
  const result = await call(io, loaded.key, { path: "/v1/usage", method: "GET" });
966
983
  if (result.network) return fail(io, loaded.key, json, SENTENCE.platform);
967
- if (result.status !== 200) return fail(io, loaded.key, json, sentenceFor(result.status));
984
+ if (result.status !== 200) return fail(io, loaded.key, json, sentenceFor(result.status, result.retryAfterSeconds));
968
985
  const human = usageText(result.body);
969
986
  if (human === null) return fail(io, loaded.key, json, SENTENCE.failed);
970
987
  return succeed(io, loaded.key, json, result.body, human);
@@ -994,7 +1011,7 @@ export async function plans(args, io) {
994
1011
  const json = parsed.values.json === true;
995
1012
  const result = await call(io, loaded.key, { path: "/v1/plans", method: "GET" });
996
1013
  if (result.network) return fail(io, loaded.key, json, SENTENCE.platform);
997
- if (result.status !== 200) return fail(io, loaded.key, json, sentenceFor(result.status));
1014
+ if (result.status !== 200) return fail(io, loaded.key, json, sentenceFor(result.status, result.retryAfterSeconds));
998
1015
  const human = plansText(result.body);
999
1016
  if (human === null) return fail(io, loaded.key, json, SENTENCE.failed);
1000
1017
  return succeed(io, loaded.key, json, result.body, human);
@@ -1043,7 +1060,7 @@ export async function whoami(args, io) {
1043
1060
  const json = parsed.values.json === true;
1044
1061
  const result = await call(io, loaded.key, { path: "/v1/me", method: "GET" });
1045
1062
  if (result.network) return fail(io, loaded.key, json, SENTENCE.platform);
1046
- if (result.status !== 200) return fail(io, loaded.key, json, sentenceFor(result.status));
1063
+ if (result.status !== 200) return fail(io, loaded.key, json, sentenceFor(result.status, result.retryAfterSeconds));
1047
1064
  const view = whoamiView(result.body);
1048
1065
  if (view === null) return fail(io, loaded.key, json, SENTENCE.failed);
1049
1066
  const plan = view.account.plan === null ? "none" : view.account.plan;
@@ -1184,7 +1201,7 @@ export async function diagnose(args, io) {
1184
1201
  method: "GET",
1185
1202
  });
1186
1203
  if (result.network) return fail(io, loaded.key, json, SENTENCE.platform);
1187
- if (result.status !== 200) return fail(io, loaded.key, json, sentenceFor(result.status));
1204
+ if (result.status !== 200) return fail(io, loaded.key, json, sentenceFor(result.status, result.retryAfterSeconds));
1188
1205
  const human = diagnoseSentences(result.body);
1189
1206
  if (human === null) return fail(io, loaded.key, json, SENTENCE.failed);
1190
1207
  return succeed(io, loaded.key, json, result.body, human);
@@ -1212,7 +1229,7 @@ async function databaseCreate(args, io, key) {
1212
1229
  });
1213
1230
  if (result.network) return fail(io, key, json, SENTENCE.platform);
1214
1231
  if (result.status !== 201) {
1215
- return fail(io, key, json, upgradeMessage(result.status, result.body) ?? sentenceFor(result.status));
1232
+ return fail(io, key, json, upgradeMessage(result.status, result.body) ?? sentenceFor(result.status, result.retryAfterSeconds));
1216
1233
  }
1217
1234
  const view = databaseFields(isRecord(result.body) ? result.body.database : null);
1218
1235
  const password = isRecord(result.body) ? result.body.password : undefined;
@@ -1235,7 +1252,7 @@ async function databaseStatus(args, io, key) {
1235
1252
  method: "GET",
1236
1253
  });
1237
1254
  if (result.network) return fail(io, key, json, SENTENCE.platform);
1238
- if (result.status !== 200) return fail(io, key, json, sentenceFor(result.status));
1255
+ if (result.status !== 200) return fail(io, key, json, sentenceFor(result.status, result.retryAfterSeconds));
1239
1256
  const view = databaseFields(isRecord(result.body) ? result.body.database : null);
1240
1257
  const statusText = isRecord(result.body) ? result.body.status : undefined;
1241
1258
  if (view === null || typeof statusText !== "string") return fail(io, key, json, SENTENCE.failed);
@@ -1271,7 +1288,7 @@ async function databaseBackup(args, io, key) {
1271
1288
  });
1272
1289
  if (result.network) return fail(io, key, json, SENTENCE.platform);
1273
1290
  if (result.status === 409) return fail(io, key, json, SENTENCE.noBackup);
1274
- if (result.status !== 202) return fail(io, key, json, sentenceFor(result.status));
1291
+ if (result.status !== 202) return fail(io, key, json, sentenceFor(result.status, result.retryAfterSeconds));
1275
1292
  return succeed(io, key, json, { database_id: id, backup: "run" }, "Backup run.\n");
1276
1293
  }
1277
1294
 
@@ -1291,7 +1308,7 @@ async function databaseDelete(args, io, key) {
1291
1308
  method: "DELETE",
1292
1309
  });
1293
1310
  if (result.network) return fail(io, key, json, SENTENCE.platform);
1294
- if (result.status !== 204) return fail(io, key, json, sentenceFor(result.status));
1311
+ if (result.status !== 204) return fail(io, key, json, sentenceFor(result.status, result.retryAfterSeconds));
1295
1312
  return succeed(io, key, json, { database_id: id, removed: true }, "Database removed.\n");
1296
1313
  }
1297
1314
 
@@ -1398,7 +1415,7 @@ async function storageCreate(args, io, key) {
1398
1415
  if (refusal !== null) return fail(io, key, json, refusal);
1399
1416
  if (result.status === 503) return fail(io, key, json, SENTENCE.storageOff);
1400
1417
  if (result.status !== 201) {
1401
- return fail(io, key, json, upgradeMessage(result.status, result.body) ?? sentenceFor(result.status));
1418
+ return fail(io, key, json, upgradeMessage(result.status, result.body) ?? sentenceFor(result.status, result.retryAfterSeconds));
1402
1419
  }
1403
1420
  const body = isRecord(result.body) ? result.body : null;
1404
1421
  const view = body === null ? null : storageFields(body.bucket);
@@ -1440,7 +1457,7 @@ async function storageList(args, io, key) {
1440
1457
  const result = await call(io, key, { path: "/v1/buckets", method: "GET" });
1441
1458
  if (result.network) return fail(io, key, json, SENTENCE.platform);
1442
1459
  if (result.status === 503) return fail(io, key, json, SENTENCE.storageOff);
1443
- if (result.status !== 200) return fail(io, key, json, sentenceFor(result.status));
1460
+ if (result.status !== 200) return fail(io, key, json, sentenceFor(result.status, result.retryAfterSeconds));
1444
1461
  const items = isRecord(result.body) && Array.isArray(result.body.buckets) ? result.body.buckets : null;
1445
1462
  if (items === null) return fail(io, key, json, SENTENCE.failed);
1446
1463
  const views = [];
@@ -1467,7 +1484,7 @@ async function storageShow(args, io, key) {
1467
1484
  });
1468
1485
  if (result.network) return fail(io, key, json, SENTENCE.platform);
1469
1486
  if (result.status === 503) return fail(io, key, json, SENTENCE.storageOff);
1470
- if (result.status !== 200) return fail(io, key, json, sentenceFor(result.status));
1487
+ if (result.status !== 200) return fail(io, key, json, sentenceFor(result.status, result.retryAfterSeconds));
1471
1488
  const view = storageFields(isRecord(result.body) ? result.body.bucket : null);
1472
1489
  if (view === null) return fail(io, key, json, SENTENCE.failed);
1473
1490
  return succeed(io, key, json, { bucket: view }, `${storageLines(view).join("\n")}\n`);
@@ -1491,7 +1508,7 @@ async function storageDelete(args, io, key) {
1491
1508
  if (result.network) return fail(io, key, json, SENTENCE.platform);
1492
1509
  if (result.status === 409) return fail(io, key, json, SENTENCE.bucketNotEmpty);
1493
1510
  if (result.status === 503) return fail(io, key, json, SENTENCE.storageOff);
1494
- if (result.status !== 204) return fail(io, key, json, sentenceFor(result.status));
1511
+ if (result.status !== 204) return fail(io, key, json, sentenceFor(result.status, result.retryAfterSeconds));
1495
1512
  return succeed(io, key, json, { bucket_id: id, removed: true }, `${SENTENCE.bucketRemoved}\n`);
1496
1513
  }
1497
1514
 
@@ -1525,7 +1542,7 @@ export async function login(args, io) {
1525
1542
  if (key === "") return fail(io, "", false, SENTENCE.emptyKey);
1526
1543
  const result = await call(io, key, { path: "/v1/me", method: "GET" });
1527
1544
  if (result.network) return fail(io, key, false, SENTENCE.platform);
1528
- if (result.status !== 200) return fail(io, key, false, sentenceFor(result.status));
1545
+ if (result.status !== 200) return fail(io, key, false, sentenceFor(result.status, result.retryAfterSeconds));
1529
1546
  saveApiKey(io.env, key);
1530
1547
  io.stdout.write(`${SENTENCE.loggedIn}\n`);
1531
1548
  return { exitCode: 0 };
@@ -1579,7 +1596,7 @@ async function appRemove(args, io, key) {
1579
1596
 
1580
1597
  const got = await call(io, key, { path: `/v1/apps/${encodeURIComponent(appId)}`, method: "GET" });
1581
1598
  if (got.network) return fail(io, key, json, SENTENCE.platform);
1582
- if (got.status !== 200) return fail(io, key, json, sentenceFor(got.status));
1599
+ if (got.status !== 200) return fail(io, key, json, sentenceFor(got.status, got.retryAfterSeconds));
1583
1600
  if (!isRecord(got.body)) return fail(io, key, json, SENTENCE.failed);
1584
1601
  const application = isRecord(got.body.application) ? got.body.application : null;
1585
1602
  if (application === null) return fail(io, key, json, SENTENCE.failed);
@@ -1593,7 +1610,7 @@ async function appRemove(args, io, key) {
1593
1610
 
1594
1611
  const deleted = await call(io, key, { path: `/v1/apps/${encodeURIComponent(appId)}`, method: "DELETE" });
1595
1612
  if (deleted.network) return fail(io, key, json, SENTENCE.platform);
1596
- if (deleted.status !== 200) return fail(io, key, json, sentenceFor(deleted.status));
1613
+ if (deleted.status !== 200) return fail(io, key, json, sentenceFor(deleted.status, deleted.retryAfterSeconds));
1597
1614
  return succeed(io, key, json, { app_id: appId, removed: true }, `${SENTENCE.appRemoved}\n`);
1598
1615
  }
1599
1616
 
@@ -1751,7 +1768,7 @@ export async function preflight(args, io) {
1751
1768
  });
1752
1769
  if (result.network) return fail(io, loaded.key, json, SENTENCE.platform, undefined, 2);
1753
1770
  if (result.status < 200 || result.status >= 300) {
1754
- return fail(io, loaded.key, json, errorMessage(result.status, result.body), undefined, 2);
1771
+ return fail(io, loaded.key, json, errorMessage(result.status, result.body, result.retryAfterSeconds), undefined, 2);
1755
1772
  }
1756
1773
 
1757
1774
  if (
@@ -1817,7 +1834,7 @@ export async function accountExport(args, io) {
1817
1834
  });
1818
1835
  if (result.network) return fail(io, loaded.key, json, SENTENCE.platform, undefined, 2);
1819
1836
  if (result.status < 200 || result.status >= 300) {
1820
- return fail(io, loaded.key, json, errorMessage(result.status, result.body), undefined, 2);
1837
+ return fail(io, loaded.key, json, errorMessage(result.status, result.body, result.retryAfterSeconds), undefined, 2);
1821
1838
  }
1822
1839
 
1823
1840
  if (json) {
@@ -1843,7 +1860,7 @@ export async function next(args, io) {
1843
1860
  const json = parsed.values.json === true;
1844
1861
  const result = await call(io, loaded.key, { path: "/v1/me/next-step", method: "GET" });
1845
1862
  if (result.network) return fail(io, loaded.key, json, SENTENCE.platform);
1846
- if (result.status !== 200) return fail(io, loaded.key, json, sentenceFor(result.status));
1863
+ if (result.status !== 200) return fail(io, loaded.key, json, sentenceFor(result.status, result.retryAfterSeconds));
1847
1864
  const body = isRecord(result.body) ? result.body : null;
1848
1865
  const step = body?.step;
1849
1866
  const sentence = body?.sentence;
@@ -1950,7 +1967,7 @@ async function composeList(args, io, key) {
1950
1967
  const json = parsed.values.json === true;
1951
1968
  const result = await call(io, key, { path: "/v1/compose", method: "GET" });
1952
1969
  if (result.network) return fail(io, key, json, SENTENCE.platform);
1953
- if (result.status !== 200) return fail(io, key, json, sentenceFor(result.status));
1970
+ if (result.status !== 200) return fail(io, key, json, sentenceFor(result.status, result.retryAfterSeconds));
1954
1971
  const items = isRecord(result.body) && Array.isArray(result.body.compose) ? result.body.compose : null;
1955
1972
  if (items === null) return fail(io, key, json, SENTENCE.failed);
1956
1973
  const views = [];
@@ -1977,7 +1994,12 @@ async function composeDeploy(args, io, key) {
1977
1994
  });
1978
1995
  if (result.network) return fail(io, key, json, SENTENCE.platform);
1979
1996
  if (result.status !== 202) {
1980
- return fail(io, key, json, upgradeMessage(result.status, result.body) ?? sentenceFor(result.status));
1997
+ return fail(
1998
+ io,
1999
+ key,
2000
+ json,
2001
+ upgradeMessage(result.status, result.body) ?? composeUnavailableMessage(result.status, result.body) ?? sentenceFor(result.status, result.retryAfterSeconds),
2002
+ );
1981
2003
  }
1982
2004
  const view = composeFields(isRecord(result.body) ? result.body.compose : null);
1983
2005
  if (view === null) return fail(io, key, json, SENTENCE.failed);
@@ -1999,7 +2021,7 @@ async function composeStartStop(args, io, key, action) {
1999
2021
  });
2000
2022
  if (result.network) return fail(io, key, json, SENTENCE.platform);
2001
2023
  if (result.status !== 200) {
2002
- return fail(io, key, json, upgradeMessage(result.status, result.body) ?? sentenceFor(result.status));
2024
+ return fail(io, key, json, upgradeMessage(result.status, result.body) ?? sentenceFor(result.status, result.retryAfterSeconds));
2003
2025
  }
2004
2026
  const body = isRecord(result.body) ? result.body : null;
2005
2027
  const view = body === null ? null : composeFields(body.compose);