@astris-labs/cli 0.0.0-stage → 0.1.1
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 +171 -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/src/key.mjs
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { Buffer } from "node:buffer";
|
|
3
|
+
import { closeSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeSync } from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
export class NoApiKeyError extends Error {
|
|
8
|
+
constructor() {
|
|
9
|
+
super("ASTRIS_API_KEY is not set");
|
|
10
|
+
this.name = "NoApiKeyError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// A usable directory is a non-empty absolute string. The trimmed string is the path.
|
|
15
|
+
function absolute(value) {
|
|
16
|
+
if (typeof value !== "string") return null;
|
|
17
|
+
const trimmed = value.trim();
|
|
18
|
+
if (trimmed === "" || !path.isAbsolute(trimmed)) return null;
|
|
19
|
+
return trimmed;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function configDir(env) {
|
|
23
|
+
const platform = os.platform();
|
|
24
|
+
if (platform === "win32") {
|
|
25
|
+
const appData = absolute(env.APPDATA);
|
|
26
|
+
if (appData) return appData;
|
|
27
|
+
const profile = absolute(env.USERPROFILE);
|
|
28
|
+
if (profile) return path.join(profile, "AppData", "Roaming");
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
if (platform === "darwin") {
|
|
32
|
+
const home = absolute(env.HOME);
|
|
33
|
+
if (home) return path.join(home, "Library", "Application Support");
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const xdg = absolute(env.XDG_CONFIG_HOME);
|
|
37
|
+
if (xdg) return xdg;
|
|
38
|
+
const home = absolute(env.HOME);
|
|
39
|
+
if (home) return path.join(home, ".config");
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function apiKeyPath(env = process.env) {
|
|
44
|
+
const dir = configDir(env);
|
|
45
|
+
if (!dir) return null;
|
|
46
|
+
return path.join(dir, "astris", "key");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function readApiKey(env = process.env) {
|
|
50
|
+
const value = env.ASTRIS_API_KEY;
|
|
51
|
+
if (typeof value === "string" && value.trim() !== "") return value;
|
|
52
|
+
const file = apiKeyPath(env);
|
|
53
|
+
if (file === null) throw new NoApiKeyError();
|
|
54
|
+
let text;
|
|
55
|
+
try {
|
|
56
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- path is the env argument's config file
|
|
57
|
+
text = readFileSync(file, "utf8");
|
|
58
|
+
} catch {
|
|
59
|
+
// A missing, forbidden, or non-file path uses the same sentence. The fs error carries the path.
|
|
60
|
+
throw new NoApiKeyError();
|
|
61
|
+
}
|
|
62
|
+
if (text.trim() === "") throw new NoApiKeyError();
|
|
63
|
+
return text.trim();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function writeAll(fd, text) {
|
|
67
|
+
const data = Buffer.from(text, "utf8");
|
|
68
|
+
let offset = 0;
|
|
69
|
+
while (offset < data.length) {
|
|
70
|
+
const written = writeSync(fd, data, offset, data.length - offset, null);
|
|
71
|
+
if (written === 0) throw new Error("Could not write the key file");
|
|
72
|
+
offset += written;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function saveApiKey(env = process.env, key) {
|
|
77
|
+
const file = apiKeyPath(env);
|
|
78
|
+
if (file === null) throw new Error("No config path");
|
|
79
|
+
const dir = path.dirname(file);
|
|
80
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- path is the env argument's config directory
|
|
81
|
+
mkdirSync(dir, { recursive: true });
|
|
82
|
+
const temp = path.join(dir, `.${randomBytes(8).toString("hex")}`);
|
|
83
|
+
// ponytail: mode is only this open (0o600). Windows stat of a writable file is 0o666 (C1); no ACL.
|
|
84
|
+
let fd;
|
|
85
|
+
let created = false;
|
|
86
|
+
try {
|
|
87
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- temp file is in the same config directory
|
|
88
|
+
fd = openSync(temp, "wx", 0o600);
|
|
89
|
+
created = true;
|
|
90
|
+
writeAll(fd, key);
|
|
91
|
+
closeSync(fd);
|
|
92
|
+
fd = undefined;
|
|
93
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- rename stays inside the config directory
|
|
94
|
+
renameSync(temp, file);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (fd !== undefined) {
|
|
97
|
+
try {
|
|
98
|
+
closeSync(fd);
|
|
99
|
+
} catch {
|
|
100
|
+
// The write error is the one to report.
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (created) {
|
|
104
|
+
try {
|
|
105
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- temp file this open created
|
|
106
|
+
unlinkSync(temp);
|
|
107
|
+
} catch {
|
|
108
|
+
// Already gone.
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function removeApiKey(env = process.env) {
|
|
116
|
+
const file = apiKeyPath(env);
|
|
117
|
+
if (file === null) return { removed: false };
|
|
118
|
+
try {
|
|
119
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- unlinks only astris/key
|
|
120
|
+
unlinkSync(file);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (error.code === "ENOENT") return { removed: false };
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
return { removed: true };
|
|
126
|
+
}
|
package/src/main.mjs
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { DEFAULT_BASE_URL } from "./client.mjs";
|
|
5
|
+
import { database, deploy, diagnose, domains, env, list, login, logout, logs, plans, restart, rollback, status, storage, usage, whoami } from "./commands.mjs";
|
|
6
|
+
|
|
7
|
+
export async function main(argv, io) {
|
|
8
|
+
const command = argv?.[2];
|
|
9
|
+
const args = Array.isArray(argv) ? argv.slice(3) : [];
|
|
10
|
+
if (command === "deploy") return (await deploy(args, io)).exitCode;
|
|
11
|
+
if (command === "status") return (await status(args, io)).exitCode;
|
|
12
|
+
if (command === "diagnose") return (await diagnose(args, io)).exitCode;
|
|
13
|
+
if (command === "database") return (await database(args, io)).exitCode;
|
|
14
|
+
if (command === "storage") return (await storage(args, io)).exitCode;
|
|
15
|
+
if (command === "env") return (await env(args, io)).exitCode;
|
|
16
|
+
if (command === "domains") return (await domains(args, io)).exitCode;
|
|
17
|
+
if (command === "restart") return (await restart(args, io)).exitCode;
|
|
18
|
+
if (command === "logs") return (await logs(args, io)).exitCode;
|
|
19
|
+
if (command === "list") return (await list(args, io)).exitCode;
|
|
20
|
+
if (command === "usage") return (await usage(args, io)).exitCode;
|
|
21
|
+
if (command === "plans") return (await plans(args, io)).exitCode;
|
|
22
|
+
if (command === "whoami") return (await whoami(args, io)).exitCode;
|
|
23
|
+
if (command === "rollback") return rollback(args, io).exitCode;
|
|
24
|
+
if (command === "login") return (await login(args, io)).exitCode;
|
|
25
|
+
if (command === "logout") return (await logout(args, io)).exitCode;
|
|
26
|
+
io.stderr.write("Unknown command.\n");
|
|
27
|
+
return 1;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function readStdin() {
|
|
31
|
+
if (process.stdin.isTTY) return "";
|
|
32
|
+
const chunks = [];
|
|
33
|
+
for await (const chunk of process.stdin) {
|
|
34
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
35
|
+
}
|
|
36
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
|
|
40
|
+
const stdin = (process.argv[2] === "env" || process.argv[2] === "login") && !process.stdin.isTTY ? await readStdin() : "";
|
|
41
|
+
const code = await main(process.argv, {
|
|
42
|
+
env: process.env,
|
|
43
|
+
fetchImpl: globalThis.fetch,
|
|
44
|
+
baseUrl: DEFAULT_BASE_URL,
|
|
45
|
+
stdin,
|
|
46
|
+
stdout: process.stdout,
|
|
47
|
+
stderr: process.stderr,
|
|
48
|
+
});
|
|
49
|
+
process.exit(code);
|
|
50
|
+
}
|