feat(extensions): add --skip-settings flag to install command (#17212)

This commit is contained in:
Ratish P
2026-03-20 21:40:59 +05:30
committed by GitHub
parent 7a65c1e91d
commit 62cb14fa52
3 changed files with 86 additions and 51 deletions

View File

@@ -23,7 +23,7 @@ Gemini CLI creates a copy of the extension during installation. You must run
GitHub, you must have `git` installed on your machine.
```bash
gemini extensions install <source> [--ref <ref>] [--auto-update] [--pre-release] [--consent]
gemini extensions install <source> [--ref <ref>] [--auto-update] [--pre-release] [--consent] [--skip-settings]
```
- `<source>`: The GitHub URL or local path of the extension.
@@ -31,6 +31,7 @@ gemini extensions install <source> [--ref <ref>] [--auto-update] [--pre-release]
- `--auto-update`: Enable automatic updates for this extension.
- `--pre-release`: Enable installation of pre-release versions.
- `--consent`: Acknowledge security risks and skip the confirmation prompt.
- `--skip-settings`: Skip the configuration on install process.
### Uninstall an extension

View File

@@ -12,48 +12,46 @@ import {
beforeEach,
afterEach,
type MockInstance,
type Mock,
} from 'vitest';
import { handleInstall, installCommand } from './install.js';
import yargs from 'yargs';
import * as core from '@google/gemini-cli-core';
import {
ExtensionManager,
type inferInstallMetadata,
} from '../../config/extension-manager.js';
import type {
promptForConsentNonInteractive,
requestConsentNonInteractive,
} from '../../config/extensions/consent.js';
import type {
isWorkspaceTrusted,
loadTrustedFolders,
} from '../../config/trustedFolders.js';
import type * as fs from 'node:fs/promises';
import type { Stats } from 'node:fs';
import * as path from 'node:path';
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
const mockInstallOrUpdateExtension: Mock<
typeof ExtensionManager.prototype.installOrUpdateExtension
> = vi.hoisted(() => vi.fn());
const mockRequestConsentNonInteractive: Mock<
typeof requestConsentNonInteractive
> = vi.hoisted(() => vi.fn());
const mockPromptForConsentNonInteractive: Mock<
typeof promptForConsentNonInteractive
> = vi.hoisted(() => vi.fn());
const mockStat: Mock<typeof fs.stat> = vi.hoisted(() => vi.fn());
const mockInferInstallMetadata: Mock<typeof inferInstallMetadata> = vi.hoisted(
() => vi.fn(),
);
const mockIsWorkspaceTrusted: Mock<typeof isWorkspaceTrusted> = vi.hoisted(() =>
vi.fn(),
);
const mockLoadTrustedFolders: Mock<typeof loadTrustedFolders> = vi.hoisted(() =>
vi.fn(),
);
const mockDiscover: Mock<typeof core.FolderTrustDiscoveryService.discover> =
vi.hoisted(() => vi.fn());
const {
mockInstallOrUpdateExtension,
mockLoadExtensions,
mockExtensionManager,
mockRequestConsentNonInteractive,
mockPromptForConsentNonInteractive,
mockStat,
mockInferInstallMetadata,
mockIsWorkspaceTrusted,
mockLoadTrustedFolders,
mockDiscover,
} = vi.hoisted(() => {
const mockLoadExtensions = vi.fn();
const mockInstallOrUpdateExtension = vi.fn();
const mockExtensionManager = vi.fn().mockImplementation(() => ({
loadExtensions: mockLoadExtensions,
installOrUpdateExtension: mockInstallOrUpdateExtension,
}));
return {
mockLoadExtensions,
mockInstallOrUpdateExtension,
mockExtensionManager,
mockRequestConsentNonInteractive: vi.fn(),
mockPromptForConsentNonInteractive: vi.fn(),
mockStat: vi.fn(),
mockInferInstallMetadata: vi.fn(),
mockIsWorkspaceTrusted: vi.fn(),
mockLoadTrustedFolders: vi.fn(),
mockDiscover: vi.fn(),
};
});
vi.mock('../../config/extensions/consent.js', () => ({
requestConsentNonInteractive: mockRequestConsentNonInteractive,
@@ -84,6 +82,7 @@ vi.mock('../../config/extension-manager.js', async (importOriginal) => ({
...(await importOriginal<
typeof import('../../config/extension-manager.js')
>()),
ExtensionManager: mockExtensionManager,
inferInstallMetadata: mockInferInstallMetadata,
}));
@@ -117,19 +116,18 @@ describe('handleInstall', () => {
let processSpy: MockInstance;
beforeEach(() => {
debugLogSpy = vi.spyOn(core.debugLogger, 'log');
debugErrorSpy = vi.spyOn(core.debugLogger, 'error');
debugLogSpy = vi
.spyOn(core.debugLogger, 'log')
.mockImplementation(() => {});
debugErrorSpy = vi
.spyOn(core.debugLogger, 'error')
.mockImplementation(() => {});
processSpy = vi
.spyOn(process, 'exit')
.mockImplementation(() => undefined as never);
vi.spyOn(ExtensionManager.prototype, 'loadExtensions').mockResolvedValue(
[],
);
vi.spyOn(
ExtensionManager.prototype,
'installOrUpdateExtension',
).mockImplementation(mockInstallOrUpdateExtension);
mockLoadExtensions.mockResolvedValue([]);
mockInstallOrUpdateExtension.mockReset();
mockIsWorkspaceTrusted.mockReturnValue({ isTrusted: true, source: 'file' });
mockDiscover.mockResolvedValue({
@@ -163,12 +161,7 @@ describe('handleInstall', () => {
});
afterEach(() => {
mockInstallOrUpdateExtension.mockClear();
mockRequestConsentNonInteractive.mockClear();
mockStat.mockClear();
mockInferInstallMetadata.mockClear();
vi.clearAllMocks();
vi.restoreAllMocks();
});
function createMockExtension(
@@ -288,6 +281,39 @@ describe('handleInstall', () => {
expect(processSpy).toHaveBeenCalledWith(1);
});
it('should pass promptForSetting when skipSettings is not provided', async () => {
mockInstallOrUpdateExtension.mockResolvedValue({
name: 'test-extension',
} as unknown as core.GeminiCLIExtension);
await handleInstall({
source: 'http://google.com',
});
expect(mockExtensionManager).toHaveBeenCalledWith(
expect.objectContaining({
requestSetting: promptForSetting,
}),
);
});
it('should pass null for requestSetting when skipSettings is true', async () => {
mockInstallOrUpdateExtension.mockResolvedValue({
name: 'test-extension',
} as unknown as core.GeminiCLIExtension);
await handleInstall({
source: 'http://google.com',
skipSettings: true,
});
expect(mockExtensionManager).toHaveBeenCalledWith(
expect.objectContaining({
requestSetting: null,
}),
);
});
it('should proceed if local path is already trusted', async () => {
mockInstallOrUpdateExtension.mockResolvedValue(
createMockExtension({

View File

@@ -37,6 +37,7 @@ interface InstallArgs {
autoUpdate?: boolean;
allowPreRelease?: boolean;
consent?: boolean;
skipSettings?: boolean;
}
export async function handleInstall(args: InstallArgs) {
@@ -153,7 +154,7 @@ export async function handleInstall(args: InstallArgs) {
const extensionManager = new ExtensionManager({
workspaceDir,
requestConsent,
requestSetting: promptForSetting,
requestSetting: args.skipSettings ? null : promptForSetting,
settings,
});
await extensionManager.loadExtensions();
@@ -196,6 +197,11 @@ export const installCommand: CommandModule = {
type: 'boolean',
default: false,
})
.option('skip-settings', {
describe: 'Skip the configuration on install process.',
type: 'boolean',
default: false,
})
.check((argv) => {
if (!argv.source) {
throw new Error('The source argument must be provided.');
@@ -214,6 +220,8 @@ export const installCommand: CommandModule = {
allowPreRelease: argv['pre-release'] as boolean | undefined,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
consent: argv['consent'] as boolean | undefined,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
skipSettings: argv['skip-settings'] as boolean | undefined,
});
await exitCli();
},