@songsid/agend 2.1.2-beta.39 → 2.1.2-beta.40

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
@@ -17,6 +17,7 @@ import { PIE_PERCENT_RE } from "./tui-glyphs.js";
17
17
  import { COMPLETION_SHELLS, completionScript } from "./completion.js";
18
18
  import { getUpdateSelector, lookupTargetVersion, reportUpdateRestart, shouldSkipUpdate, } from "./update-check.js";
19
19
  import { clearUpdateMarker, markUpdateInProgress } from "./update-marker.js";
20
+ import { acquireFleetLock, releaseProcessFleetLock, setProcessFleetLock } from "./fleet-lock.js";
20
21
  /** Prefix tmux args with -L when socket isolation is active. */
21
22
  function tmuxArgs(args) {
22
23
  const socket = getTmuxSocketName();
@@ -27,6 +28,18 @@ const __dirname = dirname(__filename);
27
28
  const DATA_DIR = getAgendHome();
28
29
  const FLEET_CONFIG_PATH = join(DATA_DIR, "fleet.yaml");
29
30
  const program = new Command();
31
+ function claimFleetSingleton() {
32
+ try {
33
+ setProcessFleetLock(acquireFleetLock(DATA_DIR));
34
+ process.once("exit", () => { releaseProcessFleetLock(); });
35
+ return true;
36
+ }
37
+ catch (err) {
38
+ console.error(err.message);
39
+ process.exitCode = 1;
40
+ return false;
41
+ }
42
+ }
30
43
  // Read version from package.json at build time
31
44
  const pkgVersion = (() => {
32
45
  try {
@@ -98,6 +111,10 @@ fleet
98
111
  }
99
112
  catch { /* fleet not running, fall through */ }
100
113
  }
114
+ // Claim the singleton before constructing FleetManager: its constructor and
115
+ // startAll path touch shared tmux, sockets, state files and adapters.
116
+ if (!claimFleetSingleton())
117
+ return;
101
118
  const { FleetManager } = await import("./fleet-manager.js");
102
119
  const fm = new FleetManager(DATA_DIR);
103
120
  // Register crash/signal handlers BEFORE startAll(). Startup routinely takes
@@ -356,6 +373,8 @@ fleet
356
373
  }
357
374
  console.log("Old fleet stopped. Starting with new code...");
358
375
  // Start new fleet in this process (new Node.js process = new code loaded)
376
+ if (!claimFleetSingleton())
377
+ return;
359
378
  const { FleetManager } = await import("./fleet-manager.js");
360
379
  const fm = new FleetManager(DATA_DIR);
361
380
  await fm.startAll(FLEET_CONFIG_PATH);
@@ -1379,8 +1398,10 @@ program
1379
1398
  .command("start")
1380
1399
  .description("Start the AgEnD service (must be installed first)")
1381
1400
  .action(async () => {
1382
- const { getServicePath, startService } = await import("./service-installer.js");
1383
- if (!getServicePath()) {
1401
+ const { getServicePath, getSystemServicePath, startService, startSystemService } = await import("./service-installer.js");
1402
+ const userServicePath = getServicePath();
1403
+ const systemServicePath = getSystemServicePath();
1404
+ if (!userServicePath && !systemServicePath) {
1384
1405
  console.log("No service installed. Starting fleet directly...");
1385
1406
  const { spawn } = await import("node:child_process");
1386
1407
  const child = spawn("sh", ["-c", "agend fleet start"], { detached: true, stdio: "ignore" });
@@ -1388,16 +1409,16 @@ program
1388
1409
  console.log("Fleet starting in background.");
1389
1410
  return;
1390
1411
  }
1391
- if (startService()) {
1412
+ const started = systemServicePath ? startSystemService() : startService();
1413
+ if (started) {
1392
1414
  console.log("Service started.");
1393
1415
  }
1394
1416
  else {
1395
- // systemd/launchd failed fallback to direct start
1396
- console.log("Service manager unavailable. Starting fleet directly...");
1397
- const { spawn } = await import("node:child_process");
1398
- const child = spawn("sh", ["-c", "agend fleet start"], { detached: true, stdio: "ignore" });
1399
- child.unref();
1400
- console.log("Fleet starting in background.");
1417
+ // A service file means the service manager owns this fleet. Falling back
1418
+ // to a detached process here creates a second fleet when the bus recovers.
1419
+ console.error("Failed to start the installed AgEnD service; refusing to start a detached duplicate.");
1420
+ console.error("Check your service manager, then retry: systemctl --user status com.agend.fleet");
1421
+ process.exitCode = 1;
1401
1422
  }
1402
1423
  });
1403
1424
  program
@@ -1409,18 +1430,10 @@ program
1409
1430
  // service's own HOME, which may differ from what this command resolves (sudo,
1410
1431
  // AGEND_HOME) — an absent pid file does NOT mean it's stopped. Ask the service
1411
1432
  // manager first. This is the canonical restart the `update` flow spawns.
1412
- const { detectPlatform } = await import("./service-installer.js");
1433
+ const { detectPlatform, getServicePath, getSystemServicePath, getSystemdServiceState } = await import("./service-installer.js");
1413
1434
  const { spawn, spawnSync } = await import("node:child_process");
1414
1435
  const plat = detectPlatform();
1415
1436
  const pidPath = join(DATA_DIR, "fleet.pid");
1416
- const isActive = (cmd) => {
1417
- try {
1418
- return spawnSync("sh", ["-c", cmd], { encoding: "utf-8", timeout: 5000 }).stdout?.trim() === "active";
1419
- }
1420
- catch {
1421
- return false;
1422
- }
1423
- };
1424
1437
  const run = (cmd) => {
1425
1438
  try {
1426
1439
  execSync(cmd, { stdio: "inherit", timeout: 15000 });
@@ -1431,7 +1444,15 @@ program
1431
1444
  }
1432
1445
  };
1433
1446
  // 1. System-level systemd service "agend"
1434
- if (plat !== "macos" && isActive("systemctl is-active agend 2>/dev/null")) {
1447
+ const systemServiceInstalled = plat !== "macos" && getSystemServicePath() !== null;
1448
+ const systemState = plat !== "macos" ? getSystemdServiceState("agend", false) : "stopped";
1449
+ if (systemServiceInstalled && systemState === "unavailable") {
1450
+ console.error("Cannot reach the system service manager; refusing a detached restart that could duplicate the fleet.");
1451
+ console.error("Check: systemctl status agend");
1452
+ process.exitCode = 1;
1453
+ return;
1454
+ }
1455
+ if (systemServiceInstalled && (systemState === "running" || systemState === "stopped")) {
1435
1456
  run("systemctl daemon-reload");
1436
1457
  if (run("systemctl restart agend")) {
1437
1458
  console.log("Service restarted (system).");
@@ -1443,7 +1464,17 @@ program
1443
1464
  return;
1444
1465
  }
1445
1466
  // 2. User-level systemd service "com.agend.fleet"
1446
- if (plat !== "macos" && isActive("systemctl --user is-active com.agend.fleet 2>/dev/null")) {
1467
+ const userServiceInstalled = plat !== "macos" && getServicePath() !== null;
1468
+ const userState = plat !== "macos"
1469
+ ? getSystemdServiceState("com.agend.fleet", true)
1470
+ : "stopped";
1471
+ if (userServiceInstalled && userState === "unavailable") {
1472
+ console.error("Cannot reach the user service manager; refusing a detached restart that could duplicate the fleet.");
1473
+ console.error("Check: systemctl --user status com.agend.fleet");
1474
+ process.exitCode = 1;
1475
+ return;
1476
+ }
1477
+ if (userServiceInstalled && (userState === "running" || userState === "stopped")) {
1447
1478
  run("systemctl --user daemon-reload");
1448
1479
  try {
1449
1480
  execSync("systemctl --user reset-failed com.agend.fleet", { stdio: "pipe", timeout: 5000 });