diff --git a/package.json b/package.json index 5fc95a6a..3df3aa6b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kaitranntt/ccs", - "version": "7.26.0", + "version": "7.26.0-dev.1", "description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6", "keywords": [ "cli", diff --git a/src/cliproxy/auth/oauth-process.ts b/src/cliproxy/auth/oauth-process.ts index 6b5cb997..64d8ee44 100644 --- a/src/cliproxy/auth/oauth-process.ts +++ b/src/cliproxy/auth/oauth-process.ts @@ -271,7 +271,7 @@ async function handleTokenNotFound( /** Handle process exit with error */ function handleProcessError(code: number | null, state: ProcessState, headless: boolean): void { console.log(''); - console.log(fail(`CLIProxy Plus auth exited with code ${code}`)); + console.log(fail(`CLIProxy auth exited with code ${code}`)); if (state.stderrData && !state.urlDisplayed) { console.log(` ${state.stderrData.trim().split('\n')[0]}`); } diff --git a/src/cliproxy/binary-manager.ts b/src/cliproxy/binary-manager.ts index 9ae8088f..203693ef 100644 --- a/src/cliproxy/binary-manager.ts +++ b/src/cliproxy/binary-manager.ts @@ -26,9 +26,10 @@ import { getVersionPinPath, readInstalledVersion, ensureBinary, + migrateVersionPin, } from './binary'; -type CLIProxyBackend = 'original' | 'plus'; +import type { CLIProxyBackend } from './types'; /** * Get backend from config or default to 'plus' @@ -61,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 }; } @@ -84,34 +86,43 @@ export class BinaryManager { /** Check for updates by comparing installed version with latest release */ async checkForUpdates(): Promise { - return checkForUpdates(this.config.binPath, this.config.version, this.config.verbose); + return checkForUpdates( + this.config.binPath, + this.config.version, + this.config.verbose, + this.backend + ); } /** 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); } } /** Convenience function respecting version pin */ export async function ensureCLIProxyBinary(verbose = false): Promise { const backend = getConfiguredBackend(); - const pinnedVersion = getPinnedVersion(); + + // Migrate old shared pin to backend-specific location (one-time migration) + migrateVersionPin(backend); + + const pinnedVersion = getPinnedVersion(backend); if (pinnedVersion) { if (verbose) console.error(`[cliproxy] Using pinned version: ${pinnedVersion}`); return new BinaryManager( @@ -127,27 +138,34 @@ export async function ensureCLIProxyBinary(verbose = false): Promise { } /** Check if CLIProxyAPI binary is installed */ -export function isCLIProxyInstalled(): boolean { - const backend = getConfiguredBackend(); - return new BinaryManager({}, backend).isBinaryInstalled(); +export function isCLIProxyInstalled(backend?: CLIProxyBackend): boolean { + const effectiveBackend = backend ?? getConfiguredBackend(); + return new BinaryManager({}, effectiveBackend).isBinaryInstalled(); } /** Get CLIProxyAPI binary path (may not exist) */ -export function getCLIProxyPath(): string { - const backend = getConfiguredBackend(); - return new BinaryManager({}, backend).getBinaryPath(); +export function getCLIProxyPath(backend?: CLIProxyBackend): string { + const effectiveBackend = backend ?? getConfiguredBackend(); + return new BinaryManager({}, effectiveBackend).getBinaryPath(); } /** Get installed CLIProxyAPI version from .version file */ -export function getInstalledCliproxyVersion(): string { - const backend = getConfiguredBackend(); - return readInstalledVersion(getBackendBinDir(backend), BACKEND_CONFIG[backend].fallbackVersion); +export function getInstalledCliproxyVersion(backend?: CLIProxyBackend): string { + const effectiveBackend = backend ?? getConfiguredBackend(); + return readInstalledVersion( + getBackendBinDir(effectiveBackend), + BACKEND_CONFIG[effectiveBackend].fallbackVersion + ); } /** Install a specific version of CLIProxyAPI */ -export async function installCliproxyVersion(version: string, verbose = false): Promise { - const backend = getConfiguredBackend(); - const manager = new BinaryManager({ version, verbose, forceVersion: true }, backend); +export async function installCliproxyVersion( + version: string, + verbose = false, + backend?: CLIProxyBackend +): Promise { + const effectiveBackend = backend ?? getConfiguredBackend(); + const manager = new BinaryManager({ version, verbose, forceVersion: true }, effectiveBackend); // Check if proxy is running and stop it first if (isProxyRunning()) { @@ -165,8 +183,11 @@ export async function installCliproxyVersion(version: string, verbose = false): } if (manager.isBinaryInstalled()) { + const label = effectiveBackend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy'; if (verbose) - console.log(info(`Removing existing CLIProxy Plus v${getInstalledCliproxyVersion()}`)); + console.log( + info(`Removing existing ${label} v${getInstalledCliproxyVersion(effectiveBackend)}`) + ); manager.deleteBinary(); } await manager.ensureBinary(); @@ -190,6 +211,9 @@ export interface CliproxyUpdateCheckResult { latestVersion: string; fromCache: boolean; checkedAt: number; + // Backend info + backend: CLIProxyBackend; + backendLabel: string; // Stability fields isStable: boolean; maxStableVersion: string; @@ -208,8 +232,12 @@ export async function checkCliproxyUpdate(): Promise ? undefined : `v${result.currentVersion} has known stability issues. Max stable: v${CLIPROXY_MAX_STABLE_VERSION}`; + const backendLabel = backend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy'; + return { ...result, + backend, + backendLabel, isStable, maxStableVersion: CLIPROXY_MAX_STABLE_VERSION, stabilityMessage, @@ -223,6 +251,7 @@ export { savePinnedVersion, clearPinnedVersion, isVersionPinned, + migrateVersionPin, }; export default BinaryManager; 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/index.ts b/src/cliproxy/binary/index.ts index e0336258..23614d93 100644 --- a/src/cliproxy/binary/index.ts +++ b/src/cliproxy/binary/index.ts @@ -25,6 +25,7 @@ export { savePinnedVersion, clearPinnedVersion, isVersionPinned, + migrateVersionPin, } from './version-cache'; // Version Checker 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/types.ts b/src/cliproxy/binary/types.ts index 12eda079..c82a05cc 100644 --- a/src/cliproxy/binary/types.ts +++ b/src/cliproxy/binary/types.ts @@ -24,14 +24,30 @@ export const VERSION_CACHE_DURATION_MS = 60 * 60 * 1000; /** Version pin file name - stores user's explicit version choice */ export const VERSION_PIN_FILE = '.version-pin'; -/** GitHub API URL for latest release (CLIProxyAPIPlus fork with Kiro + Copilot support) */ +/** + * GitHub API URLs - backend-specific + * @deprecated Use getGitHubApiUrls(backend) instead + */ export const GITHUB_API_LATEST_RELEASE = 'https://api.github.com/repos/router-for-me/CLIProxyAPIPlus/releases/latest'; - -/** GitHub API URL for all releases */ export const GITHUB_API_ALL_RELEASES = 'https://api.github.com/repos/router-for-me/CLIProxyAPIPlus/releases'; +/** GitHub repos per backend */ +export const GITHUB_REPOS = { + original: 'router-for-me/CLIProxyAPI', + plus: 'router-for-me/CLIProxyAPIPlus', +} as const; + +/** Get GitHub API URLs for specific backend */ +export function getGitHubApiUrls(backend: 'original' | 'plus') { + const repo = GITHUB_REPOS[backend]; + return { + latestRelease: `https://api.github.com/repos/${repo}/releases/latest`, + allReleases: `https://api.github.com/repos/${repo}/releases`, + }; +} + /** Version list cache structure */ export interface VersionListCache { versions: string[]; diff --git a/src/cliproxy/binary/version-cache.ts b/src/cliproxy/binary/version-cache.ts index 63a19a56..2527fd76 100644 --- a/src/cliproxy/binary/version-cache.ts +++ b/src/cliproxy/binary/version-cache.ts @@ -5,33 +5,35 @@ import * as fs from 'fs'; import * as path from 'path'; -import { getCliproxyDir, getBinDir } from '../config-generator'; +import { getBinDir } from '../config-generator'; import { VersionCache, VERSION_CACHE_DURATION_MS, VERSION_PIN_FILE, VersionListCache, } from './types'; +import { DEFAULT_BACKEND } from '../platform-detector'; +import type { CLIProxyBackend } from '../types'; /** - * Get path to version cache file + * Get path to version cache file (backend-specific) */ -export function getVersionCachePath(): string { - return path.join(getCliproxyDir(), '.version-cache.json'); +export function getVersionCachePath(backend: CLIProxyBackend = DEFAULT_BACKEND): string { + return path.join(getBinDir(), backend, '.version-cache.json'); } /** - * Get path to version pin file + * Get path to version pin file (backend-specific) */ -export function getVersionPinPath(): string { - return path.join(getBinDir(), VERSION_PIN_FILE); +export function getVersionPinPath(backend: CLIProxyBackend = DEFAULT_BACKEND): string { + return path.join(getBinDir(), backend, VERSION_PIN_FILE); } /** - * Read version cache if still valid + * Read version cache if still valid (backend-specific) */ -export function readVersionCache(): VersionCache | null { - const cachePath = getVersionCachePath(); +export function readVersionCache(backend: CLIProxyBackend = DEFAULT_BACKEND): VersionCache | null { + const cachePath = getVersionCachePath(backend); if (!fs.existsSync(cachePath)) { return null; } @@ -53,10 +55,13 @@ export function readVersionCache(): VersionCache | null { } /** - * Write version to cache + * Write version to cache (backend-specific) */ -export function writeVersionCache(version: string): void { - const cachePath = getVersionCachePath(); +export function writeVersionCache( + version: string, + backend: CLIProxyBackend = DEFAULT_BACKEND +): void { + const cachePath = getVersionCachePath(backend); const cache: VersionCache = { latestVersion: version, checkedAt: Date.now(), @@ -98,10 +103,10 @@ export function writeInstalledVersion(binPath: string, version: string): void { } /** - * Get pinned version if one exists + * Get pinned version if one exists (backend-specific) */ -export function getPinnedVersion(): string | null { - const pinPath = getVersionPinPath(); +export function getPinnedVersion(backend: CLIProxyBackend = DEFAULT_BACKEND): string | null { + const pinPath = getVersionPinPath(backend); if (!fs.existsSync(pinPath)) { return null; } @@ -113,10 +118,13 @@ export function getPinnedVersion(): string | null { } /** - * Save pinned version to persist user's explicit choice + * Save pinned version to persist user's explicit choice (backend-specific) */ -export function savePinnedVersion(version: string): void { - const pinPath = getVersionPinPath(); +export function savePinnedVersion( + version: string, + backend: CLIProxyBackend = DEFAULT_BACKEND +): void { + const pinPath = getVersionPinPath(backend); try { fs.mkdirSync(path.dirname(pinPath), { recursive: true }); fs.writeFileSync(pinPath, version, 'utf8'); @@ -126,10 +134,10 @@ export function savePinnedVersion(version: string): void { } /** - * Clear pinned version (unpin) + * Clear pinned version (unpin) - backend-specific */ -export function clearPinnedVersion(): void { - const pinPath = getVersionPinPath(); +export function clearPinnedVersion(backend: CLIProxyBackend = DEFAULT_BACKEND): void { + const pinPath = getVersionPinPath(backend); if (fs.existsSync(pinPath)) { try { fs.unlinkSync(pinPath); @@ -140,10 +148,32 @@ export function clearPinnedVersion(): void { } /** - * Check if a version is currently pinned + * Check if a version is currently pinned (backend-specific) */ -export function isVersionPinned(): boolean { - return getPinnedVersion() !== null; +export function isVersionPinned(backend: CLIProxyBackend = DEFAULT_BACKEND): boolean { + return getPinnedVersion(backend) !== null; +} + +/** + * Migrate old shared version pin to backend-specific location. + * Called once on first run after update. + */ +export function migrateVersionPin(backend: CLIProxyBackend): void { + const oldPinPath = path.join(getBinDir(), VERSION_PIN_FILE); + if (!fs.existsSync(oldPinPath)) return; + + try { + const oldVersion = fs.readFileSync(oldPinPath, 'utf8').trim(); + if (!oldVersion) return; + + // Save to new backend-specific location + savePinnedVersion(oldVersion, backend); + + // Delete old shared file + fs.unlinkSync(oldPinPath); + } catch { + // Silent fail - not critical + } } // ==================== Version List Cache ==================== @@ -151,17 +181,19 @@ export function isVersionPinned(): boolean { const VERSION_LIST_CACHE_FILE = '.version-list-cache.json'; /** - * Get path to version list cache file + * Get path to version list cache file (backend-specific) */ -export function getVersionListCachePath(): string { - return path.join(getCliproxyDir(), VERSION_LIST_CACHE_FILE); +export function getVersionListCachePath(backend: CLIProxyBackend = DEFAULT_BACKEND): string { + return path.join(getBinDir(), backend, VERSION_LIST_CACHE_FILE); } /** - * Read version list cache if still valid + * Read version list cache if still valid (backend-specific) */ -export function readVersionListCache(): VersionListCache | null { - const cachePath = getVersionListCachePath(); +export function readVersionListCache( + backend: CLIProxyBackend = DEFAULT_BACKEND +): VersionListCache | null { + const cachePath = getVersionListCachePath(backend); if (!fs.existsSync(cachePath)) { return null; } @@ -182,10 +214,13 @@ export function readVersionListCache(): VersionListCache | null { } /** - * Write version list to cache + * Write version list to cache (backend-specific) */ -export function writeVersionListCache(cache: VersionListCache): void { - const cachePath = getVersionListCachePath(); +export function writeVersionListCache( + cache: VersionListCache, + backend: CLIProxyBackend = DEFAULT_BACKEND +): void { + const cachePath = getVersionListCachePath(backend); try { fs.mkdirSync(path.dirname(cachePath), { recursive: true }); diff --git a/src/cliproxy/binary/version-checker.ts b/src/cliproxy/binary/version-checker.ts index 383b6bbc..a76d4996 100644 --- a/src/cliproxy/binary/version-checker.ts +++ b/src/cliproxy/binary/version-checker.ts @@ -11,13 +11,13 @@ import { readVersionListCache, writeVersionListCache, } from './version-cache'; +import { UpdateCheckResult, VersionListResult, getGitHubApiUrls } from './types'; import { - UpdateCheckResult, - GITHUB_API_LATEST_RELEASE, - GITHUB_API_ALL_RELEASES, - VersionListResult, -} from './types'; -import { CLIPROXY_MAX_STABLE_VERSION, CLIPROXY_FAULTY_RANGE } from '../platform-detector'; + CLIPROXY_MAX_STABLE_VERSION, + CLIPROXY_FAULTY_RANGE, + DEFAULT_BACKEND, +} from '../platform-detector'; +import type { CLIProxyBackend } from '../types'; /** * Compare semver versions (true if latest > current) @@ -56,9 +56,15 @@ export function isVersionFaulty(version: string): boolean { /** * Fetch latest version from GitHub API + * @param verbose Enable verbose logging + * @param backend Backend to fetch version for (uses correct GitHub repo) */ -export async function fetchLatestVersion(verbose = false): Promise { - const response = await fetchJson(GITHUB_API_LATEST_RELEASE, verbose); +export async function fetchLatestVersion( + verbose = false, + backend: CLIProxyBackend = DEFAULT_BACKEND +): Promise { + const urls = getGitHubApiUrls(backend); + const response = await fetchJson(urls.latestRelease, verbose); // Extract version from tag_name (format: "v6.5.27" or "6.5.27") const tagName = response.tag_name as string; @@ -72,16 +78,18 @@ export async function fetchLatestVersion(verbose = false): Promise { /** * Check for updates by comparing installed version with latest release * Uses cache to avoid hitting GitHub API on every run + * Cache is backend-specific to handle different repos for original vs plus */ export async function checkForUpdates( binPath: string, configVersion: string, - verbose = false + verbose = false, + backend: CLIProxyBackend = DEFAULT_BACKEND ): Promise { const currentVersion = readInstalledVersion(binPath, configVersion); - // Try cache first - const cache = readVersionCache(); + // Try cache first (backend-specific) + const cache = readVersionCache(backend); if (cache) { if (verbose) { console.error(`[cliproxy] Using cached version: ${cache.latestVersion}`); @@ -95,10 +103,10 @@ export async function checkForUpdates( }; } - // Fetch from GitHub API - const latestVersion = await fetchLatestVersion(verbose); + // Fetch from GitHub API (backend-specific repo) + const latestVersion = await fetchLatestVersion(verbose, backend); const now = Date.now(); - writeVersionCache(latestVersion); + writeVersionCache(latestVersion, backend); return { hasUpdate: isNewerVersion(latestVersion, currentVersion), @@ -112,10 +120,15 @@ export async function checkForUpdates( /** * Fetch all available versions from GitHub releases * Caches result for 1 hour to avoid rate limiting + * @param verbose Enable verbose logging + * @param backend Backend to fetch versions for (uses correct GitHub repo) */ -export async function fetchAllVersions(verbose = false): Promise { - // Try cache first - const cache = readVersionListCache(); +export async function fetchAllVersions( + verbose = false, + backend: CLIProxyBackend = DEFAULT_BACKEND +): Promise { + // Try cache first (backend-specific) + const cache = readVersionListCache(backend); if (cache) { if (verbose) { console.error(`[cliproxy] Using cached version list (${cache.versions.length} versions)`); @@ -123,8 +136,9 @@ export async function fetchAllVersions(verbose = false): Promise; @@ -147,6 +161,6 @@ export async function fetchAllVersions(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..bea75a25 100644 --- a/src/cliproxy/cliproxy-executor.ts +++ b/src/cliproxy/cliproxy-executor.ts @@ -724,12 +724,13 @@ export async function execClaudeWithCLIProxy( await waitForProxyReady(cfg.port, cfg.timeout, cfg.pollInterval); readySpinner.succeed(`CLIProxy ready on port ${cfg.port}`); } catch (error) { - readySpinner.fail('CLIProxy Plus startup failed'); + const backendLabel = backend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy'; + readySpinner.fail(`${backendLabel} startup failed`); proxy.kill('SIGTERM'); const err = error as Error; console.error(''); - console.error(fail('CLIProxy Plus failed to start')); + console.error(fail(`${backendLabel} failed to start`)); console.error(''); console.error('Possible causes:'); console.error(` 1. Port ${cfg.port} already in use`); @@ -746,9 +747,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/service-manager.ts b/src/cliproxy/service-manager.ts index 6788c66c..ea9dff59 100644 --- a/src/cliproxy/service-manager.ts +++ b/src/cliproxy/service-manager.ts @@ -281,11 +281,18 @@ export async function ensureCliproxyService( proxyProcess = null; } + // Get backend label for error message + const { loadOrCreateUnifiedConfig } = await import('../config/unified-config-loader'); + const { DEFAULT_BACKEND } = await import('./platform-detector'); + const config = loadOrCreateUnifiedConfig(); + const backendLabel = + (config.cliproxy?.backend ?? DEFAULT_BACKEND) === 'plus' ? 'CLIProxy Plus' : 'CLIProxy'; + return { started: false, alreadyRunning: false, port, - error: `CLIProxy Plus failed to start within 5s on port ${port}`, + error: `${backendLabel} failed to start within 5s on port ${port}`, }; } diff --git a/src/cliproxy/services/binary-service.ts b/src/cliproxy/services/binary-service.ts index 7a2458f9..41f8e803 100644 --- a/src/cliproxy/services/binary-service.ts +++ b/src/cliproxy/services/binary-service.ts @@ -58,10 +58,10 @@ export function getBinaryStatus(backend?: CLIProxyBackend): BinaryStatusResult { backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND; const backendConfig = BACKEND_CONFIG[effectiveBackend]; return { - installed: isCLIProxyInstalled(), - currentVersion: getInstalledCliproxyVersion(), - pinnedVersion: getPinnedVersion(), - binaryPath: getCLIProxyPath(), + installed: isCLIProxyInstalled(effectiveBackend), + currentVersion: getInstalledCliproxyVersion(effectiveBackend), + pinnedVersion: getPinnedVersion(effectiveBackend), + binaryPath: getCLIProxyPath(effectiveBackend), fallbackVersion: backendConfig.fallbackVersion, backend: effectiveBackend, }; @@ -70,10 +70,16 @@ export function getBinaryStatus(backend?: CLIProxyBackend): BinaryStatusResult { /** * Check for latest version */ -export async function checkLatestVersion(): Promise { +export async function checkLatestVersion(backend?: CLIProxyBackend): Promise { + const effectiveBackend = + backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND; + try { - const latestVersion = await fetchLatestCliproxyVersion(); - const currentVersion = getInstalledCliproxyVersion(); + // Use checkCliproxyUpdate which is backend-aware (uses correct GitHub repo) + const { checkCliproxyUpdate } = await import('../binary-manager'); + const updateResult = await checkCliproxyUpdate(); + const latestVersion = updateResult.latestVersion; + const currentVersion = getInstalledCliproxyVersion(effectiveBackend); const updateAvailable = latestVersion !== currentVersion; return { @@ -100,7 +106,11 @@ export function isValidVersionFormat(version: string): boolean { /** * Install a specific version and pin it */ -export async function installVersion(version: string, verbose = false): Promise { +export async function installVersion( + version: string, + verbose = false, + backend?: CLIProxyBackend +): Promise { if (!isValidVersionFormat(version)) { return { success: false, @@ -109,9 +119,12 @@ export async function installVersion(version: string, verbose = false): Promise< }; } + const effectiveBackend = + backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND; + try { - await installCliproxyVersion(version, verbose); - savePinnedVersion(version); + await installCliproxyVersion(version, verbose, effectiveBackend); + savePinnedVersion(version, effectiveBackend); return { success: true, @@ -130,13 +143,19 @@ export async function installVersion(version: string, verbose = false): Promise< /** * Install latest version and clear any pin */ -export async function installLatest(verbose = false): Promise { +export async function installLatest( + verbose = false, + backend?: CLIProxyBackend +): Promise { + const effectiveBackend = + backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND; + try { const latestVersion = await fetchLatestCliproxyVersion(); - const currentVersion = getInstalledCliproxyVersion(); - const wasPinned = isVersionPinned(); + const currentVersion = getInstalledCliproxyVersion(effectiveBackend); + const wasPinned = isVersionPinned(effectiveBackend); - if (isCLIProxyInstalled() && latestVersion === currentVersion && !wasPinned) { + if (isCLIProxyInstalled(effectiveBackend) && latestVersion === currentVersion && !wasPinned) { return { success: true, version: latestVersion, @@ -144,8 +163,8 @@ export async function installLatest(verbose = false): Promise { }; } - await installCliproxyVersion(latestVersion, verbose); - clearPinnedVersion(); + await installCliproxyVersion(latestVersion, verbose, effectiveBackend); + clearPinnedVersion(effectiveBackend); return { success: true, @@ -164,20 +183,26 @@ export async function installLatest(verbose = false): Promise { /** * Check if a version is pinned */ -export function isPinned(): boolean { - return isVersionPinned(); +export function isPinned(backend?: CLIProxyBackend): boolean { + const effectiveBackend = + backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND; + return isVersionPinned(effectiveBackend); } /** * Get pinned version if any */ -export function getPinned(): string | null { - return getPinnedVersion(); +export function getPinned(backend?: CLIProxyBackend): string | null { + const effectiveBackend = + backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND; + return getPinnedVersion(effectiveBackend); } /** * Clear version pin */ -export function clearPin(): void { - clearPinnedVersion(); +export function clearPin(backend?: CLIProxyBackend): void { + const effectiveBackend = + backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND; + clearPinnedVersion(effectiveBackend); } 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/commands/cliproxy-command.ts b/src/commands/cliproxy-command.ts index 30360803..ebb20861 100644 --- a/src/commands/cliproxy-command.ts +++ b/src/commands/cliproxy-command.ts @@ -111,6 +111,13 @@ function getEffectiveBackend(cliBackend?: CLIProxyBackend): CLIProxyBackend { return config.cliproxy?.backend ?? DEFAULT_BACKEND; } +/** + * Get display label for backend + */ +function getBackendLabel(backend: CLIProxyBackend): string { + return backend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy'; +} + interface CliproxyProfileArgs { name?: string; provider?: CLIProxyProfileName; @@ -152,8 +159,10 @@ function formatModelOption(model: ModelEntry): string { async function handleCreate(args: string[]): Promise { await initUI(); + const { backend } = parseBackendArg(args); + const effectiveBackend = getEffectiveBackend(backend); const parsedArgs = parseProfileArgs(args); - console.log(header('Create CLIProxy Plus Variant')); + console.log(header(`Create ${getBackendLabel(effectiveBackend)} Variant`)); console.log(''); // Step 1: Profile name @@ -292,7 +301,7 @@ async function handleCreate(args: string[]): Promise { // Create variant console.log(''); - console.log(info('Creating CLIProxy Plus variant...')); + console.log(info(`Creating ${getBackendLabel(effectiveBackend)} variant...`)); const result = createVariant(name, provider, model, account); if (!result.success) { @@ -479,7 +488,7 @@ async function showStatus(verbose: boolean, backend: CLIProxyBackend): Promise { - console.log(info(`Installing CLIProxy Plus v${version}...`)); +async function handleInstallVersion( + version: string, + verbose: boolean, + backend: CLIProxyBackend +): Promise { + const label = getBackendLabel(backend); + console.log(info(`Installing ${label} v${version}...`)); console.log(''); - const result = await installVersion(version, verbose); + const result = await installVersion(version, verbose, backend); if (!result.success) { console.error(''); - console.error(fail(`Failed to install CLIProxy Plus v${version}`)); + console.error(fail(`Failed to install ${label} v${version}`)); console.error(` ${result.error}`); console.error(''); console.error('Possible causes:'); @@ -546,12 +560,12 @@ async function handleInstallVersion(version: string, verbose: boolean): Promise< console.error(' 3. GitHub API rate limiting'); console.error(''); console.error('Check available versions at:'); - console.error(' https://github.com/router-for-me/CLIProxyAPIPlus/releases'); + console.error(` https://github.com/${BACKEND_CONFIG[backend].repo}/releases`); process.exit(1); } console.log(''); - console.log(ok(`CLIProxy Plus v${version} installed (pinned)`)); + console.log(ok(`${label} v${version} installed (pinned)`)); console.log(''); console.log(dim('This version will be used until you run:')); console.log( @@ -560,10 +574,11 @@ async function handleInstallVersion(version: string, verbose: boolean): Promise< console.log(''); } -async function handleInstallLatest(verbose: boolean): Promise { - console.log(info('Fetching latest CLIProxy Plus version...')); +async function handleInstallLatest(verbose: boolean, backend: CLIProxyBackend): Promise { + const label = getBackendLabel(backend); + console.log(info(`Fetching latest ${label} version...`)); - const result = await installLatest(verbose); + const result = await installLatest(verbose, backend); if (!result.success) { console.error(fail(`Failed to install latest version: ${result.error}`)); process.exit(1); @@ -575,7 +590,7 @@ async function handleInstallLatest(verbose: boolean): Promise { } console.log(''); - console.log(ok(`CLIProxy Plus updated to v${result.version}`)); + console.log(ok(`${label} updated to v${result.version}`)); console.log(dim('Auto-update is now enabled.')); console.log(''); } @@ -1036,12 +1051,12 @@ export async function handleCliproxyCommand(args: string[]): Promise { } // Strip leading 'v' prefix and whitespace (user may type " v6.6.80-0 ") version = version.trim().replace(/^v/, ''); - await handleInstallVersion(version, verbose); + await handleInstallVersion(version, verbose, effectiveBackend); return; } if (remainingArgs.includes('--latest') || remainingArgs.includes('--update')) { - await handleInstallLatest(verbose); + await handleInstallLatest(verbose, effectiveBackend); return; } 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, diff --git a/src/web-server/routes/cliproxy-stats-routes.ts b/src/web-server/routes/cliproxy-stats-routes.ts index 25319e51..e907a605 100644 --- a/src/web-server/routes/cliproxy-stats-routes.ts +++ b/src/web-server/routes/cliproxy-stats-routes.ts @@ -34,10 +34,22 @@ import { import { CLIPROXY_MAX_STABLE_VERSION, CLIPROXY_FAULTY_RANGE, + DEFAULT_BACKEND, } from '../../cliproxy/platform-detector'; +import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader'; const router = Router(); +/** Get configured backend from config */ +function getConfiguredBackend() { + try { + const config = loadOrCreateUnifiedConfig(); + return config.cliproxy?.backend || DEFAULT_BACKEND; + } catch { + return DEFAULT_BACKEND; + } +} + /** * Extract status code and model from error log file (lightweight parsing) * Reads first 4KB for model, last 2KB for status code @@ -549,8 +561,9 @@ router.get('/quota/:provider/:accountId', async (req: Request, res: Response): P */ router.get('/versions', async (_req: Request, res: Response): Promise => { try { - const result = await fetchAllVersions(); - const currentVersion = getInstalledCliproxyVersion(); + const backend = getConfiguredBackend(); + const result = await fetchAllVersions(false, backend); + const currentVersion = getInstalledCliproxyVersion(backend); res.json({ ...result, diff --git a/src/web-server/routes/proxy-routes.ts b/src/web-server/routes/proxy-routes.ts index 636fe5d8..b4cc3f68 100644 --- a/src/web-server/routes/proxy-routes.ts +++ b/src/web-server/routes/proxy-routes.ts @@ -11,6 +11,7 @@ import { Router, Request, Response } from 'express'; import { loadOrCreateUnifiedConfig, saveUnifiedConfig } from '../../config/unified-config-loader'; import { testConnection } from '../../cliproxy/remote-proxy-client'; import { isProxyRunning } from '../../cliproxy/services/proxy-lifecycle-service'; +import { DEFAULT_BACKEND } from '../../cliproxy/platform-detector'; import { DEFAULT_CLIPROXY_SERVER_CONFIG, CliproxyServerConfig, @@ -73,7 +74,7 @@ router.put('/', async (req: Request, res: Response) => { router.get('/backend', async (_req: Request, res: Response) => { try { const config = await loadOrCreateUnifiedConfig(); - res.json({ backend: config.cliproxy?.backend ?? 'plus' }); + res.json({ backend: config.cliproxy?.backend ?? DEFAULT_BACKEND }); } catch (error) { console.error('[cliproxy-server-routes] Failed to load backend config:', error); res.status(500).json({ error: 'Failed to load backend config' }); @@ -99,7 +100,7 @@ router.put('/backend', async (req: Request, res: Response) => { // Check if proxy is running - warn about restart requirement const config = await loadOrCreateUnifiedConfig(); - const currentBackend = config.cliproxy?.backend ?? 'plus'; + const currentBackend = config.cliproxy?.backend ?? DEFAULT_BACKEND; if (currentBackend !== backend && isProxyRunning() && !force) { res.status(409).json({ error: 'Proxy is running. Stop proxy first or use force=true to change backend.', diff --git a/ui/src/components/cliproxy/cliproxy-header.tsx b/ui/src/components/cliproxy/cliproxy-header.tsx index 956d37ca..6b5bb51a 100644 --- a/ui/src/components/cliproxy/cliproxy-header.tsx +++ b/ui/src/components/cliproxy/cliproxy-header.tsx @@ -15,6 +15,7 @@ interface VersionInfo { currentVersion: string; isStable: boolean; stabilityMessage?: string; + backendLabel?: string; } interface LoginButtonProps { @@ -127,6 +128,7 @@ export function CliproxyHeader({ currentVersion: data.currentVersion, isStable: data.isStable, stabilityMessage: data.stabilityMessage, + backendLabel: data.backendLabel, }); } }) @@ -157,7 +159,9 @@ export function CliproxyHeader({ {/* Top row: Title and Login Buttons */}
-

