openclaw-manager 0.1.2 → 0.1.3

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.
@@ -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,82 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { listGatewayProcesses } from "../lib/system.js";
5
+ import { stopManager } from "./stop.js";
6
+ import { readPid } from "../lib/pids.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 = listSandboxInstances();
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 listSandboxInstances() {
50
+ const dir = os.tmpdir();
51
+ let entries = [];
52
+ try {
53
+ entries = fs.readdirSync(dir, { withFileTypes: true });
54
+ }
55
+ catch {
56
+ return [];
57
+ }
58
+ return entries
59
+ .filter((entry) => {
60
+ return (entry.isDirectory() &&
61
+ (entry.name.startsWith("openclaw-manager-sandbox-") ||
62
+ entry.name.startsWith("clawdbot-manager-sandbox-")));
63
+ })
64
+ .map((entry) => path.join(dir, entry.name));
65
+ }
66
+ function stopSandboxDir(rootDir) {
67
+ const pidFile = path.join(rootDir, "manager-api.pid");
68
+ if (!fs.existsSync(pidFile)) {
69
+ return { ok: true, message: `already stopped (${rootDir})` };
70
+ }
71
+ const pid = readPid(pidFile);
72
+ if (!pid) {
73
+ return { ok: true, message: `pid invalid (${rootDir})` };
74
+ }
75
+ try {
76
+ process.kill(pid, "SIGTERM");
77
+ return { ok: true, message: `stopped pid ${pid}` };
78
+ }
79
+ catch (err) {
80
+ return { ok: false, error: `failed to stop pid ${pid}: ${String(err)}` };
81
+ }
82
+ }
@@ -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
+ }
@@ -0,0 +1,87 @@
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
+ export function parseArgs(argv) {
23
+ const flags = {};
24
+ const positionals = [];
25
+ for (let i = 0; i < argv.length; i += 1) {
26
+ const arg = argv[i];
27
+ if (arg === "--") {
28
+ positionals.push(...argv.slice(i + 1));
29
+ break;
30
+ }
31
+ if (arg.startsWith("--")) {
32
+ const [rawKey, inlineValue] = arg.slice(2).split("=");
33
+ const key = longKeyMap[rawKey] ?? rawKey;
34
+ if (key === "help" || key === "version" || key === "nonInteractive") {
35
+ flags[key] = true;
36
+ }
37
+ else if (inlineValue !== undefined) {
38
+ setFlag(flags, key, inlineValue);
39
+ }
40
+ else if (i + 1 < argv.length && !argv[i + 1].startsWith("-")) {
41
+ setFlag(flags, key, argv[i + 1]);
42
+ i += 1;
43
+ }
44
+ else {
45
+ flags[key] = true;
46
+ }
47
+ continue;
48
+ }
49
+ if (arg.startsWith("-") && arg.length > 1) {
50
+ const shorts = arg.slice(1).split("");
51
+ for (const short of shorts) {
52
+ const mapped = shortKeyMap[short] ?? short;
53
+ if (mapped === "help" || mapped === "version") {
54
+ flags[mapped] = true;
55
+ }
56
+ else if (mapped === "user" || mapped === "pass") {
57
+ if (i + 1 < argv.length && !argv[i + 1].startsWith("-")) {
58
+ setFlag(flags, mapped, argv[i + 1]);
59
+ i += 1;
60
+ }
61
+ else {
62
+ flags[mapped] = true;
63
+ }
64
+ }
65
+ else {
66
+ flags[mapped] = true;
67
+ }
68
+ }
69
+ continue;
70
+ }
71
+ positionals.push(arg);
72
+ }
73
+ const command = (positionals[0] ?? "");
74
+ return { command, flags };
75
+ }
76
+ function setFlag(flags, key, value) {
77
+ if (key === "apiPort") {
78
+ const num = Number(value);
79
+ if (Number.isFinite(num)) {
80
+ flags.apiPort = num;
81
+ return;
82
+ }
83
+ }
84
+ if (key in flags || key in longKeyMap) {
85
+ flags[key] = value;
86
+ }
87
+ }
@@ -0,0 +1,13 @@
1
+ import { readPackageVersion } from "./version.js";
2
+ const BANNER_LINES = [
3
+ " ____ __ __ __ ___ ",
4
+ " / __ \\____ ___ ____/ /___ _/ /___ _____/ |/ /___ _____ ____ _____ _____ ",
5
+ " / / / / __ \\/ _ \\/ __ / __ `/ / __ \\___/ /|_/ / __ `/ __ \\/ __ `/ __ \\/ ___/ ",
6
+ "/ /_/ / /_/ / __/ /_/ / /_/ / / / / / /__/ / / / /_/ / / / / /_/ / /_/ / / ",
7
+ "\\____/ .___/\\___/\\__,_/\\__,_/_/_/ /_/\\___/_/ /_/\\__,_/_/ /_/\\__,_/\\____/_/ ",
8
+ " /_/ "
9
+ ];
10
+ export function printBanner() {
11
+ const version = readPackageVersion();
12
+ console.log(`${BANNER_LINES.join("\n")}\nopenclaw-manager ${version}`);
13
+ }
@@ -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
+ }
@@ -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\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\nTip: First start will ask for admin username/password.\nDocs: https://openclaw-manager.com\n`);
9
+ }
@@ -0,0 +1,6 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ export function resolvePackageRoot() {
4
+ const filePath = fileURLToPath(import.meta.url);
5
+ return path.resolve(path.dirname(filePath), "..", "..");
6
+ }
@@ -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,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
+ }
@@ -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
+ }
@@ -1,511 +1,52 @@
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 { fileURLToPath } from "node:url";
9
- import prompts from "prompts";
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";
11
9
  const args = process.argv.slice(2);
