@astris-labs/cli 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +165 -2
- package/package.json +24 -4
- package/src/client.mjs +71 -0
- package/src/commands.mjs +1515 -0
- package/src/key.mjs +126 -0
- package/src/main.mjs +50 -0
package/README.md
CHANGED
|
@@ -1,3 +1,166 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Astris CLI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`astris` is a command-line client of the Astris API. The package name is `@astris-labs/cli`. The bin command is `astris`.
|
|
4
|
+
|
|
5
|
+
Run it with npx: `npx @astris-labs/cli`. It needs Node 22 or later.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Run the client with npx: `npx @astris-labs/cli`. To keep it installed, run `npm install --global @astris-labs/cli`. The command is `astris`.
|
|
10
|
+
|
|
11
|
+
From this repository, run `pnpm install` at the root. Then run the client with `node cli/src/main.mjs`. The package `bin` name is `astris`, which is `cli/src/main.mjs`.
|
|
12
|
+
|
|
13
|
+
## API key
|
|
14
|
+
|
|
15
|
+
`astris login` reads the key from stdin and saves it only after the API accepts it. `astris logout` deletes that one file.
|
|
16
|
+
|
|
17
|
+
The file name is `key`, in a directory named `astris`.
|
|
18
|
+
|
|
19
|
+
- Linux, and any other system that is not macOS or Windows: `$XDG_CONFIG_HOME/astris/key` when `XDG_CONFIG_HOME` is a non-empty absolute path. Otherwise `$HOME/.config/astris/key` when `HOME` is a non-empty absolute path. A relative `XDG_CONFIG_HOME` is ignored.
|
|
20
|
+
- macOS: `$HOME/Library/Application Support/astris/key` when `HOME` is a non-empty absolute path.
|
|
21
|
+
- Windows: `%APPDATA%\astris\key` when `APPDATA` is a non-empty absolute path. Otherwise `%USERPROFILE%\AppData\Roaming\astris\key` when `USERPROFILE` is a non-empty absolute path.
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
export ASTRIS_API_KEY=astris_example_not_a_real_key
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`astris_example_not_a_real_key` is a placeholder. Put your current Astris API key in that variable.
|
|
28
|
+
|
|
29
|
+
Requests go to `https://api.astris.run`.
|
|
30
|
+
|
|
31
|
+
## Commands
|
|
32
|
+
|
|
33
|
+
v0 commands are `deploy`, `status`, `env`, `domains`, `restart`, `diagnose`, `database`, `storage`, `list`, `logs`, `usage`, `plans`, `whoami`, `login`, and `logout`.
|
|
34
|
+
|
|
35
|
+
`--json` writes one JSON object to stdout. Without `--json`, success is text on stdout and an error is a sentence on stderr.
|
|
36
|
+
|
|
37
|
+
### deploy
|
|
38
|
+
|
|
39
|
+
`--app <id>` redeploys that application. The value is the application id.
|
|
40
|
+
|
|
41
|
+
To create a project and an application, then queue a deploy, pass these flags. The names are kebab-case.
|
|
42
|
+
|
|
43
|
+
- `--project`
|
|
44
|
+
- `--name`
|
|
45
|
+
- `--git-url`
|
|
46
|
+
- `--branch`
|
|
47
|
+
|
|
48
|
+
Optional flags are `--build-path`, `--dockerfile`, `--port`, `--build`, `--publish-directory`, `--spa`, `--kind`, `--command`, and `--schedule`.
|
|
49
|
+
|
|
50
|
+
`--build nixpacks` builds with Nixpacks. `--build dockerfile` builds from the Dockerfile named by `--dockerfile`; without `--build`, a `--dockerfile` value alone means a Dockerfile build. `--build static` serves static files; with it, `--publish-directory <dir>` names the directory to publish, relative to the build, and `--spa` serves it as a single-page application. `--spa` takes no value, and `--spa=false` is refused.
|
|
51
|
+
|
|
52
|
+
`--kind web` is the default kind. `--kind worker` runs `--command` as the container's process. `--kind cron` runs `--command` on `--schedule`, five space-separated fields. A worker or a cron app cannot use `--build static`. `--app` with any of `--build`, `--publish-directory`, `--spa`, `--kind`, `--command`, or `--schedule` is refused and makes no request; the create flags `--project`, `--name`, `--git-url`, `--branch`, `--port`, `--dockerfile`, and `--build-path` are still ignored with `--app`.
|
|
53
|
+
|
|
54
|
+
### status
|
|
55
|
+
|
|
56
|
+
`astris status --app <id>` prints the status and the hostnames.
|
|
57
|
+
|
|
58
|
+
### env
|
|
59
|
+
|
|
60
|
+
`astris env --app <id>` reads one JSON object from stdin and replaces the whole variable set.
|
|
61
|
+
|
|
62
|
+
`astris env --app <id> --names` prints the names of the application's variables, one per line, and never a value. `--json` prints `{application_id, names}`. It reads no stdin. The runner still waits for stdin to close when stdin is not a terminal, so close it when nothing is piped in, for example with `< /dev/null`.
|
|
63
|
+
|
|
64
|
+
### domains
|
|
65
|
+
|
|
66
|
+
`astris domains --app <id>` binds one `*.astris.live` hostname. `--slug <slug>` sends that slug. With no `--slug`, the route uses the application slug.
|
|
67
|
+
|
|
68
|
+
`astris domains --app <id> --hostname <host>` binds the customer's own hostname. It sends `{kind:"custom", hostname}` and prints the hostname, its status, the CNAME record to set (name, then target), and the TXT record to set (name, then value), one per line. `--hostname` together with `--slug` is refused.
|
|
69
|
+
|
|
70
|
+
`astris domains verify --app <id> --id <domainId>` asks Astris to check the DNS records. It prints the status the API returned, and the reason on the next line when the check failed. A missing or malformed `--id` makes no request.
|
|
71
|
+
|
|
72
|
+
### restart
|
|
73
|
+
|
|
74
|
+
`astris restart --app <id>` restarts the application.
|
|
75
|
+
|
|
76
|
+
### diagnose
|
|
77
|
+
|
|
78
|
+
`astris diagnose --app <id>` calls `GET /v1/apps/:id/diagnostics`. Human output is sentences only. When `diagnosis` is null, it prints `note`. Otherwise it prints `diagnosis.reason`, then `diagnosis.next_step` when that value is a string. `--json` prints one JSON object of the response body.
|
|
79
|
+
|
|
80
|
+
### database
|
|
81
|
+
|
|
82
|
+
`astris database create --project <id> --kind <kind> --name <name>` calls `POST /v1/projects/:id/databases` with `kind` and `name`. `kind` is `postgres` or `redis`. The password is printed on this command only.
|
|
83
|
+
|
|
84
|
+
`astris database status --id <id>` calls `GET /v1/databases/:id`. It does not print the password.
|
|
85
|
+
|
|
86
|
+
`astris database backup --id <id>` calls `POST /v1/databases/:id/backups` and sends no body. Human output is `Backup run.` `--json` prints one JSON object `{"database_id":"<id>","backup":"run"}`.
|
|
87
|
+
|
|
88
|
+
`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}`.
|
|
89
|
+
|
|
90
|
+
### storage
|
|
91
|
+
|
|
92
|
+
`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.
|
|
93
|
+
|
|
94
|
+
`astris storage list` calls `GET /v1/buckets`. Human output is one line per bucket in the order the API returned: the id, the name, and the status. An empty list prints nothing. `--json` prints one JSON object `{"buckets":[...]}` of the bucket rows.
|
|
95
|
+
|
|
96
|
+
`astris storage show --id <id>` calls `GET /v1/buckets/:id`. Human output is the id, the name, the endpoint, the bucket, the access key id, the status, the created_at, and the updated_at, one per line; a missing access key id skips its line. It never prints the secret key. `--json` prints one JSON object `{"bucket":{...}}`.
|
|
97
|
+
|
|
98
|
+
`astris storage delete --id <id> --yes` calls `DELETE /v1/buckets/:id` and sends no body. Without `--yes` it makes no request and says the bucket and its objects are removed and that `--yes` confirms. A repeated `--id` is refused and makes no request. A bucket that still holds objects is refused with `That bucket is not empty. Remove its objects, then try again.` Human output is `Bucket removed.` `--json` prints one JSON object `{"bucket_id":"<id>","removed":true}`.
|
|
99
|
+
|
|
100
|
+
When storage is not configured, all four commands print `Storage is not available right now. Try again later.` When the account has reached its bucket limit, `create` prints the API's message, `This account has reached its bucket safety limit. Contact support.` When new buckets cannot be made at all, `create` prints the API's message, `New storage buckets are not available right now.` Other refusals reuse the shared sentences below, and on a plan refusal `create` prints the API's own message.
|
|
101
|
+
|
|
102
|
+
### list
|
|
103
|
+
|
|
104
|
+
`astris list apps` calls `GET /v1/apps`. `astris list databases` calls `GET /v1/databases`. `astris list projects` calls `GET /v1/projects`. Human output is one line per row, in the order the API returned. An app line is the id, the name, and the project name. A database line is the id, the kind, the name, and the project name. A project line is the id and the name. For apps and databases `--json` prints the response body. For projects `--json` prints an object whose `projects` rows each carry `id`, `name`, and `created_at`.
|
|
105
|
+
|
|
106
|
+
### logs
|
|
107
|
+
|
|
108
|
+
`astris logs <app-id>` calls `GET /v1/apps/:id/logs`. `--source runtime` or `--source deploy` selects the log. Omit `--source` and the route uses runtime. `--tail <n>` sets how many lines, at most 500. Omit `--tail` and the route uses its own default. Human output is the log text. `--json` prints the response body.
|
|
109
|
+
|
|
110
|
+
### usage
|
|
111
|
+
|
|
112
|
+
`astris usage` calls `GET /v1/usage`. It takes no flags besides `--json`. `--json` prints the response body. When `plan` is null, human output is `Plan: none` and `RAM used: <n> MiB`. When `plan` is not null, human output is `Plan: <name>`, `RAM used: <n> MiB of <pool> MiB`, and `RAM left: <n> MiB`. CPU and disk are not shown.
|
|
113
|
+
|
|
114
|
+
### plans
|
|
115
|
+
|
|
116
|
+
`astris plans` calls `GET /v1/plans`. It takes no flags besides `--json`. `--json` prints the response body. Human output is one line per plan, in the order the route returns them, with no header: `<name>: RAM <ram_mb> MiB, CPU <cpu_milli> mCPU, disk <disk_gb> GB`.
|
|
117
|
+
|
|
118
|
+
### whoami
|
|
119
|
+
|
|
120
|
+
`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>)`.
|
|
121
|
+
|
|
122
|
+
### login
|
|
123
|
+
|
|
124
|
+
`astris login` reads the key from stdin. It does not take the key as an argument. When the API accepts the key, it writes `Logged in.`
|
|
125
|
+
|
|
126
|
+
### logout
|
|
127
|
+
|
|
128
|
+
`astris logout` deletes the saved key file and writes `Logged out.` When there is no file, it writes `There was nothing to remove.`
|
|
129
|
+
|
|
130
|
+
## Exit codes
|
|
131
|
+
|
|
132
|
+
Exit code 0 means the command succeeded. Any error exits with a non-zero code. With `--json`, an error that happens after the key check is one object `{"error":"<sentence>"}` on stdout. A missing key and an unknown command write their sentence to stderr.
|
|
133
|
+
|
|
134
|
+
## Not in this version
|
|
135
|
+
|
|
136
|
+
`rollback` is not in v0. The route it needs waits for T-042 item 3.
|
|
137
|
+
|
|
138
|
+
## Rules
|
|
139
|
+
|
|
140
|
+
Some arguments are refused on this machine, before any request.
|
|
141
|
+
|
|
142
|
+
The command name is checked first. `deploy`, `status`, `env`, `domains`, `restart`, `logs`, `rollback`, `diagnose`, `database`, `storage`, `list`, `usage`, `plans`, `whoami`, `login`, and `logout` are known. Any other name writes `Unknown command.` and makes no request.
|
|
143
|
+
|
|
144
|
+
Login reads the key from stdin. 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.
|
|
145
|
+
|
|
146
|
+
`login` and `logout` take no arguments. An argument is refused, and `logout` does not delete the file.
|
|
147
|
+
|
|
148
|
+
After the key, `deploy`, `status`, `env`, `domains`, `restart`, `diagnose`, `database`, and `storage` 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.`
|
|
149
|
+
|
|
150
|
+
`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.
|
|
151
|
+
|
|
152
|
+
`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.
|
|
153
|
+
|
|
154
|
+
`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.
|
|
155
|
+
|
|
156
|
+
`--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, and then refuses `--build`, `--publish-directory`, `--spa`, `--kind`, `--command`, and `--schedule`; the other create flags are ignored.
|
|
157
|
+
|
|
158
|
+
`deploy` without `--app` refuses the call unless `--project`, `--name`, `--git-url`, and `--branch` each have a non-empty value after trim. The sentence names those four flags. On that same path, a `--git-url` whose user or password is set is refused. The sentence says the Git URL must be a public http or https URL with no user or password. A `--port` on that path is refused unless it is a whole number from 1 to 65535 with no leading zero. Other Git URL rules are left to the API. On that path, `--build` is exactly `nixpacks`, `dockerfile`, or `static`. `nixpacks` refuses `--dockerfile`, `--publish-directory`, and `--spa`. `dockerfile` needs `--dockerfile`. `static` takes an optional `--publish-directory` and an optional `--spa`, and refuses `--dockerfile`. `--publish-directory` or `--spa` without `--build` is refused. `--kind` is exactly `web`, `worker`, or `cron`. A web app refuses `--command` and `--schedule`. A worker needs `--command` and refuses `--schedule`. A cron needs `--command` and a `--schedule` of five space-separated fields of digits, `*`, `,`, `-`, or `/`, at most 100 characters, each field non-empty. A command is 1 to 512 characters, with no line break and no NUL character. A worker or cron app with `--build static` is refused. Any refusal on this path makes no request.
|
|
159
|
+
|
|
160
|
+
`env` refuses stdin that is not one JSON object. An array, a bare value, and text that is not JSON are refused, before any request. `env --names` sends no replace: it prints the names of the application's variables, one per line, and never a value, and it reads no stdin.
|
|
161
|
+
|
|
162
|
+
`rollback` is refused after the key check. The sentence is `rollback is not available in this version.` It makes no request.
|
|
163
|
+
|
|
164
|
+
`logs` takes one application id. With no id, or an id that is not an application id, the sentence is `Send the application id.` `list` takes one word, `apps`, `databases`, or `projects`. A missing word or any other word is refused. Neither command makes a request in those cases.
|
|
165
|
+
|
|
166
|
+
Ownership, names, limits, and what an account is allowed to do are rules in the API. This CLI does not decide them.
|
package/package.json
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astris-labs/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The Astris CLI, a command-line client of the Astris API.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/astris-labs/astris-api.git",
|
|
8
|
+
"directory": "cli"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"astris": "./src/main.mjs"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"src/key.mjs",
|
|
15
|
+
"src/client.mjs",
|
|
16
|
+
"src/commands.mjs",
|
|
17
|
+
"src/main.mjs",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=22"
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/client.mjs
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export const DEFAULT_BASE_URL = "https://api.astris.run";
|
|
2
|
+
|
|
3
|
+
export function redact(text, key) {
|
|
4
|
+
if (typeof text !== "string" || typeof key !== "string" || key === "") {
|
|
5
|
+
return text;
|
|
6
|
+
}
|
|
7
|
+
return text.split(key).join("");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function failureMessage(err) {
|
|
11
|
+
return err instanceof Error ? err.message : String(err);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
15
|
+
|
|
16
|
+
// A late rejection must not surface after the race has settled or the call has returned.
|
|
17
|
+
function swallow(promise) {
|
|
18
|
+
promise.catch(() => undefined);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function astrisFetch({ key, baseUrl, path, method, body, fetchImpl }) {
|
|
22
|
+
const url = baseUrl + path;
|
|
23
|
+
const headers = { Authorization: `Bearer ${key}` };
|
|
24
|
+
const controller = new AbortController();
|
|
25
|
+
const init = { method, headers, signal: controller.signal };
|
|
26
|
+
let timer;
|
|
27
|
+
const timeout = new Promise((_, reject) => {
|
|
28
|
+
timer = setTimeout(() => {
|
|
29
|
+
reject(new Error("request timed out"));
|
|
30
|
+
controller.abort();
|
|
31
|
+
}, REQUEST_TIMEOUT_MS);
|
|
32
|
+
});
|
|
33
|
+
swallow(timeout);
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
if (body !== undefined) {
|
|
37
|
+
init.body = JSON.stringify(body);
|
|
38
|
+
headers["content-type"] = "application/json";
|
|
39
|
+
}
|
|
40
|
+
let request;
|
|
41
|
+
try {
|
|
42
|
+
request = Promise.resolve(fetchImpl(url, init));
|
|
43
|
+
} catch (err) {
|
|
44
|
+
request = Promise.reject(err);
|
|
45
|
+
}
|
|
46
|
+
swallow(request);
|
|
47
|
+
const response = await Promise.race([request, timeout]);
|
|
48
|
+
|
|
49
|
+
let reading;
|
|
50
|
+
try {
|
|
51
|
+
reading = Promise.resolve(response.text());
|
|
52
|
+
} catch (err) {
|
|
53
|
+
reading = Promise.reject(err);
|
|
54
|
+
}
|
|
55
|
+
swallow(reading);
|
|
56
|
+
const text = await Promise.race([reading, timeout]);
|
|
57
|
+
|
|
58
|
+
if (text === "") {
|
|
59
|
+
return { status: response.status, body: null };
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
return { status: response.status, body: JSON.parse(text) };
|
|
63
|
+
} catch {
|
|
64
|
+
return { status: response.status, body: null };
|
|
65
|
+
}
|
|
66
|
+
} catch (err) {
|
|
67
|
+
throw new Error(redact(failureMessage(err), key));
|
|
68
|
+
} finally {
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
}
|
|
71
|
+
}
|