fix(cliproxy): complete backend switching with proper binary extraction

- Add backend field to BinaryManagerConfig type for installer context
- Thread backend parameter through tar/zip extractors to use correct names
- Delete existing binary before install to prevent mismatched binaries
- Track backend in session metadata for debugging/monitoring
- Validate and preserve backend field in config mergeWithDefaults
- Pass backend to registerSession for session tracking

The core issue was extractors calling getExecutableName() without the
backend parameter, causing it to default to 'plus' regardless of user
selection. This resulted in wrong binaries being extracted/renamed.
This commit is contained in:
kaitranntt
2026-01-23 15:17:32 -05:00
parent 0a1cbcc612
commit 2794a548a5
10 changed files with 101 additions and 46 deletions
+5 -4
View File
@@ -62,6 +62,7 @@ function createDefaultConfig(backend: CLIProxyBackend = DEFAULT_BACKEND): Binary
maxRetries: 3,
verbose: false,
forceVersion: false,
backend, // Pass backend for installer to use correct download URL
};
}
@@ -95,22 +96,22 @@ export class BinaryManager {
/** Get full path to binary executable */
getBinaryPath(): string {
return getBinaryPath(this.config.binPath);
return getBinaryPath(this.config.binPath, this.backend);
}
/** Check if binary exists */
isBinaryInstalled(): boolean {
return isBinaryInstalled(this.config.binPath);
return isBinaryInstalled(this.config.binPath, this.backend);
}
/** Get binary info if installed */
async getBinaryInfo(): Promise<BinaryInfo | null> {
return getBinaryInfo(this.config.binPath, this.config.version);
return getBinaryInfo(this.config.binPath, this.config.version, this.backend);
}
/** Delete binary (for cleanup or reinstall) */
deleteBinary(): void {
deleteBinary(this.config.binPath, this.config.verbose);
deleteBinary(this.config.binPath, this.config.verbose, this.backend);
}
}
+6 -4
View File
@@ -3,7 +3,8 @@
* Facade for tar.gz and zip archive extraction.
*/
import { ArchiveExtension } from '../types';
import { ArchiveExtension, CLIProxyBackend } from '../types';
import { DEFAULT_BACKEND } from '../platform-detector';
import { extractTarGz } from './tar-extractor';
import { extractZip } from './zip-extractor';
@@ -18,11 +19,12 @@ export async function extractArchive(
archivePath: string,
destDir: string,
extension: ArchiveExtension,
verbose = false
verbose = false,
backend: CLIProxyBackend = DEFAULT_BACKEND
): Promise<void> {
if (extension === 'tar.gz') {
await extractTarGz(archivePath, destDir, verbose);
await extractTarGz(archivePath, destDir, verbose, backend);
} else {
await extractZip(archivePath, destDir, verbose);
await extractZip(archivePath, destDir, verbose, backend);
}
}
+34 -17
View File
@@ -11,6 +11,7 @@ import {
getDownloadUrl,
getChecksumsUrl,
getExecutableName,
DEFAULT_BACKEND,
} from '../platform-detector';
import { downloadWithRetry } from './downloader';
import { verifyChecksum, computeChecksum } from './verifier';
@@ -26,13 +27,23 @@ export async function downloadAndInstall(
config: BinaryManagerConfig,
verbose = false
): Promise<void> {
const platform = detectPlatform(config.version);
const downloadUrl = getDownloadUrl(config.version);
const checksumsUrl = getChecksumsUrl(config.version);
const backend = config.backend ?? DEFAULT_BACKEND;
const platform = detectPlatform(config.version, backend);
const downloadUrl = getDownloadUrl(config.version, backend);
const checksumsUrl = getChecksumsUrl(config.version, backend);
const backendLabel = backend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy';
fs.mkdirSync(config.binPath, { recursive: true });
// Delete existing binary before install to prevent mismatched binaries
const existingBinary = path.join(config.binPath, getExecutableName(backend));
if (fs.existsSync(existingBinary)) {
fs.unlinkSync(existingBinary);
if (verbose) console.error(`[cliproxy] Removed existing binary: ${existingBinary}`);
}
const archivePath = path.join(config.binPath, `cliproxy-archive.${platform.extension}`);
const spinner = new ProgressIndicator(`Downloading CLIProxy Plus v${config.version}`);
const spinner = new ProgressIndicator(`Downloading ${backendLabel} v${config.version}`);
spinner.start();
try {
@@ -63,27 +74,30 @@ export async function downloadAndInstall(
}
spinner.update('Extracting binary');
await extractArchive(archivePath, config.binPath, platform.extension, verbose);
spinner.succeed('CLIProxy Plus ready');
await extractArchive(archivePath, config.binPath, platform.extension, verbose, backend);
spinner.succeed(`${backendLabel} ready`);
fs.unlinkSync(archivePath);
const binaryPath = path.join(config.binPath, getExecutableName());
const binaryPath = path.join(config.binPath, getExecutableName(backend));
if (platform.os !== 'windows' && fs.existsSync(binaryPath)) {
fs.chmodSync(binaryPath, 0o755);
if (verbose) console.error(`[cliproxy] Set executable permissions: ${binaryPath}`);
}
writeInstalledVersion(config.binPath, config.version);
console.log(ok(`CLIProxy Plus v${config.version} installed successfully`));
console.log(ok(`${backendLabel} v${config.version} installed successfully`));
} catch (error) {
spinner.fail('Installation failed');
throw error;
}
}
import type { CLIProxyBackend } from '../types';
/** Delete binary (for cleanup or reinstall) */
export function deleteBinary(binPath: string, verbose = false): void {
const binaryPath = path.join(binPath, getExecutableName());
export function deleteBinary(binPath: string, verbose = false, backend?: CLIProxyBackend): void {
const effectiveBackend = backend ?? DEFAULT_BACKEND;
const binaryPath = path.join(binPath, getExecutableName(effectiveBackend));
if (fs.existsSync(binaryPath)) {
fs.unlinkSync(binaryPath);
if (verbose) console.error(`[cliproxy] Deleted: ${binaryPath}`);
@@ -91,29 +105,32 @@ export function deleteBinary(binPath: string, verbose = false): void {
}
/** Get binary path */
export function getBinaryPath(binPath: string): string {
return path.join(binPath, getExecutableName());
export function getBinaryPath(binPath: string, backend?: CLIProxyBackend): string {
const effectiveBackend = backend ?? DEFAULT_BACKEND;
return path.join(binPath, getExecutableName(effectiveBackend));
}
/** Check if binary exists */
export function isBinaryInstalled(binPath: string): boolean {
return fs.existsSync(getBinaryPath(binPath));
export function isBinaryInstalled(binPath: string, backend?: CLIProxyBackend): boolean {
return fs.existsSync(getBinaryPath(binPath, backend));
}
/** Get binary info if installed */
export async function getBinaryInfo(
binPath: string,
version: string
version: string,
backend?: CLIProxyBackend
): Promise<{
path: string;
version: string;
platform: ReturnType<typeof detectPlatform>;
checksum: string;
} | null> {
const binaryPath = getBinaryPath(binPath);
const effectiveBackend = backend ?? DEFAULT_BACKEND;
const binaryPath = getBinaryPath(binPath, effectiveBackend);
if (!fs.existsSync(binaryPath)) return null;
const platform = detectPlatform();
const platform = detectPlatform(undefined, effectiveBackend);
const checksum = await computeChecksum(binaryPath);
return { path: binaryPath, version, platform, checksum };
}
+17 -10
View File
@@ -4,7 +4,7 @@
*/
import * as fs from 'fs';
import { BinaryManagerConfig } from '../types';
import { BinaryManagerConfig, CLIProxyBackend } from '../types';
import {
checkForUpdates,
fetchLatestVersion,
@@ -15,7 +15,11 @@ import { downloadAndInstall, deleteBinary, getBinaryPath } from './installer';
import { info, warn } from '../../utils/ui';
import { isCliproxyRunning } from '../stats-fetcher';
import { CLIPROXY_DEFAULT_PORT } from '../config-generator';
import { CLIPROXY_MAX_STABLE_VERSION, CLIPROXY_FAULTY_RANGE } from '../platform-detector';
import {
CLIPROXY_MAX_STABLE_VERSION,
CLIPROXY_FAULTY_RANGE,
DEFAULT_BACKEND,
} from '../platform-detector';
/** Log helper */
function log(message: string, verbose: boolean): void {
@@ -47,7 +51,9 @@ function clampToMaxStable(version: string | undefined, verbose: boolean): string
/** Handle auto-update when binary exists */
async function handleAutoUpdate(config: BinaryManagerConfig, verbose: boolean): Promise<void> {
const updateResult = await checkForUpdates(config.binPath, config.version, verbose);
const backend: CLIProxyBackend = config.backend ?? DEFAULT_BACKEND;
const backendLabel = backend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy';
const updateResult = await checkForUpdates(config.binPath, config.version, verbose, backend);
const currentVersion = updateResult.currentVersion;
const latestVersion = updateResult.latestVersion;
@@ -55,7 +61,7 @@ async function handleAutoUpdate(config: BinaryManagerConfig, verbose: boolean):
if (isVersionFaulty(currentVersion)) {
console.log(
warn(
`CLIProxy Plus v${currentVersion} has known bugs (v${CLIPROXY_FAULTY_RANGE.min.replace(/-\d+$/, '')}-${CLIPROXY_FAULTY_RANGE.max.replace(/-\d+$/, '')}). ` +
`${backendLabel} v${currentVersion} has known bugs (v${CLIPROXY_FAULTY_RANGE.min.replace(/-\d+$/, '')}-${CLIPROXY_FAULTY_RANGE.max.replace(/-\d+$/, '')}). ` +
`Upgrade to latest stable recommended.`
)
);
@@ -73,16 +79,16 @@ async function handleAutoUpdate(config: BinaryManagerConfig, verbose: boolean):
const proxyRunning = await isCliproxyRunning(CLIPROXY_DEFAULT_PORT);
const latestNote = isAboveMaxStable(latestVersion) ? ` (latest v${latestVersion} unstable)` : '';
const updateMsg = `CLIProxy Plus update: v${currentVersion} -> v${targetVersion}${latestNote}`;
const updateMsg = `${backendLabel} update: v${currentVersion} -> v${targetVersion}${latestNote}`;
if (proxyRunning) {
console.log(info(updateMsg));
console.log(info('Run "ccs cliproxy stop" then restart to apply update'));
log('Skipping update: CLIProxy Plus is currently running', verbose);
log(`Skipping update: ${backendLabel} is currently running`, verbose);
} else {
console.log(info(updateMsg));
console.log(info('Updating CLIProxy Plus...'));
deleteBinary(config.binPath, verbose);
console.log(info(`Updating ${backendLabel}...`));
deleteBinary(config.binPath, verbose, backend);
config.version = targetVersion;
await downloadAndInstall(config, verbose);
}
@@ -94,7 +100,8 @@ async function handleAutoUpdate(config: BinaryManagerConfig, verbose: boolean):
*/
export async function ensureBinary(config: BinaryManagerConfig): Promise<string> {
const verbose = config.verbose;
const binaryPath = getBinaryPath(config.binPath);
const backend: CLIProxyBackend = config.backend ?? DEFAULT_BACKEND;
const binaryPath = getBinaryPath(config.binPath, backend);
// Binary exists - check for updates unless forceVersion
if (fs.existsSync(binaryPath)) {
@@ -120,7 +127,7 @@ export async function ensureBinary(config: BinaryManagerConfig): Promise<string>
if (!config.forceVersion) {
try {
const latestVersion = await fetchLatestVersion(verbose);
const latestVersion = await fetchLatestVersion(verbose, backend);
const targetVersion = clampToMaxStable(latestVersion, verbose);
if (targetVersion && isNewerVersion(targetVersion, config.version)) {
log(`Using version: ${targetVersion} (instead of ${config.version})`, verbose);
+10 -4
View File
@@ -6,15 +6,21 @@
import * as fs from 'fs';
import * as path from 'path';
import * as zlib from 'zlib';
import { getExecutableName, getArchiveBinaryName } from '../platform-detector';
import { getExecutableName, getArchiveBinaryName, DEFAULT_BACKEND } from '../platform-detector';
import type { CLIProxyBackend } from '../types';
/**
* Extract tar.gz archive using Node.js built-in modules
*/
export function extractTarGz(archivePath: string, destDir: string, verbose = false): Promise<void> {
export function extractTarGz(
archivePath: string,
destDir: string,
verbose = false,
backend: CLIProxyBackend = DEFAULT_BACKEND
): Promise<void> {
return new Promise((resolve, reject) => {
const execName = getExecutableName();
const archiveBinaryName = getArchiveBinaryName();
const execName = getExecutableName(backend);
const archiveBinaryName = getArchiveBinaryName(backend);
const gunzip = zlib.createGunzip();
const input = fs.createReadStream(archivePath);
+10 -4
View File
@@ -6,15 +6,21 @@
import * as fs from 'fs';
import * as path from 'path';
import * as zlib from 'zlib';
import { getExecutableName, getArchiveBinaryName } from '../platform-detector';
import { getExecutableName, getArchiveBinaryName, DEFAULT_BACKEND } from '../platform-detector';
import type { CLIProxyBackend } from '../types';
/**
* Extract zip archive using Node.js (simple implementation)
*/
export function extractZip(archivePath: string, destDir: string, verbose = false): Promise<void> {
export function extractZip(
archivePath: string,
destDir: string,
verbose = false,
backend: CLIProxyBackend = DEFAULT_BACKEND
): Promise<void> {
return new Promise((resolve, reject) => {
const execName = getExecutableName();
const archiveBinaryName = getArchiveBinaryName();
const execName = getExecutableName(backend);
const archiveBinaryName = getArchiveBinaryName(backend);
const buffer = fs.readFileSync(archivePath);
// Find End of Central Directory record (EOCD)
+2 -2
View File
@@ -746,9 +746,9 @@ export async function execClaudeWithCLIProxy(
throw new Error(`CLIProxy startup failed: ${err.message}`);
}
// Register this session with the new proxy, including the installed version
// Register this session with the new proxy, including version and backend
const installedVersion = getInstalledCliproxyVersion();
sessionId = registerSession(cfg.port, proxy.pid as number, installedVersion);
sessionId = registerSession(cfg.port, proxy.pid as number, installedVersion, backend);
log(
`Registered session ${sessionId} with new proxy (PID ${proxy.pid}, version ${installedVersion})`
);
+10 -1
View File
@@ -29,6 +29,8 @@ interface SessionLock {
startedAt: string;
/** CLIProxy version running (added for version mismatch detection) */
version?: string;
/** Backend type running (original vs plus) */
backend?: 'original' | 'plus';
}
/** Generate unique session ID */
@@ -175,9 +177,15 @@ export function getExistingProxy(port: number): SessionLock | null {
* @param port Port the proxy is running on
* @param proxyPid PID of the proxy process
* @param version Optional CLIProxy version (stored when spawning new proxy)
* @param backend Optional backend type (original vs plus)
* @returns Session ID for this session
*/
export function registerSession(port: number, proxyPid: number, version?: string): string {
export function registerSession(
port: number,
proxyPid: number,
version?: string,
backend?: 'original' | 'plus'
): string {
const sessionId = generateSessionId();
const existingLock = readSessionLockForPort(port);
@@ -193,6 +201,7 @@ export function registerSession(port: number, proxyPid: number, version?: string
sessions: [sessionId],
startedAt: new Date().toISOString(),
version,
backend,
};
writeSessionLockForPort(newLock);
}
+2
View File
@@ -48,6 +48,8 @@ export interface BinaryManagerConfig {
verbose: boolean;
/** Force specific version (skip auto-upgrade to latest) */
forceVersion: boolean;
/** Backend variant (original vs plus) */
backend?: CLIProxyBackend;
}
/**
+5
View File
@@ -153,6 +153,11 @@ function mergeWithDefaults(partial: Partial<UnifiedConfig>): UnifiedConfig {
},
// Auth config - preserve user values, no defaults (uses constants as fallback)
auth: partial.cliproxy?.auth,
// Backend selection - validate and preserve user choice (original vs plus)
backend:
partial.cliproxy?.backend === 'original' || partial.cliproxy?.backend === 'plus'
? partial.cliproxy.backend
: undefined, // Invalid values become undefined (defaults to 'plus' at runtime)
},
preferences: {
...defaults.preferences,