fix(cliproxy): resolve plus release asset variants (#1523)

Use version-aware CLIProxy Plus asset names so current

no-plugin releases install without breaking older fallback releases.

Tag latest-version caches with their GitHub repo and ignore

legacy or cross-repo cache entries.

Closes #1522
This commit is contained in:
Kai (Tam Nhu) Tran
2026-06-15 09:20:37 -04:00
committed by GitHub
parent af6d082f95
commit 9e9cddd101
5 changed files with 132 additions and 5 deletions
@@ -39,6 +39,7 @@ describe('version-checker stale cache fallback', () => {
getVersionCachePath('plus'),
JSON.stringify({
latestVersion: '6.9.23-0',
repo: 'kaitranntt/CLIProxyAPIPlus',
checkedAt: Date.now() - VERSION_CACHE_DURATION_MS - 1_000,
}),
'utf8'
@@ -56,6 +57,33 @@ describe('version-checker stale cache fallback', () => {
expect(result.fromCache).toBe(true);
});
it('ignores legacy or cross-repo latest-version caches', async () => {
const { getVersionCachePath, writeInstalledVersion } = await import('../version-cache');
const { checkForUpdates } = await import('../version-checker');
const plusBinDir = path.join(tempHome, '.ccs', 'cliproxy', 'bin', 'plus');
fs.mkdirSync(plusBinDir, { recursive: true });
writeInstalledVersion(plusBinDir, '6.6.80');
for (const repo of [undefined, 'router-for-me/CLIProxyAPI']) {
fs.writeFileSync(
getVersionCachePath('plus'),
JSON.stringify({
latestVersion: '7.2.5',
checkedAt: Date.now(),
...(repo ? { repo } : {}),
}),
'utf8'
);
const result = await checkForUpdates(plusBinDir, '6.6.80', false, 'plus', {
fetchLatestVersionFn: async () => '7.1.68-2',
});
expect(result.latestVersion).toBe('7.1.68-2');
expect(result.fromCache).toBe(false);
}
});
it('uses a stale release-list cache when GitHub list lookup fails', async () => {
const { getVersionListCachePath } = await import('../version-cache');
const { VERSION_CACHE_DURATION_MS } = await import('../types');
+47 -1
View File
@@ -83,11 +83,55 @@ const RELEASE_ARCH_MAP: Record<SupportedArch, SupportedArch> = {
aarch64: 'aarch64',
};
const PLUS_NO_PLUGIN_ASSET_MIN_VERSION = '7.1.68-0';
const PLUS_AARCH64_ASSET_MIN_VERSION = '7.1.45-1';
export function mapNodeArchToReleaseArch(nodeArch: string): SupportedArch | undefined {
const arch = ARCH_MAP[nodeArch];
return arch ? RELEASE_ARCH_MAP[arch] : undefined;
}
function parseVersionParts(version: string): [number, number, number, number] {
const [coreVersion, forkRelease = '0'] = version.replace(/^v/, '').split('-', 2);
const [major = 0, minor = 0, patch = 0] = coreVersion
.split('.')
.map((part) => parseInt(part, 10) || 0);
return [major, minor, patch, parseInt(forkRelease, 10) || 0];
}
function isAtLeastVersion(version: string, minimum: string): boolean {
const left = parseVersionParts(version);
const right = parseVersionParts(minimum);
for (let index = 0; index < left.length; index += 1) {
if (left[index] > right[index]) return true;
if (left[index] < right[index]) return false;
}
return true;
}
function usesPlusNoPluginAsset(version: string, os: SupportedOS): boolean {
return os !== 'windows' && isAtLeastVersion(version, PLUS_NO_PLUGIN_ASSET_MIN_VERSION);
}
function getReleaseArchForBackend(
backend: CLIProxyBackend,
version: string,
publicArch: SupportedArch,
releaseArch: SupportedArch
): SupportedArch {
if (
backend === 'plus' &&
publicArch === 'arm64' &&
!isAtLeastVersion(version, PLUS_AARCH64_ASSET_MIN_VERSION)
) {
return 'arm64';
}
return releaseArch;
}
/**
* Detect current platform and return binary info
* @param version Optional version for binaryName (defaults to backend fallback)
@@ -121,7 +165,9 @@ export function detectPlatform(
const config = BACKEND_CONFIG[backend];
const ver = version || config.fallbackVersion;
const extension: ArchiveExtension = os === 'windows' ? 'zip' : 'tar.gz';
const binaryName = `${config.binaryPrefix}_${ver}_${os}_${releaseArch}.${extension}`;
const assetArch = getReleaseArchForBackend(backend, ver, arch, releaseArch);
const assetVariant = backend === 'plus' && usesPlusNoPluginAsset(ver, os) ? '_no-plugin' : '';
const binaryName = `${config.binaryPrefix}_${ver}_${os}_${assetArch}${assetVariant}.${extension}`;
return {
os,
+7 -1
View File
@@ -7,6 +7,7 @@
export interface VersionCache {
latestVersion: string;
checkedAt: number;
repo: string;
}
/** Update check result */
@@ -39,9 +40,14 @@ export const GITHUB_REPOS = {
plus: 'kaitranntt/CLIProxyAPIPlus',
} as const;
/** Get GitHub repository for specific backend */
export function getGitHubRepo(backend: 'original' | 'plus'): string {
return GITHUB_REPOS[backend];
}
/** Get GitHub API URLs for specific backend */
export function getGitHubApiUrls(backend: 'original' | 'plus') {
const repo = GITHUB_REPOS[backend];
const repo = getGitHubRepo(backend);
return {
latestRelease: `https://api.github.com/repos/${repo}/releases/latest`,
allReleases: `https://api.github.com/repos/${repo}/releases`,
+6
View File
@@ -11,6 +11,7 @@ import {
VERSION_CACHE_DURATION_MS,
VERSION_PIN_FILE,
VersionListCache,
getGitHubRepo,
} from './types';
import { DEFAULT_BACKEND } from '../binary/platform-detector';
import type { CLIProxyBackend } from '../types';
@@ -59,6 +60,10 @@ function readVersionCacheInternal(
const content = fs.readFileSync(cachePath, 'utf8');
const cache: VersionCache = JSON.parse(content);
if (cache.repo !== getGitHubRepo(backend)) {
return null;
}
// Check if cache is still valid, unless caller explicitly allows stale fallback.
if (allowExpired || Date.now() - cache.checkedAt < VERSION_CACHE_DURATION_MS) {
return cache;
@@ -82,6 +87,7 @@ export function writeVersionCache(
const cache: VersionCache = {
latestVersion: version,
checkedAt: Date.now(),
repo: getGitHubRepo(backend),
};
try {
@@ -73,9 +73,50 @@ describe('Backend Selection', () => {
assert(!info.binaryName.includes('CLIProxyAPIPlus'));
});
it('generates correct binary name for plus backend', () => {
const info = platformDetector.detectPlatform('6.6.51-0', 'plus');
assert(info.binaryName.startsWith('CLIProxyAPIPlus_6.6.51-0_'));
it('keeps old plus non-Windows archive names unsuffixed', () => {
withMockedProcessPlatform('darwin', 'arm64', () => {
assert.strictEqual(
platformDetector.detectPlatform('6.9.45-0', 'plus').binaryName,
'CLIProxyAPIPlus_6.9.45-0_darwin_arm64.tar.gz'
);
assert.strictEqual(
platformDetector.detectPlatform('7.1.45-1', 'plus').binaryName,
'CLIProxyAPIPlus_7.1.45-1_darwin_aarch64.tar.gz'
);
});
});
it('generates no-plugin archive names for current plus backend on macOS and Linux', () => {
withMockedProcessPlatform('darwin', 'arm64', () => {
assert.strictEqual(
platformDetector.detectPlatform('7.1.68-0', 'plus').binaryName,
'CLIProxyAPIPlus_7.1.68-0_darwin_aarch64_no-plugin.tar.gz'
);
const info = platformDetector.detectPlatform('7.1.68-2', 'plus');
assert.strictEqual(
info.binaryName,
'CLIProxyAPIPlus_7.1.68-2_darwin_aarch64_no-plugin.tar.gz'
);
assert.strictEqual(
platformDetector.getDownloadUrl('7.1.68-2', 'plus'),
'https://github.com/kaitranntt/CLIProxyAPIPlus/releases/download/v7.1.68-2/CLIProxyAPIPlus_7.1.68-2_darwin_aarch64_no-plugin.tar.gz'
);
});
withMockedProcessPlatform('linux', 'x64', () => {
const info = platformDetector.detectPlatform('7.1.68-2', 'plus');
assert.strictEqual(
info.binaryName,
'CLIProxyAPIPlus_7.1.68-2_linux_amd64_no-plugin.tar.gz'
);
});
});
it('does not add no-plugin suffix to plus Windows archives', () => {
withMockedProcessPlatform('win32', 'x64', () => {
const info = platformDetector.detectPlatform('7.1.68-2', 'plus');
assert.strictEqual(info.binaryName, 'CLIProxyAPIPlus_7.1.68-2_windows_amd64.zip');
});
});
it('uses original backend by default', () => {