From 2794a548a57c94002ab8c4f926bd47f04de3f8ff Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Fri, 23 Jan 2026 15:17:32 -0500 Subject: [PATCH] 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. --- src/cliproxy/binary-manager.ts | 9 ++--- src/cliproxy/binary/extractor.ts | 10 +++--- src/cliproxy/binary/installer.ts | 51 ++++++++++++++++++---------- src/cliproxy/binary/lifecycle.ts | 27 +++++++++------ src/cliproxy/binary/tar-extractor.ts | 14 +++++--- src/cliproxy/binary/zip-extractor.ts | 14 +++++--- src/cliproxy/cliproxy-executor.ts | 4 +-- src/cliproxy/session-tracker.ts | 11 +++++- src/cliproxy/types.ts | 2 ++ src/config/unified-config-loader.ts | 5 +++ 10 files changed, 101 insertions(+), 46 deletions(-) diff --git a/src/cliproxy/binary-manager.ts b/src/cliproxy/binary-manager.ts index 986ef1b0..203693ef 100644 --- a/src/cliproxy/binary-manager.ts +++ b/src/cliproxy/binary-manager.ts @@ -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 { - 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); } } diff --git a/src/cliproxy/binary/extractor.ts b/src/cliproxy/binary/extractor.ts index 09d496b1..28905f3f 100644 --- a/src/cliproxy/binary/extractor.ts +++ b/src/cliproxy/binary/extractor.ts @@ -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 { 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); } } diff --git a/src/cliproxy/binary/installer.ts b/src/cliproxy/binary/installer.ts index 864d55d0..42b1261e 100644 --- a/src/cliproxy/binary/installer.ts +++ b/src/cliproxy/binary/installer.ts @@ -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 { - 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; 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 }; } diff --git a/src/cliproxy/binary/lifecycle.ts b/src/cliproxy/binary/lifecycle.ts index d2186975..48700ce5 100644 --- a/src/cliproxy/binary/lifecycle.ts +++ b/src/cliproxy/binary/lifecycle.ts @@ -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 { - 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 { 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 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); diff --git a/src/cliproxy/binary/tar-extractor.ts b/src/cliproxy/binary/tar-extractor.ts index 305df2e4..540a805f 100644 --- a/src/cliproxy/binary/tar-extractor.ts +++ b/src/cliproxy/binary/tar-extractor.ts @@ -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 { +export function extractTarGz( + archivePath: string, + destDir: string, + verbose = false, + backend: CLIProxyBackend = DEFAULT_BACKEND +): Promise { 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); diff --git a/src/cliproxy/binary/zip-extractor.ts b/src/cliproxy/binary/zip-extractor.ts index 5cc078ae..43d95fc8 100644 --- a/src/cliproxy/binary/zip-extractor.ts +++ b/src/cliproxy/binary/zip-extractor.ts @@ -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 { +export function extractZip( + archivePath: string, + destDir: string, + verbose = false, + backend: CLIProxyBackend = DEFAULT_BACKEND +): Promise { 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) diff --git a/src/cliproxy/cliproxy-executor.ts b/src/cliproxy/cliproxy-executor.ts index f63ff81b..f019c6f4 100644 --- a/src/cliproxy/cliproxy-executor.ts +++ b/src/cliproxy/cliproxy-executor.ts @@ -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})` ); diff --git a/src/cliproxy/session-tracker.ts b/src/cliproxy/session-tracker.ts index 2279eeba..0c3d201d 100644 --- a/src/cliproxy/session-tracker.ts +++ b/src/cliproxy/session-tracker.ts @@ -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); } diff --git a/src/cliproxy/types.ts b/src/cliproxy/types.ts index 06912f29..9759bba2 100644 --- a/src/cliproxy/types.ts +++ b/src/cliproxy/types.ts @@ -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; } /** diff --git a/src/config/unified-config-loader.ts b/src/config/unified-config-loader.ts index 273de189..a495b7e3 100644 --- a/src/config/unified-config-loader.ts +++ b/src/config/unified-config-loader.ts @@ -153,6 +153,11 @@ function mergeWithDefaults(partial: Partial): 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,