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
@@ -20,7 +20,6 @@ interface InstallArgs {
export async function handleInstall(args: InstallArgs) { export async function handleInstall(args: InstallArgs) {
try { try {
let installMetadata: ExtensionInstallMetadata; let installMetadata: ExtensionInstallMetadata;
if (args.source) { if (args.source) {
const { source } = args; const { source } = args;
if ( if (
+23 -23
View File
@@ -35,28 +35,7 @@ export async function handleUpdate(args: UpdateArgs) {
allExtensions.map((e) => e.config.name), allExtensions.map((e) => e.config.name),
workingDir, workingDir,
); );
if (args.name) {
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)
try { try {
const extension = extensions.find( const extension = extensions.find(
(extension) => extension.name === args.name, (extension) => extension.name === args.name,
@@ -99,10 +78,31 @@ export async function handleUpdate(args: UpdateArgs) {
} catch (error) { } catch (error) {
console.error(getErrorMessage(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 = { export const updateCommand: CommandModule = {
command: 'update [--all] [name]', command: 'update [<name>] [--all]',
describe: describe:
'Updates all extensions or a named extension to the latest version.', 'Updates all extensions or a named extension to the latest version.',
builder: (yargs) => builder: (yargs) =>
@@ -128,7 +128,7 @@ describe('git extension helpers', () => {
version: '1.0.0', version: '1.0.0',
isActive: true, isActive: true,
installMetadata: { installMetadata: {
type: 'local', type: 'link',
source: '', source: '',
}, },
}; };
@@ -16,6 +16,7 @@ import * as https from 'node:https';
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import * as path from 'node:path'; import * as path from 'node:path';
import { execSync } from 'node:child_process'; import { execSync } from 'node:child_process';
import { loadExtension } from '../extension.js';
function getGitHubToken(): string | undefined { function getGitHubToken(): string | undefined {
return process.env['GITHUB_TOKEN']; return process.env['GITHUB_TOKEN'];
@@ -115,9 +116,29 @@ async function fetchFromGithub(
export async function checkForExtensionUpdate( export async function checkForExtensionUpdate(
extension: GeminiCLIExtension, extension: GeminiCLIExtension,
setExtensionUpdateState: (updateState: ExtensionUpdateState) => void, setExtensionUpdateState: (updateState: ExtensionUpdateState) => void,
cwd: string = process.cwd(),
): Promise<void> { ): Promise<void> {
setExtensionUpdateState(ExtensionUpdateState.CHECKING_FOR_UPDATES); setExtensionUpdateState(ExtensionUpdateState.CHECKING_FOR_UPDATES);
const installMetadata = extension.installMetadata; 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 ( if (
!installMetadata || !installMetadata ||
(installMetadata.type !== 'git' && (installMetadata.type !== 'git' &&
@@ -341,17 +341,24 @@ describe('update tests', () => {
expect(result).toBe(ExtensionUpdateState.UP_TO_DATE); expect(result).toBe(ExtensionUpdateState.UP_TO_DATE);
}); });
it('should return NotUpdatable for a non-git extension', async () => { it('should return UpToDate for a local extension with no updates', async () => {
const extensionDir = createExtension({ 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, extensionsDir: userExtensionsDir,
name: 'local-extension', name: 'local-extension',
version: '1.0.0', version: '1.0.0',
installMetadata: { source: '/local/path', type: 'local' }, installMetadata: { source: sourceExtensionDir, type: 'local' },
}); });
const extension = annotateActiveExtensions( const extension = annotateActiveExtensions(
[ [
loadExtension({ loadExtension({
extensionDir, extensionDir: installedExtensionDir,
workspaceDir: tempWorkspaceDir, workspaceDir: tempWorkspaceDir,
})!, })!,
], ],
@@ -369,9 +376,51 @@ describe('update tests', () => {
extensionState = newState; extensionState = newState;
} }
}, },
tempWorkspaceDir,
); );
const result = results.get('local-extension'); 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 () => { it('should return Error when git check fails', async () => {
+12 -7
View File
@@ -128,6 +128,7 @@ export async function checkForAllExtensionUpdates(
setExtensionsUpdateState: Dispatch< setExtensionsUpdateState: Dispatch<
SetStateAction<Map<string, ExtensionUpdateState>> SetStateAction<Map<string, ExtensionUpdateState>>
>, >,
cwd: string = process.cwd(),
): Promise<Map<string, ExtensionUpdateState>> { ): Promise<Map<string, ExtensionUpdateState>> {
for (const extension of extensions) { for (const extension of extensions) {
const initialState = extensionsUpdateState.get(extension.name); const initialState = extensionsUpdateState.get(extension.name);
@@ -143,13 +144,17 @@ export async function checkForAllExtensionUpdates(
}); });
continue; continue;
} }
await checkForExtensionUpdate(extension, (updatedState) => { await checkForExtensionUpdate(
setExtensionsUpdateState((prev) => { extension,
extensionsUpdateState = new Map(prev); (updatedState) => {
extensionsUpdateState.set(extension.name, updatedState); setExtensionsUpdateState((prev) => {
return extensionsUpdateState; extensionsUpdateState = new Map(prev);
}); extensionsUpdateState.set(extension.name, updatedState);
}); return extensionsUpdateState;
});
},
cwd,
);
} }
} }
return extensionsUpdateState; return extensionsUpdateState;