Files
ccs/src/cliproxy/binary-manager.ts
T
Tam Nhu Tran 3af554275e test: isolate shared-state test suites for CI stability
- add dependency injection to export-command, binary-manager,
  codex-plan-compatibility, config-command, and usage-syncer
- override process.exit in api-export test to prevent silent
  termination in Bun's parallel test runner
- harden test-environment bootstrap with process.env isolation
- fix auth middleware to avoid config upgrade during checks
2026-03-25 16:41:41 -04:00

279 lines
9.0 KiB
TypeScript

/**
* Binary Manager for CLIProxyAPI
*
* Facade pattern wrapper for modular binary management.
* Pattern: Mirrors npm install behavior (fast check, download only when needed)
*/
import { info, warn } from '../utils/ui';
import { getBinDir, CLIPROXY_DEFAULT_PORT } from './config-generator';
import { BinaryInfo, BinaryManagerConfig } from './types';
import { BACKEND_CONFIG, DEFAULT_BACKEND, CLIPROXY_MAX_STABLE_VERSION } from './platform-detector';
import { stopProxy } from './services/proxy-lifecycle-service';
import { waitForPortFree } from '../utils/port-utils';
import { loadOrCreateUnifiedConfig } from '../config/unified-config-loader';
import {
UpdateCheckResult,
checkForUpdates,
deleteBinary,
getBinaryPath,
isBinaryInstalled,
getBinaryInfo,
getPinnedVersion,
savePinnedVersion,
clearPinnedVersion,
isVersionPinned,
getVersionPinPath,
readInstalledVersion,
ensureBinary,
migrateVersionPin,
} from './binary';
import type { CLIProxyBackend } from './types';
/**
* Get backend from config or default to 'plus'
*/
function getConfiguredBackend(): CLIProxyBackend {
try {
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,
backend, // Pass backend for installer to use correct download URL
};
}
/**
* Binary Manager class for CLIProxyAPI binary lifecycle
*/
export class BinaryManager {
private config: BinaryManagerConfig;
private backend: CLIProxyBackend;
constructor(config: Partial<BinaryManagerConfig> = {}, backend?: CLIProxyBackend) {
this.backend = backend ?? getConfiguredBackend();
const defaultConfig = createDefaultConfig(this.backend);
this.config = { ...defaultConfig, ...config };
}
/** Ensure binary is available (download if missing, update if outdated) */
async ensureBinary(): Promise<string> {
return ensureBinary(this.config);
}
/** 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,
this.backend
);
}
/** Get full path to binary executable */
getBinaryPath(): string {
return getBinaryPath(this.config.binPath, this.backend);
}
/** Check if binary exists */
isBinaryInstalled(): boolean {
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, this.backend);
}
/** Delete binary (for cleanup or reinstall) */
deleteBinary(): void {
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();
// 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(
{
version: pinnedVersion,
verbose,
forceVersion: true,
},
backend
).ensureBinary();
}
return new BinaryManager({ verbose }, backend).ensureBinary();
}
/** Check if CLIProxyAPI binary is installed */
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(backend?: CLIProxyBackend): string {
const effectiveBackend = backend ?? getConfiguredBackend();
return new BinaryManager({}, effectiveBackend).getBinaryPath();
}
/** Get installed CLIProxyAPI version from .version file */
export function getInstalledCliproxyVersion(backend?: CLIProxyBackend): string {
const effectiveBackend = backend ?? getConfiguredBackend();
return readInstalledVersion(
getBackendBinDir(effectiveBackend),
BACKEND_CONFIG[effectiveBackend].fallbackVersion
);
}
interface InstallCliproxyVersionDeps {
createManager?: (
config: Partial<BinaryManagerConfig>,
backend: CLIProxyBackend
) => Pick<BinaryManager, 'isBinaryInstalled' | 'deleteBinary' | 'ensureBinary'>;
stopProxyFn?: typeof stopProxy;
waitForPortFreeFn?: typeof waitForPortFree;
formatInfo?: typeof info;
formatWarn?: typeof warn;
getInstalledVersion?: typeof getInstalledCliproxyVersion;
}
/** Install a specific version of CLIProxyAPI */
export async function installCliproxyVersion(
version: string,
verbose = false,
backend?: CLIProxyBackend,
deps: InstallCliproxyVersionDeps = {}
): Promise<void> {
const effectiveBackend = backend ?? getConfiguredBackend();
const manager =
deps.createManager?.({ version, verbose, forceVersion: true }, effectiveBackend) ??
new BinaryManager({ version, verbose, forceVersion: true }, effectiveBackend);
const stopProxyFn = deps.stopProxyFn ?? stopProxy;
const waitForPortFreeFn = deps.waitForPortFreeFn ?? waitForPortFree;
const formatInfo = deps.formatInfo ?? info;
const formatWarn = deps.formatWarn ?? warn;
const getInstalledVersion = deps.getInstalledVersion ?? getInstalledCliproxyVersion;
// Always attempt a best-effort stop first so we also catch untracked proxies
// that are running without a session lock.
if (verbose) console.log(formatInfo('Stopping running CLIProxy before update...'));
const result = await stopProxyFn();
if (result.stopped) {
// Wait for port to be fully released
const portFree = await waitForPortFreeFn(CLIPROXY_DEFAULT_PORT, 5000);
if (!portFree && verbose) {
console.log(formatWarn('Port did not free up in time, proceeding anyway...'));
}
} else if (verbose && result.error && result.error !== 'No active CLIProxy session found') {
console.log(formatWarn(`Could not stop proxy: ${result.error}`));
}
if (manager.isBinaryInstalled()) {
const label = effectiveBackend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy';
if (verbose)
console.log(
formatInfo(`Removing existing ${label} v${getInstalledVersion(effectiveBackend)}`)
);
manager.deleteBinary();
}
await manager.ensureBinary();
if (verbose) {
console.log(formatInfo('New version will be active on next CLIProxy command'));
}
}
/** Fetch the latest CLIProxyAPI version from GitHub API */
export async function fetchLatestCliproxyVersion(backend?: CLIProxyBackend): Promise<string> {
const effectiveBackend = backend ?? getConfiguredBackend();
const result = await new BinaryManager({}, effectiveBackend).checkForUpdates();
return result.latestVersion;
}
/** Update check result for API response */
export interface CliproxyUpdateCheckResult {
hasUpdate: boolean;
currentVersion: string;
latestVersion: string;
fromCache: boolean;
checkedAt: number;
// Backend info
backend: CLIProxyBackend;
backendLabel: string;
// Stability fields
isStable: boolean;
maxStableVersion: string;
stabilityMessage?: string;
}
/** Check for CLIProxyAPI binary updates */
export async function checkCliproxyUpdate(
backend?: CLIProxyBackend
): Promise<CliproxyUpdateCheckResult> {
const effectiveBackend = backend ?? getConfiguredBackend();
const result = await new BinaryManager({}, effectiveBackend).checkForUpdates();
// Import isNewerVersion for stability check
const { isNewerVersion } = await import('./binary/version-checker');
const isStable = !isNewerVersion(result.currentVersion, CLIPROXY_MAX_STABLE_VERSION);
const stabilityMessage = isStable
? undefined
: `v${result.currentVersion} has known stability issues. Max stable: v${CLIPROXY_MAX_STABLE_VERSION}`;
const backendLabel = effectiveBackend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy';
return {
...result,
backend: effectiveBackend,
backendLabel,
isStable,
maxStableVersion: CLIPROXY_MAX_STABLE_VERSION,
stabilityMessage,
};
}
// Re-export version pin functions
export {
getVersionPinPath,
getPinnedVersion,
savePinnedVersion,
clearPinnedVersion,
isVersionPinned,
migrateVersionPin,
};
export default BinaryManager;