@ww-ai-lab/openclaw-office 2026.4.8 → 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.
@@ -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
 
@@ -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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
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
+ }