@machina.ai/cell-cli 1.11.0-rc1 → 1.13.0-rc2
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/package.json +16 -10
- package/dist/src/commands/extensions/disable.d.ts +1 -1
- package/dist/src/commands/extensions/disable.js +15 -7
- package/dist/src/commands/extensions/disable.js.map +1 -1
- package/dist/src/commands/extensions/enable.d.ts +1 -1
- package/dist/src/commands/extensions/enable.js +15 -7
- package/dist/src/commands/extensions/enable.js.map +1 -1
- package/dist/src/commands/extensions/install.js +14 -3
- package/dist/src/commands/extensions/install.js.map +1 -1
- package/dist/src/commands/extensions/install.test.js +39 -19
- package/dist/src/commands/extensions/install.test.js.map +1 -1
- package/dist/src/commands/extensions/link.js +14 -3
- package/dist/src/commands/extensions/link.js.map +1 -1
- package/dist/src/commands/extensions/list.js +13 -4
- package/dist/src/commands/extensions/list.js.map +1 -1
- package/dist/src/commands/extensions/uninstall.js +13 -2
- package/dist/src/commands/extensions/uninstall.js.map +1 -1
- package/dist/src/commands/extensions/update.js +18 -13
- package/dist/src/commands/extensions/update.js.map +1 -1
- package/dist/src/commands/extensions/validate.d.ts +12 -0
- package/dist/src/commands/extensions/validate.js +83 -0
- package/dist/src/commands/extensions/validate.js.map +1 -0
- package/dist/src/commands/extensions/validate.test.js +93 -0
- package/dist/src/commands/extensions/validate.test.js.map +1 -0
- package/dist/src/commands/extensions.js +3 -0
- package/dist/src/commands/extensions.js.map +1 -1
- package/dist/src/commands/mcp/add.test.js +3 -0
- package/dist/src/commands/mcp/add.test.js.map +1 -1
- package/dist/src/commands/mcp/list.js +10 -3
- package/dist/src/commands/mcp/list.js.map +1 -1
- package/dist/src/commands/mcp/list.test.js +37 -27
- package/dist/src/commands/mcp/list.test.js.map +1 -1
- package/dist/src/config/auth.js +0 -5
- package/dist/src/config/auth.js.map +1 -1
- package/dist/src/config/config.d.ts +6 -3
- package/dist/src/config/config.js +65 -80
- package/dist/src/config/config.js.map +1 -1
- package/dist/src/config/config.test.js +235 -212
- package/dist/src/config/config.test.js.map +1 -1
- package/dist/src/config/extension-manager.d.ts +63 -0
- package/dist/src/config/extension-manager.js +450 -0
- package/dist/src/config/extension-manager.js.map +1 -0
- package/dist/src/config/extension.d.ts +4 -51
- package/dist/src/config/extension.js +1 -535
- package/dist/src/config/extension.js.map +1 -1
- package/dist/src/config/extension.test.js +525 -201
- package/dist/src/config/extension.test.js.map +1 -1
- package/dist/src/config/extensions/consent.d.ts +38 -0
- package/dist/src/config/extensions/consent.js +123 -0
- package/dist/src/config/extensions/consent.js.map +1 -0
- package/dist/src/config/extensions/extensionEnablement.d.ts +1 -1
- package/dist/src/config/extensions/extensionEnablement.js +4 -3
- package/dist/src/config/extensions/extensionEnablement.js.map +1 -1
- package/dist/src/config/extensions/extensionEnablement.test.js +10 -10
- package/dist/src/config/extensions/extensionEnablement.test.js.map +1 -1
- package/dist/src/config/extensions/extensionSettings.d.ts +15 -0
- package/dist/src/config/extensions/extensionSettings.js +113 -0
- package/dist/src/config/extensions/extensionSettings.js.map +1 -0
- package/dist/src/config/extensions/extensionSettings.test.d.ts +6 -0
- package/dist/src/config/extensions/extensionSettings.test.js +254 -0
- package/dist/src/config/extensions/extensionSettings.test.js.map +1 -0
- package/dist/src/config/extensions/github.d.ts +2 -2
- package/dist/src/config/extensions/github.js +5 -10
- package/dist/src/config/extensions/github.js.map +1 -1
- package/dist/src/config/extensions/github.test.js +153 -167
- package/dist/src/config/extensions/github.test.js.map +1 -1
- package/dist/src/config/extensions/github_fetch.d.ts +1 -1
- package/dist/src/config/extensions/github_fetch.js +13 -1
- package/dist/src/config/extensions/github_fetch.js.map +1 -1
- package/dist/src/config/extensions/github_fetch.test.d.ts +6 -0
- package/dist/src/config/extensions/github_fetch.test.js +169 -0
- package/dist/src/config/extensions/github_fetch.test.js.map +1 -0
- package/dist/src/config/extensions/storage.d.ts +14 -0
- package/dist/src/config/extensions/storage.js +32 -0
- package/dist/src/config/extensions/storage.js.map +1 -0
- package/dist/src/config/extensions/update.d.ts +4 -4
- package/dist/src/config/extensions/update.js +39 -39
- package/dist/src/config/extensions/update.js.map +1 -1
- package/dist/src/config/extensions/update.test.js +72 -74
- package/dist/src/config/extensions/update.test.js.map +1 -1
- package/dist/src/config/extensions/variableSchema.d.ts +0 -6
- package/dist/src/config/extensions/variableSchema.js.map +1 -1
- package/dist/src/config/extensions/variables.d.ts +4 -0
- package/dist/src/config/extensions/variables.js +6 -0
- package/dist/src/config/extensions/variables.js.map +1 -1
- package/dist/src/config/keyBindings.d.ts +3 -0
- package/dist/src/config/keyBindings.js +30 -8
- package/dist/src/config/keyBindings.js.map +1 -1
- package/dist/src/config/keyBindings.test.js +17 -0
- package/dist/src/config/keyBindings.test.js.map +1 -1
- package/dist/src/config/policies/read-only.toml +56 -0
- package/dist/src/config/policies/write.toml +63 -0
- package/dist/src/config/policies/yolo.toml +31 -0
- package/dist/src/config/policy-engine.integration.test.js +41 -38
- package/dist/src/config/policy-engine.integration.test.js.map +1 -1
- package/dist/src/config/policy.d.ts +2 -2
- package/dist/src/config/policy.js +10 -148
- package/dist/src/config/policy.js.map +1 -1
- package/dist/src/config/sandboxConfig.d.ts +1 -1
- package/dist/src/config/sandboxConfig.js +6 -3
- package/dist/src/config/sandboxConfig.js.map +1 -1
- package/dist/src/config/settings.d.ts +2 -1
- package/dist/src/config/settings.js +58 -18
- package/dist/src/config/settings.js.map +1 -1
- package/dist/src/config/settings.test.js +128 -69
- package/dist/src/config/settings.test.js.map +1 -1
- package/dist/src/config/settingsSchema.d.ts +170 -28
- package/dist/src/config/settingsSchema.js +418 -27
- package/dist/src/config/settingsSchema.js.map +1 -1
- package/dist/src/config/settingsSchema.test.js +42 -1
- package/dist/src/config/settingsSchema.test.js.map +1 -1
- package/dist/src/config/trustedFolders.d.ts +1 -1
- package/dist/src/config/trustedFolders.js +4 -2
- package/dist/src/config/trustedFolders.js.map +1 -1
- package/dist/src/core/initializer.js +2 -1
- package/dist/src/core/initializer.js.map +1 -1
- package/dist/src/gemini.d.ts +1 -1
- package/dist/src/gemini.js +46 -16
- package/dist/src/gemini.js.map +1 -1
- package/dist/src/gemini.test.js +88 -30
- package/dist/src/gemini.test.js.map +1 -1
- package/dist/src/generated/git-commit.d.ts +2 -2
- package/dist/src/generated/git-commit.js +2 -2
- package/dist/src/nonInteractiveCli.d.ts +9 -1
- package/dist/src/nonInteractiveCli.js +114 -7
- package/dist/src/nonInteractiveCli.js.map +1 -1
- package/dist/src/nonInteractiveCli.test.js +355 -112
- package/dist/src/nonInteractiveCli.test.js.map +1 -1
- package/dist/src/services/BuiltinCommandLoader.js +4 -0
- package/dist/src/services/BuiltinCommandLoader.js.map +1 -1
- package/dist/src/services/BuiltinCommandLoader.test.js +22 -0
- package/dist/src/services/BuiltinCommandLoader.test.js.map +1 -1
- package/dist/src/services/FeedbackService.js +2 -2
- package/dist/src/services/FeedbackService.js.map +1 -1
- package/dist/src/services/McpPromptLoader.js +2 -2
- package/dist/src/services/McpPromptLoader.js.map +1 -1
- package/dist/src/services/McpPromptLoader.test.js +4 -2
- package/dist/src/services/McpPromptLoader.test.js.map +1 -1
- package/dist/src/test-utils/async.d.ts +9 -0
- package/dist/src/test-utils/async.js +29 -0
- package/dist/src/test-utils/async.js.map +1 -0
- package/dist/src/test-utils/createExtension.d.ts +3 -1
- package/dist/src/test-utils/createExtension.js +3 -3
- package/dist/src/test-utils/createExtension.js.map +1 -1
- package/dist/src/test-utils/render.d.ts +16 -2
- package/dist/src/test-utils/render.js +66 -4
- package/dist/src/test-utils/render.js.map +1 -1
- package/dist/src/test-utils/render.test.d.ts +6 -0
- package/dist/src/test-utils/render.test.js +79 -0
- package/dist/src/test-utils/render.test.js.map +1 -0
- package/dist/src/ui/App.test.js +1 -1
- package/dist/src/ui/App.test.js.map +1 -1
- package/dist/src/ui/AppContainer.js +181 -65
- package/dist/src/ui/AppContainer.js.map +1 -1
- package/dist/src/ui/AppContainer.test.js +505 -147
- package/dist/src/ui/AppContainer.test.js.map +1 -1
- package/dist/src/ui/IdeIntegrationNudge.js +1 -1
- package/dist/src/ui/IdeIntegrationNudge.js.map +1 -1
- package/dist/src/ui/auth/ApiAuthDialog.d.ts +14 -0
- package/dist/src/ui/auth/ApiAuthDialog.js +26 -0
- package/dist/src/ui/auth/ApiAuthDialog.js.map +1 -0
- package/dist/src/ui/auth/ApiAuthDialog.test.d.ts +6 -0
- package/dist/src/ui/auth/ApiAuthDialog.test.js +91 -0
- package/dist/src/ui/auth/ApiAuthDialog.test.js.map +1 -0
- package/dist/src/ui/auth/AuthDialog.js +7 -3
- package/dist/src/ui/auth/AuthDialog.js.map +1 -1
- package/dist/src/ui/auth/useAuth.d.ts +2 -0
- package/dist/src/ui/auth/useAuth.js +31 -2
- package/dist/src/ui/auth/useAuth.js.map +1 -1
- package/dist/src/ui/colors.js +3 -0
- package/dist/src/ui/colors.js.map +1 -1
- package/dist/src/ui/commands/directoryCommand.js +1 -1
- package/dist/src/ui/commands/directoryCommand.js.map +1 -1
- package/dist/src/ui/commands/extensionsCommand.js +64 -11
- package/dist/src/ui/commands/extensionsCommand.js.map +1 -1
- package/dist/src/ui/commands/extensionsCommand.test.js +72 -1
- package/dist/src/ui/commands/extensionsCommand.test.js.map +1 -1
- package/dist/src/ui/commands/mcpCommand.js +14 -14
- package/dist/src/ui/commands/mcpCommand.js.map +1 -1
- package/dist/src/ui/commands/mcpCommand.test.js +4 -0
- package/dist/src/ui/commands/mcpCommand.test.js.map +1 -1
- package/dist/src/ui/commands/memoryCommand.js +1 -1
- package/dist/src/ui/commands/memoryCommand.js.map +1 -1
- package/dist/src/ui/commands/memoryCommand.test.js +3 -1
- package/dist/src/ui/commands/memoryCommand.test.js.map +1 -1
- package/dist/src/ui/commands/policiesCommand.d.ts +7 -0
- package/dist/src/ui/commands/policiesCommand.js +59 -0
- package/dist/src/ui/commands/policiesCommand.js.map +1 -0
- package/dist/src/ui/commands/policiesCommand.test.d.ts +6 -0
- package/dist/src/ui/commands/policiesCommand.test.js +83 -0
- package/dist/src/ui/commands/policiesCommand.test.js.map +1 -0
- package/dist/src/ui/components/AnsiOutput.test.js +1 -1
- package/dist/src/ui/components/AnsiOutput.test.js.map +1 -1
- package/dist/src/ui/components/AsciiArt.d.ts +3 -3
- package/dist/src/ui/components/AsciiArt.js +3 -3
- package/dist/src/ui/components/Composer.js +1 -1
- package/dist/src/ui/components/Composer.js.map +1 -1
- package/dist/src/ui/components/Composer.test.js +5 -2
- package/dist/src/ui/components/Composer.test.js.map +1 -1
- package/dist/src/ui/components/ConfigInitDisplay.js +4 -6
- package/dist/src/ui/components/ConfigInitDisplay.js.map +1 -1
- package/dist/src/ui/components/ConsentPrompt.test.js +18 -8
- package/dist/src/ui/components/ConsentPrompt.test.js.map +1 -1
- package/dist/src/ui/components/ConsoleSummaryDisplay.js +1 -1
- package/dist/src/ui/components/ConsoleSummaryDisplay.js.map +1 -1
- package/dist/src/ui/components/ContextSummaryDisplay.test.js +11 -6
- package/dist/src/ui/components/ContextSummaryDisplay.test.js.map +1 -1
- package/dist/src/ui/components/DetailedMessagesDisplay.js +1 -1
- package/dist/src/ui/components/DetailedMessagesDisplay.js.map +1 -1
- package/dist/src/ui/components/DialogManager.js +4 -0
- package/dist/src/ui/components/DialogManager.js.map +1 -1
- package/dist/src/ui/components/FolderTrustDialog.test.js +2 -1
- package/dist/src/ui/components/FolderTrustDialog.test.js.map +1 -1
- package/dist/src/ui/components/Footer.js +4 -3
- package/dist/src/ui/components/Footer.js.map +1 -1
- package/dist/src/ui/components/Footer.test.js +83 -0
- package/dist/src/ui/components/Footer.test.js.map +1 -1
- package/dist/src/ui/components/Header.test.js +13 -5
- package/dist/src/ui/components/Header.test.js.map +1 -1
- package/dist/src/ui/components/Help.test.js +5 -4
- package/dist/src/ui/components/Help.test.js.map +1 -1
- package/dist/src/ui/components/HistoryItemDisplay.js +1 -1
- package/dist/src/ui/components/HistoryItemDisplay.js.map +1 -1
- package/dist/src/ui/components/InputPrompt.js +27 -8
- package/dist/src/ui/components/InputPrompt.js.map +1 -1
- package/dist/src/ui/components/InputPrompt.test.js +776 -727
- package/dist/src/ui/components/InputPrompt.test.js.map +1 -1
- package/dist/src/ui/components/LoadingIndicator.js +2 -2
- package/dist/src/ui/components/LoadingIndicator.js.map +1 -1
- package/dist/src/ui/components/LoadingIndicator.test.js +28 -15
- package/dist/src/ui/components/LoadingIndicator.test.js.map +1 -1
- package/dist/src/ui/components/LoopDetectionConfirmation.js +1 -1
- package/dist/src/ui/components/LoopDetectionConfirmation.js.map +1 -1
- package/dist/src/ui/components/LoopDetectionConfirmation.test.js +2 -2
- package/dist/src/ui/components/LoopDetectionConfirmation.test.js.map +1 -1
- package/dist/src/ui/components/MainContent.js +15 -4
- package/dist/src/ui/components/MainContent.js.map +1 -1
- package/dist/src/ui/components/ModelDialog.js +1 -1
- package/dist/src/ui/components/ModelDialog.js.map +1 -1
- package/dist/src/ui/components/ModelDialog.test.js +23 -13
- package/dist/src/ui/components/ModelDialog.test.js.map +1 -1
- package/dist/src/ui/components/ModelStatsDisplay.test.js +1 -1
- package/dist/src/ui/components/ModelStatsDisplay.test.js.map +1 -1
- package/dist/src/ui/components/Notifications.js +38 -5
- package/dist/src/ui/components/Notifications.js.map +1 -1
- package/dist/src/ui/components/PermissionsModifyTrustDialog.test.js +2 -2
- package/dist/src/ui/components/PermissionsModifyTrustDialog.test.js.map +1 -1
- package/dist/src/ui/components/PrepareLabel.test.js +14 -8
- package/dist/src/ui/components/PrepareLabel.test.js.map +1 -1
- package/dist/src/ui/components/ProQuotaDialog.test.js +14 -6
- package/dist/src/ui/components/ProQuotaDialog.test.js.map +1 -1
- package/dist/src/ui/components/QueuedMessageDisplay.test.js +11 -6
- package/dist/src/ui/components/QueuedMessageDisplay.test.js.map +1 -1
- package/dist/src/ui/components/SessionSummaryDisplay.test.js +1 -1
- package/dist/src/ui/components/SessionSummaryDisplay.test.js.map +1 -1
- package/dist/src/ui/components/SettingsDialog.js +32 -25
- package/dist/src/ui/components/SettingsDialog.js.map +1 -1
- package/dist/src/ui/components/SettingsDialog.test.js +428 -532
- package/dist/src/ui/components/SettingsDialog.test.js.map +1 -1
- package/dist/src/ui/components/ShellConfirmationDialog.js +1 -1
- package/dist/src/ui/components/ShellConfirmationDialog.js.map +1 -1
- package/dist/src/ui/components/ShellConfirmationDialog.test.js +2 -2
- package/dist/src/ui/components/ShellConfirmationDialog.test.js.map +1 -1
- package/dist/src/ui/components/StatsDisplay.test.js +1 -1
- package/dist/src/ui/components/StatsDisplay.test.js.map +1 -1
- package/dist/src/ui/components/SuggestionsDisplay.js +1 -1
- package/dist/src/ui/components/SuggestionsDisplay.js.map +1 -1
- package/dist/src/ui/components/ThemeDialog.test.js +2 -2
- package/dist/src/ui/components/ThemeDialog.test.js.map +1 -1
- package/dist/src/ui/components/ToolStatsDisplay.test.js +1 -1
- package/dist/src/ui/components/ToolStatsDisplay.test.js.map +1 -1
- package/dist/src/ui/components/messages/CompressionMessage.test.js +25 -17
- package/dist/src/ui/components/messages/CompressionMessage.test.js.map +1 -1
- package/dist/src/ui/components/messages/DiffRenderer.test.js +1 -1
- package/dist/src/ui/components/messages/DiffRenderer.test.js.map +1 -1
- package/dist/src/ui/components/messages/InfoMessage.js +1 -1
- package/dist/src/ui/components/messages/InfoMessage.js.map +1 -1
- package/dist/src/ui/components/messages/Todo.js +27 -5
- package/dist/src/ui/components/messages/Todo.js.map +1 -1
- package/dist/src/ui/components/messages/Todo.test.js +20 -8
- package/dist/src/ui/components/messages/Todo.test.js.map +1 -1
- package/dist/src/ui/components/messages/ToolConfirmationMessage.js +1 -1
- package/dist/src/ui/components/messages/ToolConfirmationMessage.js.map +1 -1
- package/dist/src/ui/components/messages/ToolGroupMessage.test.js +29 -15
- package/dist/src/ui/components/messages/ToolGroupMessage.test.js.map +1 -1
- package/dist/src/ui/components/messages/WarningMessage.js +2 -2
- package/dist/src/ui/components/messages/WarningMessage.js.map +1 -1
- package/dist/src/ui/components/shared/BaseSelectionList.test.js +1 -1
- package/dist/src/ui/components/shared/BaseSelectionList.test.js.map +1 -1
- package/dist/src/ui/components/shared/MaxSizedBox.test.js +43 -22
- package/dist/src/ui/components/shared/MaxSizedBox.test.js.map +1 -1
- package/dist/src/ui/components/shared/TextInput.d.ts +15 -0
- package/dist/src/ui/components/shared/TextInput.js +38 -0
- package/dist/src/ui/components/shared/TextInput.js.map +1 -0
- package/dist/src/ui/components/shared/TextInput.test.d.ts +6 -0
- package/dist/src/ui/components/shared/TextInput.test.js +242 -0
- package/dist/src/ui/components/shared/TextInput.test.js.map +1 -0
- package/dist/src/ui/components/shared/text-buffer.d.ts +9 -2
- package/dist/src/ui/components/shared/text-buffer.js +51 -13
- package/dist/src/ui/components/shared/text-buffer.js.map +1 -1
- package/dist/src/ui/components/shared/text-buffer.test.js +385 -202
- package/dist/src/ui/components/shared/text-buffer.test.js.map +1 -1
- package/dist/src/ui/components/views/ChatList.test.js +7 -4
- package/dist/src/ui/components/views/ChatList.test.js.map +1 -1
- package/dist/src/ui/components/views/ExtensionsList.d.ts +7 -1
- package/dist/src/ui/components/views/ExtensionsList.js +9 -11
- package/dist/src/ui/components/views/ExtensionsList.js.map +1 -1
- package/dist/src/ui/components/views/ExtensionsList.test.js +43 -22
- package/dist/src/ui/components/views/ExtensionsList.test.js.map +1 -1
- package/dist/src/ui/components/views/McpStatus.test.js +23 -12
- package/dist/src/ui/components/views/McpStatus.test.js.map +1 -1
- package/dist/src/ui/contexts/KeypressContext.d.ts +3 -2
- package/dist/src/ui/contexts/KeypressContext.js +610 -540
- package/dist/src/ui/contexts/KeypressContext.js.map +1 -1
- package/dist/src/ui/contexts/KeypressContext.test.js +438 -718
- package/dist/src/ui/contexts/KeypressContext.test.js.map +1 -1
- package/dist/src/ui/contexts/MouseContext.d.ts +21 -0
- package/dist/src/ui/contexts/MouseContext.js +89 -0
- package/dist/src/ui/contexts/MouseContext.js.map +1 -0
- package/dist/src/ui/contexts/MouseContext.test.d.ts +6 -0
- package/dist/src/ui/contexts/MouseContext.test.js +164 -0
- package/dist/src/ui/contexts/MouseContext.test.js.map +1 -0
- package/dist/src/ui/contexts/SessionContext.test.js +35 -17
- package/dist/src/ui/contexts/SessionContext.test.js.map +1 -1
- package/dist/src/ui/contexts/UIActionsContext.d.ts +2 -0
- package/dist/src/ui/contexts/UIActionsContext.js.map +1 -1
- package/dist/src/ui/contexts/UIStateContext.d.ts +2 -0
- package/dist/src/ui/contexts/UIStateContext.js.map +1 -1
- package/dist/src/ui/hooks/atCommandProcessor.js +31 -9
- package/dist/src/ui/hooks/atCommandProcessor.js.map +1 -1
- package/dist/src/ui/hooks/atCommandProcessor.test.js +163 -64
- package/dist/src/ui/hooks/atCommandProcessor.test.js.map +1 -1
- package/dist/src/ui/hooks/shellCommandProcessor.test.js +64 -35
- package/dist/src/ui/hooks/shellCommandProcessor.test.js.map +1 -1
- package/dist/src/ui/hooks/slashCommandProcessor.test.js +193 -165
- package/dist/src/ui/hooks/slashCommandProcessor.test.js.map +1 -1
- package/dist/src/ui/hooks/useAtCompletion.test.js +16 -5
- package/dist/src/ui/hooks/useAtCompletion.test.js.map +1 -1
- package/dist/src/ui/hooks/useAutoAcceptIndicator.js +10 -0
- package/dist/src/ui/hooks/useAutoAcceptIndicator.js.map +1 -1
- package/dist/src/ui/hooks/useAutoAcceptIndicator.test.js +32 -1
- package/dist/src/ui/hooks/useAutoAcceptIndicator.test.js.map +1 -1
- package/dist/src/ui/hooks/useCommandCompletion.test.js +66 -64
- package/dist/src/ui/hooks/useCommandCompletion.test.js.map +1 -1
- package/dist/src/ui/hooks/useConsoleMessages.test.js +26 -9
- package/dist/src/ui/hooks/useConsoleMessages.test.js.map +1 -1
- package/dist/src/ui/hooks/useEditorSettings.test.js +40 -34
- package/dist/src/ui/hooks/useEditorSettings.test.js.map +1 -1
- package/dist/src/ui/hooks/useExtensionUpdates.d.ts +14 -5
- package/dist/src/ui/hooks/useExtensionUpdates.js +18 -13
- package/dist/src/ui/hooks/useExtensionUpdates.js.map +1 -1
- package/dist/src/ui/hooks/useExtensionUpdates.test.js +49 -44
- package/dist/src/ui/hooks/useExtensionUpdates.test.js.map +1 -1
- package/dist/src/ui/hooks/useFlickerDetector.test.js +9 -5
- package/dist/src/ui/hooks/useFlickerDetector.test.js.map +1 -1
- package/dist/src/ui/hooks/useFocus.test.js +25 -9
- package/dist/src/ui/hooks/useFocus.test.js.map +1 -1
- package/dist/src/ui/hooks/useFolderTrust.test.js +46 -22
- package/dist/src/ui/hooks/useFolderTrust.test.js.map +1 -1
- package/dist/src/ui/hooks/useGeminiStream.js +56 -19
- package/dist/src/ui/hooks/useGeminiStream.js.map +1 -1
- package/dist/src/ui/hooks/useGeminiStream.test.js +260 -411
- package/dist/src/ui/hooks/useGeminiStream.test.js.map +1 -1
- package/dist/src/ui/hooks/useGitBranchName.js +4 -0
- package/dist/src/ui/hooks/useGitBranchName.js.map +1 -1
- package/dist/src/ui/hooks/useGitBranchName.test.js +46 -34
- package/dist/src/ui/hooks/useGitBranchName.test.js.map +1 -1
- package/dist/src/ui/hooks/useHistoryManager.test.js +2 -1
- package/dist/src/ui/hooks/useHistoryManager.test.js.map +1 -1
- package/dist/src/ui/hooks/useIdeTrustListener.test.js +40 -9
- package/dist/src/ui/hooks/useIdeTrustListener.test.js.map +1 -1
- package/dist/src/ui/hooks/useInputHistory.test.js +2 -1
- package/dist/src/ui/hooks/useInputHistory.test.js.map +1 -1
- package/dist/src/ui/hooks/useInputHistoryStore.test.js +2 -1
- package/dist/src/ui/hooks/useInputHistoryStore.test.js.map +1 -1
- package/dist/src/ui/hooks/useKeypress.test.js +103 -114
- package/dist/src/ui/hooks/useKeypress.test.js.map +1 -1
- package/dist/src/ui/hooks/useLoadingIndicator.test.js +24 -6
- package/dist/src/ui/hooks/useLoadingIndicator.test.js.map +1 -1
- package/dist/src/ui/hooks/useMemoryMonitor.test.js +10 -5
- package/dist/src/ui/hooks/useMemoryMonitor.test.js.map +1 -1
- package/dist/src/ui/hooks/useMessageQueue.test.js +62 -45
- package/dist/src/ui/hooks/useMessageQueue.test.js.map +1 -1
- package/dist/src/ui/hooks/useModelCommand.test.js +21 -11
- package/dist/src/ui/hooks/useModelCommand.test.js.map +1 -1
- package/dist/src/ui/hooks/useMouse.d.ts +17 -0
- package/dist/src/ui/hooks/useMouse.js +27 -0
- package/dist/src/ui/hooks/useMouse.js.map +1 -0
- package/dist/src/ui/hooks/useMouse.test.d.ts +6 -0
- package/dist/src/ui/hooks/useMouse.test.js +57 -0
- package/dist/src/ui/hooks/useMouse.test.js.map +1 -0
- package/dist/src/ui/hooks/usePermissionsModifyTrust.test.js +2 -2
- package/dist/src/ui/hooks/usePermissionsModifyTrust.test.js.map +1 -1
- package/dist/src/ui/hooks/usePhraseCycler.js +1 -1
- package/dist/src/ui/hooks/usePhraseCycler.js.map +1 -1
- package/dist/src/ui/hooks/usePhraseCycler.test.js +109 -106
- package/dist/src/ui/hooks/usePhraseCycler.test.js.map +1 -1
- package/dist/src/ui/hooks/usePrivacySettings.test.js +26 -6
- package/dist/src/ui/hooks/usePrivacySettings.test.js.map +1 -1
- package/dist/src/ui/hooks/usePromptCompletion.js +2 -2
- package/dist/src/ui/hooks/usePromptCompletion.js.map +1 -1
- package/dist/src/ui/hooks/useQuotaAndFallback.js +13 -14
- package/dist/src/ui/hooks/useQuotaAndFallback.js.map +1 -1
- package/dist/src/ui/hooks/useQuotaAndFallback.test.js +55 -48
- package/dist/src/ui/hooks/useQuotaAndFallback.test.js.map +1 -1
- package/dist/src/ui/hooks/useReactToolScheduler.d.ts +8 -1
- package/dist/src/ui/hooks/useReactToolScheduler.js +59 -34
- package/dist/src/ui/hooks/useReactToolScheduler.js.map +1 -1
- package/dist/src/ui/hooks/useReactToolScheduler.test.d.ts +6 -0
- package/dist/src/ui/hooks/useReactToolScheduler.test.js +65 -0
- package/dist/src/ui/hooks/useReactToolScheduler.test.js.map +1 -0
- package/dist/src/ui/hooks/useReverseSearchCompletion.test.js +2 -2
- package/dist/src/ui/hooks/useReverseSearchCompletion.test.js.map +1 -1
- package/dist/src/ui/hooks/useSelectionList.js +5 -4
- package/dist/src/ui/hooks/useSelectionList.js.map +1 -1
- package/dist/src/ui/hooks/useSelectionList.test.js +272 -183
- package/dist/src/ui/hooks/useSelectionList.test.js.map +1 -1
- package/dist/src/ui/hooks/useShellHistory.test.js +52 -20
- package/dist/src/ui/hooks/useShellHistory.test.js.map +1 -1
- package/dist/src/ui/hooks/useSlashCompletion.js +18 -7
- package/dist/src/ui/hooks/useSlashCompletion.js.map +1 -1
- package/dist/src/ui/hooks/useSlashCompletion.test.js +275 -137
- package/dist/src/ui/hooks/useSlashCompletion.test.js.map +1 -1
- package/dist/src/ui/hooks/useTimer.test.js +43 -14
- package/dist/src/ui/hooks/useTimer.test.js.map +1 -1
- package/dist/src/ui/hooks/useToolScheduler.test.js +226 -242
- package/dist/src/ui/hooks/useToolScheduler.test.js.map +1 -1
- package/dist/src/ui/hooks/vim.test.js +235 -355
- package/dist/src/ui/hooks/vim.test.js.map +1 -1
- package/dist/src/ui/keyMatchers.test.js +30 -3
- package/dist/src/ui/keyMatchers.test.js.map +1 -1
- package/dist/src/ui/state/extensions.d.ts +1 -0
- package/dist/src/ui/state/extensions.js +1 -0
- package/dist/src/ui/state/extensions.js.map +1 -1
- package/dist/src/ui/themes/ansi-light.js +1 -0
- package/dist/src/ui/themes/ansi-light.js.map +1 -1
- package/dist/src/ui/themes/ansi.js +1 -0
- package/dist/src/ui/themes/ansi.js.map +1 -1
- package/dist/src/ui/themes/atom-one-dark.js +2 -0
- package/dist/src/ui/themes/atom-one-dark.js.map +1 -1
- package/dist/src/ui/themes/ayu-light.js +2 -0
- package/dist/src/ui/themes/ayu-light.js.map +1 -1
- package/dist/src/ui/themes/ayu.js +2 -0
- package/dist/src/ui/themes/ayu.js.map +1 -1
- package/dist/src/ui/themes/color-utils.d.ts +1 -0
- package/dist/src/ui/themes/color-utils.js +6 -0
- package/dist/src/ui/themes/color-utils.js.map +1 -1
- package/dist/src/ui/themes/color-utils.test.js +13 -1
- package/dist/src/ui/themes/color-utils.test.js.map +1 -1
- package/dist/src/ui/themes/dracula.js +2 -0
- package/dist/src/ui/themes/dracula.js.map +1 -1
- package/dist/src/ui/themes/github-dark.js +2 -0
- package/dist/src/ui/themes/github-dark.js.map +1 -1
- package/dist/src/ui/themes/github-light.js +2 -0
- package/dist/src/ui/themes/github-light.js.map +1 -1
- package/dist/src/ui/themes/googlecode.js +2 -0
- package/dist/src/ui/themes/googlecode.js.map +1 -1
- package/dist/src/ui/themes/no-color.js +3 -0
- package/dist/src/ui/themes/no-color.js.map +1 -1
- package/dist/src/ui/themes/semantic-tokens.d.ts +2 -0
- package/dist/src/ui/themes/semantic-tokens.js +6 -0
- package/dist/src/ui/themes/semantic-tokens.js.map +1 -1
- package/dist/src/ui/themes/shades-of-purple.js +2 -0
- package/dist/src/ui/themes/shades-of-purple.js.map +1 -1
- package/dist/src/ui/themes/theme.d.ts +3 -0
- package/dist/src/ui/themes/theme.js +14 -3
- package/dist/src/ui/themes/theme.js.map +1 -1
- package/dist/src/ui/themes/theme.test.js +67 -1
- package/dist/src/ui/themes/theme.test.js.map +1 -1
- package/dist/src/ui/themes/xcode.js +2 -0
- package/dist/src/ui/themes/xcode.js.map +1 -1
- package/dist/src/ui/types.d.ts +3 -1
- package/dist/src/ui/types.js +2 -0
- package/dist/src/ui/types.js.map +1 -1
- package/dist/src/ui/utils/CodeColorizer.js +2 -1
- package/dist/src/ui/utils/CodeColorizer.js.map +1 -1
- package/dist/src/ui/utils/InlineMarkdownRenderer.d.ts +1 -0
- package/dist/src/ui/utils/InlineMarkdownRenderer.js +11 -10
- package/dist/src/ui/utils/InlineMarkdownRenderer.js.map +1 -1
- package/dist/src/ui/utils/MarkdownDisplay.js +11 -9
- package/dist/src/ui/utils/MarkdownDisplay.js.map +1 -1
- package/dist/src/ui/utils/clipboardUtils.js +2 -2
- package/dist/src/ui/utils/clipboardUtils.js.map +1 -1
- package/dist/src/ui/utils/input.d.ts +17 -0
- package/dist/src/ui/utils/input.js +51 -0
- package/dist/src/ui/utils/input.js.map +1 -0
- package/dist/src/ui/utils/input.test.d.ts +6 -0
- package/dist/src/ui/utils/input.test.js +44 -0
- package/dist/src/ui/utils/input.test.js.map +1 -0
- package/dist/src/ui/utils/kittyProtocolDetector.js +13 -4
- package/dist/src/ui/utils/kittyProtocolDetector.js.map +1 -1
- package/dist/src/ui/utils/mouse.d.ts +31 -0
- package/dist/src/ui/utils/mouse.js +164 -0
- package/dist/src/ui/utils/mouse.js.map +1 -0
- package/dist/src/ui/utils/mouse.test.d.ts +6 -0
- package/dist/src/ui/utils/mouse.test.js +131 -0
- package/dist/src/ui/utils/mouse.test.js.map +1 -0
- package/dist/src/ui/utils/textOutput.d.ts +25 -0
- package/dist/src/ui/utils/textOutput.js +49 -0
- package/dist/src/ui/utils/textOutput.js.map +1 -0
- package/dist/src/ui/utils/textOutput.test.d.ts +6 -0
- package/dist/src/ui/utils/textOutput.test.js +79 -0
- package/dist/src/ui/utils/textOutput.test.js.map +1 -0
- package/dist/src/ui/utils/updateCheck.d.ts +7 -1
- package/dist/src/ui/utils/updateCheck.js +33 -29
- package/dist/src/ui/utils/updateCheck.js.map +1 -1
- package/dist/src/ui/utils/updateCheck.test.js +24 -50
- package/dist/src/ui/utils/updateCheck.test.js.map +1 -1
- package/dist/src/utils/commentJson.js +2 -2
- package/dist/src/utils/commentJson.js.map +1 -1
- package/dist/src/utils/commentJson.test.js +7 -6
- package/dist/src/utils/commentJson.test.js.map +1 -1
- package/dist/src/utils/envVarResolver.d.ts +2 -2
- package/dist/src/utils/envVarResolver.js +10 -7
- package/dist/src/utils/envVarResolver.js.map +1 -1
- package/dist/src/utils/events.d.ts +11 -2
- package/dist/src/utils/events.js +1 -0
- package/dist/src/utils/events.js.map +1 -1
- package/dist/src/utils/handleAutoUpdate.js +9 -3
- package/dist/src/utils/handleAutoUpdate.js.map +1 -1
- package/dist/src/utils/sandbox.js +16 -18
- package/dist/src/utils/sandbox.js.map +1 -1
- package/dist/src/utils/version.js +6 -2
- package/dist/src/utils/version.js.map +1 -1
- package/dist/src/zed-integration/acp.js +2 -1
- package/dist/src/zed-integration/acp.js.map +1 -1
- package/dist/src/zed-integration/schema.d.ts +4 -4
- package/dist/src/zed-integration/zedIntegration.d.ts +2 -2
- package/dist/src/zed-integration/zedIntegration.js +12 -19
- package/dist/src/zed-integration/zedIntegration.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +18 -14
- package/dist/src/config/policy.test.js +0 -360
- package/dist/src/config/policy.test.js.map +0 -1
- package/dist/src/utils/package.d.ts +0 -12
- package/dist/src/utils/package.js +0 -24
- package/dist/src/utils/package.js.map +0 -1
- /package/dist/src/{config/policy.test.d.ts → commands/extensions/validate.test.d.ts} +0 -0
|
@@ -7,14 +7,16 @@ import { vi } from 'vitest';
|
|
|
7
7
|
import * as fs from 'node:fs';
|
|
8
8
|
import * as os from 'node:os';
|
|
9
9
|
import * as path from 'node:path';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { SettingScope } from './settings.js';
|
|
14
|
-
import { isWorkspaceTrusted } from './trustedFolders.js';
|
|
10
|
+
import { ExtensionUninstallEvent, ExtensionDisableEvent, ExtensionEnableEvent, KeychainTokenStorage, } from '@google/gemini-cli-core';
|
|
11
|
+
import { loadSettings, SettingScope } from './settings.js';
|
|
12
|
+
import { isWorkspaceTrusted, resetTrustedFoldersForTesting, } from './trustedFolders.js';
|
|
15
13
|
import { createExtension } from '../test-utils/createExtension.js';
|
|
16
14
|
import { ExtensionEnablementManager } from './extensions/extensionEnablement.js';
|
|
17
15
|
import { join } from 'node:path';
|
|
16
|
+
import { EXTENSIONS_CONFIG_FILENAME, EXTENSIONS_DIRECTORY_NAME, INSTALL_METADATA_FILENAME, } from './extensions/variables.js';
|
|
17
|
+
import { hashValue, ExtensionManager } from './extension-manager.js';
|
|
18
|
+
import { ExtensionStorage } from './extensions/storage.js';
|
|
19
|
+
import { INSTALL_WARNING_MESSAGE } from './extensions/consent.js';
|
|
18
20
|
const mockGit = {
|
|
19
21
|
clone: vi.fn(),
|
|
20
22
|
getRemotes: vi.fn(),
|
|
@@ -40,11 +42,12 @@ vi.mock('simple-git', () => ({
|
|
|
40
42
|
return mockGit;
|
|
41
43
|
}),
|
|
42
44
|
}));
|
|
45
|
+
const mockHomedir = vi.hoisted(() => vi.fn(() => '/tmp/mock-home'));
|
|
43
46
|
vi.mock('os', async (importOriginal) => {
|
|
44
47
|
const mockedOs = await importOriginal();
|
|
45
48
|
return {
|
|
46
49
|
...mockedOs,
|
|
47
|
-
homedir:
|
|
50
|
+
homedir: mockHomedir,
|
|
48
51
|
};
|
|
49
52
|
});
|
|
50
53
|
vi.mock('./trustedFolders.js', async (importOriginal) => {
|
|
@@ -72,6 +75,13 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
|
|
72
75
|
ExtensionInstallEvent: vi.fn(),
|
|
73
76
|
ExtensionUninstallEvent: vi.fn(),
|
|
74
77
|
ExtensionDisableEvent: vi.fn(),
|
|
78
|
+
KeychainTokenStorage: vi.fn().mockImplementation(() => ({
|
|
79
|
+
getSecret: vi.fn(),
|
|
80
|
+
setSecret: vi.fn(),
|
|
81
|
+
deleteSecret: vi.fn(),
|
|
82
|
+
listSecrets: vi.fn(),
|
|
83
|
+
isAvailable: vi.fn().mockResolvedValue(true),
|
|
84
|
+
})),
|
|
75
85
|
};
|
|
76
86
|
});
|
|
77
87
|
vi.mock('child_process', async (importOriginal) => {
|
|
@@ -81,15 +91,43 @@ vi.mock('child_process', async (importOriginal) => {
|
|
|
81
91
|
execSync: vi.fn(),
|
|
82
92
|
};
|
|
83
93
|
});
|
|
84
|
-
const EXTENSIONS_DIRECTORY_NAME = path.join(CELL_DIR, 'extensions');
|
|
85
94
|
describe('extension tests', () => {
|
|
86
95
|
let tempHomeDir;
|
|
87
96
|
let tempWorkspaceDir;
|
|
88
97
|
let userExtensionsDir;
|
|
98
|
+
let extensionManager;
|
|
99
|
+
let mockRequestConsent;
|
|
100
|
+
let mockPromptForSettings;
|
|
101
|
+
let mockKeychainStorage;
|
|
102
|
+
let keychainData;
|
|
89
103
|
beforeEach(() => {
|
|
104
|
+
vi.clearAllMocks();
|
|
105
|
+
keychainData = {};
|
|
106
|
+
mockKeychainStorage = {
|
|
107
|
+
getSecret: vi
|
|
108
|
+
.fn()
|
|
109
|
+
.mockImplementation(async (key) => keychainData[key] || null),
|
|
110
|
+
setSecret: vi
|
|
111
|
+
.fn()
|
|
112
|
+
.mockImplementation(async (key, value) => {
|
|
113
|
+
keychainData[key] = value;
|
|
114
|
+
}),
|
|
115
|
+
deleteSecret: vi.fn().mockImplementation(async (key) => {
|
|
116
|
+
delete keychainData[key];
|
|
117
|
+
}),
|
|
118
|
+
listSecrets: vi
|
|
119
|
+
.fn()
|
|
120
|
+
.mockImplementation(async () => Object.keys(keychainData)),
|
|
121
|
+
isAvailable: vi.fn().mockResolvedValue(true),
|
|
122
|
+
};
|
|
123
|
+
KeychainTokenStorage.mockImplementation(() => mockKeychainStorage);
|
|
90
124
|
tempHomeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cell-cli-test-home-'));
|
|
91
125
|
tempWorkspaceDir = fs.mkdtempSync(path.join(tempHomeDir, 'gemini-cli-test-workspace-'));
|
|
92
126
|
userExtensionsDir = path.join(tempHomeDir, EXTENSIONS_DIRECTORY_NAME);
|
|
127
|
+
mockRequestConsent = vi.fn();
|
|
128
|
+
mockRequestConsent.mockResolvedValue(true);
|
|
129
|
+
mockPromptForSettings = vi.fn();
|
|
130
|
+
mockPromptForSettings.mockResolvedValue('');
|
|
93
131
|
fs.mkdirSync(userExtensionsDir, { recursive: true });
|
|
94
132
|
vi.mocked(os.homedir).mockReturnValue(tempHomeDir);
|
|
95
133
|
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
|
@@ -97,6 +135,13 @@ describe('extension tests', () => {
|
|
|
97
135
|
source: undefined,
|
|
98
136
|
});
|
|
99
137
|
vi.spyOn(process, 'cwd').mockReturnValue(tempWorkspaceDir);
|
|
138
|
+
extensionManager = new ExtensionManager({
|
|
139
|
+
workspaceDir: tempWorkspaceDir,
|
|
140
|
+
requestConsent: mockRequestConsent,
|
|
141
|
+
requestSetting: mockPromptForSettings,
|
|
142
|
+
settings: loadSettings(tempWorkspaceDir).merged,
|
|
143
|
+
});
|
|
144
|
+
resetTrustedFoldersForTesting();
|
|
100
145
|
});
|
|
101
146
|
afterEach(() => {
|
|
102
147
|
fs.rmSync(tempHomeDir, { recursive: true, force: true });
|
|
@@ -104,7 +149,7 @@ describe('extension tests', () => {
|
|
|
104
149
|
vi.restoreAllMocks();
|
|
105
150
|
});
|
|
106
151
|
describe('loadExtensions', () => {
|
|
107
|
-
it('should include extension path in loaded extension', () => {
|
|
152
|
+
it('should include extension path in loaded extension', async () => {
|
|
108
153
|
const extensionDir = path.join(userExtensionsDir, 'test-extension');
|
|
109
154
|
fs.mkdirSync(extensionDir, { recursive: true });
|
|
110
155
|
createExtension({
|
|
@@ -112,12 +157,12 @@ describe('extension tests', () => {
|
|
|
112
157
|
name: 'test-extension',
|
|
113
158
|
version: '1.0.0',
|
|
114
159
|
});
|
|
115
|
-
const extensions = loadExtensions(
|
|
160
|
+
const extensions = await extensionManager.loadExtensions();
|
|
116
161
|
expect(extensions).toHaveLength(1);
|
|
117
162
|
expect(extensions[0].path).toBe(extensionDir);
|
|
118
163
|
expect(extensions[0].name).toBe('test-extension');
|
|
119
164
|
});
|
|
120
|
-
it('should load context file path when GEMINI.md is present', () => {
|
|
165
|
+
it('should load context file path when GEMINI.md is present', async () => {
|
|
121
166
|
createExtension({
|
|
122
167
|
extensionsDir: userExtensionsDir,
|
|
123
168
|
name: 'ext1',
|
|
@@ -129,7 +174,7 @@ describe('extension tests', () => {
|
|
|
129
174
|
name: 'ext2',
|
|
130
175
|
version: '2.0.0',
|
|
131
176
|
});
|
|
132
|
-
const extensions = loadExtensions(
|
|
177
|
+
const extensions = await extensionManager.loadExtensions();
|
|
133
178
|
expect(extensions).toHaveLength(2);
|
|
134
179
|
const ext1 = extensions.find((e) => e.name === 'ext1');
|
|
135
180
|
const ext2 = extensions.find((e) => e.name === 'ext2');
|
|
@@ -138,7 +183,7 @@ describe('extension tests', () => {
|
|
|
138
183
|
]);
|
|
139
184
|
expect(ext2?.contextFiles).toEqual([]);
|
|
140
185
|
});
|
|
141
|
-
it('should load context file path from the extension config', () => {
|
|
186
|
+
it('should load context file path from the extension config', async () => {
|
|
142
187
|
createExtension({
|
|
143
188
|
extensionsDir: userExtensionsDir,
|
|
144
189
|
name: 'ext1',
|
|
@@ -146,14 +191,14 @@ describe('extension tests', () => {
|
|
|
146
191
|
addContextFile: false,
|
|
147
192
|
contextFileName: 'my-context-file.md',
|
|
148
193
|
});
|
|
149
|
-
const extensions = loadExtensions(
|
|
194
|
+
const extensions = await extensionManager.loadExtensions();
|
|
150
195
|
expect(extensions).toHaveLength(1);
|
|
151
196
|
const ext1 = extensions.find((e) => e.name === 'ext1');
|
|
152
197
|
expect(ext1?.contextFiles).toEqual([
|
|
153
198
|
path.join(userExtensionsDir, 'ext1', 'my-context-file.md'),
|
|
154
199
|
]);
|
|
155
200
|
});
|
|
156
|
-
it('should annotate disabled extensions', () => {
|
|
201
|
+
it('should annotate disabled extensions', async () => {
|
|
157
202
|
createExtension({
|
|
158
203
|
extensionsDir: userExtensionsDir,
|
|
159
204
|
name: 'disabled-extension',
|
|
@@ -164,16 +209,16 @@ describe('extension tests', () => {
|
|
|
164
209
|
name: 'enabled-extension',
|
|
165
210
|
version: '2.0.0',
|
|
166
211
|
});
|
|
167
|
-
|
|
168
|
-
disableExtension('disabled-extension', SettingScope.User
|
|
169
|
-
const extensions =
|
|
212
|
+
await extensionManager.loadExtensions();
|
|
213
|
+
await extensionManager.disableExtension('disabled-extension', SettingScope.User);
|
|
214
|
+
const extensions = extensionManager.getExtensions();
|
|
170
215
|
expect(extensions).toHaveLength(2);
|
|
171
216
|
expect(extensions[0].name).toBe('disabled-extension');
|
|
172
217
|
expect(extensions[0].isActive).toBe(false);
|
|
173
218
|
expect(extensions[1].name).toBe('enabled-extension');
|
|
174
219
|
expect(extensions[1].isActive).toBe(true);
|
|
175
220
|
});
|
|
176
|
-
it('should hydrate variables', () => {
|
|
221
|
+
it('should hydrate variables', async () => {
|
|
177
222
|
createExtension({
|
|
178
223
|
extensionsDir: userExtensionsDir,
|
|
179
224
|
name: 'test-extension',
|
|
@@ -186,7 +231,7 @@ describe('extension tests', () => {
|
|
|
186
231
|
},
|
|
187
232
|
},
|
|
188
233
|
});
|
|
189
|
-
const extensions = loadExtensions(
|
|
234
|
+
const extensions = await extensionManager.loadExtensions();
|
|
190
235
|
expect(extensions).toHaveLength(1);
|
|
191
236
|
const expectedCwd = path.join(userExtensionsDir, 'test-extension', 'server');
|
|
192
237
|
expect(extensions[0].mcpServers?.['test-server'].cwd).toBe(expectedCwd);
|
|
@@ -199,12 +244,13 @@ describe('extension tests', () => {
|
|
|
199
244
|
contextFileName: 'context.md',
|
|
200
245
|
});
|
|
201
246
|
fs.writeFileSync(path.join(sourceExtDir, 'context.md'), 'linked context');
|
|
202
|
-
|
|
247
|
+
await extensionManager.loadExtensions();
|
|
248
|
+
const extension = await extensionManager.installOrUpdateExtension({
|
|
203
249
|
source: sourceExtDir,
|
|
204
250
|
type: 'link',
|
|
205
|
-
}
|
|
206
|
-
expect(
|
|
207
|
-
const extensions =
|
|
251
|
+
});
|
|
252
|
+
expect(extension.name).toEqual('my-linked-extension');
|
|
253
|
+
const extensions = extensionManager.getExtensions();
|
|
208
254
|
expect(extensions).toHaveLength(1);
|
|
209
255
|
const linkedExt = extensions[0];
|
|
210
256
|
expect(linkedExt.name).toBe('my-linked-extension');
|
|
@@ -217,7 +263,32 @@ describe('extension tests', () => {
|
|
|
217
263
|
path.join(sourceExtDir, 'context.md'),
|
|
218
264
|
]);
|
|
219
265
|
});
|
|
220
|
-
it('should
|
|
266
|
+
it('should hydrate ${extensionPath} correctly for linked extensions', async () => {
|
|
267
|
+
const sourceExtDir = createExtension({
|
|
268
|
+
extensionsDir: tempWorkspaceDir,
|
|
269
|
+
name: 'my-linked-extension-with-path',
|
|
270
|
+
version: '1.0.0',
|
|
271
|
+
mcpServers: {
|
|
272
|
+
'test-server': {
|
|
273
|
+
command: 'node',
|
|
274
|
+
args: ['${extensionPath}${/}server${/}index.js'],
|
|
275
|
+
cwd: '${extensionPath}${/}server',
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
await extensionManager.loadExtensions();
|
|
280
|
+
await extensionManager.installOrUpdateExtension({
|
|
281
|
+
source: sourceExtDir,
|
|
282
|
+
type: 'link',
|
|
283
|
+
});
|
|
284
|
+
const extensions = extensionManager.getExtensions();
|
|
285
|
+
expect(extensions).toHaveLength(1);
|
|
286
|
+
expect(extensions[0].mcpServers?.['test-server'].cwd).toBe(path.join(sourceExtDir, 'server'));
|
|
287
|
+
expect(extensions[0].mcpServers?.['test-server'].args).toEqual([
|
|
288
|
+
path.join(sourceExtDir, 'server', 'index.js'),
|
|
289
|
+
]);
|
|
290
|
+
});
|
|
291
|
+
it('should resolve environment variables in extension configuration', async () => {
|
|
221
292
|
process.env['TEST_API_KEY'] = 'test-api-key-123';
|
|
222
293
|
process.env['TEST_DB_URL'] = 'postgresql://localhost:5432/testdb';
|
|
223
294
|
try {
|
|
@@ -243,7 +314,7 @@ describe('extension tests', () => {
|
|
|
243
314
|
},
|
|
244
315
|
};
|
|
245
316
|
fs.writeFileSync(configPath, JSON.stringify(extensionConfig));
|
|
246
|
-
const extensions = loadExtensions(
|
|
317
|
+
const extensions = await extensionManager.loadExtensions();
|
|
247
318
|
expect(extensions).toHaveLength(1);
|
|
248
319
|
const extension = extensions[0];
|
|
249
320
|
expect(extension.name).toBe('test-extension');
|
|
@@ -260,7 +331,40 @@ describe('extension tests', () => {
|
|
|
260
331
|
delete process.env['TEST_DB_URL'];
|
|
261
332
|
}
|
|
262
333
|
});
|
|
263
|
-
it('should
|
|
334
|
+
it('should resolve environment variables from an extension .env file', async () => {
|
|
335
|
+
const extDir = createExtension({
|
|
336
|
+
extensionsDir: userExtensionsDir,
|
|
337
|
+
name: 'test-extension',
|
|
338
|
+
version: '1.0.0',
|
|
339
|
+
mcpServers: {
|
|
340
|
+
'test-server': {
|
|
341
|
+
command: 'node',
|
|
342
|
+
args: ['server.js'],
|
|
343
|
+
env: {
|
|
344
|
+
API_KEY: '$MY_API_KEY',
|
|
345
|
+
STATIC_VALUE: 'no-substitution',
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
settings: [
|
|
350
|
+
{
|
|
351
|
+
name: 'My API Key',
|
|
352
|
+
description: 'API key for testing.',
|
|
353
|
+
envVar: 'MY_API_KEY',
|
|
354
|
+
},
|
|
355
|
+
],
|
|
356
|
+
});
|
|
357
|
+
const envFilePath = path.join(extDir, '.env');
|
|
358
|
+
fs.writeFileSync(envFilePath, 'MY_API_KEY=test-key-from-file\n');
|
|
359
|
+
const extensions = await extensionManager.loadExtensions();
|
|
360
|
+
expect(extensions).toHaveLength(1);
|
|
361
|
+
const extension = extensions[0];
|
|
362
|
+
const serverConfig = extension.mcpServers['test-server'];
|
|
363
|
+
expect(serverConfig.env).toBeDefined();
|
|
364
|
+
expect(serverConfig.env['API_KEY']).toBe('test-key-from-file');
|
|
365
|
+
expect(serverConfig.env['STATIC_VALUE']).toBe('no-substitution');
|
|
366
|
+
});
|
|
367
|
+
it('should handle missing environment variables gracefully', async () => {
|
|
264
368
|
const userExtensionsDir = path.join(tempHomeDir, EXTENSIONS_DIRECTORY_NAME);
|
|
265
369
|
fs.mkdirSync(userExtensionsDir, { recursive: true });
|
|
266
370
|
const extDir = path.join(userExtensionsDir, 'test-extension');
|
|
@@ -280,7 +384,7 @@ describe('extension tests', () => {
|
|
|
280
384
|
},
|
|
281
385
|
};
|
|
282
386
|
fs.writeFileSync(path.join(extDir, EXTENSIONS_CONFIG_FILENAME), JSON.stringify(extensionConfig));
|
|
283
|
-
const extensions = loadExtensions(
|
|
387
|
+
const extensions = await extensionManager.loadExtensions();
|
|
284
388
|
expect(extensions).toHaveLength(1);
|
|
285
389
|
const extension = extensions[0];
|
|
286
390
|
const serverConfig = extension.mcpServers['test-server'];
|
|
@@ -288,7 +392,7 @@ describe('extension tests', () => {
|
|
|
288
392
|
expect(serverConfig.env['MISSING_VAR']).toBe('$UNDEFINED_ENV_VAR');
|
|
289
393
|
expect(serverConfig.env['MISSING_VAR_BRACES']).toBe('${ALSO_UNDEFINED}');
|
|
290
394
|
});
|
|
291
|
-
it('should skip extensions with invalid JSON and log a warning', () => {
|
|
395
|
+
it('should skip extensions with invalid JSON and log a warning', async () => {
|
|
292
396
|
const consoleSpy = vi
|
|
293
397
|
.spyOn(console, 'error')
|
|
294
398
|
.mockImplementation(() => { });
|
|
@@ -303,14 +407,13 @@ describe('extension tests', () => {
|
|
|
303
407
|
fs.mkdirSync(badExtDir);
|
|
304
408
|
const badConfigPath = path.join(badExtDir, EXTENSIONS_CONFIG_FILENAME);
|
|
305
409
|
fs.writeFileSync(badConfigPath, '{ "name": "bad-ext"'); // Malformed
|
|
306
|
-
const extensions = loadExtensions(
|
|
410
|
+
const extensions = await extensionManager.loadExtensions();
|
|
307
411
|
expect(extensions).toHaveLength(1);
|
|
308
412
|
expect(extensions[0].name).toBe('good-ext');
|
|
309
|
-
expect(consoleSpy).
|
|
310
|
-
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining(`Warning: Skipping extension in ${badExtDir}: Failed to load extension config from ${badConfigPath}`));
|
|
413
|
+
expect(consoleSpy).toHaveBeenCalledExactlyOnceWith(expect.stringContaining(`Warning: Skipping extension in ${badExtDir}: Failed to load extension config from ${badConfigPath}`));
|
|
311
414
|
consoleSpy.mockRestore();
|
|
312
415
|
});
|
|
313
|
-
it('should skip extensions with missing name and log a warning', () => {
|
|
416
|
+
it('should skip extensions with missing name and log a warning', async () => {
|
|
314
417
|
const consoleSpy = vi
|
|
315
418
|
.spyOn(console, 'error')
|
|
316
419
|
.mockImplementation(() => { });
|
|
@@ -325,14 +428,13 @@ describe('extension tests', () => {
|
|
|
325
428
|
fs.mkdirSync(badExtDir);
|
|
326
429
|
const badConfigPath = path.join(badExtDir, EXTENSIONS_CONFIG_FILENAME);
|
|
327
430
|
fs.writeFileSync(badConfigPath, JSON.stringify({ version: '1.0.0' }));
|
|
328
|
-
const extensions = loadExtensions(
|
|
431
|
+
const extensions = await extensionManager.loadExtensions();
|
|
329
432
|
expect(extensions).toHaveLength(1);
|
|
330
433
|
expect(extensions[0].name).toBe('good-ext');
|
|
331
|
-
expect(consoleSpy).
|
|
332
|
-
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining(`Warning: Skipping extension in ${badExtDir}: Failed to load extension config from ${badConfigPath}: Invalid configuration in ${badConfigPath}: missing "name"`));
|
|
434
|
+
expect(consoleSpy).toHaveBeenCalledExactlyOnceWith(expect.stringContaining(`Warning: Skipping extension in ${badExtDir}: Failed to load extension config from ${badConfigPath}: Invalid configuration in ${badConfigPath}: missing "name"`));
|
|
333
435
|
consoleSpy.mockRestore();
|
|
334
436
|
});
|
|
335
|
-
it('should filter trust out of mcp servers', () => {
|
|
437
|
+
it('should filter trust out of mcp servers', async () => {
|
|
336
438
|
createExtension({
|
|
337
439
|
extensionsDir: userExtensionsDir,
|
|
338
440
|
name: 'test-extension',
|
|
@@ -345,31 +447,28 @@ describe('extension tests', () => {
|
|
|
345
447
|
},
|
|
346
448
|
},
|
|
347
449
|
});
|
|
348
|
-
const extensions = loadExtensions(
|
|
450
|
+
const extensions = await extensionManager.loadExtensions();
|
|
349
451
|
expect(extensions).toHaveLength(1);
|
|
350
452
|
expect(extensions[0].mcpServers?.['test-server'].trust).toBeUndefined();
|
|
351
453
|
});
|
|
352
|
-
it('should throw an error for invalid extension names', () => {
|
|
454
|
+
it('should throw an error for invalid extension names', async () => {
|
|
353
455
|
const consoleSpy = vi
|
|
354
456
|
.spyOn(console, 'error')
|
|
355
457
|
.mockImplementation(() => { });
|
|
356
|
-
|
|
458
|
+
createExtension({
|
|
357
459
|
extensionsDir: userExtensionsDir,
|
|
358
460
|
name: 'bad_name',
|
|
359
461
|
version: '1.0.0',
|
|
360
462
|
});
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
extensionEnablementManager: new ExtensionEnablementManager(),
|
|
365
|
-
});
|
|
366
|
-
expect(extension).toBeNull();
|
|
463
|
+
const extensions = await extensionManager.loadExtensions();
|
|
464
|
+
const extension = extensions.find((e) => e.name === 'bad_name');
|
|
465
|
+
expect(extension).toBeUndefined();
|
|
367
466
|
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Invalid extension name: "bad_name"'));
|
|
368
467
|
consoleSpy.mockRestore();
|
|
369
468
|
});
|
|
370
469
|
describe('id generation', () => {
|
|
371
|
-
it('should generate id from source for non-github git urls', () => {
|
|
372
|
-
|
|
470
|
+
it('should generate id from source for non-github git urls', async () => {
|
|
471
|
+
createExtension({
|
|
373
472
|
extensionsDir: userExtensionsDir,
|
|
374
473
|
name: 'my-ext',
|
|
375
474
|
version: '1.0.0',
|
|
@@ -378,18 +477,12 @@ describe('extension tests', () => {
|
|
|
378
477
|
source: 'http://somehost.com/foo/bar',
|
|
379
478
|
},
|
|
380
479
|
});
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
extensionEnablementManager: new ExtensionEnablementManager(),
|
|
385
|
-
});
|
|
386
|
-
const expectedHash = createHash('sha256')
|
|
387
|
-
.update('http://somehost.com/foo/bar')
|
|
388
|
-
.digest('hex');
|
|
389
|
-
expect(extension?.id).toBe(expectedHash);
|
|
480
|
+
const extensions = await extensionManager.loadExtensions();
|
|
481
|
+
const extension = extensions.find((e) => e.name === 'my-ext');
|
|
482
|
+
expect(extension?.id).toBe(hashValue('http://somehost.com/foo/bar'));
|
|
390
483
|
});
|
|
391
|
-
it('should generate id from owner/repo for github http urls', () => {
|
|
392
|
-
|
|
484
|
+
it('should generate id from owner/repo for github http urls', async () => {
|
|
485
|
+
createExtension({
|
|
393
486
|
extensionsDir: userExtensionsDir,
|
|
394
487
|
name: 'my-ext',
|
|
395
488
|
version: '1.0.0',
|
|
@@ -398,18 +491,12 @@ describe('extension tests', () => {
|
|
|
398
491
|
source: 'http://github.com/foo/bar',
|
|
399
492
|
},
|
|
400
493
|
});
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
extensionEnablementManager: new ExtensionEnablementManager(),
|
|
405
|
-
});
|
|
406
|
-
const expectedHash = createHash('sha256')
|
|
407
|
-
.update('https://github.com/foo/bar')
|
|
408
|
-
.digest('hex');
|
|
409
|
-
expect(extension?.id).toBe(expectedHash);
|
|
494
|
+
const extensions = await extensionManager.loadExtensions();
|
|
495
|
+
const extension = extensions.find((e) => e.name === 'my-ext');
|
|
496
|
+
expect(extension?.id).toBe(hashValue('https://github.com/foo/bar'));
|
|
410
497
|
});
|
|
411
|
-
it('should generate id from owner/repo for github ssh urls', () => {
|
|
412
|
-
|
|
498
|
+
it('should generate id from owner/repo for github ssh urls', async () => {
|
|
499
|
+
createExtension({
|
|
413
500
|
extensionsDir: userExtensionsDir,
|
|
414
501
|
name: 'my-ext',
|
|
415
502
|
version: '1.0.0',
|
|
@@ -418,18 +505,12 @@ describe('extension tests', () => {
|
|
|
418
505
|
source: 'git@github.com:foo/bar',
|
|
419
506
|
},
|
|
420
507
|
});
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
extensionEnablementManager: new ExtensionEnablementManager(),
|
|
425
|
-
});
|
|
426
|
-
const expectedHash = createHash('sha256')
|
|
427
|
-
.update('https://github.com/foo/bar')
|
|
428
|
-
.digest('hex');
|
|
429
|
-
expect(extension?.id).toBe(expectedHash);
|
|
508
|
+
const extensions = await extensionManager.loadExtensions();
|
|
509
|
+
const extension = extensions.find((e) => e.name === 'my-ext');
|
|
510
|
+
expect(extension?.id).toBe(hashValue('https://github.com/foo/bar'));
|
|
430
511
|
});
|
|
431
|
-
it('should generate id from source for github-release extension', () => {
|
|
432
|
-
|
|
512
|
+
it('should generate id from source for github-release extension', async () => {
|
|
513
|
+
createExtension({
|
|
433
514
|
extensionsDir: userExtensionsDir,
|
|
434
515
|
name: 'my-ext',
|
|
435
516
|
version: '1.0.0',
|
|
@@ -438,18 +519,12 @@ describe('extension tests', () => {
|
|
|
438
519
|
source: 'https://github.com/foo/bar',
|
|
439
520
|
},
|
|
440
521
|
});
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
extensionEnablementManager: new ExtensionEnablementManager(),
|
|
445
|
-
});
|
|
446
|
-
const expectedHash = createHash('sha256')
|
|
447
|
-
.update('https://github.com/foo/bar')
|
|
448
|
-
.digest('hex');
|
|
449
|
-
expect(extension?.id).toBe(expectedHash);
|
|
522
|
+
const extensions = await extensionManager.loadExtensions();
|
|
523
|
+
const extension = extensions.find((e) => e.name === 'my-ext');
|
|
524
|
+
expect(extension?.id).toBe(hashValue('https://github.com/foo/bar'));
|
|
450
525
|
});
|
|
451
|
-
it('should generate id from the original source for local extension', () => {
|
|
452
|
-
|
|
526
|
+
it('should generate id from the original source for local extension', async () => {
|
|
527
|
+
createExtension({
|
|
453
528
|
extensionsDir: userExtensionsDir,
|
|
454
529
|
name: 'local-ext-name',
|
|
455
530
|
version: '1.0.0',
|
|
@@ -458,15 +533,9 @@ describe('extension tests', () => {
|
|
|
458
533
|
source: '/some/path',
|
|
459
534
|
},
|
|
460
535
|
});
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
extensionEnablementManager: new ExtensionEnablementManager(),
|
|
465
|
-
});
|
|
466
|
-
const expectedHash = createHash('sha256')
|
|
467
|
-
.update('/some/path')
|
|
468
|
-
.digest('hex');
|
|
469
|
-
expect(extension?.id).toBe(expectedHash);
|
|
536
|
+
const extensions = await extensionManager.loadExtensions();
|
|
537
|
+
const extension = extensions.find((e) => e.name === 'local-ext-name');
|
|
538
|
+
expect(extension?.id).toBe(hashValue('/some/path'));
|
|
470
539
|
});
|
|
471
540
|
it('should generate id from the original source for linked extensions', async () => {
|
|
472
541
|
const extDevelopmentDir = path.join(tempHomeDir, 'local_extensions');
|
|
@@ -475,35 +544,25 @@ describe('extension tests', () => {
|
|
|
475
544
|
name: 'link-ext-name',
|
|
476
545
|
version: '1.0.0',
|
|
477
546
|
});
|
|
478
|
-
|
|
547
|
+
await extensionManager.loadExtensions();
|
|
548
|
+
await extensionManager.installOrUpdateExtension({
|
|
479
549
|
type: 'link',
|
|
480
550
|
source: actualExtensionDir,
|
|
481
|
-
}, async () => true, tempWorkspaceDir);
|
|
482
|
-
const extension = loadExtension({
|
|
483
|
-
extensionDir: new ExtensionStorage(extensionName).getExtensionDir(),
|
|
484
|
-
workspaceDir: tempWorkspaceDir,
|
|
485
|
-
extensionEnablementManager: new ExtensionEnablementManager(),
|
|
486
551
|
});
|
|
487
|
-
const
|
|
488
|
-
.
|
|
489
|
-
.
|
|
490
|
-
expect(extension?.id).toBe(
|
|
552
|
+
const extension = extensionManager
|
|
553
|
+
.getExtensions()
|
|
554
|
+
.find((e) => e.name === 'link-ext-name');
|
|
555
|
+
expect(extension?.id).toBe(hashValue(actualExtensionDir));
|
|
491
556
|
});
|
|
492
|
-
it('should generate id from name for extension with no install metadata', () => {
|
|
493
|
-
|
|
557
|
+
it('should generate id from name for extension with no install metadata', async () => {
|
|
558
|
+
createExtension({
|
|
494
559
|
extensionsDir: userExtensionsDir,
|
|
495
560
|
name: 'no-meta-name',
|
|
496
561
|
version: '1.0.0',
|
|
497
562
|
});
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
extensionEnablementManager: new ExtensionEnablementManager(),
|
|
502
|
-
});
|
|
503
|
-
const expectedHash = createHash('sha256')
|
|
504
|
-
.update('no-meta-name')
|
|
505
|
-
.digest('hex');
|
|
506
|
-
expect(extension?.id).toBe(expectedHash);
|
|
563
|
+
const extensions = await extensionManager.loadExtensions();
|
|
564
|
+
const extension = extensions.find((e) => e.name === 'no-meta-name');
|
|
565
|
+
expect(extension?.id).toBe(hashValue('no-meta-name'));
|
|
507
566
|
});
|
|
508
567
|
});
|
|
509
568
|
});
|
|
@@ -516,7 +575,11 @@ describe('extension tests', () => {
|
|
|
516
575
|
});
|
|
517
576
|
const targetExtDir = path.join(userExtensionsDir, 'my-local-extension');
|
|
518
577
|
const metadataPath = path.join(targetExtDir, INSTALL_METADATA_FILENAME);
|
|
519
|
-
await
|
|
578
|
+
await extensionManager.loadExtensions();
|
|
579
|
+
await extensionManager.installOrUpdateExtension({
|
|
580
|
+
source: sourceExtDir,
|
|
581
|
+
type: 'local',
|
|
582
|
+
});
|
|
520
583
|
expect(fs.existsSync(targetExtDir)).toBe(true);
|
|
521
584
|
expect(fs.existsSync(metadataPath)).toBe(true);
|
|
522
585
|
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
|
|
@@ -532,14 +595,24 @@ describe('extension tests', () => {
|
|
|
532
595
|
name: 'my-local-extension',
|
|
533
596
|
version: '1.0.0',
|
|
534
597
|
});
|
|
535
|
-
await
|
|
536
|
-
await
|
|
598
|
+
await extensionManager.loadExtensions();
|
|
599
|
+
await extensionManager.installOrUpdateExtension({
|
|
600
|
+
source: sourceExtDir,
|
|
601
|
+
type: 'local',
|
|
602
|
+
});
|
|
603
|
+
await expect(extensionManager.installOrUpdateExtension({
|
|
604
|
+
source: sourceExtDir,
|
|
605
|
+
type: 'local',
|
|
606
|
+
})).rejects.toThrow('Extension "my-local-extension" is already installed. Please uninstall it first.');
|
|
537
607
|
});
|
|
538
608
|
it('should throw an error and cleanup if gemini-extension.json is missing', async () => {
|
|
539
609
|
const sourceExtDir = path.join(tempHomeDir, 'bad-extension');
|
|
540
610
|
fs.mkdirSync(sourceExtDir, { recursive: true });
|
|
541
611
|
const configPath = path.join(sourceExtDir, EXTENSIONS_CONFIG_FILENAME);
|
|
542
|
-
await expect(installOrUpdateExtension({
|
|
612
|
+
await expect(extensionManager.installOrUpdateExtension({
|
|
613
|
+
source: sourceExtDir,
|
|
614
|
+
type: 'local',
|
|
615
|
+
})).rejects.toThrow(`Configuration file not found at ${configPath}`);
|
|
543
616
|
const targetExtDir = path.join(userExtensionsDir, 'bad-extension');
|
|
544
617
|
expect(fs.existsSync(targetExtDir)).toBe(false);
|
|
545
618
|
});
|
|
@@ -548,7 +621,10 @@ describe('extension tests', () => {
|
|
|
548
621
|
fs.mkdirSync(sourceExtDir, { recursive: true });
|
|
549
622
|
const configPath = path.join(sourceExtDir, EXTENSIONS_CONFIG_FILENAME);
|
|
550
623
|
fs.writeFileSync(configPath, '{ "name": "bad-json", "version": "1.0.0"'); // Malformed JSON
|
|
551
|
-
await expect(installOrUpdateExtension({
|
|
624
|
+
await expect(extensionManager.installOrUpdateExtension({
|
|
625
|
+
source: sourceExtDir,
|
|
626
|
+
type: 'local',
|
|
627
|
+
})).rejects.toThrow(new RegExp(`^Failed to load extension config from ${configPath.replace(/\\/g, '\\\\')}`));
|
|
552
628
|
});
|
|
553
629
|
it('should throw an error for missing name in gemini-extension.json', async () => {
|
|
554
630
|
const sourceExtDir = createExtension({
|
|
@@ -559,7 +635,10 @@ describe('extension tests', () => {
|
|
|
559
635
|
const configPath = path.join(sourceExtDir, EXTENSIONS_CONFIG_FILENAME);
|
|
560
636
|
// Overwrite with invalid config
|
|
561
637
|
fs.writeFileSync(configPath, JSON.stringify({ version: '1.0.0' }));
|
|
562
|
-
await expect(installOrUpdateExtension({
|
|
638
|
+
await expect(extensionManager.installOrUpdateExtension({
|
|
639
|
+
source: sourceExtDir,
|
|
640
|
+
type: 'local',
|
|
641
|
+
})).rejects.toThrow(`Invalid configuration in ${configPath}: missing "name"`);
|
|
563
642
|
});
|
|
564
643
|
it('should install an extension from a git URL', async () => {
|
|
565
644
|
const gitUrl = 'https://somehost.com/somerepo.git';
|
|
@@ -578,7 +657,11 @@ describe('extension tests', () => {
|
|
|
578
657
|
failureReason: 'no release data',
|
|
579
658
|
type: 'github-release',
|
|
580
659
|
});
|
|
581
|
-
await
|
|
660
|
+
await extensionManager.loadExtensions();
|
|
661
|
+
await extensionManager.installOrUpdateExtension({
|
|
662
|
+
source: gitUrl,
|
|
663
|
+
type: 'git',
|
|
664
|
+
});
|
|
582
665
|
expect(fs.existsSync(targetExtDir)).toBe(true);
|
|
583
666
|
expect(fs.existsSync(metadataPath)).toBe(true);
|
|
584
667
|
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
|
|
@@ -596,7 +679,11 @@ describe('extension tests', () => {
|
|
|
596
679
|
const targetExtDir = path.join(userExtensionsDir, 'my-linked-extension');
|
|
597
680
|
const metadataPath = path.join(targetExtDir, INSTALL_METADATA_FILENAME);
|
|
598
681
|
const configPath = path.join(targetExtDir, EXTENSIONS_CONFIG_FILENAME);
|
|
599
|
-
await
|
|
682
|
+
await extensionManager.loadExtensions();
|
|
683
|
+
await extensionManager.installOrUpdateExtension({
|
|
684
|
+
source: sourceExtDir,
|
|
685
|
+
type: 'link',
|
|
686
|
+
});
|
|
600
687
|
expect(fs.existsSync(targetExtDir)).toBe(true);
|
|
601
688
|
expect(fs.existsSync(metadataPath)).toBe(true);
|
|
602
689
|
expect(fs.existsSync(configPath)).toBe(false);
|
|
@@ -607,6 +694,65 @@ describe('extension tests', () => {
|
|
|
607
694
|
});
|
|
608
695
|
fs.rmSync(targetExtDir, { recursive: true, force: true });
|
|
609
696
|
});
|
|
697
|
+
it('should prompt for trust if workspace is not trusted', async () => {
|
|
698
|
+
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
|
699
|
+
isTrusted: false,
|
|
700
|
+
source: undefined,
|
|
701
|
+
});
|
|
702
|
+
const sourceExtDir = createExtension({
|
|
703
|
+
extensionsDir: tempHomeDir,
|
|
704
|
+
name: 'my-local-extension',
|
|
705
|
+
version: '1.0.0',
|
|
706
|
+
});
|
|
707
|
+
await extensionManager.loadExtensions();
|
|
708
|
+
await extensionManager.installOrUpdateExtension({
|
|
709
|
+
source: sourceExtDir,
|
|
710
|
+
type: 'local',
|
|
711
|
+
});
|
|
712
|
+
expect(mockRequestConsent).toHaveBeenCalledWith(`The current workspace at "${tempWorkspaceDir}" is not trusted. Do you want to trust this workspace to install extensions?`);
|
|
713
|
+
});
|
|
714
|
+
it('should not install if user denies trust', async () => {
|
|
715
|
+
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
|
716
|
+
isTrusted: false,
|
|
717
|
+
source: undefined,
|
|
718
|
+
});
|
|
719
|
+
mockRequestConsent.mockImplementation(async (message) => {
|
|
720
|
+
if (message.includes('is not trusted. Do you want to trust this workspace to install extensions?')) {
|
|
721
|
+
return false;
|
|
722
|
+
}
|
|
723
|
+
return true;
|
|
724
|
+
});
|
|
725
|
+
const sourceExtDir = createExtension({
|
|
726
|
+
extensionsDir: tempHomeDir,
|
|
727
|
+
name: 'my-local-extension',
|
|
728
|
+
version: '1.0.0',
|
|
729
|
+
});
|
|
730
|
+
await extensionManager.loadExtensions();
|
|
731
|
+
await expect(extensionManager.installOrUpdateExtension({
|
|
732
|
+
source: sourceExtDir,
|
|
733
|
+
type: 'local',
|
|
734
|
+
})).rejects.toThrow(`Could not install extension because the current workspace at ${tempWorkspaceDir} is not trusted.`);
|
|
735
|
+
});
|
|
736
|
+
it('should add the workspace to trusted folders if user consents', async () => {
|
|
737
|
+
const trustedFoldersPath = path.join(tempHomeDir, '.cell-cli', 'trustedFolders.json');
|
|
738
|
+
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
|
739
|
+
isTrusted: false,
|
|
740
|
+
source: undefined,
|
|
741
|
+
});
|
|
742
|
+
const sourceExtDir = createExtension({
|
|
743
|
+
extensionsDir: tempHomeDir,
|
|
744
|
+
name: 'my-local-extension',
|
|
745
|
+
version: '1.0.0',
|
|
746
|
+
});
|
|
747
|
+
await extensionManager.loadExtensions();
|
|
748
|
+
await extensionManager.installOrUpdateExtension({
|
|
749
|
+
source: sourceExtDir,
|
|
750
|
+
type: 'local',
|
|
751
|
+
});
|
|
752
|
+
expect(fs.existsSync(trustedFoldersPath)).toBe(true);
|
|
753
|
+
const trustedFolders = JSON.parse(fs.readFileSync(trustedFoldersPath, 'utf-8'));
|
|
754
|
+
expect(trustedFolders[tempWorkspaceDir]).toBe('TRUST_FOLDER');
|
|
755
|
+
});
|
|
610
756
|
describe.each([true, false])('with previous extension config: %s', (isUpdate) => {
|
|
611
757
|
let sourceExtDir;
|
|
612
758
|
beforeEach(async () => {
|
|
@@ -615,14 +761,18 @@ describe('extension tests', () => {
|
|
|
615
761
|
name: 'my-local-extension',
|
|
616
762
|
version: '1.1.0',
|
|
617
763
|
});
|
|
764
|
+
await extensionManager.loadExtensions();
|
|
618
765
|
if (isUpdate) {
|
|
619
|
-
await installOrUpdateExtension({
|
|
766
|
+
await extensionManager.installOrUpdateExtension({
|
|
767
|
+
source: sourceExtDir,
|
|
768
|
+
type: 'local',
|
|
769
|
+
});
|
|
620
770
|
}
|
|
621
771
|
// Clears out any calls to mocks from the above function calls.
|
|
622
772
|
vi.clearAllMocks();
|
|
623
773
|
});
|
|
624
774
|
it(`should log an ${isUpdate ? 'update' : 'install'} event to clearcut on success`, async () => {
|
|
625
|
-
await installOrUpdateExtension({ source: sourceExtDir, type: 'local' },
|
|
775
|
+
await extensionManager.installOrUpdateExtension({ source: sourceExtDir, type: 'local' }, isUpdate
|
|
626
776
|
? {
|
|
627
777
|
name: 'my-local-extension',
|
|
628
778
|
version: '1.0.0',
|
|
@@ -640,7 +790,7 @@ describe('extension tests', () => {
|
|
|
640
790
|
it(`should ${isUpdate ? 'not ' : ''} alter the extension enablement configuration`, async () => {
|
|
641
791
|
const enablementManager = new ExtensionEnablementManager();
|
|
642
792
|
enablementManager.enable('my-local-extension', true, '/some/scope');
|
|
643
|
-
await installOrUpdateExtension({ source: sourceExtDir, type: 'local' },
|
|
793
|
+
await extensionManager.installOrUpdateExtension({ source: sourceExtDir, type: 'local' }, isUpdate
|
|
644
794
|
? {
|
|
645
795
|
name: 'my-local-extension',
|
|
646
796
|
version: '1.0.0',
|
|
@@ -673,9 +823,13 @@ describe('extension tests', () => {
|
|
|
673
823
|
},
|
|
674
824
|
},
|
|
675
825
|
});
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
826
|
+
await extensionManager.loadExtensions();
|
|
827
|
+
await expect(extensionManager.installOrUpdateExtension({
|
|
828
|
+
source: sourceExtDir,
|
|
829
|
+
type: 'local',
|
|
830
|
+
})).resolves.toMatchObject({
|
|
831
|
+
name: 'my-local-extension',
|
|
832
|
+
});
|
|
679
833
|
expect(mockRequestConsent).toHaveBeenCalledWith(`Installing extension "my-local-extension".
|
|
680
834
|
${INSTALL_WARNING_MESSAGE}
|
|
681
835
|
This extension will run the following MCP servers:
|
|
@@ -694,7 +848,11 @@ This extension will run the following MCP servers:
|
|
|
694
848
|
},
|
|
695
849
|
},
|
|
696
850
|
});
|
|
697
|
-
await
|
|
851
|
+
await extensionManager.loadExtensions();
|
|
852
|
+
await expect(extensionManager.installOrUpdateExtension({
|
|
853
|
+
source: sourceExtDir,
|
|
854
|
+
type: 'local',
|
|
855
|
+
})).resolves.toMatchObject({ name: 'my-local-extension' });
|
|
698
856
|
});
|
|
699
857
|
it('should cancel installation if user declines prompt for local extension with mcp servers', async () => {
|
|
700
858
|
const sourceExtDir = createExtension({
|
|
@@ -708,7 +866,12 @@ This extension will run the following MCP servers:
|
|
|
708
866
|
},
|
|
709
867
|
},
|
|
710
868
|
});
|
|
711
|
-
|
|
869
|
+
mockRequestConsent.mockResolvedValue(false);
|
|
870
|
+
await extensionManager.loadExtensions();
|
|
871
|
+
await expect(extensionManager.installOrUpdateExtension({
|
|
872
|
+
source: sourceExtDir,
|
|
873
|
+
type: 'local',
|
|
874
|
+
})).rejects.toThrow('Installation cancelled for "my-local-extension".');
|
|
712
875
|
});
|
|
713
876
|
it('should save the autoUpdate flag to the install metadata', async () => {
|
|
714
877
|
const sourceExtDir = createExtension({
|
|
@@ -718,11 +881,12 @@ This extension will run the following MCP servers:
|
|
|
718
881
|
});
|
|
719
882
|
const targetExtDir = path.join(userExtensionsDir, 'my-local-extension');
|
|
720
883
|
const metadataPath = path.join(targetExtDir, INSTALL_METADATA_FILENAME);
|
|
721
|
-
await
|
|
884
|
+
await extensionManager.loadExtensions();
|
|
885
|
+
await extensionManager.installOrUpdateExtension({
|
|
722
886
|
source: sourceExtDir,
|
|
723
887
|
type: 'local',
|
|
724
888
|
autoUpdate: true,
|
|
725
|
-
}
|
|
889
|
+
});
|
|
726
890
|
expect(fs.existsSync(targetExtDir)).toBe(true);
|
|
727
891
|
expect(fs.existsSync(metadataPath)).toBe(true);
|
|
728
892
|
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
|
|
@@ -745,18 +909,156 @@ This extension will run the following MCP servers:
|
|
|
745
909
|
},
|
|
746
910
|
},
|
|
747
911
|
});
|
|
748
|
-
|
|
749
|
-
// Install it
|
|
750
|
-
await installOrUpdateExtension({
|
|
912
|
+
await extensionManager.loadExtensions();
|
|
913
|
+
// Install it with hard coded consent first.
|
|
914
|
+
await extensionManager.installOrUpdateExtension({
|
|
915
|
+
source: sourceExtDir,
|
|
916
|
+
type: 'local',
|
|
917
|
+
});
|
|
918
|
+
expect(mockRequestConsent).toHaveBeenCalledOnce();
|
|
751
919
|
// Now update it without changing anything.
|
|
752
|
-
await expect(installOrUpdateExtension({ source: sourceExtDir, type: 'local' },
|
|
920
|
+
await expect(extensionManager.installOrUpdateExtension({ source: sourceExtDir, type: 'local' },
|
|
753
921
|
// Provide its own existing config as the previous config.
|
|
754
|
-
await loadExtensionConfig({
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
922
|
+
await extensionManager.loadExtensionConfig(sourceExtDir))).resolves.toMatchObject({ name: 'my-local-extension' });
|
|
923
|
+
// Still only called once
|
|
924
|
+
expect(mockRequestConsent).toHaveBeenCalledOnce();
|
|
925
|
+
});
|
|
926
|
+
it('should prompt for settings if promptForSettings', async () => {
|
|
927
|
+
const sourceExtDir = createExtension({
|
|
928
|
+
extensionsDir: tempHomeDir,
|
|
929
|
+
name: 'my-local-extension',
|
|
930
|
+
version: '1.0.0',
|
|
931
|
+
settings: [
|
|
932
|
+
{
|
|
933
|
+
name: 'API Key',
|
|
934
|
+
description: 'Your API key for the service.',
|
|
935
|
+
envVar: 'MY_API_KEY',
|
|
936
|
+
},
|
|
937
|
+
],
|
|
938
|
+
});
|
|
939
|
+
await extensionManager.loadExtensions();
|
|
940
|
+
await extensionManager.installOrUpdateExtension({
|
|
941
|
+
source: sourceExtDir,
|
|
942
|
+
type: 'local',
|
|
943
|
+
});
|
|
944
|
+
expect(mockPromptForSettings).toHaveBeenCalled();
|
|
945
|
+
});
|
|
946
|
+
it('should not prompt for settings if promptForSettings is false', async () => {
|
|
947
|
+
const sourceExtDir = createExtension({
|
|
948
|
+
extensionsDir: tempHomeDir,
|
|
949
|
+
name: 'my-local-extension',
|
|
950
|
+
version: '1.0.0',
|
|
951
|
+
settings: [
|
|
952
|
+
{
|
|
953
|
+
name: 'API Key',
|
|
954
|
+
description: 'Your API key for the service.',
|
|
955
|
+
envVar: 'MY_API_KEY',
|
|
956
|
+
},
|
|
957
|
+
],
|
|
958
|
+
});
|
|
959
|
+
extensionManager = new ExtensionManager({
|
|
960
|
+
workspaceDir: tempWorkspaceDir,
|
|
961
|
+
requestConsent: mockRequestConsent,
|
|
962
|
+
requestSetting: null,
|
|
963
|
+
settings: loadSettings(tempWorkspaceDir).merged,
|
|
964
|
+
});
|
|
965
|
+
await extensionManager.loadExtensions();
|
|
966
|
+
await extensionManager.installOrUpdateExtension({
|
|
967
|
+
source: sourceExtDir,
|
|
968
|
+
type: 'local',
|
|
969
|
+
});
|
|
970
|
+
});
|
|
971
|
+
it('should only prompt for new settings on update, and preserve old settings', async () => {
|
|
972
|
+
// 1. Create and install the "old" version of the extension.
|
|
973
|
+
const oldSourceExtDir = createExtension({
|
|
974
|
+
extensionsDir: tempHomeDir, // Create it in a temp location first
|
|
975
|
+
name: 'my-local-extension',
|
|
976
|
+
version: '1.0.0',
|
|
977
|
+
settings: [
|
|
978
|
+
{
|
|
979
|
+
name: 'API Key',
|
|
980
|
+
description: 'Your API key for the service.',
|
|
981
|
+
envVar: 'MY_API_KEY',
|
|
982
|
+
},
|
|
983
|
+
],
|
|
984
|
+
});
|
|
985
|
+
mockPromptForSettings.mockResolvedValueOnce('old-api-key');
|
|
986
|
+
await extensionManager.loadExtensions();
|
|
987
|
+
// Install it so it exists in the userExtensionsDir
|
|
988
|
+
await extensionManager.installOrUpdateExtension({
|
|
989
|
+
source: oldSourceExtDir,
|
|
990
|
+
type: 'local',
|
|
991
|
+
});
|
|
992
|
+
const envPath = new ExtensionStorage('my-local-extension').getEnvFilePath();
|
|
993
|
+
expect(fs.existsSync(envPath)).toBe(true);
|
|
994
|
+
let envContent = fs.readFileSync(envPath, 'utf-8');
|
|
995
|
+
expect(envContent).toContain('MY_API_KEY=old-api-key');
|
|
996
|
+
expect(mockPromptForSettings).toHaveBeenCalledTimes(1);
|
|
997
|
+
// 2. Create the "new" version of the extension in a new source directory.
|
|
998
|
+
const newSourceExtDir = createExtension({
|
|
999
|
+
extensionsDir: path.join(tempHomeDir, 'new-source'), // Another temp location
|
|
1000
|
+
name: 'my-local-extension', // Same name
|
|
1001
|
+
version: '1.1.0', // New version
|
|
1002
|
+
settings: [
|
|
1003
|
+
{
|
|
1004
|
+
name: 'API Key',
|
|
1005
|
+
description: 'Your API key for the service.',
|
|
1006
|
+
envVar: 'MY_API_KEY',
|
|
1007
|
+
},
|
|
1008
|
+
{
|
|
1009
|
+
name: 'New Setting',
|
|
1010
|
+
description: 'A new setting.',
|
|
1011
|
+
envVar: 'NEW_SETTING',
|
|
1012
|
+
},
|
|
1013
|
+
],
|
|
1014
|
+
});
|
|
1015
|
+
const previousExtensionConfig = extensionManager.loadExtensionConfig(path.join(userExtensionsDir, 'my-local-extension'));
|
|
1016
|
+
mockPromptForSettings.mockResolvedValueOnce('new-setting-value');
|
|
1017
|
+
// 3. Call installOrUpdateExtension to perform the update.
|
|
1018
|
+
await extensionManager.installOrUpdateExtension({ source: newSourceExtDir, type: 'local' }, previousExtensionConfig);
|
|
1019
|
+
expect(mockPromptForSettings).toHaveBeenCalledTimes(2);
|
|
1020
|
+
expect(mockPromptForSettings).toHaveBeenCalledWith(expect.objectContaining({ name: 'New Setting' }));
|
|
1021
|
+
expect(fs.existsSync(envPath)).toBe(true);
|
|
1022
|
+
envContent = fs.readFileSync(envPath, 'utf-8');
|
|
1023
|
+
expect(envContent).toContain('MY_API_KEY=old-api-key');
|
|
1024
|
+
expect(envContent).toContain('NEW_SETTING=new-setting-value');
|
|
1025
|
+
});
|
|
1026
|
+
it('should fail auto-update if settings have changed', async () => {
|
|
1027
|
+
// 1. Install initial version with autoUpdate: true
|
|
1028
|
+
const oldSourceExtDir = createExtension({
|
|
1029
|
+
extensionsDir: tempHomeDir,
|
|
1030
|
+
name: 'my-auto-update-ext',
|
|
1031
|
+
version: '1.0.0',
|
|
1032
|
+
settings: [
|
|
1033
|
+
{
|
|
1034
|
+
name: 'OLD_SETTING',
|
|
1035
|
+
envVar: 'OLD_SETTING',
|
|
1036
|
+
description: 'An old setting',
|
|
1037
|
+
},
|
|
1038
|
+
],
|
|
1039
|
+
});
|
|
1040
|
+
await extensionManager.loadExtensions();
|
|
1041
|
+
await extensionManager.installOrUpdateExtension({
|
|
1042
|
+
source: oldSourceExtDir,
|
|
1043
|
+
type: 'local',
|
|
1044
|
+
autoUpdate: true,
|
|
1045
|
+
});
|
|
1046
|
+
// 2. Create new version with different settings
|
|
1047
|
+
const newSourceExtDir = createExtension({
|
|
1048
|
+
extensionsDir: tempHomeDir,
|
|
1049
|
+
name: 'my-auto-update-ext',
|
|
1050
|
+
version: '1.1.0',
|
|
1051
|
+
settings: [
|
|
1052
|
+
{
|
|
1053
|
+
name: 'NEW_SETTING',
|
|
1054
|
+
envVar: 'NEW_SETTING',
|
|
1055
|
+
description: 'A new setting',
|
|
1056
|
+
},
|
|
1057
|
+
],
|
|
1058
|
+
});
|
|
1059
|
+
const previousExtensionConfig = extensionManager.loadExtensionConfig(path.join(userExtensionsDir, 'my-auto-update-ext'));
|
|
1060
|
+
// 3. Attempt to update and assert it fails
|
|
1061
|
+
await expect(extensionManager.installOrUpdateExtension({ source: newSourceExtDir, type: 'local', autoUpdate: true }, previousExtensionConfig)).rejects.toThrow('Extension "my-auto-update-ext" has settings changes and cannot be auto-updated. Please update manually.');
|
|
760
1062
|
});
|
|
761
1063
|
it('should throw an error for invalid extension names', async () => {
|
|
762
1064
|
const sourceExtDir = createExtension({
|
|
@@ -764,7 +1066,10 @@ This extension will run the following MCP servers:
|
|
|
764
1066
|
name: 'bad_name',
|
|
765
1067
|
version: '1.0.0',
|
|
766
1068
|
});
|
|
767
|
-
await expect(installOrUpdateExtension({
|
|
1069
|
+
await expect(extensionManager.installOrUpdateExtension({
|
|
1070
|
+
source: sourceExtDir,
|
|
1071
|
+
type: 'local',
|
|
1072
|
+
})).rejects.toThrow('Invalid extension name: "bad_name"');
|
|
768
1073
|
});
|
|
769
1074
|
describe('installing from github', () => {
|
|
770
1075
|
const gitUrl = 'https://github.com/google/gemini-test-extension.git';
|
|
@@ -797,7 +1102,11 @@ This extension will run the following MCP servers:
|
|
|
797
1102
|
version: '1.0.0',
|
|
798
1103
|
});
|
|
799
1104
|
vi.spyOn(ExtensionStorage, 'createTmpDir').mockResolvedValue(join(tempDir, extensionName));
|
|
800
|
-
await
|
|
1105
|
+
await extensionManager.loadExtensions();
|
|
1106
|
+
await extensionManager.installOrUpdateExtension({
|
|
1107
|
+
source: gitUrl,
|
|
1108
|
+
type: 'github-release',
|
|
1109
|
+
});
|
|
801
1110
|
expect(fs.existsSync(targetExtDir)).toBe(true);
|
|
802
1111
|
const metadataPath = path.join(targetExtDir, INSTALL_METADATA_FILENAME);
|
|
803
1112
|
expect(fs.existsSync(metadataPath)).toBe(true);
|
|
@@ -815,13 +1124,12 @@ This extension will run the following MCP servers:
|
|
|
815
1124
|
errorMessage: 'download failed',
|
|
816
1125
|
type: 'github-release',
|
|
817
1126
|
});
|
|
818
|
-
|
|
819
|
-
await installOrUpdateExtension({ source: gitUrl, type: 'github-release' }
|
|
820
|
-
requestConsent);
|
|
1127
|
+
await extensionManager.loadExtensions();
|
|
1128
|
+
await extensionManager.installOrUpdateExtension({ source: gitUrl, type: 'github-release' });
|
|
821
1129
|
// It gets called once to ask for a git clone, and once to consent to
|
|
822
1130
|
// the actual extension features.
|
|
823
|
-
expect(
|
|
824
|
-
expect(
|
|
1131
|
+
expect(mockRequestConsent).toHaveBeenCalledTimes(2);
|
|
1132
|
+
expect(mockRequestConsent).toHaveBeenCalledWith(expect.stringContaining('Would you like to attempt to install via "git clone" instead?'));
|
|
825
1133
|
expect(mockGit.clone).toHaveBeenCalled();
|
|
826
1134
|
const metadataPath = path.join(userExtensionsDir, extensionName, INSTALL_METADATA_FILENAME);
|
|
827
1135
|
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
|
|
@@ -833,9 +1141,13 @@ This extension will run the following MCP servers:
|
|
|
833
1141
|
errorMessage: 'download failed',
|
|
834
1142
|
type: 'github-release',
|
|
835
1143
|
});
|
|
836
|
-
|
|
837
|
-
await
|
|
838
|
-
expect(
|
|
1144
|
+
mockRequestConsent.mockResolvedValue(false);
|
|
1145
|
+
await extensionManager.loadExtensions();
|
|
1146
|
+
await expect(extensionManager.installOrUpdateExtension({
|
|
1147
|
+
source: gitUrl,
|
|
1148
|
+
type: 'github-release',
|
|
1149
|
+
})).rejects.toThrow(`Failed to install extension ${gitUrl}: download failed`);
|
|
1150
|
+
expect(mockRequestConsent).toHaveBeenCalledExactlyOnceWith(expect.stringContaining('Would you like to attempt to install via "git clone" instead?'));
|
|
839
1151
|
expect(mockGit.clone).not.toHaveBeenCalled();
|
|
840
1152
|
});
|
|
841
1153
|
it('should fallback to git clone without consent if no release data is found on first install', async () => {
|
|
@@ -844,11 +1156,14 @@ This extension will run the following MCP servers:
|
|
|
844
1156
|
failureReason: 'no release data',
|
|
845
1157
|
type: 'github-release',
|
|
846
1158
|
});
|
|
847
|
-
|
|
848
|
-
await installOrUpdateExtension({
|
|
1159
|
+
await extensionManager.loadExtensions();
|
|
1160
|
+
await extensionManager.installOrUpdateExtension({
|
|
1161
|
+
source: gitUrl,
|
|
1162
|
+
type: 'git',
|
|
1163
|
+
});
|
|
849
1164
|
// We should not see the request to use git clone, this is a repo that
|
|
850
1165
|
// has no github releases so it is the only install method.
|
|
851
|
-
expect(
|
|
1166
|
+
expect(mockRequestConsent).toHaveBeenCalledExactlyOnceWith(expect.stringContaining('Installing extension "gemini-test-extension"'));
|
|
852
1167
|
expect(mockGit.clone).toHaveBeenCalled();
|
|
853
1168
|
const metadataPath = path.join(userExtensionsDir, extensionName, INSTALL_METADATA_FILENAME);
|
|
854
1169
|
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
|
|
@@ -861,10 +1176,9 @@ This extension will run the following MCP servers:
|
|
|
861
1176
|
errorMessage: 'No release data found',
|
|
862
1177
|
type: 'github-release',
|
|
863
1178
|
});
|
|
864
|
-
|
|
865
|
-
await installOrUpdateExtension({ source: gitUrl, type: 'github-release' }
|
|
866
|
-
|
|
867
|
-
expect(requestConsent).toHaveBeenCalledWith(expect.stringContaining('Would you like to attempt to install via "git clone" instead?'));
|
|
1179
|
+
await extensionManager.loadExtensions();
|
|
1180
|
+
await extensionManager.installOrUpdateExtension({ source: gitUrl, type: 'github-release' });
|
|
1181
|
+
expect(mockRequestConsent).toHaveBeenCalledWith(expect.stringContaining('Would you like to attempt to install via "git clone" instead?'));
|
|
868
1182
|
expect(mockGit.clone).toHaveBeenCalled();
|
|
869
1183
|
});
|
|
870
1184
|
});
|
|
@@ -876,7 +1190,8 @@ This extension will run the following MCP servers:
|
|
|
876
1190
|
name: 'my-local-extension',
|
|
877
1191
|
version: '1.0.0',
|
|
878
1192
|
});
|
|
879
|
-
await
|
|
1193
|
+
await extensionManager.loadExtensions();
|
|
1194
|
+
await extensionManager.uninstallExtension('my-local-extension', false);
|
|
880
1195
|
expect(fs.existsSync(sourceExtDir)).toBe(false);
|
|
881
1196
|
});
|
|
882
1197
|
it('should uninstall an extension by name and retain existing extensions', async () => {
|
|
@@ -890,13 +1205,15 @@ This extension will run the following MCP servers:
|
|
|
890
1205
|
name: 'other-extension',
|
|
891
1206
|
version: '1.0.0',
|
|
892
1207
|
});
|
|
893
|
-
await
|
|
1208
|
+
await extensionManager.loadExtensions();
|
|
1209
|
+
await extensionManager.uninstallExtension('my-local-extension', false);
|
|
894
1210
|
expect(fs.existsSync(sourceExtDir)).toBe(false);
|
|
895
|
-
expect(
|
|
1211
|
+
expect(extensionManager.getExtensions()).toHaveLength(1);
|
|
896
1212
|
expect(fs.existsSync(otherExtDir)).toBe(true);
|
|
897
1213
|
});
|
|
898
1214
|
it('should throw an error if the extension does not exist', async () => {
|
|
899
|
-
await
|
|
1215
|
+
await extensionManager.loadExtensions();
|
|
1216
|
+
await expect(extensionManager.uninstallExtension('nonexistent-extension', false)).rejects.toThrow('Extension not found.');
|
|
900
1217
|
});
|
|
901
1218
|
describe.each([true, false])('with isUpdate: %s', (isUpdate) => {
|
|
902
1219
|
it(`should ${isUpdate ? 'not ' : ''}log uninstall event`, async () => {
|
|
@@ -909,7 +1226,8 @@ This extension will run the following MCP servers:
|
|
|
909
1226
|
type: 'local',
|
|
910
1227
|
},
|
|
911
1228
|
});
|
|
912
|
-
await
|
|
1229
|
+
await extensionManager.loadExtensions();
|
|
1230
|
+
await extensionManager.uninstallExtension('my-local-extension', isUpdate);
|
|
913
1231
|
if (isUpdate) {
|
|
914
1232
|
expect(mockLogExtensionUninstall).not.toHaveBeenCalled();
|
|
915
1233
|
expect(ExtensionUninstallEvent).not.toHaveBeenCalled();
|
|
@@ -927,7 +1245,8 @@ This extension will run the following MCP servers:
|
|
|
927
1245
|
});
|
|
928
1246
|
const enablementManager = new ExtensionEnablementManager();
|
|
929
1247
|
enablementManager.enable('test-extension', true, '/some/scope');
|
|
930
|
-
await
|
|
1248
|
+
await extensionManager.loadExtensions();
|
|
1249
|
+
await extensionManager.uninstallExtension('test-extension', isUpdate);
|
|
931
1250
|
const config = enablementManager.readConfig()['test-extension'];
|
|
932
1251
|
if (isUpdate) {
|
|
933
1252
|
expect(config).not.toBeUndefined();
|
|
@@ -949,7 +1268,8 @@ This extension will run the following MCP servers:
|
|
|
949
1268
|
type: 'git',
|
|
950
1269
|
},
|
|
951
1270
|
});
|
|
952
|
-
await
|
|
1271
|
+
await extensionManager.loadExtensions();
|
|
1272
|
+
await extensionManager.uninstallExtension(gitUrl, false);
|
|
953
1273
|
expect(fs.existsSync(sourceExtDir)).toBe(false);
|
|
954
1274
|
expect(mockLogExtensionUninstall).toHaveBeenCalled();
|
|
955
1275
|
expect(ExtensionUninstallEvent).toHaveBeenCalledWith(hashValue('gemini-sql-extension'), hashValue('https://github.com/google/gemini-sql-extension'), 'success');
|
|
@@ -961,29 +1281,32 @@ This extension will run the following MCP servers:
|
|
|
961
1281
|
version: '1.0.0',
|
|
962
1282
|
// No installMetadata provided
|
|
963
1283
|
});
|
|
964
|
-
await
|
|
1284
|
+
await extensionManager.loadExtensions();
|
|
1285
|
+
await expect(extensionManager.uninstallExtension('https://github.com/google/no-metadata-extension', false)).rejects.toThrow('Extension not found.');
|
|
965
1286
|
});
|
|
966
1287
|
});
|
|
967
1288
|
describe('disableExtension', () => {
|
|
968
|
-
it('should disable an extension at the user scope', () => {
|
|
1289
|
+
it('should disable an extension at the user scope', async () => {
|
|
969
1290
|
createExtension({
|
|
970
1291
|
extensionsDir: userExtensionsDir,
|
|
971
1292
|
name: 'my-extension',
|
|
972
1293
|
version: '1.0.0',
|
|
973
1294
|
});
|
|
974
|
-
|
|
1295
|
+
await extensionManager.loadExtensions();
|
|
1296
|
+
await extensionManager.disableExtension('my-extension', SettingScope.User);
|
|
975
1297
|
expect(isEnabled({
|
|
976
1298
|
name: 'my-extension',
|
|
977
1299
|
enabledForPath: tempWorkspaceDir,
|
|
978
1300
|
})).toBe(false);
|
|
979
1301
|
});
|
|
980
|
-
it('should disable an extension at the workspace scope', () => {
|
|
1302
|
+
it('should disable an extension at the workspace scope', async () => {
|
|
981
1303
|
createExtension({
|
|
982
1304
|
extensionsDir: userExtensionsDir,
|
|
983
1305
|
name: 'my-extension',
|
|
984
1306
|
version: '1.0.0',
|
|
985
1307
|
});
|
|
986
|
-
|
|
1308
|
+
await extensionManager.loadExtensions();
|
|
1309
|
+
await extensionManager.disableExtension('my-extension', SettingScope.Workspace);
|
|
987
1310
|
expect(isEnabled({
|
|
988
1311
|
name: 'my-extension',
|
|
989
1312
|
enabledForPath: tempHomeDir,
|
|
@@ -993,23 +1316,24 @@ This extension will run the following MCP servers:
|
|
|
993
1316
|
enabledForPath: tempWorkspaceDir,
|
|
994
1317
|
})).toBe(false);
|
|
995
1318
|
});
|
|
996
|
-
it('should handle disabling the same extension twice', () => {
|
|
1319
|
+
it('should handle disabling the same extension twice', async () => {
|
|
997
1320
|
createExtension({
|
|
998
1321
|
extensionsDir: userExtensionsDir,
|
|
999
1322
|
name: 'my-extension',
|
|
1000
1323
|
version: '1.0.0',
|
|
1001
1324
|
});
|
|
1002
|
-
|
|
1003
|
-
disableExtension('my-extension', SettingScope.User
|
|
1325
|
+
await extensionManager.loadExtensions();
|
|
1326
|
+
await extensionManager.disableExtension('my-extension', SettingScope.User);
|
|
1327
|
+
await extensionManager.disableExtension('my-extension', SettingScope.User);
|
|
1004
1328
|
expect(isEnabled({
|
|
1005
1329
|
name: 'my-extension',
|
|
1006
1330
|
enabledForPath: tempWorkspaceDir,
|
|
1007
1331
|
})).toBe(false);
|
|
1008
1332
|
});
|
|
1009
|
-
it('should throw an error if you request system scope', () => {
|
|
1010
|
-
expect(() => disableExtension('my-extension', SettingScope.System
|
|
1333
|
+
it('should throw an error if you request system scope', async () => {
|
|
1334
|
+
await expect(async () => await extensionManager.disableExtension('my-extension', SettingScope.System)).rejects.toThrow('System and SystemDefaults scopes are not supported.');
|
|
1011
1335
|
});
|
|
1012
|
-
it('should log a disable event', () => {
|
|
1336
|
+
it('should log a disable event', async () => {
|
|
1013
1337
|
createExtension({
|
|
1014
1338
|
extensionsDir: userExtensionsDir,
|
|
1015
1339
|
name: 'ext1',
|
|
@@ -1019,7 +1343,8 @@ This extension will run the following MCP servers:
|
|
|
1019
1343
|
type: 'local',
|
|
1020
1344
|
},
|
|
1021
1345
|
});
|
|
1022
|
-
|
|
1346
|
+
await extensionManager.loadExtensions();
|
|
1347
|
+
await extensionManager.disableExtension('ext1', SettingScope.Workspace);
|
|
1023
1348
|
expect(mockLogExtensionDisable).toHaveBeenCalled();
|
|
1024
1349
|
expect(ExtensionDisableEvent).toHaveBeenCalledWith(hashValue('ext1'), hashValue(userExtensionsDir), SettingScope.Workspace);
|
|
1025
1350
|
});
|
|
@@ -1029,41 +1354,40 @@ This extension will run the following MCP servers:
|
|
|
1029
1354
|
vi.restoreAllMocks();
|
|
1030
1355
|
});
|
|
1031
1356
|
const getActiveExtensions = () => {
|
|
1032
|
-
const
|
|
1033
|
-
const extensions = loadExtensions(manager);
|
|
1357
|
+
const extensions = extensionManager.getExtensions();
|
|
1034
1358
|
return extensions.filter((e) => e.isActive);
|
|
1035
1359
|
};
|
|
1036
|
-
it('should enable an extension at the user scope', () => {
|
|
1360
|
+
it('should enable an extension at the user scope', async () => {
|
|
1037
1361
|
createExtension({
|
|
1038
1362
|
extensionsDir: userExtensionsDir,
|
|
1039
1363
|
name: 'ext1',
|
|
1040
1364
|
version: '1.0.0',
|
|
1041
1365
|
});
|
|
1042
|
-
|
|
1043
|
-
disableExtension('ext1', SettingScope.User
|
|
1366
|
+
await extensionManager.loadExtensions();
|
|
1367
|
+
await extensionManager.disableExtension('ext1', SettingScope.User);
|
|
1044
1368
|
let activeExtensions = getActiveExtensions();
|
|
1045
1369
|
expect(activeExtensions).toHaveLength(0);
|
|
1046
|
-
enableExtension('ext1', SettingScope.User
|
|
1047
|
-
activeExtensions = getActiveExtensions();
|
|
1370
|
+
await extensionManager.enableExtension('ext1', SettingScope.User);
|
|
1371
|
+
activeExtensions = await getActiveExtensions();
|
|
1048
1372
|
expect(activeExtensions).toHaveLength(1);
|
|
1049
1373
|
expect(activeExtensions[0].name).toBe('ext1');
|
|
1050
1374
|
});
|
|
1051
|
-
it('should enable an extension at the workspace scope', () => {
|
|
1375
|
+
it('should enable an extension at the workspace scope', async () => {
|
|
1052
1376
|
createExtension({
|
|
1053
1377
|
extensionsDir: userExtensionsDir,
|
|
1054
1378
|
name: 'ext1',
|
|
1055
1379
|
version: '1.0.0',
|
|
1056
1380
|
});
|
|
1057
|
-
|
|
1058
|
-
disableExtension('ext1', SettingScope.Workspace
|
|
1381
|
+
await extensionManager.loadExtensions();
|
|
1382
|
+
await extensionManager.disableExtension('ext1', SettingScope.Workspace);
|
|
1059
1383
|
let activeExtensions = getActiveExtensions();
|
|
1060
1384
|
expect(activeExtensions).toHaveLength(0);
|
|
1061
|
-
enableExtension('ext1', SettingScope.Workspace
|
|
1062
|
-
activeExtensions = getActiveExtensions();
|
|
1385
|
+
await extensionManager.enableExtension('ext1', SettingScope.Workspace);
|
|
1386
|
+
activeExtensions = await getActiveExtensions();
|
|
1063
1387
|
expect(activeExtensions).toHaveLength(1);
|
|
1064
1388
|
expect(activeExtensions[0].name).toBe('ext1');
|
|
1065
1389
|
});
|
|
1066
|
-
it('should log an enable event', () => {
|
|
1390
|
+
it('should log an enable event', async () => {
|
|
1067
1391
|
createExtension({
|
|
1068
1392
|
extensionsDir: userExtensionsDir,
|
|
1069
1393
|
name: 'ext1',
|
|
@@ -1073,9 +1397,9 @@ This extension will run the following MCP servers:
|
|
|
1073
1397
|
type: 'local',
|
|
1074
1398
|
},
|
|
1075
1399
|
});
|
|
1076
|
-
|
|
1077
|
-
disableExtension('ext1', SettingScope.Workspace
|
|
1078
|
-
enableExtension('ext1', SettingScope.Workspace
|
|
1400
|
+
await extensionManager.loadExtensions();
|
|
1401
|
+
await extensionManager.disableExtension('ext1', SettingScope.Workspace);
|
|
1402
|
+
await extensionManager.enableExtension('ext1', SettingScope.Workspace);
|
|
1079
1403
|
expect(mockLogExtensionEnable).toHaveBeenCalled();
|
|
1080
1404
|
expect(ExtensionEnableEvent).toHaveBeenCalledWith(hashValue('ext1'), hashValue(userExtensionsDir), SettingScope.Workspace);
|
|
1081
1405
|
});
|