diff --git a/packages/cli/src/commands/extensions/install.ts b/packages/cli/src/commands/extensions/install.ts index 0f289bb08c..30c658c3d4 100644 --- a/packages/cli/src/commands/extensions/install.ts +++ b/packages/cli/src/commands/extensions/install.ts @@ -57,7 +57,7 @@ export async function handleInstall(args: InstallArgs) { } export const installCommand: CommandModule = { - command: 'install [source]', + command: 'install [] [--path] [--ref] [--auto-update]', describe: 'Installs an extension from a git repository URL or a local path.', builder: (yargs) => yargs diff --git a/packages/cli/src/config/extension.test.ts b/packages/cli/src/config/extension.test.ts index f2b7aa7f93..895f4d2769 100644 --- a/packages/cli/src/config/extension.test.ts +++ b/packages/cli/src/config/extension.test.ts @@ -789,16 +789,11 @@ describe('extension tests', () => { ).resolves.toBe('my-local-extension'); expect(consoleInfoSpy).toHaveBeenCalledWith( - 'This extension will run the following MCP servers: ', - ); - expect(consoleInfoSpy).toHaveBeenCalledWith( - ' * test-server (local): a local mcp server', - ); - expect(consoleInfoSpy).toHaveBeenCalledWith( - ' * test-server-2 (remote): a remote mcp server', - ); - expect(consoleInfoSpy).toHaveBeenCalledWith( - 'The extension will append info to your gemini.md context', + `Extensions may introduce unexpected behavior. +Ensure you have investigated the extension source and trust the author. +This extension will run the following MCP servers: + * test-server (local): node server.js + * test-server-2 (remote): https://google.com`, ); }); @@ -822,7 +817,7 @@ describe('extension tests', () => { ).resolves.toBe('my-local-extension'); expect(mockQuestion).toHaveBeenCalledWith( - expect.stringContaining('Do you want to continue? (y/n)'), + expect.stringContaining('Do you want to continue? [Y/n]: '), expect.any(Function), ); }); @@ -847,7 +842,7 @@ describe('extension tests', () => { ).rejects.toThrow('Installation cancelled by user.'); expect(mockQuestion).toHaveBeenCalledWith( - expect.stringContaining('Do you want to continue? (y/n)'), + expect.stringContaining('Do you want to continue? [Y/n]: '), expect.any(Function), ); }); diff --git a/packages/cli/src/config/extension.ts b/packages/cli/src/config/extension.ts index 346d9343a9..523adf8e81 100644 --- a/packages/cli/src/config/extension.ts +++ b/packages/cli/src/config/extension.ts @@ -355,7 +355,7 @@ export function annotateActiveExtensions( /** * Asks users a prompt and awaits for a y/n response * @param prompt A yes/no prompt to ask the user - * @returns Whether or not the user answers 'y' (yes) + * @returns Whether or not the user answers 'y' (yes). Defaults to 'yes' on enter. */ async function promptForContinuation(prompt: string): Promise { const readline = await import('node:readline'); @@ -367,7 +367,7 @@ async function promptForContinuation(prompt: string): Promise { return new Promise((resolve) => { rl.question(prompt, (answer) => { rl.close(); - resolve(answer.toLowerCase() === 'y'); + resolve(['y', ''].includes(answer.trim().toLowerCase())); }); }); } @@ -407,12 +407,12 @@ export async function installExtension( ) { tempDir = await ExtensionStorage.createTmpDir(); try { - const tagName = await downloadFromGitHubRelease( + const result = await downloadFromGitHubRelease( installMetadata, tempDir, ); - installMetadata.type = 'github-release'; - installMetadata.releaseTag = tagName; + installMetadata.type = result.type; + installMetadata.releaseTag = result.tagName; } catch (_error) { await cloneFromGit(installMetadata, tempDir); installMetadata.type = 'git'; @@ -511,23 +511,39 @@ export async function installExtension( } async function requestConsent(extensionConfig: ExtensionConfig) { + const output: string[] = []; const mcpServerEntries = Object.entries(extensionConfig.mcpServers || {}); + output.push('Extensions may introduce unexpected behavior.'); + output.push( + 'Ensure you have investigated the extension source and trust the author.', + ); + if (mcpServerEntries.length) { - console.info('This extension will run the following MCP servers: '); + output.push('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}`, - ); + const source = + mcpServer.httpUrl ?? + `${mcpServer.command || ''}${mcpServer.args ? ' ' + mcpServer.args.join(' ') : ''}`; + output.push(` * ${key} (${isLocal ? 'local' : 'remote'}): ${source}`); } - console.info('The extension will append info to your gemini.md context'); - - const shouldContinue = await promptForContinuation( - 'Do you want to continue? (y/n): ', + } + if (extensionConfig.contextFileName) { + output.push( + `This extension will append info to your gemini.md context using ${extensionConfig.contextFileName}`, ); - if (!shouldContinue) { - throw new Error('Installation cancelled by user.'); - } + } + if (extensionConfig.excludeTools) { + output.push( + `This extension will exclude the following core tools: ${extensionConfig.excludeTools}`, + ); + } + console.info(output.join('\n')); + const shouldContinue = await promptForContinuation( + 'Do you want to continue? [Y/n]: ', + ); + if (!shouldContinue) { + throw new Error('Installation cancelled by user.'); } } diff --git a/packages/cli/src/config/extensions/github.ts b/packages/cli/src/config/extensions/github.ts index ae2ec9077d..ead855cb98 100644 --- a/packages/cli/src/config/extensions/github.ts +++ b/packages/cli/src/config/extensions/github.ts @@ -220,11 +220,14 @@ export async function checkForExtensionUpdate( return; } } - +export interface GitHubDownloadResult { + tagName: string; + type: 'git' | 'github-release'; +} export async function downloadFromGitHubRelease( installMetadata: ExtensionInstallMetadata, destination: string, -): Promise { +): Promise { const { source, ref } = installMetadata; const { owner, repo } = parseGitHubRepoForReleases(source); @@ -289,7 +292,10 @@ export async function downloadFromGitHubRelease( } await fs.promises.unlink(downloadedAssetPath); - return releaseData.tag_name; + return { + tagName: releaseData.tag_name, + type: 'github-release', + }; } catch (error) { throw new Error( `Failed to download release from ${installMetadata.source}: ${getErrorMessage(error)}`,