CLIProxy Plus

+

+ {versionInfo?.backendLabel ?? 'CLIProxy'} +

CCS-level account management

diff --git a/ui/src/components/cliproxy/cliproxy-stats-overview.tsx b/ui/src/components/cliproxy/cliproxy-stats-overview.tsx index e3f114d2..e4668e80 100644 --- a/ui/src/components/cliproxy/cliproxy-stats-overview.tsx +++ b/ui/src/components/cliproxy/cliproxy-stats-overview.tsx @@ -27,6 +27,7 @@ import { } from 'lucide-react'; import { cn } from '@/lib/utils'; import { useCliproxyStats, useCliproxyStatus } from '@/hooks/use-cliproxy-stats'; +import { useCliproxyUpdateCheck } from '@/hooks/use-cliproxy'; import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context'; interface CliproxyStatsOverviewProps { @@ -37,6 +38,8 @@ export function CliproxyStatsOverview({ className }: CliproxyStatsOverviewProps) const { privacyMode } = usePrivacy(); const { data: status, isLoading: statusLoading } = useCliproxyStatus(); const { data: stats, isLoading: statsLoading, error } = useCliproxyStats(status?.running); + const { data: updateCheck } = useCliproxyUpdateCheck(); + const backendLabel = updateCheck?.backendLabel ?? 'CLIProxy'; const isLoading = statusLoading || (status?.running && statsLoading); @@ -71,7 +74,7 @@ export function CliproxyStatsOverview({ className }: CliproxyStatsOverviewProps) Session Statistics

