mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 22:21:22 -07:00
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:
@@ -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
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)}`,
|
||||
|
||||
Reference in New Issue
Block a user