diff --git a/packages/cli/src/commands/extensions/install.test.ts b/packages/cli/src/commands/extensions/install.test.ts index 6bf5c8608f..707536bd3a 100644 --- a/packages/cli/src/commands/extensions/install.test.ts +++ b/packages/cli/src/commands/extensions/install.test.ts @@ -5,14 +5,19 @@ */ import { describe, it, expect } from 'vitest'; -import { installCommand } from './install.js'; +import { installCommand, handleInstall } from './install.js'; import yargs from 'yargs'; +import * as extension from '../../config/extension.js'; + +vi.mock('../../config/extension.js', () => ({ + installExtension: vi.fn(), +})); 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.', + 'Either --source or --path must be provided.', ); }); @@ -23,3 +28,22 @@ describe('extensions install command', () => { ).toThrow('Arguments source and path are mutually exclusive'); }); }); + +describe('extensions install with org/repo', () => { + it('should call installExtension with the correct git URL', async () => { + const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + const installExtensionSpy = vi + .spyOn(extension, 'installExtension') + .mockResolvedValue('test-extension'); + + await handleInstall({ source: 'test-org/test-repo' }); + + expect(installExtensionSpy).toHaveBeenCalledWith({ + source: 'https://github.com/test-org/test-repo.git', + type: 'git', + }); + expect(consoleLogSpy).toHaveBeenCalledWith( + 'Extension "test-extension" installed successfully and enabled.', + ); + }); +}); diff --git a/packages/cli/src/commands/extensions/install.ts b/packages/cli/src/commands/extensions/install.ts index af411c3d47..4823f5848e 100644 --- a/packages/cli/src/commands/extensions/install.ts +++ b/packages/cli/src/commands/extensions/install.ts @@ -17,12 +17,43 @@ interface InstallArgs { path?: string; } +const ORG_REPO_REGEX = /^[a-zA-Z0-9-]+\/[\w.-]+$/; + export async function handleInstall(args: InstallArgs) { try { - const installMetadata: ExtensionInstallMetadata = { - source: (args.source || args.path) as string, - type: args.source ? 'git' : 'local', - }; + let installMetadata: ExtensionInstallMetadata; + + if (args.source) { + const { source } = args; + if ( + source.startsWith('http://') || + source.startsWith('https://') || + source.startsWith('git@') + ) { + installMetadata = { + source, + type: 'git', + }; + } else if (ORG_REPO_REGEX.test(source)) { + installMetadata = { + source: `https://github.com/${source}.git`, + type: 'git', + }; + } else { + throw new Error( + `The source "${source}" is not a valid URL or "org/repo" format.`, + ); + } + } else if (args.path) { + installMetadata = { + source: args.path, + type: 'local', + }; + } else { + // This should not be reached due to the yargs check. + throw new Error('Either --source or --path must be provided.'); + } + const extensionName = await installExtension(installMetadata); console.log( `Extension "${extensionName}" installed successfully and enabled.`, @@ -35,11 +66,12 @@ export async function handleInstall(args: InstallArgs) { export const installCommand: CommandModule = { command: 'install [--source | --path ]', - describe: 'Installs an extension from a git repository or a local path.', + describe: + 'Installs an extension from a git repository (URL or "org/repo") or a local path.', builder: (yargs) => yargs .option('source', { - describe: 'The git URL of the extension to install.', + describe: 'The git URL or "org/repo" of the extension to install.', type: 'string', }) .option('path', { @@ -49,9 +81,7 @@ export const installCommand: CommandModule = { .conflicts('source', 'path') .check((argv) => { if (!argv.source && !argv.path) { - throw new Error( - 'Either a git URL --source or a --path must be provided.', - ); + throw new Error('Either --source or --path must be provided.'); } return true; }),