@vellumai/assistant 0.9.0 → 0.10.0-staging.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/ARCHITECTURE.md +18 -34
- package/bun.lock +7 -8
- package/docs/activation-funnel-telemetry.md +28 -22
- package/docs/architecture/security.md +29 -28
- package/docs/stt-provider-onboarding.md +3 -5
- package/docs/workflows-testing.md +13 -44
- package/docs/workflows.md +3 -5
- package/node_modules/@vellumai/ces-client/src/__tests__/ces-client.test.ts +47 -0
- package/node_modules/@vellumai/ces-client/src/rpc-client.ts +28 -5
- package/node_modules/@vellumai/environments/src/seeds.ts +2 -5
- package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +97 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +10 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +32 -6
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +119 -0
- package/node_modules/@vellumai/gateway-client/src/types.ts +15 -84
- package/openapi.yaml +976 -63
- package/package.json +2 -1
- package/scripts/sync-llm-catalog.ts +6 -15
- package/scripts/sync-web-search-catalog.ts +3 -11
- package/src/__tests__/access-request-card-view.test.ts +98 -0
- package/src/__tests__/access-request-seed-content-blocks.test.ts +2 -4
- package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +72 -32
- package/src/__tests__/agent-loop-compaction-strip.test.ts +241 -0
- package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +16 -13
- package/src/__tests__/agent-loop-output-hooks.test.ts +69 -0
- package/src/__tests__/agent-loop-override-profile.test.ts +25 -0
- package/src/__tests__/always-loaded-tools-guard.test.ts +2 -3
- package/src/__tests__/app-compiler.test.ts +15 -1
- package/src/__tests__/app-dir-path-guard.test.ts +0 -1
- package/src/__tests__/assistant-feature-flag-guard.test.ts +1 -4
- package/src/__tests__/assistant-feature-flag-guardrails.test.ts +0 -2
- package/src/__tests__/auth-fallback-events-store.test.ts +6 -14
- package/src/__tests__/avatar-identity-sync.test.ts +2 -27
- package/src/__tests__/btw-routes.test.ts +6 -8
- package/src/__tests__/call-pointer-messages.test.ts +28 -0
- package/src/__tests__/cancel-clears-processing.test.ts +89 -0
- package/src/__tests__/channel-approval-routes.test.ts +0 -4
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -15
- package/src/__tests__/checker.test.ts +0 -3
- package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +3 -4
- package/src/__tests__/compactor-image-manifest-trust.test.ts +21 -1
- package/src/__tests__/compactor-summary-call-truncation.test.ts +223 -0
- package/src/__tests__/config-loader-backfill.test.ts +268 -27
- package/src/__tests__/config-schema.test.ts +35 -0
- package/src/__tests__/config-watcher.test.ts +0 -18
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +2 -2
- package/src/__tests__/contact-store-user-file.test.ts +0 -6
- package/src/__tests__/contacts-tools.test.ts +29 -0
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +22 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop.test.ts +58 -0
- package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
- package/src/__tests__/conversation-lifecycle.test.ts +7 -9
- package/src/__tests__/conversation-load-history-repair.test.ts +101 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +15 -12
- package/src/__tests__/conversation-surfaces-activation-emit.test.ts +6 -3
- package/src/__tests__/conversation-title-service.test.ts +62 -0
- package/src/__tests__/credential-broker.test.ts +449 -1
- package/src/__tests__/credential-execution-shell-lockdown.test.ts +18 -11
- package/src/__tests__/credential-execution-tools.test.ts +0 -1
- package/src/__tests__/credential-prompt-route.test.ts +4 -4
- package/src/__tests__/credential-routes.test.ts +360 -0
- package/src/__tests__/credential-security-invariants.test.ts +4 -13
- package/src/__tests__/disk-pressure-policy.test.ts +12 -0
- package/src/__tests__/disk-usage.test.ts +65 -0
- package/src/__tests__/dynamic-page-surface.test.ts +152 -1
- package/src/__tests__/fixtures/credential-security-fixtures.ts +2 -33
- package/src/__tests__/gateway-flag-listener.test.ts +110 -1
- package/src/__tests__/gateway-only-guard.test.ts +3 -7
- package/src/__tests__/guardian-binding-drift-heal.test.ts +1 -1
- package/src/__tests__/guardian-card-withdrawal.test.ts +403 -0
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
- package/src/__tests__/guardian-grant-minting.test.ts +3 -35
- package/src/__tests__/guardian-routing-invariants.test.ts +64 -26
- package/src/__tests__/guardian-routing-state.test.ts +0 -1
- package/src/__tests__/headless-browser-mode.test.ts +10 -0
- package/src/__tests__/headless-browser-navigate.test.ts +8 -3
- package/src/__tests__/helpers/create-guardian-binding.ts +0 -1
- package/src/__tests__/host-browser-proxy.test.ts +87 -0
- package/src/__tests__/identity-routes.test.ts +0 -189
- package/src/__tests__/inbound-invite-redemption.test.ts +4 -4
- package/src/__tests__/injector-v3-suppression.test.ts +27 -20
- package/src/__tests__/internal-telemetry-routes.test.ts +6 -14
- package/src/__tests__/invite-redemption-service.test.ts +4 -7
- package/src/__tests__/llm-callsite-catalog.test.ts +5 -6
- package/src/__tests__/llm-catalog-parity.test.ts +30 -23
- package/src/__tests__/llm-resolver.test.ts +70 -24
- package/src/__tests__/llm-schema.test.ts +1 -0
- package/src/__tests__/managed-profile-guard.test.ts +163 -4
- package/src/__tests__/mcp-health-check.test.ts +6 -7
- package/src/__tests__/media-stream-server-integration.test.ts +317 -13
- package/src/__tests__/oauth-provider-seed-logos.test.ts +4 -6
- package/src/__tests__/onboarding-persona-write.test.ts +1 -1
- package/src/__tests__/path-policy.test.ts +34 -0
- package/src/__tests__/persona-resolver.test.ts +49 -14
- package/src/__tests__/plugin-api-model-profiles.test.ts +178 -0
- package/src/__tests__/plugin-api-provider.test.ts +24 -0
- package/src/__tests__/plugin-tool-contribution.test.ts +6 -3
- package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +214 -0
- package/src/__tests__/provider-send-message-override-profile.test.ts +76 -0
- package/src/__tests__/reaction-persistence.test.ts +150 -29
- package/src/__tests__/registry.test.ts +2 -7
- package/src/__tests__/relay-server.test.ts +285 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
- package/src/__tests__/schedule-routes-workflow-validation.test.ts +1 -10
- package/src/__tests__/schedule-routes.test.ts +0 -30
- package/src/__tests__/schedule-tools.test.ts +2 -18
- package/src/__tests__/scheduler-reuse-conversation.test.ts +8 -5
- package/src/__tests__/skill-execute-input.test.ts +51 -1
- package/src/__tests__/skill-runtime-path.test.ts +2 -3
- package/src/__tests__/skills.test.ts +51 -0
- package/src/__tests__/slack-notification-approval-card.test.ts +176 -0
- package/src/__tests__/slack-reaction-canonical-approval.test.ts +285 -0
- package/src/__tests__/subagent-tools.test.ts +266 -0
- package/src/__tests__/surface-completion-nudge-hook.test.ts +367 -0
- package/src/__tests__/task-progress-nudge-hook.test.ts +1 -1
- package/src/__tests__/title-generate-hook.test.ts +100 -3
- package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -29
- package/src/__tests__/token-manager.test.ts +519 -0
- package/src/__tests__/tool-approval-seed-content-blocks.test.ts +1 -1
- package/src/__tests__/tool-audit-listener.test.ts +7 -7
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +6 -3
- package/src/__tests__/tool-executor.test.ts +0 -79
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +4 -2
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +220 -3
- package/src/__tests__/trusted-contact-multichannel.test.ts +3 -3
- package/src/__tests__/trusted-contact-verification.test.ts +8 -10
- package/src/__tests__/twilio-routes.test.ts +81 -1
- package/src/__tests__/voice-invite-redemption.test.ts +2 -3
- package/src/__tests__/weak-open-model.test.ts +30 -0
- package/src/__tests__/web-search-catalog-parity.test.ts +6 -25
- package/src/__tests__/workspace-greetings.test.ts +152 -0
- package/src/__tests__/workspace-migration-105-enable-memory-v3-live-for-new-workspaces.test.ts +149 -0
- package/src/__tests__/workspace-migration-108-drop-balanced-economy-profile.test.ts +285 -0
- package/src/__tests__/workspace-migration-add-send-diagnostics.test.ts +1 -1
- package/src/__tests__/workspace-migration-drop-collect-usage-data.test.ts +118 -0
- package/src/__tests__/workspace-migration-drop-send-diagnostics.test.ts +118 -0
- package/src/a2a/__tests__/e2e-a2a-channel.test.ts +0 -4
- package/src/agent/loop.ts +49 -29
- package/src/api/README.md +6 -6
- package/src/api/events/tool-result.ts +6 -0
- package/src/api/events/workflow-completed.ts +53 -0
- package/src/api/events/workflow-leaf-finished.ts +38 -0
- package/src/api/events/workflow-leaf-started.ts +35 -0
- package/src/api/events/workflow-progress.ts +32 -0
- package/src/api/events/workflow-started.ts +31 -0
- package/src/api/index.ts +40 -0
- package/src/api/responses/conversation-message.ts +28 -4
- package/src/api/responses/home.ts +26 -4
- package/src/api/responses/workflow-journal.ts +53 -0
- package/src/approvals/guardian-card-withdrawal.ts +145 -0
- package/src/approvals/guardian-decision-primitive.ts +26 -3
- package/src/approvals/guardian-request-resolvers.ts +183 -80
- package/src/calls/__tests__/channel-admission-reader.test.ts +132 -0
- package/src/calls/__tests__/relay-setup-router.test.ts +350 -0
- package/src/calls/call-pointer-messages.ts +10 -4
- package/src/calls/channel-admission-reader.ts +104 -0
- package/src/calls/guardian-dispatch.ts +17 -45
- package/src/calls/media-stream-server.ts +84 -2
- package/src/calls/relay-access-wait.ts +1 -1
- package/src/calls/relay-server.ts +66 -0
- package/src/calls/relay-setup-router.ts +82 -1
- package/src/calls/twilio-routes.ts +17 -8
- package/src/calls/voice-session-bridge.ts +2 -2
- package/src/cli/commands/clients.ts +3 -0
- package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2-compare-render.test.ts +1 -1
- package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2.test.ts +8 -7
- package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v3.test.ts +5 -4
- package/src/cli/commands/memory/index.ts +30 -0
- package/src/cli/commands/{memory-v2-compare-render.ts → memory/memory-v2-compare-render.ts} +1 -1
- package/src/cli/commands/{memory-v2.ts → memory/memory-v2.ts} +6 -15
- package/src/cli/commands/{memory-v3.ts → memory/memory-v3.ts} +97 -11
- package/src/cli/commands/oauth/status.test.ts +36 -0
- package/src/cli/commands/oauth/status.ts +23 -3
- package/src/cli/commands/plugins.ts +197 -4
- package/src/cli/lib/__tests__/diff-plugin.test.ts +443 -0
- package/src/cli/lib/__tests__/inspect-plugin.test.ts +54 -0
- package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +443 -0
- package/src/cli/lib/__tests__/plugin-surfaces.test.ts +111 -0
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +295 -2
- package/src/cli/lib/diff-plugin.ts +346 -0
- package/src/cli/lib/inspect-plugin.ts +12 -1
- package/src/cli/lib/install-from-github.ts +105 -17
- package/src/cli/lib/merge-plugin-tree.ts +328 -0
- package/src/cli/lib/plugin-fingerprint.ts +14 -0
- package/src/cli/lib/plugin-surfaces.ts +104 -0
- package/src/cli/lib/upgrade-plugin.ts +298 -10
- package/src/cli/program.ts +2 -6
- package/src/config/__tests__/sync-gated-profiles.test.ts +368 -0
- package/src/config/assistant-feature-flags.ts +22 -7
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +0 -1
- package/src/config/bundled-skills/messaging/SKILL.md +6 -4
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +9 -8
- package/src/config/bundled-skills/subagent/SKILL.md +4 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +4 -0
- package/src/config/bundled-skills/workflows/SKILL.md +14 -8
- package/src/config/bundled-tool-registry.ts +2 -7
- package/src/config/call-site-defaults.ts +15 -2
- package/src/config/feature-flag-registry.json +46 -31
- package/src/config/inference-profile-validation.ts +26 -0
- package/src/config/llm-resolver.ts +3 -0
- package/src/config/loader.ts +4 -0
- package/src/config/memory-v3-gate.ts +11 -0
- package/src/config/profile-order.ts +28 -0
- package/src/config/schema.ts +8 -6
- package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
- package/src/config/schemas/call-site-catalog.ts +7 -0
- package/src/config/schemas/channels.ts +11 -0
- package/src/config/schemas/elevenlabs.ts +0 -1
- package/src/config/schemas/llm.ts +31 -0
- package/src/config/schemas/memory-lifecycle.ts +3 -7
- package/src/config/schemas/memory-v3.ts +6 -0
- package/src/config/schemas/platform.ts +0 -8
- package/src/config/schemas/services.ts +18 -0
- package/src/config/seed-inference-profiles.ts +109 -44
- package/src/config/skills.ts +21 -0
- package/src/config/sync-gated-profiles.ts +220 -0
- package/src/contacts/contact-store.ts +89 -106
- package/src/contacts/contacts-write.ts +5 -22
- package/src/contacts/types.ts +0 -1
- package/src/context/compactor.ts +88 -54
- package/src/context/strip-injections.ts +58 -10
- package/src/context/token-estimator.ts +1 -1
- package/src/credential-execution/process-manager.ts +55 -14
- package/src/credential-execution/prompted-credential.ts +2 -3
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -2
- package/src/daemon/config-watcher.ts +0 -4
- package/src/daemon/conversation-agent-loop-handlers.ts +2 -0
- package/src/daemon/conversation-agent-loop.ts +114 -22
- package/src/daemon/conversation-history.ts +1 -1
- package/src/daemon/conversation-lifecycle.ts +3 -5
- package/src/daemon/conversation-process.ts +13 -5
- package/src/daemon/conversation-runtime-assembly.ts +13 -15
- package/src/daemon/conversation-slash.ts +2 -23
- package/src/daemon/conversation-surfaces.ts +26 -0
- package/src/daemon/conversation-tool-setup.ts +27 -14
- package/src/daemon/conversation.ts +66 -14
- package/src/daemon/disk-pressure-policy.ts +5 -3
- package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -1
- package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -1
- package/src/daemon/handlers/config-a2a.ts +0 -2
- package/src/daemon/handlers/config-channels.ts +15 -16
- package/src/daemon/handlers/config-slack-channel.ts +22 -3
- package/src/daemon/handlers/conversations.ts +107 -0
- package/src/daemon/host-browser-proxy.ts +41 -0
- package/src/daemon/lifecycle.ts +55 -27
- package/src/daemon/message-provenance.ts +2 -0
- package/src/daemon/message-types/contacts.ts +0 -1
- package/src/daemon/message-types/conversations.ts +3 -3
- package/src/daemon/message-types/sync.ts +0 -1
- package/src/daemon/message-types/web-activity.ts +7 -1
- package/src/daemon/message-types/workflows.ts +83 -1
- package/src/daemon/orphan-reaper.test.ts +0 -19
- package/src/daemon/orphan-reaper.ts +2 -24
- package/src/daemon/server.ts +0 -10
- package/src/daemon/tool-setup-types.ts +4 -0
- package/src/daemon/trust-context.ts +1 -1
- package/src/events/tool-audit-listener.ts +2 -2
- package/src/home/feed-source-enrichment.test.ts +151 -0
- package/src/home/feed-source-enrichment.ts +176 -0
- package/src/home/relationship-state.ts +2 -4
- package/src/instrument.ts +18 -6
- package/src/ipc/__tests__/binary-result-ipc.test.ts +81 -0
- package/src/ipc/__tests__/clients-list-ipc.test.ts +20 -0
- package/src/ipc/assistant-server.ts +37 -4
- package/src/ipc/gateway-flag-listener.ts +18 -2
- package/src/memory/__tests__/auto-analysis-enqueue.test.ts +5 -16
- package/src/memory/__tests__/jobs-store-enqueue-gate.test.ts +7 -11
- package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +37 -7
- package/src/memory/__tests__/memory-retrospective-job.test.ts +229 -401
- package/src/memory/__tests__/onboarding-events-store.test.ts +7 -7
- package/src/memory/auth-fallback-events-store.ts +2 -2
- package/src/memory/auto-analysis-enqueue.ts +3 -5
- package/src/memory/bookmark-crud.ts +1 -2
- package/src/memory/canonical-guardian-store.ts +39 -1
- package/src/memory/conversation-crud.ts +9 -4
- package/src/memory/conversation-key-store.ts +17 -2
- package/src/memory/conversation-title-service.ts +64 -7
- package/src/memory/db-init.ts +17 -17
- package/src/memory/embedding-backend.ts +38 -1
- package/src/memory/embedding-billing-breaker.ts +96 -0
- package/src/memory/jobs-store.ts +25 -13
- package/src/memory/jobs-worker.ts +54 -1
- package/src/memory/lifecycle-events-store.ts +2 -2
- package/src/memory/memory-retrospective-constants.ts +4 -4
- package/src/memory/memory-retrospective-enqueue.ts +31 -6
- package/src/memory/memory-retrospective-job.ts +28 -227
- package/src/memory/migrations/129-contact-channels-access-fields.ts +18 -9
- package/src/memory/migrations/131-drop-legacy-member-guardian-tables.ts +14 -2
- package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +10 -0
- package/src/memory/migrations/291-contact-channels-renormalize-addresses.ts +72 -0
- package/src/memory/migrations/292-schedule-default-no-reuse-conversation.test.ts +67 -0
- package/src/memory/migrations/292-schedule-default-no-reuse-conversation.ts +25 -0
- package/src/memory/migrations/293-workflow-journal-leaf-tokens.ts +32 -0
- package/src/memory/migrations/294-drop-external-user-id.ts +31 -0
- package/src/memory/migrations/295-drop-approval-prompt-ts-tracker.ts +20 -0
- package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.test.ts +110 -0
- package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.ts +68 -0
- package/src/memory/migrations/__tests__/131-drop-legacy-member-guardian-tables.test.ts +154 -0
- package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +31 -0
- package/src/memory/migrations/__tests__/291-contact-channels-renormalize-addresses.test.ts +341 -0
- package/src/memory/migrations/__tests__/run-migrations.test.ts +52 -0
- package/src/memory/migrations/index.ts +6 -0
- package/src/memory/migrations/run-migrations.ts +41 -0
- package/src/memory/migrations/validate-migration-state.ts +1 -1
- package/src/memory/onboarding-events-store.ts +3 -3
- package/src/memory/schema/contacts.ts +0 -5
- package/src/memory/skill-loaded-events-store.test.ts +7 -15
- package/src/memory/skill-loaded-events-store.ts +2 -2
- package/src/memory/tool-executed-events-store.test.ts +7 -7
- package/src/memory/turn-trace-store.test.ts +736 -0
- package/src/memory/turn-trace-store.ts +364 -0
- package/src/memory/v2/__tests__/consolidation-job.test.ts +8 -0
- package/src/memory/v2/__tests__/skill-content.test.ts +30 -0
- package/src/memory/v2/consolidation-job.ts +2 -2
- package/src/memory/v2/skill-content.ts +25 -7
- package/src/memory/v2/skill-store.ts +7 -1
- package/src/memory/v3-eval/__tests__/eval-packets.test.ts +248 -0
- package/src/memory/v3-eval/eval-packets.ts +546 -0
- package/src/messaging/providers/slack/adapter.ts +1 -1
- package/src/messaging/providers/slack/api.ts +31 -0
- package/src/messaging/providers/slack/send.test.ts +114 -2
- package/src/messaging/providers/slack/send.ts +30 -7
- package/src/messaging/providers/slack/withdraw.test.ts +200 -0
- package/src/messaging/providers/slack/withdraw.ts +161 -0
- package/src/notifications/AGENTS.md +2 -0
- package/src/notifications/access-request-copy.ts +72 -59
- package/src/notifications/adapters/shared.ts +29 -0
- package/src/notifications/adapters/slack.ts +58 -103
- package/src/notifications/adapters/telegram.ts +2 -20
- package/src/notifications/approval-card-data.ts +333 -0
- package/src/notifications/broadcaster.ts +16 -3
- package/src/notifications/canonical-delivery-recorder.ts +139 -0
- package/src/notifications/copy-composer.ts +3 -3
- package/src/notifications/decision-engine.ts +4 -2
- package/src/notifications/destination-resolver.ts +4 -6
- package/src/notifications/guardian-question-mode.ts +10 -0
- package/src/notifications/home-feed-side-effect.ts +7 -16
- package/src/notifications/notification-utils.ts +19 -20
- package/src/notifications/signal.ts +79 -43
- package/src/notifications/types.ts +98 -121
- package/src/oauth/AGENTS.md +5 -24
- package/src/permissions/checker.test.ts +51 -0
- package/src/permissions/checker.ts +185 -26
- package/src/permissions/ipc-risk-types.ts +24 -0
- package/src/permissions/question-prompter.test.ts +27 -0
- package/src/permissions/question-prompter.ts +4 -0
- package/src/platform/client.test.ts +119 -0
- package/src/platform/client.ts +66 -0
- package/src/platform/consent-cache.test.ts +267 -0
- package/src/platform/consent-cache.ts +174 -0
- package/src/plugin-api/constants.ts +1 -1
- package/src/plugin-api/index.ts +33 -1
- package/src/plugin-api/model-profiles.ts +33 -0
- package/src/plugin-api/types.ts +50 -2
- package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +56 -0
- package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +43 -0
- package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +137 -0
- package/src/plugins/defaults/advisor/__tests__/consult.test.ts +153 -0
- package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +138 -0
- package/src/plugins/defaults/advisor/__tests__/transcript.test.ts +147 -0
- package/src/plugins/defaults/advisor/advisor-gate.ts +29 -0
- package/src/plugins/defaults/advisor/advisor-state-store.ts +94 -0
- package/src/plugins/defaults/advisor/config.ts +21 -0
- package/src/plugins/defaults/advisor/consult.ts +93 -0
- package/src/plugins/defaults/advisor/hooks/post-model-call.ts +34 -0
- package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +30 -0
- package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +19 -0
- package/src/plugins/defaults/advisor/package.json +14 -0
- package/src/plugins/defaults/advisor/steering.ts +67 -0
- package/src/plugins/defaults/advisor/tools/advisor.ts +65 -0
- package/src/plugins/defaults/advisor/transcript.ts +76 -0
- package/src/plugins/defaults/index.ts +60 -0
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +22 -9
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +2 -2
- package/src/plugins/defaults/memory-retrieval/tail-reinjection-strip.ts +64 -0
- package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +29 -21
- package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +1 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +129 -9
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +31 -4
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +77 -2
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +1 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +7 -10
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +144 -11
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +32 -20
- package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +56 -3
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +23 -2
- package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +276 -0
- package/src/plugins/defaults/surface-completion-nudge/hooks/stop.ts +22 -0
- package/src/plugins/defaults/surface-completion-nudge/nudge-state-store.ts +46 -0
- package/src/plugins/defaults/surface-completion-nudge/package.json +14 -0
- package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +3 -13
- package/src/plugins/defaults/title-generate/hooks/stop.ts +56 -21
- package/src/prompts/persona-resolver.ts +14 -4
- package/src/prompts/templates/system-sections.ts +7 -2
- package/src/providers/__tests__/provider-env-vars.test.ts +6 -0
- package/src/providers/__tests__/provider-secret-catalog.test.ts +1 -0
- package/src/providers/__tests__/retry-callsite.test.ts +176 -0
- package/src/providers/atlascloud/client.ts +85 -0
- package/src/providers/fetch-provider-catalog.ts +85 -0
- package/src/providers/inference/adapter-factory.ts +3 -0
- package/src/providers/model-catalog.ts +58 -0
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +33 -0
- package/src/providers/openai/chat-completions-provider.ts +7 -0
- package/src/providers/openai/responses-provider.ts +10 -0
- package/src/providers/provider-send-message.ts +11 -3
- package/src/providers/retry.ts +53 -12
- package/src/providers/search-provider-catalog.ts +10 -0
- package/src/providers/weak-open-model.ts +22 -0
- package/src/runtime/AGENTS.md +0 -1
- package/src/runtime/__tests__/agent-wake.test.ts +181 -0
- package/src/runtime/__tests__/client-health.test.ts +44 -0
- package/src/runtime/access-request-helper.ts +21 -53
- package/src/runtime/actor-trust-resolver.ts +59 -63
- package/src/runtime/agent-wake.ts +52 -0
- package/src/runtime/assistant-event-hub.ts +18 -4
- package/src/runtime/auth/__tests__/route-policy.test.ts +12 -0
- package/src/runtime/auth/require-bound-guardian.ts +1 -4
- package/src/runtime/btw-sidechain.ts +3 -6
- package/src/runtime/capabilities.test.ts +120 -0
- package/src/runtime/capabilities.ts +197 -0
- package/src/runtime/channel-approval-types.ts +22 -45
- package/src/runtime/channel-invite-transports/telegram.ts +4 -4
- package/src/runtime/channel-retry-sweep.ts +1 -0
- package/src/runtime/channel-verification-service.ts +3 -3
- package/src/runtime/client-health.ts +26 -0
- package/src/runtime/confirmation-request-guardian-bridge.ts +38 -29
- package/src/runtime/effective-capabilities.test.ts +128 -0
- package/src/runtime/effective-capabilities.ts +84 -0
- package/src/runtime/guardian-reply-router.ts +106 -21
- package/src/runtime/invite-redemption-service.ts +9 -25
- package/src/runtime/migrations/__tests__/vbundle-builder-fd-leak.test.ts +123 -0
- package/src/runtime/migrations/vbundle-builder.ts +49 -20
- package/src/runtime/pending-interactions.ts +15 -0
- package/src/runtime/routes/__tests__/client-routes.test.ts +13 -0
- package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +67 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +240 -1
- package/src/runtime/routes/app-routes.ts +1 -1
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -2
- package/src/runtime/routes/assets/vellum-design-system.css +1959 -0
- package/src/runtime/routes/browser-tabs-routes.ts +9 -0
- package/src/runtime/routes/btw-routes.ts +1 -27
- package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +17 -8
- package/src/runtime/routes/client-routes.ts +10 -0
- package/src/runtime/routes/contact-routes.ts +31 -8
- package/src/runtime/routes/conversation-compaction-routes.ts +1 -1
- package/src/runtime/routes/conversation-management-routes.ts +80 -1
- package/src/runtime/routes/conversation-query-routes.ts +68 -22
- package/src/runtime/routes/conversation-routes.ts +39 -14
- package/src/runtime/routes/credential-routes.ts +40 -16
- package/src/runtime/routes/empty-state-greeting-cache.ts +1 -2
- package/src/runtime/routes/events-routes.ts +1 -3
- package/src/runtime/routes/guardian-approval-interception.ts +14 -73
- package/src/runtime/routes/guardian-approval-prompt.ts +22 -4
- package/src/runtime/routes/home-feed-routes.ts +8 -3
- package/src/runtime/routes/identity-routes.ts +1 -296
- package/src/runtime/routes/inbound-message-handler.ts +214 -228
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +89 -7
- package/src/runtime/routes/inbound-stages/admission-policy.test.ts +154 -0
- package/src/runtime/routes/inbound-stages/admission-policy.ts +140 -0
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +3 -3
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +11 -6
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +1 -2
- package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +1 -2
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +7 -7
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +47 -28
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +358 -0
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +8 -0
- package/src/runtime/routes/integrations/slack/channel.ts +36 -0
- package/src/runtime/routes/internal-telemetry-routes.ts +1 -1
- package/src/runtime/routes/mcp-auth-routes.ts +233 -41
- package/src/runtime/routes/memory-eval-routes.ts +87 -0
- package/src/runtime/routes/notification-routes.ts +122 -133
- package/src/runtime/routes/platform-routes.ts +2 -2
- package/src/runtime/routes/plugins-routes.ts +202 -3
- package/src/runtime/routes/schedule-routes.ts +0 -22
- package/src/runtime/routes/secret-routes.ts +10 -0
- package/src/runtime/routes/surface-action-routes.ts +2 -1
- package/src/runtime/routes/tool-call-question-enrichment.test.ts +146 -0
- package/src/runtime/routes/tool-call-question-enrichment.ts +66 -0
- package/src/runtime/routes/workflow-routes.test.ts +229 -44
- package/src/runtime/routes/workflow-routes.ts +131 -29
- package/src/runtime/routes/workspace-greetings.ts +55 -0
- package/src/runtime/sync/resource-sync-events.ts +1 -11
- package/src/runtime/tool-grant-request-helper.ts +18 -16
- package/src/runtime/trust-context-resolver.ts +8 -5
- package/src/schedule/inference-profile.ts +2 -14
- package/src/schedule/schedule-store.ts +1 -1
- package/src/schedule/scheduler-types.ts +5 -1
- package/src/security/__tests__/provider-key-env-fallback.test.ts +6 -0
- package/src/security/secret-patterns.ts +3 -0
- package/src/subagent/manager.ts +17 -4
- package/src/subagent/types.ts +6 -0
- package/src/telemetry/trace-collection-policy.test.ts +28 -0
- package/src/telemetry/trace-collection-policy.ts +30 -0
- package/src/telemetry/types.ts +89 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +586 -36
- package/src/telemetry/usage-telemetry-reporter.ts +148 -41
- package/src/tools/AGENTS.md +3 -3
- package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +31 -0
- package/src/tools/browser/browser-execution.ts +30 -19
- package/src/tools/document/document-tool.ts +2 -3
- package/src/tools/executor.ts +5 -3
- package/src/tools/host-terminal/host-shell.ts +5 -4
- package/src/tools/memory/register.ts +2 -2
- package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +360 -0
- package/src/tools/network/__tests__/web-search.test.ts +143 -0
- package/src/tools/network/web-fetch.ts +372 -1
- package/src/tools/network/web-search-error.ts +1 -1
- package/src/tools/network/web-search.ts +213 -10
- package/src/tools/permission-checker.ts +4 -3
- package/src/tools/registry.ts +20 -0
- package/src/tools/schedule/create.ts +7 -12
- package/src/tools/schedule/update.ts +4 -11
- package/src/tools/shared/filesystem/path-policy.ts +39 -13
- package/src/tools/side-effects.ts +2 -17
- package/src/tools/skills/execute.ts +33 -0
- package/src/tools/subagent/spawn.ts +61 -12
- package/src/tools/terminal/shell.ts +10 -4
- package/src/tools/tool-approval-handler.ts +18 -13
- package/src/tools/tool-manifest.ts +0 -2
- package/src/tools/types.ts +9 -0
- package/src/tools/ui-surface/definitions.ts +64 -3
- package/src/tools/verification-control-plane-policy.ts +3 -1
- package/src/tools/workflows/run-workflow.test.ts +8 -18
- package/src/tools/workflows/run-workflow.ts +1 -0
- package/src/util/disk-usage.ts +78 -23
- package/src/util/platform.ts +10 -3
- package/src/watcher/telemetry.ts +2 -2
- package/src/workflows/capabilities.ts +2 -3
- package/src/workflows/engine.test.ts +175 -1
- package/src/workflows/engine.ts +82 -0
- package/src/workflows/journal-store.test.ts +70 -0
- package/src/workflows/journal-store.ts +18 -3
- package/src/workflows/run-manager.test.ts +171 -28
- package/src/workflows/run-manager.ts +66 -24
- package/src/workspace/migrations/105-enable-memory-v3-live-for-new-workspaces.ts +63 -0
- package/src/workspace/migrations/106-drop-collect-usage-data.ts +47 -0
- package/src/workspace/migrations/107-drop-send-diagnostics.ts +47 -0
- package/src/workspace/migrations/108-drop-balanced-economy-profile.ts +129 -0
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/__tests__/app-control-no-global-cgevent.test.ts +0 -98
- package/src/__tests__/credential-security-e2e.test.ts +0 -362
- package/src/__tests__/credential-vault-unit.test.ts +0 -1528
- package/src/__tests__/credential-vault.test.ts +0 -1706
- package/src/__tests__/identity-intro-cache.test.ts +0 -315
- package/src/__tests__/secret-onetime-send.test.ts +0 -182
- package/src/cli/commands/__tests__/task.test.ts +0 -914
- package/src/cli/commands/task.ts +0 -771
- package/src/config/bundled-skills/personal-page/SKILL.md +0 -57
- package/src/config/bundled-skills/personal-page/TOOLS.json +0 -27
- package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +0 -17
- package/src/config/preloaded-apps/personal-page/src/components/About.tsx +0 -22
- package/src/config/preloaded-apps/personal-page/src/components/App.tsx +0 -16
- package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +0 -77
- package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +0 -57
- package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +0 -28
- package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +0 -234
- package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +0 -48
- package/src/config/preloaded-apps/personal-page/src/components/media.ts +0 -16
- package/src/config/preloaded-apps/personal-page/src/index.html +0 -20
- package/src/config/preloaded-apps/personal-page/src/main.tsx +0 -7
- package/src/config/preloaded-apps/personal-page/src/profile-data.ts +0 -82
- package/src/config/preloaded-apps/personal-page/src/styles.css +0 -759
- package/src/memory/__tests__/preloaded-apps.test.ts +0 -85
- package/src/memory/preloaded-apps.ts +0 -116
- package/src/notifications/tool-approval-copy.ts +0 -142
- package/src/runtime/routes/approval-prompt-ts-tracker.ts +0 -78
- package/src/runtime/routes/identity-intro-cache.ts +0 -172
- package/src/tools/credentials/vault.ts +0 -712
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
writeFileSync,
|
|
19
19
|
} from "node:fs";
|
|
20
20
|
import { tmpdir } from "node:os";
|
|
21
|
-
import { join } from "node:path";
|
|
21
|
+
import { dirname, join } from "node:path";
|
|
22
22
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
23
23
|
|
|
24
24
|
import {
|
|
@@ -26,8 +26,13 @@ import {
|
|
|
26
26
|
type GitRunner,
|
|
27
27
|
PluginSourceUnavailableError,
|
|
28
28
|
} from "../install-from-github.js";
|
|
29
|
+
import { computeFingerprint } from "../plugin-fingerprint.js";
|
|
29
30
|
import { PluginNotInstalledError } from "../uninstall-plugin.js";
|
|
30
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
PluginMergeBaselineError,
|
|
33
|
+
PluginNotUpgradableError,
|
|
34
|
+
upgradePlugin,
|
|
35
|
+
} from "../upgrade-plugin.js";
|
|
31
36
|
|
|
32
37
|
const SHA_A = "a".repeat(40);
|
|
33
38
|
const SHA_B = "b".repeat(40);
|
|
@@ -382,3 +387,291 @@ describe("upgradePlugin", () => {
|
|
|
382
387
|
expect(sidecarCommit(pluginsDir, "level-up")).toBe(SHA_A);
|
|
383
388
|
});
|
|
384
389
|
});
|
|
390
|
+
|
|
391
|
+
/** A file tree keyed by POSIX-relative path. */
|
|
392
|
+
type Tree = Record<string, string>;
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* A fake clone whose materialized tree depends on the fetched ref, so a merge
|
|
396
|
+
* upgrade's base clone (`source.ref` = the recorded install commit) and pin
|
|
397
|
+
* clone (`source.ref` = the marketplace pin) yield different trees. The ref is
|
|
398
|
+
* the last `git fetch` argument; `rev-parse HEAD` echoes it back so the
|
|
399
|
+
* checked-out commit matches the requested ref.
|
|
400
|
+
*/
|
|
401
|
+
function treeGitRunner(treesByRef: Record<string, Tree>): GitRunner {
|
|
402
|
+
const refByCwd = new Map<string, string>();
|
|
403
|
+
return async (args, { cwd }) => {
|
|
404
|
+
switch (args[0]) {
|
|
405
|
+
case "fetch": {
|
|
406
|
+
const ref = args[args.length - 1];
|
|
407
|
+
refByCwd.set(cwd, ref);
|
|
408
|
+
const tree = treesByRef[ref] ?? {};
|
|
409
|
+
for (const [rel, content] of Object.entries(tree)) {
|
|
410
|
+
const abs = join(cwd, rel);
|
|
411
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
412
|
+
writeFileSync(abs, content);
|
|
413
|
+
}
|
|
414
|
+
// `.git` is stripped during materialization; create it so the strip
|
|
415
|
+
// path is exercised, matching a real clone.
|
|
416
|
+
mkdirSync(join(cwd, ".git"), { recursive: true });
|
|
417
|
+
writeFileSync(join(cwd, ".git", "config"), "[core]\n");
|
|
418
|
+
return { stdout: "" };
|
|
419
|
+
}
|
|
420
|
+
case "rev-parse":
|
|
421
|
+
return { stdout: `${refByCwd.get(cwd) ?? ""}\n` };
|
|
422
|
+
default:
|
|
423
|
+
return { stdout: "" };
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Materialize an installed copy of `ours` on disk plus a provenance sidecar
|
|
430
|
+
* recording `commit` and a fingerprint. By default the fingerprint is computed
|
|
431
|
+
* over `base` (what install materialized); pass `fingerprintTree` to record a
|
|
432
|
+
* mismatching baseline.
|
|
433
|
+
*/
|
|
434
|
+
function installMergeCopy(
|
|
435
|
+
name: string,
|
|
436
|
+
ours: Tree,
|
|
437
|
+
commit: string,
|
|
438
|
+
fingerprintTree: Tree | null,
|
|
439
|
+
): void {
|
|
440
|
+
const dir = join(pluginsDir, name);
|
|
441
|
+
mkdirSync(dir, { recursive: true });
|
|
442
|
+
for (const [rel, content] of Object.entries(ours)) {
|
|
443
|
+
const abs = join(dir, rel);
|
|
444
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
445
|
+
writeFileSync(abs, content);
|
|
446
|
+
}
|
|
447
|
+
const sidecar: Record<string, unknown> = {
|
|
448
|
+
origin: "vellum",
|
|
449
|
+
name,
|
|
450
|
+
source: { kind: "github", owner: "example-org", repo: name, ref: commit },
|
|
451
|
+
commit,
|
|
452
|
+
installedAt: "2026-06-10T12:00:00.000Z",
|
|
453
|
+
};
|
|
454
|
+
if (fingerprintTree !== null) {
|
|
455
|
+
const refDir = mkdtempSync(join(tmpdir(), "merge-fingerprint-"));
|
|
456
|
+
try {
|
|
457
|
+
for (const [rel, content] of Object.entries(fingerprintTree)) {
|
|
458
|
+
const abs = join(refDir, rel);
|
|
459
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
460
|
+
writeFileSync(abs, content);
|
|
461
|
+
}
|
|
462
|
+
sidecar.fingerprint = computeFingerprint(refDir, ["install-meta.json"]);
|
|
463
|
+
} finally {
|
|
464
|
+
rmSync(refDir, { recursive: true, force: true });
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
writeFileSync(join(dir, "install-meta.json"), JSON.stringify(sidecar));
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** Read an installed file's contents, or null when absent. */
|
|
471
|
+
function installedFile(name: string, rel: string): string | null {
|
|
472
|
+
const path = join(pluginsDir, name, rel);
|
|
473
|
+
return existsSync(path) ? readFileSync(path, "utf-8") : null;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
describe("upgradePlugin --strategy", () => {
|
|
477
|
+
// base → ours / theirs trees shared by the three-way merge tests:
|
|
478
|
+
// - common.txt: edited on disjoint lines by each side (a clean auto-merge)
|
|
479
|
+
// - conflict.txt: edited differently on both sides (a true conflict)
|
|
480
|
+
// - local-only.txt / remote-only.txt: added on one side only
|
|
481
|
+
const BASE: Tree = {
|
|
482
|
+
"common.txt": "a\nb\nc\n",
|
|
483
|
+
"conflict.txt": "base\n",
|
|
484
|
+
};
|
|
485
|
+
const OURS: Tree = {
|
|
486
|
+
"common.txt": "A\nb\nc\n",
|
|
487
|
+
"conflict.txt": "ours\n",
|
|
488
|
+
"local-only.txt": "added locally\n",
|
|
489
|
+
};
|
|
490
|
+
const THEIRS: Tree = {
|
|
491
|
+
"common.txt": "a\nb\nC\n",
|
|
492
|
+
"conflict.txt": "theirs\n",
|
|
493
|
+
"remote-only.txt": "added upstream\n",
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
test("--strategy ours carries both sides' edits and resolves conflicts toward local", async () => {
|
|
497
|
+
// GIVEN an install at SHA_A with local edits, and a pin at SHA_B
|
|
498
|
+
installMergeCopy("level-up", OURS, SHA_A, BASE);
|
|
499
|
+
const fetch = makeFetch({ manifest: manifestWith("level-up", SHA_B) });
|
|
500
|
+
const runGit = treeGitRunner({ [SHA_A]: BASE, [SHA_B]: THEIRS });
|
|
501
|
+
|
|
502
|
+
// WHEN the plugin is upgraded with the `ours` strategy
|
|
503
|
+
const result = await upgradePlugin(
|
|
504
|
+
{ name: "level-up", strategy: "ours" },
|
|
505
|
+
{ fetch, runGit, workspacePluginsDir: pluginsDir },
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
// THEN it moves to the pin and records the strategy
|
|
509
|
+
expect(result.outcome).toBe("upgraded");
|
|
510
|
+
expect(result.toCommit).toBe(SHA_B);
|
|
511
|
+
expect(result.strategy).toBe("ours");
|
|
512
|
+
// AND non-conflicting edits from both sides survive
|
|
513
|
+
expect(installedFile("level-up", "common.txt")).toBe("A\nb\nC\n");
|
|
514
|
+
expect(installedFile("level-up", "local-only.txt")).toBe("added locally\n");
|
|
515
|
+
expect(installedFile("level-up", "remote-only.txt")).toBe(
|
|
516
|
+
"added upstream\n",
|
|
517
|
+
);
|
|
518
|
+
// AND the conflicting file resolves toward the local edit
|
|
519
|
+
expect(installedFile("level-up", "conflict.txt")).toBe("ours\n");
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
test("--strategy theirs carries both sides' edits and resolves conflicts toward the pin", async () => {
|
|
523
|
+
// GIVEN an install at SHA_A with local edits, and a pin at SHA_B
|
|
524
|
+
installMergeCopy("level-up", OURS, SHA_A, BASE);
|
|
525
|
+
const fetch = makeFetch({ manifest: manifestWith("level-up", SHA_B) });
|
|
526
|
+
const runGit = treeGitRunner({ [SHA_A]: BASE, [SHA_B]: THEIRS });
|
|
527
|
+
|
|
528
|
+
// WHEN the plugin is upgraded with the `theirs` strategy
|
|
529
|
+
const result = await upgradePlugin(
|
|
530
|
+
{ name: "level-up", strategy: "theirs" },
|
|
531
|
+
{ fetch, runGit, workspacePluginsDir: pluginsDir },
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
// THEN non-conflicting edits from both sides still survive
|
|
535
|
+
expect(result.strategy).toBe("theirs");
|
|
536
|
+
expect(installedFile("level-up", "common.txt")).toBe("A\nb\nC\n");
|
|
537
|
+
expect(installedFile("level-up", "local-only.txt")).toBe("added locally\n");
|
|
538
|
+
expect(installedFile("level-up", "remote-only.txt")).toBe(
|
|
539
|
+
"added upstream\n",
|
|
540
|
+
);
|
|
541
|
+
// AND the conflicting file resolves toward the pin
|
|
542
|
+
expect(installedFile("level-up", "conflict.txt")).toBe("theirs\n");
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
test("--strategy overwrite discards local edits and re-installs the pin wholesale", async () => {
|
|
546
|
+
// GIVEN an install at SHA_A with a local-only file, and a pin at SHA_B
|
|
547
|
+
installMergeCopy("level-up", OURS, SHA_A, BASE);
|
|
548
|
+
const fetch = makeFetch({ manifest: manifestWith("level-up", SHA_B) });
|
|
549
|
+
const runGit = treeGitRunner({ [SHA_A]: BASE, [SHA_B]: THEIRS });
|
|
550
|
+
|
|
551
|
+
// WHEN the plugin is upgraded with the `overwrite` strategy
|
|
552
|
+
const result = await upgradePlugin(
|
|
553
|
+
{ name: "level-up", strategy: "overwrite" },
|
|
554
|
+
{ fetch, runGit, workspacePluginsDir: pluginsDir },
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
// THEN the on-disk tree is exactly the pin — local edits are gone
|
|
558
|
+
expect(result.strategy).toBe("overwrite");
|
|
559
|
+
expect(installedFile("level-up", "common.txt")).toBe("a\nb\nC\n");
|
|
560
|
+
expect(installedFile("level-up", "conflict.txt")).toBe("theirs\n");
|
|
561
|
+
expect(installedFile("level-up", "remote-only.txt")).toBe(
|
|
562
|
+
"added upstream\n",
|
|
563
|
+
);
|
|
564
|
+
expect(installedFile("level-up", "local-only.txt")).toBeNull();
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
test("defaults to overwrite when no strategy is given", async () => {
|
|
568
|
+
// GIVEN an install with a local-only file and an advanced pin
|
|
569
|
+
installMergeCopy("level-up", OURS, SHA_A, BASE);
|
|
570
|
+
const fetch = makeFetch({ manifest: manifestWith("level-up", SHA_B) });
|
|
571
|
+
const runGit = treeGitRunner({ [SHA_A]: BASE, [SHA_B]: THEIRS });
|
|
572
|
+
|
|
573
|
+
// WHEN the plugin is upgraded without a strategy
|
|
574
|
+
const result = await upgradePlugin(
|
|
575
|
+
{ name: "level-up" },
|
|
576
|
+
{ fetch, runGit, workspacePluginsDir: pluginsDir },
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
// THEN it overwrites: the local-only file is dropped
|
|
580
|
+
expect(result.strategy).toBe("overwrite");
|
|
581
|
+
expect(installedFile("level-up", "local-only.txt")).toBeNull();
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
test("--strategy assistant writes conflict markers and reports the conflicted path", async () => {
|
|
585
|
+
// GIVEN an install at SHA_A with local edits, and a pin at SHA_B
|
|
586
|
+
installMergeCopy("level-up", OURS, SHA_A, BASE);
|
|
587
|
+
const fetch = makeFetch({ manifest: manifestWith("level-up", SHA_B) });
|
|
588
|
+
const runGit = treeGitRunner({ [SHA_A]: BASE, [SHA_B]: THEIRS });
|
|
589
|
+
|
|
590
|
+
// WHEN the plugin is upgraded with the `assistant` strategy
|
|
591
|
+
const result = await upgradePlugin(
|
|
592
|
+
{ name: "level-up", strategy: "assistant" },
|
|
593
|
+
{ fetch, runGit, workspacePluginsDir: pluginsDir },
|
|
594
|
+
);
|
|
595
|
+
|
|
596
|
+
// THEN it moves to the pin and records the strategy
|
|
597
|
+
expect(result.outcome).toBe("upgraded");
|
|
598
|
+
expect(result.toCommit).toBe(SHA_B);
|
|
599
|
+
expect(result.strategy).toBe("assistant");
|
|
600
|
+
// AND non-conflicting edits from both sides still auto-merge
|
|
601
|
+
expect(installedFile("level-up", "common.txt")).toBe("A\nb\nC\n");
|
|
602
|
+
expect(installedFile("level-up", "local-only.txt")).toBe("added locally\n");
|
|
603
|
+
expect(installedFile("level-up", "remote-only.txt")).toBe(
|
|
604
|
+
"added upstream\n",
|
|
605
|
+
);
|
|
606
|
+
// AND the true conflict carries git markers naming both commits
|
|
607
|
+
const conflict = installedFile("level-up", "conflict.txt") ?? "";
|
|
608
|
+
expect(conflict).toContain("<<<<<<<");
|
|
609
|
+
expect(conflict).toContain(SHA_A.slice(0, 7));
|
|
610
|
+
expect(conflict).toContain(SHA_B.slice(0, 7));
|
|
611
|
+
expect(conflict).toContain("ours\n");
|
|
612
|
+
expect(conflict).toContain("theirs\n");
|
|
613
|
+
// AND the conflicted path is surfaced for the assistant to resolve
|
|
614
|
+
expect(result.conflicts).toEqual(["conflict.txt"]);
|
|
615
|
+
expect(result.binaryConflicts).toEqual([]);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
test("--strategy assistant reports no conflicts on a clean three-way merge", async () => {
|
|
619
|
+
// GIVEN an install whose only divergence from the pin auto-merges cleanly
|
|
620
|
+
const cleanOurs: Tree = { "common.txt": "A\nb\nc\n" };
|
|
621
|
+
const cleanBase: Tree = { "common.txt": "a\nb\nc\n" };
|
|
622
|
+
const cleanTheirs: Tree = { "common.txt": "a\nb\nC\n" };
|
|
623
|
+
installMergeCopy("level-up", cleanOurs, SHA_A, cleanBase);
|
|
624
|
+
const fetch = makeFetch({ manifest: manifestWith("level-up", SHA_B) });
|
|
625
|
+
const runGit = treeGitRunner({ [SHA_A]: cleanBase, [SHA_B]: cleanTheirs });
|
|
626
|
+
|
|
627
|
+
// WHEN the plugin is upgraded with the `assistant` strategy
|
|
628
|
+
const result = await upgradePlugin(
|
|
629
|
+
{ name: "level-up", strategy: "assistant" },
|
|
630
|
+
{ fetch, runGit, workspacePluginsDir: pluginsDir },
|
|
631
|
+
);
|
|
632
|
+
|
|
633
|
+
// THEN both edits merge with no markers and nothing needs resolution
|
|
634
|
+
expect(result.strategy).toBe("assistant");
|
|
635
|
+
expect(result.conflicts).toEqual([]);
|
|
636
|
+
expect(result.binaryConflicts).toEqual([]);
|
|
637
|
+
const merged = installedFile("level-up", "common.txt") ?? "";
|
|
638
|
+
expect(merged).toBe("A\nb\nC\n");
|
|
639
|
+
expect(merged).not.toContain("<<<<<<<");
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
test("throws PluginMergeBaselineError when no fingerprint was recorded", async () => {
|
|
643
|
+
// GIVEN an install whose sidecar records no fingerprint (older install)
|
|
644
|
+
installMergeCopy("level-up", OURS, SHA_A, null);
|
|
645
|
+
const fetch = makeFetch({ manifest: manifestWith("level-up", SHA_B) });
|
|
646
|
+
const runGit = treeGitRunner({ [SHA_A]: BASE, [SHA_B]: THEIRS });
|
|
647
|
+
|
|
648
|
+
// WHEN a merge strategy is requested
|
|
649
|
+
// THEN the baseline cannot be trusted, so the merge is refused
|
|
650
|
+
await expect(
|
|
651
|
+
upgradePlugin(
|
|
652
|
+
{ name: "level-up", strategy: "ours" },
|
|
653
|
+
{ fetch, runGit, workspacePluginsDir: pluginsDir },
|
|
654
|
+
),
|
|
655
|
+
).rejects.toBeInstanceOf(PluginMergeBaselineError);
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
test("throws PluginMergeBaselineError when the re-materialized base drifts from the recorded fingerprint", async () => {
|
|
659
|
+
// GIVEN an install whose recorded fingerprint describes a tree that differs
|
|
660
|
+
// from what re-materializing the install commit produces (an adapter
|
|
661
|
+
// overlay that moved since install)
|
|
662
|
+
installMergeCopy("level-up", OURS, SHA_A, {
|
|
663
|
+
"common.txt": "totally different baseline\n",
|
|
664
|
+
});
|
|
665
|
+
const fetch = makeFetch({ manifest: manifestWith("level-up", SHA_B) });
|
|
666
|
+
const runGit = treeGitRunner({ [SHA_A]: BASE, [SHA_B]: THEIRS });
|
|
667
|
+
|
|
668
|
+
// WHEN a merge strategy is requested
|
|
669
|
+
// THEN the baseline is rejected rather than producing a corrupt merge
|
|
670
|
+
await expect(
|
|
671
|
+
upgradePlugin(
|
|
672
|
+
{ name: "level-up", strategy: "theirs" },
|
|
673
|
+
{ fetch, runGit, workspacePluginsDir: pluginsDir },
|
|
674
|
+
),
|
|
675
|
+
).rejects.toBeInstanceOf(PluginMergeBaselineError);
|
|
676
|
+
});
|
|
677
|
+
});
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Show the unified diff of local edits to an installed plugin, against the
|
|
3
|
+
* exact commit it was installed at.
|
|
4
|
+
*
|
|
5
|
+
* `plugins inspect` already reports *which* files drifted via the install-time
|
|
6
|
+
* per-file fingerprint (see {@link ./plugin-fingerprint}), but that digest is
|
|
7
|
+
* one-way — it cannot reconstruct the original bytes, so it can't show *what*
|
|
8
|
+
* changed. Drift here is classified the same way `inspect` does — against the
|
|
9
|
+
* fingerprint recorded at install — so the two surfaces always agree and a
|
|
10
|
+
* curated adapter overlay that changed since install never misreads as local
|
|
11
|
+
* drift.
|
|
12
|
+
*
|
|
13
|
+
* Showing *what* changed still needs the baseline *bytes*, which the fingerprint
|
|
14
|
+
* cannot reconstruct. Those are re-derived by re-materializing the recorded
|
|
15
|
+
* commit (from `install-meta.json`) through the *same* pipeline install used
|
|
16
|
+
* (see {@link ./install-from-github.materializePluginTree}): a shallow clone at
|
|
17
|
+
* the immutable SHA plus the curated adapter overlay. The adapter overlay is
|
|
18
|
+
* fetched from the canonical repo's current ref, not the install-time ref (which
|
|
19
|
+
* is not recorded), so a re-materialized file can diverge from what install
|
|
20
|
+
* produced. Each baseline file is therefore verified against the recorded
|
|
21
|
+
* fingerprint digest before it is diffed: on a mismatch the install-time bytes
|
|
22
|
+
* cannot be faithfully reconstructed, so the file is flagged rather than diffed
|
|
23
|
+
* against a fabricated baseline.
|
|
24
|
+
*
|
|
25
|
+
* The baseline is always the recorded install commit, not the marketplace's
|
|
26
|
+
* current pin: this answers "what local changes have been made since install",
|
|
27
|
+
* independent of marketplace movement. Comparing against the latest pin is the
|
|
28
|
+
* separate concern `plugins upgrade --dry-run` already covers.
|
|
29
|
+
*
|
|
30
|
+
* Designed for direct programmatic use with injected dependencies, mirroring
|
|
31
|
+
* the sibling plugin libraries. The CLI command `assistant plugins diff <name>`
|
|
32
|
+
* is a thin wrapper that supplies production deps and formats the result.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
36
|
+
import { tmpdir } from "node:os";
|
|
37
|
+
import { join } from "node:path";
|
|
38
|
+
|
|
39
|
+
import { createTwoFilesPatch } from "diff";
|
|
40
|
+
|
|
41
|
+
import { getWorkspacePluginsDir } from "../../util/platform.js";
|
|
42
|
+
import {
|
|
43
|
+
DEFAULT_PLUGIN_REF,
|
|
44
|
+
type FetchLike,
|
|
45
|
+
type GitRunner,
|
|
46
|
+
INSTALL_META_FILENAME,
|
|
47
|
+
materializePluginTree,
|
|
48
|
+
type PluginFetchSource,
|
|
49
|
+
PluginNotFoundError,
|
|
50
|
+
type PostinstallRunner,
|
|
51
|
+
readInstallMeta,
|
|
52
|
+
sanitizePluginName,
|
|
53
|
+
} from "./install-from-github.js";
|
|
54
|
+
import { readInstalledPlugin } from "./list-installed-plugins.js";
|
|
55
|
+
import {
|
|
56
|
+
compareFingerprint,
|
|
57
|
+
computeFingerprint,
|
|
58
|
+
type Fingerprint,
|
|
59
|
+
} from "./plugin-fingerprint.js";
|
|
60
|
+
import { PluginNotInstalledError } from "./uninstall-plugin.js";
|
|
61
|
+
|
|
62
|
+
/** How a file drifted from the install-time baseline. */
|
|
63
|
+
export type PluginFileDiffStatus = "modified" | "added" | "removed";
|
|
64
|
+
|
|
65
|
+
/** Unified diff of a single drifted file. */
|
|
66
|
+
export interface PluginFileDiff {
|
|
67
|
+
/** POSIX-relative path within the plugin root. */
|
|
68
|
+
readonly path: string;
|
|
69
|
+
/** Whether the file was edited, newly added, or deleted since install. */
|
|
70
|
+
readonly status: PluginFileDiffStatus;
|
|
71
|
+
/**
|
|
72
|
+
* Unified diff (`--- a/… / +++ b/…`) of the file's bytes. For a binary file
|
|
73
|
+
* this is a short `Binary files differ` marker instead of a line diff, since
|
|
74
|
+
* a line-based patch of non-text content is noise. When {@link
|
|
75
|
+
* PluginFileDiff.reconstructed} is false this is a short explanatory marker
|
|
76
|
+
* rather than a patch, since the install-time bytes are unavailable.
|
|
77
|
+
*/
|
|
78
|
+
readonly diff: string;
|
|
79
|
+
/** True when either side was detected as binary (NUL byte present). */
|
|
80
|
+
readonly binary: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* True when the install-time baseline for this file was faithfully recovered
|
|
83
|
+
* (its re-materialized bytes hash-match the digest recorded at install).
|
|
84
|
+
* False when the baseline could not be reconstructed — e.g. the curated
|
|
85
|
+
* adapter overlay the file was built from has changed since install — in
|
|
86
|
+
* which case {@link PluginFileDiff.diff} is a marker, not a real patch.
|
|
87
|
+
* Always true for `added` files, whose baseline is the empty side.
|
|
88
|
+
*/
|
|
89
|
+
readonly reconstructed: boolean;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Resolved diff of an installed plugin against its install-time baseline. */
|
|
93
|
+
export interface PluginDiffResult {
|
|
94
|
+
/** Install name. Matches `assistant plugins install <name>`. */
|
|
95
|
+
readonly name: string;
|
|
96
|
+
/** Absolute path to the installed plugin directory. */
|
|
97
|
+
readonly target: string;
|
|
98
|
+
/** Commit the baseline was re-materialized from (the recorded install SHA). */
|
|
99
|
+
readonly commit: string;
|
|
100
|
+
/** ISO-8601 committer timestamp (UTC) of {@link PluginDiffResult.commit}; `null` when unrecorded. */
|
|
101
|
+
readonly committedAt: string | null;
|
|
102
|
+
/** True when the on-disk tree exactly matches the re-materialized baseline. */
|
|
103
|
+
readonly clean: boolean;
|
|
104
|
+
/** One entry per drifted file, sorted by path. Empty when `clean`. */
|
|
105
|
+
readonly files: readonly PluginFileDiff[];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The installed copy carries no resolvable commit, so there is no immutable
|
|
110
|
+
* baseline to re-materialize and diff against.
|
|
111
|
+
*/
|
|
112
|
+
export class PluginDiffUnavailableError extends Error {
|
|
113
|
+
constructor(
|
|
114
|
+
readonly pluginName: string,
|
|
115
|
+
reason: string,
|
|
116
|
+
) {
|
|
117
|
+
super(`Plugin "${pluginName}" cannot be diffed: ${reason}.`);
|
|
118
|
+
this.name = "PluginDiffUnavailableError";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Options that control which plugin to diff. */
|
|
123
|
+
export interface DiffPluginOptions {
|
|
124
|
+
/** Install name (kebab-case directory name). */
|
|
125
|
+
readonly name: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Dependencies injected by the caller. */
|
|
129
|
+
export interface DiffPluginDeps {
|
|
130
|
+
/** HTTP client used to fetch any curated adapter stub. Production callers pass `globalThis.fetch.bind(globalThis)`. */
|
|
131
|
+
readonly fetch: FetchLike;
|
|
132
|
+
/** Override the workspace plugins directory. Falls back to the live workspace. */
|
|
133
|
+
readonly workspacePluginsDir?: string;
|
|
134
|
+
/** Override the git runner used to clone the baseline. Forwarded to {@link materializePluginTree}. */
|
|
135
|
+
readonly runGit?: GitRunner;
|
|
136
|
+
/** Override the postinstall adapter runner. Forwarded to {@link materializePluginTree}. */
|
|
137
|
+
readonly runPostinstall?: PostinstallRunner;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** A NUL byte in the leading bytes is the heuristic git uses to flag a blob as binary. */
|
|
141
|
+
function isBinary(buf: Buffer): boolean {
|
|
142
|
+
const len = Math.min(buf.length, 8000);
|
|
143
|
+
for (let i = 0; i < len; i++) {
|
|
144
|
+
if (buf[i] === 0) return true;
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
interface FileContent {
|
|
150
|
+
readonly text: string;
|
|
151
|
+
readonly binary: boolean;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function readContent(absPath: string): FileContent {
|
|
155
|
+
const buf = readFileSync(absPath);
|
|
156
|
+
const binary = isBinary(buf);
|
|
157
|
+
return { text: binary ? "" : buf.toString("utf8"), binary };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function makeDiff(
|
|
161
|
+
path: string,
|
|
162
|
+
status: PluginFileDiffStatus,
|
|
163
|
+
before: FileContent | null,
|
|
164
|
+
after: FileContent | null,
|
|
165
|
+
reconstructed: boolean,
|
|
166
|
+
): PluginFileDiff {
|
|
167
|
+
if (!reconstructed) {
|
|
168
|
+
return {
|
|
169
|
+
path,
|
|
170
|
+
status,
|
|
171
|
+
diff: `Baseline unavailable (${status}): the install-time content of this file could not be reconstructed — the curated adapter overlay it was built from has changed since install. Reinstall with 'plugins install <name> --force' to refresh the baseline.`,
|
|
172
|
+
binary: false,
|
|
173
|
+
reconstructed: false,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
const binary = (before?.binary ?? false) || (after?.binary ?? false);
|
|
177
|
+
if (binary) {
|
|
178
|
+
return {
|
|
179
|
+
path,
|
|
180
|
+
status,
|
|
181
|
+
diff: `Binary files differ (${status})`,
|
|
182
|
+
binary: true,
|
|
183
|
+
reconstructed: true,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
// `/dev/null` on the absent side and `a/`–`b/` prefixes mirror `git diff`, so
|
|
187
|
+
// the output is familiar and consumable by tools that parse unified diffs.
|
|
188
|
+
const oldName = status === "added" ? "/dev/null" : `a/${path}`;
|
|
189
|
+
const newName = status === "removed" ? "/dev/null" : `b/${path}`;
|
|
190
|
+
const diff = createTwoFilesPatch(
|
|
191
|
+
oldName,
|
|
192
|
+
newName,
|
|
193
|
+
before?.text ?? "",
|
|
194
|
+
after?.text ?? "",
|
|
195
|
+
undefined,
|
|
196
|
+
undefined,
|
|
197
|
+
{ context: 3 },
|
|
198
|
+
);
|
|
199
|
+
return { path, status, diff, binary: false, reconstructed: true };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Recover the install-time bytes of `path` from the re-materialized baseline
|
|
204
|
+
* tree, but only when they faithfully match what install produced: the
|
|
205
|
+
* re-materialized digest must equal the digest `recorded` at install. A
|
|
206
|
+
* mismatch (or a file the re-materialization did not produce) means the
|
|
207
|
+
* install-time content cannot be reconstructed — typically because the curated
|
|
208
|
+
* adapter overlay the file was built from changed since install — so `null` is
|
|
209
|
+
* returned and the caller flags the file instead of diffing fabricated bytes.
|
|
210
|
+
*/
|
|
211
|
+
function baselineContent(
|
|
212
|
+
path: string,
|
|
213
|
+
baselineRoot: string,
|
|
214
|
+
recorded: Fingerprint,
|
|
215
|
+
materialized: Fingerprint,
|
|
216
|
+
): FileContent | null {
|
|
217
|
+
if (materialized.files[path] !== recorded.files[path]) return null;
|
|
218
|
+
return readContent(join(baselineRoot, path));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Build a per-file unified diff for the on-disk install, classifying drift
|
|
223
|
+
* against the fingerprint `recorded` at install — the same baseline `inspect`
|
|
224
|
+
* uses, so the two surfaces always agree on which files changed and an adapter
|
|
225
|
+
* overlay that moved since install never reads as drift. The re-materialized
|
|
226
|
+
* tree supplies the baseline *bytes* for the diff, verified per file against
|
|
227
|
+
* `recorded`. The provenance sidecar is excluded on both sides — it never
|
|
228
|
+
* exists in the baseline and must not read as a local addition.
|
|
229
|
+
*/
|
|
230
|
+
function buildFileDiffs(
|
|
231
|
+
baselineRoot: string,
|
|
232
|
+
target: string,
|
|
233
|
+
recorded: Fingerprint,
|
|
234
|
+
): PluginFileDiff[] {
|
|
235
|
+
const comparison = compareFingerprint(target, recorded, [
|
|
236
|
+
INSTALL_META_FILENAME,
|
|
237
|
+
]);
|
|
238
|
+
const materialized = computeFingerprint(baselineRoot, [
|
|
239
|
+
INSTALL_META_FILENAME,
|
|
240
|
+
]);
|
|
241
|
+
|
|
242
|
+
const files: PluginFileDiff[] = [];
|
|
243
|
+
for (const path of comparison.modified) {
|
|
244
|
+
const before = baselineContent(path, baselineRoot, recorded, materialized);
|
|
245
|
+
files.push(
|
|
246
|
+
makeDiff(
|
|
247
|
+
path,
|
|
248
|
+
"modified",
|
|
249
|
+
before,
|
|
250
|
+
readContent(join(target, path)),
|
|
251
|
+
before !== null,
|
|
252
|
+
),
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
for (const path of comparison.added) {
|
|
256
|
+
files.push(
|
|
257
|
+
makeDiff(path, "added", null, readContent(join(target, path)), true),
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
for (const path of comparison.removed) {
|
|
261
|
+
const before = baselineContent(path, baselineRoot, recorded, materialized);
|
|
262
|
+
files.push(makeDiff(path, "removed", before, null, before !== null));
|
|
263
|
+
}
|
|
264
|
+
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
265
|
+
return files;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Resolve the unified diff of an installed plugin against its install-time
|
|
270
|
+
* baseline.
|
|
271
|
+
*
|
|
272
|
+
* Throws {@link PluginNotInstalledError} when no copy is installed,
|
|
273
|
+
* {@link PluginDiffUnavailableError} when the install recorded no commit to
|
|
274
|
+
* re-materialize, {@link PluginNotFoundError} when the recorded commit can no
|
|
275
|
+
* longer be fetched (e.g. the source repo or commit was removed), and
|
|
276
|
+
* propagates {@link materializePluginTree}'s errors (e.g. source unavailable)
|
|
277
|
+
* when the baseline clone itself fails.
|
|
278
|
+
*/
|
|
279
|
+
export async function diffPlugin(
|
|
280
|
+
opts: DiffPluginOptions,
|
|
281
|
+
deps: DiffPluginDeps,
|
|
282
|
+
): Promise<PluginDiffResult> {
|
|
283
|
+
const name = sanitizePluginName(opts.name);
|
|
284
|
+
const dir = deps.workspacePluginsDir ?? getWorkspacePluginsDir();
|
|
285
|
+
const target = join(dir, name);
|
|
286
|
+
|
|
287
|
+
if (!readInstalledPlugin(name, { workspacePluginsDir: dir })) {
|
|
288
|
+
throw new PluginNotInstalledError(name, target);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const meta = readInstallMeta(target);
|
|
292
|
+
const commit = meta?.commit ?? null;
|
|
293
|
+
if (!meta || !commit) {
|
|
294
|
+
throw new PluginDiffUnavailableError(
|
|
295
|
+
name,
|
|
296
|
+
`no install commit was recorded (an older or manually-copied install); reinstall with 'assistant plugins install ${name} --force' to record provenance`,
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
// Drift is classified against the install-time fingerprint (as `inspect`
|
|
300
|
+
// does), so without it there is no trustworthy baseline to diff against.
|
|
301
|
+
const recorded = meta.fingerprint;
|
|
302
|
+
if (!recorded) {
|
|
303
|
+
throw new PluginDiffUnavailableError(
|
|
304
|
+
name,
|
|
305
|
+
`no install-time fingerprint was recorded (an older or manually-copied install); reinstall with 'assistant plugins install ${name} --force' to record provenance`,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const source: PluginFetchSource = {
|
|
310
|
+
owner: meta.source.owner,
|
|
311
|
+
repo: meta.source.repo,
|
|
312
|
+
rootPath: meta.source.path ?? "",
|
|
313
|
+
ref: commit,
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const baselineRoot = mkdtempSync(join(tmpdir(), `plugin-diff-${name}-`));
|
|
317
|
+
try {
|
|
318
|
+
const materialized = await materializePluginTree(
|
|
319
|
+
{ source, name, stubRef: DEFAULT_PLUGIN_REF, destDir: baselineRoot },
|
|
320
|
+
{
|
|
321
|
+
fetch: deps.fetch,
|
|
322
|
+
runGit: deps.runGit,
|
|
323
|
+
runPostinstall: deps.runPostinstall,
|
|
324
|
+
},
|
|
325
|
+
);
|
|
326
|
+
if (materialized.fileCount === 0) {
|
|
327
|
+
throw new PluginNotFoundError(
|
|
328
|
+
name,
|
|
329
|
+
commit,
|
|
330
|
+
`${source.owner}/${source.repo}`,
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const files = buildFileDiffs(baselineRoot, target, recorded);
|
|
335
|
+
return {
|
|
336
|
+
name,
|
|
337
|
+
target,
|
|
338
|
+
commit,
|
|
339
|
+
committedAt: meta.committedAt ?? materialized.committedAt ?? null,
|
|
340
|
+
clean: files.length === 0,
|
|
341
|
+
files,
|
|
342
|
+
};
|
|
343
|
+
} finally {
|
|
344
|
+
rmSync(baselineRoot, { recursive: true, force: true });
|
|
345
|
+
}
|
|
346
|
+
}
|