mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-21 11:30:38 -07:00
feat(extensions): add --skip-settings flag to install command (#17212)
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user