openclaw-manager 0.1.2 → 0.1.4
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/commands/reset.js +66 -0
- package/bin/commands/start.js +108 -0
- package/bin/commands/stop-all.js +65 -0
- package/bin/commands/stop.js +63 -0
- package/bin/lib/args.js +88 -0
- package/bin/lib/banner.js +8 -0
- package/bin/lib/color.js +16 -0
- package/bin/lib/config.js +74 -0
- package/bin/lib/help.js +9 -0
- package/bin/lib/paths.js +6 -0
- package/bin/lib/pids.js +22 -0
- package/bin/lib/sandbox.js +29 -0
- package/bin/lib/system.js +31 -0
- package/bin/lib/types.js +1 -0
- package/bin/lib/version.js +17 -0
- package/bin/openclaw-manager.js +48 -499
- package/package.json +10 -1
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { resolveConfigPaths } from "../lib/config.js";
|
|
5
|
+
import { stopAll } from "./stop-all.js";
|
|
6
|
+
import { listSandboxDirs, removeSandboxDir } from "../lib/sandbox.js";
|
|
7
|
+
export function resetManager(flags) {
|
|
8
|
+
const messages = [];
|
|
9
|
+
const errors = [];
|
|
10
|
+
const stopResult = stopAll(flags);
|
|
11
|
+
messages.push(...stopResult.messages);
|
|
12
|
+
if (!stopResult.ok && stopResult.error) {
|
|
13
|
+
messages.push(`warn: stop-all failed (${stopResult.error})`);
|
|
14
|
+
}
|
|
15
|
+
const configPath = resolveConfigPaths(flags);
|
|
16
|
+
if (isSafeConfigDir(configPath.configDir)) {
|
|
17
|
+
try {
|
|
18
|
+
if (fs.existsSync(configPath.configDir)) {
|
|
19
|
+
fs.rmSync(configPath.configDir, { recursive: true, force: true });
|
|
20
|
+
messages.push(`config: removed (${configPath.configDir})`);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
messages.push(`config: not found (${configPath.configDir})`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
errors.push(`config: failed to remove (${configPath.configDir}): ${String(err)}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
errors.push(`config: refuse remove unsafe path (${configPath.configDir})`);
|
|
32
|
+
}
|
|
33
|
+
const sandboxes = listSandboxDirs();
|
|
34
|
+
if (!sandboxes.length) {
|
|
35
|
+
messages.push("sandbox: none");
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
for (const dir of sandboxes) {
|
|
39
|
+
const result = removeSandboxDir(dir);
|
|
40
|
+
if (result.ok) {
|
|
41
|
+
messages.push(`sandbox: ${result.message}`);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
errors.push(`sandbox: ${result.error ?? "remove failed"}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (errors.length) {
|
|
49
|
+
return { ok: false, messages, error: errors.join("; ") };
|
|
50
|
+
}
|
|
51
|
+
return { ok: true, messages };
|
|
52
|
+
}
|
|
53
|
+
function isSafeConfigDir(target) {
|
|
54
|
+
const resolved = path.resolve(target);
|
|
55
|
+
const home = os.homedir();
|
|
56
|
+
if (!resolved.startsWith(home))
|
|
57
|
+
return false;
|
|
58
|
+
const base = path.basename(resolved);
|
|
59
|
+
if (base.includes("openclaw-manager") || base.includes("clawdbot-manager")) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
return (resolved.endsWith(".openclaw-manager") ||
|
|
63
|
+
resolved.endsWith(".clawdbot-manager") ||
|
|
64
|
+
resolved.includes(`${path.sep}.openclaw-manager${path.sep}`) ||
|
|
65
|
+
resolved.includes(`${path.sep}.clawdbot-manager${path.sep}`));
|
|
66
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
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 prompts from "prompts";
|
|
6
|
+
import { ensureDir, hasAdminConfig, resolveConfigPaths, writeAdminConfig } from "../lib/config.js";
|
|
7
|
+
import { isPidRunning, readPid } from "../lib/pids.js";
|
|
8
|
+
import { resolvePackageRoot } from "../lib/paths.js";
|
|
9
|
+
export async function startManager(flags) {
|
|
10
|
+
const paths = resolveConfigPaths(flags);
|
|
11
|
+
ensureDir(paths.configDir);
|
|
12
|
+
ensureDir(path.dirname(paths.logPath));
|
|
13
|
+
ensureDir(path.dirname(paths.errorLogPath));
|
|
14
|
+
if (fs.existsSync(paths.pidPath)) {
|
|
15
|
+
const pid = readPid(paths.pidPath);
|
|
16
|
+
if (pid && isPidRunning(pid)) {
|
|
17
|
+
console.log(`[manager] Already running (pid: ${pid}).`);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const explicitUser = normalizeString(flags.user ??
|
|
22
|
+
process.env.MANAGER_ADMIN_USER ??
|
|
23
|
+
process.env.OPENCLAW_MANAGER_ADMIN_USER);
|
|
24
|
+
const explicitPass = normalizeString(flags.pass ??
|
|
25
|
+
process.env.MANAGER_ADMIN_PASS ??
|
|
26
|
+
process.env.OPENCLAW_MANAGER_ADMIN_PASS);
|
|
27
|
+
if (explicitUser || explicitPass) {
|
|
28
|
+
if (!explicitUser || !explicitPass) {
|
|
29
|
+
throw new Error("[manager] Both --user and --password are required when overriding admin config.");
|
|
30
|
+
}
|
|
31
|
+
writeAdminConfig(paths.configPath, explicitUser, explicitPass);
|
|
32
|
+
}
|
|
33
|
+
else if (!hasAdminConfig(paths.configPath)) {
|
|
34
|
+
if (flags.nonInteractive || !process.stdin.isTTY) {
|
|
35
|
+
throw new Error("[manager] Admin username/password is required. Use --user/--password.");
|
|
36
|
+
}
|
|
37
|
+
const response = await prompts([
|
|
38
|
+
{
|
|
39
|
+
type: "text",
|
|
40
|
+
name: "username",
|
|
41
|
+
message: "Admin username",
|
|
42
|
+
validate: (value) => (value ? true : "Username is required")
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
type: "password",
|
|
46
|
+
name: "password",
|
|
47
|
+
message: "Admin password",
|
|
48
|
+
validate: (value) => (value ? true : "Password is required")
|
|
49
|
+
}
|
|
50
|
+
], {
|
|
51
|
+
onCancel: () => {
|
|
52
|
+
throw new Error("[manager] Prompt cancelled.");
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
const username = normalizeString(response.username);
|
|
56
|
+
const password = normalizeString(response.password);
|
|
57
|
+
if (!username || !password) {
|
|
58
|
+
throw new Error("[manager] Admin username/password is required.");
|
|
59
|
+
}
|
|
60
|
+
writeAdminConfig(paths.configPath, username, password);
|
|
61
|
+
}
|
|
62
|
+
const pkgRoot = resolvePackageRoot();
|
|
63
|
+
const apiEntry = path.join(pkgRoot, "dist", "index.js");
|
|
64
|
+
const webDist = path.join(pkgRoot, "web-dist");
|
|
65
|
+
if (!fs.existsSync(apiEntry) || !fs.existsSync(webDist)) {
|
|
66
|
+
throw new Error("[manager] Package is missing build artifacts. Please reinstall or use a release that includes dist assets.");
|
|
67
|
+
}
|
|
68
|
+
const out = fs.openSync(paths.logPath, "a");
|
|
69
|
+
const err = fs.openSync(paths.errorLogPath, "a");
|
|
70
|
+
const child = spawn(process.execPath, [apiEntry], {
|
|
71
|
+
env: {
|
|
72
|
+
...process.env,
|
|
73
|
+
MANAGER_API_HOST: paths.apiHost,
|
|
74
|
+
MANAGER_API_PORT: String(paths.apiPort),
|
|
75
|
+
MANAGER_WEB_DIST: webDist,
|
|
76
|
+
MANAGER_CONFIG_PATH: paths.configPath
|
|
77
|
+
},
|
|
78
|
+
detached: true,
|
|
79
|
+
stdio: ["ignore", out, err]
|
|
80
|
+
});
|
|
81
|
+
child.unref();
|
|
82
|
+
fs.writeFileSync(paths.pidPath, String(child.pid), "utf-8");
|
|
83
|
+
const lanIp = resolveLanIp();
|
|
84
|
+
console.log(`[manager] Started (pid: ${child.pid}).`);
|
|
85
|
+
console.log(`[manager] Log: ${paths.logPath}`);
|
|
86
|
+
console.log(`[manager] Error log: ${paths.errorLogPath}`);
|
|
87
|
+
console.log(`[manager] Open (local): http://localhost:${paths.apiPort}`);
|
|
88
|
+
console.log(`[manager] Open (local): http://127.0.0.1:${paths.apiPort}`);
|
|
89
|
+
if (lanIp) {
|
|
90
|
+
console.log(`[manager] Open (LAN): http://${lanIp}:${paths.apiPort}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function resolveLanIp() {
|
|
94
|
+
const nets = os.networkInterfaces();
|
|
95
|
+
for (const name of Object.keys(nets)) {
|
|
96
|
+
for (const net of nets[name] ?? []) {
|
|
97
|
+
if (net.family === "IPv4" && !net.internal) {
|
|
98
|
+
return net.address;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
function normalizeString(value) {
|
|
105
|
+
if (typeof value !== "string")
|
|
106
|
+
return "";
|
|
107
|
+
return value.trim();
|
|
108
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { listGatewayProcesses } from "../lib/system.js";
|
|
4
|
+
import { stopManager } from "./stop.js";
|
|
5
|
+
import { readPid } from "../lib/pids.js";
|
|
6
|
+
import { listSandboxDirs } from "../lib/sandbox.js";
|
|
7
|
+
export function stopAll(flags) {
|
|
8
|
+
const messages = [];
|
|
9
|
+
const errors = [];
|
|
10
|
+
const managerResult = stopManager(flags);
|
|
11
|
+
messages.push(...managerResult.messages);
|
|
12
|
+
if (!managerResult.ok && managerResult.error)
|
|
13
|
+
errors.push(managerResult.error);
|
|
14
|
+
const sandboxes = listSandboxDirs();
|
|
15
|
+
if (!sandboxes.length) {
|
|
16
|
+
messages.push("sandbox: none");
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
for (const rootDir of sandboxes) {
|
|
20
|
+
const result = stopSandboxDir(rootDir);
|
|
21
|
+
if (result.ok) {
|
|
22
|
+
messages.push(`sandbox: ${result.message}`);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
errors.push(`sandbox: ${result.error ?? "stop failed"}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const gatewayPids = listGatewayProcesses();
|
|
30
|
+
if (!gatewayPids.length) {
|
|
31
|
+
messages.push("gateway: none");
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
for (const pid of gatewayPids) {
|
|
35
|
+
try {
|
|
36
|
+
process.kill(pid, "SIGTERM");
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// ignore individual failures
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
messages.push(`gateway: stopped (${gatewayPids.join(", ")})`);
|
|
43
|
+
}
|
|
44
|
+
if (errors.length) {
|
|
45
|
+
return { ok: false, messages, error: errors.join("; ") };
|
|
46
|
+
}
|
|
47
|
+
return { ok: true, messages };
|
|
48
|
+
}
|
|
49
|
+
function stopSandboxDir(rootDir) {
|
|
50
|
+
const pidFile = path.join(rootDir, "manager-api.pid");
|
|
51
|
+
if (!fs.existsSync(pidFile)) {
|
|
52
|
+
return { ok: true, message: `already stopped (${rootDir})` };
|
|
53
|
+
}
|
|
54
|
+
const pid = readPid(pidFile);
|
|
55
|
+
if (!pid) {
|
|
56
|
+
return { ok: true, message: `pid invalid (${rootDir})` };
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
process.kill(pid, "SIGTERM");
|
|
60
|
+
return { ok: true, message: `stopped pid ${pid}` };
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
return { ok: false, error: `failed to stop pid ${pid}: ${String(err)}` };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { resolveConfigDirCandidates } from "../lib/config.js";
|
|
5
|
+
import { readPid } from "../lib/pids.js";
|
|
6
|
+
import { commandExists, findListeningPids } from "../lib/system.js";
|
|
7
|
+
export function stopManager(flags) {
|
|
8
|
+
const messages = [];
|
|
9
|
+
const errors = [];
|
|
10
|
+
let stopped = false;
|
|
11
|
+
if (process.platform !== "win32" && commandExists("systemctl")) {
|
|
12
|
+
const serviceName = "clawdbot-manager";
|
|
13
|
+
const servicePath = `/etc/systemd/system/${serviceName}.service`;
|
|
14
|
+
if (fs.existsSync(servicePath)) {
|
|
15
|
+
const result = spawnSync("systemctl", ["stop", serviceName], { encoding: "utf-8" });
|
|
16
|
+
if (result.status === 0) {
|
|
17
|
+
messages.push("manager: stopped systemd service");
|
|
18
|
+
stopped = true;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const candidates = resolveConfigDirCandidates(flags);
|
|
23
|
+
for (const configDir of candidates) {
|
|
24
|
+
const pidPath = path.join(configDir, "manager.pid");
|
|
25
|
+
if (!fs.existsSync(pidPath))
|
|
26
|
+
continue;
|
|
27
|
+
const pid = readPid(pidPath);
|
|
28
|
+
if (!pid)
|
|
29
|
+
continue;
|
|
30
|
+
try {
|
|
31
|
+
process.kill(pid, "SIGTERM");
|
|
32
|
+
fs.rmSync(pidPath, { force: true });
|
|
33
|
+
messages.push(`manager: stopped pid ${pid}`);
|
|
34
|
+
stopped = true;
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
errors.push(`manager: failed to stop pid ${pid}: ${String(err)}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (!stopped) {
|
|
41
|
+
const port = flags.apiPort ?? Number(process.env.MANAGER_API_PORT ?? 17321);
|
|
42
|
+
const pids = findListeningPids(port);
|
|
43
|
+
if (pids.length) {
|
|
44
|
+
for (const pid of pids) {
|
|
45
|
+
try {
|
|
46
|
+
process.kill(pid, "SIGTERM");
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
errors.push(`manager: failed to stop pid ${pid}: ${String(err)}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
messages.push(`manager: stopped port ${port} (pids: ${pids.join(", ")})`);
|
|
53
|
+
stopped = true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (!stopped && !errors.length) {
|
|
57
|
+
messages.push("manager: not running");
|
|
58
|
+
}
|
|
59
|
+
if (errors.length) {
|
|
60
|
+
return { ok: false, messages, error: errors.join("; ") };
|
|
61
|
+
}
|
|
62
|
+
return { ok: true, messages };
|
|
63
|
+
}
|
package/bin/lib/args.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const longKeyMap = {
|
|
2
|
+
help: "help",
|
|
3
|
+
version: "version",
|
|
4
|
+
user: "user",
|
|
5
|
+
username: "user",
|
|
6
|
+
pass: "pass",
|
|
7
|
+
password: "pass",
|
|
8
|
+
"api-port": "apiPort",
|
|
9
|
+
"api-host": "apiHost",
|
|
10
|
+
"config-dir": "configDir",
|
|
11
|
+
"config-path": "configPath",
|
|
12
|
+
"log-path": "logPath",
|
|
13
|
+
"error-log-path": "errorLogPath",
|
|
14
|
+
"non-interactive": "nonInteractive"
|
|
15
|
+
};
|
|
16
|
+
const shortKeyMap = {
|
|
17
|
+
h: "help",
|
|
18
|
+
v: "version",
|
|
19
|
+
u: "user",
|
|
20
|
+
p: "pass"
|
|
21
|
+
};
|
|
22
|
+
const validKeys = new Set(Object.values(longKeyMap));
|
|
23
|
+
export function parseArgs(argv) {
|
|
24
|
+
const flags = {};
|
|
25
|
+
const positionals = [];
|
|
26
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
27
|
+
const arg = argv[i];
|
|
28
|
+
if (arg === "--") {
|
|
29
|
+
positionals.push(...argv.slice(i + 1));
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
if (arg.startsWith("--")) {
|
|
33
|
+
const [rawKey, inlineValue] = arg.slice(2).split("=");
|
|
34
|
+
const key = longKeyMap[rawKey] ?? rawKey;
|
|
35
|
+
if (key === "help" || key === "version" || key === "nonInteractive") {
|
|
36
|
+
flags[key] = true;
|
|
37
|
+
}
|
|
38
|
+
else if (inlineValue !== undefined) {
|
|
39
|
+
setFlag(flags, key, inlineValue);
|
|
40
|
+
}
|
|
41
|
+
else if (i + 1 < argv.length && !argv[i + 1].startsWith("-")) {
|
|
42
|
+
setFlag(flags, key, argv[i + 1]);
|
|
43
|
+
i += 1;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
flags[key] = true;
|
|
47
|
+
}
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (arg.startsWith("-") && arg.length > 1) {
|
|
51
|
+
const shorts = arg.slice(1).split("");
|
|
52
|
+
for (const short of shorts) {
|
|
53
|
+
const mapped = shortKeyMap[short] ?? short;
|
|
54
|
+
if (mapped === "help" || mapped === "version") {
|
|
55
|
+
flags[mapped] = true;
|
|
56
|
+
}
|
|
57
|
+
else if (mapped === "user" || mapped === "pass") {
|
|
58
|
+
if (i + 1 < argv.length && !argv[i + 1].startsWith("-")) {
|
|
59
|
+
setFlag(flags, mapped, argv[i + 1]);
|
|
60
|
+
i += 1;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
flags[mapped] = true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
flags[mapped] = true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
positionals.push(arg);
|
|
73
|
+
}
|
|
74
|
+
const command = (positionals[0] ?? "");
|
|
75
|
+
return { command, flags };
|
|
76
|
+
}
|
|
77
|
+
function setFlag(flags, key, value) {
|
|
78
|
+
if (!validKeys.has(key))
|
|
79
|
+
return;
|
|
80
|
+
if (key === "apiPort") {
|
|
81
|
+
const num = Number(value);
|
|
82
|
+
if (Number.isFinite(num)) {
|
|
83
|
+
flags.apiPort = num;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
flags[key] = value;
|
|
88
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { cyan, dim } from "./color.js";
|
|
2
|
+
import { readPackageVersion } from "./version.js";
|
|
3
|
+
export function printBanner() {
|
|
4
|
+
const version = readPackageVersion();
|
|
5
|
+
const title = cyan("OpenClaw Manager");
|
|
6
|
+
const ver = dim(`v${version}`);
|
|
7
|
+
console.log(`${title} ${ver}`);
|
|
8
|
+
}
|
package/bin/lib/color.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function colorize(text, code) {
|
|
2
|
+
if (!shouldColor())
|
|
3
|
+
return text;
|
|
4
|
+
return `\u001b[${code}m${text}\u001b[0m`;
|
|
5
|
+
}
|
|
6
|
+
export function cyan(text) {
|
|
7
|
+
return colorize(text, 36);
|
|
8
|
+
}
|
|
9
|
+
export function dim(text) {
|
|
10
|
+
return colorize(text, 90);
|
|
11
|
+
}
|
|
12
|
+
function shouldColor() {
|
|
13
|
+
if (process.env.NO_COLOR)
|
|
14
|
+
return false;
|
|
15
|
+
return Boolean(process.stdout.isTTY);
|
|
16
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { randomBytes, scryptSync } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
export function resolveConfigPaths(flags) {
|
|
6
|
+
const apiPort = flags.apiPort ?? Number(process.env.MANAGER_API_PORT ?? 17321);
|
|
7
|
+
const apiHost = flags.apiHost ?? process.env.MANAGER_API_HOST ?? "0.0.0.0";
|
|
8
|
+
const envConfigDir = process.env.MANAGER_CONFIG_DIR ?? "";
|
|
9
|
+
const envConfigPath = process.env.MANAGER_CONFIG_PATH ?? "";
|
|
10
|
+
let configDir = flags.configDir ?? envConfigDir;
|
|
11
|
+
let configPath = flags.configPath ?? envConfigPath;
|
|
12
|
+
if (!configDir && configPath) {
|
|
13
|
+
configDir = path.dirname(configPath);
|
|
14
|
+
}
|
|
15
|
+
if (!configDir) {
|
|
16
|
+
configDir = path.join(os.homedir(), ".openclaw-manager");
|
|
17
|
+
}
|
|
18
|
+
if (!configPath) {
|
|
19
|
+
configPath = path.join(configDir, "config.json");
|
|
20
|
+
}
|
|
21
|
+
const logPath = flags.logPath ?? process.env.MANAGER_LOG_PATH ?? path.join(configDir, "openclaw-manager.log");
|
|
22
|
+
const errorLogPath = flags.errorLogPath ??
|
|
23
|
+
process.env.MANAGER_ERROR_LOG_PATH ??
|
|
24
|
+
path.join(configDir, "openclaw-manager.error.log");
|
|
25
|
+
return {
|
|
26
|
+
apiPort,
|
|
27
|
+
apiHost,
|
|
28
|
+
configDir,
|
|
29
|
+
configPath,
|
|
30
|
+
logPath,
|
|
31
|
+
errorLogPath,
|
|
32
|
+
pidPath: path.join(configDir, "manager.pid")
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export function ensureDir(dir) {
|
|
36
|
+
if (!dir)
|
|
37
|
+
return;
|
|
38
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
export function hasAdminConfig(configPath) {
|
|
41
|
+
if (!fs.existsSync(configPath))
|
|
42
|
+
return false;
|
|
43
|
+
try {
|
|
44
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
45
|
+
const parsed = JSON.parse(raw);
|
|
46
|
+
return Boolean(parsed?.auth?.username &&
|
|
47
|
+
typeof parsed.auth.username === "string" &&
|
|
48
|
+
typeof parsed.auth.salt === "string" &&
|
|
49
|
+
typeof parsed.auth.hash === "string");
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export function writeAdminConfig(configPath, username, password) {
|
|
56
|
+
const salt = randomBytes(16).toString("base64");
|
|
57
|
+
const hash = scryptSync(password, salt, 64).toString("base64");
|
|
58
|
+
const payload = {
|
|
59
|
+
auth: { username, salt, hash },
|
|
60
|
+
createdAt: new Date().toISOString()
|
|
61
|
+
};
|
|
62
|
+
ensureDir(path.dirname(configPath));
|
|
63
|
+
fs.writeFileSync(configPath, JSON.stringify(payload, null, 2));
|
|
64
|
+
console.log(`[manager] Admin config saved to ${configPath}`);
|
|
65
|
+
}
|
|
66
|
+
export function resolveConfigDirCandidates(flags) {
|
|
67
|
+
const explicit = flags.configDir ?? process.env.MANAGER_CONFIG_DIR;
|
|
68
|
+
if (explicit)
|
|
69
|
+
return [explicit];
|
|
70
|
+
return [
|
|
71
|
+
path.join(os.homedir(), ".openclaw-manager"),
|
|
72
|
+
path.join(os.homedir(), ".clawdbot-manager")
|
|
73
|
+
];
|
|
74
|
+
}
|
package/bin/lib/help.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { printBanner } from "./banner.js";
|
|
2
|
+
export function printHelp() {
|
|
3
|
+
printBanner();
|
|
4
|
+
console.log(`\nUsage:\n openclaw-manager <command> [options]\n\nCommands:\n start Start OpenClaw Manager\n stop Stop the running Manager process\n stop-all Stop Manager, sandboxes, and gateway processes\n reset Stop all and remove local manager data\n\nOptions:\n -h, --help Show help\n -v, --version Show version\n -u, --user <name> Admin username (start)\n -p, --pass <value> Admin password (start)\n --non-interactive Fail instead of prompting for credentials\n --api-port <port> API port (default: 17321)\n --api-host <host> API host (default: 0.0.0.0)\n --config-dir <dir> Config directory\n --config-path <path> Config file path\n`);
|
|
5
|
+
}
|
|
6
|
+
export function printWelcome() {
|
|
7
|
+
printBanner();
|
|
8
|
+
console.log(`\nQuick start:\n openclaw-manager start\n\nCommon commands:\n openclaw-manager stop\n openclaw-manager stop-all\n openclaw-manager reset\n\nTip: First start will ask for admin username/password.\nDocs: https://openclaw-manager.com\n`);
|
|
9
|
+
}
|
package/bin/lib/paths.js
ADDED
package/bin/lib/pids.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
export function readPid(pidPath) {
|
|
3
|
+
try {
|
|
4
|
+
const raw = fs.readFileSync(pidPath, "utf-8").trim();
|
|
5
|
+
const pid = Number(raw);
|
|
6
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
7
|
+
return null;
|
|
8
|
+
return pid;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function isPidRunning(pid) {
|
|
15
|
+
try {
|
|
16
|
+
process.kill(pid, 0);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
export function listSandboxDirs() {
|
|
5
|
+
const dir = os.tmpdir();
|
|
6
|
+
let entries = [];
|
|
7
|
+
try {
|
|
8
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
return entries
|
|
14
|
+
.filter((entry) => {
|
|
15
|
+
return (entry.isDirectory() &&
|
|
16
|
+
(entry.name.startsWith("openclaw-manager-sandbox-") ||
|
|
17
|
+
entry.name.startsWith("clawdbot-manager-sandbox-")));
|
|
18
|
+
})
|
|
19
|
+
.map((entry) => path.join(dir, entry.name));
|
|
20
|
+
}
|
|
21
|
+
export function removeSandboxDir(rootDir) {
|
|
22
|
+
try {
|
|
23
|
+
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
24
|
+
return { ok: true, message: `removed (${rootDir})` };
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
return { ok: false, error: `failed to remove ${rootDir}: ${String(err)}` };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
export function commandExists(cmd) {
|
|
3
|
+
const result = spawnSync("command", ["-v", cmd], { encoding: "utf-8", shell: true });
|
|
4
|
+
return result.status === 0;
|
|
5
|
+
}
|
|
6
|
+
export function findListeningPids(port) {
|
|
7
|
+
if (process.platform === "win32" || !commandExists("lsof"))
|
|
8
|
+
return [];
|
|
9
|
+
const result = spawnSync("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], {
|
|
10
|
+
encoding: "utf-8"
|
|
11
|
+
});
|
|
12
|
+
if (result.error || result.status !== 0)
|
|
13
|
+
return [];
|
|
14
|
+
return String(result.stdout)
|
|
15
|
+
.split(/\s+/)
|
|
16
|
+
.map((value) => Number(value.trim()))
|
|
17
|
+
.filter((pid) => Number.isFinite(pid) && pid > 0);
|
|
18
|
+
}
|
|
19
|
+
export function listGatewayProcesses() {
|
|
20
|
+
if (process.platform === "win32" || !commandExists("pgrep"))
|
|
21
|
+
return [];
|
|
22
|
+
const result = spawnSync("pgrep", ["-fl", "clawdbot-gateway"], { encoding: "utf-8" });
|
|
23
|
+
if (result.error || result.status !== 0)
|
|
24
|
+
return [];
|
|
25
|
+
return String(result.stdout)
|
|
26
|
+
.split(/\n/)
|
|
27
|
+
.map((line) => line.trim())
|
|
28
|
+
.filter(Boolean)
|
|
29
|
+
.map((line) => Number(line.split(/\s+/)[0]))
|
|
30
|
+
.filter((pid) => Number.isFinite(pid) && pid > 0);
|
|
31
|
+
}
|
package/bin/lib/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { resolvePackageRoot } from "./paths.js";
|
|
4
|
+
export function readPackageVersion() {
|
|
5
|
+
try {
|
|
6
|
+
const pkgRoot = resolvePackageRoot();
|
|
7
|
+
const pkgPath = path.join(pkgRoot, "package.json");
|
|
8
|
+
const raw = fs.readFileSync(pkgPath, "utf-8");
|
|
9
|
+
const parsed = JSON.parse(raw);
|
|
10
|
+
if (parsed.version && typeof parsed.version === "string")
|
|
11
|
+
return parsed.version;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
// ignore
|
|
15
|
+
}
|
|
16
|
+
return "0.0.0";
|
|
17
|
+
}
|
package/bin/openclaw-manager.js
CHANGED
|
@@ -1,511 +1,60 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { randomBytes, scryptSync } from "node:crypto";
|
|
3
|
-
import { spawn, spawnSync } from "node:child_process";
|
|
4
|
-
import fs from "node:fs";
|
|
5
|
-
import os from "node:os";
|
|
6
|
-
import path from "node:path";
|
|
7
2
|
import process from "node:process";
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
10
|
-
|
|
3
|
+
import { parseArgs } from "./lib/args.js";
|
|
4
|
+
import { printHelp, printWelcome } from "./lib/help.js";
|
|
5
|
+
import { readPackageVersion } from "./lib/version.js";
|
|
6
|
+
import { startManager } from "./commands/start.js";
|
|
7
|
+
import { stopManager } from "./commands/stop.js";
|
|
8
|
+
import { stopAll } from "./commands/stop-all.js";
|
|
9
|
+
import { resetManager } from "./commands/reset.js";
|
|
11
10
|
const args = process.argv.slice(2);
|
|
12
11
|
const parsed = parseArgs(args);
|
|
13
12
|
const cmd = parsed.command;
|
|
14
|
-
|
|
15
13
|
if (parsed.flags.help || cmd === "help") {
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
printHelp();
|
|
15
|
+
process.exit(0);
|
|
18
16
|
}
|
|
19
|
-
|
|
20
17
|
if (parsed.flags.version) {
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
console.log(`openclaw-manager ${readPackageVersion()}`);
|
|
19
|
+
process.exit(0);
|
|
23
20
|
}
|
|
24
|
-
|
|
25
21
|
if (!cmd) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (cmd === "start") {
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
ensureDir(configDir);
|
|
64
|
-
ensureDir(path.dirname(logPath));
|
|
65
|
-
ensureDir(path.dirname(errorLogPath));
|
|
66
|
-
|
|
67
|
-
if (isRunning(pidPath)) {
|
|
68
|
-
const pid = fs.readFileSync(pidPath, "utf-8").trim();
|
|
69
|
-
console.log(`[manager] Already running (pid: ${pid}).`);
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const explicitUser = normalizeString(
|
|
74
|
-
flags.user ??
|
|
75
|
-
flags.username ??
|
|
76
|
-
process.env.MANAGER_ADMIN_USER ??
|
|
77
|
-
process.env.OPENCLAW_MANAGER_ADMIN_USER
|
|
78
|
-
);
|
|
79
|
-
const explicitPass = normalizeString(
|
|
80
|
-
flags.pass ??
|
|
81
|
-
flags.password ??
|
|
82
|
-
process.env.MANAGER_ADMIN_PASS ??
|
|
83
|
-
process.env.OPENCLAW_MANAGER_ADMIN_PASS
|
|
84
|
-
);
|
|
85
|
-
const hasConfig = hasAdminConfig(configPath);
|
|
86
|
-
if (explicitUser || explicitPass) {
|
|
87
|
-
if (!explicitUser || !explicitPass) {
|
|
88
|
-
console.error("[manager] Both --user and --password are required when overriding admin config.");
|
|
89
|
-
process.exit(1);
|
|
90
|
-
}
|
|
91
|
-
writeAdminConfig(configPath, explicitUser, explicitPass);
|
|
92
|
-
} else if (!hasConfig) {
|
|
93
|
-
if (flags.nonInteractive || !process.stdin.isTTY) {
|
|
94
|
-
console.error("[manager] Admin username/password is required. Use --user/--password.");
|
|
95
|
-
process.exit(1);
|
|
96
|
-
}
|
|
97
|
-
const response = await prompts(
|
|
98
|
-
[
|
|
99
|
-
{
|
|
100
|
-
type: "text",
|
|
101
|
-
name: "username",
|
|
102
|
-
message: "Admin username",
|
|
103
|
-
validate: (value) => (value ? true : "Username is required")
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
type: "password",
|
|
107
|
-
name: "password",
|
|
108
|
-
message: "Admin password",
|
|
109
|
-
validate: (value) => (value ? true : "Password is required")
|
|
110
|
-
}
|
|
111
|
-
],
|
|
112
|
-
{
|
|
113
|
-
onCancel: () => {
|
|
114
|
-
throw new Error("Prompt cancelled");
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
);
|
|
118
|
-
const username = String(response.username ?? "").trim();
|
|
119
|
-
const password = String(response.password ?? "").trim();
|
|
120
|
-
if (!username || !password) {
|
|
121
|
-
console.error("[manager] Admin username/password is required.");
|
|
122
|
-
process.exit(1);
|
|
123
|
-
}
|
|
124
|
-
writeAdminConfig(configPath, username, password);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const pkgRoot = resolvePackageRoot();
|
|
128
|
-
const apiEntry = path.join(pkgRoot, "dist", "index.js");
|
|
129
|
-
const webDist = path.join(pkgRoot, "web-dist");
|
|
130
|
-
|
|
131
|
-
if (!fs.existsSync(apiEntry) || !fs.existsSync(webDist)) {
|
|
132
|
-
console.error("[manager] Package is missing build artifacts.");
|
|
133
|
-
console.error("[manager] Please reinstall or use a release that includes dist assets.");
|
|
22
|
+
printWelcome();
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
if (cmd === "start") {
|
|
27
|
+
await startManager(parsed.flags);
|
|
28
|
+
}
|
|
29
|
+
else if (cmd === "stop") {
|
|
30
|
+
const result = stopManager(parsed.flags);
|
|
31
|
+
for (const line of result.messages)
|
|
32
|
+
console.log(line);
|
|
33
|
+
if (!result.ok)
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
else if (cmd === "stop-all") {
|
|
37
|
+
const result = stopAll(parsed.flags);
|
|
38
|
+
for (const line of result.messages)
|
|
39
|
+
console.log(line);
|
|
40
|
+
if (!result.ok)
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
else if (cmd === "reset") {
|
|
44
|
+
const result = resetManager(parsed.flags);
|
|
45
|
+
for (const line of result.messages)
|
|
46
|
+
console.log(line);
|
|
47
|
+
if (!result.ok)
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
console.error(`[manager] Unknown command: ${cmd}`);
|
|
52
|
+
printHelp();
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
58
|
+
console.error(message);
|
|
134
59
|
process.exit(1);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const out = fs.openSync(logPath, "a");
|
|
138
|
-
const err = fs.openSync(errorLogPath, "a");
|
|
139
|
-
const child = spawn(process.execPath, [apiEntry], {
|
|
140
|
-
env: {
|
|
141
|
-
...process.env,
|
|
142
|
-
MANAGER_API_HOST: apiHost,
|
|
143
|
-
MANAGER_API_PORT: apiPort,
|
|
144
|
-
MANAGER_WEB_DIST: webDist,
|
|
145
|
-
MANAGER_CONFIG_PATH: configPath
|
|
146
|
-
},
|
|
147
|
-
detached: true,
|
|
148
|
-
stdio: ["ignore", out, err]
|
|
149
|
-
});
|
|
150
|
-
child.unref();
|
|
151
|
-
|
|
152
|
-
fs.writeFileSync(pidPath, String(child.pid), "utf-8");
|
|
153
|
-
|
|
154
|
-
const lanIp = resolveLanIp();
|
|
155
|
-
console.log(`[manager] Started (pid: ${child.pid}).`);
|
|
156
|
-
console.log(`[manager] Log: ${logPath}`);
|
|
157
|
-
console.log(`[manager] Error log: ${errorLogPath}`);
|
|
158
|
-
console.log(`[manager] Open (local): http://localhost:${apiPort}`);
|
|
159
|
-
console.log(`[manager] Open (local): http://127.0.0.1:${apiPort}`);
|
|
160
|
-
if (lanIp) {
|
|
161
|
-
console.log(`[manager] Open (LAN): http://${lanIp}:${apiPort}`);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
async function stop(flags) {
|
|
166
|
-
const results = stopManagerProcess({ flags });
|
|
167
|
-
for (const line of results.messages) {
|
|
168
|
-
console.log(line);
|
|
169
|
-
}
|
|
170
|
-
if (!results.ok) process.exit(1);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
async function stopAll(flags) {
|
|
174
|
-
const results = stopAllProcesses({ flags });
|
|
175
|
-
for (const line of results.messages) {
|
|
176
|
-
console.log(line);
|
|
177
|
-
}
|
|
178
|
-
if (!results.ok) process.exit(1);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function ensureDir(dir) {
|
|
182
|
-
if (!dir) return;
|
|
183
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function isRunning(pidPath) {
|
|
187
|
-
if (!fs.existsSync(pidPath)) return false;
|
|
188
|
-
const raw = fs.readFileSync(pidPath, "utf-8").trim();
|
|
189
|
-
const pid = Number(raw);
|
|
190
|
-
if (!pid) return false;
|
|
191
|
-
try {
|
|
192
|
-
process.kill(pid, 0);
|
|
193
|
-
return true;
|
|
194
|
-
} catch {
|
|
195
|
-
return false;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function writeAdminConfig(configPath, username, password) {
|
|
200
|
-
const salt = randomBytes(16).toString("base64");
|
|
201
|
-
const hash = scryptSync(password, salt, 64).toString("base64");
|
|
202
|
-
const payload = {
|
|
203
|
-
auth: {
|
|
204
|
-
username,
|
|
205
|
-
salt,
|
|
206
|
-
hash
|
|
207
|
-
},
|
|
208
|
-
createdAt: new Date().toISOString()
|
|
209
|
-
};
|
|
210
|
-
ensureDir(path.dirname(configPath));
|
|
211
|
-
fs.writeFileSync(configPath, JSON.stringify(payload, null, 2));
|
|
212
|
-
console.log(`[manager] Admin config saved to ${configPath}`);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function resolveLanIp() {
|
|
216
|
-
const nets = os.networkInterfaces();
|
|
217
|
-
for (const name of Object.keys(nets)) {
|
|
218
|
-
for (const net of nets[name] ?? []) {
|
|
219
|
-
if (net.family === "IPv4" && !net.internal) {
|
|
220
|
-
return net.address;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
return null;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function resolvePackageRoot() {
|
|
228
|
-
const filePath = fileURLToPath(import.meta.url);
|
|
229
|
-
return path.resolve(path.dirname(filePath), "..");
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function hasAdminConfig(configPath) {
|
|
233
|
-
if (!fs.existsSync(configPath)) return false;
|
|
234
|
-
try {
|
|
235
|
-
const raw = fs.readFileSync(configPath, "utf-8");
|
|
236
|
-
const parsed = JSON.parse(raw);
|
|
237
|
-
return Boolean(
|
|
238
|
-
parsed &&
|
|
239
|
-
parsed.auth &&
|
|
240
|
-
typeof parsed.auth.username === "string" &&
|
|
241
|
-
typeof parsed.auth.salt === "string" &&
|
|
242
|
-
typeof parsed.auth.hash === "string"
|
|
243
|
-
);
|
|
244
|
-
} catch {
|
|
245
|
-
return false;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function stopManagerProcess({ flags }) {
|
|
250
|
-
const messages = [];
|
|
251
|
-
const errors = [];
|
|
252
|
-
const candidates = resolveConfigDirCandidates(flags);
|
|
253
|
-
let stopped = false;
|
|
254
|
-
|
|
255
|
-
if (process.platform !== "win32" && commandExists("systemctl")) {
|
|
256
|
-
const serviceName = "clawdbot-manager";
|
|
257
|
-
const servicePath = `/etc/systemd/system/${serviceName}.service`;
|
|
258
|
-
if (fs.existsSync(servicePath)) {
|
|
259
|
-
const result = spawnSync("systemctl", ["stop", serviceName], { encoding: "utf-8" });
|
|
260
|
-
if (result.status === 0) {
|
|
261
|
-
messages.push("manager: stopped systemd service");
|
|
262
|
-
stopped = true;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
for (const configDir of candidates) {
|
|
268
|
-
const pidPath = path.join(configDir, "manager.pid");
|
|
269
|
-
if (!fs.existsSync(pidPath)) continue;
|
|
270
|
-
const pid = readPid(pidPath);
|
|
271
|
-
if (!pid) continue;
|
|
272
|
-
try {
|
|
273
|
-
process.kill(pid, "SIGTERM");
|
|
274
|
-
fs.rmSync(pidPath, { force: true });
|
|
275
|
-
messages.push(`manager: stopped pid ${pid}`);
|
|
276
|
-
stopped = true;
|
|
277
|
-
} catch (err) {
|
|
278
|
-
errors.push(`manager: failed to stop pid ${pid}: ${String(err)}`);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (!stopped) {
|
|
283
|
-
const port = Number(flags.apiPort ?? process.env.MANAGER_API_PORT ?? 17321);
|
|
284
|
-
const pids = findListeningPids(port);
|
|
285
|
-
if (pids.length) {
|
|
286
|
-
for (const pid of pids) {
|
|
287
|
-
try {
|
|
288
|
-
process.kill(pid, "SIGTERM");
|
|
289
|
-
} catch (err) {
|
|
290
|
-
errors.push(`manager: failed to stop pid ${pid}: ${String(err)}`);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
messages.push(`manager: stopped port ${port} (pids: ${pids.join(", ")})`);
|
|
294
|
-
stopped = true;
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if (!stopped && !errors.length) {
|
|
299
|
-
messages.push("manager: not running");
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
if (errors.length) {
|
|
303
|
-
return { ok: false, messages, error: errors.join("; ") };
|
|
304
|
-
}
|
|
305
|
-
return { ok: true, messages };
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
function stopAllProcesses({ flags }) {
|
|
309
|
-
const messages = [];
|
|
310
|
-
const errors = [];
|
|
311
|
-
|
|
312
|
-
const managerResult = stopManagerProcess({ flags });
|
|
313
|
-
messages.push(...managerResult.messages);
|
|
314
|
-
if (!managerResult.ok) errors.push(managerResult.error ?? "manager stop failed");
|
|
315
|
-
|
|
316
|
-
const sandboxes = listSandboxInstances();
|
|
317
|
-
if (!sandboxes.length) {
|
|
318
|
-
messages.push("sandbox: none");
|
|
319
|
-
} else {
|
|
320
|
-
for (const sandbox of sandboxes) {
|
|
321
|
-
const result = stopSandboxDir(sandbox);
|
|
322
|
-
if (result.ok) {
|
|
323
|
-
messages.push(`sandbox: ${result.message}`);
|
|
324
|
-
} else {
|
|
325
|
-
errors.push(`sandbox: ${result.error ?? "stop failed"}`);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const gatewayResult = stopGatewayProcesses();
|
|
331
|
-
messages.push(gatewayResult.message);
|
|
332
|
-
if (!gatewayResult.ok) errors.push(gatewayResult.error ?? "gateway stop failed");
|
|
333
|
-
|
|
334
|
-
if (errors.length) {
|
|
335
|
-
return { ok: false, messages, error: errors.join("; ") };
|
|
336
|
-
}
|
|
337
|
-
return { ok: true, messages };
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function resolveConfigDirCandidates(flags) {
|
|
341
|
-
const explicit = flags.configDir ?? process.env.MANAGER_CONFIG_DIR;
|
|
342
|
-
if (explicit) return [explicit];
|
|
343
|
-
return [
|
|
344
|
-
path.join(os.homedir(), ".openclaw-manager"),
|
|
345
|
-
path.join(os.homedir(), ".clawdbot-manager")
|
|
346
|
-
];
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
function listSandboxInstances() {
|
|
350
|
-
const dir = os.tmpdir();
|
|
351
|
-
let entries = [];
|
|
352
|
-
try {
|
|
353
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
354
|
-
} catch {
|
|
355
|
-
return [];
|
|
356
|
-
}
|
|
357
|
-
return entries
|
|
358
|
-
.filter((entry) => {
|
|
359
|
-
return (
|
|
360
|
-
entry.isDirectory() &&
|
|
361
|
-
(entry.name.startsWith("openclaw-manager-sandbox-") ||
|
|
362
|
-
entry.name.startsWith("clawdbot-manager-sandbox-"))
|
|
363
|
-
);
|
|
364
|
-
})
|
|
365
|
-
.map((entry) => path.join(dir, entry.name));
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
function stopSandboxDir(rootDir) {
|
|
369
|
-
const pidFile = path.join(rootDir, "manager-api.pid");
|
|
370
|
-
if (!fs.existsSync(pidFile)) {
|
|
371
|
-
return { ok: true, message: `already stopped (${rootDir})` };
|
|
372
|
-
}
|
|
373
|
-
const pid = readPid(pidFile);
|
|
374
|
-
if (!pid) {
|
|
375
|
-
return { ok: true, message: `pid invalid (${rootDir})` };
|
|
376
|
-
}
|
|
377
|
-
try {
|
|
378
|
-
process.kill(pid, "SIGTERM");
|
|
379
|
-
return { ok: true, message: `stopped pid ${pid}` };
|
|
380
|
-
} catch (err) {
|
|
381
|
-
return { ok: false, error: `failed to stop pid ${pid}: ${String(err)}` };
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
function stopGatewayProcesses() {
|
|
386
|
-
if (process.platform === "win32" || !commandExists("pgrep")) {
|
|
387
|
-
return { ok: true, message: "gateway: skipped" };
|
|
388
|
-
}
|
|
389
|
-
const result = spawnSync("pgrep", ["-fl", "clawdbot-gateway"], { encoding: "utf-8" });
|
|
390
|
-
if (result.error || result.status !== 0) {
|
|
391
|
-
return { ok: true, message: "gateway: none" };
|
|
392
|
-
}
|
|
393
|
-
const lines = String(result.stdout)
|
|
394
|
-
.split(/\n/)
|
|
395
|
-
.map((line) => line.trim())
|
|
396
|
-
.filter(Boolean);
|
|
397
|
-
const pids = lines
|
|
398
|
-
.map((line) => Number(line.split(/\s+/)[0]))
|
|
399
|
-
.filter((pid) => Number.isFinite(pid) && pid > 0);
|
|
400
|
-
if (!pids.length) {
|
|
401
|
-
return { ok: true, message: "gateway: none" };
|
|
402
|
-
}
|
|
403
|
-
for (const pid of pids) {
|
|
404
|
-
try {
|
|
405
|
-
process.kill(pid, "SIGTERM");
|
|
406
|
-
} catch {
|
|
407
|
-
// ignore individual failures; report below
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
return { ok: true, message: `gateway: stopped (${pids.join(", ")})` };
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
function findListeningPids(port) {
|
|
414
|
-
if (process.platform === "win32" || !commandExists("lsof")) return [];
|
|
415
|
-
const result = spawnSync("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], {
|
|
416
|
-
encoding: "utf-8"
|
|
417
|
-
});
|
|
418
|
-
if (result.error || result.status !== 0) return [];
|
|
419
|
-
return String(result.stdout)
|
|
420
|
-
.split(/\s+/)
|
|
421
|
-
.map((value) => Number(value.trim()))
|
|
422
|
-
.filter((pid) => Number.isFinite(pid) && pid > 0);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
function readPid(pidPath) {
|
|
426
|
-
try {
|
|
427
|
-
const raw = fs.readFileSync(pidPath, "utf-8").trim();
|
|
428
|
-
const pid = Number(raw);
|
|
429
|
-
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
430
|
-
return pid;
|
|
431
|
-
} catch {
|
|
432
|
-
return null;
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
function normalizeString(value) {
|
|
437
|
-
if (typeof value !== "string") return "";
|
|
438
|
-
return value.trim();
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
function commandExists(cmd) {
|
|
442
|
-
const result = spawnSync("command", ["-v", cmd], { encoding: "utf-8", shell: true });
|
|
443
|
-
return result.status === 0;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
function parseArgs(argv) {
|
|
447
|
-
const flags = {};
|
|
448
|
-
const positionals = [];
|
|
449
|
-
|
|
450
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
451
|
-
const arg = argv[i];
|
|
452
|
-
if (arg === "--") {
|
|
453
|
-
positionals.push(...argv.slice(i + 1));
|
|
454
|
-
break;
|
|
455
|
-
}
|
|
456
|
-
if (arg.startsWith("--")) {
|
|
457
|
-
const [keyRaw, inlineValue] = arg.slice(2).split("=");
|
|
458
|
-
const key = normalizeFlagKey(keyRaw);
|
|
459
|
-
if (key === "help") flags.help = true;
|
|
460
|
-
else if (key === "version") flags.version = true;
|
|
461
|
-
else if (inlineValue !== undefined) flags[key] = inlineValue;
|
|
462
|
-
else if (i + 1 < argv.length && !argv[i + 1].startsWith("-")) {
|
|
463
|
-
flags[key] = argv[i + 1];
|
|
464
|
-
i += 1;
|
|
465
|
-
} else {
|
|
466
|
-
flags[key] = true;
|
|
467
|
-
}
|
|
468
|
-
continue;
|
|
469
|
-
}
|
|
470
|
-
if (arg.startsWith("-") && arg.length > 1) {
|
|
471
|
-
const shorts = arg.slice(1).split("");
|
|
472
|
-
for (const short of shorts) {
|
|
473
|
-
if (short === "h") flags.help = true;
|
|
474
|
-
else if (short === "v") flags.version = true;
|
|
475
|
-
else if (short === "u") flags.user = argv[i + 1] && !argv[i + 1].startsWith("-") ? argv[++i] : true;
|
|
476
|
-
else if (short === "p") flags.pass = argv[i + 1] && !argv[i + 1].startsWith("-") ? argv[++i] : true;
|
|
477
|
-
else flags[short] = true;
|
|
478
|
-
}
|
|
479
|
-
continue;
|
|
480
|
-
}
|
|
481
|
-
positionals.push(arg);
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
return { command: positionals[0] ?? "", flags };
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
function normalizeFlagKey(key) {
|
|
488
|
-
if (!key) return "";
|
|
489
|
-
if (key === "config-dir") return "configDir";
|
|
490
|
-
if (key === "config-path") return "configPath";
|
|
491
|
-
if (key === "log-path") return "logPath";
|
|
492
|
-
if (key === "error-log-path") return "errorLogPath";
|
|
493
|
-
if (key === "api-port") return "apiPort";
|
|
494
|
-
if (key === "api-host") return "apiHost";
|
|
495
|
-
if (key === "non-interactive") return "nonInteractive";
|
|
496
|
-
if (key === "user" || key === "username") return "user";
|
|
497
|
-
if (key === "pass" || key === "password") return "pass";
|
|
498
|
-
return key;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
function printHelp() {
|
|
502
|
-
console.log(
|
|
503
|
-
`openclaw-manager\n\nUsage:\n openclaw-manager <command> [options]\n\nCommands:\n start Start OpenClaw Manager\n stop Stop the running Manager process\n stop-all Stop Manager, sandboxes, and gateway processes\n\nOptions:\n -h, --help Show help\n -v, --version Show version\n -u, --user <name> Admin username (start)\n -p, --pass <value> Admin password (start)\n --non-interactive Fail instead of prompting for credentials\n --api-port <port> API port (default: 17321)\n --api-host <host> API host (default: 0.0.0.0)\n --config-dir <dir> Config directory\n --config-path <path> Config file path\n`
|
|
504
|
-
);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
function printWelcome() {
|
|
508
|
-
console.log(
|
|
509
|
-
`OpenClaw Manager\n\n最快开始:\n openclaw-manager start\n\n常用命令:\n openclaw-manager stop\n openclaw-manager stop-all\n\n提示:首次启动会要求设置管理员账号密码。\n文档: https://openclaw-manager.com\n`
|
|
510
|
-
);
|
|
511
60
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-manager",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"openclaw-manager": "bin/openclaw-manager.js"
|
|
@@ -15,5 +15,14 @@
|
|
|
15
15
|
"@hono/node-server": "1.13.1",
|
|
16
16
|
"hono": "4.11.4",
|
|
17
17
|
"prompts": "^2.4.2"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/prompts": "2.4.9",
|
|
21
|
+
"@types/node": "25.0.10",
|
|
22
|
+
"typescript": "5.9.3"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsc -p tsconfig.json",
|
|
26
|
+
"lint": "tsc -p tsconfig.json --noEmit"
|
|
18
27
|
}
|
|
19
28
|
}
|