macroclaw 0.13.0 → 0.14.0

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.
@@ -10,40 +10,6 @@ const LAUNCHD_LABEL = "com.macroclaw";
10
10
 
11
11
  export type Platform = "launchd" | "systemd";
12
12
 
13
- export interface ServiceDeps {
14
- existsSync: (path: string) => boolean;
15
- writeFileSync: (path: string, data: string) => void;
16
- mkdirSync: (path: string, opts?: { recursive: boolean }) => void;
17
- rmSync: (path: string) => void;
18
- execSync: (cmd: string, opts?: object) => string;
19
- tmpdir: () => string;
20
- randomUUID: () => string;
21
- userInfo: () => { username: string; homedir: string };
22
- platform: string;
23
- home: string;
24
- }
25
-
26
- function defaultDeps(): ServiceDeps {
27
- return {
28
- existsSync,
29
- writeFileSync,
30
- mkdirSync: (path, opts) => mkdirSync(path, opts),
31
- rmSync,
32
- execSync: (cmd, opts) => execSync(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], ...opts }).toString(),
33
- tmpdir,
34
- randomUUID,
35
- userInfo: () => ({ username: osUserInfo().username, homedir: osUserInfo().homedir }),
36
- platform: process.platform,
37
- home: process.env.HOME || "~",
38
- };
39
- }
40
-
41
- function detectPlatform(platform: string): Platform {
42
- if (platform === "darwin") return "launchd";
43
- if (platform === "linux") return "systemd";
44
- throw new Error("Unsupported platform. Only macOS (launchd) and Linux (systemd) are supported.");
45
- }
46
-
47
13
  interface LinuxUser {
48
14
  user: string;
49
15
  group: string;
@@ -58,23 +24,19 @@ export interface ServiceStatus {
58
24
  uptime?: string;
59
25
  }
60
26
 
61
- export interface SystemService {
62
- install: (oauthToken?: string) => string;
63
- uninstall: () => void;
64
- start: () => string;
65
- stop: () => void;
66
- update: () => string;
67
- status: () => ServiceStatus;
68
- logs: (follow?: boolean) => string;
69
- }
70
-
71
- export class ServiceManager implements SystemService {
72
- readonly #deps: ServiceDeps;
27
+ export class SystemServiceManager {
73
28
  readonly #platform: Platform;
29
+ readonly #home: string;
30
+
31
+ constructor(opts?: { platform?: string; home?: string }) {
32
+ this.#platform = SystemServiceManager.#detectPlatform(opts?.platform ?? process.platform);
33
+ this.#home = opts?.home ?? process.env.HOME ?? "~";
34
+ }
74
35
 
75
- constructor(deps?: Partial<ServiceDeps>) {
76
- this.#deps = { ...defaultDeps(), ...deps };
77
- this.#platform = detectPlatform(this.#deps.platform);
36
+ static #detectPlatform(platform: string): Platform {
37
+ if (platform === "darwin") return "launchd";
38
+ if (platform === "linux") return "systemd";
39
+ throw new Error("Unsupported platform. Only macOS (launchd) and Linux (systemd) are supported.");
78
40
  }
79
41
 
80
42
  get platform(): Platform {
@@ -83,26 +45,25 @@ export class ServiceManager implements SystemService {
83
45
 
84
46
  get serviceFilePath(): string {
85
47
  return this.#platform === "launchd"
86
- ? resolve(this.#deps.home, "Library/LaunchAgents/com.macroclaw.plist")
48
+ ? resolve(this.#home, "Library/LaunchAgents/com.macroclaw.plist")
87
49
  : "/etc/systemd/system/macroclaw.service";
88
50
  }
89
51
 
90
52
  get isInstalled(): boolean {
91
- return this.#deps.existsSync(this.serviceFilePath);
53
+ return existsSync(this.serviceFilePath);
92
54
  }
93
55
 
94
56
  get isRunning(): boolean {
95
57
  if (this.#platform === "launchd") {
96
58
  try {
97
- const out = this.#deps.execSync(`launchctl list ${LAUNCHD_LABEL}`);
98
- // If the PID line shows a number (not "-"), the service is running
59
+ const out = this.#exec(`launchctl list ${LAUNCHD_LABEL}`);
99
60
  return /"PID"\s*=\s*\d+/.test(out);
100
61
  } catch {
101
62
  return false;
102
63
  }
103
64
  }
104
65
  try {
105
- const out = this.#deps.execSync("systemctl is-active macroclaw");
66
+ const out = this.#exec("systemctl is-active macroclaw");
106
67
  return out.trim() === "active";
107
68
  } catch {
108
69
  return false;
@@ -120,27 +81,27 @@ export class ServiceManager implements SystemService {
120
81
  }
121
82
 
122
83
  #installLaunchd(oauthToken?: string): void {
123
- const settingsPath = resolve(this.#deps.home, ".macroclaw/settings.json");
124
- if (!this.#deps.existsSync(settingsPath)) {
84
+ const settingsPath = resolve(this.#home, ".macroclaw/settings.json");
85
+ if (!existsSync(settingsPath)) {
125
86
  throw new Error("Settings not found. Run `macroclaw setup` first.");
126
87
  }
127
88
 
128
- this.#deps.execSync("bun install -g macroclaw");
89
+ this.#exec("bun install -g macroclaw");
129
90
  const bunPath = this.#resolvePath("bun");
130
91
  const claudePath = this.#resolvePath("claude");
131
92
  const macroclawPath = this.#resolveGlobalBinPath("macroclaw");
132
93
 
133
94
  const pathDirs = [...new Set([dirname(bunPath), dirname(claudePath), dirname(macroclawPath)])];
134
95
 
135
- const logDir = resolve(this.#deps.home, ".macroclaw/logs");
136
- this.#deps.mkdirSync(logDir, { recursive: true });
96
+ const logDir = resolve(this.#home, ".macroclaw/logs");
97
+ mkdirSync(logDir, { recursive: true });
137
98
  if (this.isRunning) {
138
- this.#deps.execSync(`launchctl unload ${this.serviceFilePath}`);
99
+ this.#exec(`launchctl unload ${this.serviceFilePath}`);
139
100
  }
140
101
 
141
- this.#deps.writeFileSync(this.serviceFilePath, this.#generateLaunchdPlist(bunPath, macroclawPath, pathDirs, oauthToken));
102
+ writeFileSync(this.serviceFilePath, this.#generateLaunchdPlist(bunPath, macroclawPath, pathDirs, oauthToken));
142
103
  log.debug({ filePath: this.serviceFilePath }, "Wrote launchd plist");
143
- this.#deps.execSync(`launchctl load ${this.serviceFilePath}`);
104
+ this.#exec(`launchctl load ${this.serviceFilePath}`);
144
105
  }
145
106
 
146
107
  #installSystemd(): void {
@@ -150,7 +111,7 @@ export class ServiceManager implements SystemService {
150
111
  this.#sudo("systemctl stop macroclaw");
151
112
  }
152
113
 
153
- this.#deps.execSync("bun install -g macroclaw");
114
+ this.#exec("bun install -g macroclaw");
154
115
  const bunPath = this.#resolvePath("bun");
155
116
  const claudePath = this.#resolvePath("claude");
156
117
  const macroclawPath = this.#resolveGlobalBinPath("macroclaw");
@@ -170,9 +131,9 @@ export class ServiceManager implements SystemService {
170
131
 
171
132
  if (this.#platform === "launchd") {
172
133
  if (this.isRunning) {
173
- this.#deps.execSync(`launchctl unload ${this.serviceFilePath}`);
134
+ this.#exec(`launchctl unload ${this.serviceFilePath}`);
174
135
  }
175
- this.#deps.rmSync(this.serviceFilePath);
136
+ rmSync(this.serviceFilePath);
176
137
  } else {
177
138
  if (this.isRunning) {
178
139
  this.#sudo("systemctl stop macroclaw");
@@ -193,7 +154,7 @@ export class ServiceManager implements SystemService {
193
154
  }
194
155
 
195
156
  if (this.#platform === "launchd") {
196
- this.#deps.execSync(`launchctl load ${this.serviceFilePath}`);
157
+ this.#exec(`launchctl load ${this.serviceFilePath}`);
197
158
  } else {
198
159
  this.#sudo("systemctl start macroclaw");
199
160
  }
@@ -210,7 +171,7 @@ export class ServiceManager implements SystemService {
210
171
  }
211
172
 
212
173
  if (this.#platform === "launchd") {
213
- this.#deps.execSync(`launchctl unload ${this.serviceFilePath}`);
174
+ this.#exec(`launchctl unload ${this.serviceFilePath}`);
214
175
  } else {
215
176
  this.#sudo("systemctl stop macroclaw");
216
177
  }
@@ -223,15 +184,15 @@ export class ServiceManager implements SystemService {
223
184
 
224
185
  if (this.#platform === "launchd") {
225
186
  if (this.isRunning) {
226
- this.#deps.execSync(`launchctl unload ${this.serviceFilePath}`);
187
+ this.#exec(`launchctl unload ${this.serviceFilePath}`);
227
188
  }
228
- this.#deps.execSync("bun install -g macroclaw@latest");
229
- this.#deps.execSync(`launchctl load ${this.serviceFilePath}`);
189
+ this.#exec("bun install -g macroclaw@latest");
190
+ this.#exec(`launchctl load ${this.serviceFilePath}`);
230
191
  } else {
231
192
  if (this.isRunning) {
232
193
  this.#sudo("systemctl stop macroclaw");
233
194
  }
234
- this.#deps.execSync("bun install -g macroclaw@latest");
195
+ this.#exec("bun install -g macroclaw@latest");
235
196
  this.#sudo("systemctl start macroclaw");
236
197
  }
237
198
 
@@ -249,13 +210,13 @@ export class ServiceManager implements SystemService {
249
210
  if (result.running) {
250
211
  if (this.#platform === "launchd") {
251
212
  try {
252
- const out = this.#deps.execSync(`launchctl list ${LAUNCHD_LABEL}`);
213
+ const out = this.#exec(`launchctl list ${LAUNCHD_LABEL}`);
253
214
  const pidMatch = /"PID"\s*=\s*(\d+)/.exec(out);
254
215
  if (pidMatch) result.pid = Number(pidMatch[1]);
255
216
  } catch { /* best effort */ }
256
217
  } else {
257
218
  try {
258
- const out = this.#deps.execSync("systemctl show macroclaw --property=MainPID,ActiveEnterTimestamp --no-pager");
219
+ const out = this.#exec("systemctl show macroclaw --property=MainPID,ActiveEnterTimestamp --no-pager");
259
220
  const pidMatch = /MainPID=(\d+)/.exec(out);
260
221
  if (pidMatch && pidMatch[1] !== "0") result.pid = Number(pidMatch[1]);
261
222
  const tsMatch = /ActiveEnterTimestamp=(.+)/.exec(out);
@@ -269,7 +230,7 @@ export class ServiceManager implements SystemService {
269
230
 
270
231
  logs(follow = false): string {
271
232
  if (this.#platform === "launchd") {
272
- const logDir = resolve(this.#deps.home, ".macroclaw/logs");
233
+ const logDir = resolve(this.#home, ".macroclaw/logs");
273
234
  return follow
274
235
  ? `tail -f ${logDir}/stdout.log ${logDir}/stderr.log`
275
236
  : `tail -n 50 ${logDir}/stdout.log`;
@@ -279,24 +240,27 @@ export class ServiceManager implements SystemService {
279
240
  : "journalctl -u macroclaw -n 50 --no-pager";
280
241
  }
281
242
 
243
+ #exec(cmd: string): string {
244
+ return execSync(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).toString();
245
+ }
246
+
282
247
  #resolvePath(binary: string): string {
283
248
  try {
284
- return this.#deps.execSync(`which ${binary}`).trim();
249
+ return this.#exec(`which ${binary}`).trim();
285
250
  } catch {
286
251
  throw new Error(`Could not resolve ${binary} path. Is it installed?`);
287
252
  }
288
253
  }
289
254
 
290
255
  #resolveGlobalBinPath(binary: string): string {
291
- const binDir = this.#deps.execSync("bun pm bin -g").trim();
256
+ const binDir = this.#exec("bun pm bin -g").trim();
292
257
  const binPath = join(binDir, binary);
293
- if (!this.#deps.existsSync(binPath)) {
258
+ if (!existsSync(binPath)) {
294
259
  throw new Error(`Could not find ${binary} in ${binDir}. Is it installed?`);
295
260
  }
296
261
  return binPath;
297
262
  }
298
263
 
299
-
300
264
  #requireInstalled(): void {
301
265
  if (!this.isInstalled) {
302
266
  throw new Error("Service not installed. Run `macroclaw service install` first.");
@@ -305,43 +269,43 @@ export class ServiceManager implements SystemService {
305
269
 
306
270
  #logTailCommand(): string {
307
271
  if (this.#platform === "launchd") {
308
- const logDir = resolve(this.#deps.home, ".macroclaw/logs");
272
+ const logDir = resolve(this.#home, ".macroclaw/logs");
309
273
  return `tail -f ${logDir}/*.log`;
310
274
  }
311
275
  return "journalctl -u macroclaw -f";
312
276
  }
313
277
 
314
278
  #sudo(cmd: string): void {
315
- this.#deps.execSync(`sudo ${cmd}`);
279
+ this.#exec(`sudo ${cmd}`);
316
280
  }
317
281
 
318
282
  /** Write unit content to a temp file, then sudo-copy it into /etc/systemd/system/. */
319
283
  #writeSystemdUnit(content: string): void {
320
- const tmpPath = join(this.#deps.tmpdir(), `macroclaw-${this.#deps.randomUUID()}.service`);
321
- this.#deps.writeFileSync(tmpPath, content);
284
+ const tmpPath = join(tmpdir(), `macroclaw-${randomUUID()}.service`);
285
+ writeFileSync(tmpPath, content);
322
286
  try {
323
287
  this.#sudo(`cp ${tmpPath} ${this.serviceFilePath}`);
324
288
  } finally {
325
- try { this.#deps.rmSync(tmpPath); } catch { /* best-effort cleanup */ }
289
+ try { rmSync(tmpPath); } catch { /* best-effort cleanup */ }
326
290
  }
327
291
  }
328
292
 
329
293
  #resolveLinuxUser(): LinuxUser {
330
- const info = this.#deps.userInfo();
294
+ const info = osUserInfo();
331
295
  const user = info.username;
332
296
  const home = info.homedir;
333
- const group = this.#deps.execSync(`id -gn ${user}`).trim();
297
+ const group = this.#exec(`id -gn ${user}`).trim();
334
298
 
335
299
  const settingsPath = resolve(home, ".macroclaw/settings.json");
336
- if (!this.#deps.existsSync(settingsPath)) {
337
- throw new Error(`Settings not found. Run \`macroclaw setup\` first.`);
300
+ if (!existsSync(settingsPath)) {
301
+ throw new Error("Settings not found. Run `macroclaw setup` first.");
338
302
  }
339
303
 
340
304
  return { user, group, home };
341
305
  }
342
306
 
343
307
  #generateLaunchdPlist(bunPath: string, macroclawPath: string, pathDirs: string[], oauthToken?: string): string {
344
- const logDir = resolve(this.#deps.home, ".macroclaw/logs");
308
+ const logDir = resolve(this.#home, ".macroclaw/logs");
345
309
  const tokenEnv = oauthToken ? `\n\t\t<key>CLAUDE_CODE_OAUTH_TOKEN</key>\n\t\t<string>${oauthToken}</string>` : "";
346
310
  return `<?xml version="1.0" encoding="UTF-8"?>
347
311
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -364,7 +328,7 @@ export class ServiceManager implements SystemService {
364
328
  <key>EnvironmentVariables</key>
365
329
  <dict>
366
330
  <key>HOME</key>
367
- <string>${this.#deps.home}</string>
331
+ <string>${this.#home}</string>
368
332
  <key>PATH</key>
369
333
  <string>${pathDirs.join(":")}</string>${tokenEnv}
370
334
  </dict>