diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fd1a8a5..39f365bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,41 @@ +## [7.73.1](https://github.com/kaitranntt/ccs/compare/v7.73.0...v7.73.1) (2026-04-22) + +### Hotfixes + +* **cliproxy:** fallback from deleted CLIProxyAPIPlus to original backend ([60cbdce](https://github.com/kaitranntt/ccs/commit/60cbdce83dd429aeb6df5f1439ba99edd96acc3a)), closes [#1062](https://github.com/kaitranntt/ccs/issues/1062) +* close cliproxy plus fallback gaps ([7b1a7f3](https://github.com/kaitranntt/ccs/commit/7b1a7f3a09b5c6394897b5f95ad32bc9109e6a21)) +* preserve plus fallback state and guard variant updates ([5b5f4a5](https://github.com/kaitranntt/ccs/commit/5b5f4a5ef52f91cb6d4bc7fd3413eac714945d4a)) + +## [7.73.0](https://github.com/kaitranntt/ccs/compare/v7.72.1...v7.73.0) (2026-04-19) + +### Features + +* **proxy:** enhance Anthropic-to-OpenAI message transformation and schema sanitization ([2672e35](https://github.com/kaitranntt/ccs/commit/2672e35362938b054710de681760bd24c3c39e19)) + +### Bug Fixes + +* **browser:** bootstrap managed attach profile setup ([b525033](https://github.com/kaitranntt/ccs/commit/b52503300b0c434fbb9d88283643d3ceaa673d44)) +* **cliproxy:** preserve legacy openai-compat connectors on restart ([16f81fc](https://github.com/kaitranntt/ccs/commit/16f81fc8a595792f1c1a6df060f88cdf28d7afb6)) +* **codex:** probe Windows cmd wrappers via cmd shell ([210ec33](https://github.com/kaitranntt/ccs/commit/210ec33c04fe88be2b651c2163f638085b1b6bd2)) +* **proxy:** harden Anthropic request transformation semantics ([ebc9219](https://github.com/kaitranntt/ccs/commit/ebc92194bb9bf2810e10f64de1fa6e599c2fa156)) +* **proxy:** preserve valid enums during schema normalization ([22ab58b](https://github.com/kaitranntt/ccs/commit/22ab58b02e5dc34f152ecce048c1fbf7d615bdfb)) +* **proxy:** restore strict Anthropic message validation ([32d6bfd](https://github.com/kaitranntt/ccs/commit/32d6bfdda7881a42a376d0973c021f0b47eed7e2)) +* **repo:** remove tracked plans workspace files ([bc4ab98](https://github.com/kaitranntt/ccs/commit/bc4ab98af41e9c9f049cc5da2e6ff356832ef14b)) +* **targets:** use cmd.exe for escaped Windows wrapper launches ([a945b0b](https://github.com/kaitranntt/ccs/commit/a945b0b1047b85a7b343dfe35d8ae9680346e25f)) +* **windows:** align escaped wrapper shell handling ([ccdd0b6](https://github.com/kaitranntt/ccs/commit/ccdd0b6e8e4185ec2b01186cfc69e28f171d0906)) + +### Tests + +* **cliproxy:** assert routing persistence via unified config loader ([485fe4b](https://github.com/kaitranntt/ccs/commit/485fe4ba1c5878083c6e2b6a2d98b37208772a1f)) +* **cliproxy:** cover legacy connector removal cleanup ([00d902b](https://github.com/kaitranntt/ccs/commit/00d902b6e64839cd6b89446cc5dee540f2a059b8)) +* **cliproxy:** isolate routing strategy service state ([5e1e807](https://github.com/kaitranntt/ccs/commit/5e1e8070e821dbe660ea77bc96fec610937ed44a)) +* **websearch:** clear CCS_PROFILE_TYPE in SearXNG fallback spawn env ([e25d791](https://github.com/kaitranntt/ccs/commit/e25d791d9bee38e7143757d37c42bd850e471ae8)) + +### CI + +* add concurrency group and split build/test with artifact sharing ([ad54e17](https://github.com/kaitranntt/ccs/commit/ad54e179b6a3921fd26ad3ac585f5ef965b01c1b)) +* **pre-push:** skip gate for delete-only pushes ([6236cb7](https://github.com/kaitranntt/ccs/commit/6236cb7feb15e954f7f65ae98e6fd98b323a4adb)) + ## [7.72.1](https://github.com/kaitranntt/ccs/compare/v7.72.0...v7.72.1) (2026-04-17) ### Bug Fixes diff --git a/src/cliproxy/binary-manager.ts b/src/cliproxy/binary-manager.ts index c400a3dc..95c486b3 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,10 +38,31 @@ import { } from './binary'; import type { CLIProxyBackend } from './types'; +import { getVersionListCachePath } from './binary/version-cache'; +export const CLIPROXY_DELETED_PLUS_REPO = 'router-for-me/CLIProxyAPIPlus'; +export const CLIPROXY_PLUS_FALLBACK_TRACKING_URL = 'https://github.com/kaitranntt/ccs/issues/1062'; export const CLIPROXY_PLUS_FORK_URL = 'https://github.com/kaitranntt/CLIProxyAPIPlus'; export const CLIPROXY_PLUS_TRACKING_URL = 'https://github.com/kaitranntt/ccs/issues/1065'; +/** + * Track whether we've already warned the user about the legacy Plus fallback + * this process lifetime. Prevents spamming the warning on every command. + */ +let plusFallbackWarned = false; + +function emitPlusFallbackWarning(): void { + if (plusFallbackWarned) return; + plusFallbackWarned = true; + process.stderr.write( + `${warn( + 'Legacy CLIProxyAPIPlus upstream repo is unavailable; local CLIProxy is falling back to ' + + '`backend: original`. Run `ccs config` to use the maintained Plus fork. ' + + `Tracking: ${CLIPROXY_PLUS_FALLBACK_TRACKING_URL}` + )}\n` + ); +} + /** * Track whether we've already shown the optional Plus backend notice this * process lifetime. Prevents spamming the message on every command. @@ -66,13 +95,50 @@ export function getPlusBackendUnavailableMessage(provider?: string): string { interface ResolveLocalBackendOptions { notifyOnPlus?: boolean; + warnOnFallback?: boolean; +} + +function isPlusBackendFallbackActive(): boolean { + return (BACKEND_CONFIG.plus.repo as string) === CLIPROXY_DELETED_PLUS_REPO; +} + +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' || !isPlusBackendFallbackActive()) 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')); } export function resolveLocalBackend( backend: CLIProxyBackend = DEFAULT_BACKEND, options: ResolveLocalBackendOptions = {} ): CLIProxyBackend { - if (backend === 'plus' && options.notifyOnPlus) { + if (backend !== 'plus') return backend; + + if (isPlusBackendFallbackActive()) { + syncPlusFallbackStateIfNeeded(backend); + if (options.warnOnFallback) { + emitPlusFallbackWarning(); + } + return 'original'; + } + + if (options.notifyOnPlus) { emitPlusBackendInfo(); } return backend; diff --git a/tests/unit/cliproxy/binary-manager-install.test.ts b/tests/unit/cliproxy/binary-manager-install.test.ts index 8240c1e9..dfe781f5 100644 --- a/tests/unit/cliproxy/binary-manager-install.test.ts +++ b/tests/unit/cliproxy/binary-manager-install.test.ts @@ -100,6 +100,44 @@ describe('installCliproxyVersion', () => { expect(status.binaryPath).toContain('/plus/'); }); + it('copies plus binary and pin state when legacy deleted upstream fallback is active', 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 platformDetector = await import('../../../src/cliproxy/platform-detector'); + const binaryService = await import( + `../../../src/cliproxy/services/binary-service?binary-service-legacy-plus-fallback=${Date.now()}` + ); + + const plusConfig = platformDetector.BACKEND_CONFIG.plus as unknown as { repo: string }; + const originalRepo = plusConfig.repo; + + try { + plusConfig.repo = 'router-for-me/CLIProxyAPIPlus'; + + 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, platformDetector.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/'); + } finally { + plusConfig.repo = originalRepo; + } + }); + it('attempts to stop the proxy even when there is no tracked running session', async () => { const calls = { stopProxy: 0,