12
10
  const parsed = parseArgs(args);
13
11
  const cmd = parsed.command;
14
-
15
12
  if (parsed.flags.help || cmd === "help") {
16
- printHelp();
17
- process.exit(0);
13
+ printHelp();
14
+ process.exit(0);
18
15
  }
19
-
20
16
  if (parsed.flags.version) {
21
- console.log("openclaw-manager 0.1.0");
22
- process.exit(0);
17
+ console.log(`openclaw-manager ${readPackageVersion()}`);
18
+ process.exit(0);
23
19
  }
24
-
25
20
  if (!cmd) {
26
- printWelcome();
27
- process.exit(0);
28
- }
29
-
30
- if (cmd === "start") {
31
- void start(parsed.flags);
32
- } else if (cmd === "stop") {
33
- void stop(parsed.flags);
34
- } else if (cmd === "stop-all") {
35
- void stopAll(parsed.flags);
36
- } else {
37
- console.error(`[manager] Unknown command: ${cmd}`);
38
- printHelp();
39
- process.exit(1);
40
- }
41
-
42
- async function start(flags) {
43
- const apiPort = String(flags.apiPort ?? process.env.MANAGER_API_PORT ?? "17321");
44
- const apiHost = flags.apiHost ?? process.env.MANAGER_API_HOST ?? "0.0.0.0";
45
- const configDir =
46
- flags.configDir ??
47
- process.env.MANAGER_CONFIG_DIR ??
48
- path.join(os.homedir(), ".openclaw-manager");
49
- const configPath =
50
- flags.configPath ??
51
- process.env.MANAGER_CONFIG_PATH ??
52
- path.join(configDir, "config.json");
53
- const logPath =
54
- flags.logPath ??
55
- process.env.MANAGER_LOG_PATH ??
56
- path.join(configDir, "openclaw-manager.log");
57
- const errorLogPath =
58
- flags.errorLogPath ??
59
- process.env.MANAGER_ERROR_LOG_PATH ??
60
- path.join(configDir, "openclaw-manager.error.log");
61
- const pidPath = path.join(configDir, "manager.pid");
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.");
21
+ printWelcome();
22
+ process.exit(0);
23
+ }
24
+ try {
25
+ if (cmd === "start") {
26
+ await startManager(parsed.flags);
27
+ }
28
+ else if (cmd === "stop") {
29
+ const result = stopManager(parsed.flags);
30
+ for (const line of result.messages)
31
+ console.log(line);
32
+ if (!result.ok)
33
+ process.exit(1);
34
+ }
35
+ else if (cmd === "stop-all") {
36
+ const result = stopAll(parsed.flags);
37
+ for (const line of result.messages)
38
+ console.log(line);
39
+ if (!result.ok)
40
+ process.exit(1);
41
+ }
42
+ else {
43
+ console.error(`[manager] Unknown command: ${cmd}`);
44
+ printHelp();
45
+ process.exit(1);
46
+ }
47
+ }
48
+ catch (err) {
49
+ const message = err instanceof Error ? err.message : String(err);
50
+ console.error(message);
134
51
  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
52
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-manager",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
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
  }