mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-17 01:21:10 -07:00
Don't always fall back on a git clone when installing extensions (#11229)
This commit is contained in:
@@ -79,16 +79,22 @@ export async function cloneFromGit(
|
||||
export function parseGitHubRepoForReleases(source: string): {
|
||||
owner: string;
|
||||
repo: string;
|
||||
} {
|
||||
} | null {
|
||||
// Default to a github repo path, so `source` can be just an org/repo
|
||||
const parsedUrl = URL.parse(source, 'https://github.com');
|
||||
if (!parsedUrl) {
|
||||
throw new Error(`Invalid repo URL: ${source}`);
|
||||
}
|
||||
// The pathname should be "/owner/repo".
|
||||
const parts = parsedUrl?.pathname
|
||||
.substring(1)
|
||||
.split('/')
|
||||
// Remove the empty segments, fixes trailing slashes
|
||||
.filter((part) => part !== '');
|
||||
if (parts?.length !== 2 || parsedUrl?.host !== 'github.com') {
|
||||
if (parsedUrl?.host !== 'github.com') {
|
||||
return null;
|
||||
}
|
||||
if (parts?.length !== 2) {
|
||||
throw new Error(
|
||||
`Invalid GitHub repository source: ${source}. Expected "owner/repo" or a github repo uri.`,
|
||||
);
|
||||
@@ -110,7 +116,7 @@ export async function fetchReleaseFromGithub(
|
||||
repo: string,
|
||||
ref?: string,
|
||||
allowPreRelease?: boolean,
|
||||
): Promise<GithubReleaseData> {
|
||||
): Promise<GithubReleaseData | null> {
|
||||
if (ref) {
|
||||
return await fetchJson(
|
||||
`https://api.github.com/repos/${owner}/${repo}/releases/tags/${ref}`,
|
||||
@@ -120,9 +126,14 @@ export async function fetchReleaseFromGithub(
|
||||
if (!allowPreRelease) {
|
||||
// Grab the release that is tagged as the "latest", github does not allow
|
||||
// this to be a pre-release so we can blindly grab it.
|
||||
return await fetchJson(
|
||||
`https://api.github.com/repos/${owner}/${repo}/releases/latest`,
|
||||
);
|
||||
try {
|
||||
return await fetchJson(
|
||||
`https://api.github.com/repos/${owner}/${repo}/releases/latest`,
|
||||
);
|
||||
} catch (_) {
|
||||
// This can fail if there is no release marked latest. In that case
|
||||
// we want to just try the pre-release logic below.
|
||||
}
|
||||
}
|
||||
|
||||
// If pre-releases are allowed, we just grab the most recent release.
|
||||
@@ -130,7 +141,7 @@ export async function fetchReleaseFromGithub(
|
||||
`https://api.github.com/repos/${owner}/${repo}/releases?per_page=1`,
|
||||
);
|
||||
if (releases.length === 0) {
|
||||
throw new Error('No releases found');
|
||||
return null;
|
||||
}
|
||||
return releases[0];
|
||||
}
|
||||
@@ -206,7 +217,14 @@ export async function checkForExtensionUpdate(
|
||||
console.error(`No "source" provided for extension.`);
|
||||
return ExtensionUpdateState.ERROR;
|
||||
}
|
||||
const { owner, repo } = parseGitHubRepoForReleases(source);
|
||||
const repoInfo = parseGitHubRepoForReleases(source);
|
||||
if (!repoInfo) {
|
||||
console.error(
|
||||
`Source is not a valid GitHub repository for release checks: ${source}`,
|
||||
);
|
||||
return ExtensionUpdateState.ERROR;
|
||||
}
|
||||
const { owner, repo } = repoInfo;
|
||||
|
||||
const releaseData = await fetchReleaseFromGithub(
|
||||
owner,
|
||||
@@ -214,6 +232,9 @@ export async function checkForExtensionUpdate(
|
||||
installMetadata.ref,
|
||||
installMetadata.allowPreRelease,
|
||||
);
|
||||
if (!releaseData) {
|
||||
return ExtensionUpdateState.ERROR;
|
||||
}
|
||||
if (releaseData.tag_name !== releaseTag) {
|
||||
return ExtensionUpdateState.UPDATE_AVAILABLE;
|
||||
}
|
||||
@@ -226,28 +247,57 @@ export async function checkForExtensionUpdate(
|
||||
return ExtensionUpdateState.ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
export interface GitHubDownloadResult {
|
||||
tagName: string;
|
||||
tagName?: string;
|
||||
type: 'git' | 'github-release';
|
||||
success: boolean;
|
||||
failureReason?:
|
||||
| 'failed to fetch release data'
|
||||
| 'no release data'
|
||||
| 'no release asset found'
|
||||
| 'failed to download asset'
|
||||
| 'failed to extract asset'
|
||||
| 'unknown';
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export async function downloadFromGitHubRelease(
|
||||
installMetadata: ExtensionInstallMetadata,
|
||||
destination: string,
|
||||
): Promise<GitHubDownloadResult> {
|
||||
const { source, ref, allowPreRelease: preRelease } = installMetadata;
|
||||
const { owner, repo } = parseGitHubRepoForReleases(source);
|
||||
let releaseData: GithubReleaseData | null = null;
|
||||
|
||||
try {
|
||||
const releaseData = await fetchReleaseFromGithub(
|
||||
owner,
|
||||
repo,
|
||||
ref,
|
||||
preRelease,
|
||||
);
|
||||
if (!releaseData) {
|
||||
throw new Error(
|
||||
`No release data found for ${owner}/${repo} at tag ${ref}`,
|
||||
);
|
||||
const parts = parseGitHubRepoForReleases(source);
|
||||
if (!parts) {
|
||||
return {
|
||||
failureReason: 'no release data',
|
||||
success: false,
|
||||
type: 'github-release',
|
||||
errorMessage: `Not a github repo: ${source}`,
|
||||
};
|
||||
}
|
||||
const { owner, repo } = parts;
|
||||
|
||||
try {
|
||||
releaseData = await fetchReleaseFromGithub(owner, repo, ref, preRelease);
|
||||
if (!releaseData) {
|
||||
return {
|
||||
failureReason: 'no release data',
|
||||
success: false,
|
||||
type: 'github-release',
|
||||
errorMessage: `No release data found for ${owner}/${repo} at tag ${ref}`,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
failureReason: 'failed to fetch release data',
|
||||
success: false,
|
||||
type: 'github-release',
|
||||
errorMessage: `Failed to fetch release data for ${owner}/${repo} at tag ${ref}: ${getErrorMessage(error)}`,
|
||||
};
|
||||
}
|
||||
|
||||
const asset = findReleaseAsset(releaseData.assets);
|
||||
@@ -266,9 +316,13 @@ export async function downloadFromGitHubRelease(
|
||||
}
|
||||
}
|
||||
if (!archiveUrl) {
|
||||
throw new Error(
|
||||
`No assets found for release with tag ${releaseData.tag_name}`,
|
||||
);
|
||||
return {
|
||||
failureReason: 'no release asset found',
|
||||
success: false,
|
||||
type: 'github-release',
|
||||
tagName: releaseData.tag_name,
|
||||
errorMessage: `No assets found for release with tag ${releaseData.tag_name}`,
|
||||
};
|
||||
}
|
||||
let downloadedAssetPath = path.join(
|
||||
destination,
|
||||
@@ -280,9 +334,29 @@ export async function downloadFromGitHubRelease(
|
||||
downloadedAssetPath += '.zip';
|
||||
}
|
||||
|
||||
await downloadFile(archiveUrl, downloadedAssetPath);
|
||||
try {
|
||||
await downloadFile(archiveUrl, downloadedAssetPath);
|
||||
} catch (error) {
|
||||
return {
|
||||
failureReason: 'failed to download asset',
|
||||
success: false,
|
||||
type: 'github-release',
|
||||
tagName: releaseData.tag_name,
|
||||
errorMessage: `Failed to download asset from ${archiveUrl}: ${getErrorMessage(error)}`,
|
||||
};
|
||||
}
|
||||
|
||||
await extractFile(downloadedAssetPath, destination);
|
||||
try {
|
||||
await extractFile(downloadedAssetPath, destination);
|
||||
} catch (error) {
|
||||
return {
|
||||
failureReason: 'failed to extract asset',
|
||||
success: false,
|
||||
type: 'github-release',
|
||||
tagName: releaseData.tag_name,
|
||||
errorMessage: `Failed to extract asset from ${downloadedAssetPath}: ${getErrorMessage(error)}`,
|
||||
};
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -316,11 +390,16 @@ export async function downloadFromGitHubRelease(
|
||||
return {
|
||||
tagName: releaseData.tag_name,
|
||||
type: 'github-release',
|
||||
success: true,
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to download release from ${installMetadata.source}: ${getErrorMessage(error)}`,
|
||||
);
|
||||
return {
|
||||
failureReason: 'unknown',
|
||||
success: false,
|
||||
type: 'github-release',
|
||||
tagName: releaseData?.tag_name,
|
||||
errorMessage: `Failed to download release from ${installMetadata.source}: ${getErrorMessage(error)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user