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:
@@ -74,3 +74,16 @@ For example, if both a user and the `gcp` extension define a `deploy` command:
|
|||||||
|
|
||||||
- `/deploy` - Executes the user's deploy command
|
- `/deploy` - Executes the user's deploy command
|
||||||
- `/gcp.deploy` - Executes the extension's deploy command (marked with `[gcp]` tag)
|
- `/gcp.deploy` - Executes the extension's deploy command (marked with `[gcp]` tag)
|
||||||
|
|
||||||
|
## Installing Extensions
|
||||||
|
|
||||||
|
You can install extensions using the `install` command. This command allows you to install extensions from a Git repository or a local path.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
`gemini extensions install <source> | [options]`
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
- `source <url> positional argument`: The URL of a Git repository to install the extension from. The repository must contain a `gemini-extension.json` file in its root.
|
||||||
|
- `--path <path>`: The path to a local directory to install as an extension. The directory must contain a `gemini-extension.json` file.
|
||||||
|
|||||||
1
package-lock.json
generated
1
package-lock.json
generated
@@ -13,6 +13,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lvce-editor/ripgrep": "^2.1.0",
|
"@lvce-editor/ripgrep": "^2.1.0",
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
|
"simple-git": "^3.28.0",
|
||||||
"strip-ansi": "^7.1.0"
|
"strip-ansi": "^7.1.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
@@ -94,6 +94,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lvce-editor/ripgrep": "^2.1.0",
|
"@lvce-editor/ripgrep": "^2.1.0",
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
|
"simple-git": "^3.28.0",
|
||||||
"strip-ansi": "^7.1.0"
|
"strip-ansi": "^7.1.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
|
|||||||
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',
|
'test',
|
||||||
];
|
];
|
||||||
|
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
|
|
||||||
// Verify that the argument was parsed correctly
|
// Verify that the argument was parsed correctly
|
||||||
expect(argv.approvalMode).toBe('auto_edit');
|
expect(argv.approvalMode).toBe('auto_edit');
|
||||||
@@ -306,7 +306,7 @@ describe('Configuration Integration Tests', () => {
|
|||||||
'test',
|
'test',
|
||||||
];
|
];
|
||||||
|
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
|
|
||||||
expect(argv.approvalMode).toBe('yolo');
|
expect(argv.approvalMode).toBe('yolo');
|
||||||
expect(argv.prompt).toBe('test');
|
expect(argv.prompt).toBe('test');
|
||||||
@@ -329,7 +329,7 @@ describe('Configuration Integration Tests', () => {
|
|||||||
'test',
|
'test',
|
||||||
];
|
];
|
||||||
|
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
|
|
||||||
expect(argv.approvalMode).toBe('default');
|
expect(argv.approvalMode).toBe('default');
|
||||||
expect(argv.prompt).toBe('test');
|
expect(argv.prompt).toBe('test');
|
||||||
@@ -345,7 +345,7 @@ describe('Configuration Integration Tests', () => {
|
|||||||
try {
|
try {
|
||||||
process.argv = ['node', 'script.js', '--yolo', '-p', 'test'];
|
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.yolo).toBe(true);
|
||||||
expect(argv.approvalMode).toBeUndefined(); // Should NOT be set when using --yolo
|
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'];
|
process.argv = ['node', 'script.js', '--approval-mode', 'invalid_mode'];
|
||||||
|
|
||||||
// Should throw during argument parsing due to yargs validation
|
// Should throw during argument parsing due to yargs validation
|
||||||
await expect(parseArguments()).rejects.toThrow();
|
await expect(parseArguments({} as Settings)).rejects.toThrow();
|
||||||
} finally {
|
} finally {
|
||||||
process.argv = originalArgv;
|
process.argv = originalArgv;
|
||||||
}
|
}
|
||||||
@@ -381,7 +381,7 @@ describe('Configuration Integration Tests', () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
// Should throw during argument parsing due to conflict validation
|
// Should throw during argument parsing due to conflict validation
|
||||||
await expect(parseArguments()).rejects.toThrow();
|
await expect(parseArguments({} as Settings)).rejects.toThrow();
|
||||||
} finally {
|
} finally {
|
||||||
process.argv = originalArgv;
|
process.argv = originalArgv;
|
||||||
}
|
}
|
||||||
@@ -394,7 +394,7 @@ describe('Configuration Integration Tests', () => {
|
|||||||
// Test that no approval mode arguments defaults to no flags set
|
// Test that no approval mode arguments defaults to no flags set
|
||||||
process.argv = ['node', 'script.js', '-p', 'test'];
|
process.argv = ['node', 'script.js', '-p', 'test'];
|
||||||
|
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
|
|
||||||
expect(argv.approvalMode).toBeUndefined();
|
expect(argv.approvalMode).toBeUndefined();
|
||||||
expect(argv.yolo).toBe(false);
|
expect(argv.yolo).toBe(false);
|
||||||
|
|||||||
@@ -125,7 +125,9 @@ describe('parseArguments', () => {
|
|||||||
.spyOn(console, 'error')
|
.spyOn(console, 'error')
|
||||||
.mockImplementation(() => {});
|
.mockImplementation(() => {});
|
||||||
|
|
||||||
await expect(parseArguments()).rejects.toThrow('process.exit called');
|
await expect(parseArguments({} as Settings)).rejects.toThrow(
|
||||||
|
'process.exit called',
|
||||||
|
);
|
||||||
|
|
||||||
expect(mockConsoleError).toHaveBeenCalledWith(
|
expect(mockConsoleError).toHaveBeenCalledWith(
|
||||||
expect.stringContaining(
|
expect.stringContaining(
|
||||||
@@ -155,7 +157,9 @@ describe('parseArguments', () => {
|
|||||||
.spyOn(console, 'error')
|
.spyOn(console, 'error')
|
||||||
.mockImplementation(() => {});
|
.mockImplementation(() => {});
|
||||||
|
|
||||||
await expect(parseArguments()).rejects.toThrow('process.exit called');
|
await expect(parseArguments({} as Settings)).rejects.toThrow(
|
||||||
|
'process.exit called',
|
||||||
|
);
|
||||||
|
|
||||||
expect(mockConsoleError).toHaveBeenCalledWith(
|
expect(mockConsoleError).toHaveBeenCalledWith(
|
||||||
expect.stringContaining(
|
expect.stringContaining(
|
||||||
@@ -169,7 +173,7 @@ describe('parseArguments', () => {
|
|||||||
|
|
||||||
it('should allow --prompt without --prompt-interactive', async () => {
|
it('should allow --prompt without --prompt-interactive', async () => {
|
||||||
process.argv = ['node', 'script.js', '--prompt', 'test prompt'];
|
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.prompt).toBe('test prompt');
|
||||||
expect(argv.promptInteractive).toBeUndefined();
|
expect(argv.promptInteractive).toBeUndefined();
|
||||||
});
|
});
|
||||||
@@ -181,14 +185,14 @@ describe('parseArguments', () => {
|
|||||||
'--prompt-interactive',
|
'--prompt-interactive',
|
||||||
'interactive prompt',
|
'interactive prompt',
|
||||||
];
|
];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
expect(argv.promptInteractive).toBe('interactive prompt');
|
expect(argv.promptInteractive).toBe('interactive prompt');
|
||||||
expect(argv.prompt).toBeUndefined();
|
expect(argv.prompt).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow -i flag as alias for --prompt-interactive', async () => {
|
it('should allow -i flag as alias for --prompt-interactive', async () => {
|
||||||
process.argv = ['node', 'script.js', '-i', 'interactive prompt'];
|
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.promptInteractive).toBe('interactive prompt');
|
||||||
expect(argv.prompt).toBeUndefined();
|
expect(argv.prompt).toBeUndefined();
|
||||||
});
|
});
|
||||||
@@ -210,7 +214,9 @@ describe('parseArguments', () => {
|
|||||||
.spyOn(console, 'error')
|
.spyOn(console, 'error')
|
||||||
.mockImplementation(() => {});
|
.mockImplementation(() => {});
|
||||||
|
|
||||||
await expect(parseArguments()).rejects.toThrow('process.exit called');
|
await expect(parseArguments({} as Settings)).rejects.toThrow(
|
||||||
|
'process.exit called',
|
||||||
|
);
|
||||||
|
|
||||||
expect(mockConsoleError).toHaveBeenCalledWith(
|
expect(mockConsoleError).toHaveBeenCalledWith(
|
||||||
expect.stringContaining(
|
expect.stringContaining(
|
||||||
@@ -233,7 +239,9 @@ describe('parseArguments', () => {
|
|||||||
.spyOn(console, 'error')
|
.spyOn(console, 'error')
|
||||||
.mockImplementation(() => {});
|
.mockImplementation(() => {});
|
||||||
|
|
||||||
await expect(parseArguments()).rejects.toThrow('process.exit called');
|
await expect(parseArguments({} as Settings)).rejects.toThrow(
|
||||||
|
'process.exit called',
|
||||||
|
);
|
||||||
|
|
||||||
expect(mockConsoleError).toHaveBeenCalledWith(
|
expect(mockConsoleError).toHaveBeenCalledWith(
|
||||||
expect.stringContaining(
|
expect.stringContaining(
|
||||||
@@ -247,14 +255,14 @@ describe('parseArguments', () => {
|
|||||||
|
|
||||||
it('should allow --approval-mode without --yolo', async () => {
|
it('should allow --approval-mode without --yolo', async () => {
|
||||||
process.argv = ['node', 'script.js', '--approval-mode', 'auto_edit'];
|
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.approvalMode).toBe('auto_edit');
|
||||||
expect(argv.yolo).toBe(false);
|
expect(argv.yolo).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow --yolo without --approval-mode', async () => {
|
it('should allow --yolo without --approval-mode', async () => {
|
||||||
process.argv = ['node', 'script.js', '--yolo'];
|
process.argv = ['node', 'script.js', '--yolo'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
expect(argv.yolo).toBe(true);
|
expect(argv.yolo).toBe(true);
|
||||||
expect(argv.approvalMode).toBeUndefined();
|
expect(argv.approvalMode).toBeUndefined();
|
||||||
});
|
});
|
||||||
@@ -270,7 +278,9 @@ describe('parseArguments', () => {
|
|||||||
.spyOn(console, 'error')
|
.spyOn(console, 'error')
|
||||||
.mockImplementation(() => {});
|
.mockImplementation(() => {});
|
||||||
|
|
||||||
await expect(parseArguments()).rejects.toThrow('process.exit called');
|
await expect(parseArguments({} as Settings)).rejects.toThrow(
|
||||||
|
'process.exit called',
|
||||||
|
);
|
||||||
|
|
||||||
expect(mockConsoleError).toHaveBeenCalledWith(
|
expect(mockConsoleError).toHaveBeenCalledWith(
|
||||||
expect.stringContaining('Invalid values:'),
|
expect.stringContaining('Invalid values:'),
|
||||||
@@ -298,7 +308,7 @@ describe('loadCliConfig', () => {
|
|||||||
|
|
||||||
it('should set showMemoryUsage to true when --show-memory-usage flag is present', async () => {
|
it('should set showMemoryUsage to true when --show-memory-usage flag is present', async () => {
|
||||||
process.argv = ['node', 'script.js', '--show-memory-usage'];
|
process.argv = ['node', 'script.js', '--show-memory-usage'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = {};
|
const settings: Settings = {};
|
||||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
expect(config.getShowMemoryUsage()).toBe(true);
|
expect(config.getShowMemoryUsage()).toBe(true);
|
||||||
@@ -306,7 +316,7 @@ describe('loadCliConfig', () => {
|
|||||||
|
|
||||||
it('should set showMemoryUsage to false when --memory flag is not present', async () => {
|
it('should set showMemoryUsage to false when --memory flag is not present', async () => {
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = {};
|
const settings: Settings = {};
|
||||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
expect(config.getShowMemoryUsage()).toBe(false);
|
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 () => {
|
it('should set showMemoryUsage to false by default from settings if CLI flag is not present', async () => {
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = { showMemoryUsage: false };
|
const settings: Settings = { showMemoryUsage: false };
|
||||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
expect(config.getShowMemoryUsage()).toBe(false);
|
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 () => {
|
it('should prioritize CLI flag over settings for showMemoryUsage (CLI true, settings false)', async () => {
|
||||||
process.argv = ['node', 'script.js', '--show-memory-usage'];
|
process.argv = ['node', 'script.js', '--show-memory-usage'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = { showMemoryUsage: false };
|
const settings: Settings = { showMemoryUsage: false };
|
||||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
expect(config.getShowMemoryUsage()).toBe(true);
|
expect(config.getShowMemoryUsage()).toBe(true);
|
||||||
@@ -356,7 +366,7 @@ describe('loadCliConfig', () => {
|
|||||||
|
|
||||||
it(`should leave proxy to empty by default`, async () => {
|
it(`should leave proxy to empty by default`, async () => {
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = {};
|
const settings: Settings = {};
|
||||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
expect(config.getProxy()).toBeFalsy();
|
expect(config.getProxy()).toBeFalsy();
|
||||||
@@ -397,7 +407,7 @@ describe('loadCliConfig', () => {
|
|||||||
it(`should set proxy to ${expected} according to environment variable [${input.env_name}]`, async () => {
|
it(`should set proxy to ${expected} according to environment variable [${input.env_name}]`, async () => {
|
||||||
vi.stubEnv(input.env_name, input.proxy_url);
|
vi.stubEnv(input.env_name, input.proxy_url);
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = {};
|
const settings: Settings = {};
|
||||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
expect(config.getProxy()).toBe(expected);
|
expect(config.getProxy()).toBe(expected);
|
||||||
@@ -406,7 +416,7 @@ describe('loadCliConfig', () => {
|
|||||||
|
|
||||||
it('should set proxy when --proxy flag is present', async () => {
|
it('should set proxy when --proxy flag is present', async () => {
|
||||||
process.argv = ['node', 'script.js', '--proxy', 'http://localhost:7890'];
|
process.argv = ['node', 'script.js', '--proxy', 'http://localhost:7890'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = {};
|
const settings: Settings = {};
|
||||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
expect(config.getProxy()).toBe('http://localhost:7890');
|
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 () => {
|
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');
|
vi.stubEnv('http_proxy', 'http://localhost:7891');
|
||||||
process.argv = ['node', 'script.js', '--proxy', 'http://localhost:7890'];
|
process.argv = ['node', 'script.js', '--proxy', 'http://localhost:7890'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = {};
|
const settings: Settings = {};
|
||||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
expect(config.getProxy()).toBe('http://localhost:7890');
|
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 () => {
|
it('should set telemetry to false by default when no flag or setting is present', async () => {
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = {};
|
const settings: Settings = {};
|
||||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
expect(config.getTelemetryEnabled()).toBe(false);
|
expect(config.getTelemetryEnabled()).toBe(false);
|
||||||
@@ -448,7 +458,7 @@ describe('loadCliConfig telemetry', () => {
|
|||||||
|
|
||||||
it('should set telemetry to true when --telemetry flag is present', async () => {
|
it('should set telemetry to true when --telemetry flag is present', async () => {
|
||||||
process.argv = ['node', 'script.js', '--telemetry'];
|
process.argv = ['node', 'script.js', '--telemetry'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = {};
|
const settings: Settings = {};
|
||||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
expect(config.getTelemetryEnabled()).toBe(true);
|
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 () => {
|
it('should set telemetry to false when --no-telemetry flag is present', async () => {
|
||||||
process.argv = ['node', 'script.js', '--no-telemetry'];
|
process.argv = ['node', 'script.js', '--no-telemetry'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = {};
|
const settings: Settings = {};
|
||||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
expect(config.getTelemetryEnabled()).toBe(false);
|
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 () => {
|
it('should use telemetry value from settings if CLI flag is not present (settings true)', async () => {
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = { telemetry: { enabled: true } };
|
const settings: Settings = { telemetry: { enabled: true } };
|
||||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
expect(config.getTelemetryEnabled()).toBe(true);
|
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 () => {
|
it('should use telemetry value from settings if CLI flag is not present (settings false)', async () => {
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = { telemetry: { enabled: false } };
|
const settings: Settings = { telemetry: { enabled: false } };
|
||||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
expect(config.getTelemetryEnabled()).toBe(false);
|
expect(config.getTelemetryEnabled()).toBe(false);
|
||||||
@@ -480,7 +490,7 @@ describe('loadCliConfig telemetry', () => {
|
|||||||
|
|
||||||
it('should prioritize --telemetry CLI flag (true) over settings (false)', async () => {
|
it('should prioritize --telemetry CLI flag (true) over settings (false)', async () => {
|
||||||
process.argv = ['node', 'script.js', '--telemetry'];
|
process.argv = ['node', 'script.js', '--telemetry'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = { telemetry: { enabled: false } };
|
const settings: Settings = { telemetry: { enabled: false } };
|
||||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
expect(config.getTelemetryEnabled()).toBe(true);
|
expect(config.getTelemetryEnabled()).toBe(true);
|
||||||
@@ -488,7 +498,7 @@ describe('loadCliConfig telemetry', () => {
|
|||||||
|
|
||||||
it('should prioritize --no-telemetry CLI flag (false) over settings (true)', async () => {
|
it('should prioritize --no-telemetry CLI flag (false) over settings (true)', async () => {
|
||||||
process.argv = ['node', 'script.js', '--no-telemetry'];
|
process.argv = ['node', 'script.js', '--no-telemetry'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = { telemetry: { enabled: true } };
|
const settings: Settings = { telemetry: { enabled: true } };
|
||||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
expect(config.getTelemetryEnabled()).toBe(false);
|
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 () => {
|
it('should use telemetry OTLP endpoint from settings if CLI flag is not present', async () => {
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = {
|
const settings: Settings = {
|
||||||
telemetry: { otlpEndpoint: 'http://settings.example.com' },
|
telemetry: { otlpEndpoint: 'http://settings.example.com' },
|
||||||
};
|
};
|
||||||
@@ -513,7 +523,7 @@ describe('loadCliConfig telemetry', () => {
|
|||||||
'--telemetry-otlp-endpoint',
|
'--telemetry-otlp-endpoint',
|
||||||
'http://cli.example.com',
|
'http://cli.example.com',
|
||||||
];
|
];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = {
|
const settings: Settings = {
|
||||||
telemetry: { otlpEndpoint: 'http://settings.example.com' },
|
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 () => {
|
it('should use default endpoint if no OTLP endpoint is provided via CLI or settings', async () => {
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = { telemetry: { enabled: true } };
|
const settings: Settings = { telemetry: { enabled: true } };
|
||||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
expect(config.getTelemetryOtlpEndpoint()).toBe('http://localhost:4317');
|
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 () => {
|
it('should use telemetry target from settings if CLI flag is not present', async () => {
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = {
|
const settings: Settings = {
|
||||||
telemetry: { target: ServerConfig.DEFAULT_TELEMETRY_TARGET },
|
telemetry: { target: ServerConfig.DEFAULT_TELEMETRY_TARGET },
|
||||||
};
|
};
|
||||||
@@ -543,7 +553,7 @@ describe('loadCliConfig telemetry', () => {
|
|||||||
|
|
||||||
it('should prioritize --telemetry-target CLI flag over settings', async () => {
|
it('should prioritize --telemetry-target CLI flag over settings', async () => {
|
||||||
process.argv = ['node', 'script.js', '--telemetry-target', 'gcp'];
|
process.argv = ['node', 'script.js', '--telemetry-target', 'gcp'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = {
|
const settings: Settings = {
|
||||||
telemetry: { target: ServerConfig.DEFAULT_TELEMETRY_TARGET },
|
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 () => {
|
it('should use default target if no target is provided via CLI or settings', async () => {
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = { telemetry: { enabled: true } };
|
const settings: Settings = { telemetry: { enabled: true } };
|
||||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
expect(config.getTelemetryTarget()).toBe(
|
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 () => {
|
it('should use telemetry log prompts from settings if CLI flag is not present', async () => {
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = { telemetry: { logPrompts: false } };
|
const settings: Settings = { telemetry: { logPrompts: false } };
|
||||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
expect(config.getTelemetryLogPromptsEnabled()).toBe(false);
|
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 () => {
|
it('should prioritize --telemetry-log-prompts CLI flag (true) over settings (false)', async () => {
|
||||||
process.argv = ['node', 'script.js', '--telemetry-log-prompts'];
|
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 settings: Settings = { telemetry: { logPrompts: false } };
|
||||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
expect(config.getTelemetryLogPromptsEnabled()).toBe(true);
|
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 () => {
|
it('should prioritize --no-telemetry-log-prompts CLI flag (false) over settings (true)', async () => {
|
||||||
process.argv = ['node', 'script.js', '--no-telemetry-log-prompts'];
|
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 settings: Settings = { telemetry: { logPrompts: true } };
|
||||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
expect(config.getTelemetryLogPromptsEnabled()).toBe(false);
|
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 () => {
|
it('should use default log prompts (true) if no value is provided via CLI or settings', async () => {
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = { telemetry: { enabled: true } };
|
const settings: Settings = { telemetry: { enabled: true } };
|
||||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
expect(config.getTelemetryLogPromptsEnabled()).toBe(true);
|
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 () => {
|
it('should use telemetry OTLP protocol from settings if CLI flag is not present', async () => {
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = {
|
const settings: Settings = {
|
||||||
telemetry: { otlpProtocol: 'http' },
|
telemetry: { otlpProtocol: 'http' },
|
||||||
};
|
};
|
||||||
@@ -605,7 +615,7 @@ describe('loadCliConfig telemetry', () => {
|
|||||||
|
|
||||||
it('should prioritize --telemetry-otlp-protocol CLI flag over settings', async () => {
|
it('should prioritize --telemetry-otlp-protocol CLI flag over settings', async () => {
|
||||||
process.argv = ['node', 'script.js', '--telemetry-otlp-protocol', 'http'];
|
process.argv = ['node', 'script.js', '--telemetry-otlp-protocol', 'http'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = {
|
const settings: Settings = {
|
||||||
telemetry: { otlpProtocol: 'grpc' },
|
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 () => {
|
it('should use default protocol if no OTLP protocol is provided via CLI or settings', async () => {
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = { telemetry: { enabled: true } };
|
const settings: Settings = { telemetry: { enabled: true } };
|
||||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
expect(config.getTelemetryOtlpProtocol()).toBe('grpc');
|
expect(config.getTelemetryOtlpProtocol()).toBe('grpc');
|
||||||
@@ -637,7 +647,9 @@ describe('loadCliConfig telemetry', () => {
|
|||||||
.spyOn(console, 'error')
|
.spyOn(console, 'error')
|
||||||
.mockImplementation(() => {});
|
.mockImplementation(() => {});
|
||||||
|
|
||||||
await expect(parseArguments()).rejects.toThrow('process.exit called');
|
await expect(parseArguments({} as Settings)).rejects.toThrow(
|
||||||
|
'process.exit called',
|
||||||
|
);
|
||||||
|
|
||||||
expect(mockConsoleError).toHaveBeenCalledWith(
|
expect(mockConsoleError).toHaveBeenCalledWith(
|
||||||
expect.stringContaining('Invalid values:'),
|
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);
|
await loadCliConfig(settings, extensions, 'session-id', argv);
|
||||||
expect(ServerConfig.loadServerHierarchicalMemory).toHaveBeenCalledWith(
|
expect(ServerConfig.loadServerHierarchicalMemory).toHaveBeenCalledWith(
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
@@ -765,7 +777,7 @@ describe('mergeMcpServers', () => {
|
|||||||
];
|
];
|
||||||
const originalSettings = JSON.parse(JSON.stringify(settings));
|
const originalSettings = JSON.parse(JSON.stringify(settings));
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
await loadCliConfig(settings, extensions, 'test-session', argv);
|
await loadCliConfig(settings, extensions, 'test-session', argv);
|
||||||
expect(settings).toEqual(originalSettings);
|
expect(settings).toEqual(originalSettings);
|
||||||
});
|
});
|
||||||
@@ -806,7 +818,7 @@ describe('mergeExcludeTools', () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const config = await loadCliConfig(
|
const config = await loadCliConfig(
|
||||||
settings,
|
settings,
|
||||||
extensions,
|
extensions,
|
||||||
@@ -833,7 +845,7 @@ describe('mergeExcludeTools', () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const config = await loadCliConfig(
|
const config = await loadCliConfig(
|
||||||
settings,
|
settings,
|
||||||
extensions,
|
extensions,
|
||||||
@@ -869,7 +881,7 @@ describe('mergeExcludeTools', () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const config = await loadCliConfig(
|
const config = await loadCliConfig(
|
||||||
settings,
|
settings,
|
||||||
extensions,
|
extensions,
|
||||||
@@ -887,7 +899,7 @@ describe('mergeExcludeTools', () => {
|
|||||||
const settings: Settings = {};
|
const settings: Settings = {};
|
||||||
const extensions: Extension[] = [];
|
const extensions: Extension[] = [];
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const config = await loadCliConfig(
|
const config = await loadCliConfig(
|
||||||
settings,
|
settings,
|
||||||
extensions,
|
extensions,
|
||||||
@@ -902,7 +914,7 @@ describe('mergeExcludeTools', () => {
|
|||||||
const settings: Settings = {};
|
const settings: Settings = {};
|
||||||
const extensions: Extension[] = [];
|
const extensions: Extension[] = [];
|
||||||
process.argv = ['node', 'script.js', '-p', 'test'];
|
process.argv = ['node', 'script.js', '-p', 'test'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const config = await loadCliConfig(
|
const config = await loadCliConfig(
|
||||||
settings,
|
settings,
|
||||||
extensions,
|
extensions,
|
||||||
@@ -914,7 +926,7 @@ describe('mergeExcludeTools', () => {
|
|||||||
|
|
||||||
it('should handle settings with excludeTools but no extensions', async () => {
|
it('should handle settings with excludeTools but no extensions', async () => {
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = { excludeTools: ['tool1', 'tool2'] };
|
const settings: Settings = { excludeTools: ['tool1', 'tool2'] };
|
||||||
const extensions: Extension[] = [];
|
const extensions: Extension[] = [];
|
||||||
const config = await loadCliConfig(
|
const config = await loadCliConfig(
|
||||||
@@ -943,7 +955,7 @@ describe('mergeExcludeTools', () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const config = await loadCliConfig(
|
const config = await loadCliConfig(
|
||||||
settings,
|
settings,
|
||||||
extensions,
|
extensions,
|
||||||
@@ -971,7 +983,7 @@ describe('mergeExcludeTools', () => {
|
|||||||
];
|
];
|
||||||
const originalSettings = JSON.parse(JSON.stringify(settings));
|
const originalSettings = JSON.parse(JSON.stringify(settings));
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
await loadCliConfig(settings, extensions, 'test-session', argv);
|
await loadCliConfig(settings, extensions, 'test-session', argv);
|
||||||
expect(settings).toEqual(originalSettings);
|
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 () => {
|
it('should exclude all interactive tools in non-interactive mode with default approval mode', async () => {
|
||||||
process.argv = ['node', 'script.js', '-p', 'test'];
|
process.argv = ['node', 'script.js', '-p', 'test'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = {};
|
const settings: Settings = {};
|
||||||
const extensions: Extension[] = [];
|
const extensions: Extension[] = [];
|
||||||
|
|
||||||
@@ -1016,7 +1028,7 @@ describe('Approval mode tool exclusion logic', () => {
|
|||||||
'-p',
|
'-p',
|
||||||
'test',
|
'test',
|
||||||
];
|
];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = {};
|
const settings: Settings = {};
|
||||||
const extensions: Extension[] = [];
|
const extensions: Extension[] = [];
|
||||||
|
|
||||||
@@ -1042,7 +1054,7 @@ describe('Approval mode tool exclusion logic', () => {
|
|||||||
'-p',
|
'-p',
|
||||||
'test',
|
'test',
|
||||||
];
|
];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = {};
|
const settings: Settings = {};
|
||||||
const extensions: Extension[] = [];
|
const extensions: Extension[] = [];
|
||||||
|
|
||||||
@@ -1068,7 +1080,7 @@ describe('Approval mode tool exclusion logic', () => {
|
|||||||
'-p',
|
'-p',
|
||||||
'test',
|
'test',
|
||||||
];
|
];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = {};
|
const settings: Settings = {};
|
||||||
const extensions: Extension[] = [];
|
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 () => {
|
it('should exclude no interactive tools in non-interactive mode with legacy yolo flag', async () => {
|
||||||
process.argv = ['node', 'script.js', '--yolo', '-p', 'test'];
|
process.argv = ['node', 'script.js', '--yolo', '-p', 'test'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = {};
|
const settings: Settings = {};
|
||||||
const extensions: Extension[] = [];
|
const extensions: Extension[] = [];
|
||||||
|
|
||||||
@@ -1117,7 +1129,7 @@ describe('Approval mode tool exclusion logic', () => {
|
|||||||
|
|
||||||
for (const testCase of testCases) {
|
for (const testCase of testCases) {
|
||||||
process.argv = testCase.args;
|
process.argv = testCase.args;
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = {};
|
const settings: Settings = {};
|
||||||
const extensions: Extension[] = [];
|
const extensions: Extension[] = [];
|
||||||
|
|
||||||
@@ -1144,7 +1156,7 @@ describe('Approval mode tool exclusion logic', () => {
|
|||||||
'-p',
|
'-p',
|
||||||
'test',
|
'test',
|
||||||
];
|
];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = { excludeTools: ['custom_tool'] };
|
const settings: Settings = { excludeTools: ['custom_tool'] };
|
||||||
const extensions: Extension[] = [];
|
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 () => {
|
it('should allow all MCP servers if the flag is not provided', async () => {
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const config = await loadCliConfig(baseSettings, [], 'test-session', argv);
|
const config = await loadCliConfig(baseSettings, [], 'test-session', argv);
|
||||||
expect(config.getMcpServers()).toEqual(baseSettings.mcpServers);
|
expect(config.getMcpServers()).toEqual(baseSettings.mcpServers);
|
||||||
});
|
});
|
||||||
@@ -1224,7 +1236,7 @@ describe('loadCliConfig with allowed-mcp-server-names', () => {
|
|||||||
'--allowed-mcp-server-names',
|
'--allowed-mcp-server-names',
|
||||||
'server1',
|
'server1',
|
||||||
];
|
];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const config = await loadCliConfig(baseSettings, [], 'test-session', argv);
|
const config = await loadCliConfig(baseSettings, [], 'test-session', argv);
|
||||||
expect(config.getMcpServers()).toEqual({
|
expect(config.getMcpServers()).toEqual({
|
||||||
server1: { url: 'http://localhost:8080' },
|
server1: { url: 'http://localhost:8080' },
|
||||||
@@ -1240,7 +1252,7 @@ describe('loadCliConfig with allowed-mcp-server-names', () => {
|
|||||||
'--allowed-mcp-server-names',
|
'--allowed-mcp-server-names',
|
||||||
'server3',
|
'server3',
|
||||||
];
|
];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const config = await loadCliConfig(baseSettings, [], 'test-session', argv);
|
const config = await loadCliConfig(baseSettings, [], 'test-session', argv);
|
||||||
expect(config.getMcpServers()).toEqual({
|
expect(config.getMcpServers()).toEqual({
|
||||||
server1: { url: 'http://localhost:8080' },
|
server1: { url: 'http://localhost:8080' },
|
||||||
@@ -1257,7 +1269,7 @@ describe('loadCliConfig with allowed-mcp-server-names', () => {
|
|||||||
'--allowed-mcp-server-names',
|
'--allowed-mcp-server-names',
|
||||||
'server4',
|
'server4',
|
||||||
];
|
];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const config = await loadCliConfig(baseSettings, [], 'test-session', argv);
|
const config = await loadCliConfig(baseSettings, [], 'test-session', argv);
|
||||||
expect(config.getMcpServers()).toEqual({
|
expect(config.getMcpServers()).toEqual({
|
||||||
server1: { url: 'http://localhost:8080' },
|
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 () => {
|
it('should allow no MCP servers if the flag is provided but empty', async () => {
|
||||||
process.argv = ['node', 'script.js', '--allowed-mcp-server-names', ''];
|
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);
|
const config = await loadCliConfig(baseSettings, [], 'test-session', argv);
|
||||||
expect(config.getMcpServers()).toEqual({});
|
expect(config.getMcpServers()).toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should read allowMCPServers from settings', async () => {
|
it('should read allowMCPServers from settings', async () => {
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = {
|
const settings: Settings = {
|
||||||
...baseSettings,
|
...baseSettings,
|
||||||
allowMCPServers: ['server1', 'server2'],
|
allowMCPServers: ['server1', 'server2'],
|
||||||
@@ -1287,7 +1299,7 @@ describe('loadCliConfig with allowed-mcp-server-names', () => {
|
|||||||
|
|
||||||
it('should read excludeMCPServers from settings', async () => {
|
it('should read excludeMCPServers from settings', async () => {
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = {
|
const settings: Settings = {
|
||||||
...baseSettings,
|
...baseSettings,
|
||||||
excludeMCPServers: ['server1', 'server2'],
|
excludeMCPServers: ['server1', 'server2'],
|
||||||
@@ -1300,7 +1312,7 @@ describe('loadCliConfig with allowed-mcp-server-names', () => {
|
|||||||
|
|
||||||
it('should override allowMCPServers with excludeMCPServers if overlapping', async () => {
|
it('should override allowMCPServers with excludeMCPServers if overlapping', async () => {
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = {
|
const settings: Settings = {
|
||||||
...baseSettings,
|
...baseSettings,
|
||||||
excludeMCPServers: ['server1'],
|
excludeMCPServers: ['server1'],
|
||||||
@@ -1319,7 +1331,7 @@ describe('loadCliConfig with allowed-mcp-server-names', () => {
|
|||||||
'--allowed-mcp-server-names',
|
'--allowed-mcp-server-names',
|
||||||
'server1',
|
'server1',
|
||||||
];
|
];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = {
|
const settings: Settings = {
|
||||||
...baseSettings,
|
...baseSettings,
|
||||||
excludeMCPServers: ['server1'],
|
excludeMCPServers: ['server1'],
|
||||||
@@ -1348,7 +1360,7 @@ describe('loadCliConfig extensions', () => {
|
|||||||
|
|
||||||
it('should not filter extensions if --extensions flag is not used', async () => {
|
it('should not filter extensions if --extensions flag is not used', async () => {
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = {};
|
const settings: Settings = {};
|
||||||
const config = await loadCliConfig(
|
const config = await loadCliConfig(
|
||||||
settings,
|
settings,
|
||||||
@@ -1364,7 +1376,7 @@ describe('loadCliConfig extensions', () => {
|
|||||||
|
|
||||||
it('should filter extensions if --extensions flag is used', async () => {
|
it('should filter extensions if --extensions flag is used', async () => {
|
||||||
process.argv = ['node', 'script.js', '--extensions', 'ext1'];
|
process.argv = ['node', 'script.js', '--extensions', 'ext1'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = {};
|
const settings: Settings = {};
|
||||||
const config = await loadCliConfig(
|
const config = await loadCliConfig(
|
||||||
settings,
|
settings,
|
||||||
@@ -1379,7 +1391,7 @@ describe('loadCliConfig extensions', () => {
|
|||||||
describe('loadCliConfig model selection', () => {
|
describe('loadCliConfig model selection', () => {
|
||||||
it('selects a model from settings.json if provided', async () => {
|
it('selects a model from settings.json if provided', async () => {
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const config = await loadCliConfig(
|
const config = await loadCliConfig(
|
||||||
{
|
{
|
||||||
model: 'gemini-9001-ultra',
|
model: 'gemini-9001-ultra',
|
||||||
@@ -1394,7 +1406,7 @@ describe('loadCliConfig model selection', () => {
|
|||||||
|
|
||||||
it('uses the default gemini model if nothing is set', async () => {
|
it('uses the default gemini model if nothing is set', async () => {
|
||||||
process.argv = ['node', 'script.js']; // No model set.
|
process.argv = ['node', 'script.js']; // No model set.
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const config = await loadCliConfig(
|
const config = await loadCliConfig(
|
||||||
{
|
{
|
||||||
// No model set.
|
// No model set.
|
||||||
@@ -1409,7 +1421,7 @@ describe('loadCliConfig model selection', () => {
|
|||||||
|
|
||||||
it('always prefers model from argvs', async () => {
|
it('always prefers model from argvs', async () => {
|
||||||
process.argv = ['node', 'script.js', '--model', 'gemini-8675309-ultra'];
|
process.argv = ['node', 'script.js', '--model', 'gemini-8675309-ultra'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const config = await loadCliConfig(
|
const config = await loadCliConfig(
|
||||||
{
|
{
|
||||||
model: 'gemini-9001-ultra',
|
model: 'gemini-9001-ultra',
|
||||||
@@ -1424,7 +1436,7 @@ describe('loadCliConfig model selection', () => {
|
|||||||
|
|
||||||
it('selects the model from argvs if provided', async () => {
|
it('selects the model from argvs if provided', async () => {
|
||||||
process.argv = ['node', 'script.js', '--model', 'gemini-8675309-ultra'];
|
process.argv = ['node', 'script.js', '--model', 'gemini-8675309-ultra'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const config = await loadCliConfig(
|
const config = await loadCliConfig(
|
||||||
{
|
{
|
||||||
// No model provided via settings.
|
// No model provided via settings.
|
||||||
@@ -1456,14 +1468,14 @@ describe('loadCliConfig folderTrustFeature', () => {
|
|||||||
it('should be false by default', async () => {
|
it('should be false by default', async () => {
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const settings: Settings = {};
|
const settings: Settings = {};
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
expect(config.getFolderTrustFeature()).toBe(false);
|
expect(config.getFolderTrustFeature()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be true when settings.folderTrustFeature is true', async () => {
|
it('should be true when settings.folderTrustFeature is true', async () => {
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = { folderTrustFeature: true };
|
const settings: Settings = { folderTrustFeature: true };
|
||||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
expect(config.getFolderTrustFeature()).toBe(true);
|
expect(config.getFolderTrustFeature()).toBe(true);
|
||||||
@@ -1491,14 +1503,14 @@ describe('loadCliConfig folderTrust', () => {
|
|||||||
folderTrustFeature: false,
|
folderTrustFeature: false,
|
||||||
folderTrust: false,
|
folderTrust: false,
|
||||||
};
|
};
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
expect(config.getFolderTrust()).toBe(false);
|
expect(config.getFolderTrust()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be false if folderTrustFeature is true and folderTrust is false', async () => {
|
it('should be false if folderTrustFeature is true and folderTrust is false', async () => {
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = { folderTrustFeature: true, folderTrust: false };
|
const settings: Settings = { folderTrustFeature: true, folderTrust: false };
|
||||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
expect(config.getFolderTrust()).toBe(false);
|
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 () => {
|
it('should be false if folderTrustFeature is false and folderTrust is true', async () => {
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = { folderTrustFeature: false, folderTrust: true };
|
const settings: Settings = { folderTrustFeature: false, folderTrust: true };
|
||||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
expect(config.getFolderTrust()).toBe(false);
|
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 () => {
|
it('should be true when folderTrustFeature is true and folderTrust is true', async () => {
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = { folderTrustFeature: true, folderTrust: true };
|
const settings: Settings = { folderTrustFeature: true, folderTrust: true };
|
||||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
expect(config.getFolderTrust()).toBe(true);
|
expect(config.getFolderTrust()).toBe(true);
|
||||||
@@ -1547,7 +1559,7 @@ describe('loadCliConfig with includeDirectories', () => {
|
|||||||
'--include-directories',
|
'--include-directories',
|
||||||
`${path.resolve(path.sep, 'cli', 'path1')},${path.join(mockCwd, 'cli', 'path2')}`,
|
`${path.resolve(path.sep, 'cli', 'path1')},${path.join(mockCwd, 'cli', 'path2')}`,
|
||||||
];
|
];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = {
|
const settings: Settings = {
|
||||||
includeDirectories: [
|
includeDirectories: [
|
||||||
path.resolve(path.sep, 'settings', 'path1'),
|
path.resolve(path.sep, 'settings', 'path1'),
|
||||||
@@ -1590,7 +1602,7 @@ describe('loadCliConfig chatCompression', () => {
|
|||||||
|
|
||||||
it('should pass chatCompression settings to the core config', async () => {
|
it('should pass chatCompression settings to the core config', async () => {
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = {
|
const settings: Settings = {
|
||||||
chatCompression: {
|
chatCompression: {
|
||||||
contextPercentageThreshold: 0.5,
|
contextPercentageThreshold: 0.5,
|
||||||
@@ -1604,7 +1616,7 @@ describe('loadCliConfig chatCompression', () => {
|
|||||||
|
|
||||||
it('should have undefined chatCompression if not in settings', async () => {
|
it('should have undefined chatCompression if not in settings', async () => {
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = {};
|
const settings: Settings = {};
|
||||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
expect(config.getChatCompression()).toBeUndefined();
|
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 () => {
|
it('should be false by default when useRipgrep is not set in settings', async () => {
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = {};
|
const settings: Settings = {};
|
||||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
expect(config.getUseRipgrep()).toBe(false);
|
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 () => {
|
it('should be true when useRipgrep is set to true in settings', async () => {
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = { useRipgrep: true };
|
const settings: Settings = { useRipgrep: true };
|
||||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
expect(config.getUseRipgrep()).toBe(true);
|
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 () => {
|
it('should be false when useRipgrep is explicitly set to false in settings', async () => {
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = { useRipgrep: false };
|
const settings: Settings = { useRipgrep: false };
|
||||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
expect(config.getUseRipgrep()).toBe(false);
|
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 () => {
|
it('should not exclude interactive tools in interactive mode without YOLO', async () => {
|
||||||
process.stdin.isTTY = true;
|
process.stdin.isTTY = true;
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const config = await loadCliConfig({}, [], 'test-session', argv);
|
const config = await loadCliConfig({}, [], 'test-session', argv);
|
||||||
expect(config.getExcludeTools()).not.toContain('run_shell_command');
|
expect(config.getExcludeTools()).not.toContain('run_shell_command');
|
||||||
expect(config.getExcludeTools()).not.toContain('replace');
|
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 () => {
|
it('should not exclude interactive tools in interactive mode with YOLO', async () => {
|
||||||
process.stdin.isTTY = true;
|
process.stdin.isTTY = true;
|
||||||
process.argv = ['node', 'script.js', '--yolo'];
|
process.argv = ['node', 'script.js', '--yolo'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const config = await loadCliConfig({}, [], 'test-session', argv);
|
const config = await loadCliConfig({}, [], 'test-session', argv);
|
||||||
expect(config.getExcludeTools()).not.toContain('run_shell_command');
|
expect(config.getExcludeTools()).not.toContain('run_shell_command');
|
||||||
expect(config.getExcludeTools()).not.toContain('replace');
|
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 () => {
|
it('should exclude interactive tools in non-interactive mode without YOLO', async () => {
|
||||||
process.stdin.isTTY = false;
|
process.stdin.isTTY = false;
|
||||||
process.argv = ['node', 'script.js', '-p', 'test'];
|
process.argv = ['node', 'script.js', '-p', 'test'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const config = await loadCliConfig({}, [], 'test-session', argv);
|
const config = await loadCliConfig({}, [], 'test-session', argv);
|
||||||
expect(config.getExcludeTools()).toContain('run_shell_command');
|
expect(config.getExcludeTools()).toContain('run_shell_command');
|
||||||
expect(config.getExcludeTools()).toContain('replace');
|
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 () => {
|
it('should not exclude interactive tools in non-interactive mode with YOLO', async () => {
|
||||||
process.stdin.isTTY = false;
|
process.stdin.isTTY = false;
|
||||||
process.argv = ['node', 'script.js', '-p', 'test', '--yolo'];
|
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);
|
const config = await loadCliConfig({}, [], 'test-session', argv);
|
||||||
expect(config.getExcludeTools()).not.toContain('run_shell_command');
|
expect(config.getExcludeTools()).not.toContain('run_shell_command');
|
||||||
expect(config.getExcludeTools()).not.toContain('replace');
|
expect(config.getExcludeTools()).not.toContain('replace');
|
||||||
@@ -1731,7 +1743,7 @@ describe('loadCliConfig interactive', () => {
|
|||||||
it('should be interactive if isTTY and no prompt', async () => {
|
it('should be interactive if isTTY and no prompt', async () => {
|
||||||
process.stdin.isTTY = true;
|
process.stdin.isTTY = true;
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const config = await loadCliConfig({}, [], 'test-session', argv);
|
const config = await loadCliConfig({}, [], 'test-session', argv);
|
||||||
expect(config.isInteractive()).toBe(true);
|
expect(config.isInteractive()).toBe(true);
|
||||||
});
|
});
|
||||||
@@ -1739,7 +1751,7 @@ describe('loadCliConfig interactive', () => {
|
|||||||
it('should be interactive if prompt-interactive is set', async () => {
|
it('should be interactive if prompt-interactive is set', async () => {
|
||||||
process.stdin.isTTY = false;
|
process.stdin.isTTY = false;
|
||||||
process.argv = ['node', 'script.js', '--prompt-interactive', 'test'];
|
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);
|
const config = await loadCliConfig({}, [], 'test-session', argv);
|
||||||
expect(config.isInteractive()).toBe(true);
|
expect(config.isInteractive()).toBe(true);
|
||||||
});
|
});
|
||||||
@@ -1747,7 +1759,7 @@ describe('loadCliConfig interactive', () => {
|
|||||||
it('should not be interactive if not isTTY and no prompt', async () => {
|
it('should not be interactive if not isTTY and no prompt', async () => {
|
||||||
process.stdin.isTTY = false;
|
process.stdin.isTTY = false;
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const config = await loadCliConfig({}, [], 'test-session', argv);
|
const config = await loadCliConfig({}, [], 'test-session', argv);
|
||||||
expect(config.isInteractive()).toBe(false);
|
expect(config.isInteractive()).toBe(false);
|
||||||
});
|
});
|
||||||
@@ -1755,7 +1767,7 @@ describe('loadCliConfig interactive', () => {
|
|||||||
it('should not be interactive if prompt is set', async () => {
|
it('should not be interactive if prompt is set', async () => {
|
||||||
process.stdin.isTTY = true;
|
process.stdin.isTTY = true;
|
||||||
process.argv = ['node', 'script.js', '--prompt', 'test'];
|
process.argv = ['node', 'script.js', '--prompt', 'test'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const config = await loadCliConfig({}, [], 'test-session', argv);
|
const config = await loadCliConfig({}, [], 'test-session', argv);
|
||||||
expect(config.isInteractive()).toBe(false);
|
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 () => {
|
it('should default to DEFAULT approval mode when no flags are set', async () => {
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const config = await loadCliConfig({}, [], 'test-session', argv);
|
const config = await loadCliConfig({}, [], 'test-session', argv);
|
||||||
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
|
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set YOLO approval mode when --yolo flag is used', async () => {
|
it('should set YOLO approval mode when --yolo flag is used', async () => {
|
||||||
process.argv = ['node', 'script.js', '--yolo'];
|
process.argv = ['node', 'script.js', '--yolo'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const config = await loadCliConfig({}, [], 'test-session', argv);
|
const config = await loadCliConfig({}, [], 'test-session', argv);
|
||||||
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
|
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set YOLO approval mode when -y flag is used', async () => {
|
it('should set YOLO approval mode when -y flag is used', async () => {
|
||||||
process.argv = ['node', 'script.js', '-y'];
|
process.argv = ['node', 'script.js', '-y'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const config = await loadCliConfig({}, [], 'test-session', argv);
|
const config = await loadCliConfig({}, [], 'test-session', argv);
|
||||||
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
|
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set DEFAULT approval mode when --approval-mode=default', async () => {
|
it('should set DEFAULT approval mode when --approval-mode=default', async () => {
|
||||||
process.argv = ['node', 'script.js', '--approval-mode', 'default'];
|
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);
|
const config = await loadCliConfig({}, [], 'test-session', argv);
|
||||||
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
|
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set AUTO_EDIT approval mode when --approval-mode=auto_edit', async () => {
|
it('should set AUTO_EDIT approval mode when --approval-mode=auto_edit', async () => {
|
||||||
process.argv = ['node', 'script.js', '--approval-mode', 'auto_edit'];
|
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);
|
const config = await loadCliConfig({}, [], 'test-session', argv);
|
||||||
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.AUTO_EDIT);
|
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.AUTO_EDIT);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set YOLO approval mode when --approval-mode=yolo', async () => {
|
it('should set YOLO approval mode when --approval-mode=yolo', async () => {
|
||||||
process.argv = ['node', 'script.js', '--approval-mode', 'yolo'];
|
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);
|
const config = await loadCliConfig({}, [], 'test-session', argv);
|
||||||
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
|
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
|
// Note: This test documents the intended behavior, but in practice the validation
|
||||||
// prevents both flags from being used together
|
// prevents both flags from being used together
|
||||||
process.argv = ['node', 'script.js', '--approval-mode', 'default'];
|
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
|
// Manually set yolo to true to simulate what would happen if validation didn't prevent it
|
||||||
argv.yolo = true;
|
argv.yolo = true;
|
||||||
const config = await loadCliConfig({}, [], 'test-session', argv);
|
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 () => {
|
it('should fall back to --yolo behavior when --approval-mode is not set', async () => {
|
||||||
process.argv = ['node', 'script.js', '--yolo'];
|
process.argv = ['node', 'script.js', '--yolo'];
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const config = await loadCliConfig({}, [], 'test-session', argv);
|
const config = await loadCliConfig({}, [], 'test-session', argv);
|
||||||
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
|
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
|
||||||
});
|
});
|
||||||
@@ -1949,7 +1961,7 @@ describe('loadCliConfig trustedFolder', () => {
|
|||||||
(settings.folderTrust ?? true);
|
(settings.folderTrust ?? true);
|
||||||
return featureIsEnabled ? mockTrustValue : true;
|
return featureIsEnabled ? mockTrustValue : true;
|
||||||
});
|
});
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = { folderTrustFeature, folderTrust };
|
const settings: Settings = { folderTrustFeature, folderTrust };
|
||||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import yargs from 'yargs/yargs';
|
|||||||
import { hideBin } from 'yargs/helpers';
|
import { hideBin } from 'yargs/helpers';
|
||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
import { mcpCommand } from '../commands/mcp.js';
|
import { mcpCommand } from '../commands/mcp.js';
|
||||||
|
import { extensionsCommand } from '../commands/extensions.js';
|
||||||
import {
|
import {
|
||||||
Config,
|
Config,
|
||||||
loadServerHierarchicalMemory,
|
loadServerHierarchicalMemory,
|
||||||
@@ -76,7 +77,7 @@ export interface CliArgs {
|
|||||||
screenReader: boolean | undefined;
|
screenReader: boolean | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function parseArguments(): Promise<CliArgs> {
|
export async function parseArguments(settings: Settings): Promise<CliArgs> {
|
||||||
const yargsInstance = yargs(hideBin(process.argv))
|
const yargsInstance = yargs(hideBin(process.argv))
|
||||||
.locale('en')
|
.locale('en')
|
||||||
.scriptName('gemini')
|
.scriptName('gemini')
|
||||||
@@ -252,7 +253,13 @@ export async function parseArguments(): Promise<CliArgs> {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
// Register MCP subcommands
|
// 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
|
.version(await getCliVersion()) // This will enable the --version flag based on package.json
|
||||||
.alias('v', 'version')
|
.alias('v', 'version')
|
||||||
.help()
|
.help()
|
||||||
@@ -265,7 +272,10 @@ export async function parseArguments(): Promise<CliArgs> {
|
|||||||
|
|
||||||
// Handle case where MCP subcommands are executed - they should exit the process
|
// Handle case where MCP subcommands are executed - they should exit the process
|
||||||
// and not return to main CLI logic
|
// 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
|
// MCP commands handle their own execution and process exit
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,17 @@ import * as os from 'os';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import {
|
import {
|
||||||
EXTENSIONS_CONFIG_FILENAME,
|
EXTENSIONS_CONFIG_FILENAME,
|
||||||
|
INSTALL_METADATA_FILENAME,
|
||||||
annotateActiveExtensions,
|
annotateActiveExtensions,
|
||||||
|
installExtension,
|
||||||
loadExtensions,
|
loadExtensions,
|
||||||
} from './extension.js';
|
} 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) => {
|
vi.mock('os', async (importOriginal) => {
|
||||||
const os = await importOriginal<typeof import('os')>();
|
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');
|
const EXTENSIONS_DIRECTORY_NAME = path.join('.gemini', 'extensions');
|
||||||
|
|
||||||
describe('loadExtensions', () => {
|
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(
|
function createExtension(
|
||||||
extensionsDir: string,
|
extensionsDir: string,
|
||||||
name: string,
|
name: string,
|
||||||
version: string,
|
version: string,
|
||||||
addContextFile = false,
|
addContextFile = false,
|
||||||
contextFileName?: string,
|
contextFileName?: string,
|
||||||
): void {
|
): string {
|
||||||
const extDir = path.join(extensionsDir, name);
|
const extDir = path.join(extensionsDir, name);
|
||||||
fs.mkdirSync(extDir);
|
fs.mkdirSync(extDir, { recursive: true });
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
path.join(extDir, EXTENSIONS_CONFIG_FILENAME),
|
path.join(extDir, EXTENSIONS_CONFIG_FILENAME),
|
||||||
JSON.stringify({ name, version, contextFileName }),
|
JSON.stringify({ name, version, contextFileName }),
|
||||||
@@ -184,4 +301,5 @@ function createExtension(
|
|||||||
if (contextFileName) {
|
if (contextFileName) {
|
||||||
fs.writeFileSync(path.join(extDir, contextFileName), 'context');
|
fs.writeFileSync(path.join(extDir, contextFileName), 'context');
|
||||||
}
|
}
|
||||||
|
return extDir;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,13 +12,18 @@ import {
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as os from 'os';
|
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 EXTENSIONS_CONFIG_FILENAME = 'gemini-extension.json';
|
||||||
|
export const INSTALL_METADATA_FILENAME = '.gemini-extension-install.json';
|
||||||
|
|
||||||
export interface Extension {
|
export interface Extension {
|
||||||
path: string;
|
path: string;
|
||||||
config: ExtensionConfig;
|
config: ExtensionConfig;
|
||||||
contextFiles: string[];
|
contextFiles: string[];
|
||||||
|
installMetadata?: ExtensionInstallMetadata | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExtensionConfig {
|
export interface ExtensionConfig {
|
||||||
@@ -29,6 +34,45 @@ export interface ExtensionConfig {
|
|||||||
excludeTools?: string[];
|
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[] {
|
export function loadExtensions(workspaceDir: string): Extension[] {
|
||||||
const allExtensions = [
|
const allExtensions = [
|
||||||
...loadExtensionsFromDir(workspaceDir),
|
...loadExtensionsFromDir(workspaceDir),
|
||||||
@@ -45,7 +89,20 @@ export function loadExtensions(workspaceDir: string): Extension[] {
|
|||||||
return Array.from(uniqueExtensions.values());
|
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 storage = new Storage(dir);
|
||||||
const extensionsDir = storage.getExtensionsDir();
|
const extensionsDir = storage.getExtensionsDir();
|
||||||
if (!fs.existsSync(extensionsDir)) {
|
if (!fs.existsSync(extensionsDir)) {
|
||||||
@@ -64,7 +121,7 @@ function loadExtensionsFromDir(dir: string): Extension[] {
|
|||||||
return extensions;
|
return extensions;
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadExtension(extensionDir: string): Extension | null {
|
export function loadExtension(extensionDir: string): Extension | null {
|
||||||
if (!fs.statSync(extensionDir).isDirectory()) {
|
if (!fs.statSync(extensionDir).isDirectory()) {
|
||||||
console.error(
|
console.error(
|
||||||
`Warning: unexpected file ${extensionDir} in extensions directory.`,
|
`Warning: unexpected file ${extensionDir} in extensions directory.`,
|
||||||
@@ -98,6 +155,7 @@ function loadExtension(extensionDir: string): Extension | null {
|
|||||||
path: extensionDir,
|
path: extensionDir,
|
||||||
config,
|
config,
|
||||||
contextFiles,
|
contextFiles,
|
||||||
|
installMetadata: loadInstallMetadata(extensionDir),
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(
|
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[] {
|
function getContextFileNames(config: ExtensionConfig): string[] {
|
||||||
if (!config.contextFileName) {
|
if (!config.contextFileName) {
|
||||||
return ['GEMINI.md'];
|
return ['GEMINI.md'];
|
||||||
@@ -171,3 +242,99 @@ export function annotateActiveExtensions(
|
|||||||
|
|
||||||
return annotatedExtensions;
|
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.',
|
description: 'Show line numbers in the chat.',
|
||||||
showInDialog: true,
|
showInDialog: true,
|
||||||
},
|
},
|
||||||
|
extensionManagement: {
|
||||||
|
type: 'boolean',
|
||||||
|
label: 'Extension Management',
|
||||||
|
category: 'Feature Flag',
|
||||||
|
requiresRestart: true,
|
||||||
|
default: false,
|
||||||
|
description: 'Enable extension management features.',
|
||||||
|
showInDialog: false,
|
||||||
|
},
|
||||||
skipNextSpeakerCheck: {
|
skipNextSpeakerCheck: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
label: 'Skip Next Speaker Check',
|
label: 'Skip Next Speaker Check',
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ export async function main() {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const argv = await parseArguments();
|
const argv = await parseArguments(settings.merged);
|
||||||
const extensions = loadExtensions(workspaceRoot);
|
const extensions = loadExtensions(workspaceRoot);
|
||||||
const config = await loadCliConfig(
|
const config = await loadCliConfig(
|
||||||
settings.merged,
|
settings.merged,
|
||||||
|
|||||||
@@ -205,6 +205,7 @@ export interface ConfigParameters {
|
|||||||
useRipgrep?: boolean;
|
useRipgrep?: boolean;
|
||||||
shouldUseNodePtyShell?: boolean;
|
shouldUseNodePtyShell?: boolean;
|
||||||
skipNextSpeakerCheck?: boolean;
|
skipNextSpeakerCheck?: boolean;
|
||||||
|
extensionManagement?: boolean;
|
||||||
enablePromptCompletion?: boolean;
|
enablePromptCompletion?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,6 +276,7 @@ export class Config {
|
|||||||
private readonly useRipgrep: boolean;
|
private readonly useRipgrep: boolean;
|
||||||
private readonly shouldUseNodePtyShell: boolean;
|
private readonly shouldUseNodePtyShell: boolean;
|
||||||
private readonly skipNextSpeakerCheck: boolean;
|
private readonly skipNextSpeakerCheck: boolean;
|
||||||
|
private readonly extensionManagement: boolean;
|
||||||
private readonly enablePromptCompletion: boolean = false;
|
private readonly enablePromptCompletion: boolean = false;
|
||||||
private initialized: boolean = false;
|
private initialized: boolean = false;
|
||||||
readonly storage: Storage;
|
readonly storage: Storage;
|
||||||
@@ -349,6 +351,7 @@ export class Config {
|
|||||||
this.useRipgrep = params.useRipgrep ?? false;
|
this.useRipgrep = params.useRipgrep ?? false;
|
||||||
this.shouldUseNodePtyShell = params.shouldUseNodePtyShell ?? false;
|
this.shouldUseNodePtyShell = params.shouldUseNodePtyShell ?? false;
|
||||||
this.skipNextSpeakerCheck = params.skipNextSpeakerCheck ?? false;
|
this.skipNextSpeakerCheck = params.skipNextSpeakerCheck ?? false;
|
||||||
|
this.extensionManagement = params.extensionManagement ?? false;
|
||||||
this.storage = new Storage(this.targetDir);
|
this.storage = new Storage(this.targetDir);
|
||||||
this.enablePromptCompletion = params.enablePromptCompletion ?? false;
|
this.enablePromptCompletion = params.enablePromptCompletion ?? false;
|
||||||
this.fileExclusions = new FileExclusions(this);
|
this.fileExclusions = new FileExclusions(this);
|
||||||
@@ -678,6 +681,10 @@ export class Config {
|
|||||||
return this.listExtensions;
|
return this.listExtensions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getExtensionManagement(): boolean {
|
||||||
|
return this.extensionManagement;
|
||||||
|
}
|
||||||
|
|
||||||
getExtensions(): GeminiCLIExtension[] {
|
getExtensions(): GeminiCLIExtension[] {
|
||||||
return this._extensions;
|
return this._extensions;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user