diff --git a/packages/cli/src/config/extensions/github.ts b/packages/cli/src/config/extensions/github.ts index 2eab8d83d4..79f2385c41 100644 --- a/packages/cli/src/config/extensions/github.ts +++ b/packages/cli/src/config/extensions/github.ts @@ -16,7 +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'; +import { EXTENSIONS_CONFIG_FILENAME, loadExtension } from '../extension.js'; import { quote } from 'shell-quote'; function getGitHubToken(): string | undefined { @@ -273,6 +273,34 @@ export async function downloadFromGitHubRelease( extractFile(downloadedAssetPath, destination); + // For regular github releases, the repository is put inside of a top level + // directory. In this case we should see exactly two file in the destination + // dir, the archive and the directory. If we see that, validate that the + // dir has a gemini extension configuration file and then move all files + // from the directory up one level into the destination directory. + const entries = await fs.promises.readdir(destination, { + withFileTypes: true, + }); + if (entries.length === 2) { + const lonelyDir = entries.find((entry) => entry.isDirectory()); + if ( + lonelyDir && + fs.existsSync( + path.join(destination, lonelyDir.name, EXTENSIONS_CONFIG_FILENAME), + ) + ) { + const dirPathToExtract = path.join(destination, lonelyDir.name); + const extractedDirFiles = await fs.promises.readdir(dirPathToExtract); + for (const file of extractedDirFiles) { + await fs.promises.rename( + path.join(dirPathToExtract, file), + path.join(destination, file), + ); + } + await fs.promises.rmdir(dirPathToExtract); + } + } + await fs.promises.unlink(downloadedAssetPath); return { tagName: releaseData.tag_name,