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,