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
+2 -1
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. GitHub, you must have `git` installed on your machine.
```bash ```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. - `<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. - `--auto-update`: Enable automatic updates for this extension.
- `--pre-release`: Enable installation of pre-release versions. - `--pre-release`: Enable installation of pre-release versions.
- `--consent`: Acknowledge security risks and skip the confirmation prompt. - `--consent`: Acknowledge security risks and skip the confirmation prompt.
- `--skip-settings`: Skip the configuration on install process.
### Uninstall an extension ### Uninstall an extension
@@ -12,48 +12,46 @@ import {
beforeEach, beforeEach,
afterEach, afterEach,
type MockInstance, type MockInstance,
type Mock,
} from 'vitest'; } from 'vitest';
import { handleInstall, installCommand } from './install.js'; import { handleInstall, installCommand } from './install.js';
import yargs from 'yargs'; import yargs from 'yargs';
import * as core from '@google/gemini-cli-core'; 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 type { Stats } from 'node:fs';
import * as path from 'node:path'; import * as path from 'node:path';
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
const mockInstallOrUpdateExtension: Mock< const {
typeof ExtensionManager.prototype.installOrUpdateExtension mockInstallOrUpdateExtension,
> = vi.hoisted(() => vi.fn()); mockLoadExtensions,
const mockRequestConsentNonInteractive: Mock< mockExtensionManager,
typeof requestConsentNonInteractive mockRequestConsentNonInteractive,
> = vi.hoisted(() => vi.fn()); mockPromptForConsentNonInteractive,
const mockPromptForConsentNonInteractive: Mock< mockStat,
typeof promptForConsentNonInteractive mockInferInstallMetadata,
> = vi.hoisted(() => vi.fn()); mockIsWorkspaceTrusted,
const mockStat: Mock<typeof fs.stat> = vi.hoisted(() => vi.fn()); mockLoadTrustedFolders,
const mockInferInstallMetadata: Mock<typeof inferInstallMetadata> = vi.hoisted( mockDiscover,
() => vi.fn(), } = vi.hoisted(() => {
); const mockLoadExtensions = vi.fn();
const mockIsWorkspaceTrusted: Mock<typeof isWorkspaceTrusted> = vi.hoisted(() => const mockInstallOrUpdateExtension = vi.fn();
vi.fn(), const mockExtensionManager = vi.fn().mockImplementation(() => ({
); loadExtensions: mockLoadExtensions,
const mockLoadTrustedFolders: Mock<typeof loadTrustedFolders> = vi.hoisted(() => installOrUpdateExtension: mockInstallOrUpdateExtension,
vi.fn(), }));
);
const mockDiscover: Mock<typeof core.FolderTrustDiscoveryService.discover> = return {
vi.hoisted(() => vi.fn()); 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', () => ({ vi.mock('../../config/extensions/consent.js', () => ({
requestConsentNonInteractive: mockRequestConsentNonInteractive, requestConsentNonInteractive: mockRequestConsentNonInteractive,
@@ -84,6 +82,7 @@ vi.mock('../../config/extension-manager.js', async (importOriginal) => ({
...(await importOriginal< ...(await importOriginal<
typeof import('../../config/extension-manager.js') typeof import('../../config/extension-manager.js')
>()), >()),
ExtensionManager: mockExtensionManager,
inferInstallMetadata: mockInferInstallMetadata, inferInstallMetadata: mockInferInstallMetadata,
})); }));
@@ -117,19 +116,18 @@ describe('handleInstall', () => {
let processSpy: MockInstance; let processSpy: MockInstance;
beforeEach(() => { beforeEach(() => {
debugLogSpy = vi.spyOn(core.debugLogger, 'log'); debugLogSpy = vi
debugErrorSpy = vi.spyOn(core.debugLogger, 'error'); .spyOn(core.debugLogger, 'log')
.mockImplementation(() => {});
debugErrorSpy = vi
.spyOn(core.debugLogger, 'error')
.mockImplementation(() => {});
processSpy = vi processSpy = vi
.spyOn(process, 'exit') .spyOn(process, 'exit')
.mockImplementation(() => undefined as never); .mockImplementation(() => undefined as never);
vi.spyOn(ExtensionManager.prototype, 'loadExtensions').mockResolvedValue( mockLoadExtensions.mockResolvedValue([]);
[], mockInstallOrUpdateExtension.mockReset();
);
vi.spyOn(
ExtensionManager.prototype,
'installOrUpdateExtension',
).mockImplementation(mockInstallOrUpdateExtension);
mockIsWorkspaceTrusted.mockReturnValue({ isTrusted: true, source: 'file' }); mockIsWorkspaceTrusted.mockReturnValue({ isTrusted: true, source: 'file' });
mockDiscover.mockResolvedValue({ mockDiscover.mockResolvedValue({
@@ -163,12 +161,7 @@ describe('handleInstall', () => {
}); });
afterEach(() => { afterEach(() => {
mockInstallOrUpdateExtension.mockClear();
mockRequestConsentNonInteractive.mockClear();
mockStat.mockClear();
mockInferInstallMetadata.mockClear();
vi.clearAllMocks(); vi.clearAllMocks();
vi.restoreAllMocks();
}); });
function createMockExtension( function createMockExtension(
@@ -288,6 +281,39 @@ describe('handleInstall', () => {
expect(processSpy).toHaveBeenCalledWith(1); 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 () => { it('should proceed if local path is already trusted', async () => {
mockInstallOrUpdateExtension.mockResolvedValue( mockInstallOrUpdateExtension.mockResolvedValue(
createMockExtension({ createMockExtension({
@@ -37,6 +37,7 @@ interface InstallArgs {
autoUpdate?: boolean; autoUpdate?: boolean;
allowPreRelease?: boolean; allowPreRelease?: boolean;
consent?: boolean; consent?: boolean;
skipSettings?: boolean;
} }
export async function handleInstall(args: InstallArgs) { export async function handleInstall(args: InstallArgs) {
@@ -153,7 +154,7 @@ export async function handleInstall(args: InstallArgs) {
const extensionManager = new ExtensionManager({ const extensionManager = new ExtensionManager({
workspaceDir, workspaceDir,
requestConsent, requestConsent,
requestSetting: promptForSetting, requestSetting: args.skipSettings ? null : promptForSetting,
settings, settings,
}); });
await extensionManager.loadExtensions(); await extensionManager.loadExtensions();
@@ -196,6 +197,11 @@ export const installCommand: CommandModule = {
type: 'boolean', type: 'boolean',
default: false, default: false,
}) })
.option('skip-settings', {
describe: 'Skip the configuration on install process.',
type: 'boolean',
default: false,
})
.check((argv) => { .check((argv) => {
if (!argv.source) { if (!argv.source) {
throw new Error('The source argument must be provided.'); throw new Error('The source argument must be provided.');
@@ -214,6 +220,8 @@ export const installCommand: CommandModule = {
allowPreRelease: argv['pre-release'] as boolean | undefined, allowPreRelease: argv['pre-release'] as boolean | undefined,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
consent: argv['consent'] as boolean | undefined, 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(); await exitCli();
}, },