mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-21 10:34:35 -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 = {
|
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.',
|
describe: 'Installs an extension from a git repository URL or a local path.',
|
||||||
builder: (yargs) =>
|
builder: (yargs) =>
|
||||||
yargs
|
yargs
|
||||||
|
|||||||
@@ -789,16 +789,11 @@ describe('extension tests', () => {
|
|||||||
).resolves.toBe('my-local-extension');
|
).resolves.toBe('my-local-extension');
|
||||||
|
|
||||||
expect(consoleInfoSpy).toHaveBeenCalledWith(
|
expect(consoleInfoSpy).toHaveBeenCalledWith(
|
||||||
'This extension will run the following MCP servers: ',
|
`Extensions may introduce unexpected behavior.
|
||||||
);
|
Ensure you have investigated the extension source and trust the author.
|
||||||
expect(consoleInfoSpy).toHaveBeenCalledWith(
|
This extension will run the following MCP servers:
|
||||||
' * test-server (local): a local mcp server',
|
* test-server (local): node server.js
|
||||||
);
|
* test-server-2 (remote): https://google.com`,
|
||||||
expect(consoleInfoSpy).toHaveBeenCalledWith(
|
|
||||||
' * test-server-2 (remote): a remote mcp server',
|
|
||||||
);
|
|
||||||
expect(consoleInfoSpy).toHaveBeenCalledWith(
|
|
||||||
'The extension will append info to your gemini.md context',
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -822,7 +817,7 @@ describe('extension tests', () => {
|
|||||||
).resolves.toBe('my-local-extension');
|
).resolves.toBe('my-local-extension');
|
||||||
|
|
||||||
expect(mockQuestion).toHaveBeenCalledWith(
|
expect(mockQuestion).toHaveBeenCalledWith(
|
||||||
expect.stringContaining('Do you want to continue? (y/n)'),
|
expect.stringContaining('Do you want to continue? [Y/n]: '),
|
||||||
expect.any(Function),
|
expect.any(Function),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -847,7 +842,7 @@ describe('extension tests', () => {
|
|||||||
).rejects.toThrow('Installation cancelled by user.');
|
).rejects.toThrow('Installation cancelled by user.');
|
||||||
|
|
||||||
expect(mockQuestion).toHaveBeenCalledWith(
|
expect(mockQuestion).toHaveBeenCalledWith(
|
||||||
expect.stringContaining('Do you want to continue? (y/n)'),
|
expect.stringContaining('Do you want to continue? [Y/n]: '),
|
||||||
expect.any(Function),
|
expect.any(Function),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -355,7 +355,7 @@ export function annotateActiveExtensions(
|
|||||||
/**
|
/**
|
||||||
* Asks users a prompt and awaits for a y/n response
|
* Asks users a prompt and awaits for a y/n response
|
||||||
* @param prompt A yes/no prompt to ask the user
|
* @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> {
|
async function promptForContinuation(prompt: string): Promise<boolean> {
|
||||||
const readline = await import('node:readline');
|
const readline = await import('node:readline');
|
||||||
@@ -367,7 +367,7 @@ async function promptForContinuation(prompt: string): Promise<boolean> {
|
|||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
rl.question(prompt, (answer) => {
|
rl.question(prompt, (answer) => {
|
||||||
rl.close();
|
rl.close();
|
||||||
resolve(answer.toLowerCase() === 'y');
|
resolve(['y', ''].includes(answer.trim().toLowerCase()));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -407,12 +407,12 @@ export async function installExtension(
|
|||||||
) {
|
) {
|
||||||
tempDir = await ExtensionStorage.createTmpDir();
|
tempDir = await ExtensionStorage.createTmpDir();
|
||||||
try {
|
try {
|
||||||
const tagName = await downloadFromGitHubRelease(
|
const result = await downloadFromGitHubRelease(
|
||||||
installMetadata,
|
installMetadata,
|
||||||
tempDir,
|
tempDir,
|
||||||
);
|
);
|
||||||
installMetadata.type = 'github-release';
|
installMetadata.type = result.type;
|
||||||
installMetadata.releaseTag = tagName;
|
installMetadata.releaseTag = result.tagName;
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
await cloneFromGit(installMetadata, tempDir);
|
await cloneFromGit(installMetadata, tempDir);
|
||||||
installMetadata.type = 'git';
|
installMetadata.type = 'git';
|
||||||
@@ -511,23 +511,39 @@ export async function installExtension(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function requestConsent(extensionConfig: ExtensionConfig) {
|
async function requestConsent(extensionConfig: ExtensionConfig) {
|
||||||
|
const output: string[] = [];
|
||||||
const mcpServerEntries = Object.entries(extensionConfig.mcpServers || {});
|
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) {
|
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) {
|
for (const [key, mcpServer] of mcpServerEntries) {
|
||||||
const isLocal = !!mcpServer.command;
|
const isLocal = !!mcpServer.command;
|
||||||
console.info(
|
const source =
|
||||||
` * ${key} (${isLocal ? 'local' : 'remote'}): ${mcpServer.description}`,
|
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');
|
}
|
||||||
|
if (extensionConfig.contextFileName) {
|
||||||
const shouldContinue = await promptForContinuation(
|
output.push(
|
||||||
'Do you want to continue? (y/n): ',
|
`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;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export interface GitHubDownloadResult {
|
||||||
|
tagName: string;
|
||||||
|
type: 'git' | 'github-release';
|
||||||
|
}
|
||||||
export async function downloadFromGitHubRelease(
|
export async function downloadFromGitHubRelease(
|
||||||
installMetadata: ExtensionInstallMetadata,
|
installMetadata: ExtensionInstallMetadata,
|
||||||
destination: string,
|
destination: string,
|
||||||
): Promise<string> {
|
): Promise<GitHubDownloadResult> {
|
||||||
const { source, ref } = installMetadata;
|
const { source, ref } = installMetadata;
|
||||||
const { owner, repo } = parseGitHubRepoForReleases(source);
|
const { owner, repo } = parseGitHubRepoForReleases(source);
|
||||||
|
|
||||||
@@ -289,7 +292,10 @@ export async function downloadFromGitHubRelease(
|
|||||||
}
|
}
|
||||||
|
|
||||||
await fs.promises.unlink(downloadedAssetPath);
|
await fs.promises.unlink(downloadedAssetPath);
|
||||||
return releaseData.tag_name;
|
return {
|
||||||
|
tagName: releaseData.tag_name,
|
||||||
|
type: 'github-release',
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to download release from ${installMetadata.source}: ${getErrorMessage(error)}`,
|
`Failed to download release from ${installMetadata.source}: ${getErrorMessage(error)}`,
|
||||||
|
|||||||
Reference in New Issue
Block a user