mirror of
https://github.com/tiennm99/ccs.git
synced 2026-08-23 16:23:35 +00:00
feat(cliproxy): add backend selection for CLIProxyAPI vs CLIProxyAPIPlus
Add cliproxy.backend config field to choose between CLIProxyAPI (original) and CLIProxyAPIPlus backends. Includes CLI --backend flag, Dashboard toggle, and provider validation to block Kiro/ghcp on original backend.
This commit is contained in:
@@ -8,9 +8,10 @@
|
|||||||
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 { CLIPROXY_FALLBACK_VERSION, CLIPROXY_MAX_STABLE_VERSION } from './platform-detector';
|
import { BACKEND_CONFIG, DEFAULT_BACKEND, CLIPROXY_MAX_STABLE_VERSION } from './platform-detector';
|
||||||
import { isProxyRunning, stopProxy } from './services/proxy-lifecycle-service';
|
import { isProxyRunning, 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 {
|
import {
|
||||||
UpdateCheckResult,
|
UpdateCheckResult,
|
||||||
checkForUpdates,
|
checkForUpdates,
|
||||||
@@ -27,24 +28,53 @@ import {
|
|||||||
ensureBinary,
|
ensureBinary,
|
||||||
} from './binary';
|
} from './binary';
|
||||||
|
|
||||||
/** Default configuration (uses CLIProxyAPIPlus fork with Kiro + Copilot support) */
|
type CLIProxyBackend = 'original' | 'plus';
|
||||||
const DEFAULT_CONFIG: BinaryManagerConfig = {
|
|
||||||
version: CLIPROXY_FALLBACK_VERSION,
|
/**
|
||||||
releaseUrl: 'https://github.com/router-for-me/CLIProxyAPIPlus/releases/download',
|
* Get backend from config or default to 'plus'
|
||||||
binPath: getBinDir(),
|
*/
|
||||||
maxRetries: 3,
|
function getConfiguredBackend(): CLIProxyBackend {
|
||||||
verbose: false,
|
try {
|
||||||
forceVersion: false,
|
const config = loadOrCreateUnifiedConfig();
|
||||||
};
|
return config.cliproxy?.backend || DEFAULT_BACKEND;
|
||||||
|
} catch {
|
||||||
|
return DEFAULT_BACKEND;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get backend-specific binary directory.
|
||||||
|
* Stores binaries in separate dirs: bin/original/ and bin/plus/
|
||||||
|
*/
|
||||||
|
function getBackendBinDir(backend: CLIProxyBackend = DEFAULT_BACKEND): string {
|
||||||
|
const baseDir = getBinDir();
|
||||||
|
return `${baseDir}/${backend}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Default configuration (uses backend from config.yaml or defaults to 'plus') */
|
||||||
|
function createDefaultConfig(backend: CLIProxyBackend = DEFAULT_BACKEND): BinaryManagerConfig {
|
||||||
|
const backendConfig = BACKEND_CONFIG[backend];
|
||||||
|
return {
|
||||||
|
version: backendConfig.fallbackVersion,
|
||||||
|
releaseUrl: `https://github.com/${backendConfig.repo}/releases/download`,
|
||||||
|
binPath: getBackendBinDir(backend),
|
||||||
|
maxRetries: 3,
|
||||||
|
verbose: false,
|
||||||
|
forceVersion: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Binary Manager class for CLIProxyAPI binary lifecycle
|
* Binary Manager class for CLIProxyAPI binary lifecycle
|
||||||
*/
|
*/
|
||||||
export class BinaryManager {
|
export class BinaryManager {
|
||||||
private config: BinaryManagerConfig;
|
private config: BinaryManagerConfig;
|
||||||
|
private backend: CLIProxyBackend;
|
||||||
|
|
||||||
constructor(config: Partial<BinaryManagerConfig> = {}) {
|
constructor(config: Partial<BinaryManagerConfig> = {}, backend?: CLIProxyBackend) {
|
||||||
this.config = { ...DEFAULT_CONFIG, ...config };
|
this.backend = backend ?? getConfiguredBackend();
|
||||||
|
const defaultConfig = createDefaultConfig(this.backend);
|
||||||
|
this.config = { ...defaultConfig, ...config };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Ensure binary is available (download if missing, update if outdated) */
|
/** Ensure binary is available (download if missing, update if outdated) */
|
||||||
@@ -80,36 +110,44 @@ export class BinaryManager {
|
|||||||
|
|
||||||
/** Convenience function respecting version pin */
|
/** Convenience function respecting version pin */
|
||||||
export async function ensureCLIProxyBinary(verbose = false): Promise<string> {
|
export async function ensureCLIProxyBinary(verbose = false): Promise<string> {
|
||||||
|
const backend = getConfiguredBackend();
|
||||||
const pinnedVersion = getPinnedVersion();
|
const pinnedVersion = getPinnedVersion();
|
||||||
if (pinnedVersion) {
|
if (pinnedVersion) {
|
||||||
if (verbose) console.error(`[cliproxy] Using pinned version: ${pinnedVersion}`);
|
if (verbose) console.error(`[cliproxy] Using pinned version: ${pinnedVersion}`);
|
||||||
return new BinaryManager({
|
return new BinaryManager(
|
||||||
version: pinnedVersion,
|
{
|
||||||
verbose,
|
version: pinnedVersion,
|
||||||
forceVersion: true,
|
verbose,
|
||||||
}).ensureBinary();
|
forceVersion: true,
|
||||||
|
},
|
||||||
|
backend
|
||||||
|
).ensureBinary();
|
||||||
}
|
}
|
||||||
return new BinaryManager({ verbose }).ensureBinary();
|
return new BinaryManager({ verbose }, backend).ensureBinary();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Check if CLIProxyAPI binary is installed */
|
/** Check if CLIProxyAPI binary is installed */
|
||||||
export function isCLIProxyInstalled(): boolean {
|
export function isCLIProxyInstalled(): boolean {
|
||||||
return new BinaryManager().isBinaryInstalled();
|
const backend = getConfiguredBackend();
|
||||||
|
return new BinaryManager({}, backend).isBinaryInstalled();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get CLIProxyAPI binary path (may not exist) */
|
/** Get CLIProxyAPI binary path (may not exist) */
|
||||||
export function getCLIProxyPath(): string {
|
export function getCLIProxyPath(): string {
|
||||||
return new BinaryManager().getBinaryPath();
|
const backend = getConfiguredBackend();
|
||||||
|
return new BinaryManager({}, backend).getBinaryPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get installed CLIProxyAPI version from .version file */
|
/** Get installed CLIProxyAPI version from .version file */
|
||||||
export function getInstalledCliproxyVersion(): string {
|
export function getInstalledCliproxyVersion(): string {
|
||||||
return readInstalledVersion(getBinDir(), CLIPROXY_FALLBACK_VERSION);
|
const backend = getConfiguredBackend();
|
||||||
|
return readInstalledVersion(getBackendBinDir(backend), BACKEND_CONFIG[backend].fallbackVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Install a specific version of CLIProxyAPI */
|
/** Install a specific version of CLIProxyAPI */
|
||||||
export async function installCliproxyVersion(version: string, verbose = false): Promise<void> {
|
export async function installCliproxyVersion(version: string, verbose = false): Promise<void> {
|
||||||
const manager = new BinaryManager({ version, verbose, forceVersion: true });
|
const backend = getConfiguredBackend();
|
||||||
|
const manager = new BinaryManager({ version, verbose, forceVersion: true }, backend);
|
||||||
|
|
||||||
// Check if proxy is running and stop it first
|
// Check if proxy is running and stop it first
|
||||||
if (isProxyRunning()) {
|
if (isProxyRunning()) {
|
||||||
@@ -140,7 +178,8 @@ export async function installCliproxyVersion(version: string, verbose = false):
|
|||||||
|
|
||||||
/** Fetch the latest CLIProxyAPI version from GitHub API */
|
/** Fetch the latest CLIProxyAPI version from GitHub API */
|
||||||
export async function fetchLatestCliproxyVersion(): Promise<string> {
|
export async function fetchLatestCliproxyVersion(): Promise<string> {
|
||||||
const result = await new BinaryManager().checkForUpdates();
|
const backend = getConfiguredBackend();
|
||||||
|
const result = await new BinaryManager({}, backend).checkForUpdates();
|
||||||
return result.latestVersion;
|
return result.latestVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,7 +198,8 @@ export interface CliproxyUpdateCheckResult {
|
|||||||
|
|
||||||
/** Check for CLIProxyAPI binary updates */
|
/** Check for CLIProxyAPI binary updates */
|
||||||
export async function checkCliproxyUpdate(): Promise<CliproxyUpdateCheckResult> {
|
export async function checkCliproxyUpdate(): Promise<CliproxyUpdateCheckResult> {
|
||||||
const result = await new BinaryManager().checkForUpdates();
|
const backend = getConfiguredBackend();
|
||||||
|
const result = await new BinaryManager({}, backend).checkForUpdates();
|
||||||
|
|
||||||
// Import isNewerVersion for stability check
|
// Import isNewerVersion for stability check
|
||||||
const { isNewerVersion } = await import('./binary/version-checker');
|
const { isNewerVersion } = await import('./binary/version-checker');
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ import {
|
|||||||
} from './config-generator';
|
} from './config-generator';
|
||||||
import { checkRemoteProxy } from './remote-proxy-client';
|
import { checkRemoteProxy } from './remote-proxy-client';
|
||||||
import { isAuthenticated } from './auth-handler';
|
import { isAuthenticated } from './auth-handler';
|
||||||
import { CLIProxyProvider, ExecutorConfig } from './types';
|
import { CLIProxyProvider, CLIProxyBackend, PLUS_ONLY_PROVIDERS, ExecutorConfig } from './types';
|
||||||
|
import { DEFAULT_BACKEND } from './platform-detector';
|
||||||
import { configureProviderModel, getCurrentModel } from './model-config';
|
import { configureProviderModel, getCurrentModel } from './model-config';
|
||||||
import { resolveProxyConfig, PROXY_CLI_FLAGS } from './proxy-config-resolver';
|
import { resolveProxyConfig, PROXY_CLI_FLAGS } from './proxy-config-resolver';
|
||||||
import { getWebSearchHookEnv } from '../utils/websearch-manager';
|
import { getWebSearchHookEnv } from '../utils/websearch-manager';
|
||||||
@@ -141,6 +142,20 @@ export async function execClaudeWithCLIProxy(
|
|||||||
// 0. Resolve proxy configuration (CLI > ENV > config.yaml > defaults)
|
// 0. Resolve proxy configuration (CLI > ENV > config.yaml > defaults)
|
||||||
// This filters proxy flags from args and returns resolved config
|
// This filters proxy flags from args and returns resolved config
|
||||||
const unifiedConfig = loadOrCreateUnifiedConfig();
|
const unifiedConfig = loadOrCreateUnifiedConfig();
|
||||||
|
|
||||||
|
// 0a. Runtime backend/provider validation - block kiro/ghcp if backend=original
|
||||||
|
const backend: CLIProxyBackend = unifiedConfig.cliproxy?.backend ?? DEFAULT_BACKEND;
|
||||||
|
if (backend === 'original' && PLUS_ONLY_PROVIDERS.includes(provider)) {
|
||||||
|
console.error('');
|
||||||
|
console.error(fail(`${provider} requires CLIProxyAPIPlus backend`));
|
||||||
|
console.error('');
|
||||||
|
console.error('To use this provider, either:');
|
||||||
|
console.error(' 1. Set `cliproxy.backend: plus` in ~/.ccs/config.yaml');
|
||||||
|
console.error(' 2. Use --backend=plus flag: ccs ' + provider + ' --backend=plus');
|
||||||
|
console.error('');
|
||||||
|
throw new Error(`Provider ${provider} requires Plus backend`);
|
||||||
|
}
|
||||||
|
|
||||||
const cliproxyServerConfig = unifiedConfig.cliproxy_server;
|
const cliproxyServerConfig = unifiedConfig.cliproxy_server;
|
||||||
const { config: proxyConfig, remainingArgs: argsWithoutProxy } = resolveProxyConfig(args, {
|
const { config: proxyConfig, remainingArgs: argsWithoutProxy } = resolveProxyConfig(args, {
|
||||||
remote: cliproxyServerConfig?.remote
|
remote: cliproxyServerConfig?.remote
|
||||||
|
|||||||
@@ -7,12 +7,35 @@
|
|||||||
|
|
||||||
import { PlatformInfo, SupportedOS, SupportedArch, ArchiveExtension } from './types';
|
import { PlatformInfo, SupportedOS, SupportedArch, ArchiveExtension } from './types';
|
||||||
|
|
||||||
|
// Phase 1 will add CLIProxyBackend type - using string literal temporarily
|
||||||
|
type CLIProxyBackend = 'original' | 'plus';
|
||||||
|
|
||||||
|
/** Backend configuration */
|
||||||
|
export const BACKEND_CONFIG = {
|
||||||
|
original: {
|
||||||
|
repo: 'router-for-me/CLIProxyAPI',
|
||||||
|
binaryPrefix: 'CLIProxyAPI',
|
||||||
|
executable: 'cli-proxy-api',
|
||||||
|
fallbackVersion: '6.7.8',
|
||||||
|
},
|
||||||
|
plus: {
|
||||||
|
repo: 'router-for-me/CLIProxyAPIPlus',
|
||||||
|
binaryPrefix: 'CLIProxyAPIPlus',
|
||||||
|
executable: 'cli-proxy-api-plus',
|
||||||
|
fallbackVersion: '6.7.8-0',
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** Default backend */
|
||||||
|
export const DEFAULT_BACKEND: CLIProxyBackend = 'plus';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CLIProxyAPIPlus fallback version (used when GitHub API unavailable)
|
* CLIProxyAPIPlus fallback version (used when GitHub API unavailable)
|
||||||
* Auto-update fetches latest from GitHub; this is only a safety net
|
* Auto-update fetches latest from GitHub; this is only a safety net
|
||||||
* Note: CLIProxyAPIPlus uses v6.6.X-0 suffix pattern
|
* Note: CLIProxyAPIPlus uses v6.6.X-0 suffix pattern
|
||||||
|
* @deprecated Use getFallbackVersion() or BACKEND_CONFIG instead
|
||||||
*/
|
*/
|
||||||
export const CLIPROXY_FALLBACK_VERSION = '6.6.40-0';
|
export const CLIPROXY_FALLBACK_VERSION = BACKEND_CONFIG[DEFAULT_BACKEND].fallbackVersion;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maximum stable version cap - prevents auto-update to known unstable releases
|
* Maximum stable version cap - prevents auto-update to known unstable releases
|
||||||
@@ -48,10 +71,14 @@ const ARCH_MAP: Record<string, SupportedArch | undefined> = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Detect current platform and return binary info
|
* Detect current platform and return binary info
|
||||||
* @param version Optional version for binaryName (defaults to fallback)
|
* @param version Optional version for binaryName (defaults to backend fallback)
|
||||||
|
* @param backend Backend variant to use (defaults to DEFAULT_BACKEND)
|
||||||
* @throws Error if platform is unsupported
|
* @throws Error if platform is unsupported
|
||||||
*/
|
*/
|
||||||
export function detectPlatform(version: string = CLIPROXY_FALLBACK_VERSION): PlatformInfo {
|
export function detectPlatform(
|
||||||
|
version?: string,
|
||||||
|
backend: CLIProxyBackend = DEFAULT_BACKEND
|
||||||
|
): PlatformInfo {
|
||||||
const nodePlatform = process.platform;
|
const nodePlatform = process.platform;
|
||||||
const nodeArch = process.arch;
|
const nodeArch = process.arch;
|
||||||
|
|
||||||
@@ -71,8 +98,10 @@ export function detectPlatform(version: string = CLIPROXY_FALLBACK_VERSION): Pla
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const config = BACKEND_CONFIG[backend];
|
||||||
|
const ver = version || config.fallbackVersion;
|
||||||
const extension: ArchiveExtension = os === 'windows' ? 'zip' : 'tar.gz';
|
const extension: ArchiveExtension = os === 'windows' ? 'zip' : 'tar.gz';
|
||||||
const binaryName = `CLIProxyAPIPlus_${version}_${os}_${arch}.${extension}`;
|
const binaryName = `${config.binaryPrefix}_${ver}_${os}_${arch}.${extension}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
os,
|
os,
|
||||||
@@ -84,41 +113,62 @@ export function detectPlatform(version: string = CLIPROXY_FALLBACK_VERSION): Pla
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get executable name based on platform
|
* Get executable name based on platform
|
||||||
|
* @param backend Backend variant to use (defaults to DEFAULT_BACKEND)
|
||||||
* @returns Binary executable name (with .exe on Windows)
|
* @returns Binary executable name (with .exe on Windows)
|
||||||
* Note: The actual binary inside the archive is named 'cli-proxy-api-plus'
|
|
||||||
*/
|
*/
|
||||||
export function getExecutableName(): string {
|
export function getExecutableName(backend: CLIProxyBackend = DEFAULT_BACKEND): string {
|
||||||
const platform = detectPlatform();
|
const config = BACKEND_CONFIG[backend];
|
||||||
return platform.os === 'windows' ? 'cli-proxy-api-plus.exe' : 'cli-proxy-api-plus';
|
const platform = detectPlatform(undefined, backend);
|
||||||
|
return platform.os === 'windows' ? `${config.executable}.exe` : config.executable;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the name of the binary inside the archive
|
* Get the name of the binary inside the archive
|
||||||
|
* @param backend Backend variant to use (defaults to DEFAULT_BACKEND)
|
||||||
* @returns Binary name as it appears in the tar.gz/zip
|
* @returns Binary name as it appears in the tar.gz/zip
|
||||||
*/
|
*/
|
||||||
export function getArchiveBinaryName(): string {
|
export function getArchiveBinaryName(backend: CLIProxyBackend = DEFAULT_BACKEND): string {
|
||||||
const platform = detectPlatform();
|
return getExecutableName(backend);
|
||||||
return platform.os === 'windows' ? 'cli-proxy-api-plus.exe' : 'cli-proxy-api-plus';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get download URL for current platform
|
* Get download URL for current platform
|
||||||
* @param version Version to download (defaults to fallback version)
|
* @param version Version to download (defaults to backend fallback version)
|
||||||
|
* @param backend Backend variant to use (defaults to DEFAULT_BACKEND)
|
||||||
* @returns Full GitHub release download URL
|
* @returns Full GitHub release download URL
|
||||||
*/
|
*/
|
||||||
export function getDownloadUrl(version: string = CLIPROXY_FALLBACK_VERSION): string {
|
export function getDownloadUrl(
|
||||||
const platform = detectPlatform(version);
|
version?: string,
|
||||||
const baseUrl = `https://github.com/router-for-me/CLIProxyAPIPlus/releases/download/v${version}`;
|
backend: CLIProxyBackend = DEFAULT_BACKEND
|
||||||
return `${baseUrl}/${platform.binaryName}`;
|
): string {
|
||||||
|
const config = BACKEND_CONFIG[backend];
|
||||||
|
const ver = version || config.fallbackVersion;
|
||||||
|
const platform = detectPlatform(ver, backend);
|
||||||
|
return `https://github.com/${config.repo}/releases/download/v${ver}/${platform.binaryName}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get checksums.txt URL for version
|
* Get checksums.txt URL for version
|
||||||
* @param version Version to get checksums for (defaults to fallback version)
|
* @param version Version to get checksums for (defaults to backend fallback version)
|
||||||
|
* @param backend Backend variant to use (defaults to DEFAULT_BACKEND)
|
||||||
* @returns Full URL to checksums.txt
|
* @returns Full URL to checksums.txt
|
||||||
*/
|
*/
|
||||||
export function getChecksumsUrl(version: string = CLIPROXY_FALLBACK_VERSION): string {
|
export function getChecksumsUrl(
|
||||||
return `https://github.com/router-for-me/CLIProxyAPIPlus/releases/download/v${version}/checksums.txt`;
|
version?: string,
|
||||||
|
backend: CLIProxyBackend = DEFAULT_BACKEND
|
||||||
|
): string {
|
||||||
|
const config = BACKEND_CONFIG[backend];
|
||||||
|
const ver = version || config.fallbackVersion;
|
||||||
|
return `https://github.com/${config.repo}/releases/download/v${ver}/checksums.txt`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get fallback version for backend
|
||||||
|
* @param backend Backend variant to use (defaults to DEFAULT_BACKEND)
|
||||||
|
* @returns Fallback version string
|
||||||
|
*/
|
||||||
|
export function getFallbackVersion(backend: CLIProxyBackend = DEFAULT_BACKEND): string {
|
||||||
|
return BACKEND_CONFIG[backend].fallbackVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ import {
|
|||||||
clearPinnedVersion,
|
clearPinnedVersion,
|
||||||
isVersionPinned,
|
isVersionPinned,
|
||||||
} from '../binary-manager';
|
} from '../binary-manager';
|
||||||
import { CLIPROXY_FALLBACK_VERSION } from '../platform-detector';
|
import { BACKEND_CONFIG, DEFAULT_BACKEND } from '../platform-detector';
|
||||||
|
import { CLIProxyBackend } from '../types';
|
||||||
|
import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
|
||||||
|
|
||||||
/** Binary status result */
|
/** Binary status result */
|
||||||
export interface BinaryStatusResult {
|
export interface BinaryStatusResult {
|
||||||
@@ -28,6 +30,7 @@ export interface BinaryStatusResult {
|
|||||||
pinnedVersion: string | null;
|
pinnedVersion: string | null;
|
||||||
binaryPath: string;
|
binaryPath: string;
|
||||||
fallbackVersion: string;
|
fallbackVersion: string;
|
||||||
|
backend: CLIProxyBackend;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Install result */
|
/** Install result */
|
||||||
@@ -48,15 +51,19 @@ export interface LatestVersionResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get current binary status
|
* Get current binary status for a specific backend
|
||||||
*/
|
*/
|
||||||
export function getBinaryStatus(): BinaryStatusResult {
|
export function getBinaryStatus(backend?: CLIProxyBackend): BinaryStatusResult {
|
||||||
|
const effectiveBackend =
|
||||||
|
backend ?? loadOrCreateUnifiedConfig().cliproxy?.backend ?? DEFAULT_BACKEND;
|
||||||
|
const backendConfig = BACKEND_CONFIG[effectiveBackend];
|
||||||
return {
|
return {
|
||||||
installed: isCLIProxyInstalled(),
|
installed: isCLIProxyInstalled(),
|
||||||
currentVersion: getInstalledCliproxyVersion(),
|
currentVersion: getInstalledCliproxyVersion(),
|
||||||
pinnedVersion: getPinnedVersion(),
|
pinnedVersion: getPinnedVersion(),
|
||||||
binaryPath: getCLIProxyPath(),
|
binaryPath: getCLIProxyPath(),
|
||||||
fallbackVersion: CLIPROXY_FALLBACK_VERSION,
|
fallbackVersion: backendConfig.fallbackVersion,
|
||||||
|
backend: effectiveBackend,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,10 @@
|
|||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { CLIProxyProfileName } from '../../auth/profile-detector';
|
import { CLIProxyProfileName } from '../../auth/profile-detector';
|
||||||
import { CLIProxyProvider } from '../types';
|
import { CLIProxyProvider, CLIProxyBackend, PLUS_ONLY_PROVIDERS } from '../types';
|
||||||
import { isReservedName, isWindowsReservedName } from '../../config/reserved-names';
|
import { isReservedName, isWindowsReservedName } from '../../config/reserved-names';
|
||||||
|
import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
|
||||||
|
import { DEFAULT_BACKEND } from '../platform-detector';
|
||||||
import { isUnifiedMode } from '../../config/unified-config-loader';
|
import { isUnifiedMode } from '../../config/unified-config-loader';
|
||||||
import { deleteConfigForPort } from '../config-generator';
|
import { deleteConfigForPort } from '../config-generator';
|
||||||
import { deleteSessionLockForPort } from '../session-tracker';
|
import { deleteSessionLockForPort } from '../session-tracker';
|
||||||
@@ -64,6 +66,22 @@ export function validateProfileName(name: string): string | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate provider/backend compatibility
|
||||||
|
* Returns error message if provider requires Plus backend but original is configured
|
||||||
|
*/
|
||||||
|
export function validateProviderBackend(provider: CLIProxyProfileName): string | null {
|
||||||
|
const config = loadOrCreateUnifiedConfig();
|
||||||
|
const backend: CLIProxyBackend = config.cliproxy?.backend ?? DEFAULT_BACKEND;
|
||||||
|
|
||||||
|
// Normalize provider to lowercase for case-insensitive comparison
|
||||||
|
const normalizedProvider = provider.toLowerCase() as CLIProxyProvider;
|
||||||
|
if (backend === 'original' && PLUS_ONLY_PROVIDERS.includes(normalizedProvider)) {
|
||||||
|
return `${provider} requires CLIProxyAPIPlus. Set \`cliproxy.backend: plus\` in config.yaml or use --backend=plus`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if CLIProxy variant profile exists
|
* Check if CLIProxy variant profile exists
|
||||||
*/
|
*/
|
||||||
@@ -88,6 +106,12 @@ export function createVariant(
|
|||||||
account?: string
|
account?: string
|
||||||
): VariantOperationResult {
|
): VariantOperationResult {
|
||||||
try {
|
try {
|
||||||
|
// Validate provider/backend compatibility (block kiro/ghcp on original backend)
|
||||||
|
const backendError = validateProviderBackend(provider);
|
||||||
|
if (backendError) {
|
||||||
|
return { success: false, error: backendError };
|
||||||
|
}
|
||||||
|
|
||||||
// Allocate unique port for this variant
|
// Allocate unique port for this variant
|
||||||
const port = getNextAvailablePort();
|
const port = getNextAvailablePort();
|
||||||
|
|
||||||
@@ -188,6 +212,14 @@ export function updateVariant(name: string, updates: UpdateVariantOptions): Vari
|
|||||||
// Update config entry if provider or account changed
|
// Update config entry if provider or account changed
|
||||||
if (updates.provider !== undefined || updates.account !== undefined) {
|
if (updates.provider !== undefined || updates.account !== undefined) {
|
||||||
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;
|
||||||
|
|
||||||
if (isUnifiedMode()) {
|
if (isUnifiedMode()) {
|
||||||
|
|||||||
@@ -119,6 +119,18 @@ export interface DownloadResult {
|
|||||||
*/
|
*/
|
||||||
export type CLIProxyProvider = 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp';
|
export type CLIProxyProvider = 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CLIProxy backend selection
|
||||||
|
* - original: CLIProxyAPI (no Kiro/ghcp support)
|
||||||
|
* - plus: CLIProxyAPIPlus (Kiro/ghcp support, default)
|
||||||
|
*/
|
||||||
|
export type CLIProxyBackend = 'original' | 'plus';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Providers that require CLIProxyAPIPlus backend
|
||||||
|
*/
|
||||||
|
export const PLUS_ONLY_PROVIDERS: CLIProxyProvider[] = ['kiro', 'ghcp'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CLIProxy config.yaml structure (minimal)
|
* CLIProxy config.yaml structure (minimal)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -29,11 +29,11 @@ import {
|
|||||||
} from '../cliproxy/account-manager';
|
} from '../cliproxy/account-manager';
|
||||||
import { fetchAllProviderQuotas } from '../cliproxy/quota-fetcher';
|
import { fetchAllProviderQuotas } from '../cliproxy/quota-fetcher';
|
||||||
import { isOnCooldown } from '../cliproxy/quota-manager';
|
import { isOnCooldown } from '../cliproxy/quota-manager';
|
||||||
import { CLIPROXY_FALLBACK_VERSION } from '../cliproxy/platform-detector';
|
import { DEFAULT_BACKEND, CLIPROXY_FALLBACK_VERSION } from '../cliproxy/platform-detector';
|
||||||
import { CLIPROXY_PROFILES, CLIProxyProfileName } from '../auth/profile-detector';
|
import { CLIPROXY_PROFILES, CLIProxyProfileName } from '../auth/profile-detector';
|
||||||
import { supportsModelConfig, getProviderCatalog, ModelEntry } from '../cliproxy/model-catalog';
|
import { supportsModelConfig, getProviderCatalog, ModelEntry } from '../cliproxy/model-catalog';
|
||||||
import { CLIProxyProvider } from '../cliproxy/types';
|
import { CLIProxyProvider, CLIProxyBackend } from '../cliproxy/types';
|
||||||
import { isUnifiedMode } from '../config/unified-config-loader';
|
import { isUnifiedMode, loadOrCreateUnifiedConfig } from '../config/unified-config-loader';
|
||||||
import {
|
import {
|
||||||
initUI,
|
initUI,
|
||||||
header,
|
header,
|
||||||
@@ -68,6 +68,47 @@ import {
|
|||||||
// ARGUMENT PARSING
|
// ARGUMENT PARSING
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse --backend flag from args
|
||||||
|
* Returns the backend value and remaining args without --backend flag
|
||||||
|
*/
|
||||||
|
function parseBackendArg(args: string[]): {
|
||||||
|
backend: CLIProxyBackend | undefined;
|
||||||
|
remainingArgs: string[];
|
||||||
|
} {
|
||||||
|
const backendIdx = args.indexOf('--backend');
|
||||||
|
if (backendIdx === -1) {
|
||||||
|
// Also check for --backend=value format
|
||||||
|
const backendEqualsIdx = args.findIndex((a) => a.startsWith('--backend='));
|
||||||
|
if (backendEqualsIdx !== -1) {
|
||||||
|
const value = args[backendEqualsIdx].split('=')[1] as CLIProxyBackend;
|
||||||
|
if (value !== 'original' && value !== 'plus') {
|
||||||
|
return { backend: undefined, remainingArgs: args };
|
||||||
|
}
|
||||||
|
const remainingArgs = [...args];
|
||||||
|
remainingArgs.splice(backendEqualsIdx, 1);
|
||||||
|
return { backend: value, remainingArgs };
|
||||||
|
}
|
||||||
|
return { backend: undefined, remainingArgs: args };
|
||||||
|
}
|
||||||
|
const value = args[backendIdx + 1];
|
||||||
|
if (value !== 'original' && value !== 'plus') {
|
||||||
|
return { backend: undefined, remainingArgs: args };
|
||||||
|
}
|
||||||
|
const remainingArgs = [...args];
|
||||||
|
remainingArgs.splice(backendIdx, 2);
|
||||||
|
return { backend: value, remainingArgs };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get effective backend (CLI flag > config.yaml > default)
|
||||||
|
*/
|
||||||
|
function getEffectiveBackend(cliBackend?: CLIProxyBackend): CLIProxyBackend {
|
||||||
|
if (cliBackend) return cliBackend;
|
||||||
|
const config = loadOrCreateUnifiedConfig();
|
||||||
|
return config.cliproxy?.backend ?? DEFAULT_BACKEND;
|
||||||
|
}
|
||||||
|
|
||||||
interface CliproxyProfileArgs {
|
interface CliproxyProfileArgs {
|
||||||
name?: string;
|
name?: string;
|
||||||
provider?: CLIProxyProfileName;
|
provider?: CLIProxyProfileName;
|
||||||
@@ -431,14 +472,18 @@ async function handleProxyStatus(): Promise<void> {
|
|||||||
console.log('');
|
console.log('');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function showStatus(verbose: boolean): Promise<void> {
|
async function showStatus(verbose: boolean, backend: CLIProxyBackend): Promise<void> {
|
||||||
await initUI();
|
await initUI();
|
||||||
const status = getBinaryStatus();
|
const status = getBinaryStatus(backend);
|
||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log(color('CLIProxy Plus Status', 'primary'));
|
const backendLabel = backend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy (Original)';
|
||||||
|
console.log(color(`${backendLabel} Status`, 'primary'));
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
` Backend: ${color(backend, 'info')}${backend === DEFAULT_BACKEND ? dim(' (default)') : ''}`
|
||||||
|
);
|
||||||
if (status.installed) {
|
if (status.installed) {
|
||||||
console.log(` Installed: ${color('Yes', 'success')}`);
|
console.log(` Installed: ${color('Yes', 'success')}`);
|
||||||
const versionLabel = status.pinnedVersion
|
const versionLabel = status.pinnedVersion
|
||||||
@@ -576,7 +621,13 @@ async function showHelp(): Promise<void> {
|
|||||||
['--update', 'Unpin and update to latest version'],
|
['--update', 'Unpin and update to latest version'],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
['Options:', [['--verbose, -v', 'Show detailed quota fetch diagnostics']]],
|
[
|
||||||
|
'Options:',
|
||||||
|
[
|
||||||
|
['--backend <type>', 'Use specific backend: original | plus (default: from config)'],
|
||||||
|
['--verbose, -v', 'Show detailed quota fetch diagnostics'],
|
||||||
|
],
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const [title, cmds] of sections) {
|
for (const [title, cmds] of sections) {
|
||||||
@@ -909,16 +960,20 @@ async function handleQuotaStatus(verbose = false): Promise<void> {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
export async function handleCliproxyCommand(args: string[]): Promise<void> {
|
export async function handleCliproxyCommand(args: string[]): Promise<void> {
|
||||||
const verbose = args.includes('--verbose') || args.includes('-v');
|
// Parse --backend flag first (before other processing)
|
||||||
const command = args[0];
|
const { backend: cliBackend, remainingArgs } = parseBackendArg(args);
|
||||||
|
const effectiveBackend = getEffectiveBackend(cliBackend);
|
||||||
|
|
||||||
if (args.includes('--help') || args.includes('-h')) {
|
const verbose = remainingArgs.includes('--verbose') || remainingArgs.includes('-v');
|
||||||
|
const command = remainingArgs[0];
|
||||||
|
|
||||||
|
if (remainingArgs.includes('--help') || remainingArgs.includes('-h')) {
|
||||||
await showHelp();
|
await showHelp();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (command === 'create') {
|
if (command === 'create') {
|
||||||
await handleCreate(args.slice(1));
|
await handleCreate(remainingArgs.slice(1));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -928,7 +983,7 @@ export async function handleCliproxyCommand(args: string[]): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (command === 'remove' || command === 'delete' || command === 'rm') {
|
if (command === 'remove' || command === 'delete' || command === 'rm') {
|
||||||
await handleRemove(args.slice(1));
|
await handleRemove(remainingArgs.slice(1));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -949,17 +1004,17 @@ export async function handleCliproxyCommand(args: string[]): Promise<void> {
|
|||||||
|
|
||||||
// Quota management commands
|
// Quota management commands
|
||||||
if (command === 'default') {
|
if (command === 'default') {
|
||||||
await handleSetDefault(args.slice(1));
|
await handleSetDefault(remainingArgs.slice(1));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (command === 'pause') {
|
if (command === 'pause') {
|
||||||
await handlePauseAccount(args.slice(1));
|
await handlePauseAccount(remainingArgs.slice(1));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (command === 'resume') {
|
if (command === 'resume') {
|
||||||
await handleResumeAccount(args.slice(1));
|
await handleResumeAccount(remainingArgs.slice(1));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -968,9 +1023,9 @@ export async function handleCliproxyCommand(args: string[]): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const installIdx = args.indexOf('--install');
|
const installIdx = remainingArgs.indexOf('--install');
|
||||||
if (installIdx !== -1) {
|
if (installIdx !== -1) {
|
||||||
let version = args[installIdx + 1];
|
let version = remainingArgs[installIdx + 1];
|
||||||
if (!version || version.startsWith('-')) {
|
if (!version || version.startsWith('-')) {
|
||||||
console.error(fail('Missing version argument for --install'));
|
console.error(fail('Missing version argument for --install'));
|
||||||
console.error(' Usage: ccs cliproxy --install <version>');
|
console.error(' Usage: ccs cliproxy --install <version>');
|
||||||
@@ -983,10 +1038,10 @@ export async function handleCliproxyCommand(args: string[]): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.includes('--latest') || args.includes('--update')) {
|
if (remainingArgs.includes('--latest') || remainingArgs.includes('--update')) {
|
||||||
await handleInstallLatest(verbose);
|
await handleInstallLatest(verbose);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await showStatus(verbose);
|
await showStatus(verbose, effectiveBackend);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,6 +115,8 @@ export interface TokenRefreshSettings {
|
|||||||
* CLIProxy configuration section.
|
* CLIProxy configuration section.
|
||||||
*/
|
*/
|
||||||
export interface CLIProxyConfig {
|
export interface CLIProxyConfig {
|
||||||
|
/** Backend selection: 'original' or 'plus' (default: 'plus') */
|
||||||
|
backend?: 'original' | 'plus';
|
||||||
/** Nickname to email mapping for OAuth accounts */
|
/** Nickname to email mapping for OAuth accounts */
|
||||||
oauth_accounts: OAuthAccounts;
|
oauth_accounts: OAuthAccounts;
|
||||||
/** Built-in providers (read-only, for reference) */
|
/** Built-in providers (read-only, for reference) */
|
||||||
@@ -530,6 +532,7 @@ export function createEmptyUnifiedConfig(): UnifiedConfig {
|
|||||||
accounts: {},
|
accounts: {},
|
||||||
profiles: {},
|
profiles: {},
|
||||||
cliproxy: {
|
cliproxy: {
|
||||||
|
backend: 'plus',
|
||||||
oauth_accounts: {},
|
oauth_accounts: {},
|
||||||
providers: ['gemini', 'codex', 'agy', 'qwen', 'iflow', 'kiro', 'ghcp'],
|
providers: ['gemini', 'codex', 'agy', 'qwen', 'iflow', 'kiro', 'ghcp'],
|
||||||
variants: {},
|
variants: {},
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
import { Router, Request, Response } from 'express';
|
import { Router, Request, Response } from 'express';
|
||||||
import { loadOrCreateUnifiedConfig, saveUnifiedConfig } from '../../config/unified-config-loader';
|
import { loadOrCreateUnifiedConfig, saveUnifiedConfig } from '../../config/unified-config-loader';
|
||||||
import { testConnection } from '../../cliproxy/remote-proxy-client';
|
import { testConnection } from '../../cliproxy/remote-proxy-client';
|
||||||
|
import { isProxyRunning } from '../../cliproxy/services/proxy-lifecycle-service';
|
||||||
import {
|
import {
|
||||||
DEFAULT_CLIPROXY_SERVER_CONFIG,
|
DEFAULT_CLIPROXY_SERVER_CONFIG,
|
||||||
CliproxyServerConfig,
|
CliproxyServerConfig,
|
||||||
@@ -65,6 +66,60 @@ router.put('/', async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/cliproxy-server/backend - Get CLIProxy backend setting
|
||||||
|
*/
|
||||||
|
router.get('/backend', async (_req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const config = await loadOrCreateUnifiedConfig();
|
||||||
|
res.json({ backend: config.cliproxy?.backend ?? 'plus' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[cliproxy-server-routes] Failed to load backend config:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to load backend config' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT /api/cliproxy-server/backend - Update CLIProxy backend setting
|
||||||
|
*/
|
||||||
|
router.put('/backend', async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { backend, force } = req.body;
|
||||||
|
if (backend !== 'original' && backend !== 'plus') {
|
||||||
|
res.status(400).json({ error: 'Invalid backend. Must be "original" or "plus"' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if proxy is running - warn about restart requirement
|
||||||
|
const config = await loadOrCreateUnifiedConfig();
|
||||||
|
const currentBackend = config.cliproxy?.backend ?? 'plus';
|
||||||
|
if (currentBackend !== backend && isProxyRunning() && !force) {
|
||||||
|
res.status(409).json({
|
||||||
|
error: 'Proxy is running. Stop proxy first or use force=true to change backend.',
|
||||||
|
proxyRunning: true,
|
||||||
|
currentBackend,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!config.cliproxy) {
|
||||||
|
config.cliproxy = {
|
||||||
|
backend,
|
||||||
|
oauth_accounts: {},
|
||||||
|
providers: ['gemini', 'codex', 'agy', 'qwen', 'iflow', 'kiro', 'ghcp'],
|
||||||
|
variants: {},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
config.cliproxy.backend = backend;
|
||||||
|
}
|
||||||
|
|
||||||
|
await saveUnifiedConfig(config);
|
||||||
|
res.json({ backend });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[cliproxy-server-routes] Failed to save backend config:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to save backend config' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/cliproxy-server/test - Test remote proxy connection
|
* POST /api/cliproxy-server/test - Test remote proxy connection
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
/**
|
||||||
|
* Tests for CLIProxy Backend Selection
|
||||||
|
* Verifies backend selection feature for original vs plus CLIProxyAPI variants
|
||||||
|
*/
|
||||||
|
|
||||||
|
const assert = require('assert');
|
||||||
|
|
||||||
|
describe('Backend Selection', () => {
|
||||||
|
const platformDetector = require('../../../dist/cliproxy/platform-detector');
|
||||||
|
const types = require('../../../dist/cliproxy/types');
|
||||||
|
|
||||||
|
describe('BACKEND_CONFIG', () => {
|
||||||
|
it('has correct configuration for original backend', () => {
|
||||||
|
const config = platformDetector.BACKEND_CONFIG.original;
|
||||||
|
assert.strictEqual(config.repo, 'router-for-me/CLIProxyAPI');
|
||||||
|
assert.strictEqual(config.binaryPrefix, 'CLIProxyAPI');
|
||||||
|
assert.strictEqual(config.executable, 'cli-proxy-api');
|
||||||
|
assert(config.fallbackVersion.match(/^\d+\.\d+\.\d+$/), 'original version has no suffix');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has correct configuration for plus backend', () => {
|
||||||
|
const config = platformDetector.BACKEND_CONFIG.plus;
|
||||||
|
assert.strictEqual(config.repo, 'router-for-me/CLIProxyAPIPlus');
|
||||||
|
assert.strictEqual(config.binaryPrefix, 'CLIProxyAPIPlus');
|
||||||
|
assert.strictEqual(config.executable, 'cli-proxy-api-plus');
|
||||||
|
assert(config.fallbackVersion.match(/^\d+\.\d+\.\d+-\d+$/), 'plus version has -0 suffix');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('DEFAULT_BACKEND', () => {
|
||||||
|
it('defaults to plus backend for backward compatibility', () => {
|
||||||
|
assert.strictEqual(platformDetector.DEFAULT_BACKEND, 'plus');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('detectPlatform', () => {
|
||||||
|
it('generates correct binary name for original backend', () => {
|
||||||
|
const info = platformDetector.detectPlatform('6.6.51', 'original');
|
||||||
|
assert(info.binaryName.startsWith('CLIProxyAPI_6.6.51_'));
|
||||||
|
assert(!info.binaryName.includes('CLIProxyAPIPlus'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('generates correct binary name for plus backend', () => {
|
||||||
|
const info = platformDetector.detectPlatform('6.6.51-0', 'plus');
|
||||||
|
assert(info.binaryName.startsWith('CLIProxyAPIPlus_6.6.51-0_'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses plus backend by default', () => {
|
||||||
|
const info = platformDetector.detectPlatform();
|
||||||
|
assert(info.binaryName.includes('CLIProxyAPIPlus'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses fallback version when version not specified', () => {
|
||||||
|
const infoOriginal = platformDetector.detectPlatform(undefined, 'original');
|
||||||
|
const fallbackOriginal = platformDetector.BACKEND_CONFIG.original.fallbackVersion;
|
||||||
|
assert(infoOriginal.binaryName.includes(fallbackOriginal));
|
||||||
|
|
||||||
|
const infoPlus = platformDetector.detectPlatform(undefined, 'plus');
|
||||||
|
const fallbackPlus = platformDetector.BACKEND_CONFIG.plus.fallbackVersion;
|
||||||
|
assert(infoPlus.binaryName.includes(fallbackPlus));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getExecutableName', () => {
|
||||||
|
const isWindows = process.platform === 'win32';
|
||||||
|
|
||||||
|
it('returns correct name for original backend', () => {
|
||||||
|
const name = platformDetector.getExecutableName('original');
|
||||||
|
const expected = isWindows ? 'cli-proxy-api.exe' : 'cli-proxy-api';
|
||||||
|
assert.strictEqual(name, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns correct name for plus backend', () => {
|
||||||
|
const name = platformDetector.getExecutableName('plus');
|
||||||
|
const expected = isWindows ? 'cli-proxy-api-plus.exe' : 'cli-proxy-api-plus';
|
||||||
|
assert.strictEqual(name, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defaults to plus backend', () => {
|
||||||
|
const name = platformDetector.getExecutableName();
|
||||||
|
assert(name.includes('cli-proxy-api-plus'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getDownloadUrl', () => {
|
||||||
|
it('uses correct repo for original backend', () => {
|
||||||
|
const url = platformDetector.getDownloadUrl('6.6.51', 'original');
|
||||||
|
assert(url.includes('router-for-me/CLIProxyAPI/releases'));
|
||||||
|
assert(!url.includes('CLIProxyAPIPlus'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses correct repo for plus backend', () => {
|
||||||
|
const url = platformDetector.getDownloadUrl('6.6.51-0', 'plus');
|
||||||
|
assert(url.includes('router-for-me/CLIProxyAPIPlus/releases'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defaults to plus backend', () => {
|
||||||
|
const url = platformDetector.getDownloadUrl();
|
||||||
|
assert(url.includes('CLIProxyAPIPlus'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getFallbackVersion', () => {
|
||||||
|
it('returns correct version for original backend', () => {
|
||||||
|
const version = platformDetector.getFallbackVersion('original');
|
||||||
|
assert.strictEqual(version, platformDetector.BACKEND_CONFIG.original.fallbackVersion);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns correct version for plus backend', () => {
|
||||||
|
const version = platformDetector.getFallbackVersion('plus');
|
||||||
|
assert.strictEqual(version, platformDetector.BACKEND_CONFIG.plus.fallbackVersion);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PLUS_ONLY_PROVIDERS', () => {
|
||||||
|
it('includes kiro as plus-only provider', () => {
|
||||||
|
assert(types.PLUS_ONLY_PROVIDERS.includes('kiro'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes ghcp as plus-only provider', () => {
|
||||||
|
assert(types.PLUS_ONLY_PROVIDERS.includes('ghcp'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not include gemini as plus-only provider', () => {
|
||||||
|
assert(!types.PLUS_ONLY_PROVIDERS.includes('gemini'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not include agy as plus-only provider', () => {
|
||||||
|
assert(!types.PLUS_ONLY_PROVIDERS.includes('agy'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -495,6 +495,14 @@ export const api = {
|
|||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify(config),
|
body: JSON.stringify(config),
|
||||||
}),
|
}),
|
||||||
|
/** Get backend setting */
|
||||||
|
getBackend: () => request<{ backend: 'original' | 'plus' }>('/cliproxy-server/backend'),
|
||||||
|
/** Update backend setting */
|
||||||
|
updateBackend: (backend: 'original' | 'plus', force = false) =>
|
||||||
|
request<{ backend: 'original' | 'plus' }>('/cliproxy-server/backend', {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify({ backend, force }),
|
||||||
|
}),
|
||||||
/** Test remote proxy connection */
|
/** Test remote proxy connection */
|
||||||
test: (params: {
|
test: (params: {
|
||||||
host: string;
|
host: string;
|
||||||
|
|||||||
@@ -3,19 +3,32 @@
|
|||||||
* Settings section for CLIProxyAPI configuration (local/remote)
|
* Settings section for CLIProxyAPI configuration (local/remote)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { RefreshCw, CheckCircle2, AlertCircle, Laptop, Cloud, Bug } from 'lucide-react';
|
import {
|
||||||
|
RefreshCw,
|
||||||
|
CheckCircle2,
|
||||||
|
AlertCircle,
|
||||||
|
Laptop,
|
||||||
|
Cloud,
|
||||||
|
Bug,
|
||||||
|
Box,
|
||||||
|
AlertTriangle,
|
||||||
|
} from 'lucide-react';
|
||||||
import { useProxyConfig, useRawConfig } from '../../hooks';
|
import { useProxyConfig, useRawConfig } from '../../hooks';
|
||||||
import { LocalProxyCard } from './local-proxy-card';
|
import { LocalProxyCard } from './local-proxy-card';
|
||||||
import { RemoteProxyCard } from './remote-proxy-card';
|
import { RemoteProxyCard } from './remote-proxy-card';
|
||||||
|
import { api } from '@/lib/api-client';
|
||||||
|
|
||||||
/** LocalStorage key for debug mode preference */
|
/** LocalStorage key for debug mode preference */
|
||||||
const DEBUG_MODE_KEY = 'ccs_debug_mode';
|
const DEBUG_MODE_KEY = 'ccs_debug_mode';
|
||||||
|
|
||||||
|
/** Providers only available on CLIProxyAPIPlus */
|
||||||
|
const PLUS_ONLY_PROVIDERS = ['kiro', 'ghcp'];
|
||||||
|
|
||||||
export default function ProxySection() {
|
export default function ProxySection() {
|
||||||
const {
|
const {
|
||||||
config,
|
config,
|
||||||
@@ -60,6 +73,52 @@ export default function ProxySection() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Backend state (loaded from API)
|
||||||
|
const [backend, setBackend] = useState<'original' | 'plus'>('plus');
|
||||||
|
const [backendSaving, setBackendSaving] = useState(false);
|
||||||
|
const [hasKiroGhcpVariants, setHasKiroGhcpVariants] = useState(false);
|
||||||
|
|
||||||
|
// Fetch backend setting
|
||||||
|
const fetchBackend = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const result = await api.cliproxyServer.getBackend();
|
||||||
|
setBackend(result.backend);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Proxy] Failed to fetch backend:', err);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Check for Kiro/ghcp variants
|
||||||
|
const checkPlusOnlyVariants = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const result = await api.cliproxy.list();
|
||||||
|
const hasIncompatible = result.variants.some((v) => PLUS_ONLY_PROVIDERS.includes(v.provider));
|
||||||
|
setHasKiroGhcpVariants(hasIncompatible);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Proxy] Failed to check variants:', err);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Save backend setting
|
||||||
|
const handleBackendChange = async (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')) {
|
||||||
|
console.warn('[Proxy] Backend change blocked: proxy is running');
|
||||||
|
}
|
||||||
|
console.error('[Proxy] Failed to save backend:', err);
|
||||||
|
setBackend(previousValue);
|
||||||
|
} finally {
|
||||||
|
setBackendSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Log when debug mode changes (sanitize sensitive fields)
|
// Log when debug mode changes (sanitize sensitive fields)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (debugMode && config) {
|
if (debugMode && config) {
|
||||||
@@ -80,7 +139,9 @@ export default function ProxySection() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchConfig();
|
fetchConfig();
|
||||||
fetchRawConfig();
|
fetchRawConfig();
|
||||||
}, [fetchConfig, fetchRawConfig]);
|
fetchBackend();
|
||||||
|
checkPlusOnlyVariants();
|
||||||
|
}, [fetchConfig, fetchRawConfig, fetchBackend, checkPlusOnlyVariants]);
|
||||||
|
|
||||||
if (loading || !config) {
|
if (loading || !config) {
|
||||||
return (
|
return (
|
||||||
@@ -242,6 +303,64 @@ export default function ProxySection() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Backend Selection - Card based selection */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h3 className="text-base font-medium flex items-center gap-2">
|
||||||
|
<Box className="w-4 h-4" />
|
||||||
|
Backend Binary
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
{/* Plus Backend Card */}
|
||||||
|
<button
|
||||||
|
onClick={() => handleBackendChange('plus')}
|
||||||
|
disabled={backendSaving}
|
||||||
|
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'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 mb-2">
|
||||||
|
<span className="font-medium">CLIProxyAPIPlus</span>
|
||||||
|
<span className="text-xs px-1.5 py-0.5 rounded bg-green-100 text-green-700 dark:bg-green-900/50 dark:text-green-400">
|
||||||
|
Default
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Full provider support including Kiro and GitHub Copilot
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Original Backend Card */}
|
||||||
|
<button
|
||||||
|
onClick={() => handleBackendChange('original')}
|
||||||
|
disabled={backendSaving}
|
||||||
|
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'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 mb-2">
|
||||||
|
<span className="font-medium">CLIProxyAPI</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Original binary (Gemini, Codex, Antigravity only)
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/* Warning when original backend selected with Kiro/ghcp variants */}
|
||||||
|
{backend === 'original' && hasKiroGhcpVariants && (
|
||||||
|
<Alert variant="destructive" className="py-2">
|
||||||
|
<AlertTriangle className="h-4 w-4" />
|
||||||
|
<AlertDescription>
|
||||||
|
Existing Kiro/Copilot variants will not work with CLIProxyAPI. Switch to
|
||||||
|
CLIProxyAPIPlus or remove those variants.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Remote Settings - Show when remote mode is enabled */}
|
{/* Remote Settings - Show when remote mode is enabled */}
|
||||||
{isRemoteMode && (
|
{isRemoteMode && (
|
||||||
<RemoteProxyCard
|
<RemoteProxyCard
|
||||||
@@ -357,6 +476,8 @@ export default function ProxySection() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
fetchConfig();
|
fetchConfig();
|
||||||
fetchRawConfig();
|
fetchRawConfig();
|
||||||
|
fetchBackend();
|
||||||
|
checkPlusOnlyVariants();
|
||||||
}}
|
}}
|
||||||
disabled={loading || saving}
|
disabled={loading || saving}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
|
|||||||
Reference in New Issue
Block a user