agentglow 0.3.1 → 0.3.2

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/agentglow.mjs CHANGED
@@ -10,11 +10,11 @@ import {
10
10
  baseUrl, claudeSettings, installSettingsFile, isOurHook, pluginIdsIn, readState, startHookCommand, uninstallSettingsFile,
11
11
  } from "./lib/settings.mjs";
12
12
  import {
13
- clearLock, installCliCopy, localBase, readLock, readPid, removeCliCopies, serveForeground, spawnStarter,
13
+ clearLock, installCliCopy, localBase, readLock, readPid, refreshStaleServer, removeCliCopies, serveForeground, spawnStarter,
14
14
  startBackground, stopServer,
15
15
  } from "./lib/server.mjs";
16
16
  import { which } from "./lib/uv.mjs";
17
- import { hasAutostart, installAutostart, launchdLogPath, loginPath, removeAutostart, stopLoginItem } from "./lib/autostart.mjs";
17
+ import { hasAutostart, installAutostart, launchdLogPath, loginPath, removeAutostart, restartLoginItem, stopLoginItem } from "./lib/autostart.mjs";
18
18
 
19
19
  const SELF = fileURLToPath(import.meta.url);
20
20
  const here = path.dirname(SELF);
@@ -180,8 +180,12 @@ async function cmdSetup(o) {
180
180
  const wantLogin = o.autostart === true || (o.autostart !== false && process.platform !== "win32");
181
181
  const login = wantLogin ? installLoginItem(port) : null;
182
182
  try {
183
- if (login?.startsNow && (await waitHealthy(port, 120000))) console.log(`AgentGlow server is running on port ${port} (managed by ${login.method}).`);
184
- else {
183
+ if (login?.startsNow && (await waitHealthy(port, 120000))) {
184
+ // an older server may still hold the port (the login item's new `start` then exits at once): replace it
185
+ const r = await refreshStaleServer({ port, expected: VERSION, deps: { restart: () => restartLoginItem() } });
186
+ if (r === "stale") console.error(`agentglow: warning: an older AgentGlow server still runs on port ${port}; stop it: npx agentglow stop${portFlag(port)}`);
187
+ console.log(`AgentGlow server ${r === "down" ? "is starting" : "is running"} on port ${port} (managed by ${login.method}).`);
188
+ } else {
185
189
  const s2 = await startLocal(port);
186
190
  console.log(s2.started ? `Started the AgentGlow server on port ${port}.` : `AgentGlow server already running on port ${port}.`);
187
191
  }
@@ -143,6 +143,29 @@ export function stopLoginItem({ platform = process.platform } = {}) {
143
143
  }
144
144
  }
145
145
 
146
+ /** argv that restarts the login-item server now (kills the running one first); null on Windows (two schtasks calls). */
147
+ export function restartArgv({ platform = process.platform, uid = process.getuid?.() ?? 0 } = {}) {
148
+ if (platform === "darwin") return ["launchctl", "kickstart", "-k", `gui/${uid}/${LABEL}`];
149
+ if (platform === "win32") return null;
150
+ return ["systemctl", "--user", "restart", "agentglow"];
151
+ }
152
+
153
+ /** Restart the login-item server (e.g. setup installed a newer version while an older one runs). Never throws. */
154
+ export function restartLoginItem({ platform = process.platform } = {}) {
155
+ try {
156
+ if (platform === "win32") {
157
+ try { execFileSync("schtasks", ["/end", "/tn", TASK_NAME], { stdio: "pipe" }); } catch { /* not running */ }
158
+ execFileSync("schtasks", ["/run", "/tn", TASK_NAME], { stdio: "pipe" });
159
+ } else {
160
+ const [cmd, ...args] = restartArgv({ platform });
161
+ execFileSync(cmd, args, { stdio: "pipe" });
162
+ }
163
+ return true;
164
+ } catch {
165
+ return false;
166
+ }
167
+ }
168
+
146
169
  /** Best-effort check: is the autostart entry currently installed? Never throws. */
147
170
  export function hasAutostart({ platform = process.platform, home = os.homedir() } = {}) {
148
171
  try {
@@ -121,6 +121,55 @@ export function killTree(pid) {
121
121
  try { process.kill(-pid, "SIGTERM"); } catch { try { process.kill(pid, "SIGTERM"); } catch { /* gone */ } }
122
122
  }
123
123
 
124
+ /** Version the server on `port` reports in /live/health ("" when down or unknown). */
125
+ export async function serverVersion(port) {
126
+ try { return JSON.parse((await fetchText(localBase(port) + "/live/health", 2000))?.body || "{}").version || ""; } catch { return ""; }
127
+ }
128
+
129
+ /** PIDs listening on a local TCP port (lsof; macOS / Linux). [] when unknown. */
130
+ export function listenerPids(port) {
131
+ if (process.platform === "win32") return [];
132
+ const r = spawnSync("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], { encoding: "utf8" });
133
+ return (r.stdout || "").split(/\s+/).map(Number).filter((n) => n > 0 && n !== process.pid);
134
+ }
135
+
136
+ /**
137
+ * After `setup` (re)installed the login item: a server of another version still answering on the port (started by
138
+ * an older CLI, the SessionStart hook, or a login item launchd/systemd did not restart) is replaced by this version.
139
+ * Steps, each only while the old version still answers: stop the server this CLI started (pid file), restart the
140
+ * login item (launchctl kickstart -k / systemctl --user restart), then terminate whatever AgentGlow server still
141
+ * listens on the port. Waits for `expected` to answer. Returns "current" | "restarted" | "stale" (old one still up)
142
+ * | "down". Dependencies are injectable (tests).
143
+ */
144
+ export async function refreshStaleServer({ port, expected, log = console.log, deps = {} }) {
145
+ const d = {
146
+ version: serverVersion, stop: stopServer, restart: () => false, listeners: listenerPids, kill: (pid) => process.kill(pid, "SIGTERM"),
147
+ sleep, timeoutMs: 120000, ...deps,
148
+ };
149
+ const running = await d.version(port);
150
+ if (!running || !expected || running === expected) return "current";
151
+ log(`The AgentGlow server on port ${port} is ${running}; this CLI is ${expected}. Restarting it ...`);
152
+ const waitFor = async (ms) => {
153
+ for (const t0 = Date.now(); Date.now() - t0 < ms; await d.sleep(500)) {
154
+ const v = await d.version(port);
155
+ if (v === expected) return v;
156
+ if (v && v !== running) return v;
157
+ }
158
+ return d.version(port);
159
+ };
160
+ await d.stop(port);
161
+ let v = await d.version(port);
162
+ if (v === running && d.restart()) v = await waitFor(8000);
163
+ if (v === running) { // nobody manages it (or the supervisor left it): it is AgentGlow (it answered), stop it
164
+ for (const pid of d.listeners(port)) { try { d.kill(pid); } catch { /* gone */ } }
165
+ await d.sleep(1000);
166
+ if (!d.restart()) log(" (no login item to restart: the next claude session starts the server)");
167
+ }
168
+ v = await waitFor(d.timeoutMs);
169
+ if (v === expected) return "restarted";
170
+ return v ? "stale" : "down";
171
+ }
172
+
124
173
  /** Stop a server this CLI started. Returns "stopped" | "not-ours" | "none". */
125
174
  export async function stopServer(port) {
126
175
  const starter = readLock(port);