openclaw-manager 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/bin/openclaw-manager.js +198 -0
- package/dist/app.js +39 -0
- package/dist/controllers/auth.controller.js +19 -0
- package/dist/controllers/cli.controller.js +10 -0
- package/dist/controllers/discord.controller.js +21 -0
- package/dist/controllers/jobs.controller.js +138 -0
- package/dist/controllers/process.controller.js +26 -0
- package/dist/controllers/quickstart.controller.js +11 -0
- package/dist/controllers/status.controller.js +10 -0
- package/dist/deps.js +1 -0
- package/dist/dev.js +156 -0
- package/dist/index.js +37 -0
- package/dist/lib/auth.js +57 -0
- package/dist/lib/commands.js +123 -0
- package/dist/lib/config.js +48 -0
- package/dist/lib/constants.js +9 -0
- package/dist/lib/gateway.js +37 -0
- package/dist/lib/jobs.js +117 -0
- package/dist/lib/onboarding.js +96 -0
- package/dist/lib/runner.js +99 -0
- package/dist/lib/static.js +69 -0
- package/dist/lib/system.js +31 -0
- package/dist/lib/utils.js +31 -0
- package/dist/middlewares/auth.js +23 -0
- package/dist/routes/auth.js +5 -0
- package/dist/routes/cli.js +4 -0
- package/dist/routes/discord.js +5 -0
- package/dist/routes/health.js +9 -0
- package/dist/routes/index.js +18 -0
- package/dist/routes/jobs.js +11 -0
- package/dist/routes/processes.js +6 -0
- package/dist/routes/quickstart.js +4 -0
- package/dist/routes/status.js +4 -0
- package/dist/services/auth.service.js +24 -0
- package/dist/services/cli.service.js +21 -0
- package/dist/services/discord.service.js +38 -0
- package/dist/services/jobs.service.js +307 -0
- package/dist/services/process.service.js +9 -0
- package/dist/services/quickstart.service.js +124 -0
- package/dist/services/resource.service.js +46 -0
- package/dist/services/status.service.js +32 -0
- package/package.json +18 -0
- package/web-dist/assets/index-BabnD_ew.js +13 -0
- package/web-dist/assets/index-CBtcOjoT.css +1 -0
- package/web-dist/docker.sh +62 -0
- package/web-dist/index.html +13 -0
- package/web-dist/install.ps1 +110 -0
- package/web-dist/install.sh +261 -0
- package/web-dist/stop.sh +52 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { randomBytes, scryptSync } from "node:crypto";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import process from "node:process";
|
|
8
|
+
import readline from "node:readline";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
const cmd = args.find((arg) => !arg.startsWith("-")) ?? "start";
|
|
13
|
+
|
|
14
|
+
if (args.includes("-h") || args.includes("--help") || cmd === "help") {
|
|
15
|
+
printHelp();
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (args.includes("-v") || args.includes("--version")) {
|
|
20
|
+
console.log("openclaw-manager 0.1.0");
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (cmd !== "start") {
|
|
25
|
+
console.error(`[manager] Unknown command: ${cmd}`);
|
|
26
|
+
printHelp();
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
void start();
|
|
31
|
+
|
|
32
|
+
async function start() {
|
|
33
|
+
const apiPort = process.env.MANAGER_API_PORT ?? "17321";
|
|
34
|
+
const apiHost = process.env.MANAGER_API_HOST ?? "0.0.0.0";
|
|
35
|
+
const configDir = process.env.MANAGER_CONFIG_DIR ?? path.join(os.homedir(), ".openclaw-manager");
|
|
36
|
+
const configPath =
|
|
37
|
+
process.env.MANAGER_CONFIG_PATH ?? path.join(configDir, "config.json");
|
|
38
|
+
const logPath =
|
|
39
|
+
process.env.MANAGER_LOG_PATH ?? path.join(configDir, "openclaw-manager.log");
|
|
40
|
+
const errorLogPath =
|
|
41
|
+
process.env.MANAGER_ERROR_LOG_PATH ??
|
|
42
|
+
path.join(configDir, "openclaw-manager.error.log");
|
|
43
|
+
const pidPath = path.join(configDir, "manager.pid");
|
|
44
|
+
|
|
45
|
+
ensureDir(configDir);
|
|
46
|
+
ensureDir(path.dirname(logPath));
|
|
47
|
+
ensureDir(path.dirname(errorLogPath));
|
|
48
|
+
|
|
49
|
+
if (isRunning(pidPath)) {
|
|
50
|
+
const pid = fs.readFileSync(pidPath, "utf-8").trim();
|
|
51
|
+
console.log(`[manager] Already running (pid: ${pid}).`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!fs.existsSync(configPath)) {
|
|
56
|
+
const username =
|
|
57
|
+
process.env.MANAGER_ADMIN_USER ??
|
|
58
|
+
process.env.OPENCLAW_MANAGER_ADMIN_USER ??
|
|
59
|
+
(await promptLine("Admin username: "));
|
|
60
|
+
const password =
|
|
61
|
+
process.env.MANAGER_ADMIN_PASS ??
|
|
62
|
+
process.env.OPENCLAW_MANAGER_ADMIN_PASS ??
|
|
63
|
+
(await promptSecret("Admin password: "));
|
|
64
|
+
if (!username || !password) {
|
|
65
|
+
console.error("[manager] Admin username/password is required.");
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
writeAdminConfig(configPath, username, password);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const pkgRoot = resolvePackageRoot();
|
|
72
|
+
const apiEntry = path.join(pkgRoot, "dist", "index.js");
|
|
73
|
+
const webDist = path.join(pkgRoot, "web-dist");
|
|
74
|
+
|
|
75
|
+
if (!fs.existsSync(apiEntry) || !fs.existsSync(webDist)) {
|
|
76
|
+
console.error("[manager] Package is missing build artifacts.");
|
|
77
|
+
console.error("[manager] Please reinstall or use a release that includes dist assets.");
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const out = fs.openSync(logPath, "a");
|
|
82
|
+
const err = fs.openSync(errorLogPath, "a");
|
|
83
|
+
const child = spawn(process.execPath, [apiEntry], {
|
|
84
|
+
env: {
|
|
85
|
+
...process.env,
|
|
86
|
+
MANAGER_API_HOST: apiHost,
|
|
87
|
+
MANAGER_API_PORT: apiPort,
|
|
88
|
+
MANAGER_WEB_DIST: webDist,
|
|
89
|
+
MANAGER_CONFIG_PATH: configPath
|
|
90
|
+
},
|
|
91
|
+
detached: true,
|
|
92
|
+
stdio: ["ignore", out, err]
|
|
93
|
+
});
|
|
94
|
+
child.unref();
|
|
95
|
+
|
|
96
|
+
fs.writeFileSync(pidPath, String(child.pid), "utf-8");
|
|
97
|
+
|
|
98
|
+
const lanIp = resolveLanIp();
|
|
99
|
+
console.log(`[manager] Started (pid: ${child.pid}).`);
|
|
100
|
+
console.log(`[manager] Log: ${logPath}`);
|
|
101
|
+
console.log(`[manager] Error log: ${errorLogPath}`);
|
|
102
|
+
console.log(`[manager] Open (local): http://localhost:${apiPort}`);
|
|
103
|
+
console.log(`[manager] Open (local): http://127.0.0.1:${apiPort}`);
|
|
104
|
+
if (lanIp) {
|
|
105
|
+
console.log(`[manager] Open (LAN): http://${lanIp}:${apiPort}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function ensureDir(dir) {
|
|
110
|
+
if (!dir) return;
|
|
111
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isRunning(pidPath) {
|
|
115
|
+
if (!fs.existsSync(pidPath)) return false;
|
|
116
|
+
const raw = fs.readFileSync(pidPath, "utf-8").trim();
|
|
117
|
+
const pid = Number(raw);
|
|
118
|
+
if (!pid) return false;
|
|
119
|
+
try {
|
|
120
|
+
process.kill(pid, 0);
|
|
121
|
+
return true;
|
|
122
|
+
} catch {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function writeAdminConfig(configPath, username, password) {
|
|
128
|
+
const salt = randomBytes(16).toString("base64");
|
|
129
|
+
const hash = scryptSync(password, salt, 64).toString("base64");
|
|
130
|
+
const payload = {
|
|
131
|
+
auth: {
|
|
132
|
+
username,
|
|
133
|
+
salt,
|
|
134
|
+
hash
|
|
135
|
+
},
|
|
136
|
+
createdAt: new Date().toISOString()
|
|
137
|
+
};
|
|
138
|
+
ensureDir(path.dirname(configPath));
|
|
139
|
+
fs.writeFileSync(configPath, JSON.stringify(payload, null, 2));
|
|
140
|
+
console.log(`[manager] Admin config saved to ${configPath}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function promptLine(prompt) {
|
|
144
|
+
if (!process.stdin.isTTY) return "";
|
|
145
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
146
|
+
const answer = await new Promise((resolve) => rl.question(prompt, resolve));
|
|
147
|
+
rl.close();
|
|
148
|
+
return String(answer).trim();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function promptSecret(prompt) {
|
|
152
|
+
if (!process.stdin.isTTY) return "";
|
|
153
|
+
return new Promise((resolve) => {
|
|
154
|
+
const stdin = process.stdin;
|
|
155
|
+
const stdout = process.stdout;
|
|
156
|
+
let value = "";
|
|
157
|
+
stdout.write(prompt);
|
|
158
|
+
stdin.setRawMode(true);
|
|
159
|
+
stdin.resume();
|
|
160
|
+
const onData = (data) => {
|
|
161
|
+
const char = data.toString();
|
|
162
|
+
if (char === "\n" || char === "\r") {
|
|
163
|
+
stdout.write("\n");
|
|
164
|
+
stdin.setRawMode(false);
|
|
165
|
+
stdin.pause();
|
|
166
|
+
stdin.removeListener("data", onData);
|
|
167
|
+
resolve(value.trim());
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (char === "\u0003") {
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
value += char;
|
|
174
|
+
};
|
|
175
|
+
stdin.on("data", onData);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function resolveLanIp() {
|
|
180
|
+
const nets = os.networkInterfaces();
|
|
181
|
+
for (const name of Object.keys(nets)) {
|
|
182
|
+
for (const net of nets[name] ?? []) {
|
|
183
|
+
if (net.family === "IPv4" && !net.internal) {
|
|
184
|
+
return net.address;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function resolvePackageRoot() {
|
|
192
|
+
const filePath = fileURLToPath(import.meta.url);
|
|
193
|
+
return path.resolve(path.dirname(filePath), "..");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function printHelp() {
|
|
197
|
+
console.log(`openclaw-manager\n\nUsage:\n openclaw-manager start\n\nOptions:\n -h, --help Show help\n -v, --version Show version\n`);
|
|
198
|
+
}
|
package/dist/app.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { cors } from "hono/cors";
|
|
3
|
+
import { serveStaticFile } from "./lib/static.js";
|
|
4
|
+
import { createAuthMiddleware } from "./middlewares/auth.js";
|
|
5
|
+
import { registerRoutes } from "./routes/index.js";
|
|
6
|
+
const DEFAULT_CORS_ORIGINS = [
|
|
7
|
+
"http://localhost:5173",
|
|
8
|
+
"http://127.0.0.1:5173",
|
|
9
|
+
"http://localhost:5179",
|
|
10
|
+
"http://127.0.0.1:5179"
|
|
11
|
+
];
|
|
12
|
+
export function createApp(deps, options) {
|
|
13
|
+
const app = new Hono();
|
|
14
|
+
const allowed = new Set([...DEFAULT_CORS_ORIGINS, ...options.corsOrigins]);
|
|
15
|
+
app.use("*", cors({
|
|
16
|
+
origin: (origin) => {
|
|
17
|
+
if (!origin)
|
|
18
|
+
return "*";
|
|
19
|
+
if (allowed.has("*"))
|
|
20
|
+
return "*";
|
|
21
|
+
if (allowed.has(origin))
|
|
22
|
+
return origin;
|
|
23
|
+
return "null";
|
|
24
|
+
},
|
|
25
|
+
allowHeaders: ["Content-Type", "Authorization"]
|
|
26
|
+
}));
|
|
27
|
+
app.use("/api/*", createAuthMiddleware(deps));
|
|
28
|
+
registerRoutes(app, deps);
|
|
29
|
+
if (options.webDist) {
|
|
30
|
+
app.get("*", async (c) => {
|
|
31
|
+
const reqPath = c.req.path;
|
|
32
|
+
if (reqPath.startsWith("/api") || reqPath === "/health") {
|
|
33
|
+
return c.notFound();
|
|
34
|
+
}
|
|
35
|
+
return serveStaticFile(c.req.path, options.webDist ?? "");
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return app;
|
|
39
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { getAuthStatus, loginWithCredentials } from "../services/auth.service.js";
|
|
2
|
+
export function createAuthStatusHandler(deps) {
|
|
3
|
+
return (c) => {
|
|
4
|
+
const status = getAuthStatus(deps.auth.disabled);
|
|
5
|
+
return c.json({ ok: true, ...status });
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
export function createAuthLoginHandler(deps) {
|
|
9
|
+
return async (c) => {
|
|
10
|
+
const body = await c.req.json().catch(() => null);
|
|
11
|
+
const username = typeof body?.username === "string" ? body.username.trim() : "";
|
|
12
|
+
const password = typeof body?.password === "string" ? body.password : "";
|
|
13
|
+
const result = loginWithCredentials(deps.auth.disabled, username, password);
|
|
14
|
+
if (result.ok) {
|
|
15
|
+
return c.json({ ok: true, disabled: result.disabled });
|
|
16
|
+
}
|
|
17
|
+
return c.json({ ok: false, error: result.error }, result.status);
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { installCli } from "../services/cli.service.js";
|
|
2
|
+
export function createCliInstallHandler(deps) {
|
|
3
|
+
return async (c) => {
|
|
4
|
+
const result = await installCli(deps.runCommand);
|
|
5
|
+
if (result.ok) {
|
|
6
|
+
return c.json(result);
|
|
7
|
+
}
|
|
8
|
+
return c.json(result, 500);
|
|
9
|
+
};
|
|
10
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { approveDiscordPairing, saveDiscordToken } from "../services/discord.service.js";
|
|
2
|
+
export function createDiscordTokenHandler(deps) {
|
|
3
|
+
return async (c) => {
|
|
4
|
+
const body = await c.req.json().catch(() => null);
|
|
5
|
+
const token = typeof body?.token === "string" ? body.token.trim() : "";
|
|
6
|
+
if (!token)
|
|
7
|
+
return c.json({ ok: false, error: "missing token" }, 400);
|
|
8
|
+
const result = await saveDiscordToken(deps.runCommand, token);
|
|
9
|
+
return c.json(result, result.ok ? 200 : 500);
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export function createDiscordPairingHandler(deps) {
|
|
13
|
+
return async (c) => {
|
|
14
|
+
const body = await c.req.json().catch(() => null);
|
|
15
|
+
const code = typeof body?.code === "string" ? body.code.trim().toUpperCase() : "";
|
|
16
|
+
if (!code)
|
|
17
|
+
return c.json({ ok: false, error: "missing code" }, 400);
|
|
18
|
+
const result = await approveDiscordPairing(deps.runCommand, code);
|
|
19
|
+
return c.json(result, result.ok ? 200 : 500);
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { createCliInstallJob, createAiAuthJob, createDiscordPairingJob, createDiscordPairingWaitJob, createQuickstartJob, createResourceDownloadJob } from "../services/jobs.service.js";
|
|
2
|
+
export function createCliInstallJobHandler(deps) {
|
|
3
|
+
return () => {
|
|
4
|
+
const jobId = createCliInstallJob(deps);
|
|
5
|
+
return new Response(JSON.stringify({ ok: true, jobId }), {
|
|
6
|
+
status: 200,
|
|
7
|
+
headers: { "content-type": "application/json" }
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function createQuickstartJobHandler(deps) {
|
|
12
|
+
return async (c) => {
|
|
13
|
+
const body = (await c.req.json().catch(() => ({})));
|
|
14
|
+
const jobId = createQuickstartJob(deps, body);
|
|
15
|
+
return c.json({ ok: true, jobId });
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function createDiscordPairingJobHandler(deps) {
|
|
19
|
+
return async (c) => {
|
|
20
|
+
const body = await c.req.json().catch(() => null);
|
|
21
|
+
const code = typeof body?.code === "string" ? body.code.trim().toUpperCase() : "";
|
|
22
|
+
if (!code)
|
|
23
|
+
return c.json({ ok: false, error: "missing code" }, 400);
|
|
24
|
+
const jobId = createDiscordPairingJob(deps, code);
|
|
25
|
+
return c.json({ ok: true, jobId });
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function createDiscordPairingWaitJobHandler(deps) {
|
|
29
|
+
return async (c) => {
|
|
30
|
+
const body = await c.req.json().catch(() => null);
|
|
31
|
+
const timeoutMs = body?.timeoutMs;
|
|
32
|
+
const pollMs = body?.pollMs;
|
|
33
|
+
const notify = Boolean(body?.notify);
|
|
34
|
+
const jobId = createDiscordPairingWaitJob(deps, { timeoutMs, pollMs, notify });
|
|
35
|
+
return c.json({ ok: true, jobId });
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function createResourceDownloadJobHandler(deps) {
|
|
39
|
+
return async (c) => {
|
|
40
|
+
const body = await c.req.json().catch(() => null);
|
|
41
|
+
const url = typeof body?.url === "string" ? body.url.trim() : undefined;
|
|
42
|
+
const filename = typeof body?.filename === "string" ? body.filename.trim() : undefined;
|
|
43
|
+
const jobId = createResourceDownloadJob(deps, { url, filename });
|
|
44
|
+
return c.json({ ok: true, jobId });
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function createAiAuthJobHandler(deps) {
|
|
48
|
+
return async (c) => {
|
|
49
|
+
const body = await c.req.json().catch(() => null);
|
|
50
|
+
const provider = typeof body?.provider === "string" ? body.provider.trim() : "";
|
|
51
|
+
const apiKey = typeof body?.apiKey === "string" ? body.apiKey.trim() : "";
|
|
52
|
+
if (!provider)
|
|
53
|
+
return c.json({ ok: false, error: "missing provider" }, 400);
|
|
54
|
+
if (!apiKey)
|
|
55
|
+
return c.json({ ok: false, error: "missing apiKey" }, 400);
|
|
56
|
+
const jobId = createAiAuthJob(deps, { provider, apiKey });
|
|
57
|
+
return c.json({ ok: true, jobId });
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export function createJobStatusHandler(deps) {
|
|
61
|
+
return (c) => {
|
|
62
|
+
const jobId = c.req.param("id");
|
|
63
|
+
const job = deps.jobStore.getJob(jobId);
|
|
64
|
+
if (!job) {
|
|
65
|
+
return c.json({ ok: false, error: "not found" }, 404);
|
|
66
|
+
}
|
|
67
|
+
return c.json({ ok: true, job });
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export function createJobStreamHandler(deps) {
|
|
71
|
+
return (c) => {
|
|
72
|
+
const jobId = c.req.param("id");
|
|
73
|
+
const job = deps.jobStore.getJob(jobId);
|
|
74
|
+
if (!job) {
|
|
75
|
+
return c.json({ ok: false, error: "not found" }, 404);
|
|
76
|
+
}
|
|
77
|
+
const encoder = new TextEncoder();
|
|
78
|
+
const stream = new ReadableStream({
|
|
79
|
+
start(controller) {
|
|
80
|
+
const send = (event, data) => {
|
|
81
|
+
const payload = `event: ${event}\n` + `data: ${JSON.stringify(data)}\n\n`;
|
|
82
|
+
controller.enqueue(encoder.encode(payload));
|
|
83
|
+
};
|
|
84
|
+
send("status", {
|
|
85
|
+
status: job.status,
|
|
86
|
+
createdAt: job.createdAt,
|
|
87
|
+
startedAt: job.startedAt,
|
|
88
|
+
endedAt: job.endedAt
|
|
89
|
+
});
|
|
90
|
+
for (const line of job.logs) {
|
|
91
|
+
send("log", { message: line });
|
|
92
|
+
}
|
|
93
|
+
if (job.status === "success") {
|
|
94
|
+
send("done", { result: job.result ?? null });
|
|
95
|
+
controller.close();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (job.status === "failed") {
|
|
99
|
+
send("error", { error: job.error ?? "failed" });
|
|
100
|
+
controller.close();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const unsubscribe = deps.jobStore.subscribe(jobId, (event) => {
|
|
104
|
+
if (event.type === "log") {
|
|
105
|
+
send("log", { message: event.message });
|
|
106
|
+
}
|
|
107
|
+
else if (event.type === "status") {
|
|
108
|
+
send("status", { status: event.status });
|
|
109
|
+
}
|
|
110
|
+
else if (event.type === "done") {
|
|
111
|
+
send("done", { result: event.result ?? null });
|
|
112
|
+
cleanup();
|
|
113
|
+
controller.close();
|
|
114
|
+
}
|
|
115
|
+
else if (event.type === "error") {
|
|
116
|
+
send("error", { error: event.error });
|
|
117
|
+
cleanup();
|
|
118
|
+
controller.close();
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
const keepAlive = setInterval(() => {
|
|
122
|
+
controller.enqueue(encoder.encode(": keep-alive\n\n"));
|
|
123
|
+
}, 15000);
|
|
124
|
+
const cleanup = () => {
|
|
125
|
+
clearInterval(keepAlive);
|
|
126
|
+
unsubscribe();
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
return new Response(stream, {
|
|
131
|
+
headers: {
|
|
132
|
+
"content-type": "text/event-stream",
|
|
133
|
+
"cache-control": "no-cache",
|
|
134
|
+
connection: "keep-alive"
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { listProcesses, startProcess, stopProcess } from "../services/process.service.js";
|
|
2
|
+
export function createProcessListHandler(deps) {
|
|
3
|
+
return (c) => {
|
|
4
|
+
return c.json({ ok: true, processes: listProcesses(deps) });
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
export function createProcessStartHandler(deps) {
|
|
8
|
+
return async (c) => {
|
|
9
|
+
const body = await c.req.json().catch(() => null);
|
|
10
|
+
const id = typeof body?.id === "string" ? body.id : null;
|
|
11
|
+
if (!id)
|
|
12
|
+
return c.json({ ok: false, error: "missing id" }, 400);
|
|
13
|
+
const result = startProcess(deps, id);
|
|
14
|
+
return c.json(result.ok ? { ok: true, process: result.process } : result, result.ok ? 200 : 400);
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function createProcessStopHandler(deps) {
|
|
18
|
+
return async (c) => {
|
|
19
|
+
const body = await c.req.json().catch(() => null);
|
|
20
|
+
const id = typeof body?.id === "string" ? body.id : null;
|
|
21
|
+
if (!id)
|
|
22
|
+
return c.json({ ok: false, error: "missing id" }, 400);
|
|
23
|
+
const result = stopProcess(deps, id);
|
|
24
|
+
return c.json(result.ok ? { ok: true, process: result.process } : result, result.ok ? 200 : 400);
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { runQuickstart } from "../services/quickstart.service.js";
|
|
2
|
+
export function createQuickstartHandler(deps) {
|
|
3
|
+
return async (c) => {
|
|
4
|
+
const body = (await c.req.json().catch(() => ({})));
|
|
5
|
+
const result = await runQuickstart(deps, body);
|
|
6
|
+
if (!result.ok) {
|
|
7
|
+
return c.json({ ok: false, error: result.error }, result.status);
|
|
8
|
+
}
|
|
9
|
+
return c.json(result);
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { buildStatus } from "../services/status.service.js";
|
|
2
|
+
export function createStatusHandler(deps) {
|
|
3
|
+
return async (c) => {
|
|
4
|
+
const data = await buildStatus(deps, {
|
|
5
|
+
gatewayHost: c.req.query("gatewayHost") ?? undefined,
|
|
6
|
+
gatewayPort: c.req.query("gatewayPort") ?? undefined
|
|
7
|
+
});
|
|
8
|
+
return c.json(data);
|
|
9
|
+
};
|
|
10
|
+
}
|
package/dist/deps.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/dev.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
const DEFAULT_API_PORT = 17321;
|
|
8
|
+
const DEFAULT_WEB_URL = "http://127.0.0.1:5179";
|
|
9
|
+
const repoRoot = resolveRepoRoot();
|
|
10
|
+
const apiPort = Number(process.env.MANAGER_API_PORT ?? process.env.ONBOARDING_API_PORT ?? DEFAULT_API_PORT);
|
|
11
|
+
const webUrl = process.env.MANAGER_WEB_URL ?? process.env.ONBOARDING_WEB_URL ?? DEFAULT_WEB_URL;
|
|
12
|
+
const openBrowser = (process.env.MANAGER_OPEN_BROWSER ?? process.env.ONBOARDING_OPEN_BROWSER) !== "0";
|
|
13
|
+
const apiBaseUrl = `http://127.0.0.1:${apiPort}`;
|
|
14
|
+
const viteCacheDir = process.env.VITE_CACHE_DIR ?? path.join(os.tmpdir(), "clawdbot-manager-vite");
|
|
15
|
+
const apiProcess = spawnWithLabel("api", "pnpm", ["--filter", "clawdbot-manager-api", "dev"], {
|
|
16
|
+
cwd: repoRoot,
|
|
17
|
+
env: {
|
|
18
|
+
...process.env,
|
|
19
|
+
MANAGER_API_PORT: String(apiPort),
|
|
20
|
+
ONBOARDING_API_PORT: String(apiPort),
|
|
21
|
+
MANAGER_AUTH_DISABLED: process.env.MANAGER_AUTH_DISABLED ?? "1"
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
const webProcess = spawnWithLabel("web", "pnpm", ["--filter", "clawdbot-manager-web", "dev"], {
|
|
25
|
+
cwd: repoRoot,
|
|
26
|
+
env: {
|
|
27
|
+
...process.env,
|
|
28
|
+
VITE_CACHE_DIR: viteCacheDir,
|
|
29
|
+
VITE_MANAGER_API_URL: apiBaseUrl,
|
|
30
|
+
VITE_ONBOARDING_API_URL: apiBaseUrl
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
const shutdown = () => {
|
|
34
|
+
apiProcess.kill("SIGTERM");
|
|
35
|
+
webProcess.kill("SIGTERM");
|
|
36
|
+
setTimeout(() => {
|
|
37
|
+
apiProcess.kill("SIGKILL");
|
|
38
|
+
webProcess.kill("SIGKILL");
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}, 2000).unref();
|
|
41
|
+
};
|
|
42
|
+
process.on("SIGINT", shutdown);
|
|
43
|
+
process.on("SIGTERM", shutdown);
|
|
44
|
+
(async () => {
|
|
45
|
+
const ready = await waitForUrl(webUrl, 20_000);
|
|
46
|
+
if (!ready) {
|
|
47
|
+
console.error(`[manager] UI did not become ready at ${webUrl}`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
console.log(`[manager] UI ready: ${webUrl}`);
|
|
51
|
+
if (openBrowser) {
|
|
52
|
+
await openInBrowser(webUrl);
|
|
53
|
+
}
|
|
54
|
+
})();
|
|
55
|
+
function spawnWithLabel(label, command, args, options) {
|
|
56
|
+
const child = spawn(command, args, {
|
|
57
|
+
cwd: options.cwd,
|
|
58
|
+
env: options.env,
|
|
59
|
+
stdio: ["inherit", "pipe", "pipe"]
|
|
60
|
+
});
|
|
61
|
+
const prefix = `[${label}] `;
|
|
62
|
+
child.stdout?.on("data", (chunk) => {
|
|
63
|
+
process.stdout.write(prefix + chunk.toString());
|
|
64
|
+
});
|
|
65
|
+
child.stderr?.on("data", (chunk) => {
|
|
66
|
+
process.stderr.write(prefix + chunk.toString());
|
|
67
|
+
});
|
|
68
|
+
child.on("exit", (code) => {
|
|
69
|
+
if (code && code !== 0) {
|
|
70
|
+
console.error(`[${label}] exited with code ${code}`);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
return child;
|
|
74
|
+
}
|
|
75
|
+
async function waitForUrl(url, timeoutMs) {
|
|
76
|
+
const start = Date.now();
|
|
77
|
+
while (Date.now() - start < timeoutMs) {
|
|
78
|
+
try {
|
|
79
|
+
const res = await fetch(url, { method: "GET" });
|
|
80
|
+
if (res.ok)
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// retry
|
|
85
|
+
}
|
|
86
|
+
await sleep(400);
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
async function openInBrowser(url) {
|
|
91
|
+
const platform = process.platform;
|
|
92
|
+
if (platform === "darwin") {
|
|
93
|
+
await spawnDetached("open", [url]);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (platform === "win32") {
|
|
97
|
+
await spawnDetached("cmd", ["/c", "start", "", url]);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
await spawnDetached("xdg-open", [url]);
|
|
101
|
+
}
|
|
102
|
+
async function spawnDetached(command, args) {
|
|
103
|
+
try {
|
|
104
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
105
|
+
child.on("error", () => { });
|
|
106
|
+
child.unref();
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// ignore
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function sleep(ms) {
|
|
113
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
114
|
+
}
|
|
115
|
+
function resolveRepoRoot() {
|
|
116
|
+
const envRoot = process.env.MANAGER_REPO_ROOT ?? process.env.ONBOARDING_REPO_ROOT;
|
|
117
|
+
if (envRoot)
|
|
118
|
+
return path.resolve(envRoot);
|
|
119
|
+
const startPoints = [process.cwd(), path.dirname(fileURLToPath(import.meta.url))];
|
|
120
|
+
for (const start of startPoints) {
|
|
121
|
+
const found = findRepoRoot(start);
|
|
122
|
+
if (found)
|
|
123
|
+
return found;
|
|
124
|
+
}
|
|
125
|
+
return path.resolve(process.cwd(), "../..");
|
|
126
|
+
}
|
|
127
|
+
function findRepoRoot(start) {
|
|
128
|
+
let current = start;
|
|
129
|
+
for (let i = 0; i < 6; i += 1) {
|
|
130
|
+
const candidate = path.join(current, "package.json");
|
|
131
|
+
if (exists(candidate)) {
|
|
132
|
+
try {
|
|
133
|
+
const raw = fs.readFileSync(candidate, "utf-8");
|
|
134
|
+
const parsed = JSON.parse(raw);
|
|
135
|
+
if (parsed.name === "clawdbot-manager")
|
|
136
|
+
return current;
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// ignore and continue
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const parent = path.dirname(current);
|
|
143
|
+
if (parent === current)
|
|
144
|
+
break;
|
|
145
|
+
current = parent;
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
function exists(p) {
|
|
150
|
+
try {
|
|
151
|
+
return Boolean(p && fs.existsSync(p));
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { serve } from "@hono/node-server";
|
|
2
|
+
import { createApp } from "./app.js";
|
|
3
|
+
import { DEFAULT_API_HOST, DEFAULT_API_PORT } from "./lib/constants.js";
|
|
4
|
+
import { resolveRepoRoot, resolveWebDist } from "./lib/config.js";
|
|
5
|
+
import { buildCommandRegistry, createProcessManager } from "./lib/commands.js";
|
|
6
|
+
import { createJobStore } from "./lib/jobs.js";
|
|
7
|
+
import { createCommandRunner } from "./lib/runner.js";
|
|
8
|
+
import { parseOrigins, parsePort } from "./lib/utils.js";
|
|
9
|
+
const repoRoot = resolveRepoRoot();
|
|
10
|
+
const webDist = resolveWebDist(repoRoot);
|
|
11
|
+
const commandRegistry = buildCommandRegistry(repoRoot);
|
|
12
|
+
const processManager = createProcessManager(commandRegistry);
|
|
13
|
+
const runCommand = createCommandRunner(repoRoot);
|
|
14
|
+
const jobStore = createJobStore();
|
|
15
|
+
const deps = {
|
|
16
|
+
repoRoot,
|
|
17
|
+
runCommand,
|
|
18
|
+
commandRegistry,
|
|
19
|
+
processManager,
|
|
20
|
+
jobStore,
|
|
21
|
+
auth: {
|
|
22
|
+
disabled: process.env.MANAGER_AUTH_DISABLED === "1",
|
|
23
|
+
allowUnconfigured: process.env.MANAGER_AUTH_ALLOW_UNCONFIGURED === "1"
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const host = process.env.MANAGER_API_HOST ?? process.env.ONBOARDING_API_HOST ?? DEFAULT_API_HOST;
|
|
27
|
+
const port = parsePort(process.env.MANAGER_API_PORT ?? process.env.ONBOARDING_API_PORT) ??
|
|
28
|
+
DEFAULT_API_PORT;
|
|
29
|
+
const app = createApp(deps, {
|
|
30
|
+
corsOrigins: parseOrigins(process.env.MANAGER_CORS_ORIGIN),
|
|
31
|
+
webDist
|
|
32
|
+
});
|
|
33
|
+
serve({
|
|
34
|
+
fetch: app.fetch,
|
|
35
|
+
hostname: host,
|
|
36
|
+
port
|
|
37
|
+
});
|