@songsid/agend 2.2.0-alpha.1 → 2.2.0-alpha.3
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 +6 -4
- package/README.zh-TW.md +6 -4
- package/dist/agent-cli.js +6 -1
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.d.ts +4 -1
- package/dist/agent-endpoint.js +82 -14
- 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/atomic-write.d.ts +21 -0
- package/dist/atomic-write.js +65 -0
- package/dist/atomic-write.js.map +1 -0
- package/dist/auth-api.js +2 -0
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/antigravity.js +4 -3
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/codex.d.ts +7 -0
- package/dist/backend/codex.js +7 -0
- package/dist/backend/codex.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.d.ts +7 -0
- package/dist/backend/muse.js +8 -1
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +12 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/binary-probe.d.ts +80 -0
- package/dist/binary-probe.js +182 -0
- package/dist/binary-probe.js.map +1 -0
- 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 +131 -0
- package/dist/cache-service.js +452 -0
- package/dist/cache-service.js.map +1 -0
- package/dist/channel/adapters/discord.d.ts +5 -1
- package/dist/channel/adapters/discord.js +56 -11
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +38 -1
- package/dist/channel/adapters/telegram.js +82 -0
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-tools.js +1 -1
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/tool-router.d.ts +8 -1
- package/dist/channel/tool-router.js +4 -2
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/channel/types.d.ts +21 -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 +376 -54
- package/dist/cli.js.map +1 -1
- package/dist/connection-secrets.d.ts +5 -0
- package/dist/connection-secrets.js.map +1 -1
- package/dist/cpu-profile.js +50 -0
- package/dist/cpu-profile.js.map +1 -1
- package/dist/daemon.d.ts +18 -0
- package/dist/daemon.js +256 -63
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +1 -0
- package/dist/delivery-outbox.js +12 -0
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/discord-permissions.d.ts +33 -0
- package/dist/discord-permissions.js +42 -0
- package/dist/discord-permissions.js.map +1 -0
- package/dist/env-file.d.ts +11 -0
- package/dist/env-file.js +27 -0
- package/dist/env-file.js.map +1 -0
- 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-context.d.ts +5 -0
- package/dist/fleet-context.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.d.ts +8 -1
- package/dist/fleet-level-config.js +53 -0
- package/dist/fleet-level-config.js.map +1 -1
- package/dist/fleet-manager.d.ts +122 -7
- package/dist/fleet-manager.js +871 -152
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-stop-budget.d.ts +22 -0
- package/dist/fleet-stop-budget.js +95 -0
- package/dist/fleet-stop-budget.js.map +1 -0
- 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/kiro-transcript-lane.d.ts +8 -4
- package/dist/kiro-transcript-lane.js +66 -44
- package/dist/kiro-transcript-lane.js.map +1 -1
- package/dist/locale.js +96 -0
- package/dist/locale.js.map +1 -1
- package/dist/next-steps.d.ts +5 -0
- package/dist/next-steps.js +26 -0
- package/dist/next-steps.js.map +1 -0
- package/dist/outbound-handlers.js +8 -0
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +4 -0
- package/dist/outbound-schemas.js +7 -2
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/outbox-open-error.d.ts +7 -0
- package/dist/outbox-open-error.js +15 -0
- package/dist/outbox-open-error.js.map +1 -0
- 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/pane-context-cache.d.ts +25 -0
- package/dist/pane-context-cache.js +73 -0
- package/dist/pane-context-cache.js.map +1 -0
- package/dist/paths.d.ts +0 -7
- package/dist/paths.js +8 -2
- package/dist/paths.js.map +1 -1
- package/dist/provider-probe.d.ts +15 -0
- package/dist/provider-probe.js +20 -0
- package/dist/provider-probe.js.map +1 -1
- package/dist/public-link-progress.d.ts +73 -0
- package/dist/public-link-progress.js +190 -0
- package/dist/public-link-progress.js.map +1 -0
- package/dist/public-web-gateway.js +2 -2
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +6 -1
- package/dist/public-web-link.js +87 -15
- package/dist/public-web-link.js.map +1 -1
- package/dist/quickstart-api.d.ts +93 -20
- package/dist/quickstart-api.js +194 -62
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.d.ts +15 -0
- package/dist/quickstart.js +110 -33
- package/dist/quickstart.js.map +1 -1
- package/dist/reply-buttons-controller.d.ts +98 -0
- package/dist/reply-buttons-controller.js +174 -0
- package/dist/reply-buttons-controller.js.map +1 -0
- package/dist/reply-buttons.d.ts +113 -0
- package/dist/reply-buttons.js +208 -0
- package/dist/reply-buttons.js.map +1 -0
- package/dist/reply-dedup.js +3 -1
- package/dist/reply-dedup.js.map +1 -1
- package/dist/restart-guard.d.ts +53 -0
- package/dist/restart-guard.js +114 -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 +62 -7
- package/dist/service-activation.js +237 -55
- package/dist/service-activation.js.map +1 -1
- package/dist/service-installer.d.ts +56 -1
- package/dist/service-installer.js +152 -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 +92 -10
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-authority.d.ts +11 -0
- package/dist/settings-authority.js +128 -0
- package/dist/settings-authority.js.map +1 -0
- package/dist/settings-change.d.ts +5 -0
- package/dist/settings-change.js +37 -8
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-confirmation.d.ts +3 -0
- package/dist/settings-confirmation.js +7 -1
- package/dist/settings-confirmation.js.map +1 -1
- package/dist/settings-effect.js +19 -7
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.js +1 -1
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/settings-redaction.d.ts +18 -0
- package/dist/settings-redaction.js +58 -0
- package/dist/settings-redaction.js.map +1 -0
- package/dist/setup-form.d.ts +9 -1
- package/dist/setup-form.js +25 -1
- package/dist/setup-form.js.map +1 -1
- package/dist/setup-host.js +15 -6
- package/dist/setup-host.js.map +1 -1
- package/dist/setup-wizard.js +27 -19
- 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/systemd-runtime.d.ts +15 -0
- package/dist/systemd-runtime.js +41 -0
- package/dist/systemd-runtime.js.map +1 -0
- package/dist/tmux-control.d.ts +44 -0
- package/dist/tmux-control.js +170 -21
- package/dist/tmux-control.js.map +1 -1
- package/dist/token-env-name.d.ts +37 -0
- package/dist/token-env-name.js +93 -0
- package/dist/token-env-name.js.map +1 -0
- package/dist/tool-permissions.js +5 -3
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +7 -16
- package/dist/topic-commands.js +56 -85
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-monitor.js +3 -0
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/transcript-sources.d.ts +68 -0
- package/dist/transcript-sources.js +336 -29
- package/dist/transcript-sources.js.map +1 -1
- package/dist/transcript-turns.d.ts +39 -0
- package/dist/transcript-turns.js +58 -0
- package/dist/transcript-turns.js.map +1 -0
- package/dist/tunnel/cloudflared-install.d.ts +18 -0
- package/dist/tunnel/cloudflared-install.js +15 -1
- package/dist/tunnel/cloudflared-install.js.map +1 -1
- package/dist/turn-reply-guard.d.ts +3 -0
- package/dist/turn-reply-guard.js.map +1 -1
- package/dist/ui/chat-commands.js +43 -0
- package/dist/ui/chat-render.js +34 -8
- package/dist/ui/chat-store.js +51 -2
- package/dist/ui/chat-strings.js +30 -2
- package/dist/ui/chat-thread.js +141 -47
- package/dist/ui/first-run.js +64 -0
- package/dist/ui/fleet-cache.js +168 -0
- package/dist/ui/html-attachment.js +26 -0
- package/dist/ui/panel-chat.js +117 -23
- package/dist/ui/panel-fleet.js +31 -17
- package/dist/ui/panel-settings.js +29 -15
- package/dist/ui/preview-panel.js +237 -0
- package/dist/ui/preview.js +7 -3
- package/dist/ui/settings-apply.js +3 -1
- package/dist/ui/settings-dialogs.js +95 -25
- package/dist/ui/settings-discord.js +130 -0
- package/dist/ui/settings-model.js +21 -0
- package/dist/ui/settings-strings.js +50 -8
- package/dist/ui/settings-token.js +54 -0
- package/dist/ui/settings-wizard.js +63 -29
- package/dist/ui/shared/app-route.js +1 -1
- package/dist/ui/shared/app-shell.js +25 -37
- package/dist/ui/shared/app-store.js +5 -3
- package/dist/ui/shared/app-stream.js +2 -1
- package/dist/ui/shared/app.css +181 -5
- package/dist/ui/shared/app.js +2 -1
- package/dist/ui/shared/instance-nav.js +310 -0
- package/dist/ui/shared/panel-view.js +12 -190
- package/dist/ui/shared/signin.js +13 -1
- package/dist/ui/shared/ui-icons.js +2 -0
- package/dist/ui/shared/view-roster-store.js +5 -0
- package/dist/ui/shared/view-strings.js +12 -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 +78 -8
- package/dist/update-install.js +158 -30
- package/dist/update-install.js.map +1 -1
- package/dist/update-launch.d.ts +22 -0
- package/dist/update-launch.js +106 -0
- package/dist/update-launch.js.map +1 -0
- package/dist/update-marker.js +3 -8
- package/dist/update-marker.js.map +1 -1
- package/dist/usage/format-rich.js +7 -1
- package/dist/usage/format-rich.js.map +1 -1
- package/dist/usage/i18n-keys.d.ts +1 -1
- package/dist/usage/i18n-keys.js +1 -1
- package/dist/usage/i18n-keys.js.map +1 -1
- package/dist/usage/providers.d.ts +4 -0
- package/dist/usage/providers.js +2 -1
- package/dist/usage/providers.js.map +1 -1
- package/dist/usage/usage-api.js +48 -2
- package/dist/usage/usage-api.js.map +1 -1
- package/dist/view-api.d.ts +40 -0
- package/dist/view-api.js +28 -10
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +22 -0
- package/dist/web-api.js +115 -16
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.d.ts +8 -0
- package/dist/web-auth.js +34 -8
- package/dist/web-auth.js.map +1 -1
- package/dist/web-chat-history.d.ts +21 -0
- package/dist/web-chat-history.js +44 -0
- package/dist/web-chat-history.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-host-guard.d.ts +18 -2
- package/dist/web-host-guard.js +30 -5
- package/dist/web-host-guard.js.map +1 -1
- package/dist/web-session.d.ts +7 -1
- package/dist/web-session.js +23 -2
- package/dist/web-session.js.map +1 -1
- 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 +118 -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 +4 -4
package/dist/cli.js
CHANGED
|
@@ -19,13 +19,14 @@ 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
|
+
import { FLEET_STOP_TIMEOUT_MS, stopDetachedOwner, systemdStopTimeoutMs, detachedProcessStart, detachedProcessExited } from "./fleet-stop-budget.js";
|
|
29
30
|
import { acquireFleetLock, isFleetStartCommandLine, readProcessCommandLine, releaseProcessFleetLock, setProcessFleetLock } from "./fleet-lock.js";
|
|
30
31
|
import { limitFleetCoreDumps } from "./coredump-filter.js";
|
|
31
32
|
import { SYSTEMD_RESTART_TIMEOUT_MS } from "./service-installer.js";
|
|
@@ -36,6 +37,7 @@ import { DeliveryOutbox, deliveryStatusSelector } from "./delivery-outbox.js";
|
|
|
36
37
|
import { DeliveryStatusArgs } from "./outbound-schemas.js";
|
|
37
38
|
import { requestCpuProfile } from "./profile-control.js";
|
|
38
39
|
import { requestSettingsConfirmation } from "./settings-control.js";
|
|
40
|
+
import { canonicalCliEntry, selfCommand } from "./cli-entry.js";
|
|
39
41
|
/** Prefix tmux args with -L when socket isolation is active. */
|
|
40
42
|
function tmuxArgs(args) {
|
|
41
43
|
const socket = getTmuxSocketName();
|
|
@@ -44,6 +46,39 @@ function tmuxArgs(args) {
|
|
|
44
46
|
const __filename = fileURLToPath(import.meta.url);
|
|
45
47
|
const __dirname = dirname(__filename);
|
|
46
48
|
const DATA_DIR = getAgendHome();
|
|
49
|
+
/** Real-filesystem, real-process deps for the launchd plan (service-plan.ts) and its activation. */
|
|
50
|
+
function planDeps() {
|
|
51
|
+
return {
|
|
52
|
+
run: (command, args) => {
|
|
53
|
+
const r = spawnSync(command, args, { encoding: "utf-8", timeout: 15_000, stdio: ["ignore", "pipe", "pipe"] });
|
|
54
|
+
return { status: r.status, signal: r.signal, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
55
|
+
},
|
|
56
|
+
readFile: (path) => { try {
|
|
57
|
+
return readFileSync(path, "utf-8");
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
} },
|
|
62
|
+
writeFile: (path, content) => writeFileSync(path, content),
|
|
63
|
+
removeFile: (path) => { try {
|
|
64
|
+
unlinkSync(path);
|
|
65
|
+
}
|
|
66
|
+
catch { /* absent */ } },
|
|
67
|
+
realpath: (path) => { try {
|
|
68
|
+
return realpathSync(path);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
} },
|
|
73
|
+
isExecutable: (path) => { try {
|
|
74
|
+
return statSync(path).isFile() && (statSync(path).mode & 0o111) !== 0;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return false;
|
|
78
|
+
} },
|
|
79
|
+
now: () => new Date(),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
47
82
|
const FLEET_CONFIG_PATH = join(DATA_DIR, "fleet.yaml");
|
|
48
83
|
// CLI commands run outside FleetManager. Honor an explicit fleet locale while
|
|
49
84
|
// keeping the historical English CLI default when config is missing/unset.
|
|
@@ -1007,13 +1042,20 @@ backend
|
|
|
1007
1042
|
const svcPath = join(homedir(), ".config/systemd/user/com.agend.fleet.service");
|
|
1008
1043
|
if (existsSync(svcPath)) {
|
|
1009
1044
|
const svc = readFileSync(svcPath, "utf-8");
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
const
|
|
1013
|
-
|
|
1045
|
+
// The CLI the unit starts — `<entry> fleet start`, or `<node> <entry> fleet start` — against this CLI's own
|
|
1046
|
+
// canonical entry (#1450: `agend` on PATH is the sh launcher, never what a unit records).
|
|
1047
|
+
const { unitCliEntry } = await import("./service-installer.js");
|
|
1048
|
+
const svcEntry = unitCliEntry(svc);
|
|
1049
|
+
let svcReal = svcEntry;
|
|
1050
|
+
try {
|
|
1051
|
+
svcReal = realpathSync(svcEntry);
|
|
1052
|
+
}
|
|
1053
|
+
catch { /* reported as is */ }
|
|
1054
|
+
const current = canonicalCliEntry();
|
|
1055
|
+
if (svcReal === current)
|
|
1014
1056
|
ok(`service ExecStart${" ".repeat(3)} matches current binary`);
|
|
1015
1057
|
else
|
|
1016
|
-
fail(`service ExecStart${" ".repeat(3)} ${
|
|
1058
|
+
fail(`service ExecStart${" ".repeat(3)} ${svcEntry || "(none)"} ≠ ${current}`);
|
|
1017
1059
|
// Check Restart=on-failure
|
|
1018
1060
|
if (svc.includes("Restart=on-failure")) {
|
|
1019
1061
|
ok(`service restart${" ".repeat(5)} Restart=on-failure configured`);
|
|
@@ -1438,6 +1480,9 @@ program
|
|
|
1438
1480
|
}
|
|
1439
1481
|
process.exitCode = 1;
|
|
1440
1482
|
}
|
|
1483
|
+
if (restartOutcome === "pending")
|
|
1484
|
+
process.exitCode = 75;
|
|
1485
|
+
return restartOutcome;
|
|
1441
1486
|
};
|
|
1442
1487
|
const nvmSh = join(homedir(), ".nvm", "nvm.sh");
|
|
1443
1488
|
/**
|
|
@@ -1447,7 +1492,9 @@ program
|
|
|
1447
1492
|
* (process.argv[1] may be another checkout). A failed target check leaves the running fleet alone.
|
|
1448
1493
|
*/
|
|
1449
1494
|
const activateVerified = async (verified, viaNvm) => {
|
|
1450
|
-
const { newAgendInvocation } = await import("./update-install.js");
|
|
1495
|
+
const { newAgendInvocation, retireSystemCopy, activationSettled } = await import("./update-install.js");
|
|
1496
|
+
// What the restart came to — carried, never inferred from the exit code (pending ≠ restarted).
|
|
1497
|
+
let restartResult = null;
|
|
1451
1498
|
const { activateService } = await import("./service-activation.js");
|
|
1452
1499
|
const { getServicePath, getSystemServicePath, detectPlatform } = await import("./service-installer.js");
|
|
1453
1500
|
const { accessSync, constants } = await import("node:fs");
|
|
@@ -1455,10 +1502,18 @@ program
|
|
|
1455
1502
|
// The authoritative manager, chosen as `agend restart` chooses: system unit > user unit / launchd plist > none.
|
|
1456
1503
|
const systemUnit = getSystemServicePath();
|
|
1457
1504
|
const ownService = systemUnit ? null : getServicePath();
|
|
1458
|
-
const manager = systemUnit ? { kind: "systemd", unit: "agend", user: false }
|
|
1505
|
+
const manager = systemUnit ? { kind: "systemd", unit: "agend", user: false, unitPath: systemUnit }
|
|
1459
1506
|
: 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 }
|
|
1507
|
+
: ownService ? { kind: "systemd", unit: "com.agend.fleet", user: true, unitPath: ownService }
|
|
1461
1508
|
: { kind: "detached" };
|
|
1509
|
+
// C6: a failure before the fleet runs the new install puts the previous package back too (taken before npm).
|
|
1510
|
+
const { restorePackagePreimage, prunePreimages } = await import("./package-preimage.js");
|
|
1511
|
+
const rollback = verified.rollback;
|
|
1512
|
+
const restoreChecked = rollback?.preimage ? () => {
|
|
1513
|
+
const back = restorePackagePreimage(rollback.root, rollback.prefix, rollback.preimage);
|
|
1514
|
+
return { ok: back.ok, message: back.ok ? `The previous package (v${rollback.preimage.version}) is back in place` : `The previous package could NOT be put back: ${back.reason}` };
|
|
1515
|
+
} : undefined;
|
|
1516
|
+
const restorePackage = restoreChecked ? () => restoreChecked().message : undefined;
|
|
1462
1517
|
const capture = (command, args) => {
|
|
1463
1518
|
const result = spawnSync(command, args, { encoding: "utf-8", timeout: 15_000, stdio: ["ignore", "pipe", "pipe"] });
|
|
1464
1519
|
return { status: result.status, signal: result.signal, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
@@ -1505,16 +1560,47 @@ program
|
|
|
1505
1560
|
spawnSync(newAgend.command, [...newAgend.args, "completion", "install", "--refresh"], { encoding: "utf-8", timeout: 15_000, stdio: "ignore" });
|
|
1506
1561
|
}
|
|
1507
1562
|
catch { /* cosmetic */ }
|
|
1508
|
-
restartFleetForUpdate(newAgend.command, newAgend.args, verified.version);
|
|
1563
|
+
restartResult = restartFleetForUpdate(newAgend.command, newAgend.args, verified.version);
|
|
1564
|
+
return restartResult;
|
|
1509
1565
|
},
|
|
1510
1566
|
log: message => console.log(message),
|
|
1567
|
+
restorePackage,
|
|
1568
|
+
systemdRecovery: restoreChecked ? { restorePackage: restoreChecked } : undefined,
|
|
1511
1569
|
});
|
|
1570
|
+
// Settled: after a success only this transition's preimage is kept (for a repair); a failure consumed it.
|
|
1571
|
+
if (activationSettled(outcome, restartResult) && rollback)
|
|
1572
|
+
prunePreimages(rollback.prefix, rollback.preimage);
|
|
1573
|
+
// nvm transition: the old system copy is what the previous service ran. Only once the fleet POSITIVELY runs the
|
|
1574
|
+
// new install (activationSettled) is it removed; pending, failed or refused, it stays, so the old unit still starts.
|
|
1575
|
+
if (verified.retireSystemCopy) {
|
|
1576
|
+
if (activationSettled(outcome, restartResult)) {
|
|
1577
|
+
const retireResult = retireSystemCopy({ run: (command, args) => capture(command, args), log: message => console.log(message) }, verified.npmPath);
|
|
1578
|
+
if (!retireResult.ok) {
|
|
1579
|
+
console.error(` ✗ Old system install was not removed: ${retireResult.reason}`);
|
|
1580
|
+
process.exitCode = 1;
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
else {
|
|
1584
|
+
console.log(" Note: the old system install was kept, since the new one is not running yet.");
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
// The refresh above (`install --no-activate`) records a launchd plan; this activation consumed it, whatever it
|
|
1588
|
+
// came to — never leave one for a later `agend restart` to act on.
|
|
1589
|
+
if (manager.kind === "launchd") {
|
|
1590
|
+
const { planPath } = await import("./service-plan.js");
|
|
1591
|
+
planDeps().removeFile(planPath(DATA_DIR));
|
|
1592
|
+
}
|
|
1512
1593
|
if (outcome.ok && outcome.via === "launchd-activation") {
|
|
1513
1594
|
console.log(" ✓ Service restarted (launchd loaded the verified job)");
|
|
1514
1595
|
// The new fleet clears the update marker once it is up.
|
|
1515
1596
|
return;
|
|
1516
1597
|
}
|
|
1517
1598
|
if (!outcome.ok) {
|
|
1599
|
+
if (outcome.pending) {
|
|
1600
|
+
console.log(outcome.message);
|
|
1601
|
+
process.exitCode = 75;
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1518
1604
|
console.error(outcome.message);
|
|
1519
1605
|
if (!setUpdateProgressStage(DATA_DIR, "failed", { error: outcome.message.trim() }))
|
|
1520
1606
|
clearUpdateMarker(DATA_DIR);
|
|
@@ -1625,8 +1711,34 @@ program
|
|
|
1625
1711
|
console.log(" Using nvm to install Node 22...");
|
|
1626
1712
|
}
|
|
1627
1713
|
// ── Install, then verify, then clean up (#1446): nothing is removed before the new install is proven ──
|
|
1714
|
+
// #1450 C6: npm replaces THIS package in place. Everything the rest of the update runs — verification, rollback,
|
|
1715
|
+
// service work — is loaded now, before npm; a later import would load the NEW package's files into this process.
|
|
1716
|
+
await Promise.all([
|
|
1717
|
+
import("./service-activation.js"), import("./service-installer.js"), import("./service-plan.js"),
|
|
1718
|
+
import("./restart-guard.js"), import("./package-preimage.js"), import("./install-lock.js"), import("./update-check.js"),
|
|
1719
|
+
]);
|
|
1628
1720
|
const { runUpdateInstall } = await import("./update-install.js");
|
|
1629
|
-
|
|
1721
|
+
// #1450 C1: this update owns the npm prefix from before npm runs until it settles (verify, service, restart) —
|
|
1722
|
+
// released when this process exits, however it exits; a crash leaves a lock the next update reclaims.
|
|
1723
|
+
const { acquireInstallLock } = await import("./install-lock.js");
|
|
1724
|
+
const { randomBytes } = await import("node:crypto");
|
|
1725
|
+
const { createRequire } = await import("node:module");
|
|
1726
|
+
const admission = createRequire(import.meta.url)("../launcher/install-admission.cjs");
|
|
1727
|
+
let releaseLock = () => { };
|
|
1728
|
+
process.once("exit", () => releaseLock());
|
|
1729
|
+
const installed = runUpdateInstall({
|
|
1730
|
+
pkg, targetVersion, viaNvm: needsSudo, nvmSh, rollback: true,
|
|
1731
|
+
lock: prefix => {
|
|
1732
|
+
const lock = acquireInstallLock(prefix, { spec: pkg, agendHome: DATA_DIR }, {
|
|
1733
|
+
pid: process.pid, processStart: admission.processStart, newToken: () => randomBytes(16).toString("hex"),
|
|
1734
|
+
now: () => new Date(), log: message => console.log(message),
|
|
1735
|
+
});
|
|
1736
|
+
if (!lock.ok)
|
|
1737
|
+
return lock;
|
|
1738
|
+
releaseLock = lock.release;
|
|
1739
|
+
return { ok: true, token: lock.token };
|
|
1740
|
+
},
|
|
1741
|
+
}, {
|
|
1630
1742
|
run: (command, args, options = {}) => {
|
|
1631
1743
|
if (options.inherit)
|
|
1632
1744
|
console.log("");
|
|
@@ -1634,6 +1746,7 @@ program
|
|
|
1634
1746
|
encoding: "utf-8",
|
|
1635
1747
|
timeout: options.timeoutMs,
|
|
1636
1748
|
stdio: options.inherit ? "inherit" : ["ignore", "pipe", "pipe"],
|
|
1749
|
+
...(options.env ? { env: { ...process.env, ...options.env } } : {}),
|
|
1637
1750
|
});
|
|
1638
1751
|
return { status: result.status, signal: result.signal, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
1639
1752
|
},
|
|
@@ -1673,15 +1786,45 @@ program
|
|
|
1673
1786
|
.option("--activate", "Deprecated; service activation is now the default")
|
|
1674
1787
|
.option("--no-activate", "Write the service file without activating it")
|
|
1675
1788
|
.action(async (opts) => {
|
|
1676
|
-
const { installService, activateService, detectPlatform } = await import("./service-installer.js");
|
|
1677
|
-
const execPath =
|
|
1678
|
-
const
|
|
1789
|
+
const { installService, activateService, detectPlatform, renderLaunchdPlist, launchdPlistPath } = await import("./service-installer.js");
|
|
1790
|
+
const execPath = canonicalCliEntry();
|
|
1791
|
+
const serviceVars = {
|
|
1679
1792
|
label: "com.agend.fleet",
|
|
1680
1793
|
execPath,
|
|
1681
1794
|
path: process.env.PATH,
|
|
1682
1795
|
workingDirectory: DATA_DIR,
|
|
1683
1796
|
logPath: join(DATA_DIR, "fleet.log"),
|
|
1684
|
-
}
|
|
1797
|
+
};
|
|
1798
|
+
// #1450 C6: on launchd, writing a new plist without activating is a PLANNED activation — proven, recorded, and
|
|
1799
|
+
// left for `agend restart` (a reload there would itself start the job). See service-plan.ts.
|
|
1800
|
+
if (opts.activate === false && detectPlatform() === "macos") {
|
|
1801
|
+
const { refreshLaunchdWithoutActivating, planPath } = await import("./service-plan.js");
|
|
1802
|
+
// A refresh revokes any earlier plan first, whatever it comes to (a failed refresh leaves nothing to activate).
|
|
1803
|
+
planDeps().removeFile(planPath(DATA_DIR));
|
|
1804
|
+
const { expectedTuple } = await import("./restart-guard.js");
|
|
1805
|
+
const expectation = expectedTuple();
|
|
1806
|
+
if (!expectation.ok) {
|
|
1807
|
+
console.error(` ✗ ${expectation.reason}`);
|
|
1808
|
+
process.exitCode = 1;
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
const uid = process.getuid?.() ?? 501;
|
|
1812
|
+
const plistPath = launchdPlistPath(serviceVars.label);
|
|
1813
|
+
mkdirSync(dirname(plistPath), { recursive: true });
|
|
1814
|
+
const outcome = refreshLaunchdWithoutActivating({
|
|
1815
|
+
label: serviceVars.label, target: `gui/${uid}/${serviceVars.label}`, plistPath, newPlist: renderLaunchdPlist(serviceVars),
|
|
1816
|
+
expected: expectation.expected, pkgDir: dirname(dirname(execPath)), agendHome: DATA_DIR,
|
|
1817
|
+
}, planDeps());
|
|
1818
|
+
if (!outcome.ok) {
|
|
1819
|
+
console.error(` ✗ ${outcome.message}`);
|
|
1820
|
+
process.exitCode = 1;
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1823
|
+
console.log(t("install.path", plistPath));
|
|
1824
|
+
console.log(` ${outcome.message}`);
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1827
|
+
const svcPath = installService(serviceVars);
|
|
1685
1828
|
console.log(t("install.path", svcPath));
|
|
1686
1829
|
if (opts.activate !== false) {
|
|
1687
1830
|
const pidPath = join(DATA_DIR, "fleet.pid");
|
|
@@ -1816,7 +1959,9 @@ program
|
|
|
1816
1959
|
if (!userServicePath && !systemServicePath) {
|
|
1817
1960
|
console.log("No service installed. Starting fleet directly...");
|
|
1818
1961
|
const { spawn } = await import("node:child_process");
|
|
1819
|
-
|
|
1962
|
+
// This Node on this CLI, not `sh -c "agend fleet start"` through PATH (#1450 C5).
|
|
1963
|
+
const start = selfCommand(["fleet", "start"]);
|
|
1964
|
+
const child = spawn(start.command, start.args, { detached: true, stdio: "ignore" });
|
|
1820
1965
|
child.unref();
|
|
1821
1966
|
console.log("Fleet starting in background.");
|
|
1822
1967
|
return;
|
|
@@ -1837,9 +1982,52 @@ program
|
|
|
1837
1982
|
.command("restart")
|
|
1838
1983
|
.description("Restart the AgEnD service (auto-detects systemd/launchd/detached)")
|
|
1839
1984
|
.option("-y, --yes", "Confirm when run from a fleet agent session (this affects every instance)")
|
|
1985
|
+
.option("--force", "Restart even when the loaded service does not start this install on its selected Node (operators only)")
|
|
1840
1986
|
.action(async (opts) => {
|
|
1987
|
+
// --force overrides the C6 guard: an operator's own decision, never a fleet agent's or a fleet-internal spawn's.
|
|
1988
|
+
if (opts.force && !forceAllowed()) {
|
|
1989
|
+
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.");
|
|
1990
|
+
process.exit(1);
|
|
1991
|
+
}
|
|
1841
1992
|
if (!gateFleetControl(DATA_DIR, "restart", { yes: opts.yes }))
|
|
1842
1993
|
process.exit(1);
|
|
1994
|
+
// #1450 C6: what the restarted fleet must run, by this package's own selection (C2) — decided before anything stops.
|
|
1995
|
+
const guard = await import("./restart-guard.js");
|
|
1996
|
+
const guardDeps = {
|
|
1997
|
+
realpath: (path) => { try {
|
|
1998
|
+
return realpathSync(path);
|
|
1999
|
+
}
|
|
2000
|
+
catch {
|
|
2001
|
+
return null;
|
|
2002
|
+
} },
|
|
2003
|
+
isExecutable: (path) => { try {
|
|
2004
|
+
return statSync(path).isFile() && (statSync(path).mode & 0o111) !== 0;
|
|
2005
|
+
}
|
|
2006
|
+
catch {
|
|
2007
|
+
return false;
|
|
2008
|
+
} },
|
|
2009
|
+
};
|
|
2010
|
+
const captureRun = (command, args) => {
|
|
2011
|
+
const r = spawnSync(command, args, { encoding: "utf-8", timeout: 15_000, stdio: ["ignore", "pipe", "pipe"] });
|
|
2012
|
+
return { status: r.status, signal: r.signal, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
2013
|
+
};
|
|
2014
|
+
const expectation = guard.expectedTuple();
|
|
2015
|
+
/** Refuse (nothing stopped) unless the definition about to run is proven; `--force` overrides for an operator. */
|
|
2016
|
+
const refuses = (judged) => {
|
|
2017
|
+
if (judged.ok)
|
|
2018
|
+
return false;
|
|
2019
|
+
if (opts.force) {
|
|
2020
|
+
console.log(` ⚠ --force: ${judged.reason}; restarting anyway.`);
|
|
2021
|
+
return false;
|
|
2022
|
+
}
|
|
2023
|
+
console.error(` ✗ Not restarting: ${judged.reason}. Nothing was stopped.`);
|
|
2024
|
+
if (judged.recovery)
|
|
2025
|
+
console.error(` To repair: ${judged.recovery}`);
|
|
2026
|
+
process.exitCode = 1;
|
|
2027
|
+
return true;
|
|
2028
|
+
};
|
|
2029
|
+
if (!expectation.ok && refuses(expectation))
|
|
2030
|
+
return;
|
|
1843
2031
|
// Try each runtime environment in order and stop at the first that succeeds.
|
|
1844
2032
|
// Don't gate on fleet.pid: a fleet under systemd/launchd writes its pid in the
|
|
1845
2033
|
// service's own HOME, which may differ from what this command resolves (sudo,
|
|
@@ -1892,8 +2080,8 @@ program
|
|
|
1892
2080
|
const unitPath = systemdTarget.user ? getServicePath() : getSystemServicePath();
|
|
1893
2081
|
const reloadCmd = systemdTarget.user ? "systemctl --user daemon-reload" : "systemctl daemon-reload";
|
|
1894
2082
|
// #1113 rides the same migration: crash-dump, start-limit and
|
|
1895
|
-
// start-timeout directives are filled in alongside KillMode.
|
|
1896
|
-
//
|
|
2083
|
+
// start-timeout directives are filled in alongside KillMode. KillMode
|
|
2084
|
+
// and the loaded #1071 stop grace gate the restart; the others are informational.
|
|
1897
2085
|
// The drop-ins systemd applies to this unit, by its own account
|
|
1898
2086
|
// (DropInPaths), plus any on disk it has not loaded yet. Guessing the
|
|
1899
2087
|
// directories alone misses type-wide service.d and other load paths.
|
|
@@ -1951,6 +2139,23 @@ program
|
|
|
1951
2139
|
return;
|
|
1952
2140
|
}
|
|
1953
2141
|
}
|
|
2142
|
+
// Migration preserves operator overrides. Admission independently requires
|
|
2143
|
+
// the actual loaded budget for EVERY selected systemd target, even if its
|
|
2144
|
+
// unit file disappeared or no migration/classification was possible.
|
|
2145
|
+
if (hardening.TimeoutStopSec === "custom") {
|
|
2146
|
+
console.log(` ⚠ ${unitPath} sets its own TimeoutStopSec; left as is. A short grace can cut off busy CLI batches (#1071).`);
|
|
2147
|
+
}
|
|
2148
|
+
let loadedStopGrace = "";
|
|
2149
|
+
try {
|
|
2150
|
+
loadedStopGrace = execSync(`systemctl${scopeFlag} show -p TimeoutStopUSec --value ${systemdTarget.unit}`, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 5000 }).trim();
|
|
2151
|
+
}
|
|
2152
|
+
catch { /* unknown refuses */ }
|
|
2153
|
+
const milliseconds = systemdStopTimeoutMs(loadedStopGrace);
|
|
2154
|
+
if (milliseconds === null || milliseconds < FLEET_STOP_TIMEOUT_MS) {
|
|
2155
|
+
console.error(` ✗ Not restarting: systemd has ${loadedStopGrace || "unknown"} stop grace loaded; this fleet needs ${FLEET_STOP_TIMEOUT_MS / 1000}s. Set TimeoutStopSec to at least ${FLEET_STOP_TIMEOUT_MS / 1000}s in ${unitPath ?? systemdTarget.unit}, run \`${reloadCmd}\`, and retry (#1071).`);
|
|
2156
|
+
process.exitCode = 1;
|
|
2157
|
+
return;
|
|
2158
|
+
}
|
|
1954
2159
|
if (outcome === "added") {
|
|
1955
2160
|
console.log(` ✓ ${unitPath}: added KillMode=mixed (the fleet stops its CLIs; systemd no longer signals them all at once)`);
|
|
1956
2161
|
}
|
|
@@ -1958,7 +2163,7 @@ program
|
|
|
1958
2163
|
.filter(([key, state]) => key !== "KillMode" && (state === "added" || state === "upgraded"))
|
|
1959
2164
|
.map(([key]) => key);
|
|
1960
2165
|
if (updated.length > 0)
|
|
1961
|
-
console.log(` ✓ ${unitPath}: updated ${updated.join(", ")} (#1113: small crash dumps, bounded start, start limit)`);
|
|
2166
|
+
console.log(` ✓ ${unitPath}: updated ${updated.join(", ")} (#908/#1071/#1113: graceful stop, small crash dumps, bounded start, start limit)`);
|
|
1962
2167
|
// Re-judged after the reload, against what systemd now applies: an
|
|
1963
2168
|
// operator's own mask (anywhere — main file, unit or type drop-in, or a
|
|
1964
2169
|
// trailing empty assignment that restores the inherited one) is warned
|
|
@@ -1989,6 +2194,8 @@ program
|
|
|
1989
2194
|
}
|
|
1990
2195
|
if (filterCustom)
|
|
1991
2196
|
console.log(` ⚠ ${unitPath} sets its own CoredumpFilter; left as is. CoredumpFilter=0 keeps crash dumps to a few KB (#1113).`);
|
|
2197
|
+
if (expectation.ok && refuses(guard.guardSystemd(captureRun, systemdTarget.user, systemdTarget.unit, expectation.expected, guardDeps)))
|
|
2198
|
+
return;
|
|
1992
2199
|
// Clears the failed state AND the start-limit counter (#1113:
|
|
1993
2200
|
// StartLimitBurst=4 in 30min) for whichever unit this restart targets,
|
|
1994
2201
|
// so an operator's restart is never refused by the limit.
|
|
@@ -2015,62 +2222,177 @@ program
|
|
|
2015
2222
|
process.exitCode = 1;
|
|
2016
2223
|
return;
|
|
2017
2224
|
}
|
|
2018
|
-
// 3. launchd (macOS)
|
|
2225
|
+
// 3. launchd (macOS). "Loaded" is launchctl print's own answer (113 = not loaded); nothing here may fall through
|
|
2226
|
+
// to the detached restart once a job is known to be loaded.
|
|
2019
2227
|
if (plat === "macos") {
|
|
2020
2228
|
const uid = process.getuid?.() ?? 501;
|
|
2021
2229
|
const label = "com.agend.fleet";
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2230
|
+
const target = `gui/${uid}/${label}`;
|
|
2231
|
+
const deps = planDeps();
|
|
2232
|
+
const printed = deps.run("launchctl", ["print", target]);
|
|
2233
|
+
if (printed.status === 0) {
|
|
2234
|
+
const plistPath = getServicePath() ?? join(homedir(), "Library", "LaunchAgents", `${label}.plist`);
|
|
2235
|
+
// Path 2 (#1450 C6): a proven new plist waits on disk with its plan — exactly one bootout + bootstrap.
|
|
2236
|
+
const { readPlan, admitPlan, planPath } = await import("./service-plan.js");
|
|
2237
|
+
const plan = opts.force ? null : readPlan(DATA_DIR, deps);
|
|
2238
|
+
if (plan && expectation.ok) {
|
|
2239
|
+
const pkgDir = dirname(dirname(expectation.expected.entry));
|
|
2240
|
+
const admitted = admitPlan(plan, { target, expected: expectation.expected, pkgDir }, deps);
|
|
2241
|
+
if (!admitted.ok) {
|
|
2242
|
+
console.error(` ✗ Not activating the planned launchd job: ${admitted.reason}. Nothing was stopped.`);
|
|
2243
|
+
process.exitCode = 1;
|
|
2244
|
+
return;
|
|
2245
|
+
}
|
|
2246
|
+
const { activateService } = await import("./service-activation.js");
|
|
2247
|
+
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 }, {
|
|
2248
|
+
...deps,
|
|
2249
|
+
readFirstLine: path => deps.readFile(path)?.split("\n", 1)[0] ?? null,
|
|
2250
|
+
isExecutable: path => { try {
|
|
2251
|
+
return statSync(path).isFile() && (statSync(path).mode & 0o111) !== 0;
|
|
2252
|
+
}
|
|
2253
|
+
catch {
|
|
2254
|
+
return false;
|
|
2255
|
+
} },
|
|
2256
|
+
refresh: () => ({ status: 0, signal: null, stdout: "", stderr: "" }),
|
|
2257
|
+
restart: () => { throw new Error("planned launchd activation does not invoke restart"); },
|
|
2258
|
+
log: message => console.log(message),
|
|
2259
|
+
launchdPreimage: plan.preimage.plist,
|
|
2260
|
+
});
|
|
2261
|
+
deps.removeFile(planPath(DATA_DIR));
|
|
2262
|
+
if (outcome.ok) {
|
|
2263
|
+
console.log("Service restarted (launchd loaded the planned job).");
|
|
2264
|
+
return;
|
|
2265
|
+
}
|
|
2266
|
+
console.error(outcome.message);
|
|
2267
|
+
process.exitCode = 1;
|
|
2268
|
+
return;
|
|
2269
|
+
}
|
|
2270
|
+
// Path 1: an unchanged loaded job.
|
|
2271
|
+
if (expectation.ok && refuses(guard.guardLaunchd(captureRun, target, plistPath, deps.readFile, expectation.expected, guardDeps)))
|
|
2272
|
+
return;
|
|
2273
|
+
if (run(`launchctl kickstart -k ${target}`)) {
|
|
2026
2274
|
console.log("Service restarted.");
|
|
2027
2275
|
return;
|
|
2028
2276
|
}
|
|
2277
|
+
console.error(` ✗ launchctl kickstart -k ${target} failed.`);
|
|
2278
|
+
process.exitCode = 1;
|
|
2279
|
+
return;
|
|
2280
|
+
}
|
|
2281
|
+
const { LAUNCHD_NOT_LOADED } = await import("./service-plan.js");
|
|
2282
|
+
if (!LAUNCHD_NOT_LOADED.includes(printed.status ?? -1) || printed.signal !== null) {
|
|
2283
|
+
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.`);
|
|
2284
|
+
process.exitCode = 1;
|
|
2285
|
+
return;
|
|
2029
2286
|
}
|
|
2030
|
-
catch { /* not loaded — fall through */ }
|
|
2031
2287
|
}
|
|
2032
2288
|
// 4. Detached process tracked by fleet.pid (no service manager)
|
|
2033
2289
|
if (existsSync(pidPath)) {
|
|
2034
|
-
const
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
const isOurFleet = () => {
|
|
2290
|
+
const originalPidFile = readFileSync(pidPath, "utf-8");
|
|
2291
|
+
const oldPid = Number(originalPidFile.trim());
|
|
2292
|
+
let claimedStart = null;
|
|
2293
|
+
const absence = () => {
|
|
2039
2294
|
try {
|
|
2040
2295
|
process.kill(oldPid, 0);
|
|
2041
2296
|
}
|
|
2042
|
-
catch {
|
|
2043
|
-
return
|
|
2297
|
+
catch (error) {
|
|
2298
|
+
return error.code === "ESRCH" ? "gone" : "unknown";
|
|
2044
2299
|
}
|
|
2045
|
-
return
|
|
2300
|
+
return detachedProcessExited(oldPid) ? "gone" : null;
|
|
2046
2301
|
};
|
|
2047
|
-
|
|
2302
|
+
const inspect = () => {
|
|
2048
2303
|
try {
|
|
2049
|
-
|
|
2304
|
+
if (existsSync(pidPath) && readFileSync(pidPath, "utf-8") !== originalPidFile)
|
|
2305
|
+
return "unknown";
|
|
2050
2306
|
}
|
|
2051
|
-
catch {
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2307
|
+
catch {
|
|
2308
|
+
return "unknown";
|
|
2309
|
+
}
|
|
2310
|
+
const missing = absence();
|
|
2311
|
+
if (missing)
|
|
2312
|
+
return missing;
|
|
2313
|
+
const start = detachedProcessStart(oldPid);
|
|
2314
|
+
if (start === null)
|
|
2315
|
+
return absence() ?? "unknown";
|
|
2316
|
+
const command = readProcessCommandLine(oldPid);
|
|
2317
|
+
if (!command)
|
|
2318
|
+
return absence() ?? (claimedStart !== null && start === claimedStart ? "waiting" : "unknown");
|
|
2319
|
+
const fleet = isFleetStartCommandLine(command);
|
|
2320
|
+
if (claimedStart !== null && start !== claimedStart)
|
|
2321
|
+
return fleet ? "unknown" : "other";
|
|
2322
|
+
// The claimed process is still alive: rewritten argv is not exit proof.
|
|
2323
|
+
if (claimedStart !== null && !fleet)
|
|
2324
|
+
return "unknown";
|
|
2325
|
+
claimedStart ??= start;
|
|
2326
|
+
return fleet ? "fleet" : "other";
|
|
2327
|
+
};
|
|
2328
|
+
if (!Number.isSafeInteger(oldPid) || oldPid <= 1) {
|
|
2329
|
+
console.error(" ✗ Invalid detached fleet PID; nothing was stopped or started.");
|
|
2330
|
+
process.exitCode = 1;
|
|
2331
|
+
return;
|
|
2332
|
+
}
|
|
2333
|
+
const state = inspect();
|
|
2334
|
+
if (state === "unknown") {
|
|
2335
|
+
console.error(" ✗ Cannot confirm the detached fleet owner; nothing was stopped or started.");
|
|
2336
|
+
process.exitCode = 1;
|
|
2337
|
+
return;
|
|
2338
|
+
}
|
|
2339
|
+
if (state === "fleet") {
|
|
2340
|
+
if (expectation.ok && refuses(guard.guardDetached(process.execPath, expectation.expected, guardDeps)))
|
|
2341
|
+
return;
|
|
2342
|
+
console.log(`Stopping detached fleet (up to ${FLEET_STOP_TIMEOUT_MS / 60_000} minutes for graceful shutdown)...`);
|
|
2343
|
+
try {
|
|
2344
|
+
await stopDetachedOwner({
|
|
2345
|
+
inspect,
|
|
2346
|
+
signal: signal => {
|
|
2347
|
+
if (signal === "SIGKILL")
|
|
2348
|
+
console.log(" ⚠ Grace expired; forcing the confirmed fleet owner to exit.");
|
|
2349
|
+
// stdout can block. The helper's earlier proof cannot authorize a
|
|
2350
|
+
// signal after that write; prove the exact owner again now.
|
|
2351
|
+
const owner = inspect();
|
|
2352
|
+
if (owner === "gone" || owner === "other")
|
|
2353
|
+
return;
|
|
2354
|
+
if (owner !== "fleet")
|
|
2355
|
+
throw new Error("Detached fleet ownership changed before signalling; refusing the signal");
|
|
2356
|
+
try {
|
|
2357
|
+
process.kill(oldPid, signal);
|
|
2358
|
+
}
|
|
2359
|
+
catch (error) {
|
|
2360
|
+
if (error.code !== "ESRCH")
|
|
2361
|
+
throw error;
|
|
2362
|
+
}
|
|
2363
|
+
},
|
|
2364
|
+
now: () => performance.now(),
|
|
2365
|
+
sleep: ms => new Promise(resolve => setTimeout(resolve, ms)),
|
|
2366
|
+
});
|
|
2060
2367
|
}
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2368
|
+
catch (error) {
|
|
2369
|
+
console.error(` ✗ ${error.message}`);
|
|
2370
|
+
process.exitCode = 1;
|
|
2371
|
+
return;
|
|
2372
|
+
}
|
|
2373
|
+
if (expectation.ok && refuses(guard.guardDetached(process.execPath, expectation.expected, guardDeps)))
|
|
2374
|
+
return;
|
|
2375
|
+
const start = selfCommand(["fleet", "start"]);
|
|
2376
|
+
// The runtime guard and command preparation can read files. Recheck
|
|
2377
|
+
// publication AFTER both, immediately before cleanup/spawn. Unreadable
|
|
2378
|
+
// publication cannot authorize deleting a newly published owner's file.
|
|
2379
|
+
let publicationCurrent = false;
|
|
2380
|
+
try {
|
|
2381
|
+
publicationCurrent = readFileSync(pidPath, "utf-8") === originalPidFile;
|
|
2382
|
+
}
|
|
2383
|
+
catch (error) {
|
|
2384
|
+
publicationCurrent = error?.code === "ENOENT";
|
|
2385
|
+
}
|
|
2386
|
+
if (!publicationCurrent) {
|
|
2387
|
+
console.error(" ✗ Fleet owner changed or its publication is unreadable; refusing a duplicate restart.");
|
|
2388
|
+
process.exitCode = 1;
|
|
2389
|
+
return;
|
|
2068
2390
|
}
|
|
2069
2391
|
try {
|
|
2070
2392
|
unlinkSync(pidPath);
|
|
2071
2393
|
}
|
|
2072
|
-
catch { /*
|
|
2073
|
-
const child = spawn(
|
|
2394
|
+
catch { /* old owner already removed it */ }
|
|
2395
|
+
const child = spawn(start.command, start.args, { detached: true, stdio: "ignore" });
|
|
2074
2396
|
child.unref();
|
|
2075
2397
|
console.log("Fleet restarting in background.");
|
|
2076
2398
|
return;
|