openclaw-manager 0.1.1 → 0.1.2

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.
@@ -1,43 +1,61 @@
1
1
  #!/usr/bin/env node
2
2
  import { randomBytes, scryptSync } from "node:crypto";
3
- import { spawn } from "node:child_process";
3
+ import { spawn, spawnSync } from "node:child_process";
4
4
  import fs from "node:fs";
5
5
  import os from "node:os";
6
6
  import path from "node:path";
7
7
  import process from "node:process";
8
- import readline from "node:readline";
9
8
  import { fileURLToPath } from "node:url";
9
+ import prompts from "prompts";
10
10
 
11
11
  const args = process.argv.slice(2);
12
- const cmd = args.find((arg) => !arg.startsWith("-")) ?? "start";
12
+ const parsed = parseArgs(args);
13
+ const cmd = parsed.command;
13
14
 
14
- if (args.includes("-h") || args.includes("--help") || cmd === "help") {
15
+ if (parsed.flags.help || cmd === "help") {
15
16
  printHelp();
16
17
  process.exit(0);
17
18
  }
18
19
 
19
- if (args.includes("-v") || args.includes("--version")) {
20
+ if (parsed.flags.version) {
20
21
  console.log("openclaw-manager 0.1.0");
21
22
  process.exit(0);
22
23
  }
23
24
 
24
- if (cmd !== "start") {
25
+ 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 {
25
37
  console.error(`[manager] Unknown command: ${cmd}`);
26
38
  printHelp();
27
39
  process.exit(1);
28
40
  }
29
41
 
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");
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");
36
49
  const configPath =
37
- process.env.MANAGER_CONFIG_PATH ?? path.join(configDir, "config.json");
50
+ flags.configPath ??
51
+ process.env.MANAGER_CONFIG_PATH ??
52
+ path.join(configDir, "config.json");
38
53
  const logPath =
39
- process.env.MANAGER_LOG_PATH ?? path.join(configDir, "openclaw-manager.log");
54
+ flags.logPath ??
55
+ process.env.MANAGER_LOG_PATH ??
56
+ path.join(configDir, "openclaw-manager.log");
40
57
  const errorLogPath =
58
+ flags.errorLogPath ??
41
59
  process.env.MANAGER_ERROR_LOG_PATH ??
42
60
  path.join(configDir, "openclaw-manager.error.log");
43
61
  const pidPath = path.join(configDir, "manager.pid");
@@ -52,15 +70,53 @@ async function start() {
52
70
  return;
53
71
  }
54
72
 
55
- if (!fs.existsSync(configPath)) {
56
- const username =
73
+ const explicitUser = normalizeString(
74
+ flags.user ??
75
+ flags.username ??
57
76
  process.env.MANAGER_ADMIN_USER ??
58
- process.env.OPENCLAW_MANAGER_ADMIN_USER ??
59
- (await promptLine("Admin username: "));
60
- const password =
77
+ process.env.OPENCLAW_MANAGER_ADMIN_USER
78
+ );
79
+ const explicitPass = normalizeString(
80
+ flags.pass ??
81
+ flags.password ??
61
82
  process.env.MANAGER_ADMIN_PASS ??
62
- process.env.OPENCLAW_MANAGER_ADMIN_PASS ??
63
- (await promptSecret("Admin password: "));
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();
64
120
  if (!username || !password) {
65
121
  console.error("[manager] Admin username/password is required.");
66
122
  process.exit(1);
@@ -106,6 +162,22 @@ async function start() {
106
162
  }
107
163
  }
108
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
+
109
181
  function ensureDir(dir) {
110
182
  if (!dir) return;
111
183
  fs.mkdirSync(dir, { recursive: true });
@@ -140,42 +212,6 @@ function writeAdminConfig(configPath, username, password) {
140
212
  console.log(`[manager] Admin config saved to ${configPath}`);
141
213
  }
142
214
 
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
215
  function resolveLanIp() {
180
216
  const nets = os.networkInterfaces();
181
217
  for (const name of Object.keys(nets)) {
@@ -193,6 +229,283 @@ function resolvePackageRoot() {
193
229
  return path.resolve(path.dirname(filePath), "..");
194
230
  }
195
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
+
196
501
  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`);
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
+ );
198
511
  }
@@ -3,8 +3,8 @@ export function buildCommandRegistry(root) {
3
3
  return [
4
4
  {
5
5
  id: "install-cli",
6
- title: "Install Clawdbot CLI",
7
- description: "Install the latest Clawdbot CLI (may require sudo)",
6
+ title: "Install OpenClaw CLI",
7
+ description: "Install the latest OpenClaw CLI (may require sudo)",
8
8
  command: "npm",
9
9
  args: ["i", "-g", "clawdbot@latest"],
10
10
  cwd: root,
@@ -8,9 +8,9 @@ const DEFAULT_PAIRING_WAIT_TIMEOUT_MS = 180_000;
8
8
  const DEFAULT_PAIRING_POLL_MS = 3000;
9
9
  const DEFAULT_PAIRING_APPROVE_TIMEOUT_MS = 8000;
10
10
  export function createCliInstallJob(deps) {
11
- const job = deps.jobStore.createJob("Install Clawdbot CLI");
11
+ const job = deps.jobStore.createJob("Install OpenClaw CLI");
12
12
  deps.jobStore.startJob(job.id);
13
- deps.jobStore.appendLog(job.id, "开始安装 Clawdbot CLI...");
13
+ deps.jobStore.appendLog(job.id, "开始安装 OpenClaw CLI...");
14
14
  const timeoutMs = parsePositiveInt(process.env.MANAGER_CLI_INSTALL_TIMEOUT_MS) ?? 600_000;
15
15
  void (async () => {
16
16
  const current = await getCliStatus(deps.runCommand);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-manager",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "openclaw-manager": "bin/openclaw-manager.js"
@@ -13,6 +13,7 @@
13
13
  ],
14
14
  "dependencies": {
15
15
  "@hono/node-server": "1.13.1",
16
- "hono": "4.11.4"
16
+ "hono": "4.11.4",
17
+ "prompts": "^2.4.2"
17
18
  }
18
19
  }