From 628148c3590e09dcb04fb205bd41880c3f295e87 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Fri, 23 Jan 2026 10:52:01 -0500 Subject: [PATCH 01/14] fix(cliproxy): make backend switching work with version pins and status - Add backend param to isCLIProxyInstalled(), getCLIProxyPath(), getInstalledCliproxyVersion(), installCliproxyVersion() - Update getBinaryStatus() to pass backend to all helper functions - Add getBackendLabel() helper for dynamic CLI messages - Replace hardcoded "CLIProxy Plus" strings with dynamic labels - Pass --backend flag through install/update command handlers - Import CLIProxyBackend type from types.ts instead of redefining Setting `cliproxy.backend: original` in config.yaml now correctly uses the original backend for version pins and binary operations. --- src/cliproxy/binary-manager.ts | 46 +++++++++++++------- src/cliproxy/binary/index.ts | 1 + src/cliproxy/binary/version-cache.ts | 57 ++++++++++++++++++------- src/cliproxy/services/binary-service.ts | 39 +++++++++++------ src/commands/cliproxy-command.ts | 43 +++++++++++++------ 5 files changed, 129 insertions(+), 57 deletions(-) diff --git a/src/cliproxy/binary-manager.ts b/src/cliproxy/binary-manager.ts index 9ae8088f..84ec01e0 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' @@ -111,7 +112,11 @@ export class BinaryManager { /** 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 +132,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 +177,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(); @@ -223,6 +238,7 @@ export { savePinnedVersion, clearPinnedVersion, isVersionPinned, + migrateVersionPin, }; export default BinaryManager; 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/version-cache.ts b/src/cliproxy/binary/version-cache.ts index 63a19a56..7d120afc 100644 --- a/src/cliproxy/binary/version-cache.ts +++ b/src/cliproxy/binary/version-cache.ts @@ -12,6 +12,8 @@ import { VERSION_PIN_FILE, VersionListCache, } from './types'; +import { DEFAULT_BACKEND } from '../platform-detector'; +import type { CLIProxyBackend } from '../types'; /** * Get path to version cache file @@ -21,10 +23,10 @@ export function getVersionCachePath(): string { } /** - * 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); } /** @@ -98,10 +100,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 +115,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 +131,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 +145,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 ==================== diff --git a/src/cliproxy/services/binary-service.ts b/src/cliproxy/services/binary-service.ts index 7a2458f9..7202c3cb 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, }; @@ -100,7 +100,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 +113,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 +137,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 +157,8 @@ export async function installLatest(verbose = false): Promise { }; } - await installCliproxyVersion(latestVersion, verbose); - clearPinnedVersion(); + await installCliproxyVersion(latestVersion, verbose, effectiveBackend); + clearPinnedVersion(effectiveBackend); return { success: true, diff --git a/src/commands/cliproxy-command.ts b/src/commands/cliproxy-command.ts index 30360803..6a08c32d 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) { @@ -530,14 +539,19 @@ 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; } From 388ab69a970e7bbd249948f34d7ab3e7ab5ddcb9 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Fri, 23 Jan 2026 13:20:31 -0500 Subject: [PATCH 02/14] fix(cliproxy): complete backend param propagation per code review - Add backend param to checkLatestVersion() - Add backend param to isPinned(), getPinned(), clearPin() wrappers - Use getBackendLabel() consistently in showStatus() Addresses review feedback from PR #359. --- src/cliproxy/services/binary-service.ts | 25 +++++++++++++++++-------- src/commands/cliproxy-command.ts | 2 +- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/cliproxy/services/binary-service.ts b/src/cliproxy/services/binary-service.ts index 7202c3cb..6ad3f392 100644 --- a/src/cliproxy/services/binary-service.ts +++ b/src/cliproxy/services/binary-service.ts @@ -70,10 +70,13 @@ 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(); + const currentVersion = getInstalledCliproxyVersion(effectiveBackend); const updateAvailable = latestVersion !== currentVersion; return { @@ -177,20 +180,26 @@ export async function installLatest( /** * 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/commands/cliproxy-command.ts b/src/commands/cliproxy-command.ts index 6a08c32d..ebb20861 100644 --- a/src/commands/cliproxy-command.ts +++ b/src/commands/cliproxy-command.ts @@ -488,7 +488,7 @@ async function showStatus(verbose: boolean, backend: CLIProxyBackend): Promise Date: Fri, 23 Jan 2026 13:25:24 -0500 Subject: [PATCH 03/14] fix(ui): display dynamic backend label in dashboard - Add backend/backendLabel fields to CliproxyUpdateCheckResult - Update proxy-status-widget to show backendLabel from API - Update cliproxy page header to use backendLabel Dashboard now shows "CLIProxy" or "CLIProxy Plus" based on configured backend in config.yaml instead of hardcoded "CLIProxy Plus". --- src/cliproxy/binary-manager.ts | 7 +++++++ ui/src/components/monitoring/proxy-status-widget.tsx | 2 +- ui/src/pages/cliproxy.tsx | 4 +++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/cliproxy/binary-manager.ts b/src/cliproxy/binary-manager.ts index 84ec01e0..06218844 100644 --- a/src/cliproxy/binary-manager.ts +++ b/src/cliproxy/binary-manager.ts @@ -205,6 +205,9 @@ export interface CliproxyUpdateCheckResult { latestVersion: string; fromCache: boolean; checkedAt: number; + // Backend info + backend: CLIProxyBackend; + backendLabel: string; // Stability fields isStable: boolean; maxStableVersion: string; @@ -223,8 +226,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, diff --git a/ui/src/components/monitoring/proxy-status-widget.tsx b/ui/src/components/monitoring/proxy-status-widget.tsx index 2417b6d6..31c69a23 100644 --- a/ui/src/components/monitoring/proxy-status-widget.tsx +++ b/ui/src/components/monitoring/proxy-status-widget.tsx @@ -282,7 +282,7 @@ export function ProxyStatusWidget() { isRunning ? 'bg-green-500 animate-pulse' : 'bg-muted-foreground/30' )} /> - CLIProxy Plus + {updateCheck?.backendLabel ?? 'CLIProxy'} {/* Right side: icon buttons when running */} 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'}

