mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
[extensions] Add extension management install command (#6703)
This commit is contained in:
22
packages/cli/src/commands/extensions.tsx
Normal file
22
packages/cli/src/commands/extensions.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { CommandModule } from 'yargs';
|
||||
import { installCommand } from './extensions/install.js';
|
||||
|
||||
export const extensionsCommand: CommandModule = {
|
||||
command: 'extensions <command>',
|
||||
describe: 'Manage Gemini CLI extensions.',
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.command(installCommand)
|
||||
.demandCommand(1, 'You need at least one command before continuing.')
|
||||
.version(false),
|
||||
handler: () => {
|
||||
// This handler is not called when a subcommand is provided.
|
||||
// Yargs will show the help menu.
|
||||
},
|
||||
};
|
||||
25
packages/cli/src/commands/extensions/install.test.ts
Normal file
25
packages/cli/src/commands/extensions/install.test.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { installCommand } from './install.js';
|
||||
import yargs from 'yargs';
|
||||
|
||||
describe('extensions install command', () => {
|
||||
it('should fail if no source is provided', () => {
|
||||
const validationParser = yargs([]).command(installCommand).fail(false);
|
||||
expect(() => validationParser.parse('install')).toThrow(
|
||||
'Either a git URL --source or a --path must be provided.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should fail if both git source and local path are provided', () => {
|
||||
const validationParser = yargs([]).command(installCommand).fail(false);
|
||||
expect(() =>
|
||||
validationParser.parse('install --source some-url --path /some/path'),
|
||||
).toThrow('Arguments source and path are mutually exclusive');
|
||||
});
|
||||
});
|
||||
62
packages/cli/src/commands/extensions/install.ts
Normal file
62
packages/cli/src/commands/extensions/install.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { CommandModule } from 'yargs';
|
||||
import {
|
||||
installExtension,
|
||||
ExtensionInstallMetadata,
|
||||
} from '../../config/extension.js';
|
||||
|
||||
interface InstallArgs {
|
||||
source?: string;
|
||||
path?: string;
|
||||
}
|
||||
|
||||
export async function handleInstall(args: InstallArgs) {
|
||||
try {
|
||||
const installMetadata: ExtensionInstallMetadata = {
|
||||
source: (args.source || args.path) as string,
|
||||
type: args.source ? 'git' : 'local',
|
||||
};
|
||||
const extensionName = await installExtension(installMetadata);
|
||||
console.log(
|
||||
`Extension "${extensionName}" installed successfully and enabled.`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error((error as Error).message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
export const installCommand: CommandModule = {
|
||||
command: 'install [--source | --path ]',
|
||||
describe: 'Installs an extension from a git repository or a local path.',
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.option('source', {
|
||||
describe: 'The git URL of the extension to install.',
|
||||
type: 'string',
|
||||
})
|
||||
.option('path', {
|
||||
describe: 'Path to a local extension directory.',
|
||||
type: 'string',
|
||||
})
|
||||
.conflicts('source', 'path')
|
||||
.check((argv) => {
|
||||
if (!argv.source && !argv.path) {
|
||||
throw new Error(
|
||||
'Either a git URL --source or a --path must be provided.',
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
handler: async (argv) => {
|
||||
await handleInstall({
|
||||
source: argv['source'] as string | undefined,
|
||||
path: argv['path'] as string | undefined,
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -282,7 +282,7 @@ describe('Configuration Integration Tests', () => {
|
||||
'test',
|
||||
];
|
||||
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
|
||||
// Verify that the argument was parsed correctly
|
||||
expect(argv.approvalMode).toBe('auto_edit');
|
||||
@@ -306,7 +306,7 @@ describe('Configuration Integration Tests', () => {
|
||||
'test',
|
||||
];
|
||||
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
|
||||
expect(argv.approvalMode).toBe('yolo');
|
||||
expect(argv.prompt).toBe('test');
|
||||
@@ -329,7 +329,7 @@ describe('Configuration Integration Tests', () => {
|
||||
'test',
|
||||
];
|
||||
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
|
||||
expect(argv.approvalMode).toBe('default');
|
||||
expect(argv.prompt).toBe('test');
|
||||
@@ -345,7 +345,7 @@ describe('Configuration Integration Tests', () => {
|
||||
try {
|
||||
process.argv = ['node', 'script.js', '--yolo', '-p', 'test'];
|
||||
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
|
||||
expect(argv.yolo).toBe(true);
|
||||
expect(argv.approvalMode).toBeUndefined(); // Should NOT be set when using --yolo
|
||||
@@ -362,7 +362,7 @@ describe('Configuration Integration Tests', () => {
|
||||
process.argv = ['node', 'script.js', '--approval-mode', 'invalid_mode'];
|
||||
|
||||
// Should throw during argument parsing due to yargs validation
|
||||
await expect(parseArguments()).rejects.toThrow();
|
||||
await expect(parseArguments({} as Settings)).rejects.toThrow();
|
||||
} finally {
|
||||
process.argv = originalArgv;
|
||||
}
|
||||
@@ -381,7 +381,7 @@ describe('Configuration Integration Tests', () => {
|
||||
];
|
||||
|
||||
// Should throw during argument parsing due to conflict validation
|
||||
await expect(parseArguments()).rejects.toThrow();
|
||||
await expect(parseArguments({} as Settings)).rejects.toThrow();
|
||||
} finally {
|
||||
process.argv = originalArgv;
|
||||
}
|
||||
@@ -394,7 +394,7 @@ describe('Configuration Integration Tests', () => {
|
||||
// Test that no approval mode arguments defaults to no flags set
|
||||
process.argv = ['node', 'script.js', '-p', 'test'];
|
||||
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
|
||||
expect(argv.approvalMode).toBeUndefined();
|
||||
expect(argv.yolo).toBe(false);
|
||||
|
||||
@@ -125,7 +125,9 @@ describe('parseArguments', () => {
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
await expect(parseArguments()).rejects.toThrow('process.exit called');
|
||||
await expect(parseArguments({} as Settings)).rejects.toThrow(
|
||||
'process.exit called',
|
||||
);
|
||||
|
||||
expect(mockConsoleError).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
@@ -155,7 +157,9 @@ describe('parseArguments', () => {
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
await expect(parseArguments()).rejects.toThrow('process.exit called');
|
||||
await expect(parseArguments({} as Settings)).rejects.toThrow(
|
||||
'process.exit called',
|
||||
);
|
||||
|
||||
expect(mockConsoleError).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
@@ -169,7 +173,7 @@ describe('parseArguments', () => {
|
||||
|
||||
it('should allow --prompt without --prompt-interactive', async () => {
|
||||
process.argv = ['node', 'script.js', '--prompt', 'test prompt'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
expect(argv.prompt).toBe('test prompt');
|
||||
expect(argv.promptInteractive).toBeUndefined();
|
||||
});
|
||||
@@ -181,14 +185,14 @@ describe('parseArguments', () => {
|
||||
'--prompt-interactive',
|
||||
'interactive prompt',
|
||||
];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
expect(argv.promptInteractive).toBe('interactive prompt');
|
||||
expect(argv.prompt).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should allow -i flag as alias for --prompt-interactive', async () => {
|
||||
process.argv = ['node', 'script.js', '-i', 'interactive prompt'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
expect(argv.promptInteractive).toBe('interactive prompt');
|
||||
expect(argv.prompt).toBeUndefined();
|
||||
});
|
||||
@@ -210,7 +214,9 @@ describe('parseArguments', () => {
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
await expect(parseArguments()).rejects.toThrow('process.exit called');
|
||||
await expect(parseArguments({} as Settings)).rejects.toThrow(
|
||||
'process.exit called',
|
||||
);
|
||||
|
||||
expect(mockConsoleError).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
@@ -233,7 +239,9 @@ describe('parseArguments', () => {
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
await expect(parseArguments()).rejects.toThrow('process.exit called');
|
||||
await expect(parseArguments({} as Settings)).rejects.toThrow(
|
||||
'process.exit called',
|
||||
);
|
||||
|
||||
expect(mockConsoleError).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
@@ -247,14 +255,14 @@ describe('parseArguments', () => {
|
||||
|
||||
it('should allow --approval-mode without --yolo', async () => {
|
||||
process.argv = ['node', 'script.js', '--approval-mode', 'auto_edit'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
expect(argv.approvalMode).toBe('auto_edit');
|
||||
expect(argv.yolo).toBe(false);
|
||||
});
|
||||
|
||||
it('should allow --yolo without --approval-mode', async () => {
|
||||
process.argv = ['node', 'script.js', '--yolo'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
expect(argv.yolo).toBe(true);
|
||||
expect(argv.approvalMode).toBeUndefined();
|
||||
});
|
||||
@@ -270,7 +278,9 @@ describe('parseArguments', () => {
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
await expect(parseArguments()).rejects.toThrow('process.exit called');
|
||||
await expect(parseArguments({} as Settings)).rejects.toThrow(
|
||||
'process.exit called',
|
||||
);
|
||||
|
||||
expect(mockConsoleError).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Invalid values:'),
|
||||
@@ -298,7 +308,7 @@ describe('loadCliConfig', () => {
|
||||
|
||||
it('should set showMemoryUsage to true when --show-memory-usage flag is present', async () => {
|
||||
process.argv = ['node', 'script.js', '--show-memory-usage'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {};
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getShowMemoryUsage()).toBe(true);
|
||||
@@ -306,7 +316,7 @@ describe('loadCliConfig', () => {
|
||||
|
||||
it('should set showMemoryUsage to false when --memory flag is not present', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {};
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getShowMemoryUsage()).toBe(false);
|
||||
@@ -314,7 +324,7 @@ describe('loadCliConfig', () => {
|
||||
|
||||
it('should set showMemoryUsage to false by default from settings if CLI flag is not present', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = { showMemoryUsage: false };
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getShowMemoryUsage()).toBe(false);
|
||||
@@ -322,7 +332,7 @@ describe('loadCliConfig', () => {
|
||||
|
||||
it('should prioritize CLI flag over settings for showMemoryUsage (CLI true, settings false)', async () => {
|
||||
process.argv = ['node', 'script.js', '--show-memory-usage'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = { showMemoryUsage: false };
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getShowMemoryUsage()).toBe(true);
|
||||
@@ -356,7 +366,7 @@ describe('loadCliConfig', () => {
|
||||
|
||||
it(`should leave proxy to empty by default`, async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {};
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getProxy()).toBeFalsy();
|
||||
@@ -397,7 +407,7 @@ describe('loadCliConfig', () => {
|
||||
it(`should set proxy to ${expected} according to environment variable [${input.env_name}]`, async () => {
|
||||
vi.stubEnv(input.env_name, input.proxy_url);
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {};
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getProxy()).toBe(expected);
|
||||
@@ -406,7 +416,7 @@ describe('loadCliConfig', () => {
|
||||
|
||||
it('should set proxy when --proxy flag is present', async () => {
|
||||
process.argv = ['node', 'script.js', '--proxy', 'http://localhost:7890'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {};
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getProxy()).toBe('http://localhost:7890');
|
||||
@@ -415,7 +425,7 @@ describe('loadCliConfig', () => {
|
||||
it('should prioritize CLI flag over environment variable for proxy (CLI http://localhost:7890, environment variable http://localhost:7891)', async () => {
|
||||
vi.stubEnv('http_proxy', 'http://localhost:7891');
|
||||
process.argv = ['node', 'script.js', '--proxy', 'http://localhost:7890'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {};
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getProxy()).toBe('http://localhost:7890');
|
||||
@@ -440,7 +450,7 @@ describe('loadCliConfig telemetry', () => {
|
||||
|
||||
it('should set telemetry to false by default when no flag or setting is present', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {};
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getTelemetryEnabled()).toBe(false);
|
||||
@@ -448,7 +458,7 @@ describe('loadCliConfig telemetry', () => {
|
||||
|
||||
it('should set telemetry to true when --telemetry flag is present', async () => {
|
||||
process.argv = ['node', 'script.js', '--telemetry'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {};
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getTelemetryEnabled()).toBe(true);
|
||||
@@ -456,7 +466,7 @@ describe('loadCliConfig telemetry', () => {
|
||||
|
||||
it('should set telemetry to false when --no-telemetry flag is present', async () => {
|
||||
process.argv = ['node', 'script.js', '--no-telemetry'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {};
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getTelemetryEnabled()).toBe(false);
|
||||
@@ -464,7 +474,7 @@ describe('loadCliConfig telemetry', () => {
|
||||
|
||||
it('should use telemetry value from settings if CLI flag is not present (settings true)', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = { telemetry: { enabled: true } };
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getTelemetryEnabled()).toBe(true);
|
||||
@@ -472,7 +482,7 @@ describe('loadCliConfig telemetry', () => {
|
||||
|
||||
it('should use telemetry value from settings if CLI flag is not present (settings false)', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = { telemetry: { enabled: false } };
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getTelemetryEnabled()).toBe(false);
|
||||
@@ -480,7 +490,7 @@ describe('loadCliConfig telemetry', () => {
|
||||
|
||||
it('should prioritize --telemetry CLI flag (true) over settings (false)', async () => {
|
||||
process.argv = ['node', 'script.js', '--telemetry'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = { telemetry: { enabled: false } };
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getTelemetryEnabled()).toBe(true);
|
||||
@@ -488,7 +498,7 @@ describe('loadCliConfig telemetry', () => {
|
||||
|
||||
it('should prioritize --no-telemetry CLI flag (false) over settings (true)', async () => {
|
||||
process.argv = ['node', 'script.js', '--no-telemetry'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = { telemetry: { enabled: true } };
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getTelemetryEnabled()).toBe(false);
|
||||
@@ -496,7 +506,7 @@ describe('loadCliConfig telemetry', () => {
|
||||
|
||||
it('should use telemetry OTLP endpoint from settings if CLI flag is not present', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {
|
||||
telemetry: { otlpEndpoint: 'http://settings.example.com' },
|
||||
};
|
||||
@@ -513,7 +523,7 @@ describe('loadCliConfig telemetry', () => {
|
||||
'--telemetry-otlp-endpoint',
|
||||
'http://cli.example.com',
|
||||
];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {
|
||||
telemetry: { otlpEndpoint: 'http://settings.example.com' },
|
||||
};
|
||||
@@ -523,7 +533,7 @@ describe('loadCliConfig telemetry', () => {
|
||||
|
||||
it('should use default endpoint if no OTLP endpoint is provided via CLI or settings', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = { telemetry: { enabled: true } };
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getTelemetryOtlpEndpoint()).toBe('http://localhost:4317');
|
||||
@@ -531,7 +541,7 @@ describe('loadCliConfig telemetry', () => {
|
||||
|
||||
it('should use telemetry target from settings if CLI flag is not present', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {
|
||||
telemetry: { target: ServerConfig.DEFAULT_TELEMETRY_TARGET },
|
||||
};
|
||||
@@ -543,7 +553,7 @@ describe('loadCliConfig telemetry', () => {
|
||||
|
||||
it('should prioritize --telemetry-target CLI flag over settings', async () => {
|
||||
process.argv = ['node', 'script.js', '--telemetry-target', 'gcp'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {
|
||||
telemetry: { target: ServerConfig.DEFAULT_TELEMETRY_TARGET },
|
||||
};
|
||||
@@ -553,7 +563,7 @@ describe('loadCliConfig telemetry', () => {
|
||||
|
||||
it('should use default target if no target is provided via CLI or settings', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = { telemetry: { enabled: true } };
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getTelemetryTarget()).toBe(
|
||||
@@ -563,7 +573,7 @@ describe('loadCliConfig telemetry', () => {
|
||||
|
||||
it('should use telemetry log prompts from settings if CLI flag is not present', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = { telemetry: { logPrompts: false } };
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getTelemetryLogPromptsEnabled()).toBe(false);
|
||||
@@ -571,7 +581,7 @@ describe('loadCliConfig telemetry', () => {
|
||||
|
||||
it('should prioritize --telemetry-log-prompts CLI flag (true) over settings (false)', async () => {
|
||||
process.argv = ['node', 'script.js', '--telemetry-log-prompts'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = { telemetry: { logPrompts: false } };
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getTelemetryLogPromptsEnabled()).toBe(true);
|
||||
@@ -579,7 +589,7 @@ describe('loadCliConfig telemetry', () => {
|
||||
|
||||
it('should prioritize --no-telemetry-log-prompts CLI flag (false) over settings (true)', async () => {
|
||||
process.argv = ['node', 'script.js', '--no-telemetry-log-prompts'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = { telemetry: { logPrompts: true } };
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getTelemetryLogPromptsEnabled()).toBe(false);
|
||||
@@ -587,7 +597,7 @@ describe('loadCliConfig telemetry', () => {
|
||||
|
||||
it('should use default log prompts (true) if no value is provided via CLI or settings', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = { telemetry: { enabled: true } };
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getTelemetryLogPromptsEnabled()).toBe(true);
|
||||
@@ -595,7 +605,7 @@ describe('loadCliConfig telemetry', () => {
|
||||
|
||||
it('should use telemetry OTLP protocol from settings if CLI flag is not present', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {
|
||||
telemetry: { otlpProtocol: 'http' },
|
||||
};
|
||||
@@ -605,7 +615,7 @@ describe('loadCliConfig telemetry', () => {
|
||||
|
||||
it('should prioritize --telemetry-otlp-protocol CLI flag over settings', async () => {
|
||||
process.argv = ['node', 'script.js', '--telemetry-otlp-protocol', 'http'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {
|
||||
telemetry: { otlpProtocol: 'grpc' },
|
||||
};
|
||||
@@ -615,7 +625,7 @@ describe('loadCliConfig telemetry', () => {
|
||||
|
||||
it('should use default protocol if no OTLP protocol is provided via CLI or settings', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = { telemetry: { enabled: true } };
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getTelemetryOtlpProtocol()).toBe('grpc');
|
||||
@@ -637,7 +647,9 @@ describe('loadCliConfig telemetry', () => {
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
await expect(parseArguments()).rejects.toThrow('process.exit called');
|
||||
await expect(parseArguments({} as Settings)).rejects.toThrow(
|
||||
'process.exit called',
|
||||
);
|
||||
|
||||
expect(mockConsoleError).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Invalid values:'),
|
||||
@@ -691,7 +703,7 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
|
||||
],
|
||||
},
|
||||
];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
await loadCliConfig(settings, extensions, 'session-id', argv);
|
||||
expect(ServerConfig.loadServerHierarchicalMemory).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
@@ -765,7 +777,7 @@ describe('mergeMcpServers', () => {
|
||||
];
|
||||
const originalSettings = JSON.parse(JSON.stringify(settings));
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
await loadCliConfig(settings, extensions, 'test-session', argv);
|
||||
expect(settings).toEqual(originalSettings);
|
||||
});
|
||||
@@ -806,7 +818,7 @@ describe('mergeExcludeTools', () => {
|
||||
},
|
||||
];
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const config = await loadCliConfig(
|
||||
settings,
|
||||
extensions,
|
||||
@@ -833,7 +845,7 @@ describe('mergeExcludeTools', () => {
|
||||
},
|
||||
];
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const config = await loadCliConfig(
|
||||
settings,
|
||||
extensions,
|
||||
@@ -869,7 +881,7 @@ describe('mergeExcludeTools', () => {
|
||||
},
|
||||
];
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const config = await loadCliConfig(
|
||||
settings,
|
||||
extensions,
|
||||
@@ -887,7 +899,7 @@ describe('mergeExcludeTools', () => {
|
||||
const settings: Settings = {};
|
||||
const extensions: Extension[] = [];
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const config = await loadCliConfig(
|
||||
settings,
|
||||
extensions,
|
||||
@@ -902,7 +914,7 @@ describe('mergeExcludeTools', () => {
|
||||
const settings: Settings = {};
|
||||
const extensions: Extension[] = [];
|
||||
process.argv = ['node', 'script.js', '-p', 'test'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const config = await loadCliConfig(
|
||||
settings,
|
||||
extensions,
|
||||
@@ -914,7 +926,7 @@ describe('mergeExcludeTools', () => {
|
||||
|
||||
it('should handle settings with excludeTools but no extensions', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = { excludeTools: ['tool1', 'tool2'] };
|
||||
const extensions: Extension[] = [];
|
||||
const config = await loadCliConfig(
|
||||
@@ -943,7 +955,7 @@ describe('mergeExcludeTools', () => {
|
||||
},
|
||||
];
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const config = await loadCliConfig(
|
||||
settings,
|
||||
extensions,
|
||||
@@ -971,7 +983,7 @@ describe('mergeExcludeTools', () => {
|
||||
];
|
||||
const originalSettings = JSON.parse(JSON.stringify(settings));
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
await loadCliConfig(settings, extensions, 'test-session', argv);
|
||||
expect(settings).toEqual(originalSettings);
|
||||
});
|
||||
@@ -990,7 +1002,7 @@ describe('Approval mode tool exclusion logic', () => {
|
||||
|
||||
it('should exclude all interactive tools in non-interactive mode with default approval mode', async () => {
|
||||
process.argv = ['node', 'script.js', '-p', 'test'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {};
|
||||
const extensions: Extension[] = [];
|
||||
|
||||
@@ -1016,7 +1028,7 @@ describe('Approval mode tool exclusion logic', () => {
|
||||
'-p',
|
||||
'test',
|
||||
];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {};
|
||||
const extensions: Extension[] = [];
|
||||
|
||||
@@ -1042,7 +1054,7 @@ describe('Approval mode tool exclusion logic', () => {
|
||||
'-p',
|
||||
'test',
|
||||
];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {};
|
||||
const extensions: Extension[] = [];
|
||||
|
||||
@@ -1068,7 +1080,7 @@ describe('Approval mode tool exclusion logic', () => {
|
||||
'-p',
|
||||
'test',
|
||||
];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {};
|
||||
const extensions: Extension[] = [];
|
||||
|
||||
@@ -1087,7 +1099,7 @@ describe('Approval mode tool exclusion logic', () => {
|
||||
|
||||
it('should exclude no interactive tools in non-interactive mode with legacy yolo flag', async () => {
|
||||
process.argv = ['node', 'script.js', '--yolo', '-p', 'test'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {};
|
||||
const extensions: Extension[] = [];
|
||||
|
||||
@@ -1117,7 +1129,7 @@ describe('Approval mode tool exclusion logic', () => {
|
||||
|
||||
for (const testCase of testCases) {
|
||||
process.argv = testCase.args;
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {};
|
||||
const extensions: Extension[] = [];
|
||||
|
||||
@@ -1144,7 +1156,7 @@ describe('Approval mode tool exclusion logic', () => {
|
||||
'-p',
|
||||
'test',
|
||||
];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = { excludeTools: ['custom_tool'] };
|
||||
const extensions: Extension[] = [];
|
||||
|
||||
@@ -1212,7 +1224,7 @@ describe('loadCliConfig with allowed-mcp-server-names', () => {
|
||||
|
||||
it('should allow all MCP servers if the flag is not provided', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const config = await loadCliConfig(baseSettings, [], 'test-session', argv);
|
||||
expect(config.getMcpServers()).toEqual(baseSettings.mcpServers);
|
||||
});
|
||||
@@ -1224,7 +1236,7 @@ describe('loadCliConfig with allowed-mcp-server-names', () => {
|
||||
'--allowed-mcp-server-names',
|
||||
'server1',
|
||||
];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const config = await loadCliConfig(baseSettings, [], 'test-session', argv);
|
||||
expect(config.getMcpServers()).toEqual({
|
||||
server1: { url: 'http://localhost:8080' },
|
||||
@@ -1240,7 +1252,7 @@ describe('loadCliConfig with allowed-mcp-server-names', () => {
|
||||
'--allowed-mcp-server-names',
|
||||
'server3',
|
||||
];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const config = await loadCliConfig(baseSettings, [], 'test-session', argv);
|
||||
expect(config.getMcpServers()).toEqual({
|
||||
server1: { url: 'http://localhost:8080' },
|
||||
@@ -1257,7 +1269,7 @@ describe('loadCliConfig with allowed-mcp-server-names', () => {
|
||||
'--allowed-mcp-server-names',
|
||||
'server4',
|
||||
];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const config = await loadCliConfig(baseSettings, [], 'test-session', argv);
|
||||
expect(config.getMcpServers()).toEqual({
|
||||
server1: { url: 'http://localhost:8080' },
|
||||
@@ -1266,14 +1278,14 @@ describe('loadCliConfig with allowed-mcp-server-names', () => {
|
||||
|
||||
it('should allow no MCP servers if the flag is provided but empty', async () => {
|
||||
process.argv = ['node', 'script.js', '--allowed-mcp-server-names', ''];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const config = await loadCliConfig(baseSettings, [], 'test-session', argv);
|
||||
expect(config.getMcpServers()).toEqual({});
|
||||
});
|
||||
|
||||
it('should read allowMCPServers from settings', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {
|
||||
...baseSettings,
|
||||
allowMCPServers: ['server1', 'server2'],
|
||||
@@ -1287,7 +1299,7 @@ describe('loadCliConfig with allowed-mcp-server-names', () => {
|
||||
|
||||
it('should read excludeMCPServers from settings', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {
|
||||
...baseSettings,
|
||||
excludeMCPServers: ['server1', 'server2'],
|
||||
@@ -1300,7 +1312,7 @@ describe('loadCliConfig with allowed-mcp-server-names', () => {
|
||||
|
||||
it('should override allowMCPServers with excludeMCPServers if overlapping', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {
|
||||
...baseSettings,
|
||||
excludeMCPServers: ['server1'],
|
||||
@@ -1319,7 +1331,7 @@ describe('loadCliConfig with allowed-mcp-server-names', () => {
|
||||
'--allowed-mcp-server-names',
|
||||
'server1',
|
||||
];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {
|
||||
...baseSettings,
|
||||
excludeMCPServers: ['server1'],
|
||||
@@ -1348,7 +1360,7 @@ describe('loadCliConfig extensions', () => {
|
||||
|
||||
it('should not filter extensions if --extensions flag is not used', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {};
|
||||
const config = await loadCliConfig(
|
||||
settings,
|
||||
@@ -1364,7 +1376,7 @@ describe('loadCliConfig extensions', () => {
|
||||
|
||||
it('should filter extensions if --extensions flag is used', async () => {
|
||||
process.argv = ['node', 'script.js', '--extensions', 'ext1'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {};
|
||||
const config = await loadCliConfig(
|
||||
settings,
|
||||
@@ -1379,7 +1391,7 @@ describe('loadCliConfig extensions', () => {
|
||||
describe('loadCliConfig model selection', () => {
|
||||
it('selects a model from settings.json if provided', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const config = await loadCliConfig(
|
||||
{
|
||||
model: 'gemini-9001-ultra',
|
||||
@@ -1394,7 +1406,7 @@ describe('loadCliConfig model selection', () => {
|
||||
|
||||
it('uses the default gemini model if nothing is set', async () => {
|
||||
process.argv = ['node', 'script.js']; // No model set.
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const config = await loadCliConfig(
|
||||
{
|
||||
// No model set.
|
||||
@@ -1409,7 +1421,7 @@ describe('loadCliConfig model selection', () => {
|
||||
|
||||
it('always prefers model from argvs', async () => {
|
||||
process.argv = ['node', 'script.js', '--model', 'gemini-8675309-ultra'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const config = await loadCliConfig(
|
||||
{
|
||||
model: 'gemini-9001-ultra',
|
||||
@@ -1424,7 +1436,7 @@ describe('loadCliConfig model selection', () => {
|
||||
|
||||
it('selects the model from argvs if provided', async () => {
|
||||
process.argv = ['node', 'script.js', '--model', 'gemini-8675309-ultra'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const config = await loadCliConfig(
|
||||
{
|
||||
// No model provided via settings.
|
||||
@@ -1456,14 +1468,14 @@ describe('loadCliConfig folderTrustFeature', () => {
|
||||
it('should be false by default', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const settings: Settings = {};
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getFolderTrustFeature()).toBe(false);
|
||||
});
|
||||
|
||||
it('should be true when settings.folderTrustFeature is true', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = { folderTrustFeature: true };
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getFolderTrustFeature()).toBe(true);
|
||||
@@ -1491,14 +1503,14 @@ describe('loadCliConfig folderTrust', () => {
|
||||
folderTrustFeature: false,
|
||||
folderTrust: false,
|
||||
};
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getFolderTrust()).toBe(false);
|
||||
});
|
||||
|
||||
it('should be false if folderTrustFeature is true and folderTrust is false', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = { folderTrustFeature: true, folderTrust: false };
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getFolderTrust()).toBe(false);
|
||||
@@ -1506,7 +1518,7 @@ describe('loadCliConfig folderTrust', () => {
|
||||
|
||||
it('should be false if folderTrustFeature is false and folderTrust is true', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = { folderTrustFeature: false, folderTrust: true };
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getFolderTrust()).toBe(false);
|
||||
@@ -1514,7 +1526,7 @@ describe('loadCliConfig folderTrust', () => {
|
||||
|
||||
it('should be true when folderTrustFeature is true and folderTrust is true', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = { folderTrustFeature: true, folderTrust: true };
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getFolderTrust()).toBe(true);
|
||||
@@ -1547,7 +1559,7 @@ describe('loadCliConfig with includeDirectories', () => {
|
||||
'--include-directories',
|
||||
`${path.resolve(path.sep, 'cli', 'path1')},${path.join(mockCwd, 'cli', 'path2')}`,
|
||||
];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {
|
||||
includeDirectories: [
|
||||
path.resolve(path.sep, 'settings', 'path1'),
|
||||
@@ -1590,7 +1602,7 @@ describe('loadCliConfig chatCompression', () => {
|
||||
|
||||
it('should pass chatCompression settings to the core config', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {
|
||||
chatCompression: {
|
||||
contextPercentageThreshold: 0.5,
|
||||
@@ -1604,7 +1616,7 @@ describe('loadCliConfig chatCompression', () => {
|
||||
|
||||
it('should have undefined chatCompression if not in settings', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {};
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getChatCompression()).toBeUndefined();
|
||||
@@ -1628,7 +1640,7 @@ describe('loadCliConfig useRipgrep', () => {
|
||||
|
||||
it('should be false by default when useRipgrep is not set in settings', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {};
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getUseRipgrep()).toBe(false);
|
||||
@@ -1636,7 +1648,7 @@ describe('loadCliConfig useRipgrep', () => {
|
||||
|
||||
it('should be true when useRipgrep is set to true in settings', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = { useRipgrep: true };
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getUseRipgrep()).toBe(true);
|
||||
@@ -1644,7 +1656,7 @@ describe('loadCliConfig useRipgrep', () => {
|
||||
|
||||
it('should be false when useRipgrep is explicitly set to false in settings', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = { useRipgrep: false };
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getUseRipgrep()).toBe(false);
|
||||
@@ -1672,7 +1684,7 @@ describe('loadCliConfig tool exclusions', () => {
|
||||
it('should not exclude interactive tools in interactive mode without YOLO', async () => {
|
||||
process.stdin.isTTY = true;
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const config = await loadCliConfig({}, [], 'test-session', argv);
|
||||
expect(config.getExcludeTools()).not.toContain('run_shell_command');
|
||||
expect(config.getExcludeTools()).not.toContain('replace');
|
||||
@@ -1682,7 +1694,7 @@ describe('loadCliConfig tool exclusions', () => {
|
||||
it('should not exclude interactive tools in interactive mode with YOLO', async () => {
|
||||
process.stdin.isTTY = true;
|
||||
process.argv = ['node', 'script.js', '--yolo'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const config = await loadCliConfig({}, [], 'test-session', argv);
|
||||
expect(config.getExcludeTools()).not.toContain('run_shell_command');
|
||||
expect(config.getExcludeTools()).not.toContain('replace');
|
||||
@@ -1692,7 +1704,7 @@ describe('loadCliConfig tool exclusions', () => {
|
||||
it('should exclude interactive tools in non-interactive mode without YOLO', async () => {
|
||||
process.stdin.isTTY = false;
|
||||
process.argv = ['node', 'script.js', '-p', 'test'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const config = await loadCliConfig({}, [], 'test-session', argv);
|
||||
expect(config.getExcludeTools()).toContain('run_shell_command');
|
||||
expect(config.getExcludeTools()).toContain('replace');
|
||||
@@ -1702,7 +1714,7 @@ describe('loadCliConfig tool exclusions', () => {
|
||||
it('should not exclude interactive tools in non-interactive mode with YOLO', async () => {
|
||||
process.stdin.isTTY = false;
|
||||
process.argv = ['node', 'script.js', '-p', 'test', '--yolo'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const config = await loadCliConfig({}, [], 'test-session', argv);
|
||||
expect(config.getExcludeTools()).not.toContain('run_shell_command');
|
||||
expect(config.getExcludeTools()).not.toContain('replace');
|
||||
@@ -1731,7 +1743,7 @@ describe('loadCliConfig interactive', () => {
|
||||
it('should be interactive if isTTY and no prompt', async () => {
|
||||
process.stdin.isTTY = true;
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const config = await loadCliConfig({}, [], 'test-session', argv);
|
||||
expect(config.isInteractive()).toBe(true);
|
||||
});
|
||||
@@ -1739,7 +1751,7 @@ describe('loadCliConfig interactive', () => {
|
||||
it('should be interactive if prompt-interactive is set', async () => {
|
||||
process.stdin.isTTY = false;
|
||||
process.argv = ['node', 'script.js', '--prompt-interactive', 'test'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const config = await loadCliConfig({}, [], 'test-session', argv);
|
||||
expect(config.isInteractive()).toBe(true);
|
||||
});
|
||||
@@ -1747,7 +1759,7 @@ describe('loadCliConfig interactive', () => {
|
||||
it('should not be interactive if not isTTY and no prompt', async () => {
|
||||
process.stdin.isTTY = false;
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const config = await loadCliConfig({}, [], 'test-session', argv);
|
||||
expect(config.isInteractive()).toBe(false);
|
||||
});
|
||||
@@ -1755,7 +1767,7 @@ describe('loadCliConfig interactive', () => {
|
||||
it('should not be interactive if prompt is set', async () => {
|
||||
process.stdin.isTTY = true;
|
||||
process.argv = ['node', 'script.js', '--prompt', 'test'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const config = await loadCliConfig({}, [], 'test-session', argv);
|
||||
expect(config.isInteractive()).toBe(false);
|
||||
});
|
||||
@@ -1779,42 +1791,42 @@ describe('loadCliConfig approval mode', () => {
|
||||
|
||||
it('should default to DEFAULT approval mode when no flags are set', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const config = await loadCliConfig({}, [], 'test-session', argv);
|
||||
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
|
||||
});
|
||||
|
||||
it('should set YOLO approval mode when --yolo flag is used', async () => {
|
||||
process.argv = ['node', 'script.js', '--yolo'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const config = await loadCliConfig({}, [], 'test-session', argv);
|
||||
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
|
||||
});
|
||||
|
||||
it('should set YOLO approval mode when -y flag is used', async () => {
|
||||
process.argv = ['node', 'script.js', '-y'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const config = await loadCliConfig({}, [], 'test-session', argv);
|
||||
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
|
||||
});
|
||||
|
||||
it('should set DEFAULT approval mode when --approval-mode=default', async () => {
|
||||
process.argv = ['node', 'script.js', '--approval-mode', 'default'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const config = await loadCliConfig({}, [], 'test-session', argv);
|
||||
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
|
||||
});
|
||||
|
||||
it('should set AUTO_EDIT approval mode when --approval-mode=auto_edit', async () => {
|
||||
process.argv = ['node', 'script.js', '--approval-mode', 'auto_edit'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const config = await loadCliConfig({}, [], 'test-session', argv);
|
||||
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.AUTO_EDIT);
|
||||
});
|
||||
|
||||
it('should set YOLO approval mode when --approval-mode=yolo', async () => {
|
||||
process.argv = ['node', 'script.js', '--approval-mode', 'yolo'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const config = await loadCliConfig({}, [], 'test-session', argv);
|
||||
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
|
||||
});
|
||||
@@ -1823,7 +1835,7 @@ describe('loadCliConfig approval mode', () => {
|
||||
// Note: This test documents the intended behavior, but in practice the validation
|
||||
// prevents both flags from being used together
|
||||
process.argv = ['node', 'script.js', '--approval-mode', 'default'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
// Manually set yolo to true to simulate what would happen if validation didn't prevent it
|
||||
argv.yolo = true;
|
||||
const config = await loadCliConfig({}, [], 'test-session', argv);
|
||||
@@ -1832,7 +1844,7 @@ describe('loadCliConfig approval mode', () => {
|
||||
|
||||
it('should fall back to --yolo behavior when --approval-mode is not set', async () => {
|
||||
process.argv = ['node', 'script.js', '--yolo'];
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const config = await loadCliConfig({}, [], 'test-session', argv);
|
||||
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
|
||||
});
|
||||
@@ -1949,7 +1961,7 @@ describe('loadCliConfig trustedFolder', () => {
|
||||
(settings.folderTrust ?? true);
|
||||
return featureIsEnabled ? mockTrustValue : true;
|
||||
});
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = { folderTrustFeature, folderTrust };
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import yargs from 'yargs/yargs';
|
||||
import { hideBin } from 'yargs/helpers';
|
||||
import process from 'node:process';
|
||||
import { mcpCommand } from '../commands/mcp.js';
|
||||
import { extensionsCommand } from '../commands/extensions.js';
|
||||
import {
|
||||
Config,
|
||||
loadServerHierarchicalMemory,
|
||||
@@ -76,7 +77,7 @@ export interface CliArgs {
|
||||
screenReader: boolean | undefined;
|
||||
}
|
||||
|
||||
export async function parseArguments(): Promise<CliArgs> {
|
||||
export async function parseArguments(settings: Settings): Promise<CliArgs> {
|
||||
const yargsInstance = yargs(hideBin(process.argv))
|
||||
.locale('en')
|
||||
.scriptName('gemini')
|
||||
@@ -252,7 +253,13 @@ export async function parseArguments(): Promise<CliArgs> {
|
||||
}),
|
||||
)
|
||||
// Register MCP subcommands
|
||||
.command(mcpCommand)
|
||||
.command(mcpCommand);
|
||||
|
||||
if (settings?.extensionManagement ?? false) {
|
||||
yargsInstance.command(extensionsCommand);
|
||||
}
|
||||
|
||||
yargsInstance
|
||||
.version(await getCliVersion()) // This will enable the --version flag based on package.json
|
||||
.alias('v', 'version')
|
||||
.help()
|
||||
@@ -265,7 +272,10 @@ export async function parseArguments(): Promise<CliArgs> {
|
||||
|
||||
// Handle case where MCP subcommands are executed - they should exit the process
|
||||
// and not return to main CLI logic
|
||||
if (result._.length > 0 && result._[0] === 'mcp') {
|
||||
if (
|
||||
result._.length > 0 &&
|
||||
(result._[0] === 'mcp' || result._[0] === 'extensions')
|
||||
) {
|
||||
// MCP commands handle their own execution and process exit
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
@@ -10,9 +10,17 @@ import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import {
|
||||
EXTENSIONS_CONFIG_FILENAME,
|
||||
INSTALL_METADATA_FILENAME,
|
||||
annotateActiveExtensions,
|
||||
installExtension,
|
||||
loadExtensions,
|
||||
} from './extension.js';
|
||||
import { execSync } from 'child_process';
|
||||
import { SimpleGit, simpleGit } from 'simple-git';
|
||||
|
||||
vi.mock('simple-git', () => ({
|
||||
simpleGit: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('os', async (importOriginal) => {
|
||||
const os = await importOriginal<typeof import('os')>();
|
||||
@@ -22,6 +30,13 @@ vi.mock('os', async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('child_process', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('child_process')>();
|
||||
return {
|
||||
...actual,
|
||||
execSync: vi.fn(),
|
||||
};
|
||||
});
|
||||
const EXTENSIONS_DIRECTORY_NAME = path.join('.gemini', 'extensions');
|
||||
|
||||
describe('loadExtensions', () => {
|
||||
@@ -163,15 +178,117 @@ describe('annotateActiveExtensions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('installExtension', () => {
|
||||
let tempHomeDir: string;
|
||||
let userExtensionsDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempHomeDir = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'gemini-cli-test-home-'),
|
||||
);
|
||||
vi.mocked(os.homedir).mockReturnValue(tempHomeDir);
|
||||
userExtensionsDir = path.join(tempHomeDir, '.gemini', 'extensions');
|
||||
// Clean up before each test
|
||||
fs.rmSync(userExtensionsDir, { recursive: true, force: true });
|
||||
fs.mkdirSync(userExtensionsDir, { recursive: true });
|
||||
|
||||
vi.mocked(execSync).mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tempHomeDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('should install an extension from a local path', async () => {
|
||||
const sourceExtDir = createExtension(
|
||||
tempHomeDir,
|
||||
'my-local-extension',
|
||||
'1.0.0',
|
||||
);
|
||||
const targetExtDir = path.join(userExtensionsDir, 'my-local-extension');
|
||||
const metadataPath = path.join(targetExtDir, INSTALL_METADATA_FILENAME);
|
||||
|
||||
await installExtension({ source: sourceExtDir, type: 'local' });
|
||||
|
||||
expect(fs.existsSync(targetExtDir)).toBe(true);
|
||||
expect(fs.existsSync(metadataPath)).toBe(true);
|
||||
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
|
||||
expect(metadata).toEqual({
|
||||
source: sourceExtDir,
|
||||
type: 'local',
|
||||
});
|
||||
fs.rmSync(targetExtDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('should throw an error if the extension already exists', async () => {
|
||||
const sourceExtDir = createExtension(
|
||||
tempHomeDir,
|
||||
'my-local-extension',
|
||||
'1.0.0',
|
||||
);
|
||||
await installExtension({ source: sourceExtDir, type: 'local' });
|
||||
await expect(
|
||||
installExtension({ source: sourceExtDir, type: 'local' }),
|
||||
).rejects.toThrow(
|
||||
'Error: Extension "my-local-extension" is already installed. Please uninstall it first.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error and cleanup if gemini-extension.json is missing', async () => {
|
||||
const sourceExtDir = path.join(tempHomeDir, 'bad-extension');
|
||||
fs.mkdirSync(sourceExtDir, { recursive: true });
|
||||
|
||||
await expect(
|
||||
installExtension({ source: sourceExtDir, type: 'local' }),
|
||||
).rejects.toThrow(
|
||||
`Invalid extension at ${sourceExtDir}. Please make sure it has a valid gemini-extension.json file.`,
|
||||
);
|
||||
|
||||
const targetExtDir = path.join(userExtensionsDir, 'bad-extension');
|
||||
expect(fs.existsSync(targetExtDir)).toBe(false);
|
||||
});
|
||||
|
||||
it('should install an extension from a git URL', async () => {
|
||||
const gitUrl = 'https://github.com/google/gemini-extensions.git';
|
||||
const extensionName = 'gemini-extensions';
|
||||
const targetExtDir = path.join(userExtensionsDir, extensionName);
|
||||
const metadataPath = path.join(targetExtDir, INSTALL_METADATA_FILENAME);
|
||||
|
||||
const clone = vi.fn().mockImplementation(async (_, destination) => {
|
||||
fs.mkdirSync(destination, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(destination, EXTENSIONS_CONFIG_FILENAME),
|
||||
JSON.stringify({ name: extensionName, version: '1.0.0' }),
|
||||
);
|
||||
});
|
||||
|
||||
const mockedSimpleGit = simpleGit as vi.MockedFunction<typeof simpleGit>;
|
||||
mockedSimpleGit.mockReturnValue({
|
||||
clone,
|
||||
} as unknown as SimpleGit);
|
||||
|
||||
await installExtension({ source: gitUrl, type: 'git' });
|
||||
|
||||
expect(fs.existsSync(targetExtDir)).toBe(true);
|
||||
expect(fs.existsSync(metadataPath)).toBe(true);
|
||||
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
|
||||
expect(metadata).toEqual({
|
||||
source: gitUrl,
|
||||
type: 'git',
|
||||
});
|
||||
fs.rmSync(targetExtDir, { recursive: true, force: true });
|
||||
});
|
||||
});
|
||||
|
||||
function createExtension(
|
||||
extensionsDir: string,
|
||||
name: string,
|
||||
version: string,
|
||||
addContextFile = false,
|
||||
contextFileName?: string,
|
||||
): void {
|
||||
): string {
|
||||
const extDir = path.join(extensionsDir, name);
|
||||
fs.mkdirSync(extDir);
|
||||
fs.mkdirSync(extDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(extDir, EXTENSIONS_CONFIG_FILENAME),
|
||||
JSON.stringify({ name, version, contextFileName }),
|
||||
@@ -184,4 +301,5 @@ function createExtension(
|
||||
if (contextFileName) {
|
||||
fs.writeFileSync(path.join(extDir, contextFileName), 'context');
|
||||
}
|
||||
return extDir;
|
||||
}
|
||||
|
||||
@@ -12,13 +12,18 @@ import {
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { simpleGit } from 'simple-git';
|
||||
|
||||
export const EXTENSIONS_DIRECTORY_NAME = '.gemini/extensions';
|
||||
|
||||
export const EXTENSIONS_CONFIG_FILENAME = 'gemini-extension.json';
|
||||
export const INSTALL_METADATA_FILENAME = '.gemini-extension-install.json';
|
||||
|
||||
export interface Extension {
|
||||
path: string;
|
||||
config: ExtensionConfig;
|
||||
contextFiles: string[];
|
||||
installMetadata?: ExtensionInstallMetadata | undefined;
|
||||
}
|
||||
|
||||
export interface ExtensionConfig {
|
||||
@@ -29,6 +34,45 @@ export interface ExtensionConfig {
|
||||
excludeTools?: string[];
|
||||
}
|
||||
|
||||
export interface ExtensionInstallMetadata {
|
||||
source: string;
|
||||
type: 'git' | 'local';
|
||||
}
|
||||
|
||||
export class ExtensionStorage {
|
||||
private readonly extensionName: string;
|
||||
|
||||
constructor(extensionName: string) {
|
||||
this.extensionName = extensionName;
|
||||
}
|
||||
|
||||
getExtensionDir(): string {
|
||||
return path.join(
|
||||
ExtensionStorage.getUserExtensionsDir(),
|
||||
this.extensionName,
|
||||
);
|
||||
}
|
||||
|
||||
getConfigPath(): string {
|
||||
return path.join(this.getExtensionDir(), EXTENSIONS_CONFIG_FILENAME);
|
||||
}
|
||||
|
||||
static getSettingsPath(): string {
|
||||
return process.cwd();
|
||||
}
|
||||
|
||||
static getUserExtensionsDir(): string {
|
||||
const storage = new Storage(os.homedir());
|
||||
return storage.getExtensionsDir();
|
||||
}
|
||||
|
||||
static async createTmpDir(): Promise<string> {
|
||||
return await fs.promises.mkdtemp(
|
||||
path.join(os.tmpdir(), 'gemini-extension'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function loadExtensions(workspaceDir: string): Extension[] {
|
||||
const allExtensions = [
|
||||
...loadExtensionsFromDir(workspaceDir),
|
||||
@@ -45,7 +89,20 @@ export function loadExtensions(workspaceDir: string): Extension[] {
|
||||
return Array.from(uniqueExtensions.values());
|
||||
}
|
||||
|
||||
function loadExtensionsFromDir(dir: string): Extension[] {
|
||||
export function loadUserExtensions(): Extension[] {
|
||||
const userExtensions = loadExtensionsFromDir(os.homedir());
|
||||
|
||||
const uniqueExtensions = new Map<string, Extension>();
|
||||
for (const extension of userExtensions) {
|
||||
if (!uniqueExtensions.has(extension.config.name)) {
|
||||
uniqueExtensions.set(extension.config.name, extension);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(uniqueExtensions.values());
|
||||
}
|
||||
|
||||
export function loadExtensionsFromDir(dir: string): Extension[] {
|
||||
const storage = new Storage(dir);
|
||||
const extensionsDir = storage.getExtensionsDir();
|
||||
if (!fs.existsSync(extensionsDir)) {
|
||||
@@ -64,7 +121,7 @@ function loadExtensionsFromDir(dir: string): Extension[] {
|
||||
return extensions;
|
||||
}
|
||||
|
||||
function loadExtension(extensionDir: string): Extension | null {
|
||||
export function loadExtension(extensionDir: string): Extension | null {
|
||||
if (!fs.statSync(extensionDir).isDirectory()) {
|
||||
console.error(
|
||||
`Warning: unexpected file ${extensionDir} in extensions directory.`,
|
||||
@@ -98,6 +155,7 @@ function loadExtension(extensionDir: string): Extension | null {
|
||||
path: extensionDir,
|
||||
config,
|
||||
contextFiles,
|
||||
installMetadata: loadInstallMetadata(extensionDir),
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(
|
||||
@@ -107,6 +165,19 @@ function loadExtension(extensionDir: string): Extension | null {
|
||||
}
|
||||
}
|
||||
|
||||
function loadInstallMetadata(
|
||||
extensionDir: string,
|
||||
): ExtensionInstallMetadata | undefined {
|
||||
const metadataFilePath = path.join(extensionDir, INSTALL_METADATA_FILENAME);
|
||||
try {
|
||||
const configContent = fs.readFileSync(metadataFilePath, 'utf-8');
|
||||
const metadata = JSON.parse(configContent) as ExtensionInstallMetadata;
|
||||
return metadata;
|
||||
} catch (_e) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function getContextFileNames(config: ExtensionConfig): string[] {
|
||||
if (!config.contextFileName) {
|
||||
return ['GEMINI.md'];
|
||||
@@ -171,3 +242,99 @@ export function annotateActiveExtensions(
|
||||
|
||||
return annotatedExtensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clones a Git repository to a specified local path.
|
||||
* @param gitUrl The Git URL to clone.
|
||||
* @param destination The destination path to clone the repository to.
|
||||
*/
|
||||
async function cloneFromGit(
|
||||
gitUrl: string,
|
||||
destination: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
// TODO(chrstnb): Download the archive instead to avoid unnecessary .git info.
|
||||
await simpleGit().clone(gitUrl, destination, ['--depth', '1']);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to clone Git repository from ${gitUrl}`, {
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies an extension from a source to a destination path.
|
||||
* @param source The source path of the extension.
|
||||
* @param destination The destination path to copy the extension to.
|
||||
*/
|
||||
async function copyExtension(
|
||||
source: string,
|
||||
destination: string,
|
||||
): Promise<void> {
|
||||
await fs.promises.cp(source, destination, { recursive: true });
|
||||
}
|
||||
|
||||
export async function installExtension(
|
||||
installMetadata: ExtensionInstallMetadata,
|
||||
): Promise<string> {
|
||||
const extensionsDir = ExtensionStorage.getUserExtensionsDir();
|
||||
await fs.promises.mkdir(extensionsDir, { recursive: true });
|
||||
|
||||
// Convert relative paths to absolute paths for the metadata file.
|
||||
if (
|
||||
installMetadata.type === 'local' &&
|
||||
!path.isAbsolute(installMetadata.source)
|
||||
) {
|
||||
installMetadata.source = path.resolve(
|
||||
process.cwd(),
|
||||
installMetadata.source,
|
||||
);
|
||||
}
|
||||
|
||||
let localSourcePath: string;
|
||||
let tempDir: string | undefined;
|
||||
if (installMetadata.type === 'git') {
|
||||
tempDir = await ExtensionStorage.createTmpDir();
|
||||
await cloneFromGit(installMetadata.source, tempDir);
|
||||
localSourcePath = tempDir;
|
||||
} else {
|
||||
localSourcePath = installMetadata.source;
|
||||
}
|
||||
let newExtensionName: string | undefined;
|
||||
try {
|
||||
const newExtension = loadExtension(localSourcePath);
|
||||
if (!newExtension) {
|
||||
throw new Error(
|
||||
`Invalid extension at ${installMetadata.source}. Please make sure it has a valid gemini-extension.json file.`,
|
||||
);
|
||||
}
|
||||
|
||||
// ~/.gemini/extensions/{ExtensionConfig.name}.
|
||||
newExtensionName = newExtension.config.name;
|
||||
const extensionStorage = new ExtensionStorage(newExtensionName);
|
||||
const destinationPath = extensionStorage.getExtensionDir();
|
||||
|
||||
const installedExtensions = loadUserExtensions();
|
||||
if (
|
||||
installedExtensions.some(
|
||||
(installed) => installed.config.name === newExtensionName,
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
`Error: Extension "${newExtensionName}" is already installed. Please uninstall it first.`,
|
||||
);
|
||||
}
|
||||
|
||||
await copyExtension(localSourcePath, destinationPath);
|
||||
|
||||
const metadataString = JSON.stringify(installMetadata, null, 2);
|
||||
const metadataPath = path.join(destinationPath, INSTALL_METADATA_FILENAME);
|
||||
await fs.promises.writeFile(metadataPath, metadataString);
|
||||
} finally {
|
||||
if (tempDir) {
|
||||
await fs.promises.rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
return newExtensionName;
|
||||
}
|
||||
|
||||
@@ -535,6 +535,15 @@ export const SETTINGS_SCHEMA = {
|
||||
description: 'Show line numbers in the chat.',
|
||||
showInDialog: true,
|
||||
},
|
||||
extensionManagement: {
|
||||
type: 'boolean',
|
||||
label: 'Extension Management',
|
||||
category: 'Feature Flag',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description: 'Enable extension management features.',
|
||||
showInDialog: false,
|
||||
},
|
||||
skipNextSpeakerCheck: {
|
||||
type: 'boolean',
|
||||
label: 'Skip Next Speaker Check',
|
||||
|
||||
@@ -188,7 +188,7 @@ export async function main() {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const argv = await parseArguments();
|
||||
const argv = await parseArguments(settings.merged);
|
||||
const extensions = loadExtensions(workspaceRoot);
|
||||
const config = await loadCliConfig(
|
||||
settings.merged,
|
||||
|
||||
@@ -205,6 +205,7 @@ export interface ConfigParameters {
|
||||
useRipgrep?: boolean;
|
||||
shouldUseNodePtyShell?: boolean;
|
||||
skipNextSpeakerCheck?: boolean;
|
||||
extensionManagement?: boolean;
|
||||
enablePromptCompletion?: boolean;
|
||||
}
|
||||
|
||||
@@ -275,6 +276,7 @@ export class Config {
|
||||
private readonly useRipgrep: boolean;
|
||||
private readonly shouldUseNodePtyShell: boolean;
|
||||
private readonly skipNextSpeakerCheck: boolean;
|
||||
private readonly extensionManagement: boolean;
|
||||
private readonly enablePromptCompletion: boolean = false;
|
||||
private initialized: boolean = false;
|
||||
readonly storage: Storage;
|
||||
@@ -349,6 +351,7 @@ export class Config {
|
||||
this.useRipgrep = params.useRipgrep ?? false;
|
||||
this.shouldUseNodePtyShell = params.shouldUseNodePtyShell ?? false;
|
||||
this.skipNextSpeakerCheck = params.skipNextSpeakerCheck ?? false;
|
||||
this.extensionManagement = params.extensionManagement ?? false;
|
||||
this.storage = new Storage(this.targetDir);
|
||||
this.enablePromptCompletion = params.enablePromptCompletion ?? false;
|
||||
this.fileExclusions = new FileExclusions(this);
|
||||
@@ -678,6 +681,10 @@ export class Config {
|
||||
return this.listExtensions;
|
||||
}
|
||||
|
||||
getExtensionManagement(): boolean {
|
||||
return this.extensionManagement;
|
||||
}
|
||||
|
||||
getExtensions(): GeminiCLIExtension[] {
|
||||
return this._extensions;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user