@omnixal/openclaw-nats-plugin 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli/bun-setup.ts CHANGED
@@ -11,6 +11,7 @@ import { generateApiKey, writeEnvVariables } from './env-writer';
11
11
  import {
12
12
  getServiceManager, generateSystemdUnit, generateLaunchdPlist,
13
13
  installSystemdUnit, installLaunchdPlist, startService,
14
+ registerDirectCommand,
14
15
  } from './service-units';
15
16
 
16
17
  const NATS_SERVICE = 'openclaw-nats';
@@ -58,6 +59,7 @@ export async function bunSetup(): Promise<void> {
58
59
 
59
60
  // 8. Generate and install service units
60
61
  const manager = getServiceManager();
62
+ const logsDir = join(PLUGIN_DIR, 'logs');
61
63
 
62
64
  if (manager === 'systemd') {
63
65
  const natsUnit = generateSystemdUnit({
@@ -78,7 +80,7 @@ export async function bunSetup(): Promise<void> {
78
80
  after: `${NATS_SERVICE}.service`,
79
81
  });
80
82
  installSystemdUnit(SIDECAR_SERVICE, sidecarUnit);
81
- } else {
83
+ } else if (manager === 'launchd') {
82
84
  const natsPlist = generateLaunchdPlist({
83
85
  label: 'com.openclaw.nats',
84
86
  program: NATS_SERVER_BIN,
@@ -94,6 +96,11 @@ export async function bunSetup(): Promise<void> {
94
96
  workingDirectory: SIDECAR_DIR,
95
97
  });
96
98
  installLaunchdPlist('com.openclaw.nats-sidecar', sidecarPlist);
99
+ } else {
100
+ // Direct mode — containers or systems without init
101
+ console.log('No init system detected, using direct process management');
102
+ registerDirectCommand(NATS_SERVICE, NATS_SERVER_BIN, ['-c', NATS_CONF], PLUGIN_DIR, join(logsDir, 'nats.log'));
103
+ registerDirectCommand(SIDECAR_SERVICE, 'bun', ['run', join(SIDECAR_DIR, 'src/index.ts')], SIDECAR_DIR, join(logsDir, 'sidecar.log'));
97
104
  }
98
105
 
99
106
  // 9. Start services
@@ -1,5 +1,5 @@
1
- import { mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
2
- import { execFileSync } from 'node:child_process';
1
+ import { mkdirSync, writeFileSync, readFileSync, rmSync, existsSync } from 'node:fs';
2
+ import { execFileSync, spawn } from 'node:child_process';
3
3
  import { join } from 'node:path';
4
4
  import { homedir, platform } from 'node:os';
5
5
 
@@ -23,8 +23,27 @@ export interface LaunchdPlistOptions {
23
23
 
24
24
  // --- Detection ---
25
25
 
26
- export function getServiceManager(): 'systemd' | 'launchd' {
27
- return platform() === 'darwin' ? 'launchd' : 'systemd';
26
+ function isContainer(): boolean {
27
+ try {
28
+ return existsSync('/.dockerenv') || readFileSync('/proc/1/cgroup', 'utf-8').includes('docker');
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+
34
+ function hasSystemctl(): boolean {
35
+ try {
36
+ execFileSync('systemctl', ['--version'], { stdio: 'pipe' });
37
+ return true;
38
+ } catch {
39
+ return false;
40
+ }
41
+ }
42
+
43
+ export function getServiceManager(): 'systemd' | 'launchd' | 'direct' {
44
+ if (platform() === 'darwin') return 'launchd';
45
+ if (isContainer() || !hasSystemctl()) return 'direct';
46
+ return 'systemd';
28
47
  }
29
48
 
30
49
  // --- Generators ---
@@ -103,34 +122,103 @@ export function installLaunchdPlist(label: string, content: string): void {
103
122
  writeFileSync(filePath, content, 'utf-8');
104
123
  }
105
124
 
125
+ // --- Direct mode (containers / no init system) ---
126
+
127
+ function pidDir(): string {
128
+ const dir = join(homedir(), '.openclaw', 'nats-plugin', 'pids');
129
+ mkdirSync(dir, { recursive: true });
130
+ return dir;
131
+ }
132
+
133
+ function pidFile(name: string): string {
134
+ return join(pidDir(), `${name}.pid`);
135
+ }
136
+
137
+ function readPid(name: string): number | null {
138
+ try {
139
+ const pid = parseInt(readFileSync(pidFile(name), 'utf-8').trim(), 10);
140
+ process.kill(pid, 0); // check if alive
141
+ return pid;
142
+ } catch {
143
+ return null;
144
+ }
145
+ }
146
+
147
+ // Stored commands for direct-mode start
148
+ const directCommands = new Map<string, { cmd: string; args: string[]; cwd: string; logFile: string }>();
149
+
150
+ export function registerDirectCommand(name: string, cmd: string, args: string[], cwd: string, logFile: string): void {
151
+ directCommands.set(name, { cmd, args, cwd, logFile });
152
+ }
153
+
154
+ function startDirect(name: string): void {
155
+ const entry = directCommands.get(name);
156
+ if (!entry) throw new Error(`No command registered for service "${name}". Call registerDirectCommand first.`);
157
+
158
+ const { cmd, args, cwd, logFile } = entry;
159
+ const out = require('node:fs').openSync(logFile, 'a');
160
+ const child = spawn(cmd, args, {
161
+ cwd,
162
+ stdio: ['ignore', out, out],
163
+ detached: true,
164
+ });
165
+ child.unref();
166
+ if (child.pid) {
167
+ writeFileSync(pidFile(name), String(child.pid));
168
+ }
169
+ }
170
+
171
+ function stopDirect(name: string): void {
172
+ const pid = readPid(name);
173
+ if (pid) {
174
+ try { process.kill(pid, 'SIGTERM'); } catch { /* already dead */ }
175
+ rmSync(pidFile(name), { force: true });
176
+ }
177
+ }
178
+
179
+ function isRunningDirect(name: string): boolean {
180
+ return readPid(name) !== null;
181
+ }
182
+
183
+ // --- Public API ---
184
+
106
185
  export function startService(name: string): void {
107
- if (getServiceManager() === 'systemd') {
186
+ const mgr = getServiceManager();
187
+ if (mgr === 'systemd') {
108
188
  execFileSync('systemctl', ['--user', 'enable', '--now', `${name}.service`]);
109
- } else {
189
+ } else if (mgr === 'launchd') {
110
190
  const plistPath = join(launchdAgentsDir(), `${name}.plist`);
111
191
  execFileSync('launchctl', ['load', '-w', plistPath]);
192
+ } else {
193
+ startDirect(name);
112
194
  }
113
195
  }
114
196
 
115
197
  export function stopService(name: string): void {
116
- if (getServiceManager() === 'systemd') {
198
+ const mgr = getServiceManager();
199
+ if (mgr === 'systemd') {
117
200
  execFileSync('systemctl', ['--user', 'stop', `${name}.service`]);
118
- } else {
201
+ } else if (mgr === 'launchd') {
119
202
  const plistPath = join(launchdAgentsDir(), `${name}.plist`);
120
203
  execFileSync('launchctl', ['unload', plistPath]);
204
+ } else {
205
+ stopDirect(name);
121
206
  }
122
207
  }
123
208
 
124
209
  export function isServiceRunning(name: string): boolean {
125
210
  try {
126
- if (getServiceManager() === 'systemd') {
211
+ const mgr = getServiceManager();
212
+ if (mgr === 'systemd') {
127
213
  const result = execFileSync('systemctl', ['--user', 'is-active', `${name}.service`], {
128
214
  encoding: 'utf-8',
129
215
  });
130
216
  return result.trim() === 'active';
131
- } else {
217
+ } else if (mgr === 'launchd') {
132
218
  const result = execFileSync('launchctl', ['list', name], { encoding: 'utf-8' });
133
219
  return result.includes(name);
220
+ } else {
221
+ return isRunningDirect(name);
134
222
  }
135
223
  } catch {
136
224
  return false;
@@ -138,31 +226,19 @@ export function isServiceRunning(name: string): boolean {
138
226
  }
139
227
 
140
228
  export function removeServiceUnit(name: string): void {
141
- if (getServiceManager() === 'systemd') {
142
- try {
143
- execFileSync('systemctl', ['--user', 'stop', `${name}.service`]);
144
- } catch {
145
- // service may not be running
146
- }
147
- try {
148
- execFileSync('systemctl', ['--user', 'disable', `${name}.service`]);
149
- } catch {
150
- // service may not be enabled
151
- }
229
+ const mgr = getServiceManager();
230
+ if (mgr === 'systemd') {
231
+ try { execFileSync('systemctl', ['--user', 'stop', `${name}.service`]); } catch { /* not running */ }
232
+ try { execFileSync('systemctl', ['--user', 'disable', `${name}.service`]); } catch { /* not enabled */ }
152
233
  const filePath = join(systemdUserDir(), `${name}.service`);
153
234
  rmSync(filePath, { force: true });
154
- try {
155
- execFileSync('systemctl', ['--user', 'daemon-reload']);
156
- } catch {
157
- // best effort
158
- }
159
- } else {
235
+ try { execFileSync('systemctl', ['--user', 'daemon-reload']); } catch { /* best effort */ }
236
+ } else if (mgr === 'launchd') {
160
237
  const plistPath = join(launchdAgentsDir(), `${name}.plist`);
161
- try {
162
- execFileSync('launchctl', ['unload', plistPath]);
163
- } catch {
164
- // may not be loaded
165
- }
238
+ try { execFileSync('launchctl', ['unload', plistPath]); } catch { /* may not be loaded */ }
166
239
  rmSync(plistPath, { force: true });
240
+ } else {
241
+ stopDirect(name);
242
+ rmSync(pidFile(name), { force: true });
167
243
  }
168
244
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnixal/openclaw-nats-plugin",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "NATS JetStream event-driven plugin for OpenClaw",
5
5
  "license": "MIT",
6
6
  "type": "module",