agentglow 0.2.5 → 0.2.7
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 +74 -3
- package/cli/lib/autostart.mjs +159 -0
- package/cli/lib/server.mjs +10 -3
- package/dist/chunks/{KitScene-C3SZiKL5.js → KitScene-BZtsx51r.js} +1824 -1593
- package/dist/chunks/{index-CvDEulcq.js → index-B5zc-pNd.js} +1 -1
- package/dist/chunks/{index-C4F16M9q.js → index-BCfJ-w3o.js} +1 -1
- package/dist/chunks/{index-CRRHsMqy.js → index-BLui9fiT.js} +15 -15
- package/dist/chunks/{index-Bm4te7Ug.js → index-BO7u06kP.js} +1 -1
- package/dist/chunks/{index-BfGluv59.js → index-BbVDdhDk.js} +1 -1
- package/dist/chunks/{index-Bw31uayO.js → index-Bf3wbpgc.js} +1 -1
- package/dist/chunks/{index-Bsmr7FwH.js → index-BflIFHAh.js} +1 -1
- package/dist/chunks/{index-Bqv_wy49.js → index-CdGkRbWi.js} +1 -1
- package/dist/chunks/{index-CdzGoWrn.js → index-CfKe9fU0.js} +1 -1
- package/dist/chunks/{index-BZOG_CQS.js → index-D2jsa-xs.js} +1 -1
- package/dist/chunks/{index-DdCs_ye9.js → index-HcaVLPZo.js} +1 -1
- package/dist/chunks/{index-CS8eZTr_.js → index-Ofl4pl62.js} +1 -1
- package/dist/chunks/{index--dwzdFRh.js → index-T1KK0afI.js} +1 -1
- package/dist/chunks/{index-DXBfdHew.js → index-cB0OFPT_.js} +1 -1
- package/dist/chunks/{index-B2PGx_wt.js → index-dlKd-7aA.js} +1 -1
- package/dist/chunks/{index-_KrED-np.js → index-z7UiIHw8.js} +1 -1
- package/dist/index.js +1 -1
- package/dist/style.css +1 -1
- package/dist/types/world.d.ts +37 -0
- package/package.json +1 -1
package/cli/agentglow.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
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
18
|
|
|
18
19
|
const SELF = fileURLToPath(import.meta.url);
|
|
19
20
|
const here = path.dirname(SELF);
|
|
@@ -31,6 +32,7 @@ const HELP = `agentglow ${VERSION}: watch Claude Code agents in 3D
|
|
|
31
32
|
npx agentglow remove undo setup: remove the hooks and stop the server
|
|
32
33
|
npx agentglow claude [-- <args>] try it without installing: one claude session with AgentGlow
|
|
33
34
|
|
|
35
|
+
|
|
34
36
|
More: npx agentglow start [--background] [--port N] (serve = start in the foreground)
|
|
35
37
|
Env: AGENTGLOW_URL (remote server), AGENTGLOW_API_KEY (ingest key), AGENTGLOW_CACHE_DIR
|
|
36
38
|
`;
|
|
@@ -57,6 +59,8 @@ function parseArgs(argv) {
|
|
|
57
59
|
else if (a === "--no-open") o.open = false;
|
|
58
60
|
else if (a === "--install") o.install = true;
|
|
59
61
|
else if (a === "--uninstall") o.uninstall = true;
|
|
62
|
+
else if (a === "--remove") o.remove = true;
|
|
63
|
+
else if (a === "--no-autostart") o.autostart = false;
|
|
60
64
|
else if (a === "--background" || a === "-b") o.background = true;
|
|
61
65
|
else if (a === "--quiet" || a === "-q") o.quiet = true;
|
|
62
66
|
else if (a === "-h" || a === "--help") o.help = true;
|
|
@@ -154,9 +158,19 @@ async function cmdSetup(o) {
|
|
|
154
158
|
if (remote) {
|
|
155
159
|
if ((await probe(remote, 4000)) !== "agentglow") console.error(`agentglow: warning: ${remote}/live/health did not answer like AgentGlow.`);
|
|
156
160
|
} else {
|
|
161
|
+
// Default: the OS owns the server (login item: launchd / systemd --user / Task Scheduler), so it is already
|
|
162
|
+
// running for every claude session, survives reboots and restarts on crash. The SessionStart hook installed
|
|
163
|
+
// above stays as a cheap safety net. If the login item can't be installed, the hook alone covers it.
|
|
164
|
+
// Login item by default on macOS/Linux only; Windows keeps per-session auto-start via the SessionStart hook
|
|
165
|
+
// (explicit `npx agentglow autostart` still registers one there).
|
|
166
|
+
const wantLogin = o.autostart === true || (o.autostart !== false && process.platform !== "win32");
|
|
167
|
+
const login = wantLogin ? installLoginItem(port) : null;
|
|
157
168
|
try {
|
|
158
|
-
|
|
159
|
-
|
|
169
|
+
if (login?.startsNow && (await waitHealthy(port, 120000))) console.log(`AgentGlow server is running on port ${port} (managed by ${login.method}).`);
|
|
170
|
+
else {
|
|
171
|
+
const s2 = await startLocal(port);
|
|
172
|
+
console.log(s2.started ? `Started the AgentGlow server on port ${port}.` : `AgentGlow server already running on port ${port}.`);
|
|
173
|
+
}
|
|
160
174
|
} catch (e) {
|
|
161
175
|
console.error(`agentglow: ${e.message}`);
|
|
162
176
|
console.error(`The hooks are installed; start the server with: npx agentglow start --background${portFlag(port)}`);
|
|
@@ -168,6 +182,50 @@ async function cmdSetup(o) {
|
|
|
168
182
|
return 0;
|
|
169
183
|
}
|
|
170
184
|
|
|
185
|
+
/** Register the server as a login item (launchd / systemd --user / Task Scheduler or Startup folder).
|
|
186
|
+
* Returns { method, path, startsNow } or null if it could not be installed (setup then relies on the hook). */
|
|
187
|
+
function installLoginItem(port) {
|
|
188
|
+
const script = installCliCopy({ srcCliDir: here, version: VERSION });
|
|
189
|
+
const node = nodeForHook() || process.execPath;
|
|
190
|
+
const command = startHookCommand({ port, version: VERSION, node, script, cacheDir: process.env.AGENTGLOW_CACHE_DIR });
|
|
191
|
+
// macOS/Linux: the supervisor runs the server in the foreground (argv, no shell); Windows keeps the shell command
|
|
192
|
+
const argv = [node, script, "start", "--port", String(port)];
|
|
193
|
+
const env = { PATH: loginPath({ node }), HOME: os.homedir() };
|
|
194
|
+
for (const k of ["AGENTGLOW_CACHE_DIR", "AGENTGLOW_API_KEY", "AGENTGLOW_INGEST_KEY", "AGENTGLOW_SECRET", "AGENTGLOW_PY_SPEC"]) {
|
|
195
|
+
if (process.env[k]) env[k] = process.env[k];
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
const r = installAutostart({ command, argv, env });
|
|
199
|
+
const logs = process.platform === "darwin" ? launchdLogPath() : process.platform === "win32" ? null : "journalctl --user -u agentglow";
|
|
200
|
+
console.log(`Server starts at login via ${r.method}${logs ? ` (logs: ${logs})` : ""}.`);
|
|
201
|
+
return { ...r, startsNow: process.platform !== "win32" };
|
|
202
|
+
} catch (e) {
|
|
203
|
+
console.error(`agentglow: note: could not register a login item (${e.message}); the server will start with each claude session instead.`);
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function waitHealthy(port, ms) {
|
|
209
|
+
const until = Date.now() + ms;
|
|
210
|
+
let said = false;
|
|
211
|
+
while (Date.now() < until) {
|
|
212
|
+
if ((await probe(localBase(port), 1500)) === "agentglow") return true;
|
|
213
|
+
if (!said && Date.now() > until - ms + 4000) { console.log("First start downloads Python + the server (~30-60s) ..."); said = true; }
|
|
214
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
215
|
+
}
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** `autostart` = setup (which now includes the login item); `autostart --remove` = drop just the login item. */
|
|
220
|
+
async function cmdAutostart(o) {
|
|
221
|
+
if (o.remove) {
|
|
222
|
+
removeAutostart();
|
|
223
|
+
console.log(`Removed the AgentGlow login item. The hooks stay; the server now starts with each claude session.`);
|
|
224
|
+
return 0;
|
|
225
|
+
}
|
|
226
|
+
return cmdSetup({ ...o, autostart: true });
|
|
227
|
+
}
|
|
228
|
+
|
|
171
229
|
async function cmdRemove(o) {
|
|
172
230
|
const file = settingsFile();
|
|
173
231
|
const r = uninstallSettingsFile(file);
|
|
@@ -178,11 +236,17 @@ async function cmdRemove(o) {
|
|
|
178
236
|
if (st?.backup) console.log(` Settings from before setup: ${st.backup} (restore: cp "${st.backup}" "${file}")`);
|
|
179
237
|
} else console.log(`No AgentGlow entries in ${file}.`);
|
|
180
238
|
const port = o.portSet ? o.port : st?.port || o.port;
|
|
181
|
-
|
|
239
|
+
const login = hasAutostart();
|
|
240
|
+
if (!remoteUrl() && !login && (st?.startHook || !st)) {
|
|
182
241
|
const s = await stopServer(port);
|
|
183
242
|
if (s === "stopped") console.log(`Stopped the AgentGlow server on port ${port}.`);
|
|
184
243
|
else if (s === "not-ours") console.log(`An AgentGlow server on port ${port} was not started by this CLI; left it running.`);
|
|
185
244
|
}
|
|
245
|
+
if (login) { // `remove` undoes everything, incl. the login item (it points at the CLI copy removed below)
|
|
246
|
+
removeAutostart();
|
|
247
|
+
await stopServer(port); // in case a session-started server is also running
|
|
248
|
+
console.log(`Removed the AgentGlow login item and stopped its server.`);
|
|
249
|
+
}
|
|
186
250
|
try { removeCliCopies(); } catch { /* ignore */ }
|
|
187
251
|
return 0;
|
|
188
252
|
}
|
|
@@ -254,6 +318,7 @@ async function cmdStatus(o) {
|
|
|
254
318
|
console.log(`server: ${health?.ok ? `healthy at ${base} (server ${health.version || "?"})` : starting ? `starting on port ${port}` : `not running at ${base}`}`);
|
|
255
319
|
if (!remote) console.log(`port: ${port}${pid ? ` pid ${pid} (started by this CLI)` : ""}`);
|
|
256
320
|
console.log(`hooks: ${installed ? `installed in ${file}${startHook ? " (server auto-starts with claude)" : ""}` : "not installed (run: npx agentglow setup)"}`);
|
|
321
|
+
if (hasAutostart()) console.log(`login: server starts at login and restarts on crash`);
|
|
257
322
|
if (health?.ok) console.log(`view: ${base}/neural`);
|
|
258
323
|
return 0;
|
|
259
324
|
}
|
|
@@ -314,6 +379,8 @@ async function main() {
|
|
|
314
379
|
switch (o.cmd) {
|
|
315
380
|
case "setup":
|
|
316
381
|
return cmdSetup(o);
|
|
382
|
+
case "autostart":
|
|
383
|
+
return cmdAutostart(o);
|
|
317
384
|
case "remove":
|
|
318
385
|
case "uninstall":
|
|
319
386
|
return cmdRemove(o);
|
|
@@ -328,6 +395,10 @@ async function main() {
|
|
|
328
395
|
case "stop": {
|
|
329
396
|
const port = effectivePort(o);
|
|
330
397
|
const r = await stopServer(port);
|
|
398
|
+
if (r === "not-ours" && hasAutostart() && stopLoginItem()) {
|
|
399
|
+
console.log(`Stopped the AgentGlow server on port ${port} (it starts again at next login or with the next claude session).`);
|
|
400
|
+
return 0;
|
|
401
|
+
}
|
|
331
402
|
if (r === "stopped") console.log(`Stopped the AgentGlow server on port ${port}.`);
|
|
332
403
|
else if (r === "not-ours") console.log(`The AgentGlow server on port ${port} was not started by this CLI; stop it where it runs.`);
|
|
333
404
|
else console.log(`No AgentGlow server on port ${port}.`);
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// OS-native "start AgentGlow at login" (installed by `agentglow setup`): the server is always already running for
|
|
2
|
+
// every claude session, survives reboots and is restarted on crash. macOS/Linux run it in the foreground under
|
|
3
|
+
// launchd/systemd (argv); Windows uses Task Scheduler (or the Startup folder) with the hook's shell command.
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
|
|
9
|
+
export const TASK_NAME = "AgentGlow";
|
|
10
|
+
const LABEL = "com.agentglow.server";
|
|
11
|
+
|
|
12
|
+
export const plistPath = (home = os.homedir()) => path.join(home, "Library", "LaunchAgents", `${LABEL}.plist`);
|
|
13
|
+
export const systemdUnitPath = (home = os.homedir()) =>
|
|
14
|
+
path.join(home, ".config", "systemd", "user", "agentglow.service");
|
|
15
|
+
export const startupScriptPath = (home = os.homedir()) =>
|
|
16
|
+
path.join(home, "AppData", "Roaming", "Microsoft", "Windows", "Start Menu", "Programs", "Startup", "AgentGlow.vbs");
|
|
17
|
+
|
|
18
|
+
function winTaskCommand(command) {
|
|
19
|
+
// schtasks runs /tr through cmd.exe already; keep it a single quoted string, inner quotes escaped.
|
|
20
|
+
return `cmd /c "${command.replace(/"/g, '\\"')}"`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Runs hidden (no console flash) at login - no special privilege needed, just write access to the user's own
|
|
24
|
+
// Startup folder, which (unlike Task Scheduler) corporate Group Policy essentially never locks down. Wrapped in
|
|
25
|
+
// cmd /c like the Task Scheduler path (not run as a bare program) since `command` may use shell syntax (the "||
|
|
26
|
+
// fallback to npx" from startHookCommand), which WshShell.Run can't interpret on its own.
|
|
27
|
+
function startupVbs(command) {
|
|
28
|
+
const esc = winTaskCommand(command).replace(/"/g, '""');
|
|
29
|
+
return `Set WshShell = CreateObject("WScript.Shell")\r\nWshShell.Run "${esc}", 0, False\r\n`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const xml = (v) => String(v).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
33
|
+
|
|
34
|
+
export const launchdLogPath = (home = os.homedir()) => path.join(home, "Library", "Logs", "agentglow.log");
|
|
35
|
+
|
|
36
|
+
/** PATH for login-time jobs: launchd/systemd start with a bare PATH, so add where uv/uvx/node usually live. */
|
|
37
|
+
export function loginPath({ home = os.homedir(), node = process.execPath, base = process.env.PATH || "" } = {}) {
|
|
38
|
+
const dirs = [path.dirname(node), path.join(home, ".local", "bin"), path.join(home, ".cargo", "bin"),
|
|
39
|
+
"/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin", ...base.split(path.delimiter)];
|
|
40
|
+
return [...new Set(dirs.filter(Boolean))].join(path.delimiter);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// The server runs in the FOREGROUND under launchd/systemd (not `start --background`): a job whose process exits
|
|
44
|
+
// right away gets its spawned children reaped, and a supervised foreground process can be restarted on crash.
|
|
45
|
+
// `start` exits 0 when a server is already up on the port, so KeepAlive {SuccessfulExit: false} never loops.
|
|
46
|
+
export function launchdPlist({ argv, env = {}, home = os.homedir() }) {
|
|
47
|
+
const args = argv.map((a) => `<string>${xml(a)}</string>`).join("");
|
|
48
|
+
const envs = Object.entries(env).map(([k, v]) => `<key>${xml(k)}</key><string>${xml(v)}</string>`).join("");
|
|
49
|
+
const log = xml(launchdLogPath(home));
|
|
50
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
51
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
52
|
+
<plist version="1.0"><dict>
|
|
53
|
+
<key>Label</key><string>${LABEL}</string>
|
|
54
|
+
<key>ProgramArguments</key><array>${args}</array>
|
|
55
|
+
<key>EnvironmentVariables</key><dict>${envs}</dict>
|
|
56
|
+
<key>RunAtLoad</key><true/>
|
|
57
|
+
<key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
|
|
58
|
+
<key>ThrottleInterval</key><integer>10</integer>
|
|
59
|
+
<key>ProcessType</key><string>Background</string>
|
|
60
|
+
<key>StandardOutPath</key><string>${log}</string>
|
|
61
|
+
<key>StandardErrorPath</key><string>${log}</string>
|
|
62
|
+
</dict></plist>
|
|
63
|
+
`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function systemdUnit({ argv, env = {} }) {
|
|
67
|
+
const q = (a) => (/[\s"\\]/.test(a) ? JSON.stringify(a) : a);
|
|
68
|
+
const envLines = Object.entries(env).map(([k, v]) => `Environment=${q(`${k}=${v}`)}`).join("\n");
|
|
69
|
+
return `[Unit]
|
|
70
|
+
Description=AgentGlow server
|
|
71
|
+
|
|
72
|
+
[Service]
|
|
73
|
+
ExecStart=${argv.map(q).join(" ")}
|
|
74
|
+
${envLines}
|
|
75
|
+
Restart=on-failure
|
|
76
|
+
RestartSec=10
|
|
77
|
+
|
|
78
|
+
[Install]
|
|
79
|
+
WantedBy=default.target
|
|
80
|
+
`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Install an OS-level "run this command at login" entry. Returns { method, path? }. Throws on failure. */
|
|
84
|
+
export function installAutostart({ command, argv, env = {}, platform = process.platform, home = os.homedir() } = {}) {
|
|
85
|
+
if (platform === "win32") {
|
|
86
|
+
try {
|
|
87
|
+
execFileSync("schtasks", ["/create", "/tn", TASK_NAME, "/tr", winTaskCommand(command), "/sc", "onlogon", "/rl", "limited", "/f"],
|
|
88
|
+
{ stdio: "pipe" });
|
|
89
|
+
return { method: "Task Scheduler", name: TASK_NAME };
|
|
90
|
+
} catch (e) {
|
|
91
|
+
// Task Scheduler creation is commonly blocked by corporate Group Policy even for a user's own tasks;
|
|
92
|
+
// fall back to a Startup-folder launcher, which only needs normal write access to the user's own profile.
|
|
93
|
+
const file = startupScriptPath(home);
|
|
94
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
95
|
+
fs.writeFileSync(file, startupVbs(command));
|
|
96
|
+
return { method: "Startup folder (Task Scheduler was blocked)", path: file };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (platform === "darwin") {
|
|
100
|
+
const file = plistPath(home);
|
|
101
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
102
|
+
fs.mkdirSync(path.dirname(launchdLogPath(home)), { recursive: true });
|
|
103
|
+
fs.writeFileSync(file, launchdPlist({ argv, env, home }));
|
|
104
|
+
try { execFileSync("launchctl", ["unload", file], { stdio: "pipe" }); } catch { /* wasn't loaded */ }
|
|
105
|
+
execFileSync("launchctl", ["load", "-w", file], { stdio: "pipe" }); // RunAtLoad: starts it right now too
|
|
106
|
+
return { method: "launchd", path: file };
|
|
107
|
+
}
|
|
108
|
+
// linux and other unix: systemd --user
|
|
109
|
+
const file = systemdUnitPath(home);
|
|
110
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
111
|
+
fs.writeFileSync(file, systemdUnit({ argv, env }));
|
|
112
|
+
execFileSync("systemctl", ["--user", "daemon-reload"], { stdio: "pipe" });
|
|
113
|
+
execFileSync("systemctl", ["--user", "enable", "--now", "agentglow"], { stdio: "pipe" });
|
|
114
|
+
return { method: "systemd --user", path: file };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Undo installAutostart(). Best-effort: never throws, so `remove` always completes. */
|
|
118
|
+
export function removeAutostart({ platform = process.platform, home = os.homedir() } = {}) {
|
|
119
|
+
if (platform === "win32") {
|
|
120
|
+
try { execFileSync("schtasks", ["/delete", "/tn", TASK_NAME, "/f"], { stdio: "pipe" }); } catch { /* not installed */ }
|
|
121
|
+
try { fs.rmSync(startupScriptPath(home), { force: true }); } catch { /* ignore */ }
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (platform === "darwin") {
|
|
125
|
+
const file = plistPath(home);
|
|
126
|
+
try { execFileSync("launchctl", ["unload", file], { stdio: "pipe" }); } catch { /* wasn't loaded */ }
|
|
127
|
+
try { fs.rmSync(file, { force: true }); } catch { /* ignore */ }
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
try { execFileSync("systemctl", ["--user", "disable", "--now", "agentglow"], { stdio: "pipe" }); } catch { /* not installed */ }
|
|
131
|
+
try { fs.rmSync(systemdUnitPath(home), { force: true }); } catch { /* ignore */ }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Stop the login-item server for now (it starts again at next login / next claude session). Never throws. */
|
|
135
|
+
export function stopLoginItem({ platform = process.platform } = {}) {
|
|
136
|
+
try {
|
|
137
|
+
if (platform === "darwin") execFileSync("launchctl", ["stop", LABEL], { stdio: "pipe" });
|
|
138
|
+
else if (platform === "win32") execFileSync("schtasks", ["/end", "/tn", TASK_NAME], { stdio: "pipe" });
|
|
139
|
+
else execFileSync("systemctl", ["--user", "stop", "agentglow"], { stdio: "pipe" });
|
|
140
|
+
return true;
|
|
141
|
+
} catch {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Best-effort check: is the autostart entry currently installed? Never throws. */
|
|
147
|
+
export function hasAutostart({ platform = process.platform, home = os.homedir() } = {}) {
|
|
148
|
+
try {
|
|
149
|
+
if (platform === "win32") {
|
|
150
|
+
if (fs.existsSync(startupScriptPath(home))) return true;
|
|
151
|
+
execFileSync("schtasks", ["/query", "/tn", TASK_NAME], { stdio: "pipe" });
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
if (platform === "darwin") return fs.existsSync(plistPath(home));
|
|
155
|
+
return fs.existsSync(systemdUnitPath(home));
|
|
156
|
+
} catch {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
package/cli/lib/server.mjs
CHANGED
|
@@ -20,7 +20,10 @@ export function pySpecs(version, env = process.env) {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export function serveArgv(runner, spec, port) {
|
|
23
|
-
|
|
23
|
+
// a local checkout (AGENTGLOW_PY_SPEC=/path/to/backend): run it editable, uvx otherwise serves a stale cached build
|
|
24
|
+
const local = /^(\/|\.|~|[A-Za-z]:[\\/])/.test(spec);
|
|
25
|
+
return [...runner.args, ...(local ? ["--with-editable", spec] : []), "--from", spec,
|
|
26
|
+
"agentglow", "serve", "--host", "127.0.0.1", "--port", String(port)];
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
export function alive(pid) {
|
|
@@ -164,21 +167,25 @@ export function removeCliCopies() {
|
|
|
164
167
|
export async function serveForeground({ port, version, log = console.error }) {
|
|
165
168
|
const runner = await findRunner({ log });
|
|
166
169
|
const specs = pySpecs(version);
|
|
170
|
+
let stopping = false;
|
|
167
171
|
for (const [i, spec] of specs.entries()) {
|
|
168
172
|
const t0 = Date.now();
|
|
169
173
|
const code = await new Promise((resolve) => {
|
|
170
174
|
const child = spawn(runner.cmd, serveArgv(runner, spec, port), { stdio: "inherit" });
|
|
171
|
-
|
|
175
|
+
// A requested stop (Ctrl-C, `launchctl stop`, `systemctl stop`) must exit 0, or a supervisor with
|
|
176
|
+
// "restart unless it exited successfully" (launchd KeepAlive/SuccessfulExit) would bring it right back.
|
|
177
|
+
const fwd = (sig) => () => { stopping = true; child.kill(sig); };
|
|
172
178
|
const onInt = fwd("SIGINT"), onTerm = fwd("SIGTERM");
|
|
173
179
|
process.on("SIGINT", onInt);
|
|
174
180
|
process.on("SIGTERM", onTerm);
|
|
175
181
|
child.on("exit", (c, sig) => {
|
|
176
182
|
process.off("SIGINT", onInt);
|
|
177
183
|
process.off("SIGTERM", onTerm);
|
|
178
|
-
resolve(c ?? (sig ? 130 : 1));
|
|
184
|
+
resolve(stopping ? 0 : (c ?? (sig ? 130 : 1)));
|
|
179
185
|
});
|
|
180
186
|
});
|
|
181
187
|
// a pinned version missing on PyPI fails fast; fall back to latest once
|
|
188
|
+
if (stopping) return 0;
|
|
182
189
|
if (code !== 0 && i < specs.length - 1 && Date.now() - t0 < 30000) {
|
|
183
190
|
log(`${spec} failed, retrying with the latest agentglow ...`);
|
|
184
191
|
continue;
|