[extensions] Add extension management install command (#6703)

This commit is contained in:
christine betts
2025-08-25 17:02:10 +00:00
committed by GitHub
parent 49cce8a15d
commit 0bd496bd51
14 changed files with 562 additions and 115 deletions

View File

@@ -0,0 +1,22 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { CommandModule } from 'yargs';
import { installCommand } from './extensions/install.js';
export const extensionsCommand: CommandModule = {
command: 'extensions <command>',
describe: 'Manage Gemini CLI extensions.',
builder: (yargs) =>
yargs
.command(installCommand)
.demandCommand(1, 'You need at least one command before continuing.')
.version(false),
handler: () => {
// This handler is not called when a subcommand is provided.
// Yargs will show the help menu.
},
};

View File

@@ -0,0 +1,25 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect } from 'vitest';
import { installCommand } from './install.js';
import yargs from 'yargs';
describe('extensions install command', () => {
it('should fail if no source is provided', () => {
const validationParser = yargs([]).command(installCommand).fail(false);
expect(() => validationParser.parse('install')).toThrow(
'Either a git URL --source or a --path must be provided.',
);
});
it('should fail if both git source and local path are provided', () => {
const validationParser = yargs([]).command(installCommand).fail(false);
expect(() =>
validationParser.parse('install --source some-url --path /some/path'),
).toThrow('Arguments source and path are mutually exclusive');
});
});

View File

@@ -0,0 +1,62 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { CommandModule } from 'yargs';
import {
installExtension,
ExtensionInstallMetadata,
} from '../../config/extension.js';
interface InstallArgs {
source?: string;
path?: string;
}
export async function handleInstall(args: InstallArgs) {
try {
const installMetadata: ExtensionInstallMetadata = {
source: (args.source || args.path) as string,
type: args.source ? 'git' : 'local',
};
const extensionName = await installExtension(installMetadata);
console.log(
`Extension "${extensionName}" installed successfully and enabled.`,
);
} catch (error) {
console.error((error as Error).message);
process.exit(1);
}
}
export const installCommand: CommandModule = {
command: 'install [--source | --path ]',
describe: 'Installs an extension from a git repository or a local path.',
builder: (yargs) =>
yargs
.option('source', {
describe: 'The git URL of the extension to install.',
type: 'string',
})
.option('path', {
describe: 'Path to a local extension directory.',
type: 'string',
})
.conflicts('source', 'path')
.check((argv) => {
if (!argv.source && !argv.path) {
throw new Error(
'Either a git URL --source or a --path must be provided.',
);
}
return true;
}),
handler: async (argv) => {
await handleInstall({
source: argv['source'] as string | undefined,
path: argv['path'] as string | undefined,
});
},
};