@moor-sh/cli 0.5.0 → 0.6.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/bin/moor.js +2 -0
- package/dist/index.js +701 -0
- package/package.json +7 -4
- package/src/client.ts +0 -106
- package/src/commands/env.ts +0 -97
- package/src/commands/exec.ts +0 -29
- package/src/commands/history.test.ts +0 -37
- package/src/commands/history.ts +0 -117
- package/src/commands/logs.ts +0 -62
- package/src/commands/mcp.ts +0 -136
- package/src/commands/rebuild.ts +0 -40
- package/src/commands/restart.ts +0 -27
- package/src/commands/stats.ts +0 -35
- package/src/commands/status.ts +0 -56
- package/src/index.ts +0 -90
package/bin/moor.js
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// src/index.ts
|
|
5
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
6
|
+
import { join as join2 } from "path";
|
|
7
|
+
|
|
8
|
+
// ../contract/src/client.ts
|
|
9
|
+
class MoorApiError extends Error {
|
|
10
|
+
status;
|
|
11
|
+
body;
|
|
12
|
+
constructor(status, message, body = "") {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "MoorApiError";
|
|
15
|
+
this.status = status;
|
|
16
|
+
this.body = body;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function createMoorApiClient(options) {
|
|
20
|
+
const baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
21
|
+
const apiKey = options.apiKey;
|
|
22
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
23
|
+
async function request(method, path, body, init) {
|
|
24
|
+
const hasBody = body !== undefined;
|
|
25
|
+
const headers = new Headers(init?.headers);
|
|
26
|
+
headers.set("Authorization", `Bearer ${apiKey}`);
|
|
27
|
+
if (!headers.has("Accept"))
|
|
28
|
+
headers.set("Accept", "application/json");
|
|
29
|
+
if (hasBody && !headers.has("Content-Type"))
|
|
30
|
+
headers.set("Content-Type", "application/json");
|
|
31
|
+
const res = await fetchImpl(joinUrl(baseUrl, path), {
|
|
32
|
+
...init,
|
|
33
|
+
method,
|
|
34
|
+
headers,
|
|
35
|
+
body: hasBody ? JSON.stringify(body) : init?.body
|
|
36
|
+
});
|
|
37
|
+
if (!res.ok) {
|
|
38
|
+
const text = await res.text();
|
|
39
|
+
throw new MoorApiError(res.status, parseErrorMessage(text, res.status), text);
|
|
40
|
+
}
|
|
41
|
+
if (res.status === 204)
|
|
42
|
+
return;
|
|
43
|
+
return await res.json();
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
get: (path, init) => request("GET", path, undefined, init),
|
|
47
|
+
post: (path, body, init) => request("POST", path, body, init),
|
|
48
|
+
put: (path, body, init) => request("PUT", path, body, init),
|
|
49
|
+
delete: (path, init) => request("DELETE", path, undefined, init)
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function normalizeBaseUrl(baseUrl) {
|
|
53
|
+
return baseUrl.replace(/\/+$/, "");
|
|
54
|
+
}
|
|
55
|
+
function joinUrl(baseUrl, path) {
|
|
56
|
+
if (path.startsWith("/"))
|
|
57
|
+
return `${baseUrl}${path}`;
|
|
58
|
+
return `${baseUrl}/${path}`;
|
|
59
|
+
}
|
|
60
|
+
function parseErrorMessage(body, status) {
|
|
61
|
+
if (body) {
|
|
62
|
+
try {
|
|
63
|
+
const parsed = JSON.parse(body);
|
|
64
|
+
if (isJsonObject(parsed) && "error" in parsed) {
|
|
65
|
+
const error = parsed.error;
|
|
66
|
+
if (typeof error === "string")
|
|
67
|
+
return error;
|
|
68
|
+
return JSON.stringify(error);
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
return body;
|
|
72
|
+
}
|
|
73
|
+
return body;
|
|
74
|
+
}
|
|
75
|
+
return `HTTP ${status}`;
|
|
76
|
+
}
|
|
77
|
+
function isJsonObject(value) {
|
|
78
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
79
|
+
}
|
|
80
|
+
// src/client.ts
|
|
81
|
+
function getConfig() {
|
|
82
|
+
const baseUrl = process.env.MOOR_URL;
|
|
83
|
+
const apiKey = process.env.MOOR_API_KEY;
|
|
84
|
+
if (!baseUrl) {
|
|
85
|
+
console.error("Error: MOOR_URL is not set");
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
if (!apiKey) {
|
|
89
|
+
console.error("Error: MOOR_API_KEY is not set");
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
return { baseUrl: baseUrl.replace(/\/$/, ""), apiKey };
|
|
93
|
+
}
|
|
94
|
+
async function rawResponseRequest(callClient) {
|
|
95
|
+
let rawResponse;
|
|
96
|
+
const fetchRawResponse = async (input, init) => {
|
|
97
|
+
rawResponse = await globalThis.fetch(input, init);
|
|
98
|
+
return new Response(null, { status: 204 });
|
|
99
|
+
};
|
|
100
|
+
const client2 = createMoorApiClient({
|
|
101
|
+
...getConfig(),
|
|
102
|
+
fetch: fetchRawResponse
|
|
103
|
+
});
|
|
104
|
+
await callClient(client2);
|
|
105
|
+
if (!rawResponse)
|
|
106
|
+
throw new Error("No response received");
|
|
107
|
+
return rawResponse;
|
|
108
|
+
}
|
|
109
|
+
async function apiGet(path) {
|
|
110
|
+
return rawResponseRequest((client2) => client2.get(path));
|
|
111
|
+
}
|
|
112
|
+
async function apiPost(path, body) {
|
|
113
|
+
return rawResponseRequest((client2) => client2.post(path, body));
|
|
114
|
+
}
|
|
115
|
+
async function apiPut(path, body) {
|
|
116
|
+
return rawResponseRequest((client2) => client2.put(path, body));
|
|
117
|
+
}
|
|
118
|
+
async function readErrorMessage(res) {
|
|
119
|
+
const text = await res.text();
|
|
120
|
+
return parseErrorMessage(text, res.status);
|
|
121
|
+
}
|
|
122
|
+
async function resolveProject(nameOrId) {
|
|
123
|
+
const res = await apiGet("/api/projects");
|
|
124
|
+
if (!res.ok) {
|
|
125
|
+
console.error(`Failed to list projects: ${res.status}`);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
const projects = await res.json();
|
|
129
|
+
const match = projects.find((p) => p.name === nameOrId || String(p.id) === nameOrId);
|
|
130
|
+
if (!match) {
|
|
131
|
+
console.error(`Project "${nameOrId}" not found`);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
return match;
|
|
135
|
+
}
|
|
136
|
+
async function streamSSE(res, handlers) {
|
|
137
|
+
const reader = res.body?.getReader();
|
|
138
|
+
if (!reader)
|
|
139
|
+
throw new Error("No response body");
|
|
140
|
+
const decoder = new TextDecoder;
|
|
141
|
+
let buffer = "";
|
|
142
|
+
let currentEvent = "";
|
|
143
|
+
while (true) {
|
|
144
|
+
const { done, value } = await reader.read();
|
|
145
|
+
if (done)
|
|
146
|
+
break;
|
|
147
|
+
buffer += decoder.decode(value, { stream: true });
|
|
148
|
+
const lines = buffer.split(`
|
|
149
|
+
`);
|
|
150
|
+
buffer = lines.pop() || "";
|
|
151
|
+
for (const line of lines) {
|
|
152
|
+
if (line.startsWith("event: ")) {
|
|
153
|
+
currentEvent = line.slice(7).trim();
|
|
154
|
+
} else if (line.startsWith("data: ")) {
|
|
155
|
+
const data = JSON.parse(line.slice(6));
|
|
156
|
+
if (currentEvent === "log")
|
|
157
|
+
handlers.onLog?.(data);
|
|
158
|
+
else if (currentEvent === "error")
|
|
159
|
+
handlers.onError?.(data);
|
|
160
|
+
else if (currentEvent === "done")
|
|
161
|
+
handlers.onDone?.(data);
|
|
162
|
+
currentEvent = "";
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/commands/env.ts
|
|
169
|
+
async function envCommand(args) {
|
|
170
|
+
const subcommand = args[0];
|
|
171
|
+
if (subcommand === "list")
|
|
172
|
+
return envList(args.slice(1));
|
|
173
|
+
if (subcommand === "set")
|
|
174
|
+
return envSet(args.slice(1));
|
|
175
|
+
console.error("Usage: moor env <list|set> <project> [K=V ...]");
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
async function envList(args) {
|
|
179
|
+
const projectName = args[0];
|
|
180
|
+
if (!projectName) {
|
|
181
|
+
console.error("Usage: moor env list <project>");
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
const project = await resolveProject(projectName);
|
|
185
|
+
const res = await apiGet(`/api/projects/${project.id}/envs`);
|
|
186
|
+
if (!res.ok) {
|
|
187
|
+
console.error(`Failed: ${await readErrorMessage(res)}`);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
const vars = await res.json();
|
|
191
|
+
if (vars.length === 0) {
|
|
192
|
+
console.log("No environment variables set.");
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
for (const v of vars) {
|
|
196
|
+
console.log(`${v.key}=${v.value}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
async function envSet(args) {
|
|
200
|
+
const projectName = args[0];
|
|
201
|
+
const pairs = args.slice(1);
|
|
202
|
+
if (!projectName || pairs.length === 0) {
|
|
203
|
+
console.error("Usage: moor env set <project> KEY=VALUE [KEY=VALUE ...]");
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
const newVars = [];
|
|
207
|
+
for (const pair of pairs) {
|
|
208
|
+
const eq = pair.indexOf("=");
|
|
209
|
+
if (eq === -1) {
|
|
210
|
+
console.error(`Invalid format: "${pair}" (expected KEY=VALUE)`);
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
newVars.push({ key: pair.slice(0, eq), value: pair.slice(eq + 1) });
|
|
214
|
+
}
|
|
215
|
+
const project = await resolveProject(projectName);
|
|
216
|
+
const existingRes = await apiGet(`/api/projects/${project.id}/envs`);
|
|
217
|
+
if (!existingRes.ok) {
|
|
218
|
+
console.error(`Failed to get env vars: ${await readErrorMessage(existingRes)}`);
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
const existing = await existingRes.json();
|
|
222
|
+
const merged = new Map(existing.map((v) => [v.key, v.value]));
|
|
223
|
+
for (const v of newVars) {
|
|
224
|
+
merged.set(v.key, v.value);
|
|
225
|
+
}
|
|
226
|
+
const allVars = Array.from(merged, ([key, value]) => ({ key, value }));
|
|
227
|
+
const setRes = await apiPut(`/api/projects/${project.id}/envs`, allVars);
|
|
228
|
+
if (!setRes.ok) {
|
|
229
|
+
console.error(`Failed to set env vars: ${await readErrorMessage(setRes)}`);
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
for (const v of newVars) {
|
|
233
|
+
console.log(`Set ${v.key}`);
|
|
234
|
+
}
|
|
235
|
+
if (project.status === "running") {
|
|
236
|
+
console.log(`Restarting ${project.name}...`);
|
|
237
|
+
await apiPost(`/api/projects/${project.id}/stop`);
|
|
238
|
+
const startRes = await apiPost(`/api/projects/${project.id}/start`);
|
|
239
|
+
if (!startRes.ok) {
|
|
240
|
+
console.error(`Warning: failed to restart: ${await readErrorMessage(startRes)}`);
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
console.log(`${project.name} restarted.`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/commands/exec.ts
|
|
248
|
+
async function execCommand(args) {
|
|
249
|
+
const projectName = args[0];
|
|
250
|
+
const command = args.slice(1).join(" ");
|
|
251
|
+
if (!projectName || !command) {
|
|
252
|
+
console.error("Usage: moor exec <project> <command>");
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
255
|
+
const project = await resolveProject(projectName);
|
|
256
|
+
const res = await apiPost(`/api/projects/${project.id}/exec`, { command });
|
|
257
|
+
if (!res.ok) {
|
|
258
|
+
console.error(`Failed: ${await readErrorMessage(res)}`);
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
const result = await res.json();
|
|
262
|
+
if (result.stdout)
|
|
263
|
+
process.stdout.write(result.stdout);
|
|
264
|
+
if (result.stderr)
|
|
265
|
+
process.stderr.write(result.stderr);
|
|
266
|
+
process.exit(result.exitCode);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// src/commands/history.ts
|
|
270
|
+
function formatBytes(bytes) {
|
|
271
|
+
if (!Number.isFinite(bytes) || bytes <= 0)
|
|
272
|
+
return "0 B";
|
|
273
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
274
|
+
const i = Math.min(units.length - 1, Math.floor(Math.log(bytes) / Math.log(1024)));
|
|
275
|
+
const val = bytes / 1024 ** i;
|
|
276
|
+
return `${val.toFixed(val < 10 ? 1 : 0)} ${units[i]}`;
|
|
277
|
+
}
|
|
278
|
+
function parseHistoryArgs(args) {
|
|
279
|
+
let project;
|
|
280
|
+
let hours = 24;
|
|
281
|
+
for (let i = 0;i < args.length; i++) {
|
|
282
|
+
const a = args[i];
|
|
283
|
+
if (a === "--hours") {
|
|
284
|
+
const n = Number(args[++i]);
|
|
285
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
286
|
+
return { hours, error: "--hours must be a positive number" };
|
|
287
|
+
hours = n;
|
|
288
|
+
} else if (a.startsWith("--hours=")) {
|
|
289
|
+
const n = Number(a.slice("--hours=".length));
|
|
290
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
291
|
+
return { hours, error: "--hours must be a positive number" };
|
|
292
|
+
hours = n;
|
|
293
|
+
} else if (!a.startsWith("-") && project === undefined) {
|
|
294
|
+
project = a;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (!project)
|
|
298
|
+
return { hours, error: "missing project" };
|
|
299
|
+
return { project, hours };
|
|
300
|
+
}
|
|
301
|
+
async function historyCommand(args) {
|
|
302
|
+
const parsed = parseHistoryArgs(args);
|
|
303
|
+
if (parsed.error || !parsed.project) {
|
|
304
|
+
console.error(parsed.error && parsed.error !== "missing project" ? parsed.error : "Usage: moor history <project> [--hours N]");
|
|
305
|
+
process.exit(1);
|
|
306
|
+
}
|
|
307
|
+
const project = await resolveProject(parsed.project);
|
|
308
|
+
const to = Date.now();
|
|
309
|
+
const from = to - parsed.hours * 3600000;
|
|
310
|
+
const res = await apiGet(`/api/projects/${project.id}/stats/history?from=${from}&to=${to}`);
|
|
311
|
+
if (!res.ok) {
|
|
312
|
+
console.error(`Failed to get history: ${res.status} ${await readErrorMessage(res)}`);
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
315
|
+
const h = await res.json();
|
|
316
|
+
const s = h.summary;
|
|
317
|
+
const windowH = Math.round((h.to_ms - h.from_ms) / 3600000 * 10) / 10;
|
|
318
|
+
console.log(`${project.name} \u2014 history over ~${windowH}h`);
|
|
319
|
+
if (s.has_gap) {
|
|
320
|
+
console.log(" \x1B[33m\u26A0 event gap recorded: events may be incomplete\x1B[0m");
|
|
321
|
+
}
|
|
322
|
+
console.log(` Samples: ${s.sample_count} total, ${s.running_sample_count} running`);
|
|
323
|
+
console.log(` CPU: avg ${s.cpu_percent_avg ?? "n/a"}% / max ${s.cpu_percent_max ?? "n/a"}%`);
|
|
324
|
+
console.log(` Memory: max ${s.mem_bytes_max !== null ? formatBytes(s.mem_bytes_max) : "n/a"}`);
|
|
325
|
+
console.log(` Network: in ${formatBytes(s.net_rx_bytes_total)} / out ${formatBytes(s.net_tx_bytes_total)}`);
|
|
326
|
+
const counts = Object.entries(s.event_counts);
|
|
327
|
+
if (counts.length > 0) {
|
|
328
|
+
console.log(` Events: ${counts.map(([a, n]) => `${a} ${n}`).join(", ")}`);
|
|
329
|
+
}
|
|
330
|
+
const recent = h.events.slice(-10);
|
|
331
|
+
if (recent.length > 0) {
|
|
332
|
+
console.log(`
|
|
333
|
+
Recent events:`);
|
|
334
|
+
for (const e of recent) {
|
|
335
|
+
console.log(` ${new Date(e.occurred_at_ms).toISOString()} ${e.action} (${e.source})`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (s.sample_count === 0 && h.events.length === 0) {
|
|
339
|
+
console.log(" (no stored history in this window)");
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// src/commands/logs.ts
|
|
344
|
+
async function logsCommand(args) {
|
|
345
|
+
let follow = false;
|
|
346
|
+
let tail = 100;
|
|
347
|
+
let projectName;
|
|
348
|
+
for (let i = 0;i < args.length; i++) {
|
|
349
|
+
if (args[i] === "-f" || args[i] === "--follow") {
|
|
350
|
+
follow = true;
|
|
351
|
+
} else if (args[i] === "-n" || args[i] === "--lines") {
|
|
352
|
+
tail = Number(args[++i]);
|
|
353
|
+
} else if (!projectName) {
|
|
354
|
+
projectName = args[i];
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (!projectName) {
|
|
358
|
+
console.error("Usage: moor logs <project> [-f] [-n <lines>]");
|
|
359
|
+
process.exit(1);
|
|
360
|
+
}
|
|
361
|
+
const project = await resolveProject(projectName);
|
|
362
|
+
const res = await apiGet(`/api/projects/${project.id}/logs?tail=${tail}`);
|
|
363
|
+
if (!res.ok) {
|
|
364
|
+
console.error(`Failed to get logs: ${res.status}`);
|
|
365
|
+
process.exit(1);
|
|
366
|
+
}
|
|
367
|
+
const data = await res.json();
|
|
368
|
+
if (data.logs)
|
|
369
|
+
process.stdout.write(data.logs);
|
|
370
|
+
if (!follow)
|
|
371
|
+
return;
|
|
372
|
+
let since = data.lastTimestamp;
|
|
373
|
+
const poll = async () => {
|
|
374
|
+
try {
|
|
375
|
+
const res2 = await apiGet(`/api/projects/${project.id}/logs?since=${since}`);
|
|
376
|
+
if (!res2.ok)
|
|
377
|
+
return;
|
|
378
|
+
const data2 = await res2.json();
|
|
379
|
+
if (data2.logs?.trim()) {
|
|
380
|
+
process.stdout.write(data2.logs);
|
|
381
|
+
since = data2.lastTimestamp;
|
|
382
|
+
}
|
|
383
|
+
} catch {}
|
|
384
|
+
};
|
|
385
|
+
const interval = setInterval(poll, 2000);
|
|
386
|
+
process.on("SIGINT", () => {
|
|
387
|
+
clearInterval(interval);
|
|
388
|
+
process.exit(0);
|
|
389
|
+
});
|
|
390
|
+
await new Promise(() => {});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// src/commands/mcp.ts
|
|
394
|
+
import { existsSync, readFileSync } from "fs";
|
|
395
|
+
import { join } from "path";
|
|
396
|
+
var CLIENT_ALIASES = {
|
|
397
|
+
claude: "claude",
|
|
398
|
+
"claude-code": "claude",
|
|
399
|
+
codex: "codex"
|
|
400
|
+
};
|
|
401
|
+
var DEFAULT_URL = "http://127.0.0.1:8080";
|
|
402
|
+
var PLACEHOLDER_KEY = "<your-api-key>";
|
|
403
|
+
function parseDotEnv(path) {
|
|
404
|
+
if (!existsSync(path))
|
|
405
|
+
return {};
|
|
406
|
+
const content = readFileSync(path, "utf8");
|
|
407
|
+
const env = {};
|
|
408
|
+
for (const rawLine of content.split(`
|
|
409
|
+
`)) {
|
|
410
|
+
const line = rawLine.trim();
|
|
411
|
+
if (!line || line.startsWith("#"))
|
|
412
|
+
continue;
|
|
413
|
+
const eq = line.indexOf("=");
|
|
414
|
+
if (eq === -1)
|
|
415
|
+
continue;
|
|
416
|
+
const key = line.slice(0, eq).trim();
|
|
417
|
+
let value = line.slice(eq + 1).trim();
|
|
418
|
+
if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
|
|
419
|
+
value = value.slice(1, -1);
|
|
420
|
+
}
|
|
421
|
+
env[key] = value;
|
|
422
|
+
}
|
|
423
|
+
return env;
|
|
424
|
+
}
|
|
425
|
+
function resolveApiKey(flagValue) {
|
|
426
|
+
if (flagValue)
|
|
427
|
+
return flagValue;
|
|
428
|
+
if (process.env.MOOR_API_KEY)
|
|
429
|
+
return process.env.MOOR_API_KEY;
|
|
430
|
+
const dotenv = parseDotEnv(join(process.cwd(), ".env"));
|
|
431
|
+
if (dotenv.MOOR_API_KEY)
|
|
432
|
+
return dotenv.MOOR_API_KEY;
|
|
433
|
+
return PLACEHOLDER_KEY;
|
|
434
|
+
}
|
|
435
|
+
function configFor(client2, url, apiKey) {
|
|
436
|
+
if (client2 === "claude") {
|
|
437
|
+
return JSON.stringify({
|
|
438
|
+
mcpServers: {
|
|
439
|
+
moor: {
|
|
440
|
+
command: "bunx",
|
|
441
|
+
args: ["@moor-sh/mcp"],
|
|
442
|
+
env: { MOOR_URL: url, MOOR_API_KEY: apiKey }
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}, null, 2);
|
|
446
|
+
}
|
|
447
|
+
return [
|
|
448
|
+
"[mcp_servers.moor]",
|
|
449
|
+
'command = "bunx"',
|
|
450
|
+
'args = ["@moor-sh/mcp"]',
|
|
451
|
+
"",
|
|
452
|
+
"[mcp_servers.moor.env]",
|
|
453
|
+
`MOOR_URL = ${JSON.stringify(url)}`,
|
|
454
|
+
`MOOR_API_KEY = ${JSON.stringify(apiKey)}`
|
|
455
|
+
].join(`
|
|
456
|
+
`);
|
|
457
|
+
}
|
|
458
|
+
function parseFlags(args) {
|
|
459
|
+
const flags = {};
|
|
460
|
+
for (let i = 0;i < args.length; i++) {
|
|
461
|
+
const a = args[i];
|
|
462
|
+
if (a === "--client" && i + 1 < args.length)
|
|
463
|
+
flags.client = args[++i];
|
|
464
|
+
else if (a === "--url" && i + 1 < args.length)
|
|
465
|
+
flags.url = args[++i];
|
|
466
|
+
else if (a === "--api-key" && i + 1 < args.length)
|
|
467
|
+
flags.apiKey = args[++i];
|
|
468
|
+
}
|
|
469
|
+
return flags;
|
|
470
|
+
}
|
|
471
|
+
function printHelp() {
|
|
472
|
+
console.log(`moor mcp config - Generate MCP client config snippet for moor
|
|
473
|
+
|
|
474
|
+
Usage: moor mcp config --client <name> [--url <url>] [--api-key <key>]
|
|
475
|
+
|
|
476
|
+
Required:
|
|
477
|
+
--client <name> One of: claude, claude-code, codex
|
|
478
|
+
|
|
479
|
+
Optional:
|
|
480
|
+
--url <url> moor URL the MCP server should reach (default: ${DEFAULT_URL})
|
|
481
|
+
--api-key <key> bearer token. Falls back to MOOR_API_KEY env, then
|
|
482
|
+
cwd's .env, then a placeholder.
|
|
483
|
+
|
|
484
|
+
Output is printed to stdout for you to paste into your client's config file:
|
|
485
|
+
Claude Code: ~/.claude.json
|
|
486
|
+
Codex: ~/.codex/config.toml`);
|
|
487
|
+
}
|
|
488
|
+
function mcpCommand(args) {
|
|
489
|
+
const subcommand = args[0];
|
|
490
|
+
if (subcommand === "--help" || subcommand === "-h" || subcommand === "help" || !subcommand) {
|
|
491
|
+
printHelp();
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
if (subcommand !== "config") {
|
|
495
|
+
console.error(`Unknown mcp subcommand: ${subcommand}
|
|
496
|
+
`);
|
|
497
|
+
printHelp();
|
|
498
|
+
process.exit(1);
|
|
499
|
+
}
|
|
500
|
+
const flags = parseFlags(args.slice(1));
|
|
501
|
+
if (!flags.client) {
|
|
502
|
+
console.error(`moor mcp config requires --client <claude|claude-code|codex>
|
|
503
|
+
`);
|
|
504
|
+
printHelp();
|
|
505
|
+
process.exit(1);
|
|
506
|
+
}
|
|
507
|
+
const client2 = CLIENT_ALIASES[flags.client];
|
|
508
|
+
if (!client2) {
|
|
509
|
+
console.error(`Unknown client: ${flags.client}. Expected one of: claude, claude-code, codex`);
|
|
510
|
+
process.exit(1);
|
|
511
|
+
}
|
|
512
|
+
const url = flags.url || DEFAULT_URL;
|
|
513
|
+
const apiKey = resolveApiKey(flags.apiKey);
|
|
514
|
+
console.log(configFor(client2, url, apiKey));
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// src/commands/rebuild.ts
|
|
518
|
+
async function rebuildCommand(args) {
|
|
519
|
+
let noCache = false;
|
|
520
|
+
let projectName;
|
|
521
|
+
for (const arg of args) {
|
|
522
|
+
if (arg === "--no-cache")
|
|
523
|
+
noCache = true;
|
|
524
|
+
else if (!projectName)
|
|
525
|
+
projectName = arg;
|
|
526
|
+
}
|
|
527
|
+
if (!projectName) {
|
|
528
|
+
console.error("Usage: moor rebuild <project> [--no-cache]");
|
|
529
|
+
process.exit(1);
|
|
530
|
+
}
|
|
531
|
+
const project = await resolveProject(projectName);
|
|
532
|
+
const query = noCache ? "?nocache=true" : "";
|
|
533
|
+
console.log(`Rebuilding ${project.name}...`);
|
|
534
|
+
const res = await apiPost(`/api/projects/${project.id}/run${query}`);
|
|
535
|
+
if (!res.ok && res.headers.get("content-type")?.includes("text/event-stream") === false) {
|
|
536
|
+
const body = await readErrorMessage(res);
|
|
537
|
+
console.error(`Failed: ${body}`);
|
|
538
|
+
process.exit(1);
|
|
539
|
+
}
|
|
540
|
+
let failed = false;
|
|
541
|
+
await streamSSE(res, {
|
|
542
|
+
onLog: (text) => process.stdout.write(text),
|
|
543
|
+
onError: (text) => {
|
|
544
|
+
process.stderr.write(`Error: ${text}
|
|
545
|
+
`);
|
|
546
|
+
failed = true;
|
|
547
|
+
},
|
|
548
|
+
onDone: (text) => console.log(text)
|
|
549
|
+
});
|
|
550
|
+
process.exit(failed ? 1 : 0);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// src/commands/restart.ts
|
|
554
|
+
async function restartCommand(args) {
|
|
555
|
+
const projectName = args[0];
|
|
556
|
+
if (!projectName) {
|
|
557
|
+
console.error("Usage: moor restart <project>");
|
|
558
|
+
process.exit(1);
|
|
559
|
+
}
|
|
560
|
+
const project = await resolveProject(projectName);
|
|
561
|
+
console.log(`Stopping ${project.name}...`);
|
|
562
|
+
const stopRes = await apiPost(`/api/projects/${project.id}/stop`);
|
|
563
|
+
if (!stopRes.ok) {
|
|
564
|
+
console.error(`Failed to stop: ${await readErrorMessage(stopRes)}`);
|
|
565
|
+
process.exit(1);
|
|
566
|
+
}
|
|
567
|
+
console.log(`Starting ${project.name}...`);
|
|
568
|
+
const startRes = await apiPost(`/api/projects/${project.id}/start`);
|
|
569
|
+
if (!startRes.ok) {
|
|
570
|
+
console.error(`Failed to start: ${await readErrorMessage(startRes)}`);
|
|
571
|
+
process.exit(1);
|
|
572
|
+
}
|
|
573
|
+
console.log(`${project.name} restarted.`);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// src/commands/stats.ts
|
|
577
|
+
async function statsCommand() {
|
|
578
|
+
const res = await apiGet("/api/server/stats");
|
|
579
|
+
if (!res.ok) {
|
|
580
|
+
console.error(`Failed to get stats: ${res.status}`);
|
|
581
|
+
process.exit(1);
|
|
582
|
+
}
|
|
583
|
+
const s = await res.json();
|
|
584
|
+
console.log(`Host: ${s.hostname}`);
|
|
585
|
+
console.log(`OS: ${s.os}`);
|
|
586
|
+
console.log(`Uptime: ${s.uptime}`);
|
|
587
|
+
console.log(`CPU: ${s.cpu.percent}% (${s.cpu.cores} cores)`);
|
|
588
|
+
console.log(`Memory: ${s.memory.used} / ${s.memory.total} (${s.memory.percent}%)`);
|
|
589
|
+
const disks = s.disks?.length ? s.disks : [{ mount: "/", ...s.disk }];
|
|
590
|
+
disks.forEach((d, i) => {
|
|
591
|
+
const prefix = i === 0 ? "Disk:" : " ";
|
|
592
|
+
const name = d.label ? `${d.label} (${d.mount})` : d.mount;
|
|
593
|
+
console.log(`${prefix} ${name} ${d.used} / ${d.total} (${d.percent}%)`);
|
|
594
|
+
});
|
|
595
|
+
console.log(`Containers: ${s.containers.running} running / ${s.containers.total} total`);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// src/commands/status.ts
|
|
599
|
+
async function statusCommand() {
|
|
600
|
+
const res = await apiGet("/api/projects");
|
|
601
|
+
if (!res.ok) {
|
|
602
|
+
console.error(`Failed to list projects: ${res.status}`);
|
|
603
|
+
process.exit(1);
|
|
604
|
+
}
|
|
605
|
+
const projects = await res.json();
|
|
606
|
+
if (projects.length === 0) {
|
|
607
|
+
console.log("No projects found.");
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
const nameW = Math.max(4, ...projects.map((p) => p.name.length));
|
|
611
|
+
const statusW = Math.max(6, ...projects.map((p) => p.status.length));
|
|
612
|
+
const sourceW = Math.max(6, ...projects.map((p) => (p.docker_image || p.github_url || "-").length));
|
|
613
|
+
const domainW = Math.max(6, ...projects.map((p) => (p.domain || "-").length));
|
|
614
|
+
const header = [
|
|
615
|
+
"NAME".padEnd(nameW),
|
|
616
|
+
"STATUS".padEnd(statusW),
|
|
617
|
+
"SOURCE".padEnd(sourceW),
|
|
618
|
+
"DOMAIN".padEnd(domainW)
|
|
619
|
+
].join(" ");
|
|
620
|
+
console.log(header);
|
|
621
|
+
console.log("-".repeat(header.length));
|
|
622
|
+
for (const p of projects) {
|
|
623
|
+
const source = p.docker_image || p.github_url || "-";
|
|
624
|
+
console.log([
|
|
625
|
+
p.name.padEnd(nameW),
|
|
626
|
+
p.status.padEnd(statusW),
|
|
627
|
+
source.padEnd(sourceW),
|
|
628
|
+
(p.domain || "-").padEnd(domainW)
|
|
629
|
+
].join(" "));
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// src/index.ts
|
|
634
|
+
var VERSION = JSON.parse(readFileSync2(join2(import.meta.dir, "..", "package.json"), "utf8")).version;
|
|
635
|
+
function printHelp2() {
|
|
636
|
+
console.log(`moor - CLI for Moor server management
|
|
637
|
+
|
|
638
|
+
Usage: moor <command> [options]
|
|
639
|
+
|
|
640
|
+
Commands:
|
|
641
|
+
status List all projects
|
|
642
|
+
logs <project> [-f] [-n <lines>] View container logs
|
|
643
|
+
rebuild <project> [--no-cache] Rebuild and restart from source
|
|
644
|
+
restart <project> Stop and start a container
|
|
645
|
+
exec <project> <command> Run a command in a container
|
|
646
|
+
env list <project> List environment variables
|
|
647
|
+
env set <project> K=V [K=V ...] Set environment variables
|
|
648
|
+
stats Show server resource usage
|
|
649
|
+
history <project> [--hours N] Stored resource history + events (default 24h)
|
|
650
|
+
mcp config --client <name> Generate MCP client config snippet
|
|
651
|
+
|
|
652
|
+
Environment:
|
|
653
|
+
MOOR_URL Server URL (e.g. https://moor.example.com)
|
|
654
|
+
MOOR_API_KEY API key for authentication`);
|
|
655
|
+
}
|
|
656
|
+
var args = process.argv.slice(2);
|
|
657
|
+
var command = args[0];
|
|
658
|
+
switch (command) {
|
|
659
|
+
case "status":
|
|
660
|
+
await statusCommand();
|
|
661
|
+
break;
|
|
662
|
+
case "logs":
|
|
663
|
+
await logsCommand(args.slice(1));
|
|
664
|
+
break;
|
|
665
|
+
case "rebuild":
|
|
666
|
+
await rebuildCommand(args.slice(1));
|
|
667
|
+
break;
|
|
668
|
+
case "restart":
|
|
669
|
+
await restartCommand(args.slice(1));
|
|
670
|
+
break;
|
|
671
|
+
case "exec":
|
|
672
|
+
await execCommand(args.slice(1));
|
|
673
|
+
break;
|
|
674
|
+
case "env":
|
|
675
|
+
await envCommand(args.slice(1));
|
|
676
|
+
break;
|
|
677
|
+
case "stats":
|
|
678
|
+
await statsCommand();
|
|
679
|
+
break;
|
|
680
|
+
case "history":
|
|
681
|
+
await historyCommand(args.slice(1));
|
|
682
|
+
break;
|
|
683
|
+
case "mcp":
|
|
684
|
+
mcpCommand(args.slice(1));
|
|
685
|
+
break;
|
|
686
|
+
case "--help":
|
|
687
|
+
case "-h":
|
|
688
|
+
case "help":
|
|
689
|
+
printHelp2();
|
|
690
|
+
break;
|
|
691
|
+
case "--version":
|
|
692
|
+
case "-v":
|
|
693
|
+
console.log(VERSION);
|
|
694
|
+
break;
|
|
695
|
+
default:
|
|
696
|
+
if (command)
|
|
697
|
+
console.error(`Unknown command: ${command}
|
|
698
|
+
`);
|
|
699
|
+
printHelp2();
|
|
700
|
+
process.exit(command ? 1 : 0);
|
|
701
|
+
}
|