mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-23 11:34:44 -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.
|
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();
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user