@songsid/agend 2.2.0 → 2.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-cli-instructions.md +5 -2
- package/dist/agent-cli.js +284 -2
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.d.ts +2 -0
- package/dist/agent-endpoint.js +29 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-file-read.d.ts +38 -0
- package/dist/agent-file-read.js +317 -0
- package/dist/agent-file-read.js.map +1 -0
- package/dist/agent-whats-new.d.ts +11 -0
- package/dist/agent-whats-new.js +34 -2
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/atomic-write.js +9 -1
- package/dist/atomic-write.js.map +1 -1
- package/dist/auth-api.d.ts +30 -1
- package/dist/auth-api.js +61 -12
- package/dist/auth-api.js.map +1 -1
- package/dist/authority/authority-state.d.ts +38 -0
- package/dist/authority/authority-state.js +114 -0
- package/dist/authority/authority-state.js.map +1 -0
- package/dist/authority/session-authorizations.d.ts +43 -0
- package/dist/authority/session-authorizations.js +102 -0
- package/dist/authority/session-authorizations.js.map +1 -0
- package/dist/authority/web-authority.d.ts +120 -0
- package/dist/authority/web-authority.js +157 -0
- package/dist/authority/web-authority.js.map +1 -0
- package/dist/backend/antigravity.js +3 -3
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-code.d.ts +2 -2
- package/dist/backend/claude-code.js +10 -5
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/launch-effort.js +1 -1
- package/dist/backend/launch-effort.js.map +1 -1
- package/dist/backend/muse.js +10 -2
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +14 -0
- package/dist/backend/types.js +25 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/backend-availability.d.ts +120 -0
- package/dist/backend-availability.js +396 -0
- package/dist/backend-availability.js.map +1 -0
- package/dist/backend-command.d.ts +21 -3
- package/dist/backend-command.js +23 -4
- package/dist/backend-command.js.map +1 -1
- package/dist/bundled-skills.d.ts +14 -0
- package/dist/bundled-skills.js +29 -0
- package/dist/bundled-skills.js.map +1 -0
- package/dist/cache-analysis.d.ts +47 -1
- package/dist/cache-analysis.js +68 -8
- package/dist/cache-analysis.js.map +1 -1
- package/dist/cache-ledger.d.ts +16 -3
- package/dist/cache-ledger.js +33 -13
- package/dist/cache-ledger.js.map +1 -1
- package/dist/cache-report-tool.d.ts +17 -0
- package/dist/cache-report-tool.js +40 -0
- package/dist/cache-report-tool.js.map +1 -0
- package/dist/cache-service.d.ts +70 -3
- package/dist/cache-service.js +178 -17
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/adapters/discord-client-retirement.d.ts +5 -0
- package/dist/channel/adapters/discord-client-retirement.js +57 -0
- package/dist/channel/adapters/discord-client-retirement.js.map +1 -0
- package/dist/channel/adapters/discord.d.ts +11 -0
- package/dist/channel/adapters/discord.js +66 -6
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +8 -1
- package/dist/channel/adapters/telegram.js +74 -4
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-tools.js +48 -2
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/types.d.ts +21 -0
- package/dist/classic-channel-manager.d.ts +10 -5
- package/dist/classic-channel-manager.js +12 -9
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli.js +142 -110
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +119 -4
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.d.ts +10 -0
- package/dist/daemon.js +327 -31
- package/dist/daemon.js.map +1 -1
- package/dist/decision-list.d.ts +25 -0
- package/dist/decision-list.js +30 -0
- package/dist/decision-list.js.map +1 -0
- package/dist/delivery-outbox.d.ts +10 -0
- package/dist/delivery-outbox.js +12 -1
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/fleet-context.d.ts +15 -0
- package/dist/fleet-manager.d.ts +514 -12
- package/dist/fleet-manager.js +1885 -163
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-process-handlers.d.ts +18 -0
- package/dist/fleet-process-handlers.js +69 -0
- package/dist/fleet-process-handlers.js.map +1 -0
- package/dist/fleet-traffic.d.ts +74 -0
- package/dist/fleet-traffic.js +196 -0
- package/dist/fleet-traffic.js.map +1 -0
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +20 -5
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +6 -1
- package/dist/general-knowledge/skills/multi-channel/SKILL.md +3 -2
- package/dist/general-knowledge/skills/persona-emoji/SKILL.md +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +492 -0
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +482 -0
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
- package/dist/human-delivery-receipts.d.ts +24 -1
- package/dist/human-delivery-receipts.js +64 -11
- package/dist/human-delivery-receipts.js.map +1 -1
- package/dist/instance-avatar.js +3 -0
- package/dist/instance-avatar.js.map +1 -1
- package/dist/instance-config-impact.js +6 -1
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +2 -0
- package/dist/instance-lifecycle.js +20 -10
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instance-names.d.ts +4 -0
- package/dist/instance-names.js +12 -0
- package/dist/instance-names.js.map +1 -0
- package/dist/instructions.d.ts +12 -1
- package/dist/instructions.js +13 -2
- package/dist/instructions.js.map +1 -1
- package/dist/locale.js +100 -8
- package/dist/locale.js.map +1 -1
- package/dist/ls-attach-resolve.d.ts +95 -0
- package/dist/ls-attach-resolve.js +174 -0
- package/dist/ls-attach-resolve.js.map +1 -0
- package/dist/memory-hogs.d.ts +27 -0
- package/dist/memory-hogs.js +124 -0
- package/dist/memory-hogs.js.map +1 -0
- package/dist/needs-you-hub.d.ts +4 -0
- package/dist/needs-you-hub.js +31 -6
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you.d.ts +1 -13
- package/dist/needs-you.js +3 -37
- package/dist/needs-you.js.map +1 -1
- package/dist/outbound-handlers.d.ts +15 -0
- package/dist/outbound-handlers.js +152 -11
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +228 -4
- package/dist/outbound-schemas.js +147 -1
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +21 -1
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +9 -0
- package/dist/public-web-link.js +8 -0
- package/dist/public-web-link.js.map +1 -1
- package/dist/quickstart-api.d.ts +10 -1
- package/dist/quickstart-api.js +12 -4
- package/dist/quickstart-api.js.map +1 -1
- package/dist/reply-buttons-controller.d.ts +4 -0
- package/dist/reply-buttons-controller.js +2 -2
- package/dist/reply-buttons-controller.js.map +1 -1
- package/dist/repo-worktree-guard.d.ts +36 -0
- package/dist/repo-worktree-guard.js +143 -0
- package/dist/repo-worktree-guard.js.map +1 -0
- package/dist/restart-progress.d.ts +65 -1
- package/dist/restart-progress.js +236 -10
- package/dist/restart-progress.js.map +1 -1
- package/dist/rollout-index.d.ts +5 -0
- package/dist/rollout-index.js +26 -5
- package/dist/rollout-index.js.map +1 -1
- package/dist/scheduler/scheduler.d.ts +70 -1
- package/dist/scheduler/scheduler.js +256 -17
- package/dist/scheduler/scheduler.js.map +1 -1
- package/dist/service-installer.d.ts +2 -1
- package/dist/service-installer.js +18 -3
- package/dist/service-installer.js.map +1 -1
- package/dist/settings-api.d.ts +23 -0
- package/dist/settings-api.js +233 -2
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +112 -0
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +32 -2
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.js +5 -0
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/tmux-manager.d.ts +5 -0
- package/dist/tmux-manager.js +16 -0
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tool-permissions.d.ts +4 -6
- package/dist/tool-permissions.js +100 -2
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +17 -7
- package/dist/topic-commands.js +39 -21
- package/dist/topic-commands.js.map +1 -1
- package/dist/topic-link.d.ts +128 -0
- package/dist/topic-link.js +269 -0
- package/dist/topic-link.js.map +1 -0
- package/dist/types.d.ts +61 -1
- package/dist/ui/app-effects.js +245 -0
- package/dist/ui/chat-ambient.js +307 -0
- package/dist/ui/chat-effect-posters.js +42 -0
- package/dist/ui/chat-effects.js +160 -0
- package/dist/ui/chat-live-replies.js +58 -0
- package/dist/ui/chat-render.js +25 -7
- package/dist/ui/chat-store.js +41 -13
- package/dist/ui/chat-strings.js +86 -2
- package/dist/ui/chat-thread.js +204 -37
- package/dist/ui/chat-tour.js +1 -1
- package/dist/ui/effect-trial-controller.js +124 -0
- package/dist/ui/effect-trial-offer.js +31 -0
- package/dist/ui/effect-trial-panel.js +40 -0
- package/dist/ui/fleet-cache.js +23 -1
- package/dist/ui/fleet-canvas-layout.js +111 -0
- package/dist/ui/fleet-canvas.js +525 -0
- package/dist/ui/fleet-org.js +27 -12
- package/dist/ui/fleet-traffic.js +343 -0
- package/dist/ui/fleet-usage.js +401 -0
- package/dist/ui/image-lightbox.js +1 -1
- package/dist/ui/panel-chat.js +252 -58
- package/dist/ui/panel-details.js +20 -6
- package/dist/ui/panel-fleet.js +45 -19
- package/dist/ui/panel-log.js +6 -6
- package/dist/ui/panel-needs.js +3 -3
- package/dist/ui/panel-settings.js +58 -13
- package/dist/ui/preview-panel.js +25 -9
- package/dist/ui/preview.js +3 -2
- package/dist/ui/settings-appearance.js +349 -0
- package/dist/ui/settings-catalog.js +92 -0
- package/dist/ui/settings-dialogs.js +40 -12
- package/dist/ui/settings-effects.js +202 -0
- package/dist/ui/settings-guests.js +117 -0
- package/dist/ui/settings-learn.js +25 -0
- package/dist/ui/settings-model.js +15 -6
- package/dist/ui/settings-mods.js +271 -0
- package/dist/ui/settings-owner-effects.js +80 -0
- package/dist/ui/settings-strings.js +335 -10
- package/dist/ui/settings-web-authority.js +91 -0
- package/dist/ui/shared/agend-auth.js +28 -4
- package/dist/ui/shared/agent-widget.js +248 -0
- package/dist/ui/shared/app-i18n.js +6 -2
- package/dist/ui/shared/app-prefs.js +34 -13
- package/dist/ui/shared/app-route.js +42 -2
- package/dist/ui/shared/app-session.js +3 -1
- package/dist/ui/shared/app-shell.js +119 -41
- package/dist/ui/shared/app-store.js +28 -1
- package/dist/ui/shared/app-stream.js +14 -7
- package/dist/ui/shared/app-themes.js +206 -0
- package/dist/ui/shared/app.css +566 -5
- package/dist/ui/shared/app.js +23 -10
- package/dist/ui/shared/card-strings.js +32 -0
- package/dist/ui/shared/effect-app-runtime.js +297 -0
- package/dist/ui/shared/effect-baseline.js +64 -0
- package/dist/ui/shared/effect-chat-runtime.js +167 -0
- package/dist/ui/shared/effect-cursor.js +125 -0
- package/dist/ui/shared/effect-frame.js +146 -0
- package/dist/ui/shared/effect-geometry.js +137 -0
- package/dist/ui/shared/effect-host-style.js +10 -0
- package/dist/ui/shared/effect-owner-regions.js +74 -0
- package/dist/ui/shared/effect-owner-source.js +122 -0
- package/dist/ui/shared/effect-policy.js +151 -0
- package/dist/ui/shared/effect-poster-reader.js +98 -0
- package/dist/ui/shared/effect-posters.js +117 -0
- package/dist/ui/shared/effect-profiler.js +184 -0
- package/dist/ui/shared/effect-profiles.js +65 -0
- package/dist/ui/shared/effect-registry.js +543 -0
- package/dist/ui/shared/effect-runtime-loader.js +234 -0
- package/dist/ui/shared/effect-safety.js +153 -0
- package/dist/ui/shared/effect-stock-snapshot.js +43 -0
- package/dist/ui/shared/effect-stock-trust.js +217 -0
- package/dist/ui/shared/effect-trial-shadow.js +62 -0
- package/dist/ui/shared/effect-trial-source.js +103 -0
- package/dist/ui/shared/effect-trial-stage.js +170 -0
- package/dist/ui/shared/effect-trial.css +40 -0
- package/dist/ui/shared/fx-motion.js +67 -0
- package/dist/ui/shared/header-tools.js +1 -1
- package/dist/ui/shared/identity-card.js +273 -0
- package/dist/ui/shared/identity-color.js +261 -0
- package/dist/ui/shared/instance-nav.js +215 -41
- package/dist/ui/shared/owner-css.js +250 -0
- package/dist/ui/shared/owner-effect-prefs.js +117 -0
- package/dist/ui/shared/panel-view.js +103 -10
- package/dist/ui/shared/signin.js +2 -0
- package/dist/ui/shared/tokens.css +9 -3
- package/dist/ui/shared/ui-dialog.js +1 -1
- package/dist/ui/shared/ui-icons.js +3 -0
- package/dist/ui/shared/ui-menu.js +1 -1
- package/dist/ui/shared/ui-toast.js +1 -1
- package/dist/ui/shared/usage-dialog.js +22 -2
- package/dist/ui/shared/view-strings.js +98 -2
- package/dist/ui/shared/web-authorize.js +151 -0
- package/dist/ui/signin.html +1 -0
- package/dist/ui/web-terminal/terminal.css +4 -4
- package/dist/usage/providers.d.ts +28 -0
- package/dist/usage/providers.js +126 -36
- package/dist/usage/providers.js.map +1 -1
- package/dist/usage-report.d.ts +158 -0
- package/dist/usage-report.js +226 -0
- package/dist/usage-report.js.map +1 -0
- package/dist/usage-rollup.d.ts +51 -0
- package/dist/usage-rollup.js +168 -0
- package/dist/usage-rollup.js.map +1 -0
- package/dist/view-api.d.ts +68 -0
- package/dist/view-api.js +249 -8
- package/dist/view-api.js.map +1 -1
- package/dist/web-ambient.d.ts +72 -0
- package/dist/web-ambient.js +161 -0
- package/dist/web-ambient.js.map +1 -0
- package/dist/web-api.d.ts +117 -7
- package/dist/web-api.js +1283 -70
- package/dist/web-api.js.map +1 -1
- package/dist/web-appearance-limits.d.ts +82 -0
- package/dist/web-appearance-limits.js +83 -0
- package/dist/web-appearance-limits.js.map +1 -0
- package/dist/web-appearance-policy.d.ts +64 -0
- package/dist/web-appearance-policy.js +87 -0
- package/dist/web-appearance-policy.js.map +1 -0
- package/dist/web-appearance-tools.d.ts +53 -0
- package/dist/web-appearance-tools.js +124 -0
- package/dist/web-appearance-tools.js.map +1 -0
- package/dist/web-appearance-view.d.ts +124 -0
- package/dist/web-appearance-view.js +278 -0
- package/dist/web-appearance-view.js.map +1 -0
- package/dist/web-appearance.d.ts +93 -0
- package/dist/web-appearance.js +373 -0
- package/dist/web-appearance.js.map +1 -0
- package/dist/web-asset-parts.d.ts +93 -0
- package/dist/web-asset-parts.js +133 -0
- package/dist/web-asset-parts.js.map +1 -0
- package/dist/web-auth.js +20 -2
- package/dist/web-auth.js.map +1 -1
- package/dist/web-card.d.ts +110 -0
- package/dist/web-card.js +266 -0
- package/dist/web-card.js.map +1 -0
- package/dist/web-channel-echo.d.ts +7 -1
- package/dist/web-channel-echo.js +6 -2
- package/dist/web-channel-echo.js.map +1 -1
- package/dist/web-chat-history.d.ts +63 -0
- package/dist/web-chat-history.js +109 -6
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-commands.d.ts +1 -0
- package/dist/web-effect-bootstrap.d.ts +12 -0
- package/dist/web-effect-bootstrap.js +49 -0
- package/dist/web-effect-bootstrap.js.map +1 -0
- package/dist/web-effect-bundle.d.ts +28 -0
- package/dist/web-effect-bundle.js +171 -0
- package/dist/web-effect-bundle.js.map +1 -0
- package/dist/web-effect-canonical.d.ts +8 -0
- package/dist/web-effect-canonical.js +27 -0
- package/dist/web-effect-canonical.js.map +1 -0
- package/dist/web-effect-library.d.ts +114 -0
- package/dist/web-effect-library.js +530 -0
- package/dist/web-effect-library.js.map +1 -0
- package/dist/web-effect-playback.d.ts +30 -0
- package/dist/web-effect-playback.js +71 -0
- package/dist/web-effect-playback.js.map +1 -0
- package/dist/web-effect-poster.d.ts +8 -0
- package/dist/web-effect-poster.js +52 -0
- package/dist/web-effect-poster.js.map +1 -0
- package/dist/web-effect-profiles.d.ts +118 -0
- package/dist/web-effect-profiles.js +180 -0
- package/dist/web-effect-profiles.js.map +1 -0
- package/dist/web-effect-service.d.ts +93 -0
- package/dist/web-effect-service.js +335 -0
- package/dist/web-effect-service.js.map +1 -0
- package/dist/web-effect-tools.d.ts +48 -0
- package/dist/web-effect-tools.js +129 -0
- package/dist/web-effect-tools.js.map +1 -0
- package/dist/web-effect-trials.d.ts +86 -0
- package/dist/web-effect-trials.js +204 -0
- package/dist/web-effect-trials.js.map +1 -0
- package/dist/web-effect-upload.d.ts +27 -0
- package/dist/web-effect-upload.js +185 -0
- package/dist/web-effect-upload.js.map +1 -0
- package/dist/web-effect-worker.d.ts +8 -0
- package/dist/web-effect-worker.js +170 -0
- package/dist/web-effect-worker.js.map +1 -0
- package/dist/web-effect.d.ts +103 -0
- package/dist/web-effect.js +153 -0
- package/dist/web-effect.js.map +1 -0
- package/dist/web-guest-access.d.ts +125 -0
- package/dist/web-guest-access.js +234 -0
- package/dist/web-guest-access.js.map +1 -0
- package/dist/web-guest-rooms.d.ts +82 -0
- package/dist/web-guest-rooms.js +135 -0
- package/dist/web-guest-rooms.js.map +1 -0
- package/dist/web-identity-color.d.ts +87 -0
- package/dist/web-identity-color.js +209 -0
- package/dist/web-identity-color.js.map +1 -0
- package/dist/web-identity-palette.d.ts +113 -0
- package/dist/web-identity-palette.js +84 -0
- package/dist/web-identity-palette.js.map +1 -0
- package/dist/web-image-meta.d.ts +38 -0
- package/dist/web-image-meta.js +262 -0
- package/dist/web-image-meta.js.map +1 -0
- package/dist/web-login.d.ts +47 -0
- package/dist/web-login.js +115 -0
- package/dist/web-login.js.map +1 -1
- package/dist/web-mod-agent.d.ts +50 -0
- package/dist/web-mod-agent.js +207 -0
- package/dist/web-mod-agent.js.map +1 -0
- package/dist/web-mod-bundle.d.ts +157 -0
- package/dist/web-mod-bundle.js +529 -0
- package/dist/web-mod-bundle.js.map +1 -0
- package/dist/web-mod-export.d.ts +68 -0
- package/dist/web-mod-export.js +100 -0
- package/dist/web-mod-export.js.map +1 -0
- package/dist/web-mod-store.d.ts +289 -0
- package/dist/web-mod-store.js +638 -0
- package/dist/web-mod-store.js.map +1 -0
- package/dist/web-org.d.ts +3 -3
- package/dist/web-org.js +11 -17
- package/dist/web-org.js.map +1 -1
- package/dist/web-owner-css-store.d.ts +222 -0
- package/dist/web-owner-css-store.js +440 -0
- package/dist/web-owner-css-store.js.map +1 -0
- package/dist/web-owner-css.d.ts +58 -0
- package/dist/web-owner-css.js +552 -0
- package/dist/web-owner-css.js.map +1 -0
- package/dist/web-prefs.d.ts +62 -0
- package/dist/web-prefs.js +232 -36
- package/dist/web-prefs.js.map +1 -1
- package/dist/web-preview.d.ts +15 -3
- package/dist/web-preview.js +28 -9
- package/dist/web-preview.js.map +1 -1
- package/dist/web-returning-device.d.ts +41 -0
- package/dist/web-returning-device.js +57 -5
- package/dist/web-returning-device.js.map +1 -1
- package/dist/web-session-authorization.d.ts +132 -0
- package/dist/web-session-authorization.js +193 -0
- package/dist/web-session-authorization.js.map +1 -0
- package/dist/web-session.d.ts +70 -10
- package/dist/web-session.js +137 -22
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-patch.d.ts +67 -2
- package/dist/web-settings-patch.js +196 -1
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-settings-tools.d.ts +89 -0
- package/dist/web-settings-tools.js +273 -0
- package/dist/web-settings-tools.js.map +1 -0
- package/dist/web-shell-routes.d.ts +2 -2
- package/dist/web-shell-routes.js +2 -2
- package/dist/web-shell-routes.js.map +1 -1
- package/dist/web-theme-store.d.ts +257 -0
- package/dist/web-theme-store.js +627 -0
- package/dist/web-theme-store.js.map +1 -0
- package/dist/web-theme.d.ts +123 -0
- package/dist/web-theme.js +416 -0
- package/dist/web-theme.js.map +1 -0
- package/dist/web-upload.d.ts +13 -0
- package/dist/web-upload.js +39 -3
- package/dist/web-upload.js.map +1 -1
- package/dist/web-widget.d.ts +72 -0
- package/dist/web-widget.js +245 -0
- package/dist/web-widget.js.map +1 -0
- package/dist/workflow-templates/default.md +1 -1
- package/package.json +1 -1
- package/templates/systemd.service.ejs +8 -0
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #1629 r4 §4 (r4-5): installed mods, and a person's import in two steps.
|
|
3
|
+
*
|
|
4
|
+
* <dataDir>/web-mods.json { version: 1, rev, mods: { <id>: ModRecord } } (0600, atomic)
|
|
5
|
+
* <dataDir>/web-mods/<id>.agendmod.json each installed bundle, exactly as checked (its id is its content)
|
|
6
|
+
*
|
|
7
|
+
* Stage: a person's upload is checked whole (web-mod-bundle.ts) and kept **in memory** with the plan the page shows,
|
|
8
|
+
* one per person, for 10 minutes (monotonic). A fleet restart forgets it: commit then answers unknown_stage, and the
|
|
9
|
+
* person stages and reviews again. Effect entries are staged in the effect library at the same time (CW1), under the
|
|
10
|
+
* mod's own holder.
|
|
11
|
+
*
|
|
12
|
+
* Commit applies exactly the staged plan's chosen parts — or nothing: effect library first, then the theme store (one
|
|
13
|
+
* record write), the person's sidebar widget, their own CSS (as a trial they keep or revert on the page), then the
|
|
14
|
+
* index. Any failure undoes what was done, in reverse. Pictures an import replaced are deleted only once all of it held.
|
|
15
|
+
*
|
|
16
|
+
* Uninstall removes the mod from the library: its file, its index entry and its effect claim (the library keeps an
|
|
17
|
+
* effect still placed, or held by another mod or upload). What it applied stays applied — a look is the instance's (or
|
|
18
|
+
* the person's) once imported; Reset, Lock and the person's own CSS controls work on it as on any other.
|
|
19
|
+
*/
|
|
20
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
21
|
+
import { mkdirSync, readFileSync, rmSync, unlinkSync } from "node:fs";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
import { atomicWriteFileSync } from "./atomic-write.js";
|
|
24
|
+
import { MEDIA_MIME } from "./web-image-meta.js";
|
|
25
|
+
import { canonicalJson, checkModBundle } from "./web-mod-bundle.js";
|
|
26
|
+
import { AGENT_WIDGET_SLOTS } from "./web-widget.js";
|
|
27
|
+
export const MOD_STAGE_MS = 10 * 60 * 1000;
|
|
28
|
+
export const MODS_MAX_INSTALLED = 20;
|
|
29
|
+
export const MODS_MAX_BYTES = 64 * 1024 * 1024;
|
|
30
|
+
const MOD_ID = /^[0-9a-f]{32}$/;
|
|
31
|
+
const sha32 = (s) => createHash("sha256").update(s).digest("hex").slice(0, 32);
|
|
32
|
+
const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
33
|
+
export const modHolder = (id) => `mod:${id}`;
|
|
34
|
+
/** What the page shows before anything is applied: every part, in full (the owner CSS's text included). */
|
|
35
|
+
export function modPlan(mod, installed) {
|
|
36
|
+
const items = [];
|
|
37
|
+
const t = mod.theme;
|
|
38
|
+
if (t) {
|
|
39
|
+
const effectsById = (p) => mod.effects.find(e => e.path === p)?.summary.id ?? null;
|
|
40
|
+
items.push({ part: "theme", theme: t.theme, pictures: t.pictures.map(p => ({ path: p.path, slot: p.slot, id: p.id, bytes: p.kept.length })),
|
|
41
|
+
effects: t.effects.map(e => ({ effect: e.effect, id: effectsById(e.effect), params: e.params })) });
|
|
42
|
+
}
|
|
43
|
+
if (mod.identity)
|
|
44
|
+
items.push({ part: "identity", ...mod.identity });
|
|
45
|
+
for (const slot of ["chat_header", "details", "sidebar"]) {
|
|
46
|
+
const w = mod.widgets[slot];
|
|
47
|
+
if (w)
|
|
48
|
+
items.push({ part: "widget", slot, spec: w.spec, data: w.data });
|
|
49
|
+
}
|
|
50
|
+
if (mod.ambient)
|
|
51
|
+
items.push({ part: "ambient", ...mod.ambient.specs, pictures: mod.ambient.pictures.map(p => ({ path: p.path, slot: p.slot, id: p.id, bytes: p.kept.length })) });
|
|
52
|
+
for (const e of mod.effects)
|
|
53
|
+
items.push({ part: "effect", path: e.path, ...e.summary });
|
|
54
|
+
if (mod.ownerCss)
|
|
55
|
+
items.push({ part: "ownerCss", text: mod.ownerCss.text, assets: mod.ownerCss.assets.map(a => ({ path: a.path, id: a.id, kind: a.kind, bytes: a.kept.length })) });
|
|
56
|
+
const same = [...installed].find(([, r]) => r.name === mod.manifest.name);
|
|
57
|
+
const warnings = [];
|
|
58
|
+
if (mod.ownerCss)
|
|
59
|
+
warnings.push("owner_css_unchecked: Custom CSS from this file — read it before turning it on.");
|
|
60
|
+
return {
|
|
61
|
+
mod: {
|
|
62
|
+
id: mod.id, name: mod.manifest.name, version: mod.manifest.version, title: mod.manifest.title,
|
|
63
|
+
...(mod.manifest.description ? { description: mod.manifest.description } : {}),
|
|
64
|
+
...(mod.manifest.author ? { author: mod.manifest.author } : {}),
|
|
65
|
+
...(mod.manifest.requires ? { requires: mod.manifest.requires.agend } : {}),
|
|
66
|
+
bytes: mod.bytes, installed: installed.has(mod.id),
|
|
67
|
+
...(same && same[0] !== mod.id ? { replaces: { id: same[0], version: same[1].version } } : {}),
|
|
68
|
+
},
|
|
69
|
+
items,
|
|
70
|
+
needs_target: !!(mod.theme || mod.identity || mod.widgets.chat_header || mod.widgets.details || mod.ambient),
|
|
71
|
+
warnings,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export class ModStore {
|
|
75
|
+
dataDir;
|
|
76
|
+
deps;
|
|
77
|
+
index = null;
|
|
78
|
+
stages = new Map();
|
|
79
|
+
path;
|
|
80
|
+
constructor(dataDir, deps) {
|
|
81
|
+
this.dataDir = dataDir;
|
|
82
|
+
this.deps = deps;
|
|
83
|
+
this.path = join(dataDir, "web-mods.json");
|
|
84
|
+
}
|
|
85
|
+
clock() { return (this.deps.clock ?? (() => performance.now()))(); }
|
|
86
|
+
now() { return (this.deps.now ?? Date.now)(); }
|
|
87
|
+
file(id) { return join(this.dataDir, "web-mods", `${id}.agendmod.json`); }
|
|
88
|
+
load() {
|
|
89
|
+
if (this.index)
|
|
90
|
+
return this.index;
|
|
91
|
+
let text;
|
|
92
|
+
try {
|
|
93
|
+
text = readFileSync(this.path, "utf8");
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
return (this.index = err.code === "ENOENT" ? { rev: 0, mods: new Map(), proposals: new Map() } : "unreadable");
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
const doc = JSON.parse(text);
|
|
100
|
+
if (doc.version !== 1 || !Number.isSafeInteger(doc.rev) || !isObj(doc.mods))
|
|
101
|
+
return (this.index = "unreadable");
|
|
102
|
+
const mods = new Map();
|
|
103
|
+
for (const [id, r] of Object.entries(doc.mods))
|
|
104
|
+
if (MOD_ID.test(id) && isObj(r) && typeof r.name === "string")
|
|
105
|
+
mods.set(id, r);
|
|
106
|
+
const proposals = new Map();
|
|
107
|
+
if (isObj(doc.proposals))
|
|
108
|
+
for (const [name, r] of Object.entries(doc.proposals))
|
|
109
|
+
if (isObj(r) && typeof r.id === "string" && MOD_ID.test(r.id))
|
|
110
|
+
proposals.set(name, r);
|
|
111
|
+
return (this.index = { rev: doc.rev, mods, proposals });
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return (this.index = "unreadable");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
save(mods, proposals, rev) {
|
|
118
|
+
const own = (m) => { const out = Object.create(null); for (const [k, v] of m)
|
|
119
|
+
Object.defineProperty(out, k, { value: v, enumerable: true }); return out; };
|
|
120
|
+
atomicWriteFileSync(this.path, JSON.stringify({ version: 1, rev, mods: own(mods), proposals: own(proposals) }, null, 2) + "\n", { mode: 0o600 });
|
|
121
|
+
}
|
|
122
|
+
/** Write the index (disk first, then memory). `proposals`: unchanged unless given. */
|
|
123
|
+
commitIndex(mods, proposals) {
|
|
124
|
+
const idx = this.load();
|
|
125
|
+
if (idx === "unreadable")
|
|
126
|
+
return { ok: false, status: 503, error: "mods_unreadable" };
|
|
127
|
+
const rev = idx.rev + 1;
|
|
128
|
+
const props = proposals ?? idx.proposals;
|
|
129
|
+
try {
|
|
130
|
+
this.save(mods, props, rev);
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
return { ok: false, status: 503, error: "mods_write_failed", detail: err.message };
|
|
134
|
+
}
|
|
135
|
+
this.index = { rev, mods, proposals: props };
|
|
136
|
+
return { ok: true, rev };
|
|
137
|
+
}
|
|
138
|
+
proposalFile(instance) { return join(this.dataDir, "web-mods", "proposals", `${sha32(`instance\0${instance}`)}.agendmod.json`); }
|
|
139
|
+
/**
|
|
140
|
+
* r4-5b: keep an agent's proposed mod (checked already, and never carrying custom CSS — the caller refuses that by
|
|
141
|
+
* the path the bytes came in by) until a person reviews or dismisses it. One per agent: a new one replaces it.
|
|
142
|
+
*/
|
|
143
|
+
propose(instance, text, mod) {
|
|
144
|
+
const idx = this.load();
|
|
145
|
+
if (idx === "unreadable")
|
|
146
|
+
return { ok: false, status: 503, error: "mods_unreadable" };
|
|
147
|
+
const file = this.proposalFile(instance);
|
|
148
|
+
// The proposal it replaces is kept until the index names the new one: a failed index write puts it back.
|
|
149
|
+
let before = null;
|
|
150
|
+
try {
|
|
151
|
+
before = readFileSync(file, "utf8");
|
|
152
|
+
}
|
|
153
|
+
catch { /* none */ }
|
|
154
|
+
try {
|
|
155
|
+
mkdirSync(join(this.dataDir, "web-mods", "proposals"), { recursive: true, mode: 0o700 });
|
|
156
|
+
atomicWriteFileSync(file, text, { mode: 0o600 });
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
return { ok: false, status: 503, error: "mods_write_failed", detail: err.message };
|
|
160
|
+
}
|
|
161
|
+
const proposal = { id: mod.id, name: mod.manifest.name, version: mod.manifest.version, title: mod.manifest.title, bytes: mod.bytes, at: this.now() };
|
|
162
|
+
const proposals = new Map(idx.proposals);
|
|
163
|
+
proposals.set(instance, proposal);
|
|
164
|
+
const w = this.commitIndex(idx.mods, proposals);
|
|
165
|
+
if (!w.ok) {
|
|
166
|
+
try {
|
|
167
|
+
if (before === null)
|
|
168
|
+
unlinkSync(file);
|
|
169
|
+
else
|
|
170
|
+
atomicWriteFileSync(file, before, { mode: 0o600 });
|
|
171
|
+
}
|
|
172
|
+
catch { /* best effort */ }
|
|
173
|
+
return w;
|
|
174
|
+
}
|
|
175
|
+
return { ok: true, proposal };
|
|
176
|
+
}
|
|
177
|
+
/** Proposals waiting for a person, by agent. */
|
|
178
|
+
proposals() {
|
|
179
|
+
const idx = this.load();
|
|
180
|
+
return idx === "unreadable" ? [] : [...idx.proposals].map(([instance, p]) => ({ instance, ...p }));
|
|
181
|
+
}
|
|
182
|
+
/** A person opens an agent's proposal into the usual review: staged as theirs, its plan shown in full. */
|
|
183
|
+
stageProposal(person, instance) {
|
|
184
|
+
const idx = this.load();
|
|
185
|
+
if (idx === "unreadable")
|
|
186
|
+
return { ok: false, status: 503, error: "mods_unreadable" };
|
|
187
|
+
if (typeof instance !== "string" || !idx.proposals.has(instance))
|
|
188
|
+
return { ok: false, status: 404, error: "no_such_proposal" };
|
|
189
|
+
let text;
|
|
190
|
+
try {
|
|
191
|
+
text = readFileSync(this.proposalFile(instance), "utf8");
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return { ok: false, status: 404, error: "no_such_proposal" };
|
|
195
|
+
}
|
|
196
|
+
return this.stage(person, text, { proposal: instance });
|
|
197
|
+
}
|
|
198
|
+
/** A person dismisses an agent's proposal. */
|
|
199
|
+
dismiss(instance) {
|
|
200
|
+
const idx = this.load();
|
|
201
|
+
if (idx === "unreadable")
|
|
202
|
+
return { ok: false, status: 503, error: "mods_unreadable" };
|
|
203
|
+
if (typeof instance !== "string" || !idx.proposals.has(instance))
|
|
204
|
+
return { ok: false, status: 404, error: "no_such_proposal" };
|
|
205
|
+
const proposals = new Map(idx.proposals);
|
|
206
|
+
proposals.delete(instance);
|
|
207
|
+
const w = this.commitIndex(idx.mods, proposals);
|
|
208
|
+
if (!w.ok)
|
|
209
|
+
return w;
|
|
210
|
+
try {
|
|
211
|
+
unlinkSync(this.proposalFile(instance));
|
|
212
|
+
}
|
|
213
|
+
catch { /* already gone */ }
|
|
214
|
+
return { ok: true };
|
|
215
|
+
}
|
|
216
|
+
/** The context bundles are checked in (the effect runtime's checker, when the fleet has one): the agent's side uses it too. */
|
|
217
|
+
checkContext() { return this.deps.context(); }
|
|
218
|
+
/** Installed mods, for the page. */
|
|
219
|
+
list() {
|
|
220
|
+
const idx = this.load();
|
|
221
|
+
if (idx === "unreadable")
|
|
222
|
+
return { rev: 0, mods: [], unreadable: true };
|
|
223
|
+
return { rev: idx.rev, mods: [...idx.mods].map(([id, r]) => ({ id, ...r })) };
|
|
224
|
+
}
|
|
225
|
+
/** An installed bundle's file, exactly as it was checked (for export of an installed mod). */
|
|
226
|
+
bundleText(id) {
|
|
227
|
+
const idx = this.load();
|
|
228
|
+
if (!MOD_ID.test(id) || idx === "unreadable" || !idx.mods.has(id))
|
|
229
|
+
return null;
|
|
230
|
+
try {
|
|
231
|
+
return readFileSync(this.file(id), "utf8");
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* The effects an installed bundle's file carries, as library content ids — what downloading that file hands over,
|
|
239
|
+
* whether or not its import installed them (an import that left effects out still keeps the whole file). "unknown":
|
|
240
|
+
* the file no longer checks here and lists effects, so what they are can't be said; null: no such mod.
|
|
241
|
+
*/
|
|
242
|
+
bundleEffects(id) {
|
|
243
|
+
const text = this.bundleText(id);
|
|
244
|
+
if (text === null)
|
|
245
|
+
return null;
|
|
246
|
+
// An installed file, read again: #2017's placement rule is for new imports — a mod installed before it still checks.
|
|
247
|
+
const checked = checkModBundle(text, { ...this.deps.context(), placementRegions: false });
|
|
248
|
+
if (checked.ok)
|
|
249
|
+
return checked.mod.effects.map(e => e.summary.id);
|
|
250
|
+
try {
|
|
251
|
+
const listed = JSON.parse(text)?.manifest?.contents?.effects;
|
|
252
|
+
if (listed === undefined || (Array.isArray(listed) && listed.length === 0))
|
|
253
|
+
return [];
|
|
254
|
+
}
|
|
255
|
+
catch { /* unknown */ }
|
|
256
|
+
return "unknown";
|
|
257
|
+
}
|
|
258
|
+
/** Stages left idle past their time go (their effect stage is aborted). */
|
|
259
|
+
sweep() {
|
|
260
|
+
const now = this.clock();
|
|
261
|
+
for (const [person, s] of this.stages)
|
|
262
|
+
if (now - s.at > MOD_STAGE_MS || now < s.at)
|
|
263
|
+
this.drop(person);
|
|
264
|
+
}
|
|
265
|
+
drop(person) {
|
|
266
|
+
const s = this.stages.get(person);
|
|
267
|
+
if (!s)
|
|
268
|
+
return;
|
|
269
|
+
if (s.effects)
|
|
270
|
+
this.deps.effects?.abort(s.effects.stageId);
|
|
271
|
+
this.stages.delete(person);
|
|
272
|
+
}
|
|
273
|
+
/** The person's staged import (its plan), if one is still current. */
|
|
274
|
+
staged(person) {
|
|
275
|
+
this.sweep();
|
|
276
|
+
const s = this.stages.get(person);
|
|
277
|
+
return s ? { stage: s.id, plan: s.plan, plan_hash: s.planHash, expires_in_s: Math.max(0, Math.ceil((MOD_STAGE_MS - (this.clock() - s.at)) / 1000)) } : null;
|
|
278
|
+
}
|
|
279
|
+
/** #2064 review: where the person's open review came from — an agent's proposal (its instance) or a file (null). */
|
|
280
|
+
stageOrigin(person) {
|
|
281
|
+
this.sweep();
|
|
282
|
+
const s = this.stages.get(person);
|
|
283
|
+
return s ? { stage: s.id, proposal: s.proposal ?? null } : null;
|
|
284
|
+
}
|
|
285
|
+
/** A picture of this person's staged mod (its theme's), so the plan can show it before anything is applied. */
|
|
286
|
+
stagedPicture(person, id) {
|
|
287
|
+
this.sweep();
|
|
288
|
+
const mod = this.stages.get(person)?.mod;
|
|
289
|
+
const p = [...(mod?.theme?.pictures ?? []), ...(mod?.ambient?.pictures ?? [])].find(x => x.id === id);
|
|
290
|
+
return p ? { bytes: p.kept, mime: MEDIA_MIME[p.kind] } : null;
|
|
291
|
+
}
|
|
292
|
+
abort(person, stage) {
|
|
293
|
+
const s = this.stages.get(person);
|
|
294
|
+
if (!s || s.id !== stage)
|
|
295
|
+
return { ok: false, status: 404, error: "unknown_stage" };
|
|
296
|
+
this.drop(person);
|
|
297
|
+
return { ok: true };
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Step 1: check the uploaded file whole and keep it, with its plan, for this person (one stage each: a new one
|
|
301
|
+
* replaces theirs). Nothing is applied.
|
|
302
|
+
*/
|
|
303
|
+
stage(person, text, from = {}) {
|
|
304
|
+
this.sweep();
|
|
305
|
+
const idx = this.load();
|
|
306
|
+
if (idx === "unreadable")
|
|
307
|
+
return { ok: false, status: 503, error: "mods_unreadable" };
|
|
308
|
+
const checked = checkModBundle(text, this.deps.context());
|
|
309
|
+
if (!checked.ok)
|
|
310
|
+
return { ok: false, status: 400, error: "mod_invalid", problems: checked.problems };
|
|
311
|
+
const mod = checked.mod;
|
|
312
|
+
// Provenance is the path the bytes came in by: what an agent proposed never carries custom CSS (r4 §4).
|
|
313
|
+
if (from.proposal !== undefined && mod.ownerCss)
|
|
314
|
+
return { ok: false, status: 400, error: "owner_css_not_from_agent" };
|
|
315
|
+
const plan = modPlan(mod, idx.mods);
|
|
316
|
+
if (!plan.mod.installed && !plan.mod.replaces) {
|
|
317
|
+
if (idx.mods.size >= MODS_MAX_INSTALLED)
|
|
318
|
+
return { ok: false, status: 409, error: "mods_quota", detail: `at most ${MODS_MAX_INSTALLED} installed` };
|
|
319
|
+
const total = [...idx.mods.values()].reduce((n, r) => n + r.bytes, 0);
|
|
320
|
+
if (total + mod.bytes > MODS_MAX_BYTES)
|
|
321
|
+
return { ok: false, status: 409, error: "mods_quota", detail: `at most ${MODS_MAX_BYTES / 1024 / 1024} MiB installed` };
|
|
322
|
+
}
|
|
323
|
+
this.drop(person);
|
|
324
|
+
let effects;
|
|
325
|
+
if (mod.effects.length) {
|
|
326
|
+
if (!this.deps.effects)
|
|
327
|
+
return { ok: false, status: 400, error: "effects_unavailable" };
|
|
328
|
+
// The installed mod has one effect-holder principal. Reapplying its immutable bytes must not acquire an
|
|
329
|
+
// untracked claim for the next person. A mod whose effects were never selected gets its owner when selected.
|
|
330
|
+
const installed = idx.mods.get(mod.id);
|
|
331
|
+
const owner = installed?.effects.length ? installed.effectsBy ?? installed.installedBy : person;
|
|
332
|
+
const r = this.deps.effects.stage(mod.effects.map(e => e.files), { kind: "human", id: owner }, { holder: modHolder(mod.id) });
|
|
333
|
+
if (!r.ok)
|
|
334
|
+
return { ok: false, status: 409, error: "effects_refused", detail: r.error };
|
|
335
|
+
effects = { stageId: r.stageId, ids: r.ids, owner };
|
|
336
|
+
}
|
|
337
|
+
const planHash = sha32(canonicalJson(plan));
|
|
338
|
+
const s = { id: randomBytes(16).toString("hex"), person, mod, text, plan, planHash, at: this.clock(), ...(effects ? { effects } : {}), ...(from.proposal !== undefined ? { proposal: from.proposal } : {}) };
|
|
339
|
+
this.stages.set(person, s);
|
|
340
|
+
return { ok: true, stage: s.id, plan, plan_hash: planHash };
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Step 2: apply exactly the staged plan's chosen parts, atomically. `person`: the signed-in session's person (their
|
|
344
|
+
* own CSS goes to them); the sidebar widget is a preference of the page's one bucket. A changed plan hash is 409; an
|
|
345
|
+
* unknown or expired stage is 404.
|
|
346
|
+
*/
|
|
347
|
+
commit(person, body) {
|
|
348
|
+
this.sweep();
|
|
349
|
+
const s = this.stages.get(person);
|
|
350
|
+
if (!s || s.id !== body.stage)
|
|
351
|
+
return { ok: false, status: 404, error: "unknown_stage" };
|
|
352
|
+
if (body.plan_hash !== s.planHash)
|
|
353
|
+
return { ok: false, status: 409, error: "plan_changed" };
|
|
354
|
+
const include = parseInclude(body.include);
|
|
355
|
+
if (!include)
|
|
356
|
+
return { ok: false, status: 400, error: "include", detail: "{ theme?, identity?, ambient?, effects?, ownerCss?, widgets?: { chat_header?, details?, sidebar? }, replace? } of booleans" };
|
|
357
|
+
const mod = s.mod;
|
|
358
|
+
const want = {
|
|
359
|
+
theme: !!(include.theme && mod.theme), identity: !!(include.identity && mod.identity), effects: !!(include.effects && mod.effects.length),
|
|
360
|
+
ownerCss: !!(include.ownerCss && mod.ownerCss),
|
|
361
|
+
ambient: !!(include.ambient && mod.ambient),
|
|
362
|
+
widgets: AGENT_WIDGET_SLOTS.filter(slot => include.widgets?.[slot] && mod.widgets[slot]),
|
|
363
|
+
sidebar: !!(include.widgets?.sidebar && mod.widgets.sidebar),
|
|
364
|
+
};
|
|
365
|
+
// Effect placements ride with the theme, and need the effects installed.
|
|
366
|
+
if (want.theme && mod.theme?.effects.length && !want.effects)
|
|
367
|
+
return { ok: false, status: 400, error: "include", detail: "the theme places this mod's effects: include effects too" };
|
|
368
|
+
const toInstance = want.theme || want.identity || want.widgets.length > 0 || want.ambient;
|
|
369
|
+
let target;
|
|
370
|
+
if (toInstance) {
|
|
371
|
+
if (typeof body.target !== "string" || !this.deps.isKnownInstance(body.target))
|
|
372
|
+
return { ok: false, status: 400, error: "target", detail: "an instance of this fleet" };
|
|
373
|
+
target = body.target;
|
|
374
|
+
}
|
|
375
|
+
if (want.ownerCss && !this.deps.ownerCss)
|
|
376
|
+
return { ok: false, status: 404, error: "owner_css_unavailable" };
|
|
377
|
+
if (!toInstance && !want.effects && !want.ownerCss && !want.sidebar)
|
|
378
|
+
return { ok: false, status: 400, error: "include", detail: "nothing chosen" };
|
|
379
|
+
const idx = this.load();
|
|
380
|
+
if (idx === "unreadable")
|
|
381
|
+
return { ok: false, status: 503, error: "mods_unreadable" };
|
|
382
|
+
const replaces = s.plan.mod.replaces;
|
|
383
|
+
// What was installed may have moved since this plan was staged (another person's import, an uninstall): the plan
|
|
384
|
+
// — and so the consent — is for the state it showed. Checked again here, before anything is changed.
|
|
385
|
+
const sameName = [...idx.mods].find(([id, r]) => r.name === mod.manifest.name && id !== mod.id);
|
|
386
|
+
if ((sameName?.[0] ?? null) !== (replaces?.id ?? null) || idx.mods.has(mod.id) !== s.plan.mod.installed) {
|
|
387
|
+
return { ok: false, status: 409, error: "plan_changed", detail: "the installed mods changed since this file was checked: import it again" };
|
|
388
|
+
}
|
|
389
|
+
const installed = idx.mods.get(mod.id);
|
|
390
|
+
const effectOwner = installed?.effects.length ? installed.effectsBy ?? installed.installedBy : person;
|
|
391
|
+
if (want.effects && s.effects?.owner !== effectOwner) {
|
|
392
|
+
return { ok: false, status: 409, error: "plan_changed", detail: "the effect claim owner changed: import it again" };
|
|
393
|
+
}
|
|
394
|
+
if (replaces && !include.replace)
|
|
395
|
+
return { ok: false, status: 409, error: "replaces", detail: `version ${replaces.version} of ${mod.manifest.name} is installed: confirm replace` };
|
|
396
|
+
if (!idx.mods.has(mod.id)) {
|
|
397
|
+
const others = [...idx.mods].filter(([id]) => id !== replaces?.id).map(([, r]) => r);
|
|
398
|
+
if (others.length + 1 > MODS_MAX_INSTALLED)
|
|
399
|
+
return { ok: false, status: 409, error: "mods_quota", detail: `at most ${MODS_MAX_INSTALLED} installed` };
|
|
400
|
+
if (others.reduce((n, r) => n + r.bytes, 0) + mod.bytes > MODS_MAX_BYTES)
|
|
401
|
+
return { ok: false, status: 409, error: "mods_quota", detail: `at most ${MODS_MAX_BYTES / 1024 / 1024} MiB installed` };
|
|
402
|
+
}
|
|
403
|
+
const undo = [];
|
|
404
|
+
const rollback = (status, error, detail) => {
|
|
405
|
+
for (const u of undo.reverse()) {
|
|
406
|
+
try {
|
|
407
|
+
u();
|
|
408
|
+
}
|
|
409
|
+
catch { /* best effort: each step undoes its own */ }
|
|
410
|
+
}
|
|
411
|
+
return { ok: false, status, error, ...(detail ? { detail } : {}) };
|
|
412
|
+
};
|
|
413
|
+
// Everything below either completes or is undone: a participant that throws (a real filesystem failure) is caught
|
|
414
|
+
// here, inside the rollback boundary, as one that answers with an error.
|
|
415
|
+
try {
|
|
416
|
+
const applied = [];
|
|
417
|
+
let created = [];
|
|
418
|
+
let effectIds = new Map();
|
|
419
|
+
if (want.effects && s.effects && this.deps.effects) {
|
|
420
|
+
const r = this.deps.effects.commit(s.effects.stageId);
|
|
421
|
+
if (!r.ok)
|
|
422
|
+
return rollback(409, "effects_refused", r.error);
|
|
423
|
+
undo.push(() => { r.undo(); });
|
|
424
|
+
created = r.created;
|
|
425
|
+
effectIds = new Map(mod.effects.map((e, i) => [e.path, r.ids[i]]));
|
|
426
|
+
applied.push("effects");
|
|
427
|
+
}
|
|
428
|
+
let finalize = null;
|
|
429
|
+
if (target) {
|
|
430
|
+
const t = mod.theme;
|
|
431
|
+
const w = this.deps.themes.applyLook(target, {
|
|
432
|
+
...(want.theme && t ? { theme: t.theme } : {}),
|
|
433
|
+
...(want.identity && mod.identity ? { identity: mod.identity } : {}),
|
|
434
|
+
pictures: [...(want.theme && t ? t.pictures : []), ...(want.ambient && mod.ambient ? mod.ambient.pictures : [])].map(p => ({ slot: p.slot, id: p.id, kind: p.kind, kept: p.kept })),
|
|
435
|
+
...(want.ambient && mod.ambient ? { ambient: mod.ambient.specs } : {}),
|
|
436
|
+
widgets: Object.fromEntries(want.widgets.map(slot => [slot, mod.widgets[slot]])),
|
|
437
|
+
...(want.theme && t && t.effects.length ? { effects: t.effects.map(e => ({ id: effectIds.get(e.effect), params: e.params })) } : {}),
|
|
438
|
+
}, "human");
|
|
439
|
+
if (!w.ok)
|
|
440
|
+
return rollback(w.status, w.error, w.detail);
|
|
441
|
+
undo.push(() => { w.undo(); });
|
|
442
|
+
finalize = w.finalize;
|
|
443
|
+
if (want.theme)
|
|
444
|
+
applied.push("theme");
|
|
445
|
+
if (want.identity)
|
|
446
|
+
applied.push("identity");
|
|
447
|
+
for (const slot of want.widgets)
|
|
448
|
+
applied.push(`widget:${slot}`);
|
|
449
|
+
if (want.ambient)
|
|
450
|
+
applied.push("ambient");
|
|
451
|
+
}
|
|
452
|
+
if (want.sidebar) {
|
|
453
|
+
const who = this.deps.prefsIdentity;
|
|
454
|
+
const before = this.deps.prefs.read(who);
|
|
455
|
+
if (before.unreadable)
|
|
456
|
+
return rollback(503, "prefs_unreadable");
|
|
457
|
+
const had = Object.hasOwn(before.values, "widget.sidebar") ? before.values["widget.sidebar"] : null;
|
|
458
|
+
const w = this.deps.prefs.write(who, new Map([["widget.sidebar", mod.widgets.sidebar]]));
|
|
459
|
+
if (!w.ok)
|
|
460
|
+
return rollback(w.status, w.error);
|
|
461
|
+
undo.push(() => { this.deps.prefs.write(who, new Map([["widget.sidebar", had]])); });
|
|
462
|
+
applied.push("widget:sidebar");
|
|
463
|
+
}
|
|
464
|
+
let css;
|
|
465
|
+
if (want.ownerCss && mod.ownerCss && this.deps.ownerCss) {
|
|
466
|
+
const store = this.deps.ownerCss;
|
|
467
|
+
// Undo puts back exactly what was there — the version in force, a trial already running, history and assets —
|
|
468
|
+
// never just "revert the trial this import started" (#1897 review).
|
|
469
|
+
const snap = store.snapshot(person);
|
|
470
|
+
undo.push(() => { store.restoreSnapshot(person, snap); });
|
|
471
|
+
for (const a of mod.ownerCss.assets) {
|
|
472
|
+
const r = store.addAsset(person, a.kept);
|
|
473
|
+
if (!r.ok)
|
|
474
|
+
return rollback(r.status, r.error, r.detail);
|
|
475
|
+
if (r.id !== a.id)
|
|
476
|
+
return rollback(500, "owner_css_asset_id", `${a.path}: ${r.id} ≠ ${a.id}`);
|
|
477
|
+
}
|
|
478
|
+
const saved = store.save(person, mod.ownerCss.text);
|
|
479
|
+
if (!saved.ok)
|
|
480
|
+
return rollback(saved.status, saved.error, saved.detail);
|
|
481
|
+
css = { trial: saved.trial, sha: saved.sha, previous: saved.previous };
|
|
482
|
+
applied.push("ownerCss");
|
|
483
|
+
}
|
|
484
|
+
// The index: this mod in, the one it replaces out. The old mod's effect claim is released LAST (below): one library
|
|
485
|
+
// write per rollback chain, so the new commit's undo always meets the exact state it left (CW1, agreed with Sol).
|
|
486
|
+
const mods = new Map(idx.mods);
|
|
487
|
+
const old = replaces ? mods.get(replaces.id) : undefined;
|
|
488
|
+
if (replaces)
|
|
489
|
+
mods.delete(replaces.id);
|
|
490
|
+
const prev = mods.get(mod.id);
|
|
491
|
+
mods.set(mod.id, {
|
|
492
|
+
name: mod.manifest.name, version: mod.manifest.version, title: mod.manifest.title, bytes: mod.bytes,
|
|
493
|
+
installedBy: prev?.installedBy ?? person, installedAt: prev?.installedAt ?? this.now(),
|
|
494
|
+
...(want.effects ? { effectsBy: effectOwner } : prev?.effectsBy ? { effectsBy: prev.effectsBy } : {}),
|
|
495
|
+
...(target ? { target } : prev?.target ? { target: prev.target } : {}),
|
|
496
|
+
parts: [...new Set([...(prev?.parts ?? []), ...applied])],
|
|
497
|
+
effects: mod.effects.map(e => ({ path: e.path, id: effectIds.get(e.path) ?? prev?.effects.find(x => x.path === e.path)?.id ?? "" })).filter(e => e.id),
|
|
498
|
+
created: [...new Set([...(prev?.created ?? []), ...created])],
|
|
499
|
+
});
|
|
500
|
+
try {
|
|
501
|
+
mkdirSync(join(this.dataDir, "web-mods"), { recursive: true, mode: 0o700 });
|
|
502
|
+
if (!prev)
|
|
503
|
+
atomicWriteFileSync(this.file(mod.id), s.text, { mode: 0o600 });
|
|
504
|
+
}
|
|
505
|
+
catch (err) {
|
|
506
|
+
return rollback(503, "mods_write_failed", err.message);
|
|
507
|
+
}
|
|
508
|
+
if (!prev)
|
|
509
|
+
undo.push(() => { try {
|
|
510
|
+
unlinkSync(this.file(mod.id));
|
|
511
|
+
}
|
|
512
|
+
catch { /* none */ } });
|
|
513
|
+
// A reviewed proposal is done once installed (the same write).
|
|
514
|
+
let proposals;
|
|
515
|
+
if (s.proposal !== undefined && idx.proposals.get(s.proposal)?.id === mod.id) {
|
|
516
|
+
proposals = new Map(idx.proposals);
|
|
517
|
+
proposals.delete(s.proposal);
|
|
518
|
+
}
|
|
519
|
+
const w = this.commitIndex(mods, proposals);
|
|
520
|
+
if (!w.ok)
|
|
521
|
+
return rollback(w.status, w.error, w.detail);
|
|
522
|
+
undo.push(() => { this.commitIndex(idx.mods, idx.proposals); });
|
|
523
|
+
// The last fallible step: the replaced mod's effect claim. Refused: every store above is restored first, then the
|
|
524
|
+
// new library commit is undone (rollback runs in reverse). Released: nothing fallible follows.
|
|
525
|
+
let releasedOld = [];
|
|
526
|
+
if (replaces && old && old.effects.length && this.deps.effects) {
|
|
527
|
+
const r = this.deps.effects.releaseHolder(modHolder(replaces.id), { kind: "human", id: old.effectsBy ?? old.installedBy });
|
|
528
|
+
if (!r.ok)
|
|
529
|
+
return rollback(409, "effects_release_refused", r.error);
|
|
530
|
+
releasedOld = r.released;
|
|
531
|
+
}
|
|
532
|
+
if (proposals) {
|
|
533
|
+
try {
|
|
534
|
+
unlinkSync(this.proposalFile(s.proposal));
|
|
535
|
+
}
|
|
536
|
+
catch { /* already gone */ }
|
|
537
|
+
}
|
|
538
|
+
// Held: what the import replaced may go now, and the stage is done.
|
|
539
|
+
finalize?.();
|
|
540
|
+
if (replaces) {
|
|
541
|
+
try {
|
|
542
|
+
unlinkSync(this.file(replaces.id));
|
|
543
|
+
}
|
|
544
|
+
catch { /* already gone */ }
|
|
545
|
+
}
|
|
546
|
+
if (releasedOld.length)
|
|
547
|
+
this.deps.effects?.collectUnreferenced?.(releasedOld);
|
|
548
|
+
if (s.effects && !want.effects)
|
|
549
|
+
this.deps.effects?.abort(s.effects.stageId); // staged, not chosen: let it go
|
|
550
|
+
this.stages.delete(person);
|
|
551
|
+
return { ok: true, id: mod.id, applied, ...(css ? { css } : {}) };
|
|
552
|
+
}
|
|
553
|
+
catch (err) {
|
|
554
|
+
return rollback(503, "mod_apply_failed", err.message);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
/** Remove an installed mod from the library: its effect claim, its file and its entry. What it applied stays. */
|
|
558
|
+
uninstall(id) {
|
|
559
|
+
const idx = this.load();
|
|
560
|
+
if (idx === "unreadable")
|
|
561
|
+
return { ok: false, status: 503, error: "mods_unreadable" };
|
|
562
|
+
if (typeof id !== "string" || !idx.mods.has(id))
|
|
563
|
+
return { ok: false, status: 404, error: "no_such_mod" };
|
|
564
|
+
const rec = idx.mods.get(id);
|
|
565
|
+
let released = [];
|
|
566
|
+
let undo = null;
|
|
567
|
+
if (rec.effects.length) {
|
|
568
|
+
if (!this.deps.effects)
|
|
569
|
+
return { ok: false, status: 503, error: "effects_unavailable" };
|
|
570
|
+
const r = this.deps.effects.releaseHolder(modHolder(id), { kind: "human", id: rec.effectsBy ?? rec.installedBy });
|
|
571
|
+
if (!r.ok)
|
|
572
|
+
return { ok: false, status: 409, error: "effects_release_refused", detail: r.error };
|
|
573
|
+
released = r.released;
|
|
574
|
+
undo = r.undo;
|
|
575
|
+
}
|
|
576
|
+
const mods = new Map(idx.mods);
|
|
577
|
+
mods.delete(id);
|
|
578
|
+
const w = this.commitIndex(mods);
|
|
579
|
+
if (!w.ok) {
|
|
580
|
+
undo?.();
|
|
581
|
+
return w;
|
|
582
|
+
}
|
|
583
|
+
try {
|
|
584
|
+
unlinkSync(this.file(id));
|
|
585
|
+
}
|
|
586
|
+
catch { /* already gone */ }
|
|
587
|
+
if (released.length)
|
|
588
|
+
this.deps.effects?.collectUnreferenced?.(released);
|
|
589
|
+
return { ok: true, released };
|
|
590
|
+
}
|
|
591
|
+
/** An instance was removed: its name leaves the records that named it as their target. */
|
|
592
|
+
forgetInstance(name) {
|
|
593
|
+
const idx = this.load();
|
|
594
|
+
if (idx === "unreadable" || (![...idx.mods.values()].some(r => r.target === name) && !idx.proposals.has(name)))
|
|
595
|
+
return;
|
|
596
|
+
const mods = new Map([...idx.mods].map(([id, r]) => { if (r.target !== name)
|
|
597
|
+
return [id, r]; const { target: _t, ...rest } = r; void _t; return [id, rest]; }));
|
|
598
|
+
const proposals = new Map(idx.proposals);
|
|
599
|
+
proposals.delete(name);
|
|
600
|
+
if (this.commitIndex(mods, proposals).ok) {
|
|
601
|
+
try {
|
|
602
|
+
unlinkSync(this.proposalFile(name));
|
|
603
|
+
}
|
|
604
|
+
catch { /* none */ }
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
/** Tests: the folder bundles live in. */
|
|
608
|
+
get folder() { return join(this.dataDir, "web-mods"); }
|
|
609
|
+
/** Every installed bundle's file is removed with the index (tests and a reset). */
|
|
610
|
+
clear() { try {
|
|
611
|
+
rmSync(this.folder, { recursive: true, force: true });
|
|
612
|
+
}
|
|
613
|
+
catch { /* none */ } }
|
|
614
|
+
}
|
|
615
|
+
function parseInclude(raw) {
|
|
616
|
+
if (!isObj(raw))
|
|
617
|
+
return null;
|
|
618
|
+
const out = {};
|
|
619
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
620
|
+
if (["theme", "identity", "ambient", "effects", "ownerCss", "replace"].includes(k)) {
|
|
621
|
+
if (typeof v !== "boolean")
|
|
622
|
+
return null;
|
|
623
|
+
out[k] = v;
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
if (k !== "widgets" || !isObj(v))
|
|
627
|
+
return null;
|
|
628
|
+
const w = {};
|
|
629
|
+
for (const [slot, on] of Object.entries(v)) {
|
|
630
|
+
if (!["chat_header", "details", "sidebar"].includes(slot) || typeof on !== "boolean")
|
|
631
|
+
return null;
|
|
632
|
+
w[slot] = on;
|
|
633
|
+
}
|
|
634
|
+
out.widgets = w;
|
|
635
|
+
}
|
|
636
|
+
return out;
|
|
637
|
+
}
|
|
638
|
+
//# sourceMappingURL=web-mod-store.js.map
|