@astris-labs/cli 0.1.2 → 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 +23 -3
- package/package.json +1 -1
- package/src/client.mjs +20 -3
- package/src/commands.mjs +251 -34
- package/src/main.mjs +3 -1
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ Requests go to `https://api.astris.run`.
|
|
|
30
30
|
|
|
31
31
|
## Commands
|
|
32
32
|
|
|
33
|
-
v0 commands are `deploy`, `status`, `env`, `domains`, `restart`, `diagnose`, `database`, `storage`, `list`, `logs`, `usage`, `plans`, `whoami`, `login`, `logout`, `app`, `preflight`, and `export`.
|
|
33
|
+
v0 commands are `deploy`, `status`, `env`, `domains`, `restart`, `diagnose`, `database`, `storage`, `compose`, `list`, `logs`, `usage`, `plans`, `whoami`, `next`, `login`, `logout`, `app`, `preflight`, and `export`.
|
|
34
34
|
|
|
35
35
|
`--json` writes one JSON object to stdout. Without `--json`, success is text on stdout and an error is a sentence on stderr.
|
|
36
36
|
|
|
@@ -93,6 +93,18 @@ Optional flags are `--build-path`, `--dockerfile`, `--port`, `--build`, `--publi
|
|
|
93
93
|
|
|
94
94
|
`astris database delete --id <id> --yes` calls `DELETE /v1/databases/:id` and sends no body. Without `--yes` it makes no request and says the database and its data are removed and that `--yes` confirms. A repeated `--id` is refused and makes no request. Human output is `Database removed.` `--json` prints one JSON object `{"database_id":"<id>","removed":true}`.
|
|
95
95
|
|
|
96
|
+
### compose
|
|
97
|
+
|
|
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
|
+
|
|
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
|
+
|
|
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
|
+
|
|
104
|
+
`astris compose stop --id <id>` calls `POST /v1/compose/:id/stop` and sends no body. Human output matches start. `--json` prints one JSON object `{"compose":{...},"status":"<status>"}`.
|
|
105
|
+
|
|
106
|
+
Without `--id`, or with an id that is not a stack id, deploy, start, and stop make no request and say `Send --id set to the compose stack id.`
|
|
107
|
+
|
|
96
108
|
### storage
|
|
97
109
|
|
|
98
110
|
`astris storage create --name <name>` calls `POST /v1/buckets` with `name` after trim. Human output is the bucket id, the name, the endpoint, the bucket, the access key id, and the secret key, one value per line. The secret key is printed on this command only, so save it now. `--json` prints one JSON object of the response body: the bucket row and the credentials.
|
|
@@ -125,6 +137,10 @@ When storage is not configured, all four commands print `Storage is not availabl
|
|
|
125
137
|
|
|
126
138
|
`astris whoami` calls `GET /v1/me`. It takes no flags besides `--json`. `--json` prints the account id, email, status, plan, and created_at, and the key id, name, and key_prefix. Human output is three lines: `Account: <email>`, `Plan: <plan>` (`Plan: none` when plan is null), and `Key: <name> (<key_prefix>)`.
|
|
127
139
|
|
|
140
|
+
### next
|
|
141
|
+
|
|
142
|
+
`astris next` prints this account's next step as one sentence. `--json` prints `{step, sentence}`.
|
|
143
|
+
|
|
128
144
|
### login
|
|
129
145
|
|
|
130
146
|
`astris login` takes no key as an argument. On a terminal it asks `Paste your Astris API key:` and does not show what is typed. With a pipe it reads the key from stdin. When the API accepts the key, it writes `Logged in.`
|
|
@@ -160,18 +176,22 @@ Exit code 0 means the command succeeded. Any error exits with a non-zero code. W
|
|
|
160
176
|
|
|
161
177
|
Some arguments are refused on this machine, before any request.
|
|
162
178
|
|
|
163
|
-
The command name is checked first. The words `help`, `--help`, and `-h` print the usage and the command names, exit 0, and make no request. `deploy`, `status`, `env`, `domains`, `restart`, `logs`, `rollback`, `diagnose`, `database`, `storage`, `list`, `usage`, `plans`, `whoami`, `login`, `logout`, `app`, `preflight`, and `export` are known. Any other name writes `Unknown command.` and makes no request.
|
|
179
|
+
The command name is checked first. The words `help`, `--help`, and `-h` print the usage and the command names, exit 0, and make no request. `deploy`, `status`, `env`, `domains`, `restart`, `logs`, `rollback`, `diagnose`, `database`, `storage`, `compose`, `list`, `usage`, `plans`, `whoami`, `next`, `login`, `logout`, `app`, `preflight`, and `export` are known. Any other name writes `Unknown command.` and makes no request.
|
|
164
180
|
|
|
165
181
|
Login asks for the key on a terminal and reads it from stdin otherwise. Logout deletes that one file. Every other known command uses this sentence: When `ASTRIS_API_KEY` is set to a non-empty value, that value is the key, and the saved file is used only when the variable is missing or empty. A missing or empty variable and a missing or empty file write `ASTRIS_API_KEY is not set` to stderr and make no request. That is also true when `--json` is set.
|
|
166
182
|
|
|
167
183
|
`login` and `logout` take no arguments. An argument is refused, and `logout` does not delete the file.
|
|
168
184
|
|
|
169
|
-
After the key, `deploy`, `status`, `env`, `domains`, `restart`, `diagnose`, `database`, and `
|
|
185
|
+
After the key, `deploy`, `status`, `env`, `domains`, `restart`, `diagnose`, `database`, `storage`, `compose`, and `next` refuse a flag they do not know, a flag with no value, and a bare word that is not a flag. The sentence is `Those arguments were refused. Check the flags and send them again.`
|
|
170
186
|
|
|
171
187
|
`diagnose` requires `--app` set to an application id, the same check as `status`. `database` takes one word, `create`, `status`, `backup`, or `delete`. `create` requires `--project`, `--kind`, and `--name`. `--project` is a project id. `--kind` is exactly `postgres` or `redis`. `status` and `backup` each require `--id` set to a database id. `backup` is accepted. `delete` requires `--id` set to a database id and `--yes`, and a repeated `--id` is refused with `Those arguments were refused. Check the flags and send them again.` and makes no request. The plural `backups` is refused and makes no request.
|
|
172
188
|
|
|
173
189
|
`storage` takes one word, `create`, `list`, `show`, or `delete`. `create` requires `--name`. `show` and `delete` each require `--id` set to a bucket id, and a `--id` that is not a bucket id makes no request. `delete` requires `--yes`, and a repeated `--id` is refused with `Those arguments were refused. Check the flags and send them again.` and makes no request. `list` takes no word, so `storage list buckets` is refused and makes no request.
|
|
174
190
|
|
|
191
|
+
`compose` takes one word, `list`, `deploy`, `start`, or `stop`. `deploy`, `start`, and `stop` each require `--id` set to a compose stack id, and a repeated `--id` is refused with `Those arguments were refused. Check the flags and send them again.` and makes no request. `list` rejects `--id`.
|
|
192
|
+
|
|
193
|
+
`next` takes no word and no flag besides `--json`.
|
|
194
|
+
|
|
175
195
|
`domains` binds with no word, or verifies with the one word `verify`. `--hostname` with `--slug` is refused. An empty `--hostname`, or one containing a space, is refused before any request. Other hostname rules are left to the API. `verify` requires `--app` set to an application id and `--id` set to a domain id; a missing or non-UUID `--id`, and a word other than `verify`, make no request.
|
|
176
196
|
|
|
177
197
|
`--app` is refused unless the value, after trim, is an id of eight, four, four, four, and twelve hexadecimal digits, with hyphens, in either case. The sentence is `Send --app set to the application id.` `status`, `env`, `domains`, and `restart` require that id. `deploy` requires it when `--app` is present or when `--hook` is sent (`--hook` without `--app` is refused with `Send --app set to the application id.`), and then refuses `--build`, `--publish-directory`, `--spa`, `--kind`, `--command`, and `--schedule`; the other create flags are ignored.
|
package/package.json
CHANGED
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
|
@@ -16,6 +16,7 @@ const SENTENCE = {
|
|
|
16
16
|
projectId: "Send --project set to the project id.",
|
|
17
17
|
databaseFlags: "Those arguments were refused. Send --project, --kind, and --name.",
|
|
18
18
|
databaseId: "Send --id set to the database id.",
|
|
19
|
+
composeId: "Send --id set to the compose stack id.",
|
|
19
20
|
domainId: "Send --id set to the domain id.",
|
|
20
21
|
git: "The Git URL must not include a user name or password.",
|
|
21
22
|
// T-169: reused word for word from the MCP's SENTENCE.deployKey (T-139).
|
|
@@ -57,16 +58,26 @@ const HTTP_SENTENCE = new Map([
|
|
|
57
58
|
[401, SENTENCE.unauthorized],
|
|
58
59
|
[404, SENTENCE.notFound],
|
|
59
60
|
[409, SENTENCE.taken],
|
|
61
|
+
[429, "This account asked for too many changes in a short time. Wait a little, then try again."],
|
|
60
62
|
[502, SENTENCE.platform],
|
|
61
63
|
]);
|
|
62
64
|
|
|
63
|
-
function
|
|
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);
|
|
64
74
|
return HTTP_SENTENCE.get(status) ?? SENTENCE.failed;
|
|
65
75
|
}
|
|
66
76
|
|
|
67
|
-
function errorMessage(status, body) {
|
|
77
|
+
function errorMessage(status, body, retryAfterSeconds = null) {
|
|
78
|
+
if (status === 429) return rateWaitSentence(retryAfterSeconds);
|
|
68
79
|
if (isRecord(body) && typeof body.message === "string") return body.message;
|
|
69
|
-
return sentenceFor(status);
|
|
80
|
+
return sentenceFor(status, retryAfterSeconds);
|
|
70
81
|
}
|
|
71
82
|
|
|
72
83
|
function upgradeMessage(status, body) {
|
|
@@ -76,6 +87,14 @@ function upgradeMessage(status, body) {
|
|
|
76
87
|
return body.message;
|
|
77
88
|
}
|
|
78
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
|
+
|
|
79
98
|
function storageCreateMessage(status, body) {
|
|
80
99
|
if (!isRecord(body)) return null;
|
|
81
100
|
if (status === 403 && body.error === "bucket_limit") {
|
|
@@ -331,7 +350,7 @@ async function redeploy(io, key, json, appId) {
|
|
|
331
350
|
method: "POST",
|
|
332
351
|
});
|
|
333
352
|
if (result.network) return fail(io, key, json, SENTENCE.platform);
|
|
334
|
-
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));
|
|
335
354
|
const application = result.body?.application ?? {};
|
|
336
355
|
const id = text(application.id) || appId;
|
|
337
356
|
const name = text(application.name);
|
|
@@ -443,7 +462,7 @@ async function createThenDeploy(io, key, json, flags) {
|
|
|
443
462
|
try {
|
|
444
463
|
const listed = await call(io, key, { path: "/v1/projects", method: "GET" });
|
|
445
464
|
if (listed.network) return fail(io, key, json, SENTENCE.platform, created);
|
|
446
|
-
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);
|
|
447
466
|
let projectId = findProjectId(listed.body, project);
|
|
448
467
|
if (projectId === null) {
|
|
449
468
|
const made = await call(io, key, {
|
|
@@ -454,7 +473,7 @@ async function createThenDeploy(io, key, json, flags) {
|
|
|
454
473
|
if (made.network) return fail(io, key, json, SENTENCE.platform, created);
|
|
455
474
|
const madeId = made.body?.project?.id;
|
|
456
475
|
if (made.status !== 201 || typeof madeId !== "string" || madeId === "") {
|
|
457
|
-
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);
|
|
458
477
|
}
|
|
459
478
|
projectId = madeId;
|
|
460
479
|
created.projectId = madeId;
|
|
@@ -482,7 +501,7 @@ async function createThenDeploy(io, key, json, flags) {
|
|
|
482
501
|
io,
|
|
483
502
|
key,
|
|
484
503
|
json,
|
|
485
|
-
passed ?? (madeApp.status === 201 ? SENTENCE.failed : sentenceFor(madeApp.status)),
|
|
504
|
+
passed ?? (madeApp.status === 201 ? SENTENCE.failed : sentenceFor(madeApp.status, madeApp.retryAfterSeconds)),
|
|
486
505
|
created,
|
|
487
506
|
);
|
|
488
507
|
}
|
|
@@ -516,7 +535,7 @@ async function createThenDeploy(io, key, json, flags) {
|
|
|
516
535
|
method: "POST",
|
|
517
536
|
});
|
|
518
537
|
if (deployed.network) return fail(io, key, json, SENTENCE.platform, created);
|
|
519
|
-
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);
|
|
520
539
|
const shown = deployed.body?.application ?? application;
|
|
521
540
|
const id = text(shown.id) || applicationId;
|
|
522
541
|
const shownName = text(shown.name) || name;
|
|
@@ -575,7 +594,7 @@ export async function status(args, io) {
|
|
|
575
594
|
method: "GET",
|
|
576
595
|
});
|
|
577
596
|
if (result.network) return fail(io, opened.key, opened.json, SENTENCE.platform);
|
|
578
|
-
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));
|
|
579
598
|
const statusText = text(result.body?.status);
|
|
580
599
|
const hostnames = hostnamesOf(result.body);
|
|
581
600
|
const id = text(result.body?.application?.id) || required.appId;
|
|
@@ -601,7 +620,7 @@ async function hookUrl(io, key, json, appId) {
|
|
|
601
620
|
method: "GET",
|
|
602
621
|
});
|
|
603
622
|
if (result.network) return fail(io, key, json, SENTENCE.platform);
|
|
604
|
-
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));
|
|
605
624
|
const url = result.body?.hook?.url;
|
|
606
625
|
if (typeof url !== "string") return fail(io, key, json, SENTENCE.failed);
|
|
607
626
|
return succeed(io, key, json, { application_id: appId, url }, `${url}\n`);
|
|
@@ -615,7 +634,7 @@ async function envNames(io, key, json, appId) {
|
|
|
615
634
|
method: "GET",
|
|
616
635
|
});
|
|
617
636
|
if (result.network) return fail(io, key, json, SENTENCE.platform);
|
|
618
|
-
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));
|
|
619
638
|
const names = result.body?.names;
|
|
620
639
|
if (!Array.isArray(names) || !names.every((name) => typeof name === "string")) {
|
|
621
640
|
return fail(io, key, json, SENTENCE.failed);
|
|
@@ -643,7 +662,7 @@ export async function env(args, io) {
|
|
|
643
662
|
body,
|
|
644
663
|
});
|
|
645
664
|
if (result.network) return fail(io, opened.key, opened.json, SENTENCE.platform);
|
|
646
|
-
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));
|
|
647
666
|
return succeed(
|
|
648
667
|
io,
|
|
649
668
|
opened.key,
|
|
@@ -739,7 +758,7 @@ async function domainsBind(args, io, key) {
|
|
|
739
758
|
}
|
|
740
759
|
const result = await call(io, key, request);
|
|
741
760
|
if (result.network) return fail(io, key, json, SENTENCE.platform);
|
|
742
|
-
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));
|
|
743
762
|
const domain = isRecord(result.body) ? result.body.domain : undefined;
|
|
744
763
|
const hostname = text(domain?.hostname);
|
|
745
764
|
if (hostnameFlag === undefined) {
|
|
@@ -777,7 +796,7 @@ async function domainsVerify(args, io, key) {
|
|
|
777
796
|
method: "POST",
|
|
778
797
|
});
|
|
779
798
|
if (result.network) return fail(io, key, json, SENTENCE.platform);
|
|
780
|
-
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));
|
|
781
800
|
const body = isRecord(result.body) ? result.body : {};
|
|
782
801
|
const domain = isRecord(body.domain) ? body.domain : {};
|
|
783
802
|
const statusText = typeof domain.status === "string" ? domain.status : "";
|
|
@@ -810,7 +829,7 @@ export async function restart(args, io) {
|
|
|
810
829
|
method: "POST",
|
|
811
830
|
});
|
|
812
831
|
if (result.network) return fail(io, opened.key, opened.json, SENTENCE.platform);
|
|
813
|
-
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));
|
|
814
833
|
return succeed(
|
|
815
834
|
io,
|
|
816
835
|
opened.key,
|
|
@@ -861,7 +880,7 @@ export async function logs(args, io) {
|
|
|
861
880
|
method: "GET",
|
|
862
881
|
});
|
|
863
882
|
if (result.network) return fail(io, loaded.key, json, SENTENCE.platform);
|
|
864
|
-
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));
|
|
865
884
|
if (!isRecord(result.body) || typeof result.body.lines !== "string") {
|
|
866
885
|
return fail(io, loaded.key, json, SENTENCE.failed);
|
|
867
886
|
}
|
|
@@ -925,7 +944,7 @@ export async function list(args, io) {
|
|
|
925
944
|
resource === "apps" ? "/v1/apps" : resource === "databases" ? "/v1/databases" : "/v1/projects";
|
|
926
945
|
const result = await call(io, loaded.key, { path, method: "GET" });
|
|
927
946
|
if (result.network) return fail(io, loaded.key, json, SENTENCE.platform);
|
|
928
|
-
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));
|
|
929
948
|
if (resource !== "projects") {
|
|
930
949
|
const human = listText(resource, result.body);
|
|
931
950
|
if (human === null) return fail(io, loaded.key, json, SENTENCE.failed);
|
|
@@ -962,7 +981,7 @@ export async function usage(args, io) {
|
|
|
962
981
|
const json = parsed.values.json === true;
|
|
963
982
|
const result = await call(io, loaded.key, { path: "/v1/usage", method: "GET" });
|
|
964
983
|
if (result.network) return fail(io, loaded.key, json, SENTENCE.platform);
|
|
965
|
-
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));
|
|
966
985
|
const human = usageText(result.body);
|
|
967
986
|
if (human === null) return fail(io, loaded.key, json, SENTENCE.failed);
|
|
968
987
|
return succeed(io, loaded.key, json, result.body, human);
|
|
@@ -992,7 +1011,7 @@ export async function plans(args, io) {
|
|
|
992
1011
|
const json = parsed.values.json === true;
|
|
993
1012
|
const result = await call(io, loaded.key, { path: "/v1/plans", method: "GET" });
|
|
994
1013
|
if (result.network) return fail(io, loaded.key, json, SENTENCE.platform);
|
|
995
|
-
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));
|
|
996
1015
|
const human = plansText(result.body);
|
|
997
1016
|
if (human === null) return fail(io, loaded.key, json, SENTENCE.failed);
|
|
998
1017
|
return succeed(io, loaded.key, json, result.body, human);
|
|
@@ -1041,7 +1060,7 @@ export async function whoami(args, io) {
|
|
|
1041
1060
|
const json = parsed.values.json === true;
|
|
1042
1061
|
const result = await call(io, loaded.key, { path: "/v1/me", method: "GET" });
|
|
1043
1062
|
if (result.network) return fail(io, loaded.key, json, SENTENCE.platform);
|
|
1044
|
-
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));
|
|
1045
1064
|
const view = whoamiView(result.body);
|
|
1046
1065
|
if (view === null) return fail(io, loaded.key, json, SENTENCE.failed);
|
|
1047
1066
|
const plan = view.account.plan === null ? "none" : view.account.plan;
|
|
@@ -1182,7 +1201,7 @@ export async function diagnose(args, io) {
|
|
|
1182
1201
|
method: "GET",
|
|
1183
1202
|
});
|
|
1184
1203
|
if (result.network) return fail(io, loaded.key, json, SENTENCE.platform);
|
|
1185
|
-
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));
|
|
1186
1205
|
const human = diagnoseSentences(result.body);
|
|
1187
1206
|
if (human === null) return fail(io, loaded.key, json, SENTENCE.failed);
|
|
1188
1207
|
return succeed(io, loaded.key, json, result.body, human);
|
|
@@ -1210,7 +1229,7 @@ async function databaseCreate(args, io, key) {
|
|
|
1210
1229
|
});
|
|
1211
1230
|
if (result.network) return fail(io, key, json, SENTENCE.platform);
|
|
1212
1231
|
if (result.status !== 201) {
|
|
1213
|
-
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));
|
|
1214
1233
|
}
|
|
1215
1234
|
const view = databaseFields(isRecord(result.body) ? result.body.database : null);
|
|
1216
1235
|
const password = isRecord(result.body) ? result.body.password : undefined;
|
|
@@ -1233,7 +1252,7 @@ async function databaseStatus(args, io, key) {
|
|
|
1233
1252
|
method: "GET",
|
|
1234
1253
|
});
|
|
1235
1254
|
if (result.network) return fail(io, key, json, SENTENCE.platform);
|
|
1236
|
-
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));
|
|
1237
1256
|
const view = databaseFields(isRecord(result.body) ? result.body.database : null);
|
|
1238
1257
|
const statusText = isRecord(result.body) ? result.body.status : undefined;
|
|
1239
1258
|
if (view === null || typeof statusText !== "string") return fail(io, key, json, SENTENCE.failed);
|
|
@@ -1269,7 +1288,7 @@ async function databaseBackup(args, io, key) {
|
|
|
1269
1288
|
});
|
|
1270
1289
|
if (result.network) return fail(io, key, json, SENTENCE.platform);
|
|
1271
1290
|
if (result.status === 409) return fail(io, key, json, SENTENCE.noBackup);
|
|
1272
|
-
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));
|
|
1273
1292
|
return succeed(io, key, json, { database_id: id, backup: "run" }, "Backup run.\n");
|
|
1274
1293
|
}
|
|
1275
1294
|
|
|
@@ -1289,7 +1308,7 @@ async function databaseDelete(args, io, key) {
|
|
|
1289
1308
|
method: "DELETE",
|
|
1290
1309
|
});
|
|
1291
1310
|
if (result.network) return fail(io, key, json, SENTENCE.platform);
|
|
1292
|
-
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));
|
|
1293
1312
|
return succeed(io, key, json, { database_id: id, removed: true }, "Database removed.\n");
|
|
1294
1313
|
}
|
|
1295
1314
|
|
|
@@ -1396,7 +1415,7 @@ async function storageCreate(args, io, key) {
|
|
|
1396
1415
|
if (refusal !== null) return fail(io, key, json, refusal);
|
|
1397
1416
|
if (result.status === 503) return fail(io, key, json, SENTENCE.storageOff);
|
|
1398
1417
|
if (result.status !== 201) {
|
|
1399
|
-
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));
|
|
1400
1419
|
}
|
|
1401
1420
|
const body = isRecord(result.body) ? result.body : null;
|
|
1402
1421
|
const view = body === null ? null : storageFields(body.bucket);
|
|
@@ -1438,7 +1457,7 @@ async function storageList(args, io, key) {
|
|
|
1438
1457
|
const result = await call(io, key, { path: "/v1/buckets", method: "GET" });
|
|
1439
1458
|
if (result.network) return fail(io, key, json, SENTENCE.platform);
|
|
1440
1459
|
if (result.status === 503) return fail(io, key, json, SENTENCE.storageOff);
|
|
1441
|
-
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));
|
|
1442
1461
|
const items = isRecord(result.body) && Array.isArray(result.body.buckets) ? result.body.buckets : null;
|
|
1443
1462
|
if (items === null) return fail(io, key, json, SENTENCE.failed);
|
|
1444
1463
|
const views = [];
|
|
@@ -1465,7 +1484,7 @@ async function storageShow(args, io, key) {
|
|
|
1465
1484
|
});
|
|
1466
1485
|
if (result.network) return fail(io, key, json, SENTENCE.platform);
|
|
1467
1486
|
if (result.status === 503) return fail(io, key, json, SENTENCE.storageOff);
|
|
1468
|
-
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));
|
|
1469
1488
|
const view = storageFields(isRecord(result.body) ? result.body.bucket : null);
|
|
1470
1489
|
if (view === null) return fail(io, key, json, SENTENCE.failed);
|
|
1471
1490
|
return succeed(io, key, json, { bucket: view }, `${storageLines(view).join("\n")}\n`);
|
|
@@ -1489,7 +1508,7 @@ async function storageDelete(args, io, key) {
|
|
|
1489
1508
|
if (result.network) return fail(io, key, json, SENTENCE.platform);
|
|
1490
1509
|
if (result.status === 409) return fail(io, key, json, SENTENCE.bucketNotEmpty);
|
|
1491
1510
|
if (result.status === 503) return fail(io, key, json, SENTENCE.storageOff);
|
|
1492
|
-
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));
|
|
1493
1512
|
return succeed(io, key, json, { bucket_id: id, removed: true }, `${SENTENCE.bucketRemoved}\n`);
|
|
1494
1513
|
}
|
|
1495
1514
|
|
|
@@ -1523,7 +1542,7 @@ export async function login(args, io) {
|
|
|
1523
1542
|
if (key === "") return fail(io, "", false, SENTENCE.emptyKey);
|
|
1524
1543
|
const result = await call(io, key, { path: "/v1/me", method: "GET" });
|
|
1525
1544
|
if (result.network) return fail(io, key, false, SENTENCE.platform);
|
|
1526
|
-
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));
|
|
1527
1546
|
saveApiKey(io.env, key);
|
|
1528
1547
|
io.stdout.write(`${SENTENCE.loggedIn}\n`);
|
|
1529
1548
|
return { exitCode: 0 };
|
|
@@ -1577,7 +1596,7 @@ async function appRemove(args, io, key) {
|
|
|
1577
1596
|
|
|
1578
1597
|
const got = await call(io, key, { path: `/v1/apps/${encodeURIComponent(appId)}`, method: "GET" });
|
|
1579
1598
|
if (got.network) return fail(io, key, json, SENTENCE.platform);
|
|
1580
|
-
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));
|
|
1581
1600
|
if (!isRecord(got.body)) return fail(io, key, json, SENTENCE.failed);
|
|
1582
1601
|
const application = isRecord(got.body.application) ? got.body.application : null;
|
|
1583
1602
|
if (application === null) return fail(io, key, json, SENTENCE.failed);
|
|
@@ -1591,7 +1610,7 @@ async function appRemove(args, io, key) {
|
|
|
1591
1610
|
|
|
1592
1611
|
const deleted = await call(io, key, { path: `/v1/apps/${encodeURIComponent(appId)}`, method: "DELETE" });
|
|
1593
1612
|
if (deleted.network) return fail(io, key, json, SENTENCE.platform);
|
|
1594
|
-
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));
|
|
1595
1614
|
return succeed(io, key, json, { app_id: appId, removed: true }, `${SENTENCE.appRemoved}\n`);
|
|
1596
1615
|
}
|
|
1597
1616
|
|
|
@@ -1749,7 +1768,7 @@ export async function preflight(args, io) {
|
|
|
1749
1768
|
});
|
|
1750
1769
|
if (result.network) return fail(io, loaded.key, json, SENTENCE.platform, undefined, 2);
|
|
1751
1770
|
if (result.status < 200 || result.status >= 300) {
|
|
1752
|
-
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);
|
|
1753
1772
|
}
|
|
1754
1773
|
|
|
1755
1774
|
if (
|
|
@@ -1815,7 +1834,7 @@ export async function accountExport(args, io) {
|
|
|
1815
1834
|
});
|
|
1816
1835
|
if (result.network) return fail(io, loaded.key, json, SENTENCE.platform, undefined, 2);
|
|
1817
1836
|
if (result.status < 200 || result.status >= 300) {
|
|
1818
|
-
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);
|
|
1819
1838
|
}
|
|
1820
1839
|
|
|
1821
1840
|
if (json) {
|
|
@@ -1826,3 +1845,201 @@ export async function accountExport(args, io) {
|
|
|
1826
1845
|
writeStdout(io, loaded.key, `${SENTENCE.exportAccepted}\n`);
|
|
1827
1846
|
return { exitCode: 0 };
|
|
1828
1847
|
}
|
|
1848
|
+
|
|
1849
|
+
const NEXT_OPTIONS = {
|
|
1850
|
+
json: { type: "boolean" },
|
|
1851
|
+
};
|
|
1852
|
+
|
|
1853
|
+
export async function next(args, io) {
|
|
1854
|
+
const loaded = loadKey(io);
|
|
1855
|
+
if (loaded.exit) return loaded.exit;
|
|
1856
|
+
const parsed = parseStrict(args, NEXT_OPTIONS, false);
|
|
1857
|
+
if (parsed === null || parsed.positionals.length > 0) {
|
|
1858
|
+
return fail(io, loaded.key, wantsJson(args), SENTENCE.refused);
|
|
1859
|
+
}
|
|
1860
|
+
const json = parsed.values.json === true;
|
|
1861
|
+
const result = await call(io, loaded.key, { path: "/v1/me/next-step", method: "GET" });
|
|
1862
|
+
if (result.network) return fail(io, loaded.key, json, SENTENCE.platform);
|
|
1863
|
+
if (result.status !== 200) return fail(io, loaded.key, json, sentenceFor(result.status, result.retryAfterSeconds));
|
|
1864
|
+
const body = isRecord(result.body) ? result.body : null;
|
|
1865
|
+
const step = body?.step;
|
|
1866
|
+
const sentence = body?.sentence;
|
|
1867
|
+
if (typeof step !== "string" || typeof sentence !== "string") {
|
|
1868
|
+
return fail(io, loaded.key, json, SENTENCE.failed);
|
|
1869
|
+
}
|
|
1870
|
+
if (json) return succeed(io, loaded.key, json, { step, sentence }, "");
|
|
1871
|
+
return succeed(io, loaded.key, json, { step, sentence }, `${sentence}\n`);
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
const COMPOSE_LIST_OPTIONS = {
|
|
1875
|
+
json: { type: "boolean" },
|
|
1876
|
+
};
|
|
1877
|
+
|
|
1878
|
+
const COMPOSE_ACTION_OPTIONS = {
|
|
1879
|
+
json: { type: "boolean" },
|
|
1880
|
+
id: { type: "string", multiple: true },
|
|
1881
|
+
};
|
|
1882
|
+
|
|
1883
|
+
const COMPOSE_UNION = {
|
|
1884
|
+
json: { type: "boolean" },
|
|
1885
|
+
id: { type: "string" },
|
|
1886
|
+
};
|
|
1887
|
+
|
|
1888
|
+
function composePositionals(args) {
|
|
1889
|
+
try {
|
|
1890
|
+
return parseArgs({
|
|
1891
|
+
args: Array.isArray(args) ? args : [],
|
|
1892
|
+
strict: false,
|
|
1893
|
+
allowPositionals: true,
|
|
1894
|
+
options: COMPOSE_UNION,
|
|
1895
|
+
}).positionals;
|
|
1896
|
+
} catch {
|
|
1897
|
+
return null;
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
function composeFields(value) {
|
|
1902
|
+
if (!isRecord(value)) return null;
|
|
1903
|
+
const id = value.id;
|
|
1904
|
+
const projectId = value.project_id;
|
|
1905
|
+
const name = value.name;
|
|
1906
|
+
const gitUrl = value.git_url;
|
|
1907
|
+
const branch = value.branch;
|
|
1908
|
+
const composePath = value.compose_path;
|
|
1909
|
+
const memoryLimit = value.memory_limit;
|
|
1910
|
+
const createdAt = value.created_at;
|
|
1911
|
+
const updatedAt = value.updated_at;
|
|
1912
|
+
if (typeof id !== "string" || typeof projectId !== "string" || typeof name !== "string") return null;
|
|
1913
|
+
if (typeof gitUrl !== "string" || typeof branch !== "string" || typeof composePath !== "string") return null;
|
|
1914
|
+
if (typeof createdAt !== "string" || typeof updatedAt !== "string") return null;
|
|
1915
|
+
if (typeof memoryLimit !== "number" || !Number.isInteger(memoryLimit)) return null;
|
|
1916
|
+
const view = {
|
|
1917
|
+
id,
|
|
1918
|
+
project_id: projectId,
|
|
1919
|
+
name,
|
|
1920
|
+
git_url: gitUrl,
|
|
1921
|
+
branch,
|
|
1922
|
+
compose_path: composePath,
|
|
1923
|
+
memory_limit: memoryLimit,
|
|
1924
|
+
created_at: createdAt,
|
|
1925
|
+
updated_at: updatedAt,
|
|
1926
|
+
};
|
|
1927
|
+
const projectName = value.project_name;
|
|
1928
|
+
if (typeof projectName === "string") {
|
|
1929
|
+
return { ...view, project_name: projectName };
|
|
1930
|
+
}
|
|
1931
|
+
return view;
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
function composeLines(view) {
|
|
1935
|
+
return [
|
|
1936
|
+
view.id,
|
|
1937
|
+
view.project_id,
|
|
1938
|
+
view.name,
|
|
1939
|
+
view.git_url,
|
|
1940
|
+
view.branch,
|
|
1941
|
+
view.compose_path,
|
|
1942
|
+
String(view.memory_limit),
|
|
1943
|
+
view.created_at,
|
|
1944
|
+
view.updated_at,
|
|
1945
|
+
];
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
function composeListLine(view) {
|
|
1949
|
+
const base = `${view.id} ${view.name}`;
|
|
1950
|
+
return typeof view.project_name === "string" ? `${base} ${view.project_name}` : base;
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
function requireComposeId(io, key, json, rawId) {
|
|
1954
|
+
const ids = Array.isArray(rawId) ? rawId : rawId === undefined ? [] : [rawId];
|
|
1955
|
+
if (ids.length > 1) return { exit: fail(io, key, json, SENTENCE.refused) };
|
|
1956
|
+
const id = appUuid(ids[0]);
|
|
1957
|
+
if (id === null) return { exit: fail(io, key, json, SENTENCE.composeId) };
|
|
1958
|
+
return { id };
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
async function composeList(args, io, key) {
|
|
1962
|
+
const parsed = parseStrict(args, COMPOSE_LIST_OPTIONS, true);
|
|
1963
|
+
if (parsed === null || parsed.positionals.length !== 1 || parsed.positionals[0] !== "list") {
|
|
1964
|
+
return fail(io, key, wantsJson(args), SENTENCE.refused);
|
|
1965
|
+
}
|
|
1966
|
+
if (parsed.values.id !== undefined) return fail(io, key, wantsJson(args), SENTENCE.refused);
|
|
1967
|
+
const json = parsed.values.json === true;
|
|
1968
|
+
const result = await call(io, key, { path: "/v1/compose", method: "GET" });
|
|
1969
|
+
if (result.network) return fail(io, key, json, SENTENCE.platform);
|
|
1970
|
+
if (result.status !== 200) return fail(io, key, json, sentenceFor(result.status, result.retryAfterSeconds));
|
|
1971
|
+
const items = isRecord(result.body) && Array.isArray(result.body.compose) ? result.body.compose : null;
|
|
1972
|
+
if (items === null) return fail(io, key, json, SENTENCE.failed);
|
|
1973
|
+
const views = [];
|
|
1974
|
+
for (const item of items) {
|
|
1975
|
+
const row = composeFields(item);
|
|
1976
|
+
if (row === null) return fail(io, key, json, SENTENCE.failed);
|
|
1977
|
+
views.push(row);
|
|
1978
|
+
}
|
|
1979
|
+
const human = views.length === 0 ? "" : `${views.map((row) => composeListLine(row)).join("\n")}\n`;
|
|
1980
|
+
return succeed(io, key, json, { compose: views }, human);
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
async function composeDeploy(args, io, key) {
|
|
1984
|
+
const parsed = parseStrict(args, COMPOSE_ACTION_OPTIONS, true);
|
|
1985
|
+
if (parsed === null || parsed.positionals.length !== 1 || parsed.positionals[0] !== "deploy") {
|
|
1986
|
+
return fail(io, key, wantsJson(args), SENTENCE.refused);
|
|
1987
|
+
}
|
|
1988
|
+
const json = parsed.values.json === true;
|
|
1989
|
+
const required = requireComposeId(io, key, json, parsed.values.id);
|
|
1990
|
+
if (required.exit) return required.exit;
|
|
1991
|
+
const result = await call(io, key, {
|
|
1992
|
+
path: `/v1/compose/${encodeURIComponent(required.id)}/deploy`,
|
|
1993
|
+
method: "POST",
|
|
1994
|
+
});
|
|
1995
|
+
if (result.network) return fail(io, key, json, SENTENCE.platform);
|
|
1996
|
+
if (result.status !== 202) {
|
|
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
|
+
);
|
|
2003
|
+
}
|
|
2004
|
+
const view = composeFields(isRecord(result.body) ? result.body.compose : null);
|
|
2005
|
+
if (view === null) return fail(io, key, json, SENTENCE.failed);
|
|
2006
|
+
const payload = { compose: view };
|
|
2007
|
+
return succeed(io, key, json, payload, `${composeLines(view).join("\n")}\n`);
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
async function composeStartStop(args, io, key, action) {
|
|
2011
|
+
const parsed = parseStrict(args, COMPOSE_ACTION_OPTIONS, true);
|
|
2012
|
+
if (parsed === null || parsed.positionals.length !== 1 || parsed.positionals[0] !== action) {
|
|
2013
|
+
return fail(io, key, wantsJson(args), SENTENCE.refused);
|
|
2014
|
+
}
|
|
2015
|
+
const json = parsed.values.json === true;
|
|
2016
|
+
const required = requireComposeId(io, key, json, parsed.values.id);
|
|
2017
|
+
if (required.exit) return required.exit;
|
|
2018
|
+
const result = await call(io, key, {
|
|
2019
|
+
path: `/v1/compose/${encodeURIComponent(required.id)}/${action}`,
|
|
2020
|
+
method: "POST",
|
|
2021
|
+
});
|
|
2022
|
+
if (result.network) return fail(io, key, json, SENTENCE.platform);
|
|
2023
|
+
if (result.status !== 200) {
|
|
2024
|
+
return fail(io, key, json, upgradeMessage(result.status, result.body) ?? sentenceFor(result.status, result.retryAfterSeconds));
|
|
2025
|
+
}
|
|
2026
|
+
const body = isRecord(result.body) ? result.body : null;
|
|
2027
|
+
const view = body === null ? null : composeFields(body.compose);
|
|
2028
|
+
const statusText = body?.status;
|
|
2029
|
+
if (view === null || typeof statusText !== "string") return fail(io, key, json, SENTENCE.failed);
|
|
2030
|
+
const payload = { compose: view, status: statusText };
|
|
2031
|
+
const lines = [statusText, ...composeLines(view)];
|
|
2032
|
+
return succeed(io, key, json, payload, `${lines.join("\n")}\n`);
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
export async function compose(args, io) {
|
|
2036
|
+
const loaded = loadKey(io);
|
|
2037
|
+
if (loaded.exit) return loaded.exit;
|
|
2038
|
+
const words = composePositionals(args);
|
|
2039
|
+
if (words === null || words.length !== 1) return fail(io, loaded.key, wantsJson(args), SENTENCE.refused);
|
|
2040
|
+
if (words[0] === "list") return composeList(args, io, loaded.key);
|
|
2041
|
+
if (words[0] === "deploy") return composeDeploy(args, io, loaded.key);
|
|
2042
|
+
if (words[0] === "start") return composeStartStop(args, io, loaded.key, "start");
|
|
2043
|
+
if (words[0] === "stop") return composeStartStop(args, io, loaded.key, "stop");
|
|
2044
|
+
return fail(io, loaded.key, wantsJson(args), SENTENCE.refused);
|
|
2045
|
+
}
|
package/src/main.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { createInterface } from "node:readline";
|
|
|
4
4
|
import { Writable } from "node:stream";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { DEFAULT_BASE_URL } from "./client.mjs";
|
|
7
|
-
import { accountExport, app, database, deploy, diagnose, domains, env, list, login, logout, logs, plans, preflight, restart, rollback, status, storage, usage, whoami } from "./commands.mjs";
|
|
7
|
+
import { accountExport, app, compose, database, deploy, diagnose, domains, env, list, login, logout, logs, next, plans, preflight, restart, rollback, status, storage, usage, whoami } from "./commands.mjs";
|
|
8
8
|
|
|
9
9
|
const PROMPT = "Paste your Astris API key: ";
|
|
10
10
|
|
|
@@ -48,6 +48,7 @@ const handlers = new Map([
|
|
|
48
48
|
["diagnose", diagnose],
|
|
49
49
|
["database", database],
|
|
50
50
|
["storage", storage],
|
|
51
|
+
["compose", compose],
|
|
51
52
|
["env", env],
|
|
52
53
|
["domains", domains],
|
|
53
54
|
["restart", restart],
|
|
@@ -56,6 +57,7 @@ const handlers = new Map([
|
|
|
56
57
|
["usage", usage],
|
|
57
58
|
["plans", plans],
|
|
58
59
|
["whoami", whoami],
|
|
60
|
+
["next", next],
|
|
59
61
|
["rollback", rollback],
|
|
60
62
|
["login", login],
|
|
61
63
|
["logout", logout],
|