Improve extensions consent flow, command formatting, github-release behavior (#9121)

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
christine betts
2025-09-22 19:50:12 -04:00
committed by GitHub
parent 4cdf9207f3
commit 570b0086b6
4 changed files with 49 additions and 32 deletions

View File

@@ -57,7 +57,7 @@ export async function handleInstall(args: InstallArgs) {
}
export const installCommand: CommandModule = {
command: 'install [source]',
command: 'install [<source>] [--path] [--ref] [--auto-update]',
describe: 'Installs an extension from a git repository URL or a local path.',
builder: (yargs) =>
yargs

View File

@@ -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),
);
});

View File

@@ -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<boolean> {
const readline = await import('node:readline');
@@ -367,7 +367,7 @@ async function promptForContinuation(prompt: string): Promise<boolean> {
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.');
}
}

View File

@@ -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<string> {
): Promise<GitHubDownloadResult> {
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)}`,