@ww-ai-lab/openclaw-office 2026.4.7 → 2026.4.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/openclaw-office.js +15 -0
- package/bin/platform.js +38 -0
- package/bin/service-linux.js +323 -0
- package/bin/service-macos.js +298 -0
- package/bin/service.js +212 -0
- package/dist/assets/{ActivityHeatmap-BDFjAQ43.js → ActivityHeatmap-CgtstPK-.js} +1 -1
- package/dist/assets/{CostPieChart-Bq2Q_XUG.js → CostPieChart-C0jtTSIA.js} +1 -1
- package/dist/assets/{NetworkGraph-BE0tqU7M.js → NetworkGraph-B4TMVh7j.js} +1 -1
- package/dist/assets/{TokenLineChart-S8FEhT9d.js → TokenLineChart-WWwn8mbX.js} +1 -1
- package/dist/assets/{generateCategoricalChart-3lV5YNJ4.js → generateCategoricalChart-CMnC5o2Z.js} +1 -1
- package/dist/assets/index-BFpEDw4d.js +482 -0
- package/dist/assets/index-BpCZg7Ed.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/dist/assets/index-C6AnyloZ.js +0 -482
- package/dist/assets/index-CF14MXrb.css +0 -1
package/bin/openclaw-office.js
CHANGED
|
@@ -11,6 +11,14 @@ import { networkInterfaces, homedir } from "node:os";
|
|
|
11
11
|
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
12
12
|
const distDir = resolve(__dirname, "..", "dist");
|
|
13
13
|
|
|
14
|
+
// --- Service subcommand routing ---
|
|
15
|
+
// If argv[2] is "service", delegate to the service manager module.
|
|
16
|
+
if (process.argv[2] === "service") {
|
|
17
|
+
const { runService } = await import("./service.js");
|
|
18
|
+
await runService();
|
|
19
|
+
process.exit(0);
|
|
20
|
+
}
|
|
21
|
+
|
|
14
22
|
const MIME_TYPES = {
|
|
15
23
|
".html": "text/html; charset=utf-8",
|
|
16
24
|
".js": "application/javascript; charset=utf-8",
|
|
@@ -79,6 +87,13 @@ function printHelp() {
|
|
|
79
87
|
openclaw-office --token my-secret-token
|
|
80
88
|
openclaw-office --gateway ws://192.168.1.100:18789
|
|
81
89
|
PORT=3000 openclaw-office
|
|
90
|
+
|
|
91
|
+
\x1b[1mService management:\x1b[0m
|
|
92
|
+
openclaw-office service install --token <token> # Auto-start on login/boot
|
|
93
|
+
openclaw-office service status # Check service status
|
|
94
|
+
openclaw-office service stop # Stop the service
|
|
95
|
+
openclaw-office service uninstall # Remove the service
|
|
96
|
+
openclaw-office service help # Show service help
|
|
82
97
|
`);
|
|
83
98
|
}
|
|
84
99
|
|
package/bin/platform.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform detection utility for openclaw-office service management.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { platform } = await import("node:os");
|
|
6
|
+
const { exec } = await import("node:child_process");
|
|
7
|
+
const { promisify } = await import("node:util");
|
|
8
|
+
const execAsync = promisify(exec);
|
|
9
|
+
|
|
10
|
+
/** @returns {Promise<'macos' | 'linux' | 'windows' | 'unknown'>} */
|
|
11
|
+
export function detectPlatform() {
|
|
12
|
+
const p = platform();
|
|
13
|
+
if (p === "darwin") return "macos";
|
|
14
|
+
if (p === "linux") return "linux";
|
|
15
|
+
if (p === "win32") return "windows";
|
|
16
|
+
return "unknown";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check if systemd --user is available (Linux).
|
|
21
|
+
* @returns {Promise<boolean>}
|
|
22
|
+
*/
|
|
23
|
+
export async function hasSystemdUser() {
|
|
24
|
+
try {
|
|
25
|
+
await execAsync("systemctl --user is-system-running");
|
|
26
|
+
return true;
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get the path to the currently running node executable.
|
|
34
|
+
* @returns {string}
|
|
35
|
+
*/
|
|
36
|
+
export function getNodePath() {
|
|
37
|
+
return process.execPath;
|
|
38
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Linux systemd service manager for openclaw-office.
|
|
5
|
+
*
|
|
6
|
+
* Manages the OpenClaw Office service via systemd --user.
|
|
7
|
+
* Commands: install, uninstall, start, stop, restart, status, log
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, mkdirSync, writeFileSync, unlinkSync, readFileSync } from "node:fs";
|
|
11
|
+
import { execSync } from "node:child_process";
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
|
|
16
|
+
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
17
|
+
|
|
18
|
+
const SYSTEMD_DIR = join(homedir(), ".config", "systemd", "user");
|
|
19
|
+
const SERVICE_NAME = "openclaw-office.service";
|
|
20
|
+
const SERVICE_PATH = join(SYSTEMD_DIR, SERVICE_NAME);
|
|
21
|
+
const NODE_BIN = process.execPath;
|
|
22
|
+
const SERVER_SCRIPT = join(__dirname, "openclaw-office.js");
|
|
23
|
+
const LOG_DIR = join(homedir(), ".local", "state", "openclaw-office");
|
|
24
|
+
const STDOUT_LOG = join(LOG_DIR, "openclaw-office.log");
|
|
25
|
+
const STDERR_LOG = join(LOG_DIR, "openclaw-office-error.log");
|
|
26
|
+
|
|
27
|
+
// --- Colors ---
|
|
28
|
+
|
|
29
|
+
const C = {
|
|
30
|
+
reset: "\x1b[0m",
|
|
31
|
+
bold: "\x1b[1m",
|
|
32
|
+
green: "\x1b[32m",
|
|
33
|
+
red: "\x1b[31m",
|
|
34
|
+
yellow: "\x1b[33m",
|
|
35
|
+
cyan: "\x1b[36m",
|
|
36
|
+
gray: "\x1b[90m",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function printLog(msg, color = "") { console.log(`${color}${msg}${C.reset}`); }
|
|
40
|
+
function ok(msg) { printLog(` \u2713 ${msg}`, C.green); }
|
|
41
|
+
function err(msg) { printLog(` \u2717 ${msg}`, C.red); }
|
|
42
|
+
function info(msg) { printLog(` \u2022 ${msg}`, C.cyan); }
|
|
43
|
+
function warn(msg) { printLog(` \u2022 ${msg}`, C.yellow); }
|
|
44
|
+
function dim(msg) { printLog(` ${msg}`, C.gray); }
|
|
45
|
+
|
|
46
|
+
// --- Helpers ---
|
|
47
|
+
|
|
48
|
+
function systemctl(args) {
|
|
49
|
+
try {
|
|
50
|
+
execSync(`systemctl --user ${args}`, { stdio: "pipe" });
|
|
51
|
+
return true;
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isEnabled() {
|
|
58
|
+
return systemctl(`is-enabled ${SERVICE_NAME}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isActive() {
|
|
62
|
+
return systemctl(`is-active ${SERVICE_NAME}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function generateService(config) {
|
|
66
|
+
const args = [];
|
|
67
|
+
args.push(SERVER_SCRIPT);
|
|
68
|
+
if (config.gatewayUrl) args.push(`--gateway ${config.gatewayUrl}`);
|
|
69
|
+
if (config.port) args.push(`--port ${config.port}`);
|
|
70
|
+
if (config.host) args.push(`--host ${config.host}`);
|
|
71
|
+
if (config.token) args.push(`--token ${config.token}`);
|
|
72
|
+
|
|
73
|
+
return `[Unit]
|
|
74
|
+
Description=OpenClaw Office — Multi-Agent Monitoring Console
|
|
75
|
+
After=network.target
|
|
76
|
+
|
|
77
|
+
[Service]
|
|
78
|
+
Type=simple
|
|
79
|
+
ExecStart=${NODE_BIN} ${args.join(" ")}
|
|
80
|
+
Restart=on-failure
|
|
81
|
+
RestartSec=5
|
|
82
|
+
StandardOutput=append:${STDOUT_LOG}
|
|
83
|
+
StandardError=append:${STDERR_LOG}
|
|
84
|
+
Environment=HOME=${homedir()}
|
|
85
|
+
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
|
86
|
+
WorkingDirectory=${__dirname}
|
|
87
|
+
|
|
88
|
+
[Install]
|
|
89
|
+
WantedBy=default.target
|
|
90
|
+
`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function generateTimer(config) {
|
|
94
|
+
return `[Unit]
|
|
95
|
+
Description=Restart OpenClaw Office if not running
|
|
96
|
+
[Timer]
|
|
97
|
+
OnBootSec=1min
|
|
98
|
+
OnUnitActiveSec=5min
|
|
99
|
+
[Install]
|
|
100
|
+
WantedBy=timers.target
|
|
101
|
+
`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// --- Commands ---
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @param {{ token: string, gatewayUrl?: string, port?: number, host?: string }} config
|
|
108
|
+
*/
|
|
109
|
+
export async function install(config) {
|
|
110
|
+
if (!config.token) {
|
|
111
|
+
err("Token is required for service installation.");
|
|
112
|
+
info("Provide via --token flag or it will be auto-detected from ~/.openclaw/openclaw.json");
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Ensure directories
|
|
117
|
+
if (!existsSync(SYSTEMD_DIR)) {
|
|
118
|
+
mkdirSync(SYSTEMD_DIR, { recursive: true });
|
|
119
|
+
}
|
|
120
|
+
if (!existsSync(LOG_DIR)) {
|
|
121
|
+
mkdirSync(LOG_DIR, { recursive: true });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Reload systemd user daemon
|
|
125
|
+
systemctl("daemon-reload");
|
|
126
|
+
|
|
127
|
+
// Stop existing service if active
|
|
128
|
+
if (isActive()) {
|
|
129
|
+
warn("Existing service found, stopping...");
|
|
130
|
+
systemctl(`stop ${SERVICE_NAME}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Write service file
|
|
134
|
+
const service = generateService(config);
|
|
135
|
+
writeFileSync(SERVICE_PATH, service, "utf-8");
|
|
136
|
+
ok(`Service file written: ${SERVICE_PATH}`);
|
|
137
|
+
|
|
138
|
+
// Reload and enable
|
|
139
|
+
systemctl("daemon-reload");
|
|
140
|
+
systemctl(`enable ${SERVICE_NAME}`);
|
|
141
|
+
ok("Service enabled");
|
|
142
|
+
|
|
143
|
+
// Start the service
|
|
144
|
+
systemctl(`start ${SERVICE_NAME}`);
|
|
145
|
+
ok("Service started");
|
|
146
|
+
|
|
147
|
+
printLog("");
|
|
148
|
+
ok("OpenClaw Office service installed successfully!");
|
|
149
|
+
printLog("");
|
|
150
|
+
info(`Service file: ${SERVICE_PATH}`);
|
|
151
|
+
info(`Stdout log: ${STDOUT_LOG}`);
|
|
152
|
+
info(`Stderr log: ${STDERR_LOG}`);
|
|
153
|
+
printLog("");
|
|
154
|
+
info("The service will auto-start on boot and restart on failure.");
|
|
155
|
+
dim("To manage: systemctl --user start|stop|restart|status openclaw-office.service");
|
|
156
|
+
dim("To uninstall: openclaw-office service uninstall");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function uninstall() {
|
|
160
|
+
if (!existsSync(SERVICE_PATH)) {
|
|
161
|
+
warn("Service not installed. Nothing to do.");
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (isActive()) {
|
|
166
|
+
info("Stopping running service...");
|
|
167
|
+
systemctl(`stop ${SERVICE_NAME}`);
|
|
168
|
+
ok("Service stopped");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
systemctl(`disable ${SERVICE_NAME}`);
|
|
172
|
+
systemctl("daemon-reload");
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
unlinkSync(SERVICE_PATH);
|
|
176
|
+
ok(`Service file removed: ${SERVICE_PATH}`);
|
|
177
|
+
} catch {
|
|
178
|
+
err("Failed to remove service file");
|
|
179
|
+
process.exitCode = 1;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
systemctl("daemon-reload");
|
|
183
|
+
printLog("");
|
|
184
|
+
ok("OpenClaw Office service uninstalled.");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function start() {
|
|
188
|
+
if (!existsSync(SERVICE_PATH)) {
|
|
189
|
+
err("Service not installed. Run: openclaw-office service install --token <token>");
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
if (isActive()) {
|
|
193
|
+
warn("Service is already running.");
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
systemctl(`start ${SERVICE_NAME}`);
|
|
197
|
+
if (isActive()) {
|
|
198
|
+
ok("Service started");
|
|
199
|
+
} else {
|
|
200
|
+
err("Failed to start service. Check logs:");
|
|
201
|
+
dim(`journalctl --user -u ${SERVICE_NAME} --no-pager -n 20`);
|
|
202
|
+
process.exitCode = 1;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function stop() {
|
|
207
|
+
if (!isActive()) {
|
|
208
|
+
warn("Service is not running.");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
systemctl(`stop ${SERVICE_NAME}`);
|
|
212
|
+
ok("Service stopped");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function restart() {
|
|
216
|
+
if (!existsSync(SERVICE_PATH)) {
|
|
217
|
+
err("Service not installed. Run: openclaw-office service install --token <token>");
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
systemctl(`restart ${SERVICE_NAME}`);
|
|
221
|
+
ok("Service restarted");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function status() {
|
|
225
|
+
if (!existsSync(SERVICE_PATH)) {
|
|
226
|
+
warn("Service not installed");
|
|
227
|
+
printLog("");
|
|
228
|
+
dim("Install with: openclaw-office service install --token <token>");
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
info(`Service file: ${SERVICE_PATH}`);
|
|
233
|
+
|
|
234
|
+
if (isActive()) {
|
|
235
|
+
ok("Status: active (running)");
|
|
236
|
+
} else {
|
|
237
|
+
err("Status: inactive (dead)");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (isEnabled()) {
|
|
241
|
+
ok("Enabled: yes (auto-start on boot)");
|
|
242
|
+
} else {
|
|
243
|
+
warn("Enabled: no");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Show recent journal entries
|
|
247
|
+
try {
|
|
248
|
+
const journal = execSync(
|
|
249
|
+
`journalctl --user -u ${SERVICE_NAME} --no-pager -n 10 2>/dev/null || echo "(journalctl not available)"`,
|
|
250
|
+
{ stdio: "pipe" }
|
|
251
|
+
).toString();
|
|
252
|
+
if (journal.trim()) {
|
|
253
|
+
printLog("");
|
|
254
|
+
info("Recent journal entries:");
|
|
255
|
+
dim(journal.trim());
|
|
256
|
+
}
|
|
257
|
+
} catch { /* ok */ }
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function showLogs(follow = false) {
|
|
261
|
+
// Try journalctl first, fall back to file
|
|
262
|
+
try {
|
|
263
|
+
if (follow) {
|
|
264
|
+
execSync(`journalctl --user -u ${SERVICE_NAME} -f 2>/dev/null`, { stdio: "inherit" });
|
|
265
|
+
return;
|
|
266
|
+
} else {
|
|
267
|
+
const output = execSync(
|
|
268
|
+
`journalctl --user -u ${SERVICE_NAME} --no-pager -n 100 2>/dev/null`,
|
|
269
|
+
{ stdio: "pipe" }
|
|
270
|
+
).toString();
|
|
271
|
+
if (output.trim()) {
|
|
272
|
+
console.log(output.trim());
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
} catch { /* fall back to file */ }
|
|
277
|
+
|
|
278
|
+
// Fall back to file-based log
|
|
279
|
+
if (!existsSync(STDOUT_LOG)) {
|
|
280
|
+
warn("No log file found.");
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
if (follow) {
|
|
285
|
+
execSync(`tail -f "${STDOUT_LOG}"`, { stdio: "inherit" });
|
|
286
|
+
} else {
|
|
287
|
+
const content = readFileSync(STDOUT_LOG, "utf-8");
|
|
288
|
+
console.log(content);
|
|
289
|
+
}
|
|
290
|
+
} catch (e) {
|
|
291
|
+
err("Failed to read log file");
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function printHelp() {
|
|
296
|
+
console.log(`
|
|
297
|
+
${C.cyan}OpenClaw Office — Service Management (Linux)${C.reset}
|
|
298
|
+
|
|
299
|
+
${C.bold}Usage:${C.reset}
|
|
300
|
+
openclaw-office service <command> [options]
|
|
301
|
+
|
|
302
|
+
${C.bold}Commands:${C.reset}
|
|
303
|
+
install Install as a systemd --user service (auto-start on boot)
|
|
304
|
+
uninstall Remove the systemd service
|
|
305
|
+
start Start the service
|
|
306
|
+
stop Stop the service
|
|
307
|
+
restart Restart the service
|
|
308
|
+
status Show service status
|
|
309
|
+
log Show service logs (add --follow to tail)
|
|
310
|
+
|
|
311
|
+
${C.bold}Install options:${C.reset}
|
|
312
|
+
--token <token> Gateway auth token (required)
|
|
313
|
+
--gateway <url> Gateway WebSocket URL
|
|
314
|
+
--port <port> Server port (default: 5180)
|
|
315
|
+
--host <host> Bind address (default: 0.0.0.0)
|
|
316
|
+
|
|
317
|
+
${C.bold}Examples:${C.reset}
|
|
318
|
+
openclaw-office service install --token my-token
|
|
319
|
+
openclaw-office service install --token my-token --port 3000
|
|
320
|
+
openclaw-office service status
|
|
321
|
+
openclaw-office service log --follow
|
|
322
|
+
`);
|
|
323
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* macOS launchd service manager for openclaw-office.
|
|
5
|
+
*
|
|
6
|
+
* Manages the OpenClaw Office service via launchd (user-level).
|
|
7
|
+
* Commands: install, uninstall, start, stop, restart, status, log
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, mkdirSync, writeFileSync, unlinkSync, readFileSync } from "node:fs";
|
|
11
|
+
import { execSync, exec as execCb } from "node:child_process";
|
|
12
|
+
import { promisify } from "node:util";
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
|
|
17
|
+
const exec = promisify(execCb);
|
|
18
|
+
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
19
|
+
|
|
20
|
+
const LAUNCHD_DIR = join(homedir(), "Library", "LaunchAgents");
|
|
21
|
+
const PLIST_NAME = "com.user.openclaw-office.plist";
|
|
22
|
+
const PLIST_PATH = join(LAUNCHD_DIR, PLIST_NAME);
|
|
23
|
+
const LABEL = "com.user.openclaw-office";
|
|
24
|
+
const NODE_BIN = process.execPath;
|
|
25
|
+
const SERVER_SCRIPT = join(__dirname, "openclaw-office.js");
|
|
26
|
+
const LOG_DIR = join(homedir(), "Library", "Logs", "openclaw-office");
|
|
27
|
+
const STDOUT_LOG = join(LOG_DIR, "openclaw-office.log");
|
|
28
|
+
const STDERR_LOG = join(LOG_DIR, "openclaw-office-error.log");
|
|
29
|
+
|
|
30
|
+
// --- Colors ---
|
|
31
|
+
|
|
32
|
+
const C = {
|
|
33
|
+
reset: "\x1b[0m",
|
|
34
|
+
bold: "\x1b[1m",
|
|
35
|
+
green: "\x1b[32m",
|
|
36
|
+
red: "\x1b[31m",
|
|
37
|
+
yellow: "\x1b[33m",
|
|
38
|
+
cyan: "\x1b[36m",
|
|
39
|
+
gray: "\x1b[90m",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function p(msg, color = "") { console.log(`${color}${msg}${C.reset}`); }
|
|
43
|
+
function ok(msg) { p(` \u2713 ${msg}`, C.green); }
|
|
44
|
+
function err(msg) { p(` \u2717 ${msg}`, C.red); }
|
|
45
|
+
function info(msg) { p(` \u2022 ${msg}`, C.cyan); }
|
|
46
|
+
function warn(msg) { p(` \u2022 ${msg}`, C.yellow); }
|
|
47
|
+
function dim(msg) { p(` ${msg}`, C.gray); }
|
|
48
|
+
|
|
49
|
+
// --- Helpers ---
|
|
50
|
+
|
|
51
|
+
function launchctl(args) {
|
|
52
|
+
try {
|
|
53
|
+
execSync(`launchctl ${args}`, { stdio: "pipe" });
|
|
54
|
+
return true;
|
|
55
|
+
} catch (e) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isLoaded() {
|
|
61
|
+
try {
|
|
62
|
+
const out = execSync(`launchctl list | grep "${LABEL}"`, { stdio: "pipe" }).toString();
|
|
63
|
+
return out.trim().length > 0;
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function escapeXml(str) {
|
|
70
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function generatePlist(config) {
|
|
74
|
+
const args = [];
|
|
75
|
+
args.push(escapeXml(SERVER_SCRIPT));
|
|
76
|
+
if (config.gatewayUrl) args.push(`--gateway ${escapeXml(config.gatewayUrl)}`);
|
|
77
|
+
if (config.port) args.push(`--port ${config.port}`);
|
|
78
|
+
if (config.host) args.push(`--host ${escapeXml(config.host)}`);
|
|
79
|
+
if (config.token) args.push(`--token ${escapeXml(config.token)}`);
|
|
80
|
+
|
|
81
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
82
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
83
|
+
<plist version="1.0">
|
|
84
|
+
<dict>
|
|
85
|
+
<key>Label</key>
|
|
86
|
+
<string>${LABEL}</string>
|
|
87
|
+
<key>ProgramArguments</key>
|
|
88
|
+
<array>
|
|
89
|
+
<string>${escapeXml(NODE_BIN)}</string>${args.map(a => `
|
|
90
|
+
<string>${a}</string>`).join("")}
|
|
91
|
+
</array>
|
|
92
|
+
<key>WorkingDirectory</key>
|
|
93
|
+
<string>${escapeXml(__dirname)}</string>
|
|
94
|
+
<key>RunAtLoad</key>
|
|
95
|
+
<true/>
|
|
96
|
+
<key>KeepAlive</key>
|
|
97
|
+
<true/>
|
|
98
|
+
<key>StandardOutPath</key>
|
|
99
|
+
<string>${escapeXml(STDOUT_LOG)}</string>
|
|
100
|
+
<key>StandardErrorPath</key>
|
|
101
|
+
<string>${escapeXml(STDERR_LOG)}</string>
|
|
102
|
+
<key>EnvironmentVariables</key>
|
|
103
|
+
<dict>
|
|
104
|
+
<key>HOME</key>
|
|
105
|
+
<string>${escapeXml(homedir())}</string>
|
|
106
|
+
<key>PATH</key>
|
|
107
|
+
<string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
|
108
|
+
</dict>
|
|
109
|
+
</dict>
|
|
110
|
+
</plist>`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// --- Commands ---
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @param {{ token: string, gatewayUrl?: string, port?: number, host?: string }} config
|
|
117
|
+
*/
|
|
118
|
+
export async function install(config) {
|
|
119
|
+
if (!config.token) {
|
|
120
|
+
err("Token is required for service installation.");
|
|
121
|
+
info("Provide via --token flag or it will be auto-detected from ~/.openclaw/openclaw.json");
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Ensure directories
|
|
126
|
+
if (!existsSync(LAUNCHD_DIR)) {
|
|
127
|
+
mkdirSync(LAUNCHD_DIR, { recursive: true });
|
|
128
|
+
}
|
|
129
|
+
if (!existsSync(LOG_DIR)) {
|
|
130
|
+
mkdirSync(LOG_DIR, { recursive: true });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Unload existing service if running
|
|
134
|
+
if (isLoaded()) {
|
|
135
|
+
warn("Existing service found, unloading...");
|
|
136
|
+
launchctl(`bootout gui/${process.getuid()}/${LABEL} 2>/dev/null || launchctl unload "${PLIST_PATH}" 2>/dev/null`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Write plist
|
|
140
|
+
const plist = generatePlist(config);
|
|
141
|
+
writeFileSync(PLIST_PATH, plist, "utf-8");
|
|
142
|
+
ok(`Plist written: ${PLIST_PATH}`);
|
|
143
|
+
|
|
144
|
+
// Load service
|
|
145
|
+
const loaded = launchctl(`load "${PLIST_PATH}"`);
|
|
146
|
+
if (loaded) {
|
|
147
|
+
ok("Service loaded and started via launchd");
|
|
148
|
+
} else {
|
|
149
|
+
warn("Service plist installed but failed to load. Try:");
|
|
150
|
+
dim(`launchctl load "${PLIST_PATH}"`);
|
|
151
|
+
process.exitCode = 1;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
p("");
|
|
155
|
+
ok("OpenClaw Office service installed successfully!");
|
|
156
|
+
p("");
|
|
157
|
+
info(`Config: ${PLIST_PATH}`);
|
|
158
|
+
info(`Stdout log: ${STDOUT_LOG}`);
|
|
159
|
+
info(`Stderr log: ${STDERR_LOG}`);
|
|
160
|
+
p("");
|
|
161
|
+
info("The service will auto-start on login.");
|
|
162
|
+
dim("To start now: openclaw-office service start");
|
|
163
|
+
dim("To stop: openclaw-office service stop");
|
|
164
|
+
dim("To uninstall: openclaw-office service uninstall");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function uninstall() {
|
|
168
|
+
if (!existsSync(PLIST_PATH)) {
|
|
169
|
+
warn("Service not installed. Nothing to do.");
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (isLoaded()) {
|
|
174
|
+
info("Stopping running service...");
|
|
175
|
+
launchctl(`bootout gui/${process.getuid()}/${LABEL} 2>/dev/null || launchctl unload "${PLIST_PATH}" 2>/dev/null`);
|
|
176
|
+
ok("Service unloaded");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
unlinkSync(PLIST_PATH);
|
|
181
|
+
ok(`Plist removed: ${PLIST_PATH}`);
|
|
182
|
+
} catch {
|
|
183
|
+
err("Failed to remove plist file");
|
|
184
|
+
process.exitCode = 1;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
p("");
|
|
188
|
+
ok("OpenClaw Office service uninstalled.");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function start() {
|
|
192
|
+
if (!existsSync(PLIST_PATH)) {
|
|
193
|
+
err("Service not installed. Run: openclaw-office service install --token <token>");
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
if (isLoaded()) {
|
|
197
|
+
warn("Service is already running.");
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const result = launchctl(`load "${PLIST_PATH}"`);
|
|
201
|
+
if (result) {
|
|
202
|
+
ok("Service started");
|
|
203
|
+
} else {
|
|
204
|
+
err("Failed to start service. Check logs:");
|
|
205
|
+
dim(`cat "${STDERR_LOG}"`);
|
|
206
|
+
process.exitCode = 1;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function stop() {
|
|
211
|
+
if (!isLoaded()) {
|
|
212
|
+
warn("Service is not running.");
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
launchctl(`bootout gui/${process.getuid()}/${LABEL} 2>/dev/null || launchctl unload "${PLIST_PATH}" 2>/dev/null`);
|
|
216
|
+
ok("Service stopped");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function restart() {
|
|
220
|
+
stop();
|
|
221
|
+
// Small delay
|
|
222
|
+
setTimeout(() => start(), 500);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function status() {
|
|
226
|
+
if (!existsSync(PLIST_PATH)) {
|
|
227
|
+
warn("Service not installed");
|
|
228
|
+
p("");
|
|
229
|
+
dim("Install with: openclaw-office service install --token <token>");
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
info(`Plist: ${PLIST_PATH}`);
|
|
234
|
+
if (isLoaded()) {
|
|
235
|
+
ok("Status: running");
|
|
236
|
+
} else {
|
|
237
|
+
err("Status: stopped");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Show last few lines of log if exists
|
|
241
|
+
if (existsSync(STDOUT_LOG)) {
|
|
242
|
+
try {
|
|
243
|
+
const tail = execSync(`tail -5 "${STDOUT_LOG}"`, { stdio: "pipe" }).toString();
|
|
244
|
+
if (tail.trim()) {
|
|
245
|
+
p("");
|
|
246
|
+
info("Recent log (last 5 lines):");
|
|
247
|
+
dim(tail.trim());
|
|
248
|
+
}
|
|
249
|
+
} catch { /* ok */ }
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function showLogs(follow = false) {
|
|
254
|
+
if (!existsSync(STDOUT_LOG)) {
|
|
255
|
+
warn("No log file found.");
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
if (follow) {
|
|
260
|
+
execSync(`tail -f "${STDOUT_LOG}"`, { stdio: "inherit" });
|
|
261
|
+
} else {
|
|
262
|
+
const content = readFileSync(STDOUT_LOG, "utf-8");
|
|
263
|
+
console.log(content);
|
|
264
|
+
}
|
|
265
|
+
} catch (e) {
|
|
266
|
+
err("Failed to read log file");
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function printHelp() {
|
|
271
|
+
console.log(`
|
|
272
|
+
${C.cyan}OpenClaw Office — Service Management (macOS)${C.reset}
|
|
273
|
+
|
|
274
|
+
${C.bold}Usage:${C.reset}
|
|
275
|
+
openclaw-office service <command> [options]
|
|
276
|
+
|
|
277
|
+
${C.bold}Commands:${C.reset}
|
|
278
|
+
install Install as a launchd service (auto-start on login)
|
|
279
|
+
uninstall Remove the launchd service
|
|
280
|
+
start Start the service
|
|
281
|
+
stop Stop the service
|
|
282
|
+
restart Restart the service
|
|
283
|
+
status Show service status
|
|
284
|
+
log Show service logs (add --follow to tail)
|
|
285
|
+
|
|
286
|
+
${C.bold}Install options:${C.reset}
|
|
287
|
+
--token <token> Gateway auth token (required)
|
|
288
|
+
--gateway <url> Gateway WebSocket URL
|
|
289
|
+
--port <port> Server port (default: 5180)
|
|
290
|
+
--host <host> Bind address (default: 0.0.0.0)
|
|
291
|
+
|
|
292
|
+
${C.bold}Examples:${C.reset}
|
|
293
|
+
openclaw-office service install --token my-token
|
|
294
|
+
openclaw-office service install --token my-token --port 3000
|
|
295
|
+
openclaw-office service status
|
|
296
|
+
openclaw-office service log --follow
|
|
297
|
+
`);
|
|
298
|
+
}
|