Fix bug where linked extensions can't be found

This commit is contained in:
Christine Betts
2026-02-25 15:16:22 -05:00
parent eb17d94e91
commit e1f3228d29
2 changed files with 62 additions and 17 deletions
+29 -17
View File
@@ -438,6 +438,19 @@ Would you like to attempt to install via "git clone" instead?`,
extensionIdentifier.toLowerCase(),
);
if (!extension) {
const extensionDir = path.join(
ExtensionStorage.getUserExtensionsDir(),
extensionIdentifier,
);
if (fs.existsSync(extensionDir)) {
await fs.promises.rm(extensionDir, { recursive: true, force: true });
this.extensionEnablementManager.remove(extensionIdentifier);
coreEvents.emitFeedback(
'info',
`Uninstalled broken extension '${extensionIdentifier}'.`,
);
return;
}
throw new Error(`Extension not found.`);
}
await this.unloadExtension(extension);
@@ -515,25 +528,24 @@ Would you like to attempt to install via "git clone" instead?`,
extensionDir: string,
): Promise<GeminiCLIExtension | null> {
this.loadedExtensions ??= [];
if (!fs.statSync(extensionDir).isDirectory()) {
return null;
}
const installMetadata = loadInstallMetadata(extensionDir);
let effectiveExtensionPath = extensionDir;
if (
(installMetadata?.type === 'git' ||
installMetadata?.type === 'github-release') &&
this.settings.security.blockGitExtensions
) {
return null;
}
if (installMetadata?.type === 'link') {
effectiveExtensionPath = installMetadata.source;
}
try {
if (!fs.statSync(extensionDir).isDirectory()) {
return null;
}
const installMetadata = loadInstallMetadata(extensionDir);
if (
(installMetadata?.type === 'git' ||
installMetadata?.type === 'github-release') &&
this.settings.security.blockGitExtensions
) {
return null;
}
if (installMetadata?.type === 'link') {
effectiveExtensionPath = installMetadata.source;
}
let config = await this.loadExtensionConfig(effectiveExtensionPath);
if (
this.getExtensions().find((extension) => extension.name === config.name)
+33
View File
@@ -1769,6 +1769,39 @@ ${INSTALL_WARNING_MESSAGE}`,
});
describe('uninstallExtension', () => {
it('should uninstall a broken extension without crashing', async () => {
const badExtDir = path.join(userExtensionsDir, 'broken-ext');
fs.mkdirSync(badExtDir);
// Malformed JSON to simulate a broken extension
fs.writeFileSync(
path.join(badExtDir, EXTENSIONS_CONFIG_FILENAME),
'{ "name": "broken-ext"',
);
// Attempt to load extensions. The broken one should be skipped.
const consoleErrorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
await extensionManager.loadExtensions();
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining(
`Warning: Skipping extension in ${badExtDir}: Failed to load extension config from ${badExtDir}/gemini-extension.json`,
),
);
consoleErrorSpy.mockRestore();
// Ensure no extensions were loaded
expect(extensionManager.getExtensions()).toHaveLength(0);
// Attempt to uninstall the broken extension by its name.
await expect(
extensionManager.uninstallExtension('broken-ext', false),
).resolves.toBeUndefined(); // Should resolve, not throw an error
// Verify the directory is removed
expect(fs.existsSync(badExtDir)).toBe(false);
});
it('should uninstall an extension by name', async () => {
const sourceExtDir = createExtension({
extensionsDir: userExtensionsDir,