@songsid/agend 2.0.8-beta.16 → 2.0.8-beta.18

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/dist/cli.js CHANGED
@@ -131,12 +131,40 @@ fleet
131
131
  .argument("[instance]", "Specific instance to stop")
132
132
  .action(async (instance) => {
133
133
  if (instance) {
134
- const { FleetManager } = await import("./fleet-manager.js");
135
- const fm = new FleetManager(DATA_DIR);
136
- await fm.stopInstance(instance);
137
- console.log("Stopped");
134
+ // Stop a single instance via the RUNNING fleet's HTTP API. Do NOT spawn a
135
+ // detached FleetManager and call stopInstance(): its in-memory daemons map
136
+ // is empty, so lifecycle.stop falls back to reading the instance's
137
+ // daemon.pid — which is the shared fleet process pid (daemons run
138
+ // in-process) — and SIGTERMs the whole fleet.
139
+ const { loadFleetConfig } = await import("./config.js");
140
+ const fleet = loadFleetConfig(FLEET_CONFIG_PATH);
141
+ const port = fleet.health_port ?? 19280;
142
+ let token = "";
143
+ try {
144
+ token = readFileSync(join(DATA_DIR, "web.token"), "utf-8").trim();
145
+ }
146
+ catch { /* fleet may not be running */ }
147
+ try {
148
+ const resp = await fetch(`http://127.0.0.1:${port}/stop/${encodeURIComponent(instance)}`, {
149
+ method: "POST",
150
+ headers: token ? { "X-Agend-Token": token } : {},
151
+ });
152
+ const body = await resp.json().catch(() => ({}));
153
+ if (resp.ok) {
154
+ console.log(`Instance "${instance}" stopped`);
155
+ }
156
+ else {
157
+ console.error(`Stop failed: ${body.error ?? resp.statusText}`);
158
+ process.exit(1);
159
+ }
160
+ }
161
+ catch {
162
+ console.error(`Cannot connect to fleet (port ${port}). Is the fleet running?`);
163
+ process.exit(1);
164
+ }
165
+ return;
138
166
  }
139
- else {
167
+ {
140
168
  const pidPath = join(DATA_DIR, "fleet.pid");
141
169
  if (!existsSync(pidPath)) {
142
170
  console.error("Fleet is not running (no PID file found)");
@@ -185,8 +213,16 @@ fleet
185
213
  const { loadFleetConfig } = await import("./config.js");
186
214
  const fleet = loadFleetConfig(FLEET_CONFIG_PATH);
187
215
  const port = fleet.health_port ?? 19280;
216
+ let token = "";
188
217
  try {
189
- const resp = await fetch(`http://127.0.0.1:${port}/restart/${encodeURIComponent(instance)}`, { method: "POST" });
218
+ token = readFileSync(join(DATA_DIR, "web.token"), "utf-8").trim();
219
+ }
220
+ catch { /* fleet may not be running */ }
221
+ try {
222
+ const resp = await fetch(`http://127.0.0.1:${port}/restart/${encodeURIComponent(instance)}`, {
223
+ method: "POST",
224
+ headers: token ? { "X-Agend-Token": token } : {},
225
+ });
190
226
  const body = await resp.json();
191
227
  if (resp.ok) {
192
228
  console.log(`Instance "${instance}" restarted (immediate)`);
@@ -1156,69 +1192,111 @@ program
1156
1192
  }
1157
1193
  }
1158
1194
  // ── Restart fleet ──
1195
+ // Try each runtime environment in order and stop at the first that succeeds.
1196
+ // We no longer gate on fleet.pid existence: a fleet running under systemd or
1197
+ // launchd writes its pid in the service's own HOME, which may differ from the
1198
+ // path this command resolves (sudo, AGEND_HOME, etc.) — so an absent pid file
1199
+ // does NOT mean the fleet is stopped. Ask the service manager first.
1159
1200
  const pidPath = join(DATA_DIR, "fleet.pid");
1160
- if (!existsSync(pidPath)) {
1161
- console.log("\n Fleet not running. Start with: agend start\n");
1162
- return;
1163
- }
1164
- // Kill old fleet process first to prevent duplicate
1165
- const oldPid = parseInt(readFileSync(pidPath, "utf-8").trim(), 10);
1166
- if (oldPid) {
1201
+ console.log(" Restarting fleet...");
1202
+ const isActive = (cmd) => {
1167
1203
  try {
1168
- process.kill(oldPid, "SIGTERM");
1204
+ return spawnSync("sh", ["-c", cmd], { encoding: "utf-8", timeout: 5000 }).stdout?.trim() === "active";
1169
1205
  }
1170
- catch { /* already gone */ }
1171
- // Wait up to 10s for old process to exit
1172
- for (let i = 0; i < 20; i++) {
1173
- try {
1174
- process.kill(oldPid, 0);
1175
- }
1176
- catch {
1177
- break;
1178
- }
1179
- spawnSync("sleep", ["0.5"]);
1206
+ catch {
1207
+ return false;
1180
1208
  }
1181
- // Force kill if still alive
1209
+ };
1210
+ const run = (cmd) => {
1182
1211
  try {
1183
- process.kill(oldPid, "SIGKILL");
1212
+ execSync(cmd, { stdio: "inherit", timeout: 15000 });
1213
+ return true;
1214
+ }
1215
+ catch {
1216
+ return false;
1184
1217
  }
1185
- catch { /* already gone */ }
1218
+ };
1219
+ // 1. System-level systemd service "agend"
1220
+ if (plat !== "macos" && isActive("systemctl is-active agend 2>/dev/null")) {
1221
+ run("systemctl daemon-reload");
1222
+ if (run("systemctl restart agend")) {
1223
+ console.log(" ✓ Service restarted (system)\n");
1224
+ return;
1225
+ }
1226
+ }
1227
+ // 2. User-level systemd service "com.agend.fleet"
1228
+ if (plat !== "macos" && isActive("systemctl --user is-active com.agend.fleet 2>/dev/null")) {
1229
+ run("systemctl --user daemon-reload");
1186
1230
  try {
1187
- unlinkSync(pidPath);
1231
+ execSync("systemctl --user reset-failed com.agend.fleet", { stdio: "pipe", timeout: 5000 });
1188
1232
  }
1189
1233
  catch { /* best effort */ }
1234
+ if (run("systemctl --user restart com.agend.fleet")) {
1235
+ console.log(" ✓ Service restarted (user)\n");
1236
+ return;
1237
+ }
1190
1238
  }
1191
- console.log(" Restarting fleet...");
1239
+ // 3. launchd (macOS)
1192
1240
  if (plat === "macos") {
1193
1241
  const uid = process.getuid?.() ?? 501;
1194
1242
  const label = "com.agend.fleet";
1195
1243
  try {
1196
- execSync(`launchctl kickstart -k gui/${uid}/${label}`, { stdio: "inherit", timeout: 15000 });
1197
- console.log(" Service restarted\n");
1198
- return;
1244
+ // print succeeds only if the service is loaded in launchd
1245
+ execSync(`launchctl print gui/${uid}/${label}`, { stdio: "pipe", timeout: 5000 });
1246
+ if (run(`launchctl kickstart -k gui/${uid}/${label}`)) {
1247
+ console.log(" ✓ Service restarted\n");
1248
+ return;
1249
+ }
1199
1250
  }
1200
- catch { /* fall through to manual restart */ }
1251
+ catch { /* not loaded fall through */ }
1201
1252
  }
1202
- else {
1203
- try {
1204
- execSync("systemctl --user daemon-reload", { stdio: "pipe", timeout: 5000 });
1205
- // Reset failed state in case the kill above left systemd confused
1253
+ // 4. Detached process tracked by fleet.pid (no service manager)
1254
+ if (existsSync(pidPath)) {
1255
+ const oldPid = parseInt(readFileSync(pidPath, "utf-8").trim(), 10);
1256
+ let alive = false;
1257
+ if (oldPid) {
1206
1258
  try {
1207
- execSync("systemctl --user reset-failed com.agend.fleet", { stdio: "pipe", timeout: 5000 });
1259
+ process.kill(oldPid, 0);
1260
+ alive = true;
1208
1261
  }
1209
- catch { }
1210
- execSync("systemctl --user start com.agend.fleet", { stdio: "inherit", timeout: 15000 });
1211
- console.log(" ✓ Service restarted\n");
1262
+ catch { /* dead or not ours */ }
1263
+ }
1264
+ if (alive) {
1265
+ try {
1266
+ process.kill(oldPid, "SIGTERM");
1267
+ }
1268
+ catch { /* already gone */ }
1269
+ // Wait up to 10s for the old process to exit
1270
+ for (let i = 0; i < 20; i++) {
1271
+ try {
1272
+ process.kill(oldPid, 0);
1273
+ }
1274
+ catch {
1275
+ break;
1276
+ }
1277
+ spawnSync("sleep", ["0.5"]);
1278
+ }
1279
+ try {
1280
+ process.kill(oldPid, "SIGKILL");
1281
+ }
1282
+ catch { /* already gone */ }
1283
+ try {
1284
+ unlinkSync(pidPath);
1285
+ }
1286
+ catch { /* best effort */ }
1287
+ const child = spawnAsync(agendPath, ["fleet", "start"], { detached: true, stdio: "ignore" });
1288
+ child.unref();
1289
+ console.log(" ✓ Fleet restarting in background\n");
1212
1290
  return;
1213
1291
  }
1214
- catch { /* fall through */ }
1292
+ // Stale pid file (process gone) — clean it up before declaring stopped.
1293
+ try {
1294
+ unlinkSync(pidPath);
1295
+ }
1296
+ catch { /* best effort */ }
1215
1297
  }
1216
- // Fallback: start directly using new binary
1217
- const child = spawnAsync(agendPath, ["fleet", "start"], {
1218
- detached: true, stdio: "ignore",
1219
- });
1220
- child.unref();
1221
- console.log(" ✓ Fleet restarting in background\n");
1298
+ // 5. Nothing running anywhere
1299
+ console.log("\n Fleet not running. Start with: agend start\n");
1222
1300
  });
1223
1301
  program
1224
1302
  .command("reload")