{ @@ -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/hooks/use-cliproxy.ts b/ui/src/hooks/use-cliproxy.ts index 3d310f02..dce70f76 100644 --- a/ui/src/hooks/use-cliproxy.ts +++ b/ui/src/hooks/use-cliproxy.ts @@ -333,9 +333,40 @@ 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({ + mutationFn: ({ backend, force = false }: { backend: 'original' | 'plus'; force?: boolean }) => + api.cliproxyServer.updateBackend(backend, force), + onSuccess: () => { + // Invalidate all queries that depend on backend setting + queryClient.invalidateQueries({ queryKey: ['cliproxy-update-check'] }); + queryClient.invalidateQueries({ queryKey: ['cliproxy-versions'] }); + 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); + } + }, }); } diff --git a/ui/src/pages/settings/sections/proxy/index.tsx b/ui/src/pages/settings/sections/proxy/index.tsx index 275b356f..4c1518f7 100644 --- a/ui/src/pages/settings/sections/proxy/index.tsx +++ b/ui/src/pages/settings/sections/proxy/index.tsx @@ -18,8 +18,8 @@ import { Box, AlertTriangle, } from 'lucide-react'; -import { toast } from 'sonner'; import { useProxyConfig, useRawConfig } from '../../hooks'; +import { useUpdateBackend } from '@/hooks/use-cliproxy'; import { LocalProxyCard } from './local-proxy-card'; import { RemoteProxyCard } from './remote-proxy-card'; import { api } from '@/lib/api-client'; @@ -74,10 +74,10 @@ export default function ProxySection() { } }; - // Backend state (loaded from API) + // Backend state (loaded from API) + mutation hook for proper query invalidation const [backend, setBackend] = useState<'original' | 'plus'>('plus'); - const [backendSaving, setBackendSaving] = useState(false); const [hasKiroGhcpVariants, setHasKiroGhcpVariants] = useState(false); + const updateBackendMutation = useUpdateBackend(); // Fetch backend setting const fetchBackend = useCallback(async () => { @@ -100,24 +100,18 @@ export default function ProxySection() { } }, []); - // Save backend setting - const handleBackendChange = async (value: 'original' | 'plus') => { + // Save backend setting using mutation hook (invalidates all related queries) + const handleBackendChange = (value: 'original' | 'plus') => { const previousValue = backend; - setBackend(value); - setBackendSaving(true); - try { - await api.cliproxyServer.updateBackend(value); - } catch (err) { - const errorMessage = err instanceof Error ? err.message : 'Failed to save backend'; - // Check if error is due to proxy running (409 conflict) - if (errorMessage.includes('Proxy is running')) { - toast.error('Stop the proxy first to change backend'); + setBackend(value); // Optimistic update + updateBackendMutation.mutate( + { backend: value }, + { + onError: () => { + setBackend(previousValue); // Rollback on error + }, } - console.error('[Proxy] Failed to save backend:', err); - setBackend(previousValue); - } finally { - setBackendSaving(false); - } + ); }; // Log when debug mode changes (sanitize sensitive fields) @@ -140,8 +134,10 @@ export default function ProxySection() { useEffect(() => { fetchConfig(); fetchRawConfig(); - fetchBackend(); - checkPlusOnlyVariants(); + // eslint-disable-next-line react-hooks/set-state-in-effect -- Async data fetching on mount is intended + void fetchBackend(); + + void checkPlusOnlyVariants(); }, [fetchConfig, fetchRawConfig, fetchBackend, checkPlusOnlyVariants]); if (loading || !config) { @@ -253,7 +249,8 @@ export default function ProxySection() {

- Configure local or remote CLIProxy Plus connection for proxy-based profiles + Configure local or remote {backend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy'} connection + for proxy-based profiles

{/* Mode Toggle - Card based selection */} @@ -277,7 +274,7 @@ export default function ProxySection() { Local

- Run CLIProxy Plus binary on this machine + Run {backend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy'} binary on this machine

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

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

@@ -314,7 +311,7 @@ export default function ProxySection() { {/* Plus Backend Card */} - - - {/* 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 && ( From 2794a548a57c94002ab8c4f926bd47f04de3f8ff Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Fri, 23 Jan 2026 15:17:32 -0500 Subject: [PATCH 11/14] 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, From f0c845c32e7f389d8427941dd685898a3f894faa Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Fri, 23 Jan 2026 15:37:40 -0500 Subject: [PATCH 12/14] fix(cliproxy): use backend-aware labels in error messages and API - Use checkCliproxyUpdate() in checkLatestVersion() for correct repo - Dynamic error messages based on backend in service-manager - Dynamic error messages based on backend in cliproxy-executor - Generic "CLIProxy" message in oauth-process (no backend context) --- src/cliproxy/auth/oauth-process.ts | 2 +- src/cliproxy/cliproxy-executor.ts | 5 +++-- src/cliproxy/service-manager.ts | 9 ++++++++- src/cliproxy/services/binary-service.ts | 5 ++++- 4 files changed, 16 insertions(+), 5 deletions(-) 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/cliproxy-executor.ts b/src/cliproxy/cliproxy-executor.ts index f019c6f4..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`); 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 6ad3f392..41f8e803 100644 --- a/src/cliproxy/services/binary-service.ts +++ b/src/cliproxy/services/binary-service.ts @@ -75,7 +75,10 @@ export async function checkLatestVersion(backend?: CLIProxyBackend): Promise Date: Fri, 23 Jan 2026 15:44:04 -0500 Subject: [PATCH 13/14] fix(ui): add backend fields to CliproxyUpdateCheckResult type Sync UI type definition with server response that now includes backend and backendLabel fields for dynamic backend display. --- ui/src/lib/api-client.ts | 4 ++++ 1 file changed, 4 insertions(+) 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 From f21cc1fed0c2e19a44421fc9dfb906e5794f7c1e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 23 Jan 2026 20:47:43 +0000 Subject: [PATCH 14/14] chore(release): 7.26.0-dev.1 [skip ci] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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",