macroclaw 0.30.0 → 0.32.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.
@@ -1,7 +1,6 @@
1
1
  import { execSync } from "node:child_process";
2
- import { randomUUID } from "node:crypto";
3
2
  import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
4
- import { userInfo as osUserInfo, tmpdir } from "node:os";
3
+ import { userInfo as osUserInfo } from "node:os";
5
4
  import { dirname, join, resolve } from "node:path";
6
5
  import { createLogger } from "./logger";
7
6
 
@@ -10,12 +9,6 @@ const LAUNCHD_LABEL = "com.macroclaw";
10
9
 
11
10
  export type Platform = "launchd" | "systemd";
12
11
 
13
- interface LinuxUser {
14
- user: string;
15
- group: string;
16
- home: string;
17
- }
18
-
19
12
  export interface ServiceStatus {
20
13
  installed: boolean;
21
14
  running: boolean;
@@ -51,7 +44,7 @@ export class SystemServiceManager {
51
44
  get serviceFilePath(): string {
52
45
  return this.#platform === "launchd"
53
46
  ? resolve(this.#home, "Library/LaunchAgents/com.macroclaw.plist")
54
- : "/etc/systemd/system/macroclaw.service";
47
+ : resolve(this.#home, ".config/systemd/user/macroclaw.service");
55
48
  }
56
49
 
57
50
  get isInstalled(): boolean {
@@ -68,7 +61,7 @@ export class SystemServiceManager {
68
61
  }
69
62
  }
70
63
  try {
71
- const out = this.#exec("systemctl is-active macroclaw");
64
+ const out = this.#exec("systemctl --user is-active macroclaw");
72
65
  return out.trim() === "active";
73
66
  } catch {
74
67
  return false;
@@ -110,10 +103,13 @@ export class SystemServiceManager {
110
103
  }
111
104
 
112
105
  #installSystemd(): void {
113
- const target = this.#resolveLinuxUser();
106
+ const settingsPath = resolve(this.#home, ".macroclaw/settings.json");
107
+ if (!existsSync(settingsPath)) {
108
+ throw new Error("Settings not found. Run `macroclaw setup` first.");
109
+ }
114
110
 
115
111
  if (this.isRunning) {
116
- this.#sudo("systemctl stop macroclaw");
112
+ this.#exec("systemctl --user stop macroclaw");
117
113
  }
118
114
 
119
115
  this.#exec("bun install -g macroclaw");
@@ -123,12 +119,19 @@ export class SystemServiceManager {
123
119
 
124
120
  const pathDirs = [...new Set([dirname(bunPath), dirname(claudePath), dirname(macroclawPath)])];
125
121
 
126
- const unitContent = this.#generateSystemdUnit(bunPath, macroclawPath, target, pathDirs);
127
- this.#writeSystemdUnit(unitContent);
128
- log.debug({ filePath: this.serviceFilePath, user: target.user }, "Wrote systemd unit");
129
- this.#sudo("systemctl daemon-reload");
130
- this.#sudo("systemctl enable macroclaw");
131
- this.#sudo("systemctl start macroclaw");
122
+ // Enable lingering so user services run without an active login session
123
+ const username = osUserInfo().username;
124
+ if (!existsSync(`/var/lib/systemd/linger/${username}`)) {
125
+ this.#sudo(`loginctl enable-linger ${username}`);
126
+ }
127
+
128
+ const unitContent = this.#generateSystemdUnit(bunPath, macroclawPath, pathDirs);
129
+ mkdirSync(dirname(this.serviceFilePath), { recursive: true });
130
+ writeFileSync(this.serviceFilePath, unitContent);
131
+ log.debug({ filePath: this.serviceFilePath }, "Wrote systemd unit");
132
+ this.#exec("systemctl --user daemon-reload");
133
+ this.#exec("systemctl --user enable macroclaw");
134
+ this.#exec("systemctl --user start macroclaw");
132
135
  }
133
136
 
134
137
  uninstall(): void {
@@ -141,11 +144,11 @@ export class SystemServiceManager {
141
144
  rmSync(this.serviceFilePath);
142
145
  } else {
143
146
  if (this.isRunning) {
144
- this.#sudo("systemctl stop macroclaw");
147
+ this.#exec("systemctl --user stop macroclaw");
145
148
  }
146
- try { this.#sudo("systemctl disable macroclaw"); } catch { /* already disabled */ }
147
- this.#sudo(`rm ${this.serviceFilePath}`);
148
- this.#sudo("systemctl daemon-reload");
149
+ try { this.#exec("systemctl --user disable macroclaw"); } catch { /* already disabled */ }
150
+ rmSync(this.serviceFilePath);
151
+ this.#exec("systemctl --user daemon-reload");
149
152
  }
150
153
 
151
154
  log.debug("Service uninstalled");
@@ -161,7 +164,7 @@ export class SystemServiceManager {
161
164
  if (this.#platform === "launchd") {
162
165
  this.#exec(`launchctl load ${this.serviceFilePath}`);
163
166
  } else {
164
- this.#sudo("systemctl start macroclaw");
167
+ this.#exec("systemctl --user start macroclaw");
165
168
  }
166
169
 
167
170
  log.debug("Service started");
@@ -178,7 +181,7 @@ export class SystemServiceManager {
178
181
  if (this.#platform === "launchd") {
179
182
  this.#exec(`launchctl unload ${this.serviceFilePath}`);
180
183
  } else {
181
- this.#sudo("systemctl stop macroclaw");
184
+ this.#exec("systemctl --user stop macroclaw");
182
185
  }
183
186
 
184
187
  log.debug("Service stopped");
@@ -211,7 +214,7 @@ export class SystemServiceManager {
211
214
  } catch { /* best effort */ }
212
215
  } else {
213
216
  try {
214
- const out = this.#exec("systemctl show macroclaw --property=MainPID,ActiveEnterTimestamp --no-pager");
217
+ const out = this.#exec("systemctl --user show macroclaw --property=MainPID,ActiveEnterTimestamp --no-pager");
215
218
  const pidMatch = /MainPID=(\d+)/.exec(out);
216
219
  if (pidMatch && pidMatch[1] !== "0") result.pid = Number(pidMatch[1]);
217
220
  const tsMatch = /ActiveEnterTimestamp=(.+)/.exec(out);
@@ -231,8 +234,8 @@ export class SystemServiceManager {
231
234
  : `tail -n 50 ${logDir}/stdout.log`;
232
235
  }
233
236
  return follow
234
- ? "journalctl -u macroclaw -f"
235
- : "journalctl -u macroclaw -n 50 --no-pager";
237
+ ? "journalctl --user -u macroclaw -f"
238
+ : "journalctl --user -u macroclaw -n 50 --no-pager";
236
239
  }
237
240
 
238
241
  #exec(cmd: string): string {
@@ -278,38 +281,13 @@ export class SystemServiceManager {
278
281
  const logDir = resolve(this.#home, ".macroclaw/logs");
279
282
  return `tail -f ${logDir}/*.log`;
280
283
  }
281
- return "journalctl -u macroclaw -f";
284
+ return "journalctl --user -u macroclaw -f";
282
285
  }
283
286
 
284
287
  #sudo(cmd: string): void {
285
288
  this.#exec(`sudo ${cmd}`);
286
289
  }
287
290
 
288
- /** Write unit content to a temp file, then sudo-copy it into /etc/systemd/system/. */
289
- #writeSystemdUnit(content: string): void {
290
- const tmpPath = join(tmpdir(), `macroclaw-${randomUUID()}.service`);
291
- writeFileSync(tmpPath, content);
292
- try {
293
- this.#sudo(`cp ${tmpPath} ${this.serviceFilePath}`);
294
- } finally {
295
- try { rmSync(tmpPath); } catch { /* best-effort cleanup */ }
296
- }
297
- }
298
-
299
- #resolveLinuxUser(): LinuxUser {
300
- const info = osUserInfo();
301
- const user = info.username;
302
- const home = info.homedir;
303
- const group = this.#exec(`id -gn ${user}`).trim();
304
-
305
- const settingsPath = resolve(home, ".macroclaw/settings.json");
306
- if (!existsSync(settingsPath)) {
307
- throw new Error("Settings not found. Run `macroclaw setup` first.");
308
- }
309
-
310
- return { user, group, home };
311
- }
312
-
313
291
  #generateLaunchdPlist(bunPath: string, macroclawPath: string, pathDirs: string[], oauthToken?: string): string {
314
292
  const logDir = resolve(this.#home, ".macroclaw/logs");
315
293
  const tokenEnv = oauthToken ? `\n\t\t<key>CLAUDE_CODE_OAUTH_TOKEN</key>\n\t\t<string>${oauthToken}</string>` : "";
@@ -343,24 +321,22 @@ export class SystemServiceManager {
343
321
  `;
344
322
  }
345
323
 
346
- #generateSystemdUnit(bunPath: string, macroclawPath: string, target: LinuxUser, pathDirs: string[]): string {
324
+ #generateSystemdUnit(bunPath: string, macroclawPath: string, pathDirs: string[]): string {
347
325
  return `[Unit]
348
326
  Description=Macroclaw - Telegram-to-Claude-Code bridge
349
327
  After=network.target
350
328
 
351
329
  [Service]
352
330
  Type=simple
353
- User=${target.user}
354
- Group=${target.group}
355
- Environment=HOME=${target.home}
331
+ Environment=HOME=${this.#home}
356
332
  Environment=PATH=${pathDirs.join(":")}
357
- WorkingDirectory=${target.home}
333
+ WorkingDirectory=${this.#home}
358
334
  ExecStart=${bunPath} ${macroclawPath} start
359
335
  Restart=on-failure
360
336
  RestartSec=5
361
337
 
362
338
  [Install]
363
- WantedBy=multi-user.target
339
+ WantedBy=default.target
364
340
  `;
365
341
  }
366
342
  }