mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-04 18:13:17 +00:00
183 lines
6.2 KiB
TypeScript
183 lines
6.2 KiB
TypeScript
/**
|
|
* Binary Lifecycle Manager
|
|
* Handles ensuring binary availability and auto-updates.
|
|
*/
|
|
|
|
import * as fs from 'fs';
|
|
import { BinaryManagerConfig, CLIProxyBackend } from '../types';
|
|
import {
|
|
checkForUpdates,
|
|
fetchLatestVersion,
|
|
isNewerVersion,
|
|
isVersionFaulty,
|
|
} from './version-checker';
|
|
import { downloadAndInstall, getBinaryPath } from './installer';
|
|
import { info, warn } from '../../utils/ui';
|
|
import { isCliproxyRunning } from '../services/stats-fetcher';
|
|
import { resolveLifecyclePort } from '../config/port-manager';
|
|
import { BinaryError } from '../../errors/error-types';
|
|
import {
|
|
CLIPROXY_MAX_STABLE_VERSION,
|
|
CLIPROXY_FAULTY_RANGE,
|
|
DEFAULT_BACKEND,
|
|
} from '../binary/platform-detector';
|
|
|
|
/** Log helper */
|
|
function log(message: string, verbose: boolean): void {
|
|
if (verbose) console.error(`[cliproxy] ${message}`);
|
|
}
|
|
|
|
function getBackendLabel(backend: CLIProxyBackend): string {
|
|
return backend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy';
|
|
}
|
|
|
|
/**
|
|
* Check if version is above max stable (known unstable)
|
|
*/
|
|
function isAboveMaxStable(version: string): boolean {
|
|
return isNewerVersion(version, CLIPROXY_MAX_STABLE_VERSION);
|
|
}
|
|
|
|
/**
|
|
* Clamp version to max stable if newer versions are unstable
|
|
* Returns max stable version if input is empty/invalid
|
|
*/
|
|
function clampToMaxStable(version: string | undefined, verbose: boolean): string {
|
|
if (!version) {
|
|
log(`Empty version, using max stable ${CLIPROXY_MAX_STABLE_VERSION}`, verbose);
|
|
return CLIPROXY_MAX_STABLE_VERSION;
|
|
}
|
|
if (isAboveMaxStable(version)) {
|
|
log(`Clamping ${version} to max stable ${CLIPROXY_MAX_STABLE_VERSION}`, verbose);
|
|
return CLIPROXY_MAX_STABLE_VERSION;
|
|
}
|
|
return version;
|
|
}
|
|
|
|
/** Handle auto-update when binary exists */
|
|
async function handleAutoUpdate(config: BinaryManagerConfig, verbose: boolean): Promise<void> {
|
|
const backend: CLIProxyBackend = config.backend ?? DEFAULT_BACKEND;
|
|
const backendLabel = getBackendLabel(backend);
|
|
const checkFn = config.checkForUpdatesFn ?? checkForUpdates;
|
|
const updateResult = await checkFn(config.binPath, config.version, verbose, backend);
|
|
const currentVersion = updateResult.currentVersion;
|
|
const latestVersion = updateResult.latestVersion;
|
|
|
|
// Check if user is on known faulty version - recommend upgrade
|
|
if (isVersionFaulty(currentVersion)) {
|
|
console.log(
|
|
warn(
|
|
`${backendLabel} v${currentVersion} has known bugs (v${CLIPROXY_FAULTY_RANGE.min.replace(/-\d+$/, '')}-${CLIPROXY_FAULTY_RANGE.max.replace(/-\d+$/, '')}). ` +
|
|
`Upgrade to latest stable recommended.`
|
|
)
|
|
);
|
|
console.log(info(`Run "ccs cliproxy install" to upgrade to latest stable`));
|
|
}
|
|
|
|
if (!updateResult.hasUpdate) return;
|
|
|
|
// Clamp to max stable version
|
|
const targetVersion = clampToMaxStable(latestVersion, verbose);
|
|
if (!isNewerVersion(targetVersion, currentVersion)) {
|
|
log(`Already at max stable version ${currentVersion}`, verbose);
|
|
return;
|
|
}
|
|
|
|
const proxyRunning = await isCliproxyRunning(resolveLifecyclePort());
|
|
const latestNote = isAboveMaxStable(latestVersion) ? ` (latest v${latestVersion} unstable)` : '';
|
|
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: ${backendLabel} is currently running`, verbose);
|
|
} else {
|
|
console.log(info(updateMsg));
|
|
console.log(info(`Updating ${backendLabel}...`));
|
|
// The installer stages and validates the replacement before atomically swapping it.
|
|
config.version = targetVersion;
|
|
await downloadAndInstall(config, verbose);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ensure binary is available (download if missing, update if outdated)
|
|
* @returns Path to executable binary
|
|
*/
|
|
interface EnsureBinaryDeps {
|
|
downloadAndInstallFn?: typeof downloadAndInstall;
|
|
}
|
|
|
|
export async function ensureBinary(
|
|
config: BinaryManagerConfig,
|
|
deps: EnsureBinaryDeps = {}
|
|
): Promise<string> {
|
|
const verbose = config.verbose;
|
|
const backend: CLIProxyBackend = config.backend ?? DEFAULT_BACKEND;
|
|
const binaryPath = getBinaryPath(config.binPath, backend);
|
|
const downloadAndInstallFn = deps.downloadAndInstallFn ?? downloadAndInstall;
|
|
|
|
// Binary exists - check for updates unless forceVersion
|
|
if (fs.existsSync(binaryPath)) {
|
|
log(`Binary exists: ${binaryPath}`, verbose);
|
|
|
|
if (config.replaceExisting) {
|
|
log(`Force version mode: installing specified version ${config.version}`, verbose);
|
|
await downloadAndInstallFn(config, verbose);
|
|
return binaryPath;
|
|
}
|
|
|
|
if (config.forceVersion) {
|
|
log(`Pinned version mode: using installed version ${config.version}`, verbose);
|
|
return binaryPath;
|
|
}
|
|
|
|
if (config.skipAutoUpdate) {
|
|
log('Runtime bootstrap mode: skipping auto-update check', verbose);
|
|
return binaryPath;
|
|
}
|
|
|
|
try {
|
|
await handleAutoUpdate(config, verbose);
|
|
} catch (error) {
|
|
const err = error as Error;
|
|
log(`Update check failed (non-blocking): ${err.message}`, verbose);
|
|
}
|
|
|
|
return binaryPath;
|
|
}
|
|
|
|
// Binary missing
|
|
if (!config.allowInstall) {
|
|
throw new BinaryError(
|
|
`${getBackendLabel(backend)} binary is not installed locally. ` +
|
|
'Run "ccs cliproxy install" when you have network access.',
|
|
binaryPath
|
|
);
|
|
}
|
|
|
|
// Only log download intent when a download will actually happen
|
|
log('Binary not found, downloading...', verbose);
|
|
|
|
if (!config.forceVersion) {
|
|
try {
|
|
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);
|
|
config.version = targetVersion;
|
|
}
|
|
} catch {
|
|
// API failed - use fallback but still clamp to max stable
|
|
const fallbackVersion = clampToMaxStable(config.version, verbose);
|
|
config.version = fallbackVersion;
|
|
log(`Using fallback version: ${fallbackVersion}`, verbose);
|
|
}
|
|
} else {
|
|
log(`Force version mode: using specified version ${config.version}`, verbose);
|
|
}
|
|
|
|
await downloadAndInstallFn(config, verbose);
|
|
return binaryPath;
|
|
}
|