hotfix: preserve plus fallback state and guard variant updates

This commit is contained in:
Tam Nhu Tran
2026-04-22 08:45:05 -04:00
parent 7b1a7f3a09
commit 5b5f4a5ef5
11 changed files with 177 additions and 90 deletions
+70 -32
View File
@@ -5,10 +5,17 @@
* Pattern: Mirrors npm install behavior (fast check, download only when needed) * 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 { info, warn } from '../utils/ui';
import { getBinDir, CLIPROXY_DEFAULT_PORT } from './config-generator'; import { getBinDir, CLIPROXY_DEFAULT_PORT } from './config-generator';
import { BinaryInfo, BinaryManagerConfig } from './types'; 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 { stopProxy } from './services/proxy-lifecycle-service';
import { waitForPortFree } from '../utils/port-utils'; import { waitForPortFree } from '../utils/port-utils';
import { loadOrCreateUnifiedConfig } from '../config/unified-config-loader'; import { loadOrCreateUnifiedConfig } from '../config/unified-config-loader';
@@ -16,6 +23,7 @@ import {
UpdateCheckResult, UpdateCheckResult,
checkForUpdates, checkForUpdates,
deleteBinary, deleteBinary,
getVersionCachePath,
getBinaryPath, getBinaryPath,
isBinaryInstalled, isBinaryInstalled,
getBinaryInfo, getBinaryInfo,
@@ -30,6 +38,7 @@ import {
} from './binary'; } from './binary';
import type { CLIProxyBackend } from './types'; import type { CLIProxyBackend } from './types';
import { getVersionListCachePath } from './binary/version-cache';
export const CLIPROXY_PLUS_TRACKING_URL = 'https://github.com/kaitranntt/ccs/issues/1062'; export const CLIPROXY_PLUS_TRACKING_URL = 'https://github.com/kaitranntt/ccs/issues/1062';
@@ -72,6 +81,41 @@ export function resolveLocalBackend(
return 'original'; 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 * Get backend from config, with runtime fallback to 'original' when the user
* still has `backend: plus` saved. * still has `backend: plus` saved.
@@ -83,15 +127,7 @@ export function resolveLocalBackend(
* reconfig step, while CCS self-maintains its own Plus fork (future work). * reconfig step, while CCS self-maintains its own Plus fork (future work).
*/ */
export function getConfiguredBackend(options: { warnOnFallback?: boolean } = {}): CLIProxyBackend { export function getConfiguredBackend(options: { warnOnFallback?: boolean } = {}): CLIProxyBackend {
let configured: CLIProxyBackend; return resolveLocalBackend(getConfiguredOrDefaultBackend(), options);
try {
const config = loadOrCreateUnifiedConfig();
configured = config.cliproxy?.backend || DEFAULT_BACKEND;
} catch {
return DEFAULT_BACKEND;
}
return resolveLocalBackend(configured, options);
} }
/** /**
@@ -127,9 +163,9 @@ export class BinaryManager {
private backend: CLIProxyBackend; private backend: CLIProxyBackend;
constructor(config: Partial<BinaryManagerConfig> = {}, backend?: CLIProxyBackend) { constructor(config: Partial<BinaryManagerConfig> = {}, backend?: CLIProxyBackend) {
this.backend = backend const configuredBackend = backend ?? getConfiguredOrDefaultBackend();
? resolveLocalBackend(backend, { warnOnFallback: true }) syncPlusFallbackStateIfNeeded(configuredBackend);
: getConfiguredBackend({ warnOnFallback: true }); this.backend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
const defaultConfig = createDefaultConfig(this.backend); const defaultConfig = createDefaultConfig(this.backend);
this.config = { ...defaultConfig, ...config }; this.config = { ...defaultConfig, ...config };
} }
@@ -180,7 +216,9 @@ export async function ensureCLIProxyBinary(
verbose = false, verbose = false,
options: EnsureCLIProxyBinaryOptions = {} options: EnsureCLIProxyBinaryOptions = {}
): Promise<string> { ): Promise<string> {
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) // Migrate old shared pin to backend-specific location (one-time migration)
migrateVersionPin(backend); migrateVersionPin(backend);
@@ -211,25 +249,25 @@ export async function ensureCLIProxyBinary(
/** Check if CLIProxyAPI binary is installed */ /** Check if CLIProxyAPI binary is installed */
export function isCLIProxyInstalled(backend?: CLIProxyBackend): boolean { export function isCLIProxyInstalled(backend?: CLIProxyBackend): boolean {
const effectiveBackend = backend const configuredBackend = backend ?? getConfiguredOrDefaultBackend();
? resolveLocalBackend(backend, { warnOnFallback: true }) syncPlusFallbackStateIfNeeded(configuredBackend);
: getConfiguredBackend({ warnOnFallback: true }); const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
return new BinaryManager({}, effectiveBackend).isBinaryInstalled(); return new BinaryManager({}, effectiveBackend).isBinaryInstalled();
} }
/** Get CLIProxyAPI binary path (may not exist) */ /** Get CLIProxyAPI binary path (may not exist) */
export function getCLIProxyPath(backend?: CLIProxyBackend): string { export function getCLIProxyPath(backend?: CLIProxyBackend): string {
const effectiveBackend = backend const configuredBackend = backend ?? getConfiguredOrDefaultBackend();
? resolveLocalBackend(backend, { warnOnFallback: true }) syncPlusFallbackStateIfNeeded(configuredBackend);
: getConfiguredBackend({ warnOnFallback: true }); const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
return new BinaryManager({}, effectiveBackend).getBinaryPath(); return new BinaryManager({}, effectiveBackend).getBinaryPath();
} }
/** Get installed CLIProxyAPI version from .version file */ /** Get installed CLIProxyAPI version from .version file */
export function getInstalledCliproxyVersion(backend?: CLIProxyBackend): string { export function getInstalledCliproxyVersion(backend?: CLIProxyBackend): string {
const effectiveBackend = backend const configuredBackend = backend ?? getConfiguredOrDefaultBackend();
? resolveLocalBackend(backend, { warnOnFallback: true }) syncPlusFallbackStateIfNeeded(configuredBackend);
: getConfiguredBackend({ warnOnFallback: true }); const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
return readInstalledVersion( return readInstalledVersion(
getBackendBinDir(effectiveBackend), getBackendBinDir(effectiveBackend),
BACKEND_CONFIG[effectiveBackend].fallbackVersion BACKEND_CONFIG[effectiveBackend].fallbackVersion
@@ -255,9 +293,9 @@ export async function installCliproxyVersion(
backend?: CLIProxyBackend, backend?: CLIProxyBackend,
deps: InstallCliproxyVersionDeps = {} deps: InstallCliproxyVersionDeps = {}
): Promise<void> { ): Promise<void> {
const effectiveBackend = backend const configuredBackend = backend ?? getConfiguredOrDefaultBackend();
? resolveLocalBackend(backend, { warnOnFallback: true }) syncPlusFallbackStateIfNeeded(configuredBackend);
: getConfiguredBackend({ warnOnFallback: true }); const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
const manager = const manager =
deps.createManager?.({ version, verbose, forceVersion: true }, effectiveBackend) ?? deps.createManager?.({ version, verbose, forceVersion: true }, effectiveBackend) ??
new BinaryManager({ 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 */ /** Fetch the latest CLIProxyAPI version from GitHub API */
export async function fetchLatestCliproxyVersion(backend?: CLIProxyBackend): Promise<string> { export async function fetchLatestCliproxyVersion(backend?: CLIProxyBackend): Promise<string> {
const effectiveBackend = backend const configuredBackend = backend ?? getConfiguredOrDefaultBackend();
? resolveLocalBackend(backend, { warnOnFallback: true }) syncPlusFallbackStateIfNeeded(configuredBackend);
: getConfiguredBackend({ warnOnFallback: true }); const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
const result = await new BinaryManager({}, effectiveBackend).checkForUpdates(); const result = await new BinaryManager({}, effectiveBackend).checkForUpdates();
return result.latestVersion; return result.latestVersion;
} }
@@ -325,9 +363,9 @@ export interface CliproxyUpdateCheckResult {
export async function checkCliproxyUpdate( export async function checkCliproxyUpdate(
backend?: CLIProxyBackend backend?: CLIProxyBackend
): Promise<CliproxyUpdateCheckResult> { ): Promise<CliproxyUpdateCheckResult> {
const effectiveBackend = backend const configuredBackend = backend ?? getConfiguredOrDefaultBackend();
? resolveLocalBackend(backend, { warnOnFallback: true }) syncPlusFallbackStateIfNeeded(configuredBackend);
: getConfiguredBackend({ warnOnFallback: true }); const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
const result = await new BinaryManager({}, effectiveBackend).checkForUpdates(); const result = await new BinaryManager({}, effectiveBackend).checkForUpdates();
// Import isNewerVersion for stability check // Import isNewerVersion for stability check
+29 -28
View File
@@ -19,6 +19,7 @@ import {
clearPinnedVersion, clearPinnedVersion,
isVersionPinned, isVersionPinned,
resolveLocalBackend, resolveLocalBackend,
syncPlusFallbackStateIfNeeded,
} from '../binary-manager'; } from '../binary-manager';
import { BACKEND_CONFIG, DEFAULT_BACKEND } from '../platform-detector'; import { BACKEND_CONFIG, DEFAULT_BACKEND } from '../platform-detector';
import { CLIProxyBackend } from '../types'; import { CLIProxyBackend } from '../types';
@@ -55,10 +56,10 @@ export interface LatestVersionResult {
* Get current binary status for a specific backend * Get current binary status for a specific backend
*/ */
export function getBinaryStatus(backend?: CLIProxyBackend): BinaryStatusResult { export function getBinaryStatus(backend?: CLIProxyBackend): BinaryStatusResult {
const effectiveBackend = resolveLocalBackend( const configuredBackend =
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND, backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
{ warnOnFallback: true } syncPlusFallbackStateIfNeeded(configuredBackend);
); const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
const backendConfig = BACKEND_CONFIG[effectiveBackend]; const backendConfig = BACKEND_CONFIG[effectiveBackend];
return { return {
installed: isCLIProxyInstalled(effectiveBackend), installed: isCLIProxyInstalled(effectiveBackend),
@@ -74,10 +75,10 @@ export function getBinaryStatus(backend?: CLIProxyBackend): BinaryStatusResult {
* Check for latest version * Check for latest version
*/ */
export async function checkLatestVersion(backend?: CLIProxyBackend): Promise<LatestVersionResult> { export async function checkLatestVersion(backend?: CLIProxyBackend): Promise<LatestVersionResult> {
const effectiveBackend = resolveLocalBackend( const configuredBackend =
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND, backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
{ warnOnFallback: true } syncPlusFallbackStateIfNeeded(configuredBackend);
); const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
try { try {
// Use checkCliproxyUpdate which is backend-aware (uses correct GitHub repo) // Use checkCliproxyUpdate which is backend-aware (uses correct GitHub repo)
@@ -124,10 +125,10 @@ export async function installVersion(
}; };
} }
const effectiveBackend = resolveLocalBackend( const configuredBackend =
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND, backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
{ warnOnFallback: true } syncPlusFallbackStateIfNeeded(configuredBackend);
); const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
try { try {
await installCliproxyVersion(version, verbose, effectiveBackend); await installCliproxyVersion(version, verbose, effectiveBackend);
@@ -154,10 +155,10 @@ export async function installLatest(
verbose = false, verbose = false,
backend?: CLIProxyBackend backend?: CLIProxyBackend
): Promise<InstallResult> { ): Promise<InstallResult> {
const effectiveBackend = resolveLocalBackend( const configuredBackend =
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND, backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
{ warnOnFallback: true } syncPlusFallbackStateIfNeeded(configuredBackend);
); const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
try { try {
const latestVersion = await fetchLatestCliproxyVersion(effectiveBackend); const latestVersion = await fetchLatestCliproxyVersion(effectiveBackend);
@@ -193,10 +194,10 @@ export async function installLatest(
* Check if a version is pinned * Check if a version is pinned
*/ */
export function isPinned(backend?: CLIProxyBackend): boolean { export function isPinned(backend?: CLIProxyBackend): boolean {
const effectiveBackend = resolveLocalBackend( const configuredBackend =
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND, backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
{ warnOnFallback: true } syncPlusFallbackStateIfNeeded(configuredBackend);
); const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
return isVersionPinned(effectiveBackend); return isVersionPinned(effectiveBackend);
} }
@@ -204,10 +205,10 @@ export function isPinned(backend?: CLIProxyBackend): boolean {
* Get pinned version if any * Get pinned version if any
*/ */
export function getPinned(backend?: CLIProxyBackend): string | null { export function getPinned(backend?: CLIProxyBackend): string | null {
const effectiveBackend = resolveLocalBackend( const configuredBackend =
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND, backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
{ warnOnFallback: true } syncPlusFallbackStateIfNeeded(configuredBackend);
); const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
return getPinnedVersion(effectiveBackend); return getPinnedVersion(effectiveBackend);
} }
@@ -215,9 +216,9 @@ export function getPinned(backend?: CLIProxyBackend): string | null {
* Clear version pin * Clear version pin
*/ */
export function clearPin(backend?: CLIProxyBackend): void { export function clearPin(backend?: CLIProxyBackend): void {
const effectiveBackend = resolveLocalBackend( const configuredBackend =
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND, backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
{ warnOnFallback: true } syncPlusFallbackStateIfNeeded(configuredBackend);
); const effectiveBackend = resolveLocalBackend(configuredBackend, { warnOnFallback: true });
clearPinnedVersion(effectiveBackend); clearPinnedVersion(effectiveBackend);
} }
+7 -8
View File
@@ -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 // Update settings file
if (existing.settings) { if (existing.settings) {
const settingsPath = existing.settings.replace(/^~/, os.homedir()); 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 // Update config entry if provider/account/target changed
if (updates.provider !== undefined || updates.account !== undefined || targetChanged) { if (updates.provider !== undefined || updates.account !== undefined || targetChanged) {
const newProvider = updates.provider ?? existing.provider; 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 newAccount = updates.account !== undefined ? updates.account : existing.account;
const newTarget = updates.target ?? existingTarget; const newTarget = updates.target ?? existingTarget;
+9 -1
View File
@@ -84,7 +84,10 @@ export async function showHelp(): Promise<void> {
[ [
'Options:', 'Options:',
[ [
['--backend <type>', 'Use specific backend: original | plus (default: from config)'], [
'--backend <type>',
'Use specific backend: original | plus (local default: original; plus currently falls back locally)',
],
['--target <cli>', 'Default target for created/edited variants: claude | droid'], ['--target <cli>', 'Default target for created/edited variants: claude | droid'],
['--verbose, -v', 'Show detailed diagnostics including routing hints and quota fetches'], ['--verbose, -v', 'Show detailed diagnostics including routing hints and quota fetches'],
], ],
@@ -102,6 +105,11 @@ export async function showHelp(): Promise<void> {
console.log(dim(' Note: CLIProxy now persists by default. Use "stop" to terminate.')); console.log(dim(' Note: CLIProxy now persists by default. Use "stop" to terminate.'));
console.log(dim(' Routing: use gcli/<model> or agy/<model> to keep overlapping models pinned.')); console.log(dim(' Routing: use gcli/<model> or agy/<model> 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('');
console.log(subheader('Notes:')); console.log(subheader('Notes:'));
console.log(` Default fallback version: ${color(getFallbackVersion(), 'info')}`); console.log(` Default fallback version: ${color(getFallbackVersion(), 'info')}`);
+3 -6
View File
@@ -6,7 +6,7 @@
*/ */
import { CLIProxyBackend } from '../../cliproxy/types'; import { CLIProxyBackend } from '../../cliproxy/types';
import { getConfiguredBackend, resolveLocalBackend } from '../../cliproxy/binary-manager'; import { getStoredConfiguredBackend } from '../../cliproxy/binary-manager';
import { import {
type QuotaSupportedProvider, type QuotaSupportedProvider,
QUOTA_PROVIDER_HELP_TEXT, 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 { function getEffectiveBackend(cliBackend?: CLIProxyBackend): CLIProxyBackend {
if (cliBackend) { return cliBackend ?? getStoredConfiguredBackend();
return resolveLocalBackend(cliBackend, { warnOnFallback: true });
}
return getConfiguredBackend({ warnOnFallback: true });
} }
/** /**
@@ -40,8 +40,8 @@ import { getProxyStatus as getProxyProcessStatus, stopProxy } from '../../clipro
import { ensureCliproxyService } from '../../cliproxy/service-manager'; import { ensureCliproxyService } from '../../cliproxy/service-manager';
import { import {
checkCliproxyUpdate, checkCliproxyUpdate,
getConfiguredBackend,
getInstalledCliproxyVersion, getInstalledCliproxyVersion,
getStoredConfiguredBackend,
} from '../../cliproxy/binary-manager'; } from '../../cliproxy/binary-manager';
import { import {
fetchAllVersions, fetchAllVersions,
@@ -146,7 +146,7 @@ export function shouldCacheQuotaResult(result: {
} }
function buildUpdateCheckFallback( function buildUpdateCheckFallback(
backend: ReturnType<typeof getConfiguredBackend>, backend: ReturnType<typeof getStoredConfiguredBackend>,
getInstalledVersionFn: typeof getInstalledCliproxyVersion = getInstalledCliproxyVersion getInstalledVersionFn: typeof getInstalledCliproxyVersion = getInstalledCliproxyVersion
) { ) {
const currentVersion = getInstalledVersionFn(backend); const currentVersion = getInstalledVersionFn(backend);
@@ -170,7 +170,7 @@ function buildUpdateCheckFallback(
} }
function buildVersionsFallback( function buildVersionsFallback(
backend: ReturnType<typeof getConfiguredBackend>, backend: ReturnType<typeof getStoredConfiguredBackend>,
getInstalledVersionFn: typeof getInstalledCliproxyVersion = getInstalledCliproxyVersion getInstalledVersionFn: typeof getInstalledCliproxyVersion = getInstalledCliproxyVersion
) { ) {
const currentVersion = getInstalledVersionFn(backend); const currentVersion = getInstalledVersionFn(backend);
@@ -198,7 +198,7 @@ interface ResolveVersionsDeps {
} }
export async function resolveCliproxyUpdateCheckPayload( export async function resolveCliproxyUpdateCheckPayload(
backend: ReturnType<typeof getConfiguredBackend>, backend: ReturnType<typeof getStoredConfiguredBackend>,
deps: ResolveUpdateCheckDeps = {} deps: ResolveUpdateCheckDeps = {}
) { ) {
const checkCliproxyUpdateFn = deps.checkCliproxyUpdateFn ?? checkCliproxyUpdate; const checkCliproxyUpdateFn = deps.checkCliproxyUpdateFn ?? checkCliproxyUpdate;
@@ -210,7 +210,7 @@ export async function resolveCliproxyUpdateCheckPayload(
} }
export async function resolveCliproxyVersionsPayload( export async function resolveCliproxyVersionsPayload(
backend: ReturnType<typeof getConfiguredBackend>, backend: ReturnType<typeof getStoredConfiguredBackend>,
deps: ResolveVersionsDeps = {} deps: ResolveVersionsDeps = {}
) { ) {
const fetchAllVersionsFn = deps.fetchAllVersionsFn ?? fetchAllVersions; const fetchAllVersionsFn = deps.fetchAllVersionsFn ?? fetchAllVersions;
@@ -401,7 +401,7 @@ router.post('/proxy-stop', async (_req: Request, res: Response): Promise<void> =
*/ */
router.get('/update-check', async (_req: Request, res: Response): Promise<void> => { router.get('/update-check', async (_req: Request, res: Response): Promise<void> => {
try { try {
const backend = getConfiguredBackend({ warnOnFallback: true }); const backend = getStoredConfiguredBackend();
const result = await resolveCliproxyUpdateCheckPayload(backend); const result = await resolveCliproxyUpdateCheckPayload(backend);
res.json(result); 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<void> => { router.get('/versions', async (_req: Request, res: Response): Promise<void> => {
try { try {
const backend = getConfiguredBackend({ warnOnFallback: true }); const backend = getStoredConfiguredBackend();
res.json(await resolveCliproxyVersionsPayload(backend)); res.json(await resolveCliproxyVersionsPayload(backend));
} catch (error) { } catch (error) {
console.error(`[cliproxy-stats] ${(error as Error).message}`); console.error(`[cliproxy-stats] ${(error as Error).message}`);
@@ -1065,7 +1065,7 @@ router.post('/install', async (req: Request, res: Response): Promise<void> => {
return; return;
} }
const backend = getConfiguredBackend({ warnOnFallback: true }); const backend = getStoredConfiguredBackend();
const installResult = await installDashboardCliproxyVersion(version, backend); const installResult = await installDashboardCliproxyVersion(version, backend);
res.json({ res.json({
@@ -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 { ensureCliproxyService, type ServiceStartResult } from '../../cliproxy/service-manager';
import { getProxyStatus as getProxyProcessStatus } from '../../cliproxy/session-tracker'; import { getProxyStatus as getProxyProcessStatus } from '../../cliproxy/session-tracker';
import { isCliproxyRunning } from '../../cliproxy/stats-fetcher'; import { isCliproxyRunning } from '../../cliproxy/stats-fetcher';
@@ -52,6 +56,7 @@ export async function installDashboardCliproxyVersion(
backend: CLIProxyBackend, backend: CLIProxyBackend,
deps: InstallDashboardCliproxyVersionDeps = defaultDeps deps: InstallDashboardCliproxyVersionDeps = defaultDeps
): Promise<DashboardCliproxyInstallResult> { ): Promise<DashboardCliproxyInstallResult> {
syncPlusFallbackStateIfNeeded(backend);
const effectiveBackend = resolveLocalBackend(backend, { warnOnFallback: true }); const effectiveBackend = resolveLocalBackend(backend, { warnOnFallback: true });
const backendLabel = effectiveBackend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy'; const backendLabel = effectiveBackend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy';
const shouldRestoreService = await wasProxyRunning(deps); const shouldRestoreService = await wasProxyRunning(deps);
@@ -73,6 +73,32 @@ describe('installCliproxyVersion', () => {
expect(writes.join('')).toContain('backend: original'); 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 () => { it('attempts to stop the proxy even when there is no tracked running session', async () => {
const calls = { const calls = {
stopProxy: 0, stopProxy: 0,
@@ -102,6 +102,19 @@ cliproxy:
expect(error).toContain('issues/1062'); 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', () => { it('updates provider and regenerates provider-specific core env in same settings file', () => {
const result = updateVariant('demo', { const result = updateVariant('demo', {
provider: 'codex', provider: 'codex',
@@ -31,16 +31,16 @@ beforeAll(async () => {
)); ));
const ccsDir = path.join(tempHome, '.ccs'); const ccsDir = path.join(tempHome, '.ccs');
const originalBinDir = path.join(ccsDir, 'cliproxy', 'bin', 'original'); const plusBinDir = path.join(ccsDir, 'cliproxy', 'bin', 'plus');
fs.mkdirSync(originalBinDir, { recursive: true }); fs.mkdirSync(plusBinDir, { recursive: true });
setGlobalConfigDir(ccsDir); setGlobalConfigDir(ccsDir);
const config = createEmptyUnifiedConfig(); const config = createEmptyUnifiedConfig();
config.cliproxy = { backend: 'plus' }; config.cliproxy = { backend: 'plus' };
saveUnifiedConfig(config); saveUnifiedConfig(config);
writeInstalledVersion(originalBinDir, '6.6.80'); writeInstalledVersion(plusBinDir, '6.6.80');
writeVersionCache('6.6.89', 'original'); writeVersionCache('6.6.89', 'plus');
writeVersionListCache( writeVersionListCache(
{ {
versions: ['6.6.89', '6.6.88', '6.6.81', '6.6.80'], versions: ['6.6.89', '6.6.88', '6.6.81', '6.6.80'],
@@ -48,7 +48,7 @@ beforeAll(async () => {
latest: '6.6.89', latest: '6.6.89',
checkedAt: Date.now(), checkedAt: Date.now(),
}, },
'original' 'plus'
); );
({ default: cliproxyStatsRoutes } = await import( ({ default: cliproxyStatsRoutes } = await import(
@@ -36,7 +36,7 @@ import { useTranslation } from 'react-i18next';
const DEBUG_MODE_KEY = 'ccs_debug_mode'; const DEBUG_MODE_KEY = 'ccs_debug_mode';
/** Providers only available on CLIProxyAPIPlus */ /** 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 { function normalizeRiskAckPhrase(value: string): string {
return value.trim().replace(/\s+/g, ' ').toUpperCase(); return value.trim().replace(/\s+/g, ' ').toUpperCase();