mirror of
https://github.com/tiennm99/ccs.git
synced 2026-08-15 18:23:12 +00:00
Merge pull request #360 from kaitranntt/dev
fix(cliproxy): complete backend switching implementation
This commit is contained in:
+1
-1
@@ -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",
|
||||
|
||||
@@ -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]}`);
|
||||
}
|
||||
|
||||
@@ -26,9 +26,10 @@ import {
|
||||
getVersionPinPath,
|
||||
readInstalledVersion,
|
||||
ensureBinary,
|
||||
migrateVersionPin,
|
||||
} from './binary';
|
||||
|
||||
type CLIProxyBackend = 'original' | 'plus';
|
||||
import type { CLIProxyBackend } from './types';
|
||||
|
||||
/**
|
||||
* Get backend from config or default to 'plus'
|
||||
@@ -61,6 +62,7 @@ function createDefaultConfig(backend: CLIProxyBackend = DEFAULT_BACKEND): Binary
|
||||
maxRetries: 3,
|
||||
verbose: false,
|
||||
forceVersion: false,
|
||||
backend, // Pass backend for installer to use correct download URL
|
||||
};
|
||||
}
|
||||
|
||||
@@ -84,34 +86,43 @@ export class BinaryManager {
|
||||
|
||||
/** Check for updates by comparing installed version with latest release */
|
||||
async checkForUpdates(): Promise<UpdateCheckResult> {
|
||||
return checkForUpdates(this.config.binPath, this.config.version, this.config.verbose);
|
||||
return checkForUpdates(
|
||||
this.config.binPath,
|
||||
this.config.version,
|
||||
this.config.verbose,
|
||||
this.backend
|
||||
);
|
||||
}
|
||||
|
||||
/** Get full path to binary executable */
|
||||
getBinaryPath(): string {
|
||||
return getBinaryPath(this.config.binPath);
|
||||
return getBinaryPath(this.config.binPath, this.backend);
|
||||
}
|
||||
|
||||
/** Check if binary exists */
|
||||
isBinaryInstalled(): boolean {
|
||||
return isBinaryInstalled(this.config.binPath);
|
||||
return isBinaryInstalled(this.config.binPath, this.backend);
|
||||
}
|
||||
|
||||
/** Get binary info if installed */
|
||||
async getBinaryInfo(): Promise<BinaryInfo | null> {
|
||||
return getBinaryInfo(this.config.binPath, this.config.version);
|
||||
return getBinaryInfo(this.config.binPath, this.config.version, this.backend);
|
||||
}
|
||||
|
||||
/** Delete binary (for cleanup or reinstall) */
|
||||
deleteBinary(): void {
|
||||
deleteBinary(this.config.binPath, this.config.verbose);
|
||||
deleteBinary(this.config.binPath, this.config.verbose, this.backend);
|
||||
}
|
||||
}
|
||||
|
||||
/** Convenience function respecting version pin */
|
||||
export async function ensureCLIProxyBinary(verbose = false): Promise<string> {
|
||||
const backend = getConfiguredBackend();
|
||||
const pinnedVersion = getPinnedVersion();
|
||||
|
||||
// Migrate old shared pin to backend-specific location (one-time migration)
|
||||
migrateVersionPin(backend);
|
||||
|
||||
const pinnedVersion = getPinnedVersion(backend);
|
||||
if (pinnedVersion) {
|
||||
if (verbose) console.error(`[cliproxy] Using pinned version: ${pinnedVersion}`);
|
||||
return new BinaryManager(
|
||||
@@ -127,27 +138,34 @@ export async function ensureCLIProxyBinary(verbose = false): Promise<string> {
|
||||
}
|
||||
|
||||
/** 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<void> {
|
||||
const backend = getConfiguredBackend();
|
||||
const manager = new BinaryManager({ version, verbose, forceVersion: true }, backend);
|
||||
export async function installCliproxyVersion(
|
||||
version: string,
|
||||
verbose = false,
|
||||
backend?: CLIProxyBackend
|
||||
): Promise<void> {
|
||||
const effectiveBackend = backend ?? getConfiguredBackend();
|
||||
const manager = new BinaryManager({ version, verbose, forceVersion: true }, effectiveBackend);
|
||||
|
||||
// Check if proxy is running and stop it first
|
||||
if (isProxyRunning()) {
|
||||
@@ -165,8 +183,11 @@ export async function installCliproxyVersion(version: string, verbose = false):
|
||||
}
|
||||
|
||||
if (manager.isBinaryInstalled()) {
|
||||
const label = effectiveBackend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy';
|
||||
if (verbose)
|
||||
console.log(info(`Removing existing CLIProxy Plus v${getInstalledCliproxyVersion()}`));
|
||||
console.log(
|
||||
info(`Removing existing ${label} v${getInstalledCliproxyVersion(effectiveBackend)}`)
|
||||
);
|
||||
manager.deleteBinary();
|
||||
}
|
||||
await manager.ensureBinary();
|
||||
@@ -190,6 +211,9 @@ export interface CliproxyUpdateCheckResult {
|
||||
latestVersion: string;
|
||||
fromCache: boolean;
|
||||
checkedAt: number;
|
||||
// Backend info
|
||||
backend: CLIProxyBackend;
|
||||
backendLabel: string;
|
||||
// Stability fields
|
||||
isStable: boolean;
|
||||
maxStableVersion: string;
|
||||
@@ -208,8 +232,12 @@ export async function checkCliproxyUpdate(): Promise<CliproxyUpdateCheckResult>
|
||||
? undefined
|
||||
: `v${result.currentVersion} has known stability issues. Max stable: v${CLIPROXY_MAX_STABLE_VERSION}`;
|
||||
|
||||
const backendLabel = backend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy';
|
||||
|
||||
return {
|
||||
...result,
|
||||
backend,
|
||||
backendLabel,
|
||||
isStable,
|
||||
maxStableVersion: CLIPROXY_MAX_STABLE_VERSION,
|
||||
stabilityMessage,
|
||||
@@ -223,6 +251,7 @@ export {
|
||||
savePinnedVersion,
|
||||
clearPinnedVersion,
|
||||
isVersionPinned,
|
||||
migrateVersionPin,
|
||||
};
|
||||
|
||||
export default BinaryManager;
|
||||
|
||||
@@ -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<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ export {
|
||||
savePinnedVersion,
|
||||
clearPinnedVersion,
|
||||
isVersionPinned,
|
||||
migrateVersionPin,
|
||||
} from './version-cache';
|
||||
|
||||
// Version Checker
|
||||
|
||||
@@ -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<void> {
|
||||
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<typeof detectPlatform>;
|
||||
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 };
|
||||
}
|
||||
|
||||
@@ -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<void> {
|
||||
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<string> {
|
||||
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<string>
|
||||
|
||||
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);
|
||||
|
||||
@@ -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<void> {
|
||||
export function extractTarGz(
|
||||
archivePath: string,
|
||||
destDir: string,
|
||||
verbose = false,
|
||||
backend: CLIProxyBackend = DEFAULT_BACKEND
|
||||
): Promise<void> {
|
||||
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);
|
||||
|
||||
|
||||
@@ -24,14 +24,30 @@ export const VERSION_CACHE_DURATION_MS = 60 * 60 * 1000;
|
||||
/** Version pin file name - stores user's explicit version choice */
|
||||
export const VERSION_PIN_FILE = '.version-pin';
|
||||
|
||||
/** GitHub API URL for latest release (CLIProxyAPIPlus fork with Kiro + Copilot support) */
|
||||
/**
|
||||
* GitHub API URLs - backend-specific
|
||||
* @deprecated Use getGitHubApiUrls(backend) instead
|
||||
*/
|
||||
export const GITHUB_API_LATEST_RELEASE =
|
||||
'https://api.github.com/repos/router-for-me/CLIProxyAPIPlus/releases/latest';
|
||||
|
||||
/** GitHub API URL for all releases */
|
||||
export const GITHUB_API_ALL_RELEASES =
|
||||
'https://api.github.com/repos/router-for-me/CLIProxyAPIPlus/releases';
|
||||
|
||||
/** GitHub repos per backend */
|
||||
export const GITHUB_REPOS = {
|
||||
original: 'router-for-me/CLIProxyAPI',
|
||||
plus: 'router-for-me/CLIProxyAPIPlus',
|
||||
} as const;
|
||||
|
||||
/** Get GitHub API URLs for specific backend */
|
||||
export function getGitHubApiUrls(backend: 'original' | 'plus') {
|
||||
const repo = GITHUB_REPOS[backend];
|
||||
return {
|
||||
latestRelease: `https://api.github.com/repos/${repo}/releases/latest`,
|
||||
allReleases: `https://api.github.com/repos/${repo}/releases`,
|
||||
};
|
||||
}
|
||||
|
||||
/** Version list cache structure */
|
||||
export interface VersionListCache {
|
||||
versions: string[];
|
||||
|
||||
@@ -5,33 +5,35 @@
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { getCliproxyDir, getBinDir } from '../config-generator';
|
||||
import { getBinDir } from '../config-generator';
|
||||
import {
|
||||
VersionCache,
|
||||
VERSION_CACHE_DURATION_MS,
|
||||
VERSION_PIN_FILE,
|
||||
VersionListCache,
|
||||
} from './types';
|
||||
import { DEFAULT_BACKEND } from '../platform-detector';
|
||||
import type { CLIProxyBackend } from '../types';
|
||||
|
||||
/**
|
||||
* Get path to version cache file
|
||||
* Get path to version cache file (backend-specific)
|
||||
*/
|
||||
export function getVersionCachePath(): string {
|
||||
return path.join(getCliproxyDir(), '.version-cache.json');
|
||||
export function getVersionCachePath(backend: CLIProxyBackend = DEFAULT_BACKEND): string {
|
||||
return path.join(getBinDir(), backend, '.version-cache.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get path to version pin file
|
||||
* Get path to version pin file (backend-specific)
|
||||
*/
|
||||
export function getVersionPinPath(): string {
|
||||
return path.join(getBinDir(), VERSION_PIN_FILE);
|
||||
export function getVersionPinPath(backend: CLIProxyBackend = DEFAULT_BACKEND): string {
|
||||
return path.join(getBinDir(), backend, VERSION_PIN_FILE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read version cache if still valid
|
||||
* Read version cache if still valid (backend-specific)
|
||||
*/
|
||||
export function readVersionCache(): VersionCache | null {
|
||||
const cachePath = getVersionCachePath();
|
||||
export function readVersionCache(backend: CLIProxyBackend = DEFAULT_BACKEND): VersionCache | null {
|
||||
const cachePath = getVersionCachePath(backend);
|
||||
if (!fs.existsSync(cachePath)) {
|
||||
return null;
|
||||
}
|
||||
@@ -53,10 +55,13 @@ export function readVersionCache(): VersionCache | null {
|
||||
}
|
||||
|
||||
/**
|
||||
* Write version to cache
|
||||
* Write version to cache (backend-specific)
|
||||
*/
|
||||
export function writeVersionCache(version: string): void {
|
||||
const cachePath = getVersionCachePath();
|
||||
export function writeVersionCache(
|
||||
version: string,
|
||||
backend: CLIProxyBackend = DEFAULT_BACKEND
|
||||
): void {
|
||||
const cachePath = getVersionCachePath(backend);
|
||||
const cache: VersionCache = {
|
||||
latestVersion: version,
|
||||
checkedAt: Date.now(),
|
||||
@@ -98,10 +103,10 @@ export function writeInstalledVersion(binPath: string, version: string): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pinned version if one exists
|
||||
* Get pinned version if one exists (backend-specific)
|
||||
*/
|
||||
export function getPinnedVersion(): string | null {
|
||||
const pinPath = getVersionPinPath();
|
||||
export function getPinnedVersion(backend: CLIProxyBackend = DEFAULT_BACKEND): string | null {
|
||||
const pinPath = getVersionPinPath(backend);
|
||||
if (!fs.existsSync(pinPath)) {
|
||||
return null;
|
||||
}
|
||||
@@ -113,10 +118,13 @@ export function getPinnedVersion(): string | null {
|
||||
}
|
||||
|
||||
/**
|
||||
* Save pinned version to persist user's explicit choice
|
||||
* Save pinned version to persist user's explicit choice (backend-specific)
|
||||
*/
|
||||
export function savePinnedVersion(version: string): void {
|
||||
const pinPath = getVersionPinPath();
|
||||
export function savePinnedVersion(
|
||||
version: string,
|
||||
backend: CLIProxyBackend = DEFAULT_BACKEND
|
||||
): void {
|
||||
const pinPath = getVersionPinPath(backend);
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(pinPath), { recursive: true });
|
||||
fs.writeFileSync(pinPath, version, 'utf8');
|
||||
@@ -126,10 +134,10 @@ export function savePinnedVersion(version: string): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear pinned version (unpin)
|
||||
* Clear pinned version (unpin) - backend-specific
|
||||
*/
|
||||
export function clearPinnedVersion(): void {
|
||||
const pinPath = getVersionPinPath();
|
||||
export function clearPinnedVersion(backend: CLIProxyBackend = DEFAULT_BACKEND): void {
|
||||
const pinPath = getVersionPinPath(backend);
|
||||
if (fs.existsSync(pinPath)) {
|
||||
try {
|
||||
fs.unlinkSync(pinPath);
|
||||
@@ -140,10 +148,32 @@ export function clearPinnedVersion(): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a version is currently pinned
|
||||
* Check if a version is currently pinned (backend-specific)
|
||||
*/
|
||||
export function isVersionPinned(): boolean {
|
||||
return getPinnedVersion() !== null;
|
||||
export function isVersionPinned(backend: CLIProxyBackend = DEFAULT_BACKEND): boolean {
|
||||
return getPinnedVersion(backend) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate old shared version pin to backend-specific location.
|
||||
* Called once on first run after update.
|
||||
*/
|
||||
export function migrateVersionPin(backend: CLIProxyBackend): void {
|
||||
const oldPinPath = path.join(getBinDir(), VERSION_PIN_FILE);
|
||||
if (!fs.existsSync(oldPinPath)) return;
|
||||
|
||||
try {
|
||||
const oldVersion = fs.readFileSync(oldPinPath, 'utf8').trim();
|
||||
if (!oldVersion) return;
|
||||
|
||||
// Save to new backend-specific location
|
||||
savePinnedVersion(oldVersion, backend);
|
||||
|
||||
// Delete old shared file
|
||||
fs.unlinkSync(oldPinPath);
|
||||
} catch {
|
||||
// Silent fail - not critical
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Version List Cache ====================
|
||||
@@ -151,17 +181,19 @@ export function isVersionPinned(): boolean {
|
||||
const VERSION_LIST_CACHE_FILE = '.version-list-cache.json';
|
||||
|
||||
/**
|
||||
* Get path to version list cache file
|
||||
* Get path to version list cache file (backend-specific)
|
||||
*/
|
||||
export function getVersionListCachePath(): string {
|
||||
return path.join(getCliproxyDir(), VERSION_LIST_CACHE_FILE);
|
||||
export function getVersionListCachePath(backend: CLIProxyBackend = DEFAULT_BACKEND): string {
|
||||
return path.join(getBinDir(), backend, VERSION_LIST_CACHE_FILE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read version list cache if still valid
|
||||
* Read version list cache if still valid (backend-specific)
|
||||
*/
|
||||
export function readVersionListCache(): VersionListCache | null {
|
||||
const cachePath = getVersionListCachePath();
|
||||
export function readVersionListCache(
|
||||
backend: CLIProxyBackend = DEFAULT_BACKEND
|
||||
): VersionListCache | null {
|
||||
const cachePath = getVersionListCachePath(backend);
|
||||
if (!fs.existsSync(cachePath)) {
|
||||
return null;
|
||||
}
|
||||
@@ -182,10 +214,13 @@ export function readVersionListCache(): VersionListCache | null {
|
||||
}
|
||||
|
||||
/**
|
||||
* Write version list to cache
|
||||
* Write version list to cache (backend-specific)
|
||||
*/
|
||||
export function writeVersionListCache(cache: VersionListCache): void {
|
||||
const cachePath = getVersionListCachePath();
|
||||
export function writeVersionListCache(
|
||||
cache: VersionListCache,
|
||||
backend: CLIProxyBackend = DEFAULT_BACKEND
|
||||
): void {
|
||||
const cachePath = getVersionListCachePath(backend);
|
||||
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
||||
|
||||
@@ -11,13 +11,13 @@ import {
|
||||
readVersionListCache,
|
||||
writeVersionListCache,
|
||||
} from './version-cache';
|
||||
import { UpdateCheckResult, VersionListResult, getGitHubApiUrls } from './types';
|
||||
import {
|
||||
UpdateCheckResult,
|
||||
GITHUB_API_LATEST_RELEASE,
|
||||
GITHUB_API_ALL_RELEASES,
|
||||
VersionListResult,
|
||||
} from './types';
|
||||
import { CLIPROXY_MAX_STABLE_VERSION, CLIPROXY_FAULTY_RANGE } from '../platform-detector';
|
||||
CLIPROXY_MAX_STABLE_VERSION,
|
||||
CLIPROXY_FAULTY_RANGE,
|
||||
DEFAULT_BACKEND,
|
||||
} from '../platform-detector';
|
||||
import type { CLIProxyBackend } from '../types';
|
||||
|
||||
/**
|
||||
* Compare semver versions (true if latest > current)
|
||||
@@ -56,9 +56,15 @@ export function isVersionFaulty(version: string): boolean {
|
||||
|
||||
/**
|
||||
* Fetch latest version from GitHub API
|
||||
* @param verbose Enable verbose logging
|
||||
* @param backend Backend to fetch version for (uses correct GitHub repo)
|
||||
*/
|
||||
export async function fetchLatestVersion(verbose = false): Promise<string> {
|
||||
const response = await fetchJson(GITHUB_API_LATEST_RELEASE, verbose);
|
||||
export async function fetchLatestVersion(
|
||||
verbose = false,
|
||||
backend: CLIProxyBackend = DEFAULT_BACKEND
|
||||
): Promise<string> {
|
||||
const urls = getGitHubApiUrls(backend);
|
||||
const response = await fetchJson(urls.latestRelease, verbose);
|
||||
|
||||
// Extract version from tag_name (format: "v6.5.27" or "6.5.27")
|
||||
const tagName = response.tag_name as string;
|
||||
@@ -72,16 +78,18 @@ export async function fetchLatestVersion(verbose = false): Promise<string> {
|
||||
/**
|
||||
* Check for updates by comparing installed version with latest release
|
||||
* Uses cache to avoid hitting GitHub API on every run
|
||||
* Cache is backend-specific to handle different repos for original vs plus
|
||||
*/
|
||||
export async function checkForUpdates(
|
||||
binPath: string,
|
||||
configVersion: string,
|
||||
verbose = false
|
||||
verbose = false,
|
||||
backend: CLIProxyBackend = DEFAULT_BACKEND
|
||||
): Promise<UpdateCheckResult> {
|
||||
const currentVersion = readInstalledVersion(binPath, configVersion);
|
||||
|
||||
// Try cache first
|
||||
const cache = readVersionCache();
|
||||
// Try cache first (backend-specific)
|
||||
const cache = readVersionCache(backend);
|
||||
if (cache) {
|
||||
if (verbose) {
|
||||
console.error(`[cliproxy] Using cached version: ${cache.latestVersion}`);
|
||||
@@ -95,10 +103,10 @@ export async function checkForUpdates(
|
||||
};
|
||||
}
|
||||
|
||||
// Fetch from GitHub API
|
||||
const latestVersion = await fetchLatestVersion(verbose);
|
||||
// Fetch from GitHub API (backend-specific repo)
|
||||
const latestVersion = await fetchLatestVersion(verbose, backend);
|
||||
const now = Date.now();
|
||||
writeVersionCache(latestVersion);
|
||||
writeVersionCache(latestVersion, backend);
|
||||
|
||||
return {
|
||||
hasUpdate: isNewerVersion(latestVersion, currentVersion),
|
||||
@@ -112,10 +120,15 @@ export async function checkForUpdates(
|
||||
/**
|
||||
* Fetch all available versions from GitHub releases
|
||||
* Caches result for 1 hour to avoid rate limiting
|
||||
* @param verbose Enable verbose logging
|
||||
* @param backend Backend to fetch versions for (uses correct GitHub repo)
|
||||
*/
|
||||
export async function fetchAllVersions(verbose = false): Promise<VersionListResult> {
|
||||
// Try cache first
|
||||
const cache = readVersionListCache();
|
||||
export async function fetchAllVersions(
|
||||
verbose = false,
|
||||
backend: CLIProxyBackend = DEFAULT_BACKEND
|
||||
): Promise<VersionListResult> {
|
||||
// Try cache first (backend-specific)
|
||||
const cache = readVersionListCache(backend);
|
||||
if (cache) {
|
||||
if (verbose) {
|
||||
console.error(`[cliproxy] Using cached version list (${cache.versions.length} versions)`);
|
||||
@@ -123,8 +136,9 @@ export async function fetchAllVersions(verbose = false): Promise<VersionListResu
|
||||
return { ...cache, fromCache: true };
|
||||
}
|
||||
|
||||
// Fetch from GitHub API
|
||||
const response = await fetchJson(GITHUB_API_ALL_RELEASES, verbose);
|
||||
// Fetch from GitHub API (backend-specific repo)
|
||||
const urls = getGitHubApiUrls(backend);
|
||||
const response = await fetchJson(urls.allReleases, verbose);
|
||||
|
||||
// Extract and normalize versions
|
||||
const releases = response as unknown as Array<{ tag_name: string }>;
|
||||
@@ -147,6 +161,6 @@ export async function fetchAllVersions(verbose = false): Promise<VersionListResu
|
||||
checkedAt: Date.now(),
|
||||
};
|
||||
|
||||
writeVersionListCache(result);
|
||||
writeVersionListCache(result, backend);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -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<void> {
|
||||
export function extractZip(
|
||||
archivePath: string,
|
||||
destDir: string,
|
||||
verbose = false,
|
||||
backend: CLIProxyBackend = DEFAULT_BACKEND
|
||||
): Promise<void> {
|
||||
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)
|
||||
|
||||
@@ -724,12 +724,13 @@ export async function execClaudeWithCLIProxy(
|
||||
await waitForProxyReady(cfg.port, cfg.timeout, cfg.pollInterval);
|
||||
readySpinner.succeed(`CLIProxy ready on port ${cfg.port}`);
|
||||
} catch (error) {
|
||||
readySpinner.fail('CLIProxy Plus startup failed');
|
||||
const backendLabel = backend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy';
|
||||
readySpinner.fail(`${backendLabel} startup failed`);
|
||||
proxy.kill('SIGTERM');
|
||||
|
||||
const err = error as Error;
|
||||
console.error('');
|
||||
console.error(fail('CLIProxy Plus failed to start'));
|
||||
console.error(fail(`${backendLabel} failed to start`));
|
||||
console.error('');
|
||||
console.error('Possible causes:');
|
||||
console.error(` 1. Port ${cfg.port} already in use`);
|
||||
@@ -746,9 +747,9 @@ export async function execClaudeWithCLIProxy(
|
||||
throw new Error(`CLIProxy startup failed: ${err.message}`);
|
||||
}
|
||||
|
||||
// Register this session with the new proxy, including the installed version
|
||||
// Register this session with the new proxy, including version and backend
|
||||
const installedVersion = getInstalledCliproxyVersion();
|
||||
sessionId = registerSession(cfg.port, proxy.pid as number, installedVersion);
|
||||
sessionId = registerSession(cfg.port, proxy.pid as number, installedVersion, backend);
|
||||
log(
|
||||
`Registered session ${sessionId} with new proxy (PID ${proxy.pid}, version ${installedVersion})`
|
||||
);
|
||||
|
||||
@@ -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}`,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -58,10 +58,10 @@ export function getBinaryStatus(backend?: CLIProxyBackend): BinaryStatusResult {
|
||||
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
|
||||
const backendConfig = BACKEND_CONFIG[effectiveBackend];
|
||||
return {
|
||||
installed: isCLIProxyInstalled(),
|
||||
currentVersion: getInstalledCliproxyVersion(),
|
||||
pinnedVersion: getPinnedVersion(),
|
||||
binaryPath: getCLIProxyPath(),
|
||||
installed: isCLIProxyInstalled(effectiveBackend),
|
||||
currentVersion: getInstalledCliproxyVersion(effectiveBackend),
|
||||
pinnedVersion: getPinnedVersion(effectiveBackend),
|
||||
binaryPath: getCLIProxyPath(effectiveBackend),
|
||||
fallbackVersion: backendConfig.fallbackVersion,
|
||||
backend: effectiveBackend,
|
||||
};
|
||||
@@ -70,10 +70,16 @@ export function getBinaryStatus(backend?: CLIProxyBackend): BinaryStatusResult {
|
||||
/**
|
||||
* Check for latest version
|
||||
*/
|
||||
export async function checkLatestVersion(): Promise<LatestVersionResult> {
|
||||
export async function checkLatestVersion(backend?: CLIProxyBackend): Promise<LatestVersionResult> {
|
||||
const effectiveBackend =
|
||||
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
|
||||
|
||||
try {
|
||||
const latestVersion = await fetchLatestCliproxyVersion();
|
||||
const currentVersion = getInstalledCliproxyVersion();
|
||||
// Use checkCliproxyUpdate which is backend-aware (uses correct GitHub repo)
|
||||
const { checkCliproxyUpdate } = await import('../binary-manager');
|
||||
const updateResult = await checkCliproxyUpdate();
|
||||
const latestVersion = updateResult.latestVersion;
|
||||
const currentVersion = getInstalledCliproxyVersion(effectiveBackend);
|
||||
const updateAvailable = latestVersion !== currentVersion;
|
||||
|
||||
return {
|
||||
@@ -100,7 +106,11 @@ export function isValidVersionFormat(version: string): boolean {
|
||||
/**
|
||||
* Install a specific version and pin it
|
||||
*/
|
||||
export async function installVersion(version: string, verbose = false): Promise<InstallResult> {
|
||||
export async function installVersion(
|
||||
version: string,
|
||||
verbose = false,
|
||||
backend?: CLIProxyBackend
|
||||
): Promise<InstallResult> {
|
||||
if (!isValidVersionFormat(version)) {
|
||||
return {
|
||||
success: false,
|
||||
@@ -109,9 +119,12 @@ export async function installVersion(version: string, verbose = false): Promise<
|
||||
};
|
||||
}
|
||||
|
||||
const effectiveBackend =
|
||||
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
|
||||
|
||||
try {
|
||||
await installCliproxyVersion(version, verbose);
|
||||
savePinnedVersion(version);
|
||||
await installCliproxyVersion(version, verbose, effectiveBackend);
|
||||
savePinnedVersion(version, effectiveBackend);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -130,13 +143,19 @@ export async function installVersion(version: string, verbose = false): Promise<
|
||||
/**
|
||||
* Install latest version and clear any pin
|
||||
*/
|
||||
export async function installLatest(verbose = false): Promise<InstallResult> {
|
||||
export async function installLatest(
|
||||
verbose = false,
|
||||
backend?: CLIProxyBackend
|
||||
): Promise<InstallResult> {
|
||||
const effectiveBackend =
|
||||
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
|
||||
|
||||
try {
|
||||
const latestVersion = await fetchLatestCliproxyVersion();
|
||||
const currentVersion = getInstalledCliproxyVersion();
|
||||
const wasPinned = isVersionPinned();
|
||||
const currentVersion = getInstalledCliproxyVersion(effectiveBackend);
|
||||
const wasPinned = isVersionPinned(effectiveBackend);
|
||||
|
||||
if (isCLIProxyInstalled() && latestVersion === currentVersion && !wasPinned) {
|
||||
if (isCLIProxyInstalled(effectiveBackend) && latestVersion === currentVersion && !wasPinned) {
|
||||
return {
|
||||
success: true,
|
||||
version: latestVersion,
|
||||
@@ -144,8 +163,8 @@ export async function installLatest(verbose = false): Promise<InstallResult> {
|
||||
};
|
||||
}
|
||||
|
||||
await installCliproxyVersion(latestVersion, verbose);
|
||||
clearPinnedVersion();
|
||||
await installCliproxyVersion(latestVersion, verbose, effectiveBackend);
|
||||
clearPinnedVersion(effectiveBackend);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -164,20 +183,26 @@ export async function installLatest(verbose = false): Promise<InstallResult> {
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
|
||||
// Create variant
|
||||
console.log('');
|
||||
console.log(info('Creating CLIProxy Plus variant...'));
|
||||
console.log(info(`Creating ${getBackendLabel(effectiveBackend)} variant...`));
|
||||
const result = createVariant(name, provider, model, account);
|
||||
|
||||
if (!result.success) {
|
||||
@@ -479,7 +488,7 @@ async function showStatus(verbose: boolean, backend: CLIProxyBackend): Promise<v
|
||||
const status = getBinaryStatus(backend);
|
||||
|
||||
console.log('');
|
||||
const backendLabel = backend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy (Original)';
|
||||
const backendLabel = getBackendLabel(backend);
|
||||
console.log(color(`${backendLabel} Status`, 'primary'));
|
||||
console.log('');
|
||||
|
||||
@@ -530,14 +539,19 @@ async function showStatus(verbose: boolean, backend: CLIProxyBackend): Promise<v
|
||||
console.log('');
|
||||
}
|
||||
|
||||
async function handleInstallVersion(version: string, verbose: boolean): Promise<void> {
|
||||
console.log(info(`Installing CLIProxy Plus v${version}...`));
|
||||
async function handleInstallVersion(
|
||||
version: string,
|
||||
verbose: boolean,
|
||||
backend: CLIProxyBackend
|
||||
): Promise<void> {
|
||||
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<void> {
|
||||
console.log(info('Fetching latest CLIProxy Plus version...'));
|
||||
async function handleInstallLatest(verbose: boolean, backend: CLIProxyBackend): Promise<void> {
|
||||
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<void> {
|
||||
}
|
||||
|
||||
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<void> {
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -153,6 +153,11 @@ function mergeWithDefaults(partial: Partial<UnifiedConfig>): 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,
|
||||
|
||||
@@ -34,10 +34,22 @@ import {
|
||||
import {
|
||||
CLIPROXY_MAX_STABLE_VERSION,
|
||||
CLIPROXY_FAULTY_RANGE,
|
||||
DEFAULT_BACKEND,
|
||||
} from '../../cliproxy/platform-detector';
|
||||
import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
|
||||
|
||||
const router = Router();
|
||||
|
||||
/** Get configured backend from config */
|
||||
function getConfiguredBackend() {
|
||||
try {
|
||||
const config = loadOrCreateUnifiedConfig();
|
||||
return config.cliproxy?.backend || DEFAULT_BACKEND;
|
||||
} catch {
|
||||
return DEFAULT_BACKEND;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract status code and model from error log file (lightweight parsing)
|
||||
* Reads first 4KB for model, last 2KB for status code
|
||||
@@ -549,8 +561,9 @@ router.get('/quota/:provider/:accountId', async (req: Request, res: Response): P
|
||||
*/
|
||||
router.get('/versions', async (_req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const result = await fetchAllVersions();
|
||||
const currentVersion = getInstalledCliproxyVersion();
|
||||
const backend = getConfiguredBackend();
|
||||
const result = await fetchAllVersions(false, backend);
|
||||
const currentVersion = getInstalledCliproxyVersion(backend);
|
||||
|
||||
res.json({
|
||||
...result,
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Router, Request, Response } from 'express';
|
||||
import { loadOrCreateUnifiedConfig, saveUnifiedConfig } from '../../config/unified-config-loader';
|
||||
import { testConnection } from '../../cliproxy/remote-proxy-client';
|
||||
import { isProxyRunning } from '../../cliproxy/services/proxy-lifecycle-service';
|
||||
import { DEFAULT_BACKEND } from '../../cliproxy/platform-detector';
|
||||
import {
|
||||
DEFAULT_CLIPROXY_SERVER_CONFIG,
|
||||
CliproxyServerConfig,
|
||||
@@ -73,7 +74,7 @@ router.put('/', async (req: Request, res: Response) => {
|
||||
router.get('/backend', async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const config = await loadOrCreateUnifiedConfig();
|
||||
res.json({ backend: config.cliproxy?.backend ?? 'plus' });
|
||||
res.json({ backend: config.cliproxy?.backend ?? DEFAULT_BACKEND });
|
||||
} catch (error) {
|
||||
console.error('[cliproxy-server-routes] Failed to load backend config:', error);
|
||||
res.status(500).json({ error: 'Failed to load backend config' });
|
||||
@@ -99,7 +100,7 @@ router.put('/backend', async (req: Request, res: Response) => {
|
||||
|
||||
// Check if proxy is running - warn about restart requirement
|
||||
const config = await loadOrCreateUnifiedConfig();
|
||||
const currentBackend = config.cliproxy?.backend ?? 'plus';
|
||||
const currentBackend = config.cliproxy?.backend ?? DEFAULT_BACKEND;
|
||||
if (currentBackend !== backend && isProxyRunning() && !force) {
|
||||
res.status(409).json({
|
||||
error: 'Proxy is running. Stop proxy first or use force=true to change backend.',
|
||||
|
||||
@@ -15,6 +15,7 @@ interface VersionInfo {
|
||||
currentVersion: string;
|
||||
isStable: boolean;
|
||||
stabilityMessage?: string;
|
||||
backendLabel?: string;
|
||||
}
|
||||
|
||||
interface LoginButtonProps {
|
||||
@@ -127,6 +128,7 @@ export function CliproxyHeader({
|
||||
currentVersion: data.currentVersion,
|
||||
isStable: data.isStable,
|
||||
stabilityMessage: data.stabilityMessage,
|
||||
backendLabel: data.backendLabel,
|
||||
});
|
||||
}
|
||||
})
|
||||
@@ -157,7 +159,9 @@ export function CliproxyHeader({
|
||||
{/* Top row: Title and Login Buttons */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight">CLIProxy Plus</h1>
|
||||
<h1 className="text-2xl font-bold tracking-tight">
|
||||
{versionInfo?.backendLabel ?? 'CLIProxy'}
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">CCS-level account management</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useCliproxyStats, useCliproxyStatus } from '@/hooks/use-cliproxy-stats';
|
||||
import { useCliproxyUpdateCheck } from '@/hooks/use-cliproxy';
|
||||
import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
|
||||
|
||||
interface CliproxyStatsOverviewProps {
|
||||
@@ -37,6 +38,8 @@ export function CliproxyStatsOverview({ className }: CliproxyStatsOverviewProps)
|
||||
const { privacyMode } = usePrivacy();
|
||||
const { data: status, isLoading: statusLoading } = useCliproxyStatus();
|
||||
const { data: stats, isLoading: statsLoading, error } = useCliproxyStats(status?.running);
|
||||
const { data: updateCheck } = useCliproxyUpdateCheck();
|
||||
const backendLabel = updateCheck?.backendLabel ?? 'CLIProxy';
|
||||
|
||||
const isLoading = statusLoading || (status?.running && statsLoading);
|
||||
|
||||
@@ -71,7 +74,7 @@ export function CliproxyStatsOverview({ className }: CliproxyStatsOverviewProps)
|
||||
Session Statistics
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Real-time usage metrics from CLIProxy Plus
|
||||
Real-time usage metrics from {backendLabel}
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant="secondary" className="w-fit gap-1.5">
|
||||
@@ -146,7 +149,9 @@ export function CliproxyStatsOverview({ className }: CliproxyStatsOverviewProps)
|
||||
<Activity className="h-5 w-5" />
|
||||
Session Statistics
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">Real-time usage metrics from CLIProxyAPI</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Real-time usage metrics from {backendLabel}
|
||||
</p>
|
||||
</div>
|
||||
<Badge
|
||||
variant="outline"
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/com
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { Cpu, AlertCircle } from 'lucide-react';
|
||||
import { useCliproxyModels } from '@/hooks/use-cliproxy';
|
||||
import { useCliproxyModels, useCliproxyUpdateCheck } from '@/hooks/use-cliproxy';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
/** Category display configuration */
|
||||
@@ -70,6 +70,8 @@ function EmptyModelsState() {
|
||||
|
||||
export function ModelPreferencesGrid() {
|
||||
const { data: modelsData, isLoading, isError } = useCliproxyModels();
|
||||
const { data: updateCheck } = useCliproxyUpdateCheck();
|
||||
const backendLabel = updateCheck?.backendLabel ?? 'CLIProxy';
|
||||
|
||||
// Sort categories by model count
|
||||
const sortedCategories = useMemo(() => {
|
||||
@@ -102,7 +104,7 @@ export function ModelPreferencesGrid() {
|
||||
</Badge>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Models available through CLIProxy Plus, grouped by provider
|
||||
Models available through {backendLabel}, grouped by provider
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
|
||||
@@ -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 */}
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
tooltip={item.label}
|
||||
tooltip={getItemLabel(item)}
|
||||
isActive={isParentActive(item.children)}
|
||||
onClick={() => navigate(item.path)}
|
||||
>
|
||||
{item.icon && <item.icon className="w-4 h-4" />}
|
||||
<span className="group-data-[collapsible=icon]:hidden">
|
||||
{item.label}
|
||||
{getItemLabel(item)}
|
||||
</span>
|
||||
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90 group-data-[collapsible=icon]:hidden" />
|
||||
</SidebarMenuButton>
|
||||
@@ -155,12 +168,12 @@ export function AppSidebar() {
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
isActive={isRouteActive(item.path)}
|
||||
tooltip={item.label}
|
||||
tooltip={getItemLabel(item)}
|
||||
>
|
||||
<Link to={item.path}>
|
||||
{item.icon && <item.icon className="w-4 h-4" />}
|
||||
<span className="group-data-[collapsible=icon]:hidden flex-1">
|
||||
{item.label}
|
||||
{getItemLabel(item)}
|
||||
</span>
|
||||
{item.badge && (
|
||||
<Tooltip>
|
||||
|
||||
@@ -46,7 +46,7 @@ import {
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useQuery, useIsMutating } from '@tanstack/react-query';
|
||||
import { api, type CliproxyServerConfig } from '@/lib/api-client';
|
||||
import {
|
||||
useProxyStatus,
|
||||
@@ -167,6 +167,9 @@ export function ProxyStatusWidget() {
|
||||
staleTime: 30000, // 30 seconds
|
||||
});
|
||||
|
||||
// Detect if backend switch is in progress (prevents race condition)
|
||||
const isBackendSwitching = useIsMutating({ mutationKey: ['update-backend'] }) > 0;
|
||||
|
||||
// Determine if remote mode is enabled
|
||||
const remoteConfig = cliproxyConfig?.remote;
|
||||
const isRemoteMode = remoteConfig?.enabled && remoteConfig?.host;
|
||||
@@ -176,7 +179,8 @@ export function ProxyStatusWidget() {
|
||||
startProxy.isPending ||
|
||||
stopProxy.isPending ||
|
||||
restartProxy.isPending ||
|
||||
installVersion.isPending;
|
||||
installVersion.isPending ||
|
||||
isBackendSwitching;
|
||||
const hasUpdate = updateCheck?.hasUpdate ?? false;
|
||||
const isUnstable = updateCheck?.isStable === false;
|
||||
const currentVersion = updateCheck?.currentVersion;
|
||||
@@ -282,10 +286,10 @@ export function ProxyStatusWidget() {
|
||||
isRunning ? 'bg-green-500 animate-pulse' : 'bg-muted-foreground/30'
|
||||
)}
|
||||
/>
|
||||
<span className="text-sm font-medium">CLIProxy Plus</span>
|
||||
<span className="text-sm font-medium">{updateCheck?.backendLabel ?? 'CLIProxy'}</span>
|
||||
</div>
|
||||
|
||||
{/* Right side: icon buttons when running */}
|
||||
{/* Right side: icon buttons */}
|
||||
<div className="flex items-center gap-1">
|
||||
{isLoading ? (
|
||||
<RefreshCw className="w-3 h-3 animate-spin text-muted-foreground" />
|
||||
@@ -306,16 +310,15 @@ export function ProxyStatusWidget() {
|
||||
isPending={stopProxy.isPending}
|
||||
variant="destructive-ghost"
|
||||
/>
|
||||
<IconButton
|
||||
icon={isExpanded ? X : Settings}
|
||||
tooltip={isExpanded ? 'Close' : 'Version settings'}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className={isExpanded ? 'bg-muted' : undefined}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Power className="w-3 h-3 text-muted-foreground" />
|
||||
)}
|
||||
) : null}
|
||||
{/* Settings button always visible */}
|
||||
<IconButton
|
||||
icon={isExpanded ? X : Settings}
|
||||
tooltip={isExpanded ? 'Close' : 'Version settings'}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className={isExpanded ? 'bg-muted' : undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -372,82 +375,80 @@ export function ProxyStatusWidget() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Expanded section: Version Management */}
|
||||
{isRunning && (
|
||||
<Collapsible open={isExpanded} onOpenChange={setIsExpanded}>
|
||||
<CollapsibleContent className="mt-3 pt-3 border-t border-muted">
|
||||
{/* Section header */}
|
||||
<h4 className="text-xs font-medium text-muted-foreground mb-3">Version Management</h4>
|
||||
{/* Expanded section: Version Management (available even when not running) */}
|
||||
<Collapsible open={isExpanded} onOpenChange={setIsExpanded}>
|
||||
<CollapsibleContent className="mt-3 pt-3 border-t border-muted">
|
||||
{/* Section header */}
|
||||
<h4 className="text-xs font-medium text-muted-foreground mb-3">Version Management</h4>
|
||||
|
||||
{/* Version picker row */}
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Dropdown - full width, no truncation */}
|
||||
<Select
|
||||
value={selectedVersion}
|
||||
onValueChange={setSelectedVersion}
|
||||
disabled={versionsLoading}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs flex-1">
|
||||
<SelectValue placeholder="Select version to install..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{versionsData?.versions.slice(0, 20).map((v) => {
|
||||
const vIsUnstable =
|
||||
versionsData?.maxStableVersion &&
|
||||
isNewerVersionClient(v, versionsData.maxStableVersion);
|
||||
return (
|
||||
<SelectItem key={v} value={v} className="text-xs">
|
||||
<span className="flex items-center gap-2">
|
||||
v{v}
|
||||
{v === versionsData.latestStable && (
|
||||
<span className="text-green-600 dark:text-green-400">(stable)</span>
|
||||
)}
|
||||
{vIsUnstable && (
|
||||
<span className="text-amber-600 dark:text-amber-400">⚠</span>
|
||||
)}
|
||||
</span>
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{/* Version picker row */}
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Dropdown - full width, no truncation */}
|
||||
<Select
|
||||
value={selectedVersion}
|
||||
onValueChange={setSelectedVersion}
|
||||
disabled={versionsLoading}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs flex-1">
|
||||
<SelectValue placeholder="Select version to install..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{versionsData?.versions.slice(0, 20).map((v) => {
|
||||
const vIsUnstable =
|
||||
versionsData?.maxStableVersion &&
|
||||
isNewerVersionClient(v, versionsData.maxStableVersion);
|
||||
return (
|
||||
<SelectItem key={v} value={v} className="text-xs">
|
||||
<span className="flex items-center gap-2">
|
||||
v{v}
|
||||
{v === versionsData.latestStable && (
|
||||
<span className="text-green-600 dark:text-green-400">(stable)</span>
|
||||
)}
|
||||
{vIsUnstable && (
|
||||
<span className="text-amber-600 dark:text-amber-400">⚠</span>
|
||||
)}
|
||||
</span>
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* Install button */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8 text-xs gap-1.5 px-3"
|
||||
onClick={() => handleInstallVersion(selectedVersion)}
|
||||
disabled={installVersion.isPending || !selectedVersion}
|
||||
>
|
||||
{installVersion.isPending ? (
|
||||
<RefreshCw className="w-3.5 h-3.5 animate-spin" />
|
||||
) : (
|
||||
<Download className="w-3.5 h-3.5" />
|
||||
)}
|
||||
Install
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Stability warning for selected version */}
|
||||
{selectedVersion &&
|
||||
versionsData?.maxStableVersion &&
|
||||
isNewerVersionClient(selectedVersion, versionsData.maxStableVersion) && (
|
||||
<div className="mt-2 flex items-center gap-1.5 text-[11px] text-amber-600 dark:text-amber-400">
|
||||
<AlertTriangle className="w-3.5 h-3.5 flex-shrink-0" />
|
||||
<span>Versions above {versionsData.maxStableVersion} have known issues</span>
|
||||
</div>
|
||||
{/* Install button */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8 text-xs gap-1.5 px-3"
|
||||
onClick={() => handleInstallVersion(selectedVersion)}
|
||||
disabled={installVersion.isPending || !selectedVersion}
|
||||
>
|
||||
{installVersion.isPending ? (
|
||||
<RefreshCw className="w-3.5 h-3.5 animate-spin" />
|
||||
) : (
|
||||
<Download className="w-3.5 h-3.5" />
|
||||
)}
|
||||
Install
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Sync time */}
|
||||
{updateCheck?.checkedAt && (
|
||||
<div className="mt-2 text-[10px] text-muted-foreground/60">
|
||||
Last checked {formatTimeAgo(updateCheck.checkedAt)}
|
||||
{/* Stability warning for selected version */}
|
||||
{selectedVersion &&
|
||||
versionsData?.maxStableVersion &&
|
||||
isNewerVersionClient(selectedVersion, versionsData.maxStableVersion) && (
|
||||
<div className="mt-2 flex items-center gap-1.5 text-[11px] text-amber-600 dark:text-amber-400">
|
||||
<AlertTriangle className="w-3.5 h-3.5 flex-shrink-0" />
|
||||
<span>Versions above {versionsData.maxStableVersion} have known issues</span>
|
||||
</div>
|
||||
)}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
)}
|
||||
|
||||
{/* Sync time */}
|
||||
{updateCheck?.checkedAt && (
|
||||
<div className="mt-2 text-[10px] text-muted-foreground/60">
|
||||
Last checked {formatTimeAgo(updateCheck.checkedAt)}
|
||||
</div>
|
||||
)}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
{/* Not running state */}
|
||||
{!isRunning && (
|
||||
|
||||
@@ -333,9 +333,43 @@ export function useCliproxyUpdateCheck() {
|
||||
return useQuery({
|
||||
queryKey: ['cliproxy-update-check'],
|
||||
queryFn: () => api.cliproxy.updateCheck(),
|
||||
staleTime: 60 * 60 * 1000, // 1 hour (matches backend cache)
|
||||
refetchInterval: 60 * 60 * 1000, // Refresh every hour
|
||||
refetchOnWindowFocus: false, // Don't refresh on window focus (save API calls)
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes (reduced from 1 hour for faster backend switch response)
|
||||
refetchInterval: 5 * 60 * 1000, // Refresh every 5 minutes
|
||||
refetchOnWindowFocus: true, // Refetch on window focus to catch backend changes
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== Backend Management ====================
|
||||
|
||||
/**
|
||||
* Hook for switching CLIProxy backend (original vs plus)
|
||||
* Invalidates all backend-dependent queries to ensure UI consistency
|
||||
*/
|
||||
export function useUpdateBackend() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationKey: ['update-backend'], // Used by ProxyStatusWidget to detect backend switching
|
||||
mutationFn: ({ backend, force = false }: { backend: 'original' | 'plus'; force?: boolean }) =>
|
||||
api.cliproxyServer.updateBackend(backend, force),
|
||||
onSuccess: () => {
|
||||
// Invalidate all queries that depend on backend setting
|
||||
// Use refetchType: 'all' to force immediate refetch even if query is stale
|
||||
queryClient.invalidateQueries({ queryKey: ['cliproxy-update-check'], refetchType: 'all' });
|
||||
queryClient.invalidateQueries({ queryKey: ['cliproxy-versions'], refetchType: 'all' });
|
||||
queryClient.invalidateQueries({ queryKey: ['cliproxy-server-config'], refetchType: 'all' });
|
||||
queryClient.invalidateQueries({ queryKey: ['proxy-status'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['cliproxy-stats'] });
|
||||
toast.success('Backend updated');
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
// Handle 409 conflict (proxy running)
|
||||
if (error.message.includes('Proxy is running')) {
|
||||
toast.error('Stop the proxy first to change backend');
|
||||
} else {
|
||||
toast.error(error.message);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -345,8 +379,8 @@ export function useCliproxyVersions() {
|
||||
return useQuery({
|
||||
queryKey: ['cliproxy-versions'],
|
||||
queryFn: () => api.cliproxy.versions(),
|
||||
staleTime: 60 * 60 * 1000, // 1 hour (matches backend cache)
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes (reduced for faster backend switch response)
|
||||
refetchOnWindowFocus: true, // Refetch on focus to catch backend changes
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Zap className="w-5 h-5 text-primary" />
|
||||
<h1 className="font-semibold">CLIProxy Plus</h1>
|
||||
<h1 className="font-semibold">{updateCheck?.backendLabel ?? 'CLIProxy'}</h1>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
@@ -18,10 +18,11 @@ import {
|
||||
Box,
|
||||
AlertTriangle,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { useProxyConfig, useRawConfig } from '../../hooks';
|
||||
import { useUpdateBackend, useProxyStatus } from '@/hooks/use-cliproxy';
|
||||
import { LocalProxyCard } from './local-proxy-card';
|
||||
import { RemoteProxyCard } from './remote-proxy-card';
|
||||
import { ProxyStatusWidget } from '@/components/monitoring/proxy-status-widget';
|
||||
import { api } from '@/lib/api-client';
|
||||
|
||||
/** LocalStorage key for debug mode preference */
|
||||
@@ -74,10 +75,12 @@ 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();
|
||||
const { data: proxyStatus } = useProxyStatus();
|
||||
const isProxyRunning = proxyStatus?.running ?? false;
|
||||
|
||||
// Fetch backend setting
|
||||
const fetchBackend = useCallback(async () => {
|
||||
@@ -100,24 +103,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 +137,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,9 +252,18 @@ export default function ProxySection() {
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="p-5 space-y-6">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
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
|
||||
</p>
|
||||
|
||||
{/* Proxy Status Widget - Quick access to start/stop controls */}
|
||||
{!isRemoteMode && (
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-base font-medium">Instance Status</h3>
|
||||
<ProxyStatusWidget />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mode Toggle - Card based selection */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-base font-medium">Connection Mode</h3>
|
||||
@@ -277,7 +285,7 @@ export default function ProxySection() {
|
||||
<span className="font-medium">Local</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Run CLIProxy Plus binary on this machine
|
||||
Run {backend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy'} binary on this machine
|
||||
</p>
|
||||
</button>
|
||||
|
||||
@@ -298,7 +306,7 @@ export default function ProxySection() {
|
||||
<span className="font-medium">Remote</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Connect to a remote CLIProxy Plus server
|
||||
Connect to a remote {backend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy'} server
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
@@ -310,16 +318,25 @@ export default function ProxySection() {
|
||||
<Box className="w-4 h-4" />
|
||||
Backend Binary
|
||||
</h3>
|
||||
{/* Warning when proxy is running - must stop to change backend */}
|
||||
{isProxyRunning && (
|
||||
<Alert className="py-2 border-amber-200 bg-amber-50 dark:border-amber-900/50 dark:bg-amber-900/20 [&>svg]:top-2.5">
|
||||
<AlertTriangle className="h-4 w-4 text-amber-600" />
|
||||
<AlertDescription className="text-amber-700 dark:text-amber-400">
|
||||
Stop the running proxy in Instance Status to switch backend.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{/* Plus Backend Card */}
|
||||
<button
|
||||
onClick={() => handleBackendChange('plus')}
|
||||
disabled={backendSaving}
|
||||
disabled={updateBackendMutation.isPending || isProxyRunning}
|
||||
className={`p-4 rounded-lg border-2 text-left transition-all ${
|
||||
backend === 'plus'
|
||||
? 'border-primary bg-primary/5'
|
||||
: 'border-border hover:border-muted-foreground/50'
|
||||
}`}
|
||||
} ${isProxyRunning ? 'opacity-60 cursor-not-allowed' : ''}`}
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<span className="font-medium">CLIProxyAPIPlus</span>
|
||||
@@ -335,12 +352,12 @@ export default function ProxySection() {
|
||||
{/* Original Backend Card */}
|
||||
<button
|
||||
onClick={() => handleBackendChange('original')}
|
||||
disabled={backendSaving}
|
||||
disabled={updateBackendMutation.isPending || isProxyRunning}
|
||||
className={`p-4 rounded-lg border-2 text-left transition-all ${
|
||||
backend === 'original'
|
||||
? 'border-primary bg-primary/5'
|
||||
: 'border-border hover:border-muted-foreground/50'
|
||||
}`}
|
||||
} ${isProxyRunning ? 'opacity-60 cursor-not-allowed' : ''}`}
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<span className="font-medium">CLIProxyAPI</span>
|
||||
|
||||
Reference in New Issue
Block a user