From 3e7ce1743b491a7698b210df05bdc815cc02191b Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Wed, 22 Apr 2026 08:45:05 -0400 Subject: [PATCH] hotfix: preserve plus fallback state and guard variant updates --- src/cliproxy/binary-manager.ts | 102 ++++++++++++------ src/cliproxy/services/binary-service.ts | 57 +++++----- src/cliproxy/services/variant-service.ts | 15 ++- src/commands/cliproxy/help-subcommand.ts | 10 +- src/commands/cliproxy/index.ts | 9 +- .../routes/cliproxy-stats-routes.ts | 16 +-- .../cliproxy-dashboard-install-service.ts | 7 +- .../cliproxy/binary-manager-install.test.ts | 26 +++++ .../cliproxy/variant-update-service.test.ts | 13 +++ .../cliproxy-stats-routes-install.test.ts | 10 +- .../pages/settings/sections/proxy/index.tsx | 2 +- 11 files changed, 177 insertions(+), 90 deletions(-) diff --git a/src/cliproxy/binary-manager.ts b/src/cliproxy/binary-manager.ts index 5f507e53..081c7b5c 100644 --- a/src/cliproxy/binary-manager.ts +++ b/src/cliproxy/binary-manager.ts @@ -5,10 +5,17 @@ * Pattern: Mirrors npm install behavior (fast check, download only when needed) */ +import * as fs from 'fs'; +import * as path from 'path'; import { info, warn } from '../utils/ui'; import { getBinDir, CLIPROXY_DEFAULT_PORT } from './config-generator'; import { BinaryInfo, BinaryManagerConfig } from './types'; -import { BACKEND_CONFIG, DEFAULT_BACKEND, CLIPROXY_MAX_STABLE_VERSION } from './platform-detector'; +import { + BACKEND_CONFIG, + DEFAULT_BACKEND, + CLIPROXY_MAX_STABLE_VERSION, + getExecutableName, +} from './platform-detector'; import { stopProxy } from './services/proxy-lifecycle-service'; import { waitForPortFree } from '../utils/port-utils'; import { loadOrCreateUnifiedConfig } from '../config/unified-config-loader'; @@ -16,6 +23,7 @@ import { UpdateCheckResult, checkForUpdates, deleteBinary, + getVersionCachePath, getBinaryPath, isBinaryInstalled, getBinaryInfo, @@ -30,6 +38,7 @@ import { } from './binary'; import type { CLIProxyBackend } from './types'; +import { getVersionListCachePath } from './binary/version-cache'; export const CLIPROXY_PLUS_TRACKING_URL = 'https://github.com/kaitranntt/ccs/issues/1062'; @@ -72,6 +81,41 @@ export function resolveLocalBackend( return 'original'; } +function copyFallbackStateIfMissing(sourcePath: string, targetPath: string): void { + if (!fs.existsSync(sourcePath) || fs.existsSync(targetPath)) return; + fs.mkdirSync(path.dirname(targetPath), { recursive: true }); + fs.copyFileSync(sourcePath, targetPath); +} + +export function syncPlusFallbackStateIfNeeded(configuredBackend: CLIProxyBackend): void { + if (configuredBackend !== 'plus') return; + + const plusDir = getBackendBinDir('plus'); + const originalDir = getBackendBinDir('original'); + + copyFallbackStateIfMissing( + path.join(plusDir, getExecutableName('plus')), + path.join(originalDir, getExecutableName('original')) + ); + copyFallbackStateIfMissing(path.join(plusDir, '.version'), path.join(originalDir, '.version')); + copyFallbackStateIfMissing(getVersionPinPath('plus'), getVersionPinPath('original')); + copyFallbackStateIfMissing(getVersionCachePath('plus'), getVersionCachePath('original')); + copyFallbackStateIfMissing(getVersionListCachePath('plus'), getVersionListCachePath('original')); +} + +function getConfiguredOrDefaultBackend(): CLIProxyBackend { + try { + const config = loadOrCreateUnifiedConfig(); + return config.cliproxy?.backend || DEFAULT_BACKEND; + } catch { + return DEFAULT_BACKEND; + } +} + +export function getStoredConfiguredBackend(): CLIProxyBackend { + return getConfiguredOrDefaultBackend(); +} + /** * Get backend from config, with runtime fallback to 'original' when the user * still has `backend: plus` saved. @@ -83,15 +127,7 @@ export function resolveLocalBackend( * reconfig step, while CCS self-maintains its own Plus fork (future work). */ export function getConfiguredBackend(options: { warnOnFallback?: boolean } = {}): CLIProxyBackend { - let configured: CLIProxyBackend; - try { - const config = loadOrCreateUnifiedConfig(); - configured = config.cliproxy?.backend || DEFAULT_BACKEND; - } catch { - return DEFAULT_BACKEND; - } - - return resolveLocalBackend(configured, options); + return resolveLocalBackend(getConfiguredOrDefaultBackend(), options); } /** @@ -127,9 +163,9 @@ export class BinaryManager { private backend: CLIProxyBackend; constructor(config: Partial = {}, backend?: CLIProxyBackend) { - this.backend = backend - ? resolveLocalBackend(backend, { warnOnFallback: true }) - : getConfiguredBackend({ warnOnFallback: true }); + const configuredBackend = backend ?? getConfiguredOrDefaultBackend(); + syncPlusFallbackStateIfNeeded(configuredBackend); + this.backend = resolveLocalBackend(configuredBackend, { warnOnFallback: true }); const defaultConfig = createDefaultConfig(this.backend); this.config = { ...defaultConfig, ...config }; } @@ -180,7 +216,9 @@ export async function ensureCLIProxyBinary( verbose = false, options: EnsureCLIProxyBinaryOptions = {} ): Promise { - const backend = getConfiguredBackend({ warnOnFallback: true }); + const configuredBackend = getConfiguredOrDefaultBackend(); + syncPlusFallbackStateIfNeeded(configuredBackend); + const backend = resolveLocalBackend(configuredBackend, { warnOnFallback: true }); // Migrate old shared pin to backend-specific location (one-time migration) migrateVersionPin(backend); @@ -211,25 +249,25 @@ export async function ensureCLIProxyBinary( /** Check if CLIProxyAPI binary is installed */ export function isCLIProxyInstalled(backend?: CLIProxyBackend): boolean { - const effectiveBackend = backend - ? resolveLocalBackend(backend, { warnOnFallback: true }) - : getConfiguredBackend({ warnOnFallback: true }); + const configuredBackend = backend ?? getConfiguredOrDefaultBackend(); + syncPlusFallbackStateIfNeeded(configuredBackend); + const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true }); return new BinaryManager({}, effectiveBackend).isBinaryInstalled(); } /** Get CLIProxyAPI binary path (may not exist) */ export function getCLIProxyPath(backend?: CLIProxyBackend): string { - const effectiveBackend = backend - ? resolveLocalBackend(backend, { warnOnFallback: true }) - : getConfiguredBackend({ warnOnFallback: true }); + const configuredBackend = backend ?? getConfiguredOrDefaultBackend(); + syncPlusFallbackStateIfNeeded(configuredBackend); + const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true }); return new BinaryManager({}, effectiveBackend).getBinaryPath(); } /** Get installed CLIProxyAPI version from .version file */ export function getInstalledCliproxyVersion(backend?: CLIProxyBackend): string { - const effectiveBackend = backend - ? resolveLocalBackend(backend, { warnOnFallback: true }) - : getConfiguredBackend({ warnOnFallback: true }); + const configuredBackend = backend ?? getConfiguredOrDefaultBackend(); + syncPlusFallbackStateIfNeeded(configuredBackend); + const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true }); return readInstalledVersion( getBackendBinDir(effectiveBackend), BACKEND_CONFIG[effectiveBackend].fallbackVersion @@ -255,9 +293,9 @@ export async function installCliproxyVersion( backend?: CLIProxyBackend, deps: InstallCliproxyVersionDeps = {} ): Promise { - const effectiveBackend = backend - ? resolveLocalBackend(backend, { warnOnFallback: true }) - : getConfiguredBackend({ warnOnFallback: true }); + const configuredBackend = backend ?? getConfiguredOrDefaultBackend(); + syncPlusFallbackStateIfNeeded(configuredBackend); + const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true }); const manager = deps.createManager?.({ version, verbose, forceVersion: true }, effectiveBackend) ?? new BinaryManager({ version, verbose, forceVersion: true }, effectiveBackend); @@ -298,9 +336,9 @@ export async function installCliproxyVersion( /** Fetch the latest CLIProxyAPI version from GitHub API */ export async function fetchLatestCliproxyVersion(backend?: CLIProxyBackend): Promise { - const effectiveBackend = backend - ? resolveLocalBackend(backend, { warnOnFallback: true }) - : getConfiguredBackend({ warnOnFallback: true }); + const configuredBackend = backend ?? getConfiguredOrDefaultBackend(); + syncPlusFallbackStateIfNeeded(configuredBackend); + const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true }); const result = await new BinaryManager({}, effectiveBackend).checkForUpdates(); return result.latestVersion; } @@ -325,9 +363,9 @@ export interface CliproxyUpdateCheckResult { export async function checkCliproxyUpdate( backend?: CLIProxyBackend ): Promise { - const effectiveBackend = backend - ? resolveLocalBackend(backend, { warnOnFallback: true }) - : getConfiguredBackend({ warnOnFallback: true }); + const configuredBackend = backend ?? getConfiguredOrDefaultBackend(); + syncPlusFallbackStateIfNeeded(configuredBackend); + const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true }); const result = await new BinaryManager({}, effectiveBackend).checkForUpdates(); // Import isNewerVersion for stability check diff --git a/src/cliproxy/services/binary-service.ts b/src/cliproxy/services/binary-service.ts index 798511ad..834df41c 100644 --- a/src/cliproxy/services/binary-service.ts +++ b/src/cliproxy/services/binary-service.ts @@ -19,6 +19,7 @@ import { clearPinnedVersion, isVersionPinned, resolveLocalBackend, + syncPlusFallbackStateIfNeeded, } from '../binary-manager'; import { BACKEND_CONFIG, DEFAULT_BACKEND } from '../platform-detector'; import { CLIProxyBackend } from '../types'; @@ -55,10 +56,10 @@ export interface LatestVersionResult { * Get current binary status for a specific backend */ export function getBinaryStatus(backend?: CLIProxyBackend): BinaryStatusResult { - const effectiveBackend = resolveLocalBackend( - backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND, - { warnOnFallback: true } - ); + const configuredBackend = + backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND; + syncPlusFallbackStateIfNeeded(configuredBackend); + const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true }); const backendConfig = BACKEND_CONFIG[effectiveBackend]; return { installed: isCLIProxyInstalled(effectiveBackend), @@ -74,10 +75,10 @@ export function getBinaryStatus(backend?: CLIProxyBackend): BinaryStatusResult { * Check for latest version */ export async function checkLatestVersion(backend?: CLIProxyBackend): Promise { - const effectiveBackend = resolveLocalBackend( - backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND, - { warnOnFallback: true } - ); + const configuredBackend = + backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND; + syncPlusFallbackStateIfNeeded(configuredBackend); + const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true }); try { // Use checkCliproxyUpdate which is backend-aware (uses correct GitHub repo) @@ -124,10 +125,10 @@ export async function installVersion( }; } - const effectiveBackend = resolveLocalBackend( - backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND, - { warnOnFallback: true } - ); + const configuredBackend = + backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND; + syncPlusFallbackStateIfNeeded(configuredBackend); + const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true }); try { await installCliproxyVersion(version, verbose, effectiveBackend); @@ -154,10 +155,10 @@ export async function installLatest( verbose = false, backend?: CLIProxyBackend ): Promise { - const effectiveBackend = resolveLocalBackend( - backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND, - { warnOnFallback: true } - ); + const configuredBackend = + backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND; + syncPlusFallbackStateIfNeeded(configuredBackend); + const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true }); try { const latestVersion = await fetchLatestCliproxyVersion(effectiveBackend); @@ -193,10 +194,10 @@ export async function installLatest( * Check if a version is pinned */ export function isPinned(backend?: CLIProxyBackend): boolean { - const effectiveBackend = resolveLocalBackend( - backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND, - { warnOnFallback: true } - ); + const configuredBackend = + backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND; + syncPlusFallbackStateIfNeeded(configuredBackend); + const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true }); return isVersionPinned(effectiveBackend); } @@ -204,10 +205,10 @@ export function isPinned(backend?: CLIProxyBackend): boolean { * Get pinned version if any */ export function getPinned(backend?: CLIProxyBackend): string | null { - const effectiveBackend = resolveLocalBackend( - backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND, - { warnOnFallback: true } - ); + const configuredBackend = + backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND; + syncPlusFallbackStateIfNeeded(configuredBackend); + const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true }); return getPinnedVersion(effectiveBackend); } @@ -215,9 +216,9 @@ export function getPinned(backend?: CLIProxyBackend): string | null { * Clear version pin */ export function clearPin(backend?: CLIProxyBackend): void { - const effectiveBackend = resolveLocalBackend( - backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND, - { warnOnFallback: true } - ); + const configuredBackend = + backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND; + syncPlusFallbackStateIfNeeded(configuredBackend); + const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true }); clearPinnedVersion(effectiveBackend); } diff --git a/src/cliproxy/services/variant-service.ts b/src/cliproxy/services/variant-service.ts index f15ae110..90361c78 100644 --- a/src/cliproxy/services/variant-service.ts +++ b/src/cliproxy/services/variant-service.ts @@ -278,6 +278,13 @@ export function updateVariant(name: string, updates: UpdateVariantOptions): Vari } } + if (updates.provider !== undefined) { + const backendError = validateProviderBackend(updates.provider); + if (backendError) { + return { success: false, error: backendError }; + } + } + // Update settings file if (existing.settings) { const settingsPath = existing.settings.replace(/^~/, os.homedir()); @@ -300,14 +307,6 @@ export function updateVariant(name: string, updates: UpdateVariantOptions): Vari // Update config entry if provider/account/target changed if (updates.provider !== undefined || updates.account !== undefined || targetChanged) { const newProvider = updates.provider ?? existing.provider; - - // Validate provider/backend compatibility on provider change - if (updates.provider !== undefined) { - const backendError = validateProviderBackend(updates.provider); - if (backendError) { - return { success: false, error: backendError }; - } - } const newAccount = updates.account !== undefined ? updates.account : existing.account; const newTarget = updates.target ?? existingTarget; diff --git a/src/commands/cliproxy/help-subcommand.ts b/src/commands/cliproxy/help-subcommand.ts index 823b1317..f3f81797 100644 --- a/src/commands/cliproxy/help-subcommand.ts +++ b/src/commands/cliproxy/help-subcommand.ts @@ -84,7 +84,10 @@ export async function showHelp(): Promise { [ 'Options:', [ - ['--backend ', 'Use specific backend: original | plus (default: from config)'], + [ + '--backend ', + 'Use specific backend: original | plus (local default: original; plus currently falls back locally)', + ], ['--target ', 'Default target for created/edited variants: claude | droid'], ['--verbose, -v', 'Show detailed diagnostics including routing hints and quota fetches'], ], @@ -102,6 +105,11 @@ export async function showHelp(): Promise { console.log(dim(' Note: CLIProxy now persists by default. Use "stop" to terminate.')); console.log(dim(' Routing: use gcli/ or agy/ to keep overlapping models pinned.')); + console.log( + dim( + ' Backend: local CLIProxy currently uses original by default; saved plus configs fall back locally.' + ) + ); console.log(''); console.log(subheader('Notes:')); console.log(` Default fallback version: ${color(getFallbackVersion(), 'info')}`); diff --git a/src/commands/cliproxy/index.ts b/src/commands/cliproxy/index.ts index 7456a5ba..b7de5ebb 100644 --- a/src/commands/cliproxy/index.ts +++ b/src/commands/cliproxy/index.ts @@ -6,7 +6,7 @@ */ import { CLIProxyBackend } from '../../cliproxy/types'; -import { getConfiguredBackend, resolveLocalBackend } from '../../cliproxy/binary-manager'; +import { getStoredConfiguredBackend } from '../../cliproxy/binary-manager'; import { type QuotaSupportedProvider, QUOTA_PROVIDER_HELP_TEXT, @@ -70,13 +70,10 @@ function parseBackendArg(args: string[]): { } /** - * Get effective backend (CLI flag > config.yaml > default) + * Get selected backend input (CLI flag > config.yaml > default) */ function getEffectiveBackend(cliBackend?: CLIProxyBackend): CLIProxyBackend { - if (cliBackend) { - return resolveLocalBackend(cliBackend, { warnOnFallback: true }); - } - return getConfiguredBackend({ warnOnFallback: true }); + return cliBackend ?? getStoredConfiguredBackend(); } /** diff --git a/src/web-server/routes/cliproxy-stats-routes.ts b/src/web-server/routes/cliproxy-stats-routes.ts index 7e57dac2..2bf87cc6 100644 --- a/src/web-server/routes/cliproxy-stats-routes.ts +++ b/src/web-server/routes/cliproxy-stats-routes.ts @@ -40,8 +40,8 @@ import { getProxyStatus as getProxyProcessStatus, stopProxy } from '../../clipro import { ensureCliproxyService } from '../../cliproxy/service-manager'; import { checkCliproxyUpdate, - getConfiguredBackend, getInstalledCliproxyVersion, + getStoredConfiguredBackend, } from '../../cliproxy/binary-manager'; import { fetchAllVersions, @@ -146,7 +146,7 @@ export function shouldCacheQuotaResult(result: { } function buildUpdateCheckFallback( - backend: ReturnType, + backend: ReturnType, getInstalledVersionFn: typeof getInstalledCliproxyVersion = getInstalledCliproxyVersion ) { const currentVersion = getInstalledVersionFn(backend); @@ -170,7 +170,7 @@ function buildUpdateCheckFallback( } function buildVersionsFallback( - backend: ReturnType, + backend: ReturnType, getInstalledVersionFn: typeof getInstalledCliproxyVersion = getInstalledCliproxyVersion ) { const currentVersion = getInstalledVersionFn(backend); @@ -198,7 +198,7 @@ interface ResolveVersionsDeps { } export async function resolveCliproxyUpdateCheckPayload( - backend: ReturnType, + backend: ReturnType, deps: ResolveUpdateCheckDeps = {} ) { const checkCliproxyUpdateFn = deps.checkCliproxyUpdateFn ?? checkCliproxyUpdate; @@ -210,7 +210,7 @@ export async function resolveCliproxyUpdateCheckPayload( } export async function resolveCliproxyVersionsPayload( - backend: ReturnType, + backend: ReturnType, deps: ResolveVersionsDeps = {} ) { const fetchAllVersionsFn = deps.fetchAllVersionsFn ?? fetchAllVersions; @@ -401,7 +401,7 @@ router.post('/proxy-stop', async (_req: Request, res: Response): Promise = */ router.get('/update-check', async (_req: Request, res: Response): Promise => { try { - const backend = getConfiguredBackend({ warnOnFallback: true }); + const backend = getStoredConfiguredBackend(); const result = await resolveCliproxyUpdateCheckPayload(backend); res.json(result); @@ -1011,7 +1011,7 @@ router.get('/quota/:provider/:accountId', async (req: Request, res: Response): P */ router.get('/versions', async (_req: Request, res: Response): Promise => { try { - const backend = getConfiguredBackend({ warnOnFallback: true }); + const backend = getStoredConfiguredBackend(); res.json(await resolveCliproxyVersionsPayload(backend)); } catch (error) { console.error(`[cliproxy-stats] ${(error as Error).message}`); @@ -1065,7 +1065,7 @@ router.post('/install', async (req: Request, res: Response): Promise => { return; } - const backend = getConfiguredBackend({ warnOnFallback: true }); + const backend = getStoredConfiguredBackend(); const installResult = await installDashboardCliproxyVersion(version, backend); res.json({ diff --git a/src/web-server/services/cliproxy-dashboard-install-service.ts b/src/web-server/services/cliproxy-dashboard-install-service.ts index 3fc32388..305d3315 100644 --- a/src/web-server/services/cliproxy-dashboard-install-service.ts +++ b/src/web-server/services/cliproxy-dashboard-install-service.ts @@ -1,4 +1,8 @@ -import { installCliproxyVersion, resolveLocalBackend } from '../../cliproxy/binary-manager'; +import { + installCliproxyVersion, + resolveLocalBackend, + syncPlusFallbackStateIfNeeded, +} from '../../cliproxy/binary-manager'; import { ensureCliproxyService, type ServiceStartResult } from '../../cliproxy/service-manager'; import { getProxyStatus as getProxyProcessStatus } from '../../cliproxy/session-tracker'; import { isCliproxyRunning } from '../../cliproxy/stats-fetcher'; @@ -52,6 +56,7 @@ export async function installDashboardCliproxyVersion( backend: CLIProxyBackend, deps: InstallDashboardCliproxyVersionDeps = defaultDeps ): Promise { + syncPlusFallbackStateIfNeeded(backend); const effectiveBackend = resolveLocalBackend(backend, { warnOnFallback: true }); const backendLabel = effectiveBackend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy'; const shouldRestoreService = await wasProxyRunning(deps); diff --git a/tests/unit/cliproxy/binary-manager-install.test.ts b/tests/unit/cliproxy/binary-manager-install.test.ts index f11cee5c..d76f1fdd 100644 --- a/tests/unit/cliproxy/binary-manager-install.test.ts +++ b/tests/unit/cliproxy/binary-manager-install.test.ts @@ -73,6 +73,32 @@ describe('installCliproxyVersion', () => { expect(writes.join('')).toContain('backend: original'); }); + it('reuses plus binary and pin state when local runtime falls back to original', async () => { + const { createEmptyUnifiedConfig } = await import('../../../src/config/unified-config-types'); + const { saveUnifiedConfig } = await import('../../../src/config/unified-config-loader'); + const { savePinnedVersion } = await import('../../../src/cliproxy/binary/version-cache'); + const { getExecutableName } = await import('../../../src/cliproxy/platform-detector'); + const binaryService = await import( + `../../../src/cliproxy/services/binary-service?binary-service-plus-migration=${Date.now()}` + ); + + const config = createEmptyUnifiedConfig(); + config.cliproxy = { ...config.cliproxy, backend: 'plus' }; + saveUnifiedConfig(config); + + const plusBinDir = path.join(tempHome, '.ccs', 'cliproxy', 'bin', 'plus'); + fs.mkdirSync(plusBinDir, { recursive: true }); + fs.writeFileSync(path.join(plusBinDir, getExecutableName('plus')), 'fake-binary'); + fs.writeFileSync(path.join(plusBinDir, '.version'), '6.6.80-0'); + savePinnedVersion('6.6.80-0', 'plus'); + + const status = binaryService.getBinaryStatus(); + + expect(status.installed).toBe(true); + expect(status.pinnedVersion).toBe('6.6.80-0'); + expect(status.binaryPath).toContain('/original/'); + }); + it('attempts to stop the proxy even when there is no tracked running session', async () => { const calls = { stopProxy: 0, diff --git a/tests/unit/cliproxy/variant-update-service.test.ts b/tests/unit/cliproxy/variant-update-service.test.ts index 25ef9a21..abd2039c 100644 --- a/tests/unit/cliproxy/variant-update-service.test.ts +++ b/tests/unit/cliproxy/variant-update-service.test.ts @@ -102,6 +102,19 @@ cliproxy: expect(error).toContain('issues/1062'); }); + it('leaves the settings file unchanged when a plus-only provider update is rejected', () => { + const settingsPath = path.join(tmpDir, 'gemini-demo.settings.json'); + const before = fs.readFileSync(settingsPath, 'utf-8'); + + const result = updateVariant('demo', { + provider: 'ghcp', + model: 'gpt-5.4-mini', + }); + + expect(result.success).toBe(false); + expect(fs.readFileSync(settingsPath, 'utf-8')).toBe(before); + }); + it('updates provider and regenerates provider-specific core env in same settings file', () => { const result = updateVariant('demo', { provider: 'codex', diff --git a/tests/unit/web-server/cliproxy-stats-routes-install.test.ts b/tests/unit/web-server/cliproxy-stats-routes-install.test.ts index 60470a3c..296851dc 100644 --- a/tests/unit/web-server/cliproxy-stats-routes-install.test.ts +++ b/tests/unit/web-server/cliproxy-stats-routes-install.test.ts @@ -31,16 +31,16 @@ beforeAll(async () => { )); const ccsDir = path.join(tempHome, '.ccs'); - const originalBinDir = path.join(ccsDir, 'cliproxy', 'bin', 'original'); - fs.mkdirSync(originalBinDir, { recursive: true }); + const plusBinDir = path.join(ccsDir, 'cliproxy', 'bin', 'plus'); + fs.mkdirSync(plusBinDir, { recursive: true }); setGlobalConfigDir(ccsDir); const config = createEmptyUnifiedConfig(); config.cliproxy = { backend: 'plus' }; saveUnifiedConfig(config); - writeInstalledVersion(originalBinDir, '6.6.80'); - writeVersionCache('6.6.89', 'original'); + writeInstalledVersion(plusBinDir, '6.6.80'); + writeVersionCache('6.6.89', 'plus'); writeVersionListCache( { versions: ['6.6.89', '6.6.88', '6.6.81', '6.6.80'], @@ -48,7 +48,7 @@ beforeAll(async () => { latest: '6.6.89', checkedAt: Date.now(), }, - 'original' + 'plus' ); ({ default: cliproxyStatsRoutes } = await import( diff --git a/ui/src/pages/settings/sections/proxy/index.tsx b/ui/src/pages/settings/sections/proxy/index.tsx index 218d4326..76ec80e1 100644 --- a/ui/src/pages/settings/sections/proxy/index.tsx +++ b/ui/src/pages/settings/sections/proxy/index.tsx @@ -36,7 +36,7 @@ import { useTranslation } from 'react-i18next'; const DEBUG_MODE_KEY = 'ccs_debug_mode'; /** Providers only available on CLIProxyAPIPlus */ -const PLUS_ONLY_PROVIDERS = ['kiro', 'ghcp']; +const PLUS_ONLY_PROVIDERS = ['kiro', 'ghcp', 'cursor', 'gitlab', 'codebuddy', 'kilo']; function normalizeRiskAckPhrase(value: string): string { return value.trim().replace(/\s+/g, ' ').toUpperCase();