Something went wrong. Try again.
fork of prime agent with changes i want but are not upstreamable probqbly (and some fixes)
Something went wrong. Try again.
9.4 kB · 259 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260import { isNativePlatform } from "./native-installation.js";import { getPiUserAgent } from "./pi-user-agent.js";
const DEFAULT_PRIME_AGENT_DOWNLOAD_BASE_URL = "https://pub-728493de92a943e2a9b2d17b4719f318.r2.dev";const STABLE_VERSION_MANIFEST_PATH = "latest.json";const BETA_VERSION_MANIFEST_PATH = "beta.json";const DEFAULT_VERSION_CHECK_TIMEOUT_MS = 10000;
export type UpdateChannel = "stable" | "nightly";
export interface LatestPiRelease { version: string; packageName?: string; installSpec?: string; binaries?: NativeReleaseArtifact[];}
interface NativeReleaseArtifact { platform: string; file: string; sha256: string;}
interface ParsedVersion { major: number; minor: number; patch: number; prerelease?: string;}
function comparePrereleaseIdentifiers(leftPrerelease: string, rightPrerelease: string): number { const leftIdentifiers = leftPrerelease.split("."); const rightIdentifiers = rightPrerelease.split("."); const length = Math.max(leftIdentifiers.length, rightIdentifiers.length);
for (let index = 0; index < length; index += 1) { const left = leftIdentifiers[index]; const right = rightIdentifiers[index]; if (left === right) continue; if (left === undefined) return -1; if (right === undefined) return 1;
const leftIsNumeric = /^\d+$/.test(left); const rightIsNumeric = /^\d+$/.test(right); if (leftIsNumeric && rightIsNumeric) { const leftNumber = left.replace(/^0+(?=\d)/, ""); const rightNumber = right.replace(/^0+(?=\d)/, ""); if (leftNumber.length !== rightNumber.length) return leftNumber.length - rightNumber.length; const comparison = leftNumber.localeCompare(rightNumber); if (comparison !== 0) return comparison; continue; } if (leftIsNumeric) return -1; if (rightIsNumeric) return 1; return left.localeCompare(right); }
return 0;}
function parsePackageVersion(version: string): ParsedVersion | undefined { const match = version.trim().match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+.*)?$/); if (!match) { return undefined; } return { major: Number.parseInt(match[1], 10), minor: Number.parseInt(match[2], 10), patch: Number.parseInt(match[3], 10), prerelease: match[4], };}
export function comparePackageVersions(leftVersion: string, rightVersion: string): number | undefined { const left = parsePackageVersion(leftVersion); const right = parsePackageVersion(rightVersion); if (!left || !right) { return undefined; }
if (left.major !== right.major) return left.major - right.major; if (left.minor !== right.minor) return left.minor - right.minor; if (left.patch !== right.patch) return left.patch - right.patch; if (left.prerelease === right.prerelease) return 0; if (!left.prerelease) return 1; if (!right.prerelease) return -1; return comparePrereleaseIdentifiers(left.prerelease, right.prerelease);}
export function isNewerPackageVersion(candidateVersion: string, currentVersion: string): boolean { const comparison = comparePackageVersions(candidateVersion, currentVersion); if (comparison !== undefined) { return comparison > 0; } return candidateVersion.trim() !== currentVersion.trim();}
function getPrimeAgentDownloadBaseUrl(): string { return (process.env.PRIME_AGENT_DOWNLOAD_BASE_URL?.trim() || DEFAULT_PRIME_AGENT_DOWNLOAD_BASE_URL).replace( /\/+$/, "", );}
function normalizeReleaseVersion(version: string): string { return version.trim().replace(/^v/, "");}
/** * A preferred channel wins; otherwise a build tagged `-beta` stays on nightly and anything else * follows stable. Nightly builds are what the release bucket publishes as beta. */export function resolveUpdateChannel(currentVersion: string, preferred?: UpdateChannel): UpdateChannel { if (preferred) return preferred; const prerelease = parsePackageVersion(currentVersion)?.prerelease; return prerelease?.match(/^beta(?:\.|$)/) ? "nightly" : "stable";}
/** * Whether `candidateVersion` should replace `currentVersion` on the effective channel. * Same-channel updates must be strictly newer. An explicit switch to another channel * accepts any different version whose base version is not older, so a stable 1.2.3 * can move onto 1.2.3-beta.5 even though prerelease ordering ranks that lower. */export function isReleaseUpdateCandidate( candidateVersion: string, currentVersion: string, channel?: UpdateChannel,): boolean { if (isNewerPackageVersion(candidateVersion, currentVersion)) return true; if (!channel || channel === resolveUpdateChannel(currentVersion)) return false; if (normalizeReleaseVersion(candidateVersion) === normalizeReleaseVersion(currentVersion)) return false; const candidate = parsePackageVersion(candidateVersion); const current = parsePackageVersion(currentVersion); if (!candidate || !current) return true; if (candidate.major !== current.major) return candidate.major > current.major; if (candidate.minor !== current.minor) return candidate.minor > current.minor; return candidate.patch >= current.patch;}
/** True when installing `candidateVersion` would lower the major.minor.patch base, prerelease tags aside. */export function isBaseVersionDowngrade(candidateVersion: string, currentVersion: string): boolean { const candidate = parsePackageVersion(candidateVersion); const current = parsePackageVersion(currentVersion); if (!candidate || !current) return false; if (candidate.major !== current.major) return candidate.major < current.major; if (candidate.minor !== current.minor) return candidate.minor < current.minor; return candidate.patch < current.patch;}
function getReleaseManifestPath(currentVersion: string, channel?: UpdateChannel): string { return resolveUpdateChannel(currentVersion, channel) === "nightly" ? BETA_VERSION_MANIFEST_PATH : STABLE_VERSION_MANIFEST_PATH;}
function resolveReleaseUrl(baseUrl: string, pathOrUrl: string): string | undefined { const trimmed = pathOrUrl.trim(); if (!trimmed) return undefined; try { return new URL(trimmed).toString(); } catch { return `${baseUrl}/${trimmed.replace(/^\/+/, "")}`; }}
export async function getLatestPiRelease( currentVersion: string, options: { timeoutMs?: number; baseUrl?: string; channel?: UpdateChannel } = {},): Promise<LatestPiRelease | undefined> { if (process.env.PI_SKIP_VERSION_CHECK || process.env.PI_OFFLINE) return undefined;
const baseUrl = options.baseUrl?.replace(/\/+$/, "") ?? getPrimeAgentDownloadBaseUrl(); const response = await fetch(`${baseUrl}/${getReleaseManifestPath(currentVersion, options.channel)}`, { headers: { "User-Agent": getPiUserAgent(currentVersion), accept: "application/json", }, signal: AbortSignal.timeout(options.timeoutMs ?? DEFAULT_VERSION_CHECK_TIMEOUT_MS), }); if (!response.ok) return undefined;
const data = (await response.json()) as { package?: unknown; packageName?: unknown; tarball?: unknown; version?: unknown; binaries?: unknown; binariesV2?: unknown; }; if (typeof data.version !== "string" || !data.version.trim()) { return undefined; } const packageName = typeof data.package === "string" && data.package.trim() ? data.package.trim() : typeof data.packageName === "string" && data.packageName.trim() ? data.packageName.trim() : undefined; const installSpec = typeof data.tarball === "string" ? resolveReleaseUrl(baseUrl, data.tarball) : undefined; const release: LatestPiRelease = { version: normalizeReleaseVersion(data.version) }; if (packageName) { release.packageName = packageName; } if (installSpec) { release.installSpec = installSpec; } // Prefer the complete v2 schema, with the v1 schema as a compatibility // fallback. Structurally valid entries for future platforms are ignored, // while malformed or duplicate supported-platform entries reject the list. const binarySource = Array.isArray(data.binariesV2) ? data.binariesV2 : Array.isArray(data.binaries) ? data.binaries : undefined; if (binarySource) { const binaries: NativeReleaseArtifact[] = []; const platforms = new Set<string>(); for (const candidate of binarySource) { if (!candidate || typeof candidate !== "object") return release; const artifact = candidate as Partial<NativeReleaseArtifact>; if (typeof artifact.platform !== "string") return release; if (!isNativePlatform(artifact.platform)) continue; if ( platforms.has(artifact.platform) || artifact.file !== `prime-agent-${release.version}-${artifact.platform}.tar.gz` || typeof artifact.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(artifact.sha256) ) return release; platforms.add(artifact.platform); binaries.push({ platform: artifact.platform, file: artifact.file, sha256: artifact.sha256 }); } if (binaries.length > 0) release.binaries = binaries; } return release;}
export async function getLatestPiVersion( currentVersion: string, options: { timeoutMs?: number; channel?: UpdateChannel } = {},): Promise<string | undefined> { return (await getLatestPiRelease(currentVersion, options))?.version;}
export async function checkForNewPiVersion( currentVersion: string, channel?: UpdateChannel,): Promise<string | undefined> { try { const latestVersion = await getLatestPiVersion(currentVersion, { channel }); if (latestVersion && isReleaseUpdateCandidate(latestVersion, currentVersion, channel)) { return latestVersion; } return undefined; } catch { return undefined; }}