Reinstate support for updating locally-installed extensions (#8833)

This commit is contained in:
christine betts
2025-09-21 23:44:58 -04:00
committed by GitHub
parent 8fdb61aabf
commit d9828e2571
6 changed files with 111 additions and 37 deletions

View File

@@ -20,7 +20,6 @@ interface InstallArgs {
export async function handleInstall(args: InstallArgs) {
try {
let installMetadata: ExtensionInstallMetadata;
if (args.source) {
const { source } = args;
if (

View File

@@ -35,28 +35,7 @@ export async function handleUpdate(args: UpdateArgs) {
allExtensions.map((e) => e.config.name),
workingDir,
);
if (args.all) {
try {
let updateInfos = await updateAllUpdatableExtensions(
workingDir,
extensions,
await checkForAllExtensionUpdates(extensions, new Map(), (_) => {}),
() => {},
);
updateInfos = updateInfos.filter(
(info) => info.originalVersion !== info.updatedVersion,
);
if (updateInfos.length === 0) {
console.log('No extensions to update.');
return;
}
console.log(updateInfos.map((info) => updateOutput(info)).join('\n'));
} catch (error) {
console.error(getErrorMessage(error));
}
}
if (args.name)
if (args.name) {
try {
const extension = extensions.find(
(extension) => extension.name === args.name,
@@ -99,10 +78,31 @@ export async function handleUpdate(args: UpdateArgs) {
} catch (error) {
console.error(getErrorMessage(error));
}
}
if (args.all) {
try {
let updateInfos = await updateAllUpdatableExtensions(
workingDir,
extensions,
await checkForAllExtensionUpdates(extensions, new Map(), (_) => {}),
() => {},
);
updateInfos = updateInfos.filter(
(info) => info.originalVersion !== info.updatedVersion,
);
if (updateInfos.length === 0) {
console.log('No extensions to update.');
return;
}
console.log(updateInfos.map((info) => updateOutput(info)).join('\n'));
} catch (error) {
console.error(getErrorMessage(error));
}
}
}
export const updateCommand: CommandModule = {
command: 'update [--all] [name]',
command: 'update [<name>] [--all]',
describe:
'Updates all extensions or a named extension to the latest version.',
builder: (yargs) =>

View File

@@ -128,7 +128,7 @@ describe('git extension helpers', () => {
version: '1.0.0',
isActive: true,
installMetadata: {
type: 'local',
type: 'link',
source: '',
},
};

View File

@@ -16,6 +16,7 @@ import * as https from 'node:https';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { execSync } from 'node:child_process';
import { loadExtension } from '../extension.js';
function getGitHubToken(): string | undefined {
return process.env['GITHUB_TOKEN'];
@@ -115,9 +116,29 @@ async function fetchFromGithub(
export async function checkForExtensionUpdate(
extension: GeminiCLIExtension,
setExtensionUpdateState: (updateState: ExtensionUpdateState) => void,
cwd: string = process.cwd(),
): Promise<void> {
setExtensionUpdateState(ExtensionUpdateState.CHECKING_FOR_UPDATES);
const installMetadata = extension.installMetadata;
if (installMetadata?.type === 'local') {
const newExtension = loadExtension({
extensionDir: installMetadata.source,
workspaceDir: cwd,
});
if (!newExtension) {
console.error(
`Failed to check for update for local extension "${extension.name}". Could not load extension from source path: ${installMetadata.source}`,
);
setExtensionUpdateState(ExtensionUpdateState.ERROR);
return;
}
if (newExtension.config.version !== extension.version) {
setExtensionUpdateState(ExtensionUpdateState.UPDATE_AVAILABLE);
return;
}
setExtensionUpdateState(ExtensionUpdateState.UP_TO_DATE);
return;
}
if (
!installMetadata ||
(installMetadata.type !== 'git' &&

View File

@@ -341,17 +341,24 @@ describe('update tests', () => {
expect(result).toBe(ExtensionUpdateState.UP_TO_DATE);
});
it('should return NotUpdatable for a non-git extension', async () => {
const extensionDir = createExtension({
it('should return UpToDate for a local extension with no updates', async () => {
const localExtensionSourcePath = path.join(tempHomeDir, 'local-source');
const sourceExtensionDir = createExtension({
extensionsDir: localExtensionSourcePath,
name: 'my-local-ext',
version: '1.0.0',
});
const installedExtensionDir = createExtension({
extensionsDir: userExtensionsDir,
name: 'local-extension',
version: '1.0.0',
installMetadata: { source: '/local/path', type: 'local' },
installMetadata: { source: sourceExtensionDir, type: 'local' },
});
const extension = annotateActiveExtensions(
[
loadExtension({
extensionDir,
extensionDir: installedExtensionDir,
workspaceDir: tempWorkspaceDir,
})!,
],
@@ -369,9 +376,51 @@ describe('update tests', () => {
extensionState = newState;
}
},
tempWorkspaceDir,
);
const result = results.get('local-extension');
expect(result).toBe(ExtensionUpdateState.NOT_UPDATABLE);
expect(result).toBe(ExtensionUpdateState.UP_TO_DATE);
});
it('should return UpdateAvailable for a local extension with updates', async () => {
const localExtensionSourcePath = path.join(tempHomeDir, 'local-source');
const sourceExtensionDir = createExtension({
extensionsDir: localExtensionSourcePath,
name: 'my-local-ext',
version: '1.1.0',
});
const installedExtensionDir = createExtension({
extensionsDir: userExtensionsDir,
name: 'local-extension',
version: '1.0.0',
installMetadata: { source: sourceExtensionDir, type: 'local' },
});
const extension = annotateActiveExtensions(
[
loadExtension({
extensionDir: installedExtensionDir,
workspaceDir: tempWorkspaceDir,
})!,
],
[],
process.cwd(),
)[0];
let extensionState = new Map();
const results = await checkForAllExtensionUpdates(
[extension],
extensionState,
(newState) => {
if (typeof newState === 'function') {
newState(extensionState);
} else {
extensionState = newState;
}
},
tempWorkspaceDir,
);
const result = results.get('local-extension');
expect(result).toBe(ExtensionUpdateState.UPDATE_AVAILABLE);
});
it('should return Error when git check fails', async () => {

View File

@@ -128,6 +128,7 @@ export async function checkForAllExtensionUpdates(
setExtensionsUpdateState: Dispatch<
SetStateAction<Map<string, ExtensionUpdateState>>
>,
cwd: string = process.cwd(),
): Promise<Map<string, ExtensionUpdateState>> {
for (const extension of extensions) {
const initialState = extensionsUpdateState.get(extension.name);
@@ -143,13 +144,17 @@ export async function checkForAllExtensionUpdates(
});
continue;
}
await checkForExtensionUpdate(extension, (updatedState) => {
setExtensionsUpdateState((prev) => {
extensionsUpdateState = new Map(prev);
extensionsUpdateState.set(extension.name, updatedState);
return extensionsUpdateState;
});
});
await checkForExtensionUpdate(
extension,
(updatedState) => {
setExtensionsUpdateState((prev) => {
extensionsUpdateState = new Map(prev);
extensionsUpdateState.set(extension.name, updatedState);
return extensionsUpdateState;
});
},
cwd,
);
}
}
return extensionsUpdateState;