@songsid/agend 2.1.12 → 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 +4 -2
- package/README.zh-TW.md +4 -2
- 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/auth/one-time-code.d.ts +19 -0
- package/dist/auth/one-time-code.js +49 -0
- package/dist/auth/one-time-code.js.map +1 -0
- package/dist/auth-api.d.ts +52 -0
- package/dist/auth-api.js +346 -0
- package/dist/auth-api.js.map +1 -0
- package/dist/backend/antigravity.js +4 -3
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-code.d.ts +4 -0
- package/dist/backend/claude-code.js +97 -0
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex.js +59 -30
- 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-agent.d.ts +66 -0
- package/dist/backend/kiro-agent.js +302 -0
- package/dist/backend/kiro-agent.js.map +1 -0
- package/dist/backend/kiro-identity.d.ts +80 -0
- package/dist/backend/kiro-identity.js +537 -0
- package/dist/backend/kiro-identity.js.map +1 -0
- package/dist/backend/kiro-v2-store-worker.d.ts +1 -0
- package/dist/backend/kiro-v2-store-worker.js +87 -0
- package/dist/backend/kiro-v2-store-worker.js.map +1 -0
- package/dist/backend/kiro-v2-store.d.ts +54 -0
- package/dist/backend/kiro-v2-store.js +131 -0
- package/dist/backend/kiro-v2-store.js.map +1 -0
- package/dist/backend/kiro-v3-identity.d.ts +5 -0
- package/dist/backend/kiro-v3-identity.js +22 -0
- package/dist/backend/kiro-v3-identity.js.map +1 -1
- package/dist/backend/kiro.d.ts +90 -7
- package/dist/backend/kiro.js +500 -124
- 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 +51 -3
- 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/channel/adapters/discord.d.ts +1 -0
- package/dist/channel/adapters/discord.js +29 -10
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +1 -0
- package/dist/channel/adapters/telegram.js +13 -1
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/types.d.ts +9 -1
- package/dist/classic-channel-manager.d.ts +18 -10
- package/dist/classic-channel-manager.js +45 -18
- package/dist/classic-channel-manager.js.map +1 -1
- 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 +471 -146
- package/dist/cli.js.map +1 -1
- package/dist/command-table.d.ts +5 -3
- package/dist/command-table.js +29 -27
- package/dist/command-table.js.map +1 -1
- package/dist/config-validator.js +46 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/config.js +12 -0
- package/dist/config.js.map +1 -1
- package/dist/connection-secrets.d.ts +6 -0
- package/dist/connection-secrets.js +4 -0
- package/dist/connection-secrets.js.map +1 -1
- package/dist/daemon.d.ts +95 -4
- package/dist/daemon.js +748 -111
- package/dist/daemon.js.map +1 -1
- package/dist/darwin-memory.d.ts +6 -1
- package/dist/darwin-memory.js +17 -6
- package/dist/darwin-memory.js.map +1 -1
- package/dist/delivery-consumed-watch.d.ts +85 -0
- package/dist/delivery-consumed-watch.js +244 -0
- package/dist/delivery-consumed-watch.js.map +1 -0
- package/dist/delivery-outbox.d.ts +61 -0
- package/dist/delivery-outbox.js +120 -2
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/delivery-reconciliation.d.ts +14 -5
- package/dist/delivery-reconciliation.js +27 -4
- package/dist/delivery-reconciliation.js.map +1 -1
- package/dist/delivery-reconciliation.test.js +35 -1
- package/dist/delivery-reconciliation.test.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/event-loop-watch.d.ts +28 -0
- package/dist/event-loop-watch.js +75 -3
- package/dist/event-loop-watch.js.map +1 -1
- package/dist/fleet-context.d.ts +22 -2
- 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 +2 -2
- package/dist/fleet-level-config.js +28 -1
- package/dist/fleet-level-config.js.map +1 -1
- package/dist/fleet-manager.d.ts +339 -12
- package/dist/fleet-manager.js +2623 -461
- 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/host-memory.d.ts +4 -0
- package/dist/host-memory.js.map +1 -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 +8 -2
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +10 -5
- package/dist/instance-lifecycle.js +162 -53
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +164 -16
- package/dist/locale.js.map +1 -1
- package/dist/login-controller.d.ts +2 -0
- package/dist/login-controller.js +17 -5
- package/dist/login-controller.js.map +1 -1
- package/dist/memory-pressure.d.ts +7 -8
- package/dist/memory-pressure.js +76 -15
- package/dist/memory-pressure.js.map +1 -1
- package/dist/monotonic-budget.d.ts +2 -0
- package/dist/monotonic-budget.js +28 -0
- package/dist/monotonic-budget.js.map +1 -0
- package/dist/needs-you-hub.d.ts +114 -0
- package/dist/needs-you-hub.js +370 -0
- package/dist/needs-you-hub.js.map +1 -0
- package/dist/needs-you-live.d.ts +116 -0
- package/dist/needs-you-live.js +228 -0
- package/dist/needs-you-live.js.map +1 -0
- package/dist/needs-you.d.ts +132 -0
- package/dist/needs-you.js +164 -0
- package/dist/needs-you.js.map +1 -0
- package/dist/node-version-guard.d.ts +30 -0
- package/dist/node-version-guard.js +50 -0
- package/dist/node-version-guard.js.map +1 -0
- package/dist/operator-control-paths.d.ts +4 -0
- package/dist/operator-control-paths.js +24 -0
- package/dist/operator-control-paths.js.map +1 -0
- package/dist/outbound-handlers.d.ts +2 -0
- package/dist/outbound-handlers.js +9 -2
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +5 -5
- 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/profile-control.js +4 -20
- package/dist/profile-control.js.map +1 -1
- package/dist/public-web-gateway.d.ts +18 -0
- package/dist/public-web-gateway.js +141 -0
- package/dist/public-web-gateway.js.map +1 -0
- package/dist/public-web-link.d.ts +53 -0
- package/dist/public-web-link.js +175 -0
- package/dist/public-web-link.js.map +1 -0
- package/dist/quickstart-api.d.ts +8 -1
- package/dist/quickstart-api.js +86 -48
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.d.ts +1 -0
- package/dist/quickstart.js +16 -6
- 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.d.ts +7 -1
- package/dist/scheduler/db.js +44 -2
- package/dist/scheduler/db.js.map +1 -1
- package/dist/scheduler/index.d.ts +1 -1
- package/dist/scheduler/retry-label.d.ts +11 -0
- package/dist/scheduler/retry-label.js +24 -0
- package/dist/scheduler/retry-label.js.map +1 -0
- package/dist/scheduler/scheduler.d.ts +55 -2
- package/dist/scheduler/scheduler.js +220 -8
- package/dist/scheduler/scheduler.js.map +1 -1
- package/dist/scheduler/types.d.ts +22 -0
- package/dist/scheduler/types.js.map +1 -1
- package/dist/secret-store.d.ts +5 -0
- package/dist/secret-store.js +20 -0
- package/dist/secret-store.js.map +1 -1
- package/dist/service-activation.d.ts +152 -0
- package/dist/service-activation.js +407 -0
- package/dist/service-activation.js.map +1 -0
- package/dist/service-installer.d.ts +63 -1
- package/dist/service-installer.js +141 -16
- 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/service-restart-selection.d.ts +6 -5
- package/dist/service-restart-selection.js +6 -5
- package/dist/service-restart-selection.js.map +1 -1
- package/dist/settings-api.d.ts +10 -2
- package/dist/settings-api.js +216 -72
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-baseline.d.ts +20 -0
- package/dist/settings-baseline.js +104 -0
- package/dist/settings-baseline.js.map +1 -0
- package/dist/settings-change.d.ts +21 -0
- package/dist/settings-change.js +161 -0
- package/dist/settings-change.js.map +1 -0
- package/dist/settings-confirmation.d.ts +105 -0
- package/dist/settings-confirmation.js +232 -0
- package/dist/settings-confirmation.js.map +1 -0
- package/dist/settings-control.d.ts +41 -0
- package/dist/settings-control.js +294 -0
- package/dist/settings-control.js.map +1 -0
- package/dist/settings-effect.d.ts +24 -0
- package/dist/settings-effect.js +169 -0
- package/dist/settings-effect.js.map +1 -0
- package/dist/settings-http-confirmation.d.ts +29 -0
- package/dist/settings-http-confirmation.js +123 -0
- package/dist/settings-http-confirmation.js.map +1 -0
- package/dist/settings-instance-patch.d.ts +4 -0
- package/dist/settings-instance-patch.js +26 -0
- package/dist/settings-instance-patch.js.map +1 -0
- package/dist/settings-request-capability.d.ts +16 -0
- package/dist/settings-request-capability.js +32 -0
- package/dist/settings-request-capability.js.map +1 -0
- package/dist/settings-transaction.d.ts +65 -0
- package/dist/settings-transaction.js +223 -0
- package/dist/settings-transaction.js.map +1 -0
- package/dist/setup-auth.d.ts +10 -18
- package/dist/setup-auth.js +14 -38
- package/dist/setup-auth.js.map +1 -1
- package/dist/setup-form.d.ts +1 -1
- package/dist/setup-form.js +75 -18
- package/dist/setup-form.js.map +1 -1
- package/dist/setup-guide.d.ts +13 -0
- package/dist/setup-guide.js +25 -0
- package/dist/setup-guide.js.map +1 -0
- package/dist/setup-host.d.ts +16 -0
- package/dist/setup-host.js +199 -35
- package/dist/setup-host.js.map +1 -1
- package/dist/setup-wizard.js +5 -1
- package/dist/setup-wizard.js.map +1 -1
- package/dist/slash-authz.d.ts +17 -3
- package/dist/slash-authz.js +5 -0
- package/dist/slash-authz.js.map +1 -1
- package/dist/spawn-gate.js +3 -3
- package/dist/spawn-gate.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/statusline-watcher.d.ts +5 -0
- package/dist/statusline-watcher.js +2 -0
- package/dist/statusline-watcher.js.map +1 -1
- package/dist/steer-capability.d.ts +6 -0
- package/dist/steer-capability.js +8 -0
- package/dist/steer-capability.js.map +1 -1
- package/dist/sync-work-attribution.d.ts +14 -0
- package/dist/sync-work-attribution.js +43 -8
- package/dist/sync-work-attribution.js.map +1 -1
- package/dist/tips.js +10 -5
- package/dist/tips.js.map +1 -1
- package/dist/tmux-control.d.ts +57 -5
- package/dist/tmux-control.js +379 -61
- package/dist/tmux-control.js.map +1 -1
- package/dist/tmux-manager.d.ts +20 -6
- package/dist/tmux-manager.js +90 -47
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tmux-read.d.ts +59 -0
- package/dist/tmux-read.js +213 -0
- package/dist/tmux-read.js.map +1 -0
- package/dist/topic-commands.d.ts +21 -10
- package/dist/topic-commands.js +205 -63
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-delta-reader.js +2 -1
- package/dist/transcript-delta-reader.js.map +1 -1
- package/dist/tunnel/cloudflared-install.d.ts +2 -0
- package/dist/tunnel/cloudflared-install.js +1 -1
- package/dist/tunnel/cloudflared-install.js.map +1 -1
- package/dist/tunnel/purpose-lane.d.ts +14 -0
- package/dist/tunnel/purpose-lane.js +43 -0
- package/dist/tunnel/purpose-lane.js.map +1 -0
- package/dist/turn-reply-guard.d.ts +2 -0
- package/dist/turn-reply-guard.js +10 -1
- package/dist/turn-reply-guard.js.map +1 -1
- package/dist/types.d.ts +33 -0
- package/dist/ui/app.html +18 -0
- package/dist/ui/chat-commands.js +43 -0
- package/dist/ui/chat-render.js +478 -0
- package/dist/ui/chat-store.js +318 -0
- package/dist/ui/chat-strings.js +89 -0
- package/dist/ui/chat-thread.js +246 -0
- package/dist/ui/chat-tour.js +116 -0
- package/dist/ui/fleet-cache.js +168 -0
- package/dist/ui/fleet-org.js +142 -0
- package/dist/ui/panel-chat.js +504 -0
- package/dist/ui/panel-fleet.js +427 -0
- package/dist/ui/panel-needs.js +202 -0
- package/dist/ui/panel-settings.js +754 -0
- package/dist/ui/preview-panel.js +202 -0
- package/dist/ui/preview.js +270 -0
- package/dist/ui/settings-apply.js +239 -0
- package/dist/ui/settings-confirm.js +186 -0
- package/dist/ui/settings-dialogs.js +546 -0
- package/dist/ui/settings-model.js +277 -0
- package/dist/ui/settings-strings.js +654 -0
- package/dist/ui/settings-wizard.js +140 -0
- package/dist/ui/shared/agend-auth.js +93 -0
- package/dist/ui/shared/app-ctx.js +80 -0
- package/dist/ui/shared/app-html.js +9 -0
- package/dist/ui/shared/app-i18n.js +95 -0
- package/dist/ui/shared/app-nav.js +73 -0
- package/dist/ui/shared/app-needs.js +114 -0
- package/dist/ui/shared/app-route.js +73 -0
- package/dist/ui/shared/app-session.js +66 -0
- package/dist/ui/shared/app-shell.js +346 -0
- package/dist/ui/shared/app-store.js +51 -0
- package/dist/ui/shared/app-stream.js +201 -0
- package/dist/ui/shared/app.css +820 -0
- package/dist/ui/shared/app.js +88 -0
- package/dist/ui/shared/fonts/OFL.txt +92 -0
- package/dist/ui/shared/fonts/README.md +21 -0
- package/dist/ui/shared/fonts/inter.woff2 +0 -0
- package/dist/ui/shared/htm.module.js +1 -0
- package/dist/ui/shared/panel-view.js +655 -0
- package/dist/ui/shared/preact-hooks.module.js +2 -0
- package/dist/ui/shared/preact.module.js +2 -0
- package/dist/ui/shared/signin.js +142 -0
- package/dist/ui/shared/theme.js +29 -0
- package/dist/ui/shared/tokens.css +153 -0
- package/dist/ui/shared/ui-confirm.js +43 -0
- package/dist/ui/shared/ui-dialog.js +45 -0
- package/dist/ui/shared/ui-icons.js +63 -0
- package/dist/ui/shared/ui-menu.js +39 -0
- package/dist/ui/shared/ui-states.js +20 -0
- package/dist/ui/shared/ui-toast.js +18 -0
- package/dist/ui/shared/vendor/htm.LICENSE +202 -0
- package/dist/ui/shared/vendor/preact.LICENSE +21 -0
- package/dist/ui/shared/vendor/vendor.json +39 -0
- package/dist/ui/shared/view-strings.js +309 -0
- package/dist/ui/signin.html +52 -0
- package/dist/update-check.d.ts +6 -2
- package/dist/update-check.js +12 -3
- package/dist/update-check.js.map +1 -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 +129 -0
- package/dist/update-install.js +262 -0
- package/dist/update-install.js.map +1 -0
- package/dist/upgrade-notices.d.ts +17 -0
- package/dist/upgrade-notices.js +93 -0
- package/dist/upgrade-notices.js.map +1 -0
- package/dist/usage/statusline-usage.d.ts +2 -0
- package/dist/usage/statusline-usage.js +14 -10
- package/dist/usage/statusline-usage.js.map +1 -1
- package/dist/view-api.d.ts +6 -1
- package/dist/view-api.js +62 -51
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +85 -14
- package/dist/web-api.js +557 -77
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.d.ts +71 -27
- package/dist/web-auth.js +169 -69
- package/dist/web-auth.js.map +1 -1
- package/dist/web-body.d.ts +3 -0
- package/dist/web-body.js +55 -0
- package/dist/web-body.js.map +1 -0
- package/dist/web-channel-echo.d.ts +9 -0
- package/dist/web-channel-echo.js +55 -0
- package/dist/web-channel-echo.js.map +1 -0
- package/dist/web-chat-history.d.ts +125 -0
- package/dist/web-chat-history.js +184 -0
- package/dist/web-chat-history.js.map +1 -0
- 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-continuation.d.ts +7 -0
- package/dist/web-continuation.js +22 -0
- package/dist/web-continuation.js.map +1 -0
- package/dist/web-host-guard.d.ts +34 -0
- package/dist/web-host-guard.js +57 -1
- package/dist/web-host-guard.js.map +1 -1
- package/dist/web-login.d.ts +72 -0
- package/dist/web-login.js +132 -0
- package/dist/web-login.js.map +1 -0
- package/dist/web-org.d.ts +55 -0
- package/dist/web-org.js +67 -0
- package/dist/web-org.js.map +1 -0
- package/dist/web-preview.d.ts +77 -0
- package/dist/web-preview.js +240 -0
- package/dist/web-preview.js.map +1 -0
- package/dist/web-request-context.d.ts +10 -0
- package/dist/web-request-context.js +13 -0
- package/dist/web-request-context.js.map +1 -0
- package/dist/web-session.d.ts +167 -0
- package/dist/web-session.js +389 -0
- package/dist/web-session.js.map +1 -0
- package/dist/web-shell-routes.d.ts +59 -0
- package/dist/web-shell-routes.js +96 -0
- package/dist/web-shell-routes.js.map +1 -0
- package/dist/web-upload.d.ts +183 -0
- package/dist/web-upload.js +468 -0
- package/dist/web-upload.js.map +1 -0
- package/dist/website-tips-renderer.d.ts +2 -2
- package/dist/website-tips-renderer.js +3 -3
- package/dist/website-tips-renderer.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 +18 -7
- package/scripts/preinstall-guard.cjs +55 -0
- package/templates/launchd.plist.ejs +2 -2
- package/templates/systemd.service.ejs +1 -1
- package/dist/ui/dashboard.html +0 -774
- package/dist/ui/settings.html +0 -2165
- package/dist/ui/view.html +0 -1139
package/dist/cli.js
CHANGED
|
@@ -4,20 +4,28 @@ import { applyNetworkReliabilityDefaults } from "./network-family.js";
|
|
|
4
4
|
// Preserve Node's dual-stack Happy Eyeballs behavior, but avoid its 250ms
|
|
5
5
|
// premature-connect failure on WSL/VPN/high-load hosts (#658).
|
|
6
6
|
applyNetworkReliabilityDefaults();
|
|
7
|
+
// ── Runtime version guard ───────────────────────────────────────────────────
|
|
8
|
+
// better-sqlite3 v13 requires Node-API 10 (>=22.14.0 / >=23.6.0 / >=24).
|
|
9
|
+
// This MUST run at bootstrap so that even `agend --version` exits 1 on an
|
|
10
|
+
// incompatible runtime. The old updater checks `agend --version` before
|
|
11
|
+
// restarting; a non-zero exit makes it call failUpdate() and abort — the fleet
|
|
12
|
+
// never restarts into a binary that would crash at the first native DB open.
|
|
13
|
+
import { checkNodeVersion } from "./node-version-guard.js";
|
|
14
|
+
checkNodeVersion();
|
|
7
15
|
import { Command } from "commander";
|
|
8
16
|
import { join, dirname } from "node:path";
|
|
9
17
|
import { SchedulerDb } from "./scheduler/db.js";
|
|
10
18
|
import { Cron } from "croner";
|
|
11
|
-
import { existsSync, readFileSync, writeFileSync, appendFileSync, unlinkSync, mkdirSync, chmodSync, readdirSync, rmSync, statSync, } from "node:fs";
|
|
19
|
+
import { existsSync, readFileSync, writeFileSync, appendFileSync, unlinkSync, mkdirSync, chmodSync, readdirSync, rmSync, statSync, realpathSync, } from "node:fs";
|
|
12
20
|
import { homedir, totalmem, freemem, userInfo } from "node:os";
|
|
13
21
|
import { fileURLToPath } from "node:url";
|
|
14
|
-
import { spawn, execSync, execFileSync } from "node:child_process";
|
|
22
|
+
import { spawn, spawnSync, execSync, execFileSync } from "node:child_process";
|
|
15
23
|
import { getAgendHome, getTmuxSocketName } from "./paths.js";
|
|
16
24
|
import { hasPausedMarker } from "./pause-marker.js";
|
|
17
25
|
import { COMPLETION_SHELLS, completionScript } from "./completion.js";
|
|
18
26
|
import { getUpdateSelector, isExactVersion, isUnrequestedDowngrade, lookupTargetVersion, reportUpdateRestart, shouldSkipUpdate, runningFleetPredatesInstall, processStartMs, } from "./update-check.js";
|
|
19
27
|
import { clearUpdateMarker, markUpdateInProgress, setUpdateProgressStage } from "./update-marker.js";
|
|
20
|
-
import { describeSignalSource, gateFleetControl, recordInstanceControl, withOrigin } from "./fleet-control-audit.js";
|
|
28
|
+
import { describeSignalSource, forceAllowed, gateFleetControl, recordInstanceControl, withOrigin } from "./fleet-control-audit.js";
|
|
21
29
|
import { acquireFleetLock, isFleetStartCommandLine, readProcessCommandLine, releaseProcessFleetLock, setProcessFleetLock } from "./fleet-lock.js";
|
|
22
30
|
import { limitFleetCoreDumps } from "./coredump-filter.js";
|
|
23
31
|
import { SYSTEMD_RESTART_TIMEOUT_MS } from "./service-installer.js";
|
|
@@ -27,6 +35,8 @@ import { setLocale, t } from "./locale.js";
|
|
|
27
35
|
import { DeliveryOutbox, deliveryStatusSelector } from "./delivery-outbox.js";
|
|
28
36
|
import { DeliveryStatusArgs } from "./outbound-schemas.js";
|
|
29
37
|
import { requestCpuProfile } from "./profile-control.js";
|
|
38
|
+
import { requestSettingsConfirmation } from "./settings-control.js";
|
|
39
|
+
import { canonicalCliEntry, selfCommand } from "./cli-entry.js";
|
|
30
40
|
/** Prefix tmux args with -L when socket isolation is active. */
|
|
31
41
|
function tmuxArgs(args) {
|
|
32
42
|
const socket = getTmuxSocketName();
|
|
@@ -35,6 +45,39 @@ function tmuxArgs(args) {
|
|
|
35
45
|
const __filename = fileURLToPath(import.meta.url);
|
|
36
46
|
const __dirname = dirname(__filename);
|
|
37
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
|
+
}
|
|
38
81
|
const FLEET_CONFIG_PATH = join(DATA_DIR, "fleet.yaml");
|
|
39
82
|
// CLI commands run outside FleetManager. Honor an explicit fleet locale while
|
|
40
83
|
// keeping the historical English CLI default when config is missing/unset.
|
|
@@ -90,6 +133,47 @@ function signalFleetReload() {
|
|
|
90
133
|
}
|
|
91
134
|
// === Fleet commands ===
|
|
92
135
|
const fleet = program.command("fleet").description("Fleet management");
|
|
136
|
+
const settingsCommand = program.command("settings").description("Confirm sensitive Settings changes on this host");
|
|
137
|
+
settingsCommand.command("confirm <id>").option("--yes", "Explicit noninteractive confirmation after printing the authoritative diff")
|
|
138
|
+
.action(async (id, options) => {
|
|
139
|
+
try {
|
|
140
|
+
const inspected = await requestSettingsConfirmation(DATA_DIR, { action: "inspect", id });
|
|
141
|
+
console.log(`Source: ${inspected.pending_change.source}\nRequester: ${inspected.pending_change.requested_by}\n${inspected.pending_change.summary.join("\n")}`);
|
|
142
|
+
let confirmed = options.yes === true;
|
|
143
|
+
if (!confirmed) {
|
|
144
|
+
if (!process.stdin.isTTY)
|
|
145
|
+
throw new Error("Interactive confirmation required; --yes explicitly confirms the printed diff.");
|
|
146
|
+
const { createInterface } = await import("node:readline/promises");
|
|
147
|
+
const input = createInterface({ input: process.stdin, output: process.stdout });
|
|
148
|
+
try {
|
|
149
|
+
confirmed = (await input.question("Apply this exact change? [y/N] ")).trim().toLowerCase() === "y";
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
input.close();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const result = await requestSettingsConfirmation(DATA_DIR, { action: confirmed ? "confirm" : "reject", ticket: inspected.ticket });
|
|
156
|
+
console.log(result.pending_change.outcome?.message ?? result.pending_change.state);
|
|
157
|
+
if (result.pending_change.state !== "applied" && confirmed)
|
|
158
|
+
process.exitCode = 1;
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
console.error(err.message);
|
|
162
|
+
process.exitCode = 1;
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
settingsCommand.command("reject <id>").action(async (id) => {
|
|
166
|
+
try {
|
|
167
|
+
const inspected = await requestSettingsConfirmation(DATA_DIR, { action: "inspect", id });
|
|
168
|
+
console.log(`Source: ${inspected.pending_change.source}\nRequester: ${inspected.pending_change.requested_by}\n${inspected.pending_change.summary.join("\n")}`);
|
|
169
|
+
const result = await requestSettingsConfirmation(DATA_DIR, { action: "reject", ticket: inspected.ticket });
|
|
170
|
+
console.log(result.pending_change.outcome?.message ?? result.pending_change.state);
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
console.error(err.message);
|
|
174
|
+
process.exitCode = 1;
|
|
175
|
+
}
|
|
176
|
+
});
|
|
93
177
|
program.command("profile")
|
|
94
178
|
.description("Record the running fleet's CPU profile (local operator only; no restart)")
|
|
95
179
|
.argument("[seconds]", "Recording duration, 1–1800 seconds", "60")
|
|
@@ -223,6 +307,7 @@ fleet
|
|
|
223
307
|
}
|
|
224
308
|
});
|
|
225
309
|
await fm.startCpuProfileControl().catch(err => console.warn(`Local profile control unavailable: ${String(err)}`));
|
|
310
|
+
await fm.startSettingsConfirmationControl().catch(err => console.warn(`Local Settings confirmation unavailable: ${String(err)}`));
|
|
226
311
|
if (stopping)
|
|
227
312
|
return;
|
|
228
313
|
cpuProfile = await fm.startEnvironmentCpuProfile();
|
|
@@ -956,13 +1041,20 @@ backend
|
|
|
956
1041
|
const svcPath = join(homedir(), ".config/systemd/user/com.agend.fleet.service");
|
|
957
1042
|
if (existsSync(svcPath)) {
|
|
958
1043
|
const svc = readFileSync(svcPath, "utf-8");
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
const
|
|
962
|
-
|
|
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)
|
|
963
1055
|
ok(`service ExecStart${" ".repeat(3)} matches current binary`);
|
|
964
1056
|
else
|
|
965
|
-
fail(`service ExecStart${" ".repeat(3)} ${
|
|
1057
|
+
fail(`service ExecStart${" ".repeat(3)} ${svcEntry || "(none)"} ≠ ${current}`);
|
|
966
1058
|
// Check Restart=on-failure
|
|
967
1059
|
if (svc.includes("Restart=on-failure")) {
|
|
968
1060
|
ok(`service restart${" ".repeat(5)} Restart=on-failure configured`);
|
|
@@ -1377,7 +1469,9 @@ program
|
|
|
1377
1469
|
// part of it, not a second command for the agent-session guard.
|
|
1378
1470
|
env: withOrigin("agend-update"),
|
|
1379
1471
|
});
|
|
1380
|
-
|
|
1472
|
+
const restartOutcome = reportUpdateRestart(restartResult.status);
|
|
1473
|
+
// "pending": systemd is still running the restart job; the replacement fleet settles the marker.
|
|
1474
|
+
if (restartOutcome === "failed") {
|
|
1381
1475
|
// No new fleet is coming up to clear the marker — do it here, or the next
|
|
1382
1476
|
// 15 minutes of genuine crashes would go unreported.
|
|
1383
1477
|
if (!setUpdateProgressStage(DATA_DIR, "failed", { error: "fleet restart failed" })) {
|
|
@@ -1385,6 +1479,118 @@ program
|
|
|
1385
1479
|
}
|
|
1386
1480
|
process.exitCode = 1;
|
|
1387
1481
|
}
|
|
1482
|
+
return restartOutcome;
|
|
1483
|
+
};
|
|
1484
|
+
const nvmSh = join(homedir(), ".nvm", "nvm.sh");
|
|
1485
|
+
/**
|
|
1486
|
+
* Activate a VERIFIED install (#1449 review), on both the update path and the already-installed retry: refresh the
|
|
1487
|
+
* service through the verified binary, require the authoritative service definition to start code from the
|
|
1488
|
+
* verified package, then restart through the verified binary — never through whatever invoked this command
|
|
1489
|
+
* (process.argv[1] may be another checkout). A failed target check leaves the running fleet alone.
|
|
1490
|
+
*/
|
|
1491
|
+
const activateVerified = async (verified, viaNvm) => {
|
|
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;
|
|
1495
|
+
const { activateService } = await import("./service-activation.js");
|
|
1496
|
+
const { getServicePath, getSystemServicePath, detectPlatform } = await import("./service-installer.js");
|
|
1497
|
+
const { accessSync, constants } = await import("node:fs");
|
|
1498
|
+
const newAgend = newAgendInvocation({ viaNvm, nvmSh }, verified.agendPath);
|
|
1499
|
+
// The authoritative manager, chosen as `agend restart` chooses: system unit > user unit / launchd plist > none.
|
|
1500
|
+
const systemUnit = getSystemServicePath();
|
|
1501
|
+
const ownService = systemUnit ? null : getServicePath();
|
|
1502
|
+
const manager = systemUnit ? { kind: "systemd", unit: "agend", user: false, unitPath: systemUnit }
|
|
1503
|
+
: ownService && detectPlatform() === "macos" ? { kind: "launchd", label: "com.agend.fleet", plistPath: ownService, domain: `gui/${process.getuid?.() ?? 501}` }
|
|
1504
|
+
: ownService ? { kind: "systemd", unit: "com.agend.fleet", user: true, unitPath: ownService }
|
|
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;
|
|
1513
|
+
const capture = (command, args) => {
|
|
1514
|
+
const result = spawnSync(command, args, { encoding: "utf-8", timeout: 15_000, stdio: ["ignore", "pipe", "pipe"] });
|
|
1515
|
+
return { status: result.status, signal: result.signal, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
1516
|
+
};
|
|
1517
|
+
const outcome = activateService(manager, verified, {
|
|
1518
|
+
run: capture,
|
|
1519
|
+
readFile: path => { try {
|
|
1520
|
+
return readFileSync(path, "utf-8");
|
|
1521
|
+
}
|
|
1522
|
+
catch {
|
|
1523
|
+
return null;
|
|
1524
|
+
} },
|
|
1525
|
+
writeFile: (path, content) => writeFileSync(path, content),
|
|
1526
|
+
realpath: path => { try {
|
|
1527
|
+
return realpathSync(path);
|
|
1528
|
+
}
|
|
1529
|
+
catch {
|
|
1530
|
+
return null;
|
|
1531
|
+
} },
|
|
1532
|
+
readFirstLine: path => { try {
|
|
1533
|
+
return readFileSync(path, "utf-8").split("\n", 1)[0] ?? null;
|
|
1534
|
+
}
|
|
1535
|
+
catch {
|
|
1536
|
+
return null;
|
|
1537
|
+
} },
|
|
1538
|
+
isExecutable: path => { try {
|
|
1539
|
+
accessSync(path, constants.X_OK);
|
|
1540
|
+
return statSync(path).isFile();
|
|
1541
|
+
}
|
|
1542
|
+
catch {
|
|
1543
|
+
return false;
|
|
1544
|
+
} },
|
|
1545
|
+
// `agend install` activates by default; the update owns stop/start, so only the file is refreshed here.
|
|
1546
|
+
refresh: () => {
|
|
1547
|
+
const result = capture(newAgend.command, [...newAgend.args, "install", "--no-activate"]);
|
|
1548
|
+
if (result.status === 0)
|
|
1549
|
+
console.log(" ✓ Service file refreshed");
|
|
1550
|
+
return result;
|
|
1551
|
+
},
|
|
1552
|
+
// Through the verified binary: never this process's code, which may be the old version or another checkout.
|
|
1553
|
+
restart: () => {
|
|
1554
|
+
// The completion script embeds this version's subcommand names; --refresh only rewrites existing artifacts.
|
|
1555
|
+
try {
|
|
1556
|
+
spawnSync(newAgend.command, [...newAgend.args, "completion", "install", "--refresh"], { encoding: "utf-8", timeout: 15_000, stdio: "ignore" });
|
|
1557
|
+
}
|
|
1558
|
+
catch { /* cosmetic */ }
|
|
1559
|
+
restartResult = restartFleetForUpdate(newAgend.command, newAgend.args, verified.version);
|
|
1560
|
+
},
|
|
1561
|
+
log: message => console.log(message),
|
|
1562
|
+
restorePackage,
|
|
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
|
+
}
|
|
1583
|
+
if (outcome.ok && outcome.via === "launchd-activation") {
|
|
1584
|
+
console.log(" ✓ Service restarted (launchd loaded the verified job)");
|
|
1585
|
+
// The new fleet clears the update marker once it is up.
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
if (!outcome.ok) {
|
|
1589
|
+
console.error(outcome.message);
|
|
1590
|
+
if (!setUpdateProgressStage(DATA_DIR, "failed", { error: outcome.message.trim() }))
|
|
1591
|
+
clearUpdateMarker(DATA_DIR);
|
|
1592
|
+
process.exitCode = 1;
|
|
1593
|
+
}
|
|
1388
1594
|
};
|
|
1389
1595
|
if (shouldSkipUpdate(pkgVersion, targetVersion, opts.force)) {
|
|
1390
1596
|
// Installed already — but is the running fleet? An update whose restart
|
|
@@ -1407,9 +1613,27 @@ program
|
|
|
1407
1613
|
processStartMs: pid => processStartMs(pid),
|
|
1408
1614
|
isFleetProcess: pid => isFleetStartCommandLine(readProcessCommandLine(pid)),
|
|
1409
1615
|
})) {
|
|
1410
|
-
console.log(`\n
|
|
1616
|
+
console.log(`\n v${pkgVersion} is installed, but the running fleet started before it was — verifying it before restarting onto it.`);
|
|
1617
|
+
// The earlier update may have stopped exactly because this install failed verification (#1449 review): a
|
|
1618
|
+
// second `agend update` must not restart onto it unchecked.
|
|
1619
|
+
const { verifyInstalledPackage } = await import("./update-install.js");
|
|
1620
|
+
const verified = verifyInstalledPackage({ pkg: `@songsid/agend@${pkgVersion}`, targetVersion: pkgVersion, viaNvm: false, nvmSh }, {
|
|
1621
|
+
run: (command, args, options = {}) => {
|
|
1622
|
+
const result = spawnSync(command, args, { encoding: "utf-8", timeout: options.timeoutMs, stdio: ["ignore", "pipe", "pipe"] });
|
|
1623
|
+
return { status: result.status, signal: result.signal, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
1624
|
+
},
|
|
1625
|
+
log: message => console.log(message),
|
|
1626
|
+
});
|
|
1627
|
+
if (!verified.ok) {
|
|
1628
|
+
const message = `${verified.message}\n Not restarting the fleet onto it. Reinstall a working version: npm install -g @songsid/agend@<version>`;
|
|
1629
|
+
console.error(message);
|
|
1630
|
+
setUpdateProgressStage(DATA_DIR, "failed", { error: message.trim() });
|
|
1631
|
+
process.exitCode = 1;
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
console.log(` ✓ v${pkgVersion} verified — restarting the fleet onto it.\n`);
|
|
1411
1635
|
markUpdateInProgress(DATA_DIR);
|
|
1412
|
-
|
|
1636
|
+
await activateVerified(verified, false);
|
|
1413
1637
|
return;
|
|
1414
1638
|
}
|
|
1415
1639
|
console.log(`\n ✓ Already up to date (v${pkgVersion})\n`);
|
|
@@ -1445,24 +1669,6 @@ program
|
|
|
1445
1669
|
process.exit(1);
|
|
1446
1670
|
};
|
|
1447
1671
|
setUpdateProgressStage(DATA_DIR, "downloading");
|
|
1448
|
-
// Detect and remove stale npm link (local build that shadows global install)
|
|
1449
|
-
try {
|
|
1450
|
-
const agendPath = execSync("which agend", { encoding: "utf-8", stdio: "pipe" }).trim();
|
|
1451
|
-
const resolved = execSync(`readlink -f "${agendPath}"`, { encoding: "utf-8", stdio: "pipe" }).trim();
|
|
1452
|
-
if (resolved.includes("/Projects/") || resolved.includes("@songsid") || resolved.includes("@suzuke") || resolved.includes("/src/")) {
|
|
1453
|
-
console.log(` ⚠️ Detected local npm link: ${resolved}`);
|
|
1454
|
-
console.log(" Removing link to allow global install...\n");
|
|
1455
|
-
try {
|
|
1456
|
-
execSync("npm unlink -g @suzuke/agend", { stdio: "pipe", timeout: 15000 });
|
|
1457
|
-
}
|
|
1458
|
-
catch { }
|
|
1459
|
-
try {
|
|
1460
|
-
execSync("npm unlink -g @songsid/agend", { stdio: "pipe", timeout: 15000 });
|
|
1461
|
-
}
|
|
1462
|
-
catch { }
|
|
1463
|
-
}
|
|
1464
|
-
}
|
|
1465
|
-
catch { /* which/readlink failed — no agend installed, fine */ }
|
|
1466
1672
|
// ── Check if npm global needs sudo ──
|
|
1467
1673
|
let needsSudo = false;
|
|
1468
1674
|
try {
|
|
@@ -1478,8 +1684,6 @@ program
|
|
|
1478
1684
|
catch { /* assume no sudo needed */ }
|
|
1479
1685
|
if (needsSudo) {
|
|
1480
1686
|
// ── nvm path: install without sudo ──
|
|
1481
|
-
const nvmDir = join(homedir(), ".nvm");
|
|
1482
|
-
const nvmSh = join(nvmDir, "nvm.sh");
|
|
1483
1687
|
if (!existsSync(nvmSh)) {
|
|
1484
1688
|
console.log(" Installing nvm (npm global requires sudo)...");
|
|
1485
1689
|
try {
|
|
@@ -1490,89 +1694,56 @@ program
|
|
|
1490
1694
|
}
|
|
1491
1695
|
}
|
|
1492
1696
|
console.log(" Using nvm to install Node 22...");
|
|
1493
|
-
const nvmPrefix = `source ${nvmSh} && nvm install 22 && nvm use 22`;
|
|
1494
|
-
try {
|
|
1495
|
-
execSync(`bash -c '${nvmPrefix} && npm install -g ${pkg}'`, { stdio: "inherit" });
|
|
1496
|
-
}
|
|
1497
|
-
catch {
|
|
1498
|
-
failUpdate(" Failed to install via nvm.");
|
|
1499
|
-
}
|
|
1500
|
-
// Try to remove old system binary
|
|
1501
|
-
console.log(" Note: removing old system install (may require sudo)...");
|
|
1502
|
-
// Never wait for a password in an agent/non-interactive terminal. This is
|
|
1503
|
-
// best-effort cleanup; the new nvm install already succeeded.
|
|
1504
|
-
spawnSync("sudo", ["-n", "npm", "uninstall", "-g", "@songsid/agend"], {
|
|
1505
|
-
stdio: "inherit",
|
|
1506
|
-
timeout: 10_000,
|
|
1507
|
-
});
|
|
1508
|
-
}
|
|
1509
|
-
else {
|
|
1510
|
-
// ── Direct install ──
|
|
1511
|
-
try {
|
|
1512
|
-
execSync(`npm install -g ${pkg}`, { stdio: "inherit" });
|
|
1513
|
-
}
|
|
1514
|
-
catch {
|
|
1515
|
-
failUpdate(` Failed to update. Try: npm install -g ${pkg}`);
|
|
1516
|
-
}
|
|
1517
|
-
}
|
|
1518
|
-
// ── Verify installation ──
|
|
1519
|
-
console.log("\n Verifying installation...");
|
|
1520
|
-
const nvmSh2 = join(homedir(), ".nvm", "nvm.sh");
|
|
1521
|
-
const agendPath = needsSudo
|
|
1522
|
-
? spawnSync("bash", ["-c", `source ${nvmSh2} && nvm use 22 > /dev/null 2>&1 && which agend`], { encoding: "utf-8" }).stdout?.trim()
|
|
1523
|
-
: spawnSync("which", ["agend"], { encoding: "utf-8" }).stdout?.trim();
|
|
1524
|
-
if (!agendPath) {
|
|
1525
|
-
if (needsSudo)
|
|
1526
|
-
console.error(" You may need to add nvm to your shell profile and restart.");
|
|
1527
|
-
failUpdate(" ✗ Verification failed: agend not found in PATH after install.");
|
|
1528
|
-
}
|
|
1529
|
-
const verifyResult = spawnSync(agendPath, ["--version"], { encoding: "utf-8", timeout: 5000 });
|
|
1530
|
-
if (verifyResult.status !== 0) {
|
|
1531
|
-
failUpdate(" ✗ Verification failed: agend --version returned error.");
|
|
1532
|
-
}
|
|
1533
|
-
const newVersion = (verifyResult.stdout ?? "").trim();
|
|
1534
|
-
console.log(` ✓ Installed: ${newVersion}`);
|
|
1535
|
-
setUpdateProgressStage(DATA_DIR, "installed", { version: newVersion.replace(/^v/, "") });
|
|
1536
|
-
// ── Update service file ──
|
|
1537
|
-
if (agendPath) {
|
|
1538
|
-
try {
|
|
1539
|
-
// Use the NEW binary to install service (old binary's templates may be deleted)
|
|
1540
|
-
// `agend install` activates by default. During update the existing
|
|
1541
|
-
// restart stage owns stop/start and progress reporting, so only refresh
|
|
1542
|
-
// the service file here to avoid restarting the fleet twice.
|
|
1543
|
-
const installResult = spawnSync(agendPath, ["install", "--no-activate"], { encoding: "utf-8", timeout: 15000 });
|
|
1544
|
-
if (installResult.status === 0) {
|
|
1545
|
-
console.log(` ✓ Service updated`);
|
|
1546
|
-
}
|
|
1547
|
-
else {
|
|
1548
|
-
console.log(` ⚠ Service file update failed (non-fatal): ${(installResult.stderr || installResult.stdout || "unknown error").trim()}`);
|
|
1549
|
-
}
|
|
1550
|
-
}
|
|
1551
|
-
catch (e) {
|
|
1552
|
-
console.log(` ⚠ Service file update failed (non-fatal): ${e.message}`);
|
|
1553
|
-
}
|
|
1554
|
-
}
|
|
1555
|
-
// ── Refresh installed shell completions ──
|
|
1556
|
-
// The completion script embeds this version's subcommand names, so a
|
|
1557
|
-
// previously installed static file goes stale on update. --refresh only
|
|
1558
|
-
// rewrites artifacts that already exist — an update never starts
|
|
1559
|
-
// installing completions the user didn't ask for. Run through the NEW
|
|
1560
|
-
// binary so the regenerated names are the new version's.
|
|
1561
|
-
try {
|
|
1562
|
-
spawnSync(agendPath, ["completion", "install", "--refresh"], {
|
|
1563
|
-
encoding: "utf-8",
|
|
1564
|
-
timeout: 15_000,
|
|
1565
|
-
stdio: "ignore",
|
|
1566
|
-
});
|
|
1567
1697
|
}
|
|
1568
|
-
|
|
1569
|
-
//
|
|
1570
|
-
//
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
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
|
+
]);
|
|
1705
|
+
const { runUpdateInstall } = await import("./update-install.js");
|
|
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
|
+
}, {
|
|
1727
|
+
run: (command, args, options = {}) => {
|
|
1728
|
+
if (options.inherit)
|
|
1729
|
+
console.log("");
|
|
1730
|
+
const result = spawnSync(command, args, {
|
|
1731
|
+
encoding: "utf-8",
|
|
1732
|
+
timeout: options.timeoutMs,
|
|
1733
|
+
stdio: options.inherit ? "inherit" : ["ignore", "pipe", "pipe"],
|
|
1734
|
+
...(options.env ? { env: { ...process.env, ...options.env } } : {}),
|
|
1735
|
+
});
|
|
1736
|
+
return { status: result.status, signal: result.signal, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
1737
|
+
},
|
|
1738
|
+
log: message => console.log(message),
|
|
1739
|
+
});
|
|
1740
|
+
if (!installed.ok)
|
|
1741
|
+
return failUpdate(installed.message);
|
|
1742
|
+
const agendPath = installed.agendPath;
|
|
1743
|
+
const newVersion = installed.version;
|
|
1744
|
+
console.log(`\n ✓ Installed: v${newVersion} (${agendPath}; opened a database on its Node)`);
|
|
1745
|
+
setUpdateProgressStage(DATA_DIR, "installed", { version: newVersion });
|
|
1746
|
+
await activateVerified(installed, needsSudo);
|
|
1576
1747
|
});
|
|
1577
1748
|
program
|
|
1578
1749
|
.command("reload")
|
|
@@ -1600,15 +1771,45 @@ program
|
|
|
1600
1771
|
.option("--activate", "Deprecated; service activation is now the default")
|
|
1601
1772
|
.option("--no-activate", "Write the service file without activating it")
|
|
1602
1773
|
.action(async (opts) => {
|
|
1603
|
-
const { installService, activateService, detectPlatform } = await import("./service-installer.js");
|
|
1604
|
-
const execPath =
|
|
1605
|
-
const
|
|
1774
|
+
const { installService, activateService, detectPlatform, renderLaunchdPlist, launchdPlistPath } = await import("./service-installer.js");
|
|
1775
|
+
const execPath = canonicalCliEntry();
|
|
1776
|
+
const serviceVars = {
|
|
1606
1777
|
label: "com.agend.fleet",
|
|
1607
1778
|
execPath,
|
|
1608
1779
|
path: process.env.PATH,
|
|
1609
1780
|
workingDirectory: DATA_DIR,
|
|
1610
1781
|
logPath: join(DATA_DIR, "fleet.log"),
|
|
1611
|
-
}
|
|
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);
|
|
1612
1813
|
console.log(t("install.path", svcPath));
|
|
1613
1814
|
if (opts.activate !== false) {
|
|
1614
1815
|
const pidPath = join(DATA_DIR, "fleet.pid");
|
|
@@ -1743,7 +1944,9 @@ program
|
|
|
1743
1944
|
if (!userServicePath && !systemServicePath) {
|
|
1744
1945
|
console.log("No service installed. Starting fleet directly...");
|
|
1745
1946
|
const { spawn } = await import("node:child_process");
|
|
1746
|
-
|
|
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" });
|
|
1747
1950
|
child.unref();
|
|
1748
1951
|
console.log("Fleet starting in background.");
|
|
1749
1952
|
return;
|
|
@@ -1764,15 +1967,56 @@ program
|
|
|
1764
1967
|
.command("restart")
|
|
1765
1968
|
.description("Restart the AgEnD service (auto-detects systemd/launchd/detached)")
|
|
1766
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)")
|
|
1767
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
|
+
}
|
|
1768
1977
|
if (!gateFleetControl(DATA_DIR, "restart", { yes: opts.yes }))
|
|
1769
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;
|
|
1770
2014
|
// Try each runtime environment in order and stop at the first that succeeds.
|
|
1771
2015
|
// Don't gate on fleet.pid: a fleet under systemd/launchd writes its pid in the
|
|
1772
2016
|
// service's own HOME, which may differ from what this command resolves (sudo,
|
|
1773
2017
|
// AGEND_HOME) — an absent pid file does NOT mean it's stopped. Ask the service
|
|
1774
2018
|
// manager first. This is the canonical restart the `update` flow spawns.
|
|
1775
|
-
const { detectPlatform, getServicePath, getSystemServicePath, getSystemdServiceState,
|
|
2019
|
+
const { detectPlatform, getServicePath, getSystemServicePath, getSystemdServiceState, systemdRestartOutcome, ensureSystemdUnitHardening, effectiveUnitDropIns, unitCoredumpFilterState, } = await import("./service-installer.js");
|
|
1776
2020
|
const { spawn, spawnSync } = await import("node:child_process");
|
|
1777
2021
|
const plat = detectPlatform();
|
|
1778
2022
|
const pidPath = join(DATA_DIR, "fleet.pid");
|
|
@@ -1916,6 +2160,8 @@ program
|
|
|
1916
2160
|
}
|
|
1917
2161
|
if (filterCustom)
|
|
1918
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;
|
|
1919
2165
|
// Clears the failed state AND the start-limit counter (#1113:
|
|
1920
2166
|
// StartLimitBurst=4 in 30min) for whichever unit this restart targets,
|
|
1921
2167
|
// so an operator's restart is never refused by the limit.
|
|
@@ -1923,30 +2169,87 @@ program
|
|
|
1923
2169
|
execSync(`systemctl${systemdTarget.user ? " --user" : ""} reset-failed ${systemdTarget.unit}`, { stdio: "pipe", timeout: 5000 });
|
|
1924
2170
|
}
|
|
1925
2171
|
catch { /* best effort */ }
|
|
1926
|
-
|
|
2172
|
+
const restartOutcome = systemdRestartOutcome(systemdTarget.unit, systemdTarget.user);
|
|
2173
|
+
if (restartOutcome === "restarted") {
|
|
1927
2174
|
console.log(systemdTarget.user ? "Service restarted (user)." : "Service restarted (system).");
|
|
1928
2175
|
return;
|
|
1929
2176
|
}
|
|
1930
2177
|
// An installed service remains authoritative even when this invocation
|
|
1931
2178
|
// fails. Falling through would create a second detached fleet.
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
2179
|
+
if (restartOutcome === "timed-out") {
|
|
2180
|
+
// The job may still finish: the replacement fleet settles the update marker, not this command.
|
|
2181
|
+
console.log(` ⚠ ${scope} restart is still running after ${Math.round(SYSTEMD_RESTART_TIMEOUT_MS / 60_000)} minutes; systemd keeps the job going.`);
|
|
2182
|
+
console.log(` Check: ${statusCommand}`);
|
|
2183
|
+
process.exitCode = SYSTEMD_RESTART_INDETERMINATE_EXIT_CODE;
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
2186
|
+
console.error(` ✗ ${scope} restart failed (systemctl reported an error). systemd may retry it on its own.`);
|
|
2187
|
+
console.error(` Check: ${statusCommand}`);
|
|
2188
|
+
process.exitCode = 1;
|
|
1935
2189
|
return;
|
|
1936
2190
|
}
|
|
1937
|
-
// 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.
|
|
1938
2193
|
if (plat === "macos") {
|
|
1939
2194
|
const uid = process.getuid?.() ?? 501;
|
|
1940
2195
|
const label = "com.agend.fleet";
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
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}`)) {
|
|
1945
2240
|
console.log("Service restarted.");
|
|
1946
2241
|
return;
|
|
1947
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;
|
|
1948
2252
|
}
|
|
1949
|
-
catch { /* not loaded — fall through */ }
|
|
1950
2253
|
}
|
|
1951
2254
|
// 4. Detached process tracked by fleet.pid (no service manager)
|
|
1952
2255
|
if (existsSync(pidPath)) {
|
|
@@ -1964,6 +2267,9 @@ program
|
|
|
1964
2267
|
return isFleetStartCommandLine(readProcessCommandLine(oldPid));
|
|
1965
2268
|
};
|
|
1966
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;
|
|
1967
2273
|
try {
|
|
1968
2274
|
process.kill(oldPid, "SIGTERM");
|
|
1969
2275
|
}
|
|
@@ -1989,7 +2295,9 @@ program
|
|
|
1989
2295
|
unlinkSync(pidPath);
|
|
1990
2296
|
}
|
|
1991
2297
|
catch { /* best effort */ }
|
|
1992
|
-
|
|
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" });
|
|
1993
2301
|
child.unref();
|
|
1994
2302
|
console.log("Fleet restarting in background.");
|
|
1995
2303
|
return;
|
|
@@ -2115,8 +2423,9 @@ program
|
|
|
2115
2423
|
});
|
|
2116
2424
|
program
|
|
2117
2425
|
.command("web")
|
|
2118
|
-
.description("Open the Web UI
|
|
2119
|
-
.
|
|
2426
|
+
.description("Open the Web UI sign-in page in your browser and print a one-time sign-in code")
|
|
2427
|
+
.option("--code", "Only print the sign-in page and code; do not open a browser")
|
|
2428
|
+
.action(async (opts) => {
|
|
2120
2429
|
const tokenPath = join(DATA_DIR, "web.token");
|
|
2121
2430
|
if (!existsSync(tokenPath)) {
|
|
2122
2431
|
console.error("Web token not found. Is the fleet running?");
|
|
@@ -2126,20 +2435,36 @@ program
|
|
|
2126
2435
|
const { loadFleetConfig } = await import("./config.js");
|
|
2127
2436
|
const fleet = loadFleetConfig(FLEET_CONFIG_PATH);
|
|
2128
2437
|
const port = fleet.health_port ?? 19280;
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
//
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2438
|
+
// The code is minted by the running fleet (it lives in that process's memory), so ask it — with
|
|
2439
|
+
// the header token, the one credential a local script has. No credential ever goes into a URL:
|
|
2440
|
+
// the browser is opened on the sign-in page and the code is typed there.
|
|
2441
|
+
let code;
|
|
2442
|
+
let minutes;
|
|
2443
|
+
try {
|
|
2444
|
+
const resp = await fetch(`http://127.0.0.1:${port}/auth/issue-code`, {
|
|
2445
|
+
method: "POST",
|
|
2446
|
+
headers: { "X-Agend-Token": token },
|
|
2447
|
+
signal: AbortSignal.timeout(5000),
|
|
2448
|
+
});
|
|
2449
|
+
const body = await resp.json().catch(() => ({}));
|
|
2450
|
+
if (!resp.ok || !body.code) {
|
|
2451
|
+
console.error(`Could not get a sign-in code: ${body.error ?? resp.statusText}`);
|
|
2452
|
+
process.exit(1);
|
|
2453
|
+
}
|
|
2454
|
+
code = body.code;
|
|
2455
|
+
minutes = Math.max(1, Math.round(((body.expiresAt ?? Date.now()) - Date.now()) / 60_000));
|
|
2456
|
+
}
|
|
2457
|
+
catch {
|
|
2458
|
+
console.error(`Cannot connect to fleet (port ${port}). Is the fleet running?`);
|
|
2459
|
+
process.exit(1);
|
|
2460
|
+
}
|
|
2461
|
+
const signin = `http://localhost:${port}/signin`;
|
|
2462
|
+
console.log(`Sign-in page: ${signin}`);
|
|
2463
|
+
console.log(`Code: ${code} (works once, valid ~${minutes} min)`);
|
|
2464
|
+
if (opts.code)
|
|
2465
|
+
return;
|
|
2141
2466
|
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
|
|
2142
|
-
const child = spawn(cmd, [
|
|
2467
|
+
const child = spawn(cmd, [signin], { detached: true, stdio: "ignore" });
|
|
2143
2468
|
child.unref();
|
|
2144
2469
|
});
|
|
2145
2470
|
program
|
|
@@ -2158,7 +2483,7 @@ program
|
|
|
2158
2483
|
console.log("Web token rotated.");
|
|
2159
2484
|
console.log("Every previously issued dashboard link and browser session is now rejected.");
|
|
2160
2485
|
console.log("A running fleet picks this up immediately — no restart needed.");
|
|
2161
|
-
console.log("
|
|
2486
|
+
console.log("Sign in again with `agend web` (prints a one-time code), or /dashboard in your chat channel.");
|
|
2162
2487
|
});
|
|
2163
2488
|
program
|
|
2164
2489
|
.command("view")
|