diff --git a/packages/cli/src/commands/extensions/install.ts b/packages/cli/src/commands/extensions/install.ts index fe43fdbf0a..53ed0ffd31 100644 --- a/packages/cli/src/commands/extensions/install.ts +++ b/packages/cli/src/commands/extensions/install.ts @@ -47,7 +47,7 @@ export async function handleInstall(args: InstallArgs) { throw new Error('Either --source or --path must be provided.'); } - const name = await installExtension(installMetadata); + const name = await installExtension(installMetadata, true); console.log(`Extension "${name}" installed successfully and enabled.`); } catch (error) { console.error(getErrorMessage(error)); diff --git a/packages/cli/src/config/extension.test.ts b/packages/cli/src/config/extension.test.ts index c22c6340cc..0453d4319e 100644 --- a/packages/cli/src/config/extension.test.ts +++ b/packages/cli/src/config/extension.test.ts @@ -596,7 +596,7 @@ describe('extension tests', () => { mockQuestion.mockImplementation((_query, callback) => callback('y')); await expect( - installExtension({ source: sourceExtDir, type: 'local' }), + installExtension({ source: sourceExtDir, type: 'local' }, true), ).resolves.toBe('my-local-extension'); expect(consoleInfoSpy).toHaveBeenCalledWith( @@ -629,7 +629,7 @@ describe('extension tests', () => { mockQuestion.mockImplementation((_query, callback) => callback('y')); await expect( - installExtension({ source: sourceExtDir, type: 'local' }), + installExtension({ source: sourceExtDir, type: 'local' }, true), ).resolves.toBe('my-local-extension'); expect(mockQuestion).toHaveBeenCalledWith( @@ -654,7 +654,7 @@ describe('extension tests', () => { mockQuestion.mockImplementation((_query, callback) => callback('n')); await expect( - installExtension({ source: sourceExtDir, type: 'local' }), + installExtension({ source: sourceExtDir, type: 'local' }, true), ).rejects.toThrow('Installation cancelled by user.'); expect(mockQuestion).toHaveBeenCalledWith( @@ -662,6 +662,24 @@ describe('extension tests', () => { expect.any(Function), ); }); + + it('should ignore consent flow if not required', async () => { + const sourceExtDir = createExtension({ + extensionsDir: tempHomeDir, + name: 'my-local-extension', + version: '1.0.0', + mcpServers: { + 'test-server': { + command: 'node', + args: ['server.js'], + }, + }, + }); + + await expect( + installExtension({ source: sourceExtDir, type: 'local' }, false), + ).resolves.toBe('my-local-extension'); + }); }); describe('uninstallExtension', () => { diff --git a/packages/cli/src/config/extension.ts b/packages/cli/src/config/extension.ts index cbaf155fb8..89dce696f2 100644 --- a/packages/cli/src/config/extension.ts +++ b/packages/cli/src/config/extension.ts @@ -418,6 +418,7 @@ async function promptForContinuation(prompt: string): Promise { export async function installExtension( installMetadata: ExtensionInstallMetadata, + askConsent: boolean = false, cwd: string = process.cwd(), ): Promise { const logger = getClearcutLogger(cwd); @@ -482,30 +483,9 @@ export async function installExtension( `Extension "${newExtensionName}" is already installed. Please uninstall it first.`, ); } - - const mcpServerEntries = Object.entries( - newExtensionConfig.mcpServers || {}, - ); - if (mcpServerEntries.length) { - console.info('This extension will run the following MCP servers: '); - for (const [key, mcpServer] of mcpServerEntries) { - const isLocal = !!mcpServer.command; - console.info( - ` * ${key} (${isLocal ? 'local' : 'remote'}): ${mcpServer.description}`, - ); - } - console.info( - 'The extension will append info to your gemini.md context', - ); - - const shouldContinue = await promptForContinuation( - 'Do you want to continue? (y/n): ', - ); - if (!shouldContinue) { - throw new Error('Installation cancelled by user.'); - } + if (askConsent) { + await requestConsent(newExtensionConfig); } - await fs.promises.mkdir(destinationPath, { recursive: true }); if (installMetadata.type === 'local' || installMetadata.type === 'git') { @@ -556,6 +536,27 @@ export async function installExtension( } } +async function requestConsent(extensionConfig: ExtensionConfig) { + const mcpServerEntries = Object.entries(extensionConfig.mcpServers || {}); + if (mcpServerEntries.length) { + console.info('This extension will run the following MCP servers: '); + for (const [key, mcpServer] of mcpServerEntries) { + const isLocal = !!mcpServer.command; + console.info( + ` * ${key} (${isLocal ? 'local' : 'remote'}): ${mcpServer.description}`, + ); + } + console.info('The extension will append info to your gemini.md context'); + + const shouldContinue = await promptForContinuation( + 'Do you want to continue? (y/n): ', + ); + if (!shouldContinue) { + throw new Error('Installation cancelled by user.'); + } + } +} + export async function loadExtensionConfig( context: LoadExtensionContext, ): Promise { @@ -684,6 +685,7 @@ export async function updateExtension( type: extension.type, ref: extension.ref, }, + false, cwd, );