mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-21 10:34:35 -07:00
Add ExtensionLoader interface, use that on Config object (#12116)
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -48,6 +48,10 @@ import { appEvents } from '../utils/events.js';
|
||||
|
||||
import { isWorkspaceTrusted } from './trustedFolders.js';
|
||||
import { createPolicyEngineConfig } from './policy.js';
|
||||
import { ExtensionManager } from './extension-manager.js';
|
||||
import type { ExtensionLoader } from '@google/gemini-cli-core/src/utils/extensionLoader.js';
|
||||
import { requestConsentNonInteractive } from './extensions/consent.js';
|
||||
import { promptForSetting } from './extensions/extensionSettings.js';
|
||||
|
||||
export interface CliArgs {
|
||||
query: string | undefined;
|
||||
@@ -293,7 +297,7 @@ export async function loadHierarchicalGeminiMemory(
|
||||
debugMode: boolean,
|
||||
fileService: FileDiscoveryService,
|
||||
settings: Settings,
|
||||
extensions: GeminiCLIExtension[],
|
||||
extensionLoader: ExtensionLoader,
|
||||
folderTrust: boolean,
|
||||
memoryImportFormat: 'flat' | 'tree' = 'tree',
|
||||
fileFilteringOptions?: FileFilteringOptions,
|
||||
@@ -319,7 +323,7 @@ export async function loadHierarchicalGeminiMemory(
|
||||
includeDirectoriesToReadGemini,
|
||||
debugMode,
|
||||
fileService,
|
||||
extensions,
|
||||
extensionLoader,
|
||||
folderTrust,
|
||||
memoryImportFormat,
|
||||
fileFilteringOptions,
|
||||
@@ -368,7 +372,6 @@ export function isDebugMode(argv: CliArgs): boolean {
|
||||
|
||||
export async function loadCliConfig(
|
||||
settings: Settings,
|
||||
allExtensions: GeminiCLIExtension[],
|
||||
sessionId: string,
|
||||
argv: CliArgs,
|
||||
cwd: string = process.cwd(),
|
||||
@@ -413,6 +416,15 @@ export async function loadCliConfig(
|
||||
.map(resolvePath)
|
||||
.concat((argv.includeDirectories || []).map(resolvePath));
|
||||
|
||||
const extensionManager = new ExtensionManager({
|
||||
settings,
|
||||
requestConsent: requestConsentNonInteractive,
|
||||
requestSetting: promptForSetting,
|
||||
workspaceDir: cwd,
|
||||
enabledExtensionOverrides: argv.extensions,
|
||||
});
|
||||
extensionManager.loadExtensions();
|
||||
|
||||
// Call the (now wrapper) loadHierarchicalGeminiMemory which calls the server's version
|
||||
const { memoryContent, fileCount, filePaths } =
|
||||
await loadHierarchicalGeminiMemory(
|
||||
@@ -423,13 +435,13 @@ export async function loadCliConfig(
|
||||
debugMode,
|
||||
fileService,
|
||||
settings,
|
||||
allExtensions,
|
||||
extensionManager,
|
||||
trustedFolder,
|
||||
memoryImportFormat,
|
||||
memoryFileFiltering,
|
||||
);
|
||||
|
||||
let mcpServers = mergeMcpServers(settings, allExtensions);
|
||||
let mcpServers = mergeMcpServers(settings, extensionManager.getExtensions());
|
||||
const question = argv.promptInteractive || argv.prompt || '';
|
||||
|
||||
// Determine approval mode with backward compatibility
|
||||
@@ -540,7 +552,7 @@ export async function loadCliConfig(
|
||||
|
||||
const excludeTools = mergeExcludeTools(
|
||||
settings,
|
||||
allExtensions,
|
||||
extensionManager.getExtensions(),
|
||||
extraExcludes.length > 0 ? extraExcludes : undefined,
|
||||
);
|
||||
const blockedMcpServers: Array<{ name: string; extensionName: string }> = [];
|
||||
@@ -636,7 +648,7 @@ export async function loadCliConfig(
|
||||
experimentalZedIntegration: argv.experimentalAcp || false,
|
||||
listExtensions: argv.listExtensions || false,
|
||||
enabledExtensions: argv.extensions,
|
||||
extensions: allExtensions,
|
||||
extensionLoader: extensionManager,
|
||||
blockedMcpServers,
|
||||
noBrowser: !!process.env['NO_BROWSER'],
|
||||
summarizeToolOutput: settings.model?.summarizeToolOutput,
|
||||
|
||||
@@ -9,7 +9,7 @@ import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import chalk from 'chalk';
|
||||
import { ExtensionEnablementManager } from './extensions/extensionEnablement.js';
|
||||
import { type LoadedSettings, SettingScope } from './settings.js';
|
||||
import { type Settings, SettingScope } from './settings.js';
|
||||
import { createHash, randomUUID } from 'node:crypto';
|
||||
import { loadInstallMetadata, type ExtensionConfig } from './extension.js';
|
||||
import { isWorkspaceTrusted } from './trustedFolders.js';
|
||||
@@ -50,33 +50,45 @@ import {
|
||||
maybePromptForSettings,
|
||||
type ExtensionSetting,
|
||||
} from './extensions/extensionSettings.js';
|
||||
import type {
|
||||
ExtensionEvents,
|
||||
ExtensionLoader,
|
||||
} from '@google/gemini-cli-core/src/utils/extensionLoader.js';
|
||||
import { EventEmitter } from 'node:events';
|
||||
|
||||
interface ExtensionManagerParams {
|
||||
enabledExtensionOverrides?: string[];
|
||||
loadedSettings: LoadedSettings;
|
||||
settings: Settings;
|
||||
requestConsent: (consent: string) => Promise<boolean>;
|
||||
requestSetting: ((setting: ExtensionSetting) => Promise<string>) | null;
|
||||
workspaceDir: string;
|
||||
}
|
||||
|
||||
export class ExtensionManager {
|
||||
/**
|
||||
* Actual implementation of an ExtensionLoader.
|
||||
*
|
||||
* You must call `loadExtensions` prior to calling other methods on this class.
|
||||
*/
|
||||
export class ExtensionManager implements ExtensionLoader {
|
||||
private extensionEnablementManager: ExtensionEnablementManager;
|
||||
private loadedSettings: LoadedSettings;
|
||||
private settings: Settings;
|
||||
private requestConsent: (consent: string) => Promise<boolean>;
|
||||
private requestSetting:
|
||||
| ((setting: ExtensionSetting) => Promise<string>)
|
||||
| null;
|
||||
| undefined;
|
||||
private telemetryConfig: Config;
|
||||
private workspaceDir: string;
|
||||
private loadedExtensions: GeminiCLIExtension[] | undefined;
|
||||
private eventEmitter: EventEmitter<ExtensionEvents>;
|
||||
|
||||
constructor(options: ExtensionManagerParams) {
|
||||
this.workspaceDir = options.workspaceDir;
|
||||
this.extensionEnablementManager = new ExtensionEnablementManager(
|
||||
options.enabledExtensionOverrides,
|
||||
);
|
||||
this.loadedSettings = options.loadedSettings;
|
||||
this.settings = options.settings;
|
||||
this.telemetryConfig = new Config({
|
||||
telemetry: options.loadedSettings.merged.telemetry,
|
||||
telemetry: options.settings.telemetry,
|
||||
interactive: false,
|
||||
sessionId: randomUUID(),
|
||||
targetDir: options.workspaceDir,
|
||||
@@ -85,19 +97,45 @@ export class ExtensionManager {
|
||||
debugMode: false,
|
||||
});
|
||||
this.requestConsent = options.requestConsent;
|
||||
this.requestSetting = options.requestSetting;
|
||||
this.requestSetting = options.requestSetting ?? undefined;
|
||||
this.eventEmitter = new EventEmitter();
|
||||
}
|
||||
|
||||
setRequestConsent(
|
||||
requestConsent: (consent: string) => Promise<boolean>,
|
||||
): void {
|
||||
this.requestConsent = requestConsent;
|
||||
}
|
||||
|
||||
setRequestSetting(
|
||||
requestSetting?: (setting: ExtensionSetting) => Promise<string>,
|
||||
): void {
|
||||
this.requestSetting = requestSetting;
|
||||
}
|
||||
|
||||
getExtensions(): GeminiCLIExtension[] {
|
||||
if (!this.loadedExtensions) {
|
||||
throw new Error(
|
||||
'Extensions not yet loaded, must call `loadExtensions` first',
|
||||
);
|
||||
}
|
||||
return this.loadedExtensions!;
|
||||
}
|
||||
|
||||
extensionEvents(): EventEmitter<ExtensionEvents> {
|
||||
return this.eventEmitter;
|
||||
}
|
||||
|
||||
async installOrUpdateExtension(
|
||||
installMetadata: ExtensionInstallMetadata,
|
||||
previousExtensionConfig?: ExtensionConfig,
|
||||
): Promise<string> {
|
||||
): Promise<GeminiCLIExtension> {
|
||||
const isUpdate = !!previousExtensionConfig;
|
||||
let newExtensionConfig: ExtensionConfig | null = null;
|
||||
let localSourcePath: string | undefined;
|
||||
let extension: GeminiCLIExtension;
|
||||
try {
|
||||
const settings = this.loadedSettings.merged;
|
||||
if (!isWorkspaceTrusted(settings).isTrusted) {
|
||||
if (!isWorkspaceTrusted(this.settings).isTrusted) {
|
||||
throw new Error(
|
||||
`Could not install extension from untrusted folder at ${installMetadata.source}`,
|
||||
);
|
||||
@@ -187,17 +225,17 @@ export class ExtensionManager {
|
||||
}
|
||||
|
||||
const newExtensionName = newExtensionConfig.name;
|
||||
if (!isUpdate) {
|
||||
const installedExtensions = this.loadExtensions();
|
||||
if (
|
||||
installedExtensions.some(
|
||||
(installed) => installed.name === newExtensionName,
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
`Extension "${newExtensionName}" is already installed. Please uninstall it first.`,
|
||||
);
|
||||
}
|
||||
const previous = this.getExtensions().find(
|
||||
(installed) => installed.name === newExtensionName,
|
||||
);
|
||||
if (isUpdate && !previous) {
|
||||
throw new Error(
|
||||
`Extension "${newExtensionName}" was not already installed, cannot update it.`,
|
||||
);
|
||||
} else if (!isUpdate && previous) {
|
||||
throw new Error(
|
||||
`Extension "${newExtensionName}" is already installed. Please uninstall it first.`,
|
||||
);
|
||||
}
|
||||
|
||||
await maybeRequestConsentOrFail(
|
||||
@@ -245,39 +283,43 @@ export class ExtensionManager {
|
||||
INSTALL_METADATA_FILENAME,
|
||||
);
|
||||
await fs.promises.writeFile(metadataPath, metadataString);
|
||||
|
||||
// TODO: Gracefully handle this call failing, we should back up the old
|
||||
// extension prior to overwriting it and then restore it.
|
||||
extension = this.loadExtension(destinationPath)!;
|
||||
if (isUpdate) {
|
||||
logExtensionUpdateEvent(
|
||||
this.telemetryConfig,
|
||||
new ExtensionUpdateEvent(
|
||||
hashValue(newExtensionConfig.name),
|
||||
getExtensionId(newExtensionConfig, installMetadata),
|
||||
newExtensionConfig.version,
|
||||
previousExtensionConfig.version,
|
||||
installMetadata.type,
|
||||
'success',
|
||||
),
|
||||
);
|
||||
this.eventEmitter.emit('extensionUpdated', { extension });
|
||||
} else {
|
||||
logExtensionInstallEvent(
|
||||
this.telemetryConfig,
|
||||
new ExtensionInstallEvent(
|
||||
hashValue(newExtensionConfig.name),
|
||||
getExtensionId(newExtensionConfig, installMetadata),
|
||||
newExtensionConfig.version,
|
||||
installMetadata.type,
|
||||
'success',
|
||||
),
|
||||
);
|
||||
this.eventEmitter.emit('extensionInstalled', { extension });
|
||||
this.enableExtension(newExtensionConfig.name, SettingScope.User);
|
||||
}
|
||||
} finally {
|
||||
if (tempDir) {
|
||||
await fs.promises.rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
if (isUpdate) {
|
||||
logExtensionUpdateEvent(
|
||||
this.telemetryConfig,
|
||||
new ExtensionUpdateEvent(
|
||||
hashValue(newExtensionConfig.name),
|
||||
getExtensionId(newExtensionConfig, installMetadata),
|
||||
newExtensionConfig.version,
|
||||
previousExtensionConfig.version,
|
||||
installMetadata.type,
|
||||
'success',
|
||||
),
|
||||
);
|
||||
} else {
|
||||
logExtensionInstallEvent(
|
||||
this.telemetryConfig,
|
||||
new ExtensionInstallEvent(
|
||||
hashValue(newExtensionConfig.name),
|
||||
getExtensionId(newExtensionConfig, installMetadata),
|
||||
newExtensionConfig.version,
|
||||
installMetadata.type,
|
||||
'success',
|
||||
),
|
||||
);
|
||||
this.enableExtension(newExtensionConfig.name, SettingScope.User);
|
||||
}
|
||||
|
||||
return newExtensionConfig!.name;
|
||||
return extension;
|
||||
} catch (error) {
|
||||
// Attempt to load config from the source path even if installation fails
|
||||
// to get the name and version for logging.
|
||||
@@ -324,7 +366,7 @@ export class ExtensionManager {
|
||||
extensionIdentifier: string,
|
||||
isUpdate: boolean,
|
||||
): Promise<void> {
|
||||
const installedExtensions = this.loadExtensions();
|
||||
const installedExtensions = this.getExtensions();
|
||||
const extension = installedExtensions.find(
|
||||
(installed) =>
|
||||
installed.name.toLowerCase() === extensionIdentifier.toLowerCase() ||
|
||||
@@ -334,6 +376,7 @@ export class ExtensionManager {
|
||||
if (!extension) {
|
||||
throw new Error(`Extension not found.`);
|
||||
}
|
||||
this.unloadExtension(extension);
|
||||
const storage = new ExtensionStorage(extension.name);
|
||||
|
||||
await fs.promises.rm(storage.getExtensionDir(), {
|
||||
@@ -355,36 +398,28 @@ export class ExtensionManager {
|
||||
'success',
|
||||
),
|
||||
);
|
||||
this.eventEmitter.emit('extensionUninstalled', { extension });
|
||||
}
|
||||
|
||||
loadExtensions(): GeminiCLIExtension[] {
|
||||
const extensionsDir = ExtensionStorage.getUserExtensionsDir();
|
||||
if (!fs.existsSync(extensionsDir)) {
|
||||
return [];
|
||||
if (this.loadedExtensions) {
|
||||
throw new Error('Extensions already loaded, only load extensions once.');
|
||||
}
|
||||
const extensionsDir = ExtensionStorage.getUserExtensionsDir();
|
||||
this.loadedExtensions = [];
|
||||
if (!fs.existsSync(extensionsDir)) {
|
||||
return this.loadedExtensions;
|
||||
}
|
||||
|
||||
const extensions: GeminiCLIExtension[] = [];
|
||||
for (const subdir of fs.readdirSync(extensionsDir)) {
|
||||
const extensionDir = path.join(extensionsDir, subdir);
|
||||
|
||||
const extension = this.loadExtension(extensionDir);
|
||||
if (extension != null) {
|
||||
extensions.push(extension);
|
||||
}
|
||||
this.loadExtension(extensionDir);
|
||||
}
|
||||
|
||||
const uniqueExtensions = new Map<string, GeminiCLIExtension>();
|
||||
|
||||
for (const extension of extensions) {
|
||||
if (!uniqueExtensions.has(extension.name)) {
|
||||
uniqueExtensions.set(extension.name, extension);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(uniqueExtensions.values());
|
||||
return this.loadedExtensions;
|
||||
}
|
||||
|
||||
loadExtension(extensionDir: string): GeminiCLIExtension | null {
|
||||
private loadExtension(extensionDir: string): GeminiCLIExtension | null {
|
||||
this.loadedExtensions ??= [];
|
||||
if (!fs.statSync(extensionDir).isDirectory()) {
|
||||
return null;
|
||||
}
|
||||
@@ -398,6 +433,13 @@ export class ExtensionManager {
|
||||
|
||||
try {
|
||||
let config = this.loadExtensionConfig(effectiveExtensionPath);
|
||||
if (
|
||||
this.getExtensions().find((extension) => extension.name === config.name)
|
||||
) {
|
||||
throw new Error(
|
||||
`Extension with name ${config.name} already was loaded.`,
|
||||
);
|
||||
}
|
||||
|
||||
const customEnv = getEnvContents(new ExtensionStorage(config.name));
|
||||
config = resolveEnvVarsInObject(config, customEnv);
|
||||
@@ -417,7 +459,7 @@ export class ExtensionManager {
|
||||
)
|
||||
.filter((contextFilePath) => fs.existsSync(contextFilePath));
|
||||
|
||||
return {
|
||||
const extension = {
|
||||
name: config.name,
|
||||
version: config.version,
|
||||
path: effectiveExtensionPath,
|
||||
@@ -431,6 +473,9 @@ export class ExtensionManager {
|
||||
),
|
||||
id: getExtensionId(config, installMetadata),
|
||||
};
|
||||
this.eventEmitter.emit('extensionLoaded', { extension });
|
||||
this.getExtensions().push(extension);
|
||||
return extension;
|
||||
} catch (e) {
|
||||
debugLogger.error(
|
||||
`Warning: Skipping extension in ${effectiveExtensionPath}: ${getErrorMessage(
|
||||
@@ -441,24 +486,11 @@ export class ExtensionManager {
|
||||
}
|
||||
}
|
||||
|
||||
loadExtensionByName(name: string): GeminiCLIExtension | null {
|
||||
const userExtensionsDir = ExtensionStorage.getUserExtensionsDir();
|
||||
if (!fs.existsSync(userExtensionsDir)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const subdir of fs.readdirSync(userExtensionsDir)) {
|
||||
const extensionDir = path.join(userExtensionsDir, subdir);
|
||||
if (!fs.statSync(extensionDir).isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
const extension = this.loadExtension(extensionDir);
|
||||
if (extension && extension.name.toLowerCase() === name.toLowerCase()) {
|
||||
return extension;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
private unloadExtension(extension: GeminiCLIExtension) {
|
||||
this.loadedExtensions = this.getExtensions().filter(
|
||||
(entry) => extension !== entry,
|
||||
);
|
||||
this.eventEmitter.emit('extensionUnloaded', { extension });
|
||||
}
|
||||
|
||||
loadExtensionConfig(extensionDir: string): ExtensionConfig {
|
||||
@@ -548,7 +580,9 @@ export class ExtensionManager {
|
||||
) {
|
||||
throw new Error('System and SystemDefaults scopes are not supported.');
|
||||
}
|
||||
const extension = this.loadExtensionByName(name);
|
||||
const extension = this.getExtensions().find(
|
||||
(extension) => extension.name === name,
|
||||
);
|
||||
if (!extension) {
|
||||
throw new Error(`Extension with name ${name} does not exist.`);
|
||||
}
|
||||
@@ -560,6 +594,8 @@ export class ExtensionManager {
|
||||
this.telemetryConfig,
|
||||
new ExtensionDisableEvent(hashValue(name), extension.id, scope),
|
||||
);
|
||||
extension.isActive = false;
|
||||
this.eventEmitter.emit('extensionDisabled', { extension });
|
||||
}
|
||||
|
||||
enableExtension(name: string, scope: SettingScope) {
|
||||
@@ -569,7 +605,9 @@ export class ExtensionManager {
|
||||
) {
|
||||
throw new Error('System and SystemDefaults scopes are not supported.');
|
||||
}
|
||||
const extension = this.loadExtensionByName(name);
|
||||
const extension = this.getExtensions().find(
|
||||
(extension) => extension.name === name,
|
||||
);
|
||||
if (!extension) {
|
||||
throw new Error(`Extension with name ${name} does not exist.`);
|
||||
}
|
||||
@@ -580,6 +618,8 @@ export class ExtensionManager {
|
||||
this.telemetryConfig,
|
||||
new ExtensionEnableEvent(hashValue(name), extension.id, scope),
|
||||
);
|
||||
extension.isActive = true;
|
||||
this.eventEmitter.emit('extensionEnabled', { extension });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -140,7 +140,7 @@ describe('extension tests', () => {
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
requestConsent: mockRequestConsent,
|
||||
requestSetting: mockPromptForSettings,
|
||||
loadedSettings: loadSettings(tempWorkspaceDir),
|
||||
settings: loadSettings(tempWorkspaceDir).merged,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -220,11 +220,12 @@ describe('extension tests', () => {
|
||||
name: 'enabled-extension',
|
||||
version: '2.0.0',
|
||||
});
|
||||
extensionManager.loadExtensions();
|
||||
extensionManager.disableExtension(
|
||||
'disabled-extension',
|
||||
SettingScope.User,
|
||||
);
|
||||
const extensions = extensionManager.loadExtensions();
|
||||
const extensions = extensionManager.getExtensions();
|
||||
expect(extensions).toHaveLength(2);
|
||||
expect(extensions[0].name).toBe('disabled-extension');
|
||||
expect(extensions[0].isActive).toBe(false);
|
||||
@@ -265,13 +266,14 @@ describe('extension tests', () => {
|
||||
});
|
||||
fs.writeFileSync(path.join(sourceExtDir, 'context.md'), 'linked context');
|
||||
|
||||
const extensionName = await extensionManager.installOrUpdateExtension({
|
||||
extensionManager.loadExtensions();
|
||||
const extension = await extensionManager.installOrUpdateExtension({
|
||||
source: sourceExtDir,
|
||||
type: 'link',
|
||||
});
|
||||
|
||||
expect(extensionName).toEqual('my-linked-extension');
|
||||
const extensions = extensionManager.loadExtensions();
|
||||
expect(extension.name).toEqual('my-linked-extension');
|
||||
const extensions = extensionManager.getExtensions();
|
||||
expect(extensions).toHaveLength(1);
|
||||
|
||||
const linkedExt = extensions[0];
|
||||
@@ -301,12 +303,13 @@ describe('extension tests', () => {
|
||||
},
|
||||
});
|
||||
|
||||
extensionManager.loadExtensions();
|
||||
await extensionManager.installOrUpdateExtension({
|
||||
source: sourceExtDir,
|
||||
type: 'link',
|
||||
});
|
||||
|
||||
const extensions = extensionManager.loadExtensions();
|
||||
const extensions = extensionManager.getExtensions();
|
||||
expect(extensions).toHaveLength(1);
|
||||
expect(extensions[0].mcpServers?.['test-server'].cwd).toBe(
|
||||
path.join(sourceExtDir, 'server'),
|
||||
@@ -525,15 +528,17 @@ describe('extension tests', () => {
|
||||
const consoleSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
const badExtDir = createExtension({
|
||||
createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
name: 'bad_name',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
const extension = extensionManager.loadExtension(badExtDir);
|
||||
const extension = extensionManager
|
||||
.loadExtensions()
|
||||
.find((e) => e.name === 'bad_name');
|
||||
|
||||
expect(extension).toBeNull();
|
||||
expect(extension).toBeUndefined();
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Invalid extension name: "bad_name"'),
|
||||
);
|
||||
@@ -542,7 +547,7 @@ describe('extension tests', () => {
|
||||
|
||||
describe('id generation', () => {
|
||||
it('should generate id from source for non-github git urls', () => {
|
||||
const extensionDir = createExtension({
|
||||
createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
name: 'my-ext',
|
||||
version: '1.0.0',
|
||||
@@ -552,12 +557,14 @@ describe('extension tests', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const extension = extensionManager.loadExtension(extensionDir);
|
||||
const extension = extensionManager
|
||||
.loadExtensions()
|
||||
.find((e) => e.name === 'my-ext');
|
||||
expect(extension?.id).toBe(hashValue('http://somehost.com/foo/bar'));
|
||||
});
|
||||
|
||||
it('should generate id from owner/repo for github http urls', () => {
|
||||
const extensionDir = createExtension({
|
||||
createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
name: 'my-ext',
|
||||
version: '1.0.0',
|
||||
@@ -567,12 +574,14 @@ describe('extension tests', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const extension = extensionManager.loadExtension(extensionDir);
|
||||
const extension = extensionManager
|
||||
.loadExtensions()
|
||||
.find((e) => e.name === 'my-ext');
|
||||
expect(extension?.id).toBe(hashValue('https://github.com/foo/bar'));
|
||||
});
|
||||
|
||||
it('should generate id from owner/repo for github ssh urls', () => {
|
||||
const extensionDir = createExtension({
|
||||
createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
name: 'my-ext',
|
||||
version: '1.0.0',
|
||||
@@ -582,12 +591,14 @@ describe('extension tests', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const extension = extensionManager.loadExtension(extensionDir);
|
||||
const extension = extensionManager
|
||||
.loadExtensions()
|
||||
.find((e) => e.name === 'my-ext');
|
||||
expect(extension?.id).toBe(hashValue('https://github.com/foo/bar'));
|
||||
});
|
||||
|
||||
it('should generate id from source for github-release extension', () => {
|
||||
const extensionDir = createExtension({
|
||||
createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
name: 'my-ext',
|
||||
version: '1.0.0',
|
||||
@@ -597,12 +608,14 @@ describe('extension tests', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const extension = extensionManager.loadExtension(extensionDir);
|
||||
const extension = extensionManager
|
||||
.loadExtensions()
|
||||
.find((e) => e.name === 'my-ext');
|
||||
expect(extension?.id).toBe(hashValue('https://github.com/foo/bar'));
|
||||
});
|
||||
|
||||
it('should generate id from the original source for local extension', () => {
|
||||
const extensionDir = createExtension({
|
||||
createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
name: 'local-ext-name',
|
||||
version: '1.0.0',
|
||||
@@ -612,7 +625,9 @@ describe('extension tests', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const extension = extensionManager.loadExtension(extensionDir);
|
||||
const extension = extensionManager
|
||||
.loadExtensions()
|
||||
.find((e) => e.name === 'local-ext-name');
|
||||
expect(extension?.id).toBe(hashValue('/some/path'));
|
||||
});
|
||||
|
||||
@@ -623,25 +638,28 @@ describe('extension tests', () => {
|
||||
name: 'link-ext-name',
|
||||
version: '1.0.0',
|
||||
});
|
||||
const extensionName = await extensionManager.installOrUpdateExtension({
|
||||
extensionManager.loadExtensions();
|
||||
await extensionManager.installOrUpdateExtension({
|
||||
type: 'link',
|
||||
source: actualExtensionDir,
|
||||
});
|
||||
|
||||
const extension = extensionManager.loadExtension(
|
||||
new ExtensionStorage(extensionName).getExtensionDir(),
|
||||
);
|
||||
const extension = extensionManager
|
||||
.getExtensions()
|
||||
.find((e) => e.name === 'link-ext-name');
|
||||
expect(extension?.id).toBe(hashValue(actualExtensionDir));
|
||||
});
|
||||
|
||||
it('should generate id from name for extension with no install metadata', () => {
|
||||
const extensionDir = createExtension({
|
||||
createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
name: 'no-meta-name',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
const extension = extensionManager.loadExtension(extensionDir);
|
||||
const extension = extensionManager
|
||||
.loadExtensions()
|
||||
.find((e) => e.name === 'no-meta-name');
|
||||
expect(extension?.id).toBe(hashValue('no-meta-name'));
|
||||
});
|
||||
});
|
||||
@@ -657,6 +675,7 @@ describe('extension tests', () => {
|
||||
const targetExtDir = path.join(userExtensionsDir, 'my-local-extension');
|
||||
const metadataPath = path.join(targetExtDir, INSTALL_METADATA_FILENAME);
|
||||
|
||||
extensionManager.loadExtensions();
|
||||
await extensionManager.installOrUpdateExtension({
|
||||
source: sourceExtDir,
|
||||
type: 'local',
|
||||
@@ -678,6 +697,7 @@ describe('extension tests', () => {
|
||||
name: 'my-local-extension',
|
||||
version: '1.0.0',
|
||||
});
|
||||
extensionManager.loadExtensions();
|
||||
await extensionManager.installOrUpdateExtension({
|
||||
source: sourceExtDir,
|
||||
type: 'local',
|
||||
@@ -771,6 +791,7 @@ describe('extension tests', () => {
|
||||
type: 'github-release',
|
||||
});
|
||||
|
||||
extensionManager.loadExtensions();
|
||||
await extensionManager.installOrUpdateExtension({
|
||||
source: gitUrl,
|
||||
type: 'git',
|
||||
@@ -795,6 +816,7 @@ describe('extension tests', () => {
|
||||
const metadataPath = path.join(targetExtDir, INSTALL_METADATA_FILENAME);
|
||||
const configPath = path.join(targetExtDir, EXTENSIONS_CONFIG_FILENAME);
|
||||
|
||||
extensionManager.loadExtensions();
|
||||
await extensionManager.installOrUpdateExtension({
|
||||
source: sourceExtDir,
|
||||
type: 'link',
|
||||
@@ -824,6 +846,7 @@ describe('extension tests', () => {
|
||||
name: 'my-local-extension',
|
||||
version: '1.1.0',
|
||||
});
|
||||
extensionManager.loadExtensions();
|
||||
if (isUpdate) {
|
||||
await extensionManager.installOrUpdateExtension({
|
||||
source: sourceExtDir,
|
||||
@@ -897,12 +920,15 @@ describe('extension tests', () => {
|
||||
},
|
||||
});
|
||||
|
||||
extensionManager.loadExtensions();
|
||||
await expect(
|
||||
extensionManager.installOrUpdateExtension({
|
||||
source: sourceExtDir,
|
||||
type: 'local',
|
||||
}),
|
||||
).resolves.toBe('my-local-extension');
|
||||
).resolves.toMatchObject({
|
||||
name: 'my-local-extension',
|
||||
});
|
||||
|
||||
expect(mockRequestConsent).toHaveBeenCalledWith(
|
||||
`Installing extension "my-local-extension".
|
||||
@@ -926,12 +952,13 @@ This extension will run the following MCP servers:
|
||||
},
|
||||
});
|
||||
|
||||
extensionManager.loadExtensions();
|
||||
await expect(
|
||||
extensionManager.installOrUpdateExtension({
|
||||
source: sourceExtDir,
|
||||
type: 'local',
|
||||
}),
|
||||
).resolves.toBe('my-local-extension');
|
||||
).resolves.toMatchObject({ name: 'my-local-extension' });
|
||||
});
|
||||
|
||||
it('should cancel installation if user declines prompt for local extension with mcp servers', async () => {
|
||||
@@ -947,6 +974,7 @@ This extension will run the following MCP servers:
|
||||
},
|
||||
});
|
||||
mockRequestConsent.mockResolvedValue(false);
|
||||
extensionManager.loadExtensions();
|
||||
await expect(
|
||||
extensionManager.installOrUpdateExtension({
|
||||
source: sourceExtDir,
|
||||
@@ -964,6 +992,7 @@ This extension will run the following MCP servers:
|
||||
const targetExtDir = path.join(userExtensionsDir, 'my-local-extension');
|
||||
const metadataPath = path.join(targetExtDir, INSTALL_METADATA_FILENAME);
|
||||
|
||||
extensionManager.loadExtensions();
|
||||
await extensionManager.installOrUpdateExtension({
|
||||
source: sourceExtDir,
|
||||
type: 'local',
|
||||
@@ -994,6 +1023,7 @@ This extension will run the following MCP servers:
|
||||
},
|
||||
});
|
||||
|
||||
extensionManager.loadExtensions();
|
||||
// Install it with hard coded consent first.
|
||||
await extensionManager.installOrUpdateExtension({
|
||||
source: sourceExtDir,
|
||||
@@ -1008,7 +1038,7 @@ This extension will run the following MCP servers:
|
||||
// Provide its own existing config as the previous config.
|
||||
await extensionManager.loadExtensionConfig(sourceExtDir),
|
||||
),
|
||||
).resolves.toBe('my-local-extension');
|
||||
).resolves.toMatchObject({ name: 'my-local-extension' });
|
||||
|
||||
// Still only called once
|
||||
expect(mockRequestConsent).toHaveBeenCalledOnce();
|
||||
@@ -1028,6 +1058,7 @@ This extension will run the following MCP servers:
|
||||
],
|
||||
});
|
||||
|
||||
extensionManager.loadExtensions();
|
||||
await extensionManager.installOrUpdateExtension({
|
||||
source: sourceExtDir,
|
||||
type: 'local',
|
||||
@@ -1054,9 +1085,10 @@ This extension will run the following MCP servers:
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
requestConsent: mockRequestConsent,
|
||||
requestSetting: null,
|
||||
loadedSettings: loadSettings(tempWorkspaceDir),
|
||||
settings: loadSettings(tempWorkspaceDir).merged,
|
||||
});
|
||||
|
||||
extensionManager.loadExtensions();
|
||||
await extensionManager.installOrUpdateExtension({
|
||||
source: sourceExtDir,
|
||||
type: 'local',
|
||||
@@ -1079,6 +1111,7 @@ This extension will run the following MCP servers:
|
||||
});
|
||||
|
||||
mockPromptForSettings.mockResolvedValueOnce('old-api-key');
|
||||
extensionManager.loadExtensions();
|
||||
// Install it so it exists in the userExtensionsDir
|
||||
await extensionManager.installOrUpdateExtension({
|
||||
source: oldSourceExtDir,
|
||||
@@ -1148,6 +1181,7 @@ This extension will run the following MCP servers:
|
||||
},
|
||||
],
|
||||
});
|
||||
extensionManager.loadExtensions();
|
||||
await extensionManager.installOrUpdateExtension({
|
||||
source: oldSourceExtDir,
|
||||
type: 'local',
|
||||
@@ -1239,6 +1273,7 @@ This extension will run the following MCP servers:
|
||||
join(tempDir, extensionName),
|
||||
);
|
||||
|
||||
extensionManager.loadExtensions();
|
||||
await extensionManager.installOrUpdateExtension({
|
||||
source: gitUrl,
|
||||
type: 'github-release',
|
||||
@@ -1263,6 +1298,7 @@ This extension will run the following MCP servers:
|
||||
type: 'github-release',
|
||||
});
|
||||
|
||||
extensionManager.loadExtensions();
|
||||
await extensionManager.installOrUpdateExtension(
|
||||
{ source: gitUrl, type: 'github-release' }, // Use github-release to force consent
|
||||
);
|
||||
@@ -1293,6 +1329,7 @@ This extension will run the following MCP servers:
|
||||
});
|
||||
mockRequestConsent.mockResolvedValue(false);
|
||||
|
||||
extensionManager.loadExtensions();
|
||||
await expect(
|
||||
extensionManager.installOrUpdateExtension({
|
||||
source: gitUrl,
|
||||
@@ -1317,6 +1354,7 @@ This extension will run the following MCP servers:
|
||||
type: 'github-release',
|
||||
});
|
||||
|
||||
extensionManager.loadExtensions();
|
||||
await extensionManager.installOrUpdateExtension({
|
||||
source: gitUrl,
|
||||
type: 'git',
|
||||
@@ -1347,6 +1385,7 @@ This extension will run the following MCP servers:
|
||||
type: 'github-release',
|
||||
});
|
||||
|
||||
extensionManager.loadExtensions();
|
||||
await extensionManager.installOrUpdateExtension(
|
||||
{ source: gitUrl, type: 'github-release' }, // Note the type
|
||||
);
|
||||
@@ -1369,6 +1408,7 @@ This extension will run the following MCP servers:
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
extensionManager.loadExtensions();
|
||||
await extensionManager.uninstallExtension('my-local-extension', false);
|
||||
|
||||
expect(fs.existsSync(sourceExtDir)).toBe(false);
|
||||
@@ -1386,14 +1426,16 @@ This extension will run the following MCP servers:
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
extensionManager.loadExtensions();
|
||||
await extensionManager.uninstallExtension('my-local-extension', false);
|
||||
|
||||
expect(fs.existsSync(sourceExtDir)).toBe(false);
|
||||
expect(extensionManager.loadExtensions()).toHaveLength(1);
|
||||
expect(extensionManager.getExtensions()).toHaveLength(1);
|
||||
expect(fs.existsSync(otherExtDir)).toBe(true);
|
||||
});
|
||||
|
||||
it('should throw an error if the extension does not exist', async () => {
|
||||
extensionManager.loadExtensions();
|
||||
await expect(
|
||||
extensionManager.uninstallExtension('nonexistent-extension', false),
|
||||
).rejects.toThrow('Extension not found.');
|
||||
@@ -1411,6 +1453,7 @@ This extension will run the following MCP servers:
|
||||
},
|
||||
});
|
||||
|
||||
extensionManager.loadExtensions();
|
||||
await extensionManager.uninstallExtension(
|
||||
'my-local-extension',
|
||||
isUpdate,
|
||||
@@ -1438,6 +1481,7 @@ This extension will run the following MCP servers:
|
||||
const enablementManager = new ExtensionEnablementManager();
|
||||
enablementManager.enable('test-extension', true, '/some/scope');
|
||||
|
||||
extensionManager.loadExtensions();
|
||||
await extensionManager.uninstallExtension('test-extension', isUpdate);
|
||||
|
||||
const config = enablementManager.readConfig()['test-extension'];
|
||||
@@ -1462,6 +1506,7 @@ This extension will run the following MCP servers:
|
||||
},
|
||||
});
|
||||
|
||||
extensionManager.loadExtensions();
|
||||
await extensionManager.uninstallExtension(gitUrl, false);
|
||||
|
||||
expect(fs.existsSync(sourceExtDir)).toBe(false);
|
||||
@@ -1481,6 +1526,7 @@ This extension will run the following MCP servers:
|
||||
// No installMetadata provided
|
||||
});
|
||||
|
||||
extensionManager.loadExtensions();
|
||||
await expect(
|
||||
extensionManager.uninstallExtension(
|
||||
'https://github.com/google/no-metadata-extension',
|
||||
@@ -1498,6 +1544,7 @@ This extension will run the following MCP servers:
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
extensionManager.loadExtensions();
|
||||
extensionManager.disableExtension('my-extension', SettingScope.User);
|
||||
expect(
|
||||
isEnabled({
|
||||
@@ -1514,6 +1561,7 @@ This extension will run the following MCP servers:
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
extensionManager.loadExtensions();
|
||||
extensionManager.disableExtension('my-extension', SettingScope.Workspace);
|
||||
expect(
|
||||
isEnabled({
|
||||
@@ -1536,6 +1584,7 @@ This extension will run the following MCP servers:
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
extensionManager.loadExtensions();
|
||||
extensionManager.disableExtension('my-extension', SettingScope.User);
|
||||
extensionManager.disableExtension('my-extension', SettingScope.User);
|
||||
expect(
|
||||
@@ -1563,6 +1612,7 @@ This extension will run the following MCP servers:
|
||||
},
|
||||
});
|
||||
|
||||
extensionManager.loadExtensions();
|
||||
extensionManager.disableExtension('ext1', SettingScope.Workspace);
|
||||
|
||||
expect(mockLogExtensionDisable).toHaveBeenCalled();
|
||||
@@ -1580,7 +1630,7 @@ This extension will run the following MCP servers:
|
||||
});
|
||||
|
||||
const getActiveExtensions = (): GeminiCLIExtension[] => {
|
||||
const extensions = extensionManager.loadExtensions();
|
||||
const extensions = extensionManager.getExtensions();
|
||||
return extensions.filter((e) => e.isActive);
|
||||
};
|
||||
|
||||
@@ -1590,6 +1640,7 @@ This extension will run the following MCP servers:
|
||||
name: 'ext1',
|
||||
version: '1.0.0',
|
||||
});
|
||||
extensionManager.loadExtensions();
|
||||
extensionManager.disableExtension('ext1', SettingScope.User);
|
||||
let activeExtensions = getActiveExtensions();
|
||||
expect(activeExtensions).toHaveLength(0);
|
||||
@@ -1606,6 +1657,7 @@ This extension will run the following MCP servers:
|
||||
name: 'ext1',
|
||||
version: '1.0.0',
|
||||
});
|
||||
extensionManager.loadExtensions();
|
||||
extensionManager.disableExtension('ext1', SettingScope.Workspace);
|
||||
let activeExtensions = getActiveExtensions();
|
||||
expect(activeExtensions).toHaveLength(0);
|
||||
@@ -1626,6 +1678,7 @@ This extension will run the following MCP servers:
|
||||
type: 'local',
|
||||
},
|
||||
});
|
||||
extensionManager.loadExtensions();
|
||||
extensionManager.disableExtension('ext1', SettingScope.Workspace);
|
||||
extensionManager.enableExtension('ext1', SettingScope.Workspace);
|
||||
|
||||
|
||||
@@ -170,7 +170,7 @@ describe('git extension helpers', () => {
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
requestConsent: mockRequestConsent,
|
||||
requestSetting: mockPromptForSettings,
|
||||
loadedSettings: loadSettings(tempWorkspaceDir),
|
||||
settings: loadSettings(tempWorkspaceDir).merged,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -157,14 +157,16 @@ export async function checkForExtensionUpdate(
|
||||
): Promise<ExtensionUpdateState> {
|
||||
const installMetadata = extension.installMetadata;
|
||||
if (installMetadata?.type === 'local') {
|
||||
const newExtension = extensionManager.loadExtension(installMetadata.source);
|
||||
if (!newExtension) {
|
||||
const latestConfig = extensionManager.loadExtensionConfig(
|
||||
installMetadata.source,
|
||||
);
|
||||
if (!latestConfig) {
|
||||
debugLogger.error(
|
||||
`Failed to check for update for local extension "${extension.name}". Could not load extension from source path: ${installMetadata.source}`,
|
||||
);
|
||||
return ExtensionUpdateState.ERROR;
|
||||
}
|
||||
if (newExtension.version !== extension.version) {
|
||||
if (latestConfig.version !== extension.version) {
|
||||
return ExtensionUpdateState.UPDATE_AVAILABLE;
|
||||
}
|
||||
return ExtensionUpdateState.UP_TO_DATE;
|
||||
|
||||
@@ -109,7 +109,7 @@ describe('update tests', () => {
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
requestConsent: mockRequestConsent,
|
||||
requestSetting: mockPromptForSettings,
|
||||
loadedSettings: loadSettings(tempWorkspaceDir),
|
||||
settings: loadSettings(tempWorkspaceDir).merged,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -145,7 +145,9 @@ describe('update tests', () => {
|
||||
);
|
||||
});
|
||||
mockGit.getRemotes.mockResolvedValue([{ name: 'origin' }]);
|
||||
const extension = extensionManager.loadExtension(targetExtDir)!;
|
||||
const extension = extensionManager
|
||||
.loadExtensions()
|
||||
.find((e) => e.name === extensionName)!;
|
||||
const updateInfo = await updateExtension(
|
||||
extension,
|
||||
extensionManager,
|
||||
@@ -170,7 +172,7 @@ describe('update tests', () => {
|
||||
|
||||
it('should call setExtensionUpdateState with UPDATING and then UPDATED_NEEDS_RESTART on success', async () => {
|
||||
const extensionName = 'test-extension';
|
||||
const extensionDir = createExtension({
|
||||
createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
name: extensionName,
|
||||
version: '1.0.0',
|
||||
@@ -192,7 +194,10 @@ describe('update tests', () => {
|
||||
mockGit.getRemotes.mockResolvedValue([{ name: 'origin' }]);
|
||||
|
||||
const dispatch = vi.fn();
|
||||
const extension = extensionManager.loadExtension(extensionDir)!;
|
||||
|
||||
const extension = extensionManager
|
||||
.loadExtensions()
|
||||
.find((e) => e.name === extensionName)!;
|
||||
await updateExtension(
|
||||
extension,
|
||||
extensionManager,
|
||||
@@ -218,7 +223,7 @@ describe('update tests', () => {
|
||||
|
||||
it('should call setExtensionUpdateState with ERROR on failure', async () => {
|
||||
const extensionName = 'test-extension';
|
||||
const extensionDir = createExtension({
|
||||
createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
name: extensionName,
|
||||
version: '1.0.0',
|
||||
@@ -232,7 +237,9 @@ describe('update tests', () => {
|
||||
mockGit.getRemotes.mockResolvedValue([{ name: 'origin' }]);
|
||||
|
||||
const dispatch = vi.fn();
|
||||
const extension = extensionManager.loadExtension(extensionDir)!;
|
||||
const extension = extensionManager
|
||||
.loadExtensions()
|
||||
.find((e) => e.name === extensionName)!;
|
||||
await expect(
|
||||
updateExtension(
|
||||
extension,
|
||||
@@ -261,7 +268,7 @@ describe('update tests', () => {
|
||||
|
||||
describe('checkForAllExtensionUpdates', () => {
|
||||
it('should return UpdateAvailable for a git extension with updates', async () => {
|
||||
const extensionDir = createExtension({
|
||||
createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
name: 'test-extension',
|
||||
version: '1.0.0',
|
||||
@@ -270,7 +277,6 @@ describe('update tests', () => {
|
||||
type: 'git',
|
||||
},
|
||||
});
|
||||
const extension = extensionManager.loadExtension(extensionDir)!;
|
||||
|
||||
mockGit.getRemotes.mockResolvedValue([
|
||||
{ name: 'origin', refs: { fetch: 'https://some.git/repo' } },
|
||||
@@ -280,7 +286,7 @@ describe('update tests', () => {
|
||||
|
||||
const dispatch = vi.fn();
|
||||
await checkForAllExtensionUpdates(
|
||||
[extension],
|
||||
extensionManager.loadExtensions(),
|
||||
extensionManager,
|
||||
dispatch,
|
||||
);
|
||||
@@ -294,7 +300,7 @@ describe('update tests', () => {
|
||||
});
|
||||
|
||||
it('should return UpToDate for a git extension with no updates', async () => {
|
||||
const extensionDir = createExtension({
|
||||
createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
name: 'test-extension',
|
||||
version: '1.0.0',
|
||||
@@ -303,7 +309,6 @@ describe('update tests', () => {
|
||||
type: 'git',
|
||||
},
|
||||
});
|
||||
const extension = extensionManager.loadExtension(extensionDir)!;
|
||||
|
||||
mockGit.getRemotes.mockResolvedValue([
|
||||
{ name: 'origin', refs: { fetch: 'https://some.git/repo' } },
|
||||
@@ -313,7 +318,7 @@ describe('update tests', () => {
|
||||
|
||||
const dispatch = vi.fn();
|
||||
await checkForAllExtensionUpdates(
|
||||
[extension],
|
||||
extensionManager.loadExtensions(),
|
||||
extensionManager,
|
||||
dispatch,
|
||||
);
|
||||
@@ -334,16 +339,15 @@ describe('update tests', () => {
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
const installedExtensionDir = createExtension({
|
||||
createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
name: 'local-extension',
|
||||
version: '1.0.0',
|
||||
installMetadata: { source: sourceExtensionDir, type: 'local' },
|
||||
});
|
||||
const extension = extensionManager.loadExtension(installedExtensionDir)!;
|
||||
const dispatch = vi.fn();
|
||||
await checkForAllExtensionUpdates(
|
||||
[extension],
|
||||
extensionManager.loadExtensions(),
|
||||
extensionManager,
|
||||
dispatch,
|
||||
);
|
||||
@@ -360,20 +364,19 @@ describe('update tests', () => {
|
||||
const localExtensionSourcePath = path.join(tempHomeDir, 'local-source');
|
||||
const sourceExtensionDir = createExtension({
|
||||
extensionsDir: localExtensionSourcePath,
|
||||
name: 'my-local-ext',
|
||||
name: 'local-extension',
|
||||
version: '1.1.0',
|
||||
});
|
||||
|
||||
const installedExtensionDir = createExtension({
|
||||
createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
name: 'local-extension',
|
||||
version: '1.0.0',
|
||||
installMetadata: { source: sourceExtensionDir, type: 'local' },
|
||||
});
|
||||
const extension = extensionManager.loadExtension(installedExtensionDir)!;
|
||||
const dispatch = vi.fn();
|
||||
await checkForAllExtensionUpdates(
|
||||
[extension],
|
||||
extensionManager.loadExtensions(),
|
||||
extensionManager,
|
||||
dispatch,
|
||||
);
|
||||
@@ -387,7 +390,7 @@ describe('update tests', () => {
|
||||
});
|
||||
|
||||
it('should return Error when git check fails', async () => {
|
||||
const extensionDir = createExtension({
|
||||
createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
name: 'error-extension',
|
||||
version: '1.0.0',
|
||||
@@ -396,13 +399,12 @@ describe('update tests', () => {
|
||||
type: 'git',
|
||||
},
|
||||
});
|
||||
const extension = extensionManager.loadExtension(extensionDir)!;
|
||||
|
||||
mockGit.getRemotes.mockRejectedValue(new Error('Git error'));
|
||||
|
||||
const dispatch = vi.fn();
|
||||
await checkForAllExtensionUpdates(
|
||||
[extension],
|
||||
extensionManager.loadExtensions(),
|
||||
extensionManager,
|
||||
dispatch,
|
||||
);
|
||||
|
||||
@@ -61,20 +61,20 @@ export async function updateExtension(
|
||||
const previousExtensionConfig = await extensionManager.loadExtensionConfig(
|
||||
extension.path,
|
||||
);
|
||||
await extensionManager.installOrUpdateExtension(
|
||||
installMetadata,
|
||||
previousExtensionConfig,
|
||||
);
|
||||
const updatedExtensionStorage = new ExtensionStorage(extension.name);
|
||||
const updatedExtension = extensionManager.loadExtension(
|
||||
updatedExtensionStorage.getExtensionDir(),
|
||||
);
|
||||
if (!updatedExtension) {
|
||||
let updatedExtension: GeminiCLIExtension;
|
||||
try {
|
||||
updatedExtension = await extensionManager.installOrUpdateExtension(
|
||||
installMetadata,
|
||||
previousExtensionConfig,
|
||||
);
|
||||
} catch (e) {
|
||||
dispatchExtensionStateUpdate({
|
||||
type: 'SET_STATE',
|
||||
payload: { name: extension.name, state: ExtensionUpdateState.ERROR },
|
||||
});
|
||||
throw new Error('Updated extension not found after installation.');
|
||||
throw new Error(
|
||||
`Updated extension not found after installation, got error:\n${e}`,
|
||||
);
|
||||
}
|
||||
const updatedVersion = updatedExtension.version;
|
||||
dispatchExtensionStateUpdate({
|
||||
|
||||
@@ -2433,7 +2433,7 @@ describe('Settings Loading and Merging', () => {
|
||||
const loadedSettings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
const setValueSpy = vi.spyOn(loadedSettings, 'setValue');
|
||||
const extensionManager = new ExtensionManager({
|
||||
loadedSettings,
|
||||
settings: loadedSettings.merged,
|
||||
workspaceDir: MOCK_WORKSPACE_DIR,
|
||||
requestConsent: vi.fn(),
|
||||
requestSetting: vi.fn(),
|
||||
@@ -2506,7 +2506,7 @@ describe('Settings Loading and Merging', () => {
|
||||
const loadedSettings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
const setValueSpy = vi.spyOn(loadedSettings, 'setValue');
|
||||
const extensionManager = new ExtensionManager({
|
||||
loadedSettings,
|
||||
settings: loadedSettings.merged,
|
||||
workspaceDir: MOCK_WORKSPACE_DIR,
|
||||
requestConsent: vi.fn(),
|
||||
requestSetting: vi.fn(),
|
||||
|
||||
Reference in New Issue
Block a user