@moor-sh/cli 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/package.json +32 -0
- package/src/client.ts +106 -0
- package/src/commands/env.ts +97 -0
- package/src/commands/exec.ts +29 -0
- package/src/commands/logs.ts +62 -0
- package/src/commands/rebuild.ts +40 -0
- package/src/commands/restart.ts +27 -0
- package/src/commands/stats.ts +29 -0
- package/src/commands/status.ts +56 -0
- package/src/index.ts +80 -0
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@moor-sh/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Command-line interface for moor - manage your moor server's projects, logs, env vars, and container lifecycle.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/caiopizzol/moor.git",
|
|
9
|
+
"directory": "packages/cli"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/caiopizzol/moor#cli",
|
|
12
|
+
"bugs": "https://github.com/caiopizzol/moor/issues",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"moor",
|
|
15
|
+
"cli",
|
|
16
|
+
"docker"
|
|
17
|
+
],
|
|
18
|
+
"type": "module",
|
|
19
|
+
"bin": {
|
|
20
|
+
"moor": "src/index.ts"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"src/"
|
|
24
|
+
],
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"dev": "bun run src/index.ts",
|
|
30
|
+
"build": "bun build src/index.ts --compile --outfile moor"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
function getConfig(): { baseUrl: string; apiKey: string } {
|
|
2
|
+
const baseUrl = process.env.MOOR_URL;
|
|
3
|
+
const apiKey = process.env.MOOR_API_KEY;
|
|
4
|
+
if (!baseUrl) {
|
|
5
|
+
console.error("Error: MOOR_URL is not set");
|
|
6
|
+
process.exit(1);
|
|
7
|
+
}
|
|
8
|
+
if (!apiKey) {
|
|
9
|
+
console.error("Error: MOOR_API_KEY is not set");
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
return { baseUrl: baseUrl.replace(/\/$/, ""), apiKey };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function headers(apiKey: string, json = false): Record<string, string> {
|
|
16
|
+
const h: Record<string, string> = { Authorization: `Bearer ${apiKey}` };
|
|
17
|
+
if (json) h["Content-Type"] = "application/json";
|
|
18
|
+
return h;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function apiGet(path: string): Promise<Response> {
|
|
22
|
+
const { baseUrl, apiKey } = getConfig();
|
|
23
|
+
return fetch(`${baseUrl}${path}`, { headers: headers(apiKey) });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function apiPost(path: string, body?: unknown): Promise<Response> {
|
|
27
|
+
const { baseUrl, apiKey } = getConfig();
|
|
28
|
+
return fetch(`${baseUrl}${path}`, {
|
|
29
|
+
method: "POST",
|
|
30
|
+
headers: headers(apiKey, body !== undefined),
|
|
31
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function apiPut(path: string, body: unknown): Promise<Response> {
|
|
36
|
+
const { baseUrl, apiKey } = getConfig();
|
|
37
|
+
return fetch(`${baseUrl}${path}`, {
|
|
38
|
+
method: "PUT",
|
|
39
|
+
headers: headers(apiKey, true),
|
|
40
|
+
body: JSON.stringify(body),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type Project = {
|
|
45
|
+
id: number;
|
|
46
|
+
name: string;
|
|
47
|
+
status: string;
|
|
48
|
+
container_id: string | null;
|
|
49
|
+
image_tag: string | null;
|
|
50
|
+
domain: string | null;
|
|
51
|
+
docker_image: string | null;
|
|
52
|
+
github_url: string | null;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export async function resolveProject(nameOrId: string): Promise<Project> {
|
|
56
|
+
const res = await apiGet("/api/projects");
|
|
57
|
+
if (!res.ok) {
|
|
58
|
+
console.error(`Failed to list projects: ${res.status}`);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
const projects = (await res.json()) as Project[];
|
|
62
|
+
const match = projects.find((p) => p.name === nameOrId || String(p.id) === nameOrId);
|
|
63
|
+
if (!match) {
|
|
64
|
+
console.error(`Project "${nameOrId}" not found`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
return match;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function streamSSE(
|
|
71
|
+
res: Response,
|
|
72
|
+
handlers: {
|
|
73
|
+
onLog?: (text: string) => void;
|
|
74
|
+
onError?: (text: string) => void;
|
|
75
|
+
onDone?: (text: string) => void;
|
|
76
|
+
},
|
|
77
|
+
): Promise<void> {
|
|
78
|
+
const reader = res.body?.getReader();
|
|
79
|
+
if (!reader) throw new Error("No response body");
|
|
80
|
+
|
|
81
|
+
const decoder = new TextDecoder();
|
|
82
|
+
let buffer = "";
|
|
83
|
+
let currentEvent = "";
|
|
84
|
+
|
|
85
|
+
while (true) {
|
|
86
|
+
const { done, value } = await reader.read();
|
|
87
|
+
if (done) break;
|
|
88
|
+
buffer += decoder.decode(value, { stream: true });
|
|
89
|
+
|
|
90
|
+
const lines = buffer.split("\n");
|
|
91
|
+
buffer = lines.pop() || "";
|
|
92
|
+
|
|
93
|
+
for (const line of lines) {
|
|
94
|
+
if (line.startsWith("event: ")) {
|
|
95
|
+
currentEvent = line.slice(7).trim();
|
|
96
|
+
} else if (line.startsWith("data: ")) {
|
|
97
|
+
const data = JSON.parse(line.slice(6));
|
|
98
|
+
if (currentEvent === "log") handlers.onLog?.(data);
|
|
99
|
+
else if (currentEvent === "error") handlers.onError?.(data);
|
|
100
|
+
else if (currentEvent === "done") handlers.onDone?.(data);
|
|
101
|
+
currentEvent = "";
|
|
102
|
+
}
|
|
103
|
+
// Ignore keepalive comments starting with ":"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { apiGet, apiPost, apiPut, resolveProject } from "../client";
|
|
2
|
+
|
|
3
|
+
type EnvVar = { key: string; value: string };
|
|
4
|
+
|
|
5
|
+
export async function envCommand(args: string[]) {
|
|
6
|
+
const subcommand = args[0];
|
|
7
|
+
|
|
8
|
+
if (subcommand === "list") return envList(args.slice(1));
|
|
9
|
+
if (subcommand === "set") return envSet(args.slice(1));
|
|
10
|
+
|
|
11
|
+
console.error("Usage: moor env <list|set> <project> [K=V ...]");
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function envList(args: string[]) {
|
|
16
|
+
const projectName = args[0];
|
|
17
|
+
if (!projectName) {
|
|
18
|
+
console.error("Usage: moor env list <project>");
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const project = await resolveProject(projectName);
|
|
23
|
+
const res = await apiGet(`/api/projects/${project.id}/envs`);
|
|
24
|
+
if (!res.ok) {
|
|
25
|
+
console.error(`Failed: ${await res.text()}`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const vars = (await res.json()) as EnvVar[];
|
|
30
|
+
if (vars.length === 0) {
|
|
31
|
+
console.log("No environment variables set.");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
for (const v of vars) {
|
|
35
|
+
console.log(`${v.key}=${v.value}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function envSet(args: string[]) {
|
|
40
|
+
const projectName = args[0];
|
|
41
|
+
const pairs = args.slice(1);
|
|
42
|
+
|
|
43
|
+
if (!projectName || pairs.length === 0) {
|
|
44
|
+
console.error("Usage: moor env set <project> KEY=VALUE [KEY=VALUE ...]");
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Parse K=V pairs
|
|
49
|
+
const newVars: EnvVar[] = [];
|
|
50
|
+
for (const pair of pairs) {
|
|
51
|
+
const eq = pair.indexOf("=");
|
|
52
|
+
if (eq === -1) {
|
|
53
|
+
console.error(`Invalid format: "${pair}" (expected KEY=VALUE)`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
newVars.push({ key: pair.slice(0, eq), value: pair.slice(eq + 1) });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const project = await resolveProject(projectName);
|
|
60
|
+
|
|
61
|
+
// Fetch existing env vars and merge
|
|
62
|
+
const existingRes = await apiGet(`/api/projects/${project.id}/envs`);
|
|
63
|
+
if (!existingRes.ok) {
|
|
64
|
+
console.error(`Failed to get env vars: ${await existingRes.text()}`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
const existing = (await existingRes.json()) as EnvVar[];
|
|
68
|
+
|
|
69
|
+
// Merge: new values overwrite existing keys
|
|
70
|
+
const merged = new Map(existing.map((v) => [v.key, v.value]));
|
|
71
|
+
for (const v of newVars) {
|
|
72
|
+
merged.set(v.key, v.value);
|
|
73
|
+
}
|
|
74
|
+
const allVars = Array.from(merged, ([key, value]) => ({ key, value }));
|
|
75
|
+
|
|
76
|
+
const setRes = await apiPut(`/api/projects/${project.id}/envs`, allVars);
|
|
77
|
+
if (!setRes.ok) {
|
|
78
|
+
console.error(`Failed to set env vars: ${await setRes.text()}`);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const v of newVars) {
|
|
83
|
+
console.log(`Set ${v.key}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Restart if container is running
|
|
87
|
+
if (project.status === "running") {
|
|
88
|
+
console.log(`Restarting ${project.name}...`);
|
|
89
|
+
await apiPost(`/api/projects/${project.id}/stop`);
|
|
90
|
+
const startRes = await apiPost(`/api/projects/${project.id}/start`);
|
|
91
|
+
if (!startRes.ok) {
|
|
92
|
+
console.error(`Warning: failed to restart: ${await startRes.text()}`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
console.log(`${project.name} restarted.`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { apiPost, resolveProject } from "../client";
|
|
2
|
+
|
|
3
|
+
export async function execCommand(args: string[]) {
|
|
4
|
+
const projectName = args[0];
|
|
5
|
+
const command = args.slice(1).join(" ");
|
|
6
|
+
|
|
7
|
+
if (!projectName || !command) {
|
|
8
|
+
console.error("Usage: moor exec <project> <command>");
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const project = await resolveProject(projectName);
|
|
13
|
+
const res = await apiPost(`/api/projects/${project.id}/exec`, { command });
|
|
14
|
+
|
|
15
|
+
if (!res.ok) {
|
|
16
|
+
console.error(`Failed: ${await res.text()}`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const result = (await res.json()) as {
|
|
21
|
+
exitCode: number;
|
|
22
|
+
stdout: string;
|
|
23
|
+
stderr: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
27
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
28
|
+
process.exit(result.exitCode);
|
|
29
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { apiGet, resolveProject } from "../client";
|
|
2
|
+
|
|
3
|
+
export async function logsCommand(args: string[]) {
|
|
4
|
+
let follow = false;
|
|
5
|
+
let tail = 100;
|
|
6
|
+
let projectName: string | undefined;
|
|
7
|
+
|
|
8
|
+
for (let i = 0; i < args.length; i++) {
|
|
9
|
+
if (args[i] === "-f" || args[i] === "--follow") {
|
|
10
|
+
follow = true;
|
|
11
|
+
} else if (args[i] === "-n" || args[i] === "--lines") {
|
|
12
|
+
tail = Number(args[++i]);
|
|
13
|
+
} else if (!projectName) {
|
|
14
|
+
projectName = args[i];
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!projectName) {
|
|
19
|
+
console.error("Usage: moor logs <project> [-f] [-n <lines>]");
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const project = await resolveProject(projectName);
|
|
24
|
+
|
|
25
|
+
// Initial fetch with tail
|
|
26
|
+
const res = await apiGet(`/api/projects/${project.id}/logs?tail=${tail}`);
|
|
27
|
+
if (!res.ok) {
|
|
28
|
+
console.error(`Failed to get logs: ${res.status}`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const data = (await res.json()) as { logs: string; lastTimestamp: number };
|
|
33
|
+
if (data.logs) process.stdout.write(data.logs);
|
|
34
|
+
|
|
35
|
+
if (!follow) return;
|
|
36
|
+
|
|
37
|
+
// Poll for new logs
|
|
38
|
+
let since = data.lastTimestamp;
|
|
39
|
+
const poll = async () => {
|
|
40
|
+
try {
|
|
41
|
+
const res = await apiGet(`/api/projects/${project.id}/logs?since=${since}`);
|
|
42
|
+
if (!res.ok) return;
|
|
43
|
+
const data = (await res.json()) as { logs: string; lastTimestamp: number };
|
|
44
|
+
if (data.logs?.trim()) {
|
|
45
|
+
process.stdout.write(data.logs);
|
|
46
|
+
since = data.lastTimestamp;
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
// Connection error, keep polling
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const interval = setInterval(poll, 2000);
|
|
54
|
+
|
|
55
|
+
process.on("SIGINT", () => {
|
|
56
|
+
clearInterval(interval);
|
|
57
|
+
process.exit(0);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Keep the process alive
|
|
61
|
+
await new Promise(() => {});
|
|
62
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { apiPost, resolveProject, streamSSE } from "../client";
|
|
2
|
+
|
|
3
|
+
export async function rebuildCommand(args: string[]) {
|
|
4
|
+
let noCache = false;
|
|
5
|
+
let projectName: string | undefined;
|
|
6
|
+
|
|
7
|
+
for (const arg of args) {
|
|
8
|
+
if (arg === "--no-cache") noCache = true;
|
|
9
|
+
else if (!projectName) projectName = arg;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (!projectName) {
|
|
13
|
+
console.error("Usage: moor rebuild <project> [--no-cache]");
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const project = await resolveProject(projectName);
|
|
18
|
+
const query = noCache ? "?nocache=true" : "";
|
|
19
|
+
|
|
20
|
+
console.log(`Rebuilding ${project.name}...`);
|
|
21
|
+
const res = await apiPost(`/api/projects/${project.id}/run${query}`);
|
|
22
|
+
|
|
23
|
+
if (!res.ok && res.headers.get("content-type")?.includes("text/event-stream") === false) {
|
|
24
|
+
const body = await res.text();
|
|
25
|
+
console.error(`Failed: ${body}`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let failed = false;
|
|
30
|
+
await streamSSE(res, {
|
|
31
|
+
onLog: (text) => process.stdout.write(text),
|
|
32
|
+
onError: (text) => {
|
|
33
|
+
process.stderr.write(`Error: ${text}\n`);
|
|
34
|
+
failed = true;
|
|
35
|
+
},
|
|
36
|
+
onDone: (text) => console.log(text),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
process.exit(failed ? 1 : 0);
|
|
40
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { apiPost, resolveProject } from "../client";
|
|
2
|
+
|
|
3
|
+
export async function restartCommand(args: string[]) {
|
|
4
|
+
const projectName = args[0];
|
|
5
|
+
if (!projectName) {
|
|
6
|
+
console.error("Usage: moor restart <project>");
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const project = await resolveProject(projectName);
|
|
11
|
+
|
|
12
|
+
console.log(`Stopping ${project.name}...`);
|
|
13
|
+
const stopRes = await apiPost(`/api/projects/${project.id}/stop`);
|
|
14
|
+
if (!stopRes.ok) {
|
|
15
|
+
console.error(`Failed to stop: ${await stopRes.text()}`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
console.log(`Starting ${project.name}...`);
|
|
20
|
+
const startRes = await apiPost(`/api/projects/${project.id}/start`);
|
|
21
|
+
if (!startRes.ok) {
|
|
22
|
+
console.error(`Failed to start: ${await startRes.text()}`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
console.log(`${project.name} restarted.`);
|
|
27
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { apiGet } from "../client";
|
|
2
|
+
|
|
3
|
+
type ServerStats = {
|
|
4
|
+
hostname: string;
|
|
5
|
+
os: string;
|
|
6
|
+
uptime: string;
|
|
7
|
+
cpu: { percent: number; cores: number };
|
|
8
|
+
memory: { total: string; used: string; percent: number };
|
|
9
|
+
disk: { total: string; used: string; percent: number };
|
|
10
|
+
containers: { running: number; total: number };
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export async function statsCommand() {
|
|
14
|
+
const res = await apiGet("/api/server/stats");
|
|
15
|
+
if (!res.ok) {
|
|
16
|
+
console.error(`Failed to get stats: ${res.status}`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const s = (await res.json()) as ServerStats;
|
|
21
|
+
|
|
22
|
+
console.log(`Host: ${s.hostname}`);
|
|
23
|
+
console.log(`OS: ${s.os}`);
|
|
24
|
+
console.log(`Uptime: ${s.uptime}`);
|
|
25
|
+
console.log(`CPU: ${s.cpu.percent}% (${s.cpu.cores} cores)`);
|
|
26
|
+
console.log(`Memory: ${s.memory.used} / ${s.memory.total} (${s.memory.percent}%)`);
|
|
27
|
+
console.log(`Disk: ${s.disk.used} / ${s.disk.total} (${s.disk.percent}%)`);
|
|
28
|
+
console.log(`Containers: ${s.containers.running} running / ${s.containers.total} total`);
|
|
29
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { apiGet } from "../client";
|
|
2
|
+
|
|
3
|
+
type Project = {
|
|
4
|
+
id: number;
|
|
5
|
+
name: string;
|
|
6
|
+
status: string;
|
|
7
|
+
image_tag: string | null;
|
|
8
|
+
domain: string | null;
|
|
9
|
+
docker_image: string | null;
|
|
10
|
+
github_url: string | null;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export async function statusCommand() {
|
|
14
|
+
const res = await apiGet("/api/projects");
|
|
15
|
+
if (!res.ok) {
|
|
16
|
+
console.error(`Failed to list projects: ${res.status}`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const projects = (await res.json()) as Project[];
|
|
21
|
+
if (projects.length === 0) {
|
|
22
|
+
console.log("No projects found.");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Calculate column widths
|
|
27
|
+
const nameW = Math.max(4, ...projects.map((p) => p.name.length));
|
|
28
|
+
const statusW = Math.max(6, ...projects.map((p) => p.status.length));
|
|
29
|
+
const sourceW = Math.max(
|
|
30
|
+
6,
|
|
31
|
+
...projects.map((p) => (p.docker_image || p.github_url || "-").length),
|
|
32
|
+
);
|
|
33
|
+
const domainW = Math.max(6, ...projects.map((p) => (p.domain || "-").length));
|
|
34
|
+
|
|
35
|
+
const header = [
|
|
36
|
+
"NAME".padEnd(nameW),
|
|
37
|
+
"STATUS".padEnd(statusW),
|
|
38
|
+
"SOURCE".padEnd(sourceW),
|
|
39
|
+
"DOMAIN".padEnd(domainW),
|
|
40
|
+
].join(" ");
|
|
41
|
+
|
|
42
|
+
console.log(header);
|
|
43
|
+
console.log("-".repeat(header.length));
|
|
44
|
+
|
|
45
|
+
for (const p of projects) {
|
|
46
|
+
const source = p.docker_image || p.github_url || "-";
|
|
47
|
+
console.log(
|
|
48
|
+
[
|
|
49
|
+
p.name.padEnd(nameW),
|
|
50
|
+
p.status.padEnd(statusW),
|
|
51
|
+
source.padEnd(sourceW),
|
|
52
|
+
(p.domain || "-").padEnd(domainW),
|
|
53
|
+
].join(" "),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { envCommand } from "./commands/env";
|
|
5
|
+
import { execCommand } from "./commands/exec";
|
|
6
|
+
import { logsCommand } from "./commands/logs";
|
|
7
|
+
import { rebuildCommand } from "./commands/rebuild";
|
|
8
|
+
import { restartCommand } from "./commands/restart";
|
|
9
|
+
import { statsCommand } from "./commands/stats";
|
|
10
|
+
import { statusCommand } from "./commands/status";
|
|
11
|
+
|
|
12
|
+
// Read version from package.json at runtime so the binary always reports the
|
|
13
|
+
// real shipped version. import.meta.dir resolves to packages/cli/src in this
|
|
14
|
+
// repo and to <install-root>/src in a published install; ../package.json is
|
|
15
|
+
// the package root in both cases.
|
|
16
|
+
const VERSION = (
|
|
17
|
+
JSON.parse(readFileSync(join(import.meta.dir, "..", "package.json"), "utf8")) as {
|
|
18
|
+
version: string;
|
|
19
|
+
}
|
|
20
|
+
).version;
|
|
21
|
+
|
|
22
|
+
function printHelp() {
|
|
23
|
+
console.log(`moor - CLI for Moor server management
|
|
24
|
+
|
|
25
|
+
Usage: moor <command> [options]
|
|
26
|
+
|
|
27
|
+
Commands:
|
|
28
|
+
status List all projects
|
|
29
|
+
logs <project> [-f] [-n <lines>] View container logs
|
|
30
|
+
rebuild <project> [--no-cache] Rebuild and restart from source
|
|
31
|
+
restart <project> Stop and start a container
|
|
32
|
+
exec <project> <command> Run a command in a container
|
|
33
|
+
env list <project> List environment variables
|
|
34
|
+
env set <project> K=V [K=V ...] Set environment variables
|
|
35
|
+
stats Show server resource usage
|
|
36
|
+
|
|
37
|
+
Environment:
|
|
38
|
+
MOOR_URL Server URL (e.g. https://moor.example.com)
|
|
39
|
+
MOOR_API_KEY API key for authentication`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const args = process.argv.slice(2);
|
|
43
|
+
const command = args[0];
|
|
44
|
+
|
|
45
|
+
switch (command) {
|
|
46
|
+
case "status":
|
|
47
|
+
await statusCommand();
|
|
48
|
+
break;
|
|
49
|
+
case "logs":
|
|
50
|
+
await logsCommand(args.slice(1));
|
|
51
|
+
break;
|
|
52
|
+
case "rebuild":
|
|
53
|
+
await rebuildCommand(args.slice(1));
|
|
54
|
+
break;
|
|
55
|
+
case "restart":
|
|
56
|
+
await restartCommand(args.slice(1));
|
|
57
|
+
break;
|
|
58
|
+
case "exec":
|
|
59
|
+
await execCommand(args.slice(1));
|
|
60
|
+
break;
|
|
61
|
+
case "env":
|
|
62
|
+
await envCommand(args.slice(1));
|
|
63
|
+
break;
|
|
64
|
+
case "stats":
|
|
65
|
+
await statsCommand();
|
|
66
|
+
break;
|
|
67
|
+
case "--help":
|
|
68
|
+
case "-h":
|
|
69
|
+
case "help":
|
|
70
|
+
printHelp();
|
|
71
|
+
break;
|
|
72
|
+
case "--version":
|
|
73
|
+
case "-v":
|
|
74
|
+
console.log(VERSION);
|
|
75
|
+
break;
|
|
76
|
+
default:
|
|
77
|
+
if (command) console.error(`Unknown command: ${command}\n`);
|
|
78
|
+
printHelp();
|
|
79
|
+
process.exit(command ? 1 : 0);
|
|
80
|
+
}
|