mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-12 21:03:05 -07:00
Add flag to update all extensions (#7321)
This commit is contained in:
@@ -5,43 +5,72 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { CommandModule } from 'yargs';
|
import type { CommandModule } from 'yargs';
|
||||||
import { updateExtension } from '../../config/extension.js';
|
import {
|
||||||
|
updateExtensionByName,
|
||||||
|
updateAllUpdatableExtensions,
|
||||||
|
type ExtensionUpdateInfo,
|
||||||
|
} from '../../config/extension.js';
|
||||||
import { getErrorMessage } from '../../utils/errors.js';
|
import { getErrorMessage } from '../../utils/errors.js';
|
||||||
|
|
||||||
interface UpdateArgs {
|
interface UpdateArgs {
|
||||||
name: string;
|
name?: string;
|
||||||
|
all?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateOutput = (info: ExtensionUpdateInfo) =>
|
||||||
|
`Extension "${info.name}" successfully updated: ${info.originalVersion} → ${info.updatedVersion}.`;
|
||||||
|
|
||||||
export async function handleUpdate(args: UpdateArgs) {
|
export async function handleUpdate(args: UpdateArgs) {
|
||||||
|
if (args.all) {
|
||||||
try {
|
try {
|
||||||
// TODO(chrstnb): we should list extensions if the requested extension is not installed.
|
const updateInfos = await updateAllUpdatableExtensions();
|
||||||
const updatedExtensionInfo = await updateExtension(args.name);
|
if (updateInfos.length === 0) {
|
||||||
if (!updatedExtensionInfo) {
|
console.log('No extensions to update.');
|
||||||
console.log(`Extension "${args.name}" failed to update.`);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
console.log(updateInfos.map((info) => updateOutput(info)).join('\n'));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(getErrorMessage(error));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (args.name)
|
||||||
|
try {
|
||||||
|
// TODO(chrstnb): we should list extensions if the requested extension is not installed.
|
||||||
|
const updatedExtensionInfo = await updateExtensionByName(args.name);
|
||||||
console.log(
|
console.log(
|
||||||
`Extension "${args.name}" successfully updated: ${updatedExtensionInfo.originalVersion} → ${updatedExtensionInfo.updatedVersion}.`,
|
`Extension "${args.name}" successfully updated: ${updatedExtensionInfo.originalVersion} → ${updatedExtensionInfo.updatedVersion}.`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(getErrorMessage(error));
|
console.error(getErrorMessage(error));
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const updateCommand: CommandModule = {
|
export const updateCommand: CommandModule = {
|
||||||
command: 'update <name>',
|
command: 'update [--all] [name]',
|
||||||
describe: 'Updates an extension.',
|
describe:
|
||||||
|
'Updates all extensions or a named extension to the latest version.',
|
||||||
builder: (yargs) =>
|
builder: (yargs) =>
|
||||||
yargs
|
yargs
|
||||||
.positional('name', {
|
.positional('name', {
|
||||||
describe: 'The name of the extension to update.',
|
describe: 'The name of the extension to update.',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
})
|
})
|
||||||
.check((_argv) => true),
|
.option('all', {
|
||||||
|
describe: 'Update all extensions.',
|
||||||
|
type: 'boolean',
|
||||||
|
})
|
||||||
|
.conflicts('name', 'all')
|
||||||
|
.check((argv) => {
|
||||||
|
if (!argv.all && !argv.name) {
|
||||||
|
throw new Error('Either an extension name or --all must be provided');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
handler: async (argv) => {
|
handler: async (argv) => {
|
||||||
await handleUpdate({
|
await handleUpdate({
|
||||||
name: argv['name'] as string,
|
name: argv['name'] as string | undefined,
|
||||||
|
all: argv['all'] as boolean | undefined,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -635,13 +635,11 @@ describe('updateExtension', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should update a git-installed extension', async () => {
|
it('should update a git-installed extension', async () => {
|
||||||
// 1. "Install" an extension
|
|
||||||
const gitUrl = 'https://github.com/google/gemini-extensions.git';
|
const gitUrl = 'https://github.com/google/gemini-extensions.git';
|
||||||
const extensionName = 'gemini-extensions';
|
const extensionName = 'gemini-extensions';
|
||||||
const targetExtDir = path.join(userExtensionsDir, extensionName);
|
const targetExtDir = path.join(userExtensionsDir, extensionName);
|
||||||
const metadataPath = path.join(targetExtDir, INSTALL_METADATA_FILENAME);
|
const metadataPath = path.join(targetExtDir, INSTALL_METADATA_FILENAME);
|
||||||
|
|
||||||
// Create the "installed" extension directory and files
|
|
||||||
fs.mkdirSync(targetExtDir, { recursive: true });
|
fs.mkdirSync(targetExtDir, { recursive: true });
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
path.join(targetExtDir, EXTENSIONS_CONFIG_FILENAME),
|
path.join(targetExtDir, EXTENSIONS_CONFIG_FILENAME),
|
||||||
@@ -652,10 +650,8 @@ describe('updateExtension', () => {
|
|||||||
JSON.stringify({ source: gitUrl, type: 'git' }),
|
JSON.stringify({ source: gitUrl, type: 'git' }),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 2. Mock the git clone for the update
|
|
||||||
const clone = vi.fn().mockImplementation(async (_, destination) => {
|
const clone = vi.fn().mockImplementation(async (_, destination) => {
|
||||||
fs.mkdirSync(destination, { recursive: true });
|
fs.mkdirSync(destination, { recursive: true });
|
||||||
// This is the "updated" version
|
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
path.join(destination, EXTENSIONS_CONFIG_FILENAME),
|
path.join(destination, EXTENSIONS_CONFIG_FILENAME),
|
||||||
JSON.stringify({ name: extensionName, version: '1.1.0' }),
|
JSON.stringify({ name: extensionName, version: '1.1.0' }),
|
||||||
@@ -667,16 +663,14 @@ describe('updateExtension', () => {
|
|||||||
clone,
|
clone,
|
||||||
} as unknown as SimpleGit);
|
} as unknown as SimpleGit);
|
||||||
|
|
||||||
// 3. Call updateExtension
|
const updateInfo = await updateExtension(loadExtension(targetExtDir));
|
||||||
const updateInfo = await updateExtension(extensionName);
|
|
||||||
|
|
||||||
// 4. Assertions
|
|
||||||
expect(updateInfo).toEqual({
|
expect(updateInfo).toEqual({
|
||||||
|
name: 'gemini-extensions',
|
||||||
originalVersion: '1.0.0',
|
originalVersion: '1.0.0',
|
||||||
updatedVersion: '1.1.0',
|
updatedVersion: '1.1.0',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check that the config file reflects the new version
|
|
||||||
const updatedConfig = JSON.parse(
|
const updatedConfig = JSON.parse(
|
||||||
fs.readFileSync(
|
fs.readFileSync(
|
||||||
path.join(targetExtDir, EXTENSIONS_CONFIG_FILENAME),
|
path.join(targetExtDir, EXTENSIONS_CONFIG_FILENAME),
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export interface ExtensionInstallMetadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ExtensionUpdateInfo {
|
export interface ExtensionUpdateInfo {
|
||||||
|
name: string;
|
||||||
originalVersion: string;
|
originalVersion: string;
|
||||||
updatedVersion: string;
|
updatedVersion: string;
|
||||||
}
|
}
|
||||||
@@ -444,10 +445,10 @@ export function toOutputString(extension: Extension): string {
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateExtension(
|
export async function updateExtensionByName(
|
||||||
extensionName: string,
|
extensionName: string,
|
||||||
cwd: string = process.cwd(),
|
cwd: string = process.cwd(),
|
||||||
): Promise<ExtensionUpdateInfo | undefined> {
|
): Promise<ExtensionUpdateInfo> {
|
||||||
const installedExtensions = loadUserExtensions();
|
const installedExtensions = loadUserExtensions();
|
||||||
const extension = installedExtensions.find(
|
const extension = installedExtensions.find(
|
||||||
(installed) => installed.config.name === extensionName,
|
(installed) => installed.config.name === extensionName,
|
||||||
@@ -457,16 +458,21 @@ export async function updateExtension(
|
|||||||
`Extension "${extensionName}" not found. Run gemini extensions list to see available extensions.`,
|
`Extension "${extensionName}" not found. Run gemini extensions list to see available extensions.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return await updateExtension(extension, cwd);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateExtension(
|
||||||
|
extension: Extension,
|
||||||
|
cwd: string = process.cwd(),
|
||||||
|
): Promise<ExtensionUpdateInfo> {
|
||||||
if (!extension.installMetadata) {
|
if (!extension.installMetadata) {
|
||||||
throw new Error(
|
throw new Error(`Extension ${extension.config.name} cannot be updated.`);
|
||||||
`Extension cannot be updated because it is missing the .gemini-extension.install.json file. To update manually, uninstall and then reinstall the updated version.`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
const originalVersion = extension.config.version;
|
const originalVersion = extension.config.version;
|
||||||
const tempDir = await ExtensionStorage.createTmpDir();
|
const tempDir = await ExtensionStorage.createTmpDir();
|
||||||
try {
|
try {
|
||||||
await copyExtension(extension.path, tempDir);
|
await copyExtension(extension.path, tempDir);
|
||||||
await uninstallExtension(extensionName, cwd);
|
await uninstallExtension(extension.config.name, cwd);
|
||||||
await installExtension(extension.installMetadata, cwd);
|
await installExtension(extension.installMetadata, cwd);
|
||||||
|
|
||||||
const updatedExtension = loadExtension(extension.path);
|
const updatedExtension = loadExtension(extension.path);
|
||||||
@@ -475,6 +481,7 @@ export async function updateExtension(
|
|||||||
}
|
}
|
||||||
const updatedVersion = updatedExtension.config.version;
|
const updatedVersion = updatedExtension.config.version;
|
||||||
return {
|
return {
|
||||||
|
name: extension.config.name,
|
||||||
originalVersion,
|
originalVersion,
|
||||||
updatedVersion,
|
updatedVersion,
|
||||||
};
|
};
|
||||||
@@ -537,3 +544,14 @@ function removeFromDisabledExtensions(
|
|||||||
settings.setValue(scope, 'extensions', extensionSettings);
|
settings.setValue(scope, 'extensions', extensionSettings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateAllUpdatableExtensions(
|
||||||
|
cwd: string = process.cwd(),
|
||||||
|
): Promise<ExtensionUpdateInfo[]> {
|
||||||
|
const extensions = loadExtensions(cwd).filter(
|
||||||
|
(extension) => !!extension.installMetadata,
|
||||||
|
);
|
||||||
|
return await Promise.all(
|
||||||
|
extensions.map((extension) => updateExtension(extension, cwd)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user