- Real-time usage metrics from CLIProxy Plus + Real-time usage metrics from {backendLabel}

@@ -146,7 +149,9 @@ export function CliproxyStatsOverview({ className }: CliproxyStatsOverviewProps) Session Statistics -

Real-time usage metrics from CLIProxyAPI

+

+ Real-time usage metrics from {backendLabel} +

{ @@ -102,7 +104,7 @@ export function ModelPreferencesGrid() { - Models available through CLIProxy Plus, grouped by provider + Models available through {backendLabel}, grouped by provider diff --git a/ui/src/components/layout/app-sidebar.tsx b/ui/src/components/layout/app-sidebar.tsx index dc5da3b4..a2cf8040 100644 --- a/ui/src/components/layout/app-sidebar.tsx +++ b/ui/src/components/layout/app-sidebar.tsx @@ -30,6 +30,7 @@ import { } from '@/components/ui/sidebar'; import { CcsLogo } from '@/components/shared/ccs-logo'; import { useSidebar } from '@/hooks/use-sidebar'; +import { useCliproxyUpdateCheck } from '@/hooks/use-cliproxy'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; @@ -87,6 +88,18 @@ export function AppSidebar() { const location = useLocation(); const navigate = useNavigate(); const { state } = useSidebar(); + const { data: updateCheck } = useCliproxyUpdateCheck(); + + // Dynamic label for CLIProxy based on backend + const cliproxyLabel = updateCheck?.backendLabel ?? 'CLIProxy'; + + // Helper to get dynamic label (for CLIProxy route) + const getItemLabel = (item: { path: string; label: string }) => { + if (item.path === '/cliproxy') { + return cliproxyLabel; + } + return item.label; + }; // Helper to check if a route is active (exact match) const isRouteActive = (path: string) => location.pathname === path; @@ -122,13 +135,13 @@ export function AppSidebar() { {/* Click navigates to overview AND opens submenu */} navigate(item.path)} > {item.icon && } - {item.label} + {getItemLabel(item)} @@ -155,12 +168,12 @@ export function AppSidebar() { {item.icon && } - {item.label} + {getItemLabel(item)} {item.badge && ( diff --git a/ui/src/components/monitoring/proxy-status-widget.tsx b/ui/src/components/monitoring/proxy-status-widget.tsx index 2417b6d6..423ad17e 100644 --- a/ui/src/components/monitoring/proxy-status-widget.tsx +++ b/ui/src/components/monitoring/proxy-status-widget.tsx @@ -46,7 +46,7 @@ import { AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; -import { useQuery } from '@tanstack/react-query'; +import { useQuery, useIsMutating } from '@tanstack/react-query'; import { api, type CliproxyServerConfig } from '@/lib/api-client'; import { useProxyStatus, @@ -167,6 +167,9 @@ export function ProxyStatusWidget() { staleTime: 30000, // 30 seconds }); + // Detect if backend switch is in progress (prevents race condition) + const isBackendSwitching = useIsMutating({ mutationKey: ['update-backend'] }) > 0; + // Determine if remote mode is enabled const remoteConfig = cliproxyConfig?.remote; const isRemoteMode = remoteConfig?.enabled && remoteConfig?.host; @@ -176,7 +179,8 @@ export function ProxyStatusWidget() { startProxy.isPending || stopProxy.isPending || restartProxy.isPending || - installVersion.isPending; + installVersion.isPending || + isBackendSwitching; const hasUpdate = updateCheck?.hasUpdate ?? false; const isUnstable = updateCheck?.isStable === false; const currentVersion = updateCheck?.currentVersion; @@ -282,10 +286,10 @@ export function ProxyStatusWidget() { isRunning ? 'bg-green-500 animate-pulse' : 'bg-muted-foreground/30' )} /> - CLIProxy Plus + {updateCheck?.backendLabel ?? 'CLIProxy'} - {/* Right side: icon buttons when running */} + {/* Right side: icon buttons */}
{isLoading ? ( @@ -306,16 +310,15 @@ export function ProxyStatusWidget() { isPending={stopProxy.isPending} variant="destructive-ghost" /> - setIsExpanded(!isExpanded)} - className={isExpanded ? 'bg-muted' : undefined} - /> - ) : ( - - )} + ) : null} + {/* Settings button always visible */} + setIsExpanded(!isExpanded)} + className={isExpanded ? 'bg-muted' : undefined} + />
@@ -372,82 +375,80 @@ export function ProxyStatusWidget() { )} - {/* Expanded section: Version Management */} - {isRunning && ( - - - {/* Section header */} -

Version Management

+ {/* Expanded section: Version Management (available even when not running) */} + + + {/* Section header */} +

Version Management

- {/* Version picker row */} -
- {/* Dropdown - full width, no truncation */} - + {/* Version picker row */} +
+ {/* Dropdown - full width, no truncation */} + - {/* Install button */} - -
- - {/* Stability warning for selected version */} - {selectedVersion && - versionsData?.maxStableVersion && - isNewerVersionClient(selectedVersion, versionsData.maxStableVersion) && ( -
- - Versions above {versionsData.maxStableVersion} have known issues -
+ {/* Install button */} + +
- {/* Sync time */} - {updateCheck?.checkedAt && ( -
- Last checked {formatTimeAgo(updateCheck.checkedAt)} + {/* Stability warning for selected version */} + {selectedVersion && + versionsData?.maxStableVersion && + isNewerVersionClient(selectedVersion, versionsData.maxStableVersion) && ( +
+ + Versions above {versionsData.maxStableVersion} have known issues
)} - - - )} + + {/* Sync time */} + {updateCheck?.checkedAt && ( +
+ Last checked {formatTimeAgo(updateCheck.checkedAt)} +
+ )} + + {/* Not running state */} {!isRunning && ( diff --git a/ui/src/hooks/use-cliproxy.ts b/ui/src/hooks/use-cliproxy.ts index 3d310f02..493fe6ea 100644 --- a/ui/src/hooks/use-cliproxy.ts +++ b/ui/src/hooks/use-cliproxy.ts @@ -333,9 +333,43 @@ export function useCliproxyUpdateCheck() { return useQuery({ queryKey: ['cliproxy-update-check'], queryFn: () => api.cliproxy.updateCheck(), - staleTime: 60 * 60 * 1000, // 1 hour (matches backend cache) - refetchInterval: 60 * 60 * 1000, // Refresh every hour - refetchOnWindowFocus: false, // Don't refresh on window focus (save API calls) + staleTime: 5 * 60 * 1000, // 5 minutes (reduced from 1 hour for faster backend switch response) + refetchInterval: 5 * 60 * 1000, // Refresh every 5 minutes + refetchOnWindowFocus: true, // Refetch on window focus to catch backend changes + }); +} + +// ==================== Backend Management ==================== + +/** + * Hook for switching CLIProxy backend (original vs plus) + * Invalidates all backend-dependent queries to ensure UI consistency + */ +export function useUpdateBackend() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationKey: ['update-backend'], // Used by ProxyStatusWidget to detect backend switching + mutationFn: ({ backend, force = false }: { backend: 'original' | 'plus'; force?: boolean }) => + api.cliproxyServer.updateBackend(backend, force), + onSuccess: () => { + // Invalidate all queries that depend on backend setting + // Use refetchType: 'all' to force immediate refetch even if query is stale + queryClient.invalidateQueries({ queryKey: ['cliproxy-update-check'], refetchType: 'all' }); + queryClient.invalidateQueries({ queryKey: ['cliproxy-versions'], refetchType: 'all' }); + queryClient.invalidateQueries({ queryKey: ['cliproxy-server-config'], refetchType: 'all' }); + queryClient.invalidateQueries({ queryKey: ['proxy-status'] }); + queryClient.invalidateQueries({ queryKey: ['cliproxy-stats'] }); + toast.success('Backend updated'); + }, + onError: (error: Error) => { + // Handle 409 conflict (proxy running) + if (error.message.includes('Proxy is running')) { + toast.error('Stop the proxy first to change backend'); + } else { + toast.error(error.message); + } + }, }); } @@ -345,8 +379,8 @@ export function useCliproxyVersions() { return useQuery({ queryKey: ['cliproxy-versions'], queryFn: () => api.cliproxy.versions(), - staleTime: 60 * 60 * 1000, // 1 hour (matches backend cache) - refetchOnWindowFocus: false, + staleTime: 5 * 60 * 1000, // 5 minutes (reduced for faster backend switch response) + refetchOnWindowFocus: true, // Refetch on focus to catch backend changes }); } diff --git a/ui/src/lib/api-client.ts b/ui/src/lib/api-client.ts index d2361016..445b9133 100644 --- a/ui/src/lib/api-client.ts +++ b/ui/src/lib/api-client.ts @@ -268,6 +268,10 @@ export interface CliproxyUpdateCheckResult { latestVersion: string; fromCache: boolean; checkedAt: number; // Unix timestamp of last check + // Backend info + backend: 'original' | 'plus'; + backendLabel: string; + // Stability fields isStable: boolean; // Whether current version is at or below max stable maxStableVersion: string; // Maximum stable version (e.g., "6.6.80") stabilityMessage?: string; // Warning message if running unstable version diff --git a/ui/src/pages/cliproxy.tsx b/ui/src/pages/cliproxy.tsx index 52d2027e..ec6daa57 100644 --- a/ui/src/pages/cliproxy.tsx +++ b/ui/src/pages/cliproxy.tsx @@ -19,6 +19,7 @@ import { ProxyStatusWidget } from '@/components/monitoring/proxy-status-widget'; import { useCliproxy, useCliproxyAuth, + useCliproxyUpdateCheck, useSetDefaultAccount, useRemoveAccount, usePauseAccount, @@ -179,6 +180,7 @@ export function CliproxyPage() { const queryClient = useQueryClient(); const { data: authData, isLoading: authLoading } = useCliproxyAuth(); const { data: variantsData, isFetching } = useCliproxy(); + const { data: updateCheck } = useCliproxyUpdateCheck(); const setDefaultMutation = useSetDefaultAccount(); const removeMutation = useRemoveAccount(); const pauseMutation = usePauseAccount(); @@ -249,7 +251,7 @@ export function CliproxyPage() {
-

CLIProxy Plus

+

{updateCheck?.backendLabel ?? 'CLIProxy'}

@@ -298,7 +306,7 @@ export default function ProxySection() { Remote

- Connect to a remote CLIProxy Plus server + Connect to a remote {backend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy'} server

@@ -310,16 +318,25 @@ export default function ProxySection() { Backend Binary + {/* Warning when proxy is running - must stop to change backend */} + {isProxyRunning && ( + + + + Stop the running proxy in Instance Status to switch backend. + + + )}
{/* Plus Backend Card */}