@songsid/agend 2.2.0-alpha.1 → 2.2.0-alpha.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/README.md +2 -0
- package/README.zh-TW.md +2 -0
- package/dist/agent-cli.js +6 -1
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.js +68 -12
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-token-header.d.ts +13 -0
- package/dist/agent-token-header.js +16 -0
- package/dist/agent-token-header.js.map +1 -0
- package/dist/backend/antigravity.js +4 -3
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/grok.js +1 -1
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/kiro.d.ts +7 -0
- package/dist/backend/kiro.js +82 -33
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/muse.js +1 -1
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +6 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/cache-analysis.d.ts +92 -0
- package/dist/cache-analysis.js +147 -0
- package/dist/cache-analysis.js.map +1 -0
- package/dist/cache-ledger.d.ts +108 -0
- package/dist/cache-ledger.js +230 -0
- package/dist/cache-ledger.js.map +1 -0
- package/dist/cache-prices.d.ts +32 -0
- package/dist/cache-prices.js +73 -0
- package/dist/cache-prices.js.map +1 -0
- package/dist/cache-scan.d.ts +39 -0
- package/dist/cache-scan.js +115 -0
- package/dist/cache-scan.js.map +1 -0
- package/dist/cache-service.d.ts +115 -0
- package/dist/cache-service.js +417 -0
- package/dist/cache-service.js.map +1 -0
- package/dist/cli-entry.d.ts +19 -0
- package/dist/cli-entry.js +36 -0
- package/dist/cli-entry.js.map +1 -0
- package/dist/cli.js +251 -24
- package/dist/cli.js.map +1 -1
- package/dist/daemon.d.ts +6 -0
- package/dist/daemon.js +90 -37
- package/dist/daemon.js.map +1 -1
- package/dist/event-log.d.ts +4 -1
- package/dist/event-log.js +6 -3
- package/dist/event-log.js.map +1 -1
- package/dist/fleet-control-audit.d.ts +5 -0
- package/dist/fleet-control-audit.js +7 -0
- package/dist/fleet-control-audit.js.map +1 -1
- package/dist/fleet-level-config.js +22 -0
- package/dist/fleet-level-config.js.map +1 -1
- package/dist/fleet-manager.d.ts +46 -0
- package/dist/fleet-manager.js +287 -40
- package/dist/fleet-manager.js.map +1 -1
- package/dist/full-restart.js +2 -3
- package/dist/full-restart.js.map +1 -1
- package/dist/general-knowledge/skills/persona-emoji/SKILL.md +3 -1
- package/dist/install-lock.d.ts +49 -0
- package/dist/install-lock.js +200 -0
- package/dist/install-lock.js.map +1 -0
- package/dist/instance-config-impact.js +3 -2
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/locale.js +14 -0
- package/dist/locale.js.map +1 -1
- package/dist/outbound-schemas.js +1 -1
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/package-preimage.d.ts +26 -0
- package/dist/package-preimage.js +96 -0
- package/dist/package-preimage.js.map +1 -0
- package/dist/public-web-gateway.js +2 -2
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/quickstart.js +10 -4
- package/dist/quickstart.js.map +1 -1
- package/dist/restart-guard.d.ts +52 -0
- package/dist/restart-guard.js +109 -0
- package/dist/restart-guard.js.map +1 -0
- package/dist/scheduler/db.js +6 -2
- package/dist/scheduler/db.js.map +1 -1
- package/dist/service-activation.d.ts +47 -5
- package/dist/service-activation.js +156 -51
- package/dist/service-activation.js.map +1 -1
- package/dist/service-installer.d.ts +56 -1
- package/dist/service-installer.js +134 -12
- package/dist/service-installer.js.map +1 -1
- package/dist/service-plan.d.ts +65 -0
- package/dist/service-plan.js +108 -0
- package/dist/service-plan.js.map +1 -0
- package/dist/settings-api.js +46 -5
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +34 -6
- package/dist/settings-change.js.map +1 -1
- package/dist/setup-host.js +2 -3
- package/dist/setup-host.js.map +1 -1
- package/dist/setup-wizard.js +2 -1
- package/dist/setup-wizard.js.map +1 -1
- package/dist/sqlite-open-errors.d.ts +12 -0
- package/dist/sqlite-open-errors.js +13 -0
- package/dist/sqlite-open-errors.js.map +1 -0
- package/dist/tmux-control.d.ts +22 -0
- package/dist/tmux-control.js +89 -5
- package/dist/tmux-control.js.map +1 -1
- package/dist/topic-commands.js +14 -5
- package/dist/topic-commands.js.map +1 -1
- package/dist/ui/chat-commands.js +43 -0
- package/dist/ui/chat-render.js +2 -2
- package/dist/ui/chat-store.js +30 -1
- package/dist/ui/chat-strings.js +22 -2
- package/dist/ui/chat-thread.js +47 -10
- package/dist/ui/fleet-cache.js +168 -0
- package/dist/ui/panel-chat.js +104 -18
- package/dist/ui/panel-fleet.js +26 -12
- package/dist/ui/panel-settings.js +12 -8
- package/dist/ui/preview-panel.js +202 -0
- package/dist/ui/preview.js +7 -3
- package/dist/ui/settings-strings.js +4 -2
- package/dist/ui/shared/app-route.js +1 -1
- package/dist/ui/shared/app-shell.js +6 -4
- package/dist/ui/shared/app.css +147 -3
- package/dist/ui/shared/signin.js +13 -1
- package/dist/ui/shared/ui-icons.js +2 -0
- package/dist/ui/signin.html +2 -1
- package/dist/update-dispatch.d.ts +28 -0
- package/dist/update-dispatch.js +73 -0
- package/dist/update-dispatch.js.map +1 -0
- package/dist/update-install.d.ts +65 -7
- package/dist/update-install.js +126 -26
- package/dist/update-install.js.map +1 -1
- package/dist/web-api.d.ts +14 -0
- package/dist/web-api.js +65 -0
- package/dist/web-api.js.map +1 -1
- package/dist/web-commands.d.ts +77 -0
- package/dist/web-commands.js +81 -0
- package/dist/web-commands.js.map +1 -0
- package/dist/web-shell-routes.d.ts +1 -1
- package/dist/web-shell-routes.js +1 -1
- package/dist/web-shell-routes.js.map +1 -1
- package/launcher/agend +110 -0
- package/launcher/agend-agent +110 -0
- package/launcher/agend-agent.cjs +4 -0
- package/launcher/agend.cjs +4 -0
- package/launcher/install-admission.cjs +105 -0
- package/launcher/launch.cjs +64 -0
- package/launcher/postinstall.cjs +114 -0
- package/launcher/run-in-process.cjs +13 -0
- package/launcher/runtime-platform.cjs +86 -0
- package/launcher/runtime-select.cjs +217 -0
- package/package.json +12 -4
- package/scripts/preinstall-guard.cjs +40 -21
- package/templates/launchd.plist.ejs +2 -2
- package/templates/systemd.service.ejs +1 -1
package/dist/cli.js
CHANGED
|
@@ -19,13 +19,13 @@ import { Cron } from "croner";
|
|
|
19
19
|
import { existsSync, readFileSync, writeFileSync, appendFileSync, unlinkSync, mkdirSync, chmodSync, readdirSync, rmSync, statSync, realpathSync, } from "node:fs";
|
|
20
20
|
import { homedir, totalmem, freemem, userInfo } from "node:os";
|
|
21
21
|
import { fileURLToPath } from "node:url";
|
|
22
|
-
import { spawn, execSync, execFileSync } from "node:child_process";
|
|
22
|
+
import { spawn, spawnSync, execSync, execFileSync } from "node:child_process";
|
|
23
23
|
import { getAgendHome, getTmuxSocketName } from "./paths.js";
|
|
24
24
|
import { hasPausedMarker } from "./pause-marker.js";
|
|
25
25
|
import { COMPLETION_SHELLS, completionScript } from "./completion.js";
|
|
26
26
|
import { getUpdateSelector, isExactVersion, isUnrequestedDowngrade, lookupTargetVersion, reportUpdateRestart, shouldSkipUpdate, runningFleetPredatesInstall, processStartMs, } from "./update-check.js";
|
|
27
27
|
import { clearUpdateMarker, markUpdateInProgress, setUpdateProgressStage } from "./update-marker.js";
|
|
28
|
-
import { describeSignalSource, gateFleetControl, recordInstanceControl, withOrigin } from "./fleet-control-audit.js";
|
|
28
|
+
import { describeSignalSource, forceAllowed, gateFleetControl, recordInstanceControl, withOrigin } from "./fleet-control-audit.js";
|
|
29
29
|
import { acquireFleetLock, isFleetStartCommandLine, readProcessCommandLine, releaseProcessFleetLock, setProcessFleetLock } from "./fleet-lock.js";
|
|
30
30
|
import { limitFleetCoreDumps } from "./coredump-filter.js";
|
|
31
31
|
import { SYSTEMD_RESTART_TIMEOUT_MS } from "./service-installer.js";
|
|
@@ -36,6 +36,7 @@ import { DeliveryOutbox, deliveryStatusSelector } from "./delivery-outbox.js";
|
|
|
36
36
|
import { DeliveryStatusArgs } from "./outbound-schemas.js";
|
|
37
37
|
import { requestCpuProfile } from "./profile-control.js";
|
|
38
38
|
import { requestSettingsConfirmation } from "./settings-control.js";
|
|
39
|
+
import { canonicalCliEntry, selfCommand } from "./cli-entry.js";
|
|
39
40
|
/** Prefix tmux args with -L when socket isolation is active. */
|
|
40
41
|
function tmuxArgs(args) {
|
|
41
42
|
const socket = getTmuxSocketName();
|
|
@@ -44,6 +45,39 @@ function tmuxArgs(args) {
|
|
|
44
45
|
const __filename = fileURLToPath(import.meta.url);
|
|
45
46
|
const __dirname = dirname(__filename);
|
|
46
47
|
const DATA_DIR = getAgendHome();
|
|
48
|
+
/** Real-filesystem, real-process deps for the launchd plan (service-plan.ts) and its activation. */
|
|
49
|
+
function planDeps() {
|
|
50
|
+
return {
|
|
51
|
+
run: (command, args) => {
|
|
52
|
+
const r = spawnSync(command, args, { encoding: "utf-8", timeout: 15_000, stdio: ["ignore", "pipe", "pipe"] });
|
|
53
|
+
return { status: r.status, signal: r.signal, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
54
|
+
},
|
|
55
|
+
readFile: (path) => { try {
|
|
56
|
+
return readFileSync(path, "utf-8");
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
} },
|
|
61
|
+
writeFile: (path, content) => writeFileSync(path, content),
|
|
62
|
+
removeFile: (path) => { try {
|
|
63
|
+
unlinkSync(path);
|
|
64
|
+
}
|
|
65
|
+
catch { /* absent */ } },
|
|
66
|
+
realpath: (path) => { try {
|
|
67
|
+
return realpathSync(path);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return null;
|
|
71
|
+
} },
|
|
72
|
+
isExecutable: (path) => { try {
|
|
73
|
+
return statSync(path).isFile() && (statSync(path).mode & 0o111) !== 0;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return false;
|
|
77
|
+
} },
|
|
78
|
+
now: () => new Date(),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
47
81
|
const FLEET_CONFIG_PATH = join(DATA_DIR, "fleet.yaml");
|
|
48
82
|
// CLI commands run outside FleetManager. Honor an explicit fleet locale while
|
|
49
83
|
// keeping the historical English CLI default when config is missing/unset.
|
|
@@ -1007,13 +1041,20 @@ backend
|
|
|
1007
1041
|
const svcPath = join(homedir(), ".config/systemd/user/com.agend.fleet.service");
|
|
1008
1042
|
if (existsSync(svcPath)) {
|
|
1009
1043
|
const svc = readFileSync(svcPath, "utf-8");
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
const
|
|
1013
|
-
|
|
1044
|
+
// The CLI the unit starts — `<entry> fleet start`, or `<node> <entry> fleet start` — against this CLI's own
|
|
1045
|
+
// canonical entry (#1450: `agend` on PATH is the sh launcher, never what a unit records).
|
|
1046
|
+
const { unitCliEntry } = await import("./service-installer.js");
|
|
1047
|
+
const svcEntry = unitCliEntry(svc);
|
|
1048
|
+
let svcReal = svcEntry;
|
|
1049
|
+
try {
|
|
1050
|
+
svcReal = realpathSync(svcEntry);
|
|
1051
|
+
}
|
|
1052
|
+
catch { /* reported as is */ }
|
|
1053
|
+
const current = canonicalCliEntry();
|
|
1054
|
+
if (svcReal === current)
|
|
1014
1055
|
ok(`service ExecStart${" ".repeat(3)} matches current binary`);
|
|
1015
1056
|
else
|
|
1016
|
-
fail(`service ExecStart${" ".repeat(3)} ${
|
|
1057
|
+
fail(`service ExecStart${" ".repeat(3)} ${svcEntry || "(none)"} ≠ ${current}`);
|
|
1017
1058
|
// Check Restart=on-failure
|
|
1018
1059
|
if (svc.includes("Restart=on-failure")) {
|
|
1019
1060
|
ok(`service restart${" ".repeat(5)} Restart=on-failure configured`);
|
|
@@ -1438,6 +1479,7 @@ program
|
|
|
1438
1479
|
}
|
|
1439
1480
|
process.exitCode = 1;
|
|
1440
1481
|
}
|
|
1482
|
+
return restartOutcome;
|
|
1441
1483
|
};
|
|
1442
1484
|
const nvmSh = join(homedir(), ".nvm", "nvm.sh");
|
|
1443
1485
|
/**
|
|
@@ -1447,7 +1489,9 @@ program
|
|
|
1447
1489
|
* (process.argv[1] may be another checkout). A failed target check leaves the running fleet alone.
|
|
1448
1490
|
*/
|
|
1449
1491
|
const activateVerified = async (verified, viaNvm) => {
|
|
1450
|
-
const { newAgendInvocation } = await import("./update-install.js");
|
|
1492
|
+
const { newAgendInvocation, retireSystemCopy, activationSettled } = await import("./update-install.js");
|
|
1493
|
+
// What the restart came to — carried, never inferred from the exit code (pending ≠ restarted).
|
|
1494
|
+
let restartResult = null;
|
|
1451
1495
|
const { activateService } = await import("./service-activation.js");
|
|
1452
1496
|
const { getServicePath, getSystemServicePath, detectPlatform } = await import("./service-installer.js");
|
|
1453
1497
|
const { accessSync, constants } = await import("node:fs");
|
|
@@ -1455,10 +1499,17 @@ program
|
|
|
1455
1499
|
// The authoritative manager, chosen as `agend restart` chooses: system unit > user unit / launchd plist > none.
|
|
1456
1500
|
const systemUnit = getSystemServicePath();
|
|
1457
1501
|
const ownService = systemUnit ? null : getServicePath();
|
|
1458
|
-
const manager = systemUnit ? { kind: "systemd", unit: "agend", user: false }
|
|
1502
|
+
const manager = systemUnit ? { kind: "systemd", unit: "agend", user: false, unitPath: systemUnit }
|
|
1459
1503
|
: ownService && detectPlatform() === "macos" ? { kind: "launchd", label: "com.agend.fleet", plistPath: ownService, domain: `gui/${process.getuid?.() ?? 501}` }
|
|
1460
|
-
: ownService ? { kind: "systemd", unit: "com.agend.fleet", user: true }
|
|
1504
|
+
: ownService ? { kind: "systemd", unit: "com.agend.fleet", user: true, unitPath: ownService }
|
|
1461
1505
|
: { kind: "detached" };
|
|
1506
|
+
// C6: a failure before the fleet runs the new install puts the previous package back too (taken before npm).
|
|
1507
|
+
const { restorePackagePreimage, prunePreimages } = await import("./package-preimage.js");
|
|
1508
|
+
const rollback = verified.rollback;
|
|
1509
|
+
const restorePackage = rollback?.preimage ? () => {
|
|
1510
|
+
const back = restorePackagePreimage(rollback.root, rollback.prefix, rollback.preimage);
|
|
1511
|
+
return back.ok ? `The previous package (v${rollback.preimage.version}) is back in place` : `The previous package could NOT be put back: ${back.reason}`;
|
|
1512
|
+
} : undefined;
|
|
1462
1513
|
const capture = (command, args) => {
|
|
1463
1514
|
const result = spawnSync(command, args, { encoding: "utf-8", timeout: 15_000, stdio: ["ignore", "pipe", "pipe"] });
|
|
1464
1515
|
return { status: result.status, signal: result.signal, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
@@ -1505,10 +1556,30 @@ program
|
|
|
1505
1556
|
spawnSync(newAgend.command, [...newAgend.args, "completion", "install", "--refresh"], { encoding: "utf-8", timeout: 15_000, stdio: "ignore" });
|
|
1506
1557
|
}
|
|
1507
1558
|
catch { /* cosmetic */ }
|
|
1508
|
-
restartFleetForUpdate(newAgend.command, newAgend.args, verified.version);
|
|
1559
|
+
restartResult = restartFleetForUpdate(newAgend.command, newAgend.args, verified.version);
|
|
1509
1560
|
},
|
|
1510
1561
|
log: message => console.log(message),
|
|
1562
|
+
restorePackage,
|
|
1511
1563
|
});
|
|
1564
|
+
// Settled: after a success only this transition's preimage is kept (for a repair); a failure consumed it.
|
|
1565
|
+
if (outcome.ok && rollback)
|
|
1566
|
+
prunePreimages(rollback.prefix, rollback.preimage);
|
|
1567
|
+
// nvm transition: the old system copy is what the previous service ran. Only once the fleet POSITIVELY runs the
|
|
1568
|
+
// new install (activationSettled) is it removed; pending, failed or refused, it stays, so the old unit still starts.
|
|
1569
|
+
if (verified.retireSystemCopy) {
|
|
1570
|
+
if (activationSettled(outcome, restartResult)) {
|
|
1571
|
+
retireSystemCopy({ run: (command, args) => capture(command, args), log: message => console.log(message) });
|
|
1572
|
+
}
|
|
1573
|
+
else {
|
|
1574
|
+
console.log(" Note: the old system install was kept, since the new one is not running yet.");
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
// The refresh above (`install --no-activate`) records a launchd plan; this activation consumed it, whatever it
|
|
1578
|
+
// came to — never leave one for a later `agend restart` to act on.
|
|
1579
|
+
if (manager.kind === "launchd") {
|
|
1580
|
+
const { planPath } = await import("./service-plan.js");
|
|
1581
|
+
planDeps().removeFile(planPath(DATA_DIR));
|
|
1582
|
+
}
|
|
1512
1583
|
if (outcome.ok && outcome.via === "launchd-activation") {
|
|
1513
1584
|
console.log(" ✓ Service restarted (launchd loaded the verified job)");
|
|
1514
1585
|
// The new fleet clears the update marker once it is up.
|
|
@@ -1625,8 +1696,34 @@ program
|
|
|
1625
1696
|
console.log(" Using nvm to install Node 22...");
|
|
1626
1697
|
}
|
|
1627
1698
|
// ── Install, then verify, then clean up (#1446): nothing is removed before the new install is proven ──
|
|
1699
|
+
// #1450 C6: npm replaces THIS package in place. Everything the rest of the update runs — verification, rollback,
|
|
1700
|
+
// service work — is loaded now, before npm; a later import would load the NEW package's files into this process.
|
|
1701
|
+
await Promise.all([
|
|
1702
|
+
import("./service-activation.js"), import("./service-installer.js"), import("./service-plan.js"),
|
|
1703
|
+
import("./restart-guard.js"), import("./package-preimage.js"), import("./install-lock.js"), import("./update-check.js"),
|
|
1704
|
+
]);
|
|
1628
1705
|
const { runUpdateInstall } = await import("./update-install.js");
|
|
1629
|
-
|
|
1706
|
+
// #1450 C1: this update owns the npm prefix from before npm runs until it settles (verify, service, restart) —
|
|
1707
|
+
// released when this process exits, however it exits; a crash leaves a lock the next update reclaims.
|
|
1708
|
+
const { acquireInstallLock } = await import("./install-lock.js");
|
|
1709
|
+
const { randomBytes } = await import("node:crypto");
|
|
1710
|
+
const { createRequire } = await import("node:module");
|
|
1711
|
+
const admission = createRequire(import.meta.url)("../launcher/install-admission.cjs");
|
|
1712
|
+
let releaseLock = () => { };
|
|
1713
|
+
process.once("exit", () => releaseLock());
|
|
1714
|
+
const installed = runUpdateInstall({
|
|
1715
|
+
pkg, targetVersion, viaNvm: needsSudo, nvmSh, rollback: true,
|
|
1716
|
+
lock: prefix => {
|
|
1717
|
+
const lock = acquireInstallLock(prefix, { spec: pkg, agendHome: DATA_DIR }, {
|
|
1718
|
+
pid: process.pid, processStart: admission.processStart, newToken: () => randomBytes(16).toString("hex"),
|
|
1719
|
+
now: () => new Date(), log: message => console.log(message),
|
|
1720
|
+
});
|
|
1721
|
+
if (!lock.ok)
|
|
1722
|
+
return lock;
|
|
1723
|
+
releaseLock = lock.release;
|
|
1724
|
+
return { ok: true, token: lock.token };
|
|
1725
|
+
},
|
|
1726
|
+
}, {
|
|
1630
1727
|
run: (command, args, options = {}) => {
|
|
1631
1728
|
if (options.inherit)
|
|
1632
1729
|
console.log("");
|
|
@@ -1634,6 +1731,7 @@ program
|
|
|
1634
1731
|
encoding: "utf-8",
|
|
1635
1732
|
timeout: options.timeoutMs,
|
|
1636
1733
|
stdio: options.inherit ? "inherit" : ["ignore", "pipe", "pipe"],
|
|
1734
|
+
...(options.env ? { env: { ...process.env, ...options.env } } : {}),
|
|
1637
1735
|
});
|
|
1638
1736
|
return { status: result.status, signal: result.signal, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
1639
1737
|
},
|
|
@@ -1673,15 +1771,45 @@ program
|
|
|
1673
1771
|
.option("--activate", "Deprecated; service activation is now the default")
|
|
1674
1772
|
.option("--no-activate", "Write the service file without activating it")
|
|
1675
1773
|
.action(async (opts) => {
|
|
1676
|
-
const { installService, activateService, detectPlatform } = await import("./service-installer.js");
|
|
1677
|
-
const execPath =
|
|
1678
|
-
const
|
|
1774
|
+
const { installService, activateService, detectPlatform, renderLaunchdPlist, launchdPlistPath } = await import("./service-installer.js");
|
|
1775
|
+
const execPath = canonicalCliEntry();
|
|
1776
|
+
const serviceVars = {
|
|
1679
1777
|
label: "com.agend.fleet",
|
|
1680
1778
|
execPath,
|
|
1681
1779
|
path: process.env.PATH,
|
|
1682
1780
|
workingDirectory: DATA_DIR,
|
|
1683
1781
|
logPath: join(DATA_DIR, "fleet.log"),
|
|
1684
|
-
}
|
|
1782
|
+
};
|
|
1783
|
+
// #1450 C6: on launchd, writing a new plist without activating is a PLANNED activation — proven, recorded, and
|
|
1784
|
+
// left for `agend restart` (a reload there would itself start the job). See service-plan.ts.
|
|
1785
|
+
if (opts.activate === false && detectPlatform() === "macos") {
|
|
1786
|
+
const { refreshLaunchdWithoutActivating, planPath } = await import("./service-plan.js");
|
|
1787
|
+
// A refresh revokes any earlier plan first, whatever it comes to (a failed refresh leaves nothing to activate).
|
|
1788
|
+
planDeps().removeFile(planPath(DATA_DIR));
|
|
1789
|
+
const { expectedTuple } = await import("./restart-guard.js");
|
|
1790
|
+
const expectation = expectedTuple();
|
|
1791
|
+
if (!expectation.ok) {
|
|
1792
|
+
console.error(` ✗ ${expectation.reason}`);
|
|
1793
|
+
process.exitCode = 1;
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1796
|
+
const uid = process.getuid?.() ?? 501;
|
|
1797
|
+
const plistPath = launchdPlistPath(serviceVars.label);
|
|
1798
|
+
mkdirSync(dirname(plistPath), { recursive: true });
|
|
1799
|
+
const outcome = refreshLaunchdWithoutActivating({
|
|
1800
|
+
label: serviceVars.label, target: `gui/${uid}/${serviceVars.label}`, plistPath, newPlist: renderLaunchdPlist(serviceVars),
|
|
1801
|
+
expected: expectation.expected, pkgDir: dirname(dirname(execPath)), agendHome: DATA_DIR,
|
|
1802
|
+
}, planDeps());
|
|
1803
|
+
if (!outcome.ok) {
|
|
1804
|
+
console.error(` ✗ ${outcome.message}`);
|
|
1805
|
+
process.exitCode = 1;
|
|
1806
|
+
return;
|
|
1807
|
+
}
|
|
1808
|
+
console.log(t("install.path", plistPath));
|
|
1809
|
+
console.log(` ${outcome.message}`);
|
|
1810
|
+
return;
|
|
1811
|
+
}
|
|
1812
|
+
const svcPath = installService(serviceVars);
|
|
1685
1813
|
console.log(t("install.path", svcPath));
|
|
1686
1814
|
if (opts.activate !== false) {
|
|
1687
1815
|
const pidPath = join(DATA_DIR, "fleet.pid");
|
|
@@ -1816,7 +1944,9 @@ program
|
|
|
1816
1944
|
if (!userServicePath && !systemServicePath) {
|
|
1817
1945
|
console.log("No service installed. Starting fleet directly...");
|
|
1818
1946
|
const { spawn } = await import("node:child_process");
|
|
1819
|
-
|
|
1947
|
+
// This Node on this CLI, not `sh -c "agend fleet start"` through PATH (#1450 C5).
|
|
1948
|
+
const start = selfCommand(["fleet", "start"]);
|
|
1949
|
+
const child = spawn(start.command, start.args, { detached: true, stdio: "ignore" });
|
|
1820
1950
|
child.unref();
|
|
1821
1951
|
console.log("Fleet starting in background.");
|
|
1822
1952
|
return;
|
|
@@ -1837,9 +1967,50 @@ program
|
|
|
1837
1967
|
.command("restart")
|
|
1838
1968
|
.description("Restart the AgEnD service (auto-detects systemd/launchd/detached)")
|
|
1839
1969
|
.option("-y, --yes", "Confirm when run from a fleet agent session (this affects every instance)")
|
|
1970
|
+
.option("--force", "Restart even when the loaded service does not start this install on its selected Node (operators only)")
|
|
1840
1971
|
.action(async (opts) => {
|
|
1972
|
+
// --force overrides the C6 guard: an operator's own decision, never a fleet agent's or a fleet-internal spawn's.
|
|
1973
|
+
if (opts.force && !forceAllowed()) {
|
|
1974
|
+
console.error(" ✗ Not restarting: --force is for an operator's own shell, not a fleet agent session or the fleet's own commands. Nothing was stopped.");
|
|
1975
|
+
process.exit(1);
|
|
1976
|
+
}
|
|
1841
1977
|
if (!gateFleetControl(DATA_DIR, "restart", { yes: opts.yes }))
|
|
1842
1978
|
process.exit(1);
|
|
1979
|
+
// #1450 C6: what the restarted fleet must run, by this package's own selection (C2) — decided before anything stops.
|
|
1980
|
+
const guard = await import("./restart-guard.js");
|
|
1981
|
+
const guardDeps = {
|
|
1982
|
+
realpath: (path) => { try {
|
|
1983
|
+
return realpathSync(path);
|
|
1984
|
+
}
|
|
1985
|
+
catch {
|
|
1986
|
+
return null;
|
|
1987
|
+
} },
|
|
1988
|
+
isExecutable: (path) => { try {
|
|
1989
|
+
return statSync(path).isFile() && (statSync(path).mode & 0o111) !== 0;
|
|
1990
|
+
}
|
|
1991
|
+
catch {
|
|
1992
|
+
return false;
|
|
1993
|
+
} },
|
|
1994
|
+
};
|
|
1995
|
+
const captureRun = (command, args) => {
|
|
1996
|
+
const r = spawnSync(command, args, { encoding: "utf-8", timeout: 15_000, stdio: ["ignore", "pipe", "pipe"] });
|
|
1997
|
+
return { status: r.status, signal: r.signal, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
1998
|
+
};
|
|
1999
|
+
const expectation = guard.expectedTuple();
|
|
2000
|
+
/** Refuse (nothing stopped) unless the definition about to run is proven; `--force` overrides for an operator. */
|
|
2001
|
+
const refuses = (judged) => {
|
|
2002
|
+
if (judged.ok)
|
|
2003
|
+
return false;
|
|
2004
|
+
if (opts.force) {
|
|
2005
|
+
console.log(` ⚠ --force: ${judged.reason}; restarting anyway.`);
|
|
2006
|
+
return false;
|
|
2007
|
+
}
|
|
2008
|
+
console.error(` ✗ Not restarting: ${judged.reason}. Nothing was stopped.`);
|
|
2009
|
+
process.exitCode = 1;
|
|
2010
|
+
return true;
|
|
2011
|
+
};
|
|
2012
|
+
if (!expectation.ok && refuses(expectation))
|
|
2013
|
+
return;
|
|
1843
2014
|
// Try each runtime environment in order and stop at the first that succeeds.
|
|
1844
2015
|
// Don't gate on fleet.pid: a fleet under systemd/launchd writes its pid in the
|
|
1845
2016
|
// service's own HOME, which may differ from what this command resolves (sudo,
|
|
@@ -1989,6 +2160,8 @@ program
|
|
|
1989
2160
|
}
|
|
1990
2161
|
if (filterCustom)
|
|
1991
2162
|
console.log(` ⚠ ${unitPath} sets its own CoredumpFilter; left as is. CoredumpFilter=0 keeps crash dumps to a few KB (#1113).`);
|
|
2163
|
+
if (expectation.ok && refuses(guard.guardSystemd(captureRun, systemdTarget.user, systemdTarget.unit, expectation.expected, guardDeps)))
|
|
2164
|
+
return;
|
|
1992
2165
|
// Clears the failed state AND the start-limit counter (#1113:
|
|
1993
2166
|
// StartLimitBurst=4 in 30min) for whichever unit this restart targets,
|
|
1994
2167
|
// so an operator's restart is never refused by the limit.
|
|
@@ -2015,19 +2188,68 @@ program
|
|
|
2015
2188
|
process.exitCode = 1;
|
|
2016
2189
|
return;
|
|
2017
2190
|
}
|
|
2018
|
-
// 3. launchd (macOS)
|
|
2191
|
+
// 3. launchd (macOS). "Loaded" is launchctl print's own answer (113 = not loaded); nothing here may fall through
|
|
2192
|
+
// to the detached restart once a job is known to be loaded.
|
|
2019
2193
|
if (plat === "macos") {
|
|
2020
2194
|
const uid = process.getuid?.() ?? 501;
|
|
2021
2195
|
const label = "com.agend.fleet";
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2196
|
+
const target = `gui/${uid}/${label}`;
|
|
2197
|
+
const deps = planDeps();
|
|
2198
|
+
const printed = deps.run("launchctl", ["print", target]);
|
|
2199
|
+
if (printed.status === 0) {
|
|
2200
|
+
const plistPath = getServicePath() ?? join(homedir(), "Library", "LaunchAgents", `${label}.plist`);
|
|
2201
|
+
// Path 2 (#1450 C6): a proven new plist waits on disk with its plan — exactly one bootout + bootstrap.
|
|
2202
|
+
const { readPlan, admitPlan, planPath } = await import("./service-plan.js");
|
|
2203
|
+
const plan = opts.force ? null : readPlan(DATA_DIR, deps);
|
|
2204
|
+
if (plan && expectation.ok) {
|
|
2205
|
+
const pkgDir = dirname(dirname(expectation.expected.entry));
|
|
2206
|
+
const admitted = admitPlan(plan, { target, expected: expectation.expected, pkgDir }, deps);
|
|
2207
|
+
if (!admitted.ok) {
|
|
2208
|
+
console.error(` ✗ Not activating the planned launchd job: ${admitted.reason}. Nothing was stopped.`);
|
|
2209
|
+
process.exitCode = 1;
|
|
2210
|
+
return;
|
|
2211
|
+
}
|
|
2212
|
+
const { activateService } = await import("./service-activation.js");
|
|
2213
|
+
const outcome = activateService({ kind: "launchd", label, plistPath: plan.plistPath, domain: `gui/${uid}` }, { bin: expectation.expected.entry, entry: expectation.expected.entry, node: expectation.expected.node, dir: pkgDir }, {
|
|
2214
|
+
...deps,
|
|
2215
|
+
readFirstLine: path => deps.readFile(path)?.split("\n", 1)[0] ?? null,
|
|
2216
|
+
isExecutable: path => { try {
|
|
2217
|
+
return statSync(path).isFile() && (statSync(path).mode & 0o111) !== 0;
|
|
2218
|
+
}
|
|
2219
|
+
catch {
|
|
2220
|
+
return false;
|
|
2221
|
+
} },
|
|
2222
|
+
refresh: () => ({ status: 0, signal: null, stdout: "", stderr: "" }),
|
|
2223
|
+
restart: () => { },
|
|
2224
|
+
log: message => console.log(message),
|
|
2225
|
+
launchdPreimage: plan.preimage.plist,
|
|
2226
|
+
});
|
|
2227
|
+
deps.removeFile(planPath(DATA_DIR));
|
|
2228
|
+
if (outcome.ok) {
|
|
2229
|
+
console.log("Service restarted (launchd loaded the planned job).");
|
|
2230
|
+
return;
|
|
2231
|
+
}
|
|
2232
|
+
console.error(outcome.message);
|
|
2233
|
+
process.exitCode = 1;
|
|
2234
|
+
return;
|
|
2235
|
+
}
|
|
2236
|
+
// Path 1: an unchanged loaded job.
|
|
2237
|
+
if (expectation.ok && refuses(guard.guardLaunchd(captureRun, target, plistPath, deps.readFile, expectation.expected, guardDeps)))
|
|
2238
|
+
return;
|
|
2239
|
+
if (run(`launchctl kickstart -k ${target}`)) {
|
|
2026
2240
|
console.log("Service restarted.");
|
|
2027
2241
|
return;
|
|
2028
2242
|
}
|
|
2243
|
+
console.error(` ✗ launchctl kickstart -k ${target} failed.`);
|
|
2244
|
+
process.exitCode = 1;
|
|
2245
|
+
return;
|
|
2246
|
+
}
|
|
2247
|
+
const { LAUNCHD_NOT_LOADED } = await import("./service-plan.js");
|
|
2248
|
+
if (!LAUNCHD_NOT_LOADED.includes(printed.status ?? -1) || printed.signal !== null) {
|
|
2249
|
+
console.error(` ✗ Could not tell whether ${label} is loaded (launchctl print ${printed.signal ? `killed by ${printed.signal}` : `exited ${printed.status}`}); refusing a detached restart that could duplicate the fleet.`);
|
|
2250
|
+
process.exitCode = 1;
|
|
2251
|
+
return;
|
|
2029
2252
|
}
|
|
2030
|
-
catch { /* not loaded — fall through */ }
|
|
2031
2253
|
}
|
|
2032
2254
|
// 4. Detached process tracked by fleet.pid (no service manager)
|
|
2033
2255
|
if (existsSync(pidPath)) {
|
|
@@ -2045,6 +2267,9 @@ program
|
|
|
2045
2267
|
return isFleetStartCommandLine(readProcessCommandLine(oldPid));
|
|
2046
2268
|
};
|
|
2047
2269
|
if (oldPid && isOurFleet()) {
|
|
2270
|
+
// The replacement is `<this Node> <entry> fleet start` (C5): this Node must be the selected one.
|
|
2271
|
+
if (expectation.ok && refuses(guard.guardDetached(process.execPath, expectation.expected, guardDeps)))
|
|
2272
|
+
return;
|
|
2048
2273
|
try {
|
|
2049
2274
|
process.kill(oldPid, "SIGTERM");
|
|
2050
2275
|
}
|
|
@@ -2070,7 +2295,9 @@ program
|
|
|
2070
2295
|
unlinkSync(pidPath);
|
|
2071
2296
|
}
|
|
2072
2297
|
catch { /* best effort */ }
|
|
2073
|
-
|
|
2298
|
+
// This Node on this CLI, not `sh -c "agend fleet start"` through PATH (#1450 C5).
|
|
2299
|
+
const start = selfCommand(["fleet", "start"]);
|
|
2300
|
+
const child = spawn(start.command, start.args, { detached: true, stdio: "ignore" });
|
|
2074
2301
|
child.unref();
|
|
2075
2302
|
console.log("Fleet restarting in background.");
|
|
2076
2303
|
return;
|