Skip to content

Commit 62cb14f

Browse files
authored
feat(extensions): add --skip-settings flag to install command (#17212)
1 parent 7a65c1e commit 62cb14f

3 files changed

Lines changed: 86 additions & 51 deletions

File tree

docs/extensions/reference.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,15 @@ Gemini CLI creates a copy of the extension during installation. You must run
2323
GitHub, you must have `git` installed on your machine.
2424

2525
```bash
26-
gemini extensions install <source> [--ref <ref>] [--auto-update] [--pre-release] [--consent]
26+
gemini extensions install <source> [--ref <ref>] [--auto-update] [--pre-release] [--consent] [--skip-settings]
2727
```
2828

2929
- `<source>`: The GitHub URL or local path of the extension.
3030
- `--ref`: The git ref (branch, tag, or commit) to install.
3131
- `--auto-update`: Enable automatic updates for this extension.
3232
- `--pre-release`: Enable installation of pre-release versions.
3333
- `--consent`: Acknowledge security risks and skip the confirmation prompt.
34+
- `--skip-settings`: Skip the configuration on install process.
3435

3536
### Uninstall an extension
3637

packages/cli/src/commands/extensions/install.test.ts

Lines changed: 75 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -12,48 +12,46 @@ import {
1212
beforeEach,
1313
afterEach,
1414
type MockInstance,
15-
type Mock,
1615
} from 'vitest';
1716
import { handleInstall, installCommand } from './install.js';
1817
import yargs from 'yargs';
1918
import * as core from '@google/gemini-cli-core';
20-
import {
21-
ExtensionManager,
22-
type inferInstallMetadata,
23-
} from '../../config/extension-manager.js';
24-
import type {
25-
promptForConsentNonInteractive,
26-
requestConsentNonInteractive,
27-
} from '../../config/extensions/consent.js';
28-
import type {
29-
isWorkspaceTrusted,
30-
loadTrustedFolders,
31-
} from '../../config/trustedFolders.js';
32-
import type * as fs from 'node:fs/promises';
3319
import type { Stats } from 'node:fs';
3420
import * as path from 'node:path';
21+
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
22+
23+
const {
24+
mockInstallOrUpdateExtension,
25+
mockLoadExtensions,
26+
mockExtensionManager,
27+
mockRequestConsentNonInteractive,
28+
mockPromptForConsentNonInteractive,
29+
mockStat,
30+
mockInferInstallMetadata,
31+
mockIsWorkspaceTrusted,
32+
mockLoadTrustedFolders,
33+
mockDiscover,
34+
} = vi.hoisted(() => {
35+
const mockLoadExtensions = vi.fn();
36+
const mockInstallOrUpdateExtension = vi.fn();
37+
const mockExtensionManager = vi.fn().mockImplementation(() => ({
38+
loadExtensions: mockLoadExtensions,
39+
installOrUpdateExtension: mockInstallOrUpdateExtension,
40+
}));
3541

36-
const mockInstallOrUpdateExtension: Mock<
37-
typeof ExtensionManager.prototype.installOrUpdateExtension
38-
> = vi.hoisted(() => vi.fn());
39-
const mockRequestConsentNonInteractive: Mock<
40-
typeof requestConsentNonInteractive
41-
> = vi.hoisted(() => vi.fn());
42-
const mockPromptForConsentNonInteractive: Mock<
43-
typeof promptForConsentNonInteractive
44-
> = vi.hoisted(() => vi.fn());
45-
const mockStat: Mock<typeof fs.stat> = vi.hoisted(() => vi.fn());
46-
const mockInferInstallMetadata: Mock<typeof inferInstallMetadata> = vi.hoisted(
47-
() => vi.fn(),
48-
);
49-
const mockIsWorkspaceTrusted: Mock<typeof isWorkspaceTrusted> = vi.hoisted(() =>
50-
vi.fn(),
51-
);
52-
const mockLoadTrustedFolders: Mock<typeof loadTrustedFolders> = vi.hoisted(() =>
53-
vi.fn(),
54-
);
55-
const mockDiscover: Mock<typeof core.FolderTrustDiscoveryService.discover> =
56-
vi.hoisted(() => vi.fn());
42+
return {
43+
mockLoadExtensions,
44+
mockInstallOrUpdateExtension,
45+
mockExtensionManager,
46+
mockRequestConsentNonInteractive: vi.fn(),
47+
mockPromptForConsentNonInteractive: vi.fn(),
48+
mockStat: vi.fn(),
49+
mockInferInstallMetadata: vi.fn(),
50+
mockIsWorkspaceTrusted: vi.fn(),
51+
mockLoadTrustedFolders: vi.fn(),
52+
mockDiscover: vi.fn(),
53+
};
54+
});
5755

5856
vi.mock('../../config/extensions/consent.js', () => ({
5957
requestConsentNonInteractive: mockRequestConsentNonInteractive,
@@ -84,6 +82,7 @@ vi.mock('../../config/extension-manager.js', async (importOriginal) => ({
8482
...(await importOriginal<
8583
typeof import('../../config/extension-manager.js')
8684
>()),
85+
ExtensionManager: mockExtensionManager,
8786
inferInstallMetadata: mockInferInstallMetadata,
8887
}));
8988

@@ -117,19 +116,18 @@ describe('handleInstall', () => {
117116
let processSpy: MockInstance;
118117

119118
beforeEach(() => {
120-
debugLogSpy = vi.spyOn(core.debugLogger, 'log');
121-
debugErrorSpy = vi.spyOn(core.debugLogger, 'error');
119+
debugLogSpy = vi
120+
.spyOn(core.debugLogger, 'log')
121+
.mockImplementation(() => {});
122+
debugErrorSpy = vi
123+
.spyOn(core.debugLogger, 'error')
124+
.mockImplementation(() => {});
122125
processSpy = vi
123126
.spyOn(process, 'exit')
124127
.mockImplementation(() => undefined as never);
125128

126-
vi.spyOn(ExtensionManager.prototype, 'loadExtensions').mockResolvedValue(
127-
[],
128-
);
129-
vi.spyOn(
130-
ExtensionManager.prototype,
131-
'installOrUpdateExtension',
132-
).mockImplementation(mockInstallOrUpdateExtension);
129+
mockLoadExtensions.mockResolvedValue([]);
130+
mockInstallOrUpdateExtension.mockReset();
133131

134132
mockIsWorkspaceTrusted.mockReturnValue({ isTrusted: true, source: 'file' });
135133
mockDiscover.mockResolvedValue({
@@ -163,12 +161,7 @@ describe('handleInstall', () => {
163161
});
164162

165163
afterEach(() => {
166-
mockInstallOrUpdateExtension.mockClear();
167-
mockRequestConsentNonInteractive.mockClear();
168-
mockStat.mockClear();
169-
mockInferInstallMetadata.mockClear();
170164
vi.clearAllMocks();
171-
vi.restoreAllMocks();
172165
});
173166

174167
function createMockExtension(
@@ -288,6 +281,39 @@ describe('handleInstall', () => {
288281
expect(processSpy).toHaveBeenCalledWith(1);
289282
});
290283

284+
it('should pass promptForSetting when skipSettings is not provided', async () => {
285+
mockInstallOrUpdateExtension.mockResolvedValue({
286+
name: 'test-extension',
287+
} as unknown as core.GeminiCLIExtension);
288+
289+
await handleInstall({
290+
source: 'http://google.com',
291+
});
292+
293+
expect(mockExtensionManager).toHaveBeenCalledWith(
294+
expect.objectContaining({
295+
requestSetting: promptForSetting,
296+
}),
297+
);
298+
});
299+
300+
it('should pass null for requestSetting when skipSettings is true', async () => {
301+
mockInstallOrUpdateExtension.mockResolvedValue({
302+
name: 'test-extension',
303+
} as unknown as core.GeminiCLIExtension);
304+
305+
await handleInstall({
306+
source: 'http://google.com',
307+
skipSettings: true,
308+
});
309+
310+
expect(mockExtensionManager).toHaveBeenCalledWith(
311+
expect.objectContaining({
312+
requestSetting: null,
313+
}),
314+
);
315+
});
316+
291317
it('should proceed if local path is already trusted', async () => {
292318
mockInstallOrUpdateExtension.mockResolvedValue(
293319
createMockExtension({

packages/cli/src/commands/extensions/install.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ interface InstallArgs {
3737
autoUpdate?: boolean;
3838
allowPreRelease?: boolean;
3939
consent?: boolean;
40+
skipSettings?: boolean;
4041
}
4142

4243
export async function handleInstall(args: InstallArgs) {
@@ -153,7 +154,7 @@ export async function handleInstall(args: InstallArgs) {
153154
const extensionManager = new ExtensionManager({
154155
workspaceDir,
155156
requestConsent,
156-
requestSetting: promptForSetting,
157+
requestSetting: args.skipSettings ? null : promptForSetting,
157158
settings,
158159
});
159160
await extensionManager.loadExtensions();
@@ -196,6 +197,11 @@ export const installCommand: CommandModule = {
196197
type: 'boolean',
197198
default: false,
198199
})
200+
.option('skip-settings', {
201+
describe: 'Skip the configuration on install process.',
202+
type: 'boolean',
203+
default: false,
204+
})
199205
.check((argv) => {
200206
if (!argv.source) {
201207
throw new Error('The source argument must be provided.');
@@ -214,6 +220,8 @@ export const installCommand: CommandModule = {
214220
allowPreRelease: argv['pre-release'] as boolean | undefined,
215221
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
216222
consent: argv['consent'] as boolean | undefined,
223+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
224+
skipSettings: argv['skip-settings'] as boolean | undefined,
217225
});
218226
await exitCli();
219227
},

0 commit comments

Comments
 (0)