mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 10:16:49 +00:00
fix(cliproxy): respect version pin when user installs specific version
When a user explicitly installs a specific CLIProxy version via `ccs cliproxy --install <version>`, this version should persist and not be auto-updated. Previously, subsequent commands would check for updates and overwrite the user's choice. Changes: - Add .version-pin file to persist user's explicit version choice - Update ensureCLIProxyBinary() to check for version pin and skip auto-update - Update --install command to save version pin with "(pinned)" confirmation - Update --latest command to clear version pin and enable auto-update - Add --update command as alias for --latest (for unpinning workflow) - Update status display to show "(pinned)" indicator when version is pinned - Update help text to document new pin/unpin behavior Closes #88
This commit is contained in:
@@ -39,6 +39,9 @@ import {
|
||||
/** Cache duration for version check (1 hour in milliseconds) */
|
||||
const VERSION_CACHE_DURATION_MS = 60 * 60 * 1000;
|
||||
|
||||
/** Version pin file name - stores user's explicit version choice */
|
||||
const VERSION_PIN_FILE = '.version-pin';
|
||||
|
||||
/** GitHub API URL for latest release */
|
||||
const GITHUB_API_LATEST_RELEASE =
|
||||
'https://api.github.com/repos/router-for-me/CLIProxyAPI/releases/latest';
|
||||
@@ -887,9 +890,26 @@ export class BinaryManager {
|
||||
|
||||
/**
|
||||
* Convenience function to ensure binary is available
|
||||
* Respects version pin if set by user via 'ccs cliproxy --install <version>'
|
||||
* @returns Path to CLIProxyAPI executable
|
||||
*/
|
||||
export async function ensureCLIProxyBinary(verbose = false): Promise<string> {
|
||||
const pinnedVersion = getPinnedVersion();
|
||||
|
||||
if (pinnedVersion) {
|
||||
// Version is pinned - use forceVersion to prevent auto-update
|
||||
if (verbose) {
|
||||
console.error(`[cliproxy] Using pinned version: ${pinnedVersion}`);
|
||||
}
|
||||
const manager = new BinaryManager({
|
||||
version: pinnedVersion,
|
||||
verbose,
|
||||
forceVersion: true,
|
||||
});
|
||||
return manager.ensureBinary();
|
||||
}
|
||||
|
||||
// No pin - allow auto-update to latest
|
||||
const manager = new BinaryManager({ verbose });
|
||||
return manager.ensureBinary();
|
||||
}
|
||||
@@ -960,4 +980,64 @@ export async function fetchLatestCliproxyVersion(): Promise<string> {
|
||||
return result.latestVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get path to version pin file
|
||||
* @returns Absolute path to .version-pin file
|
||||
*/
|
||||
export function getVersionPinPath(): string {
|
||||
return path.join(getBinDir(), VERSION_PIN_FILE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pinned version if one exists
|
||||
* @returns Pinned version string, or null if not pinned
|
||||
*/
|
||||
export function getPinnedVersion(): string | null {
|
||||
const pinPath = getVersionPinPath();
|
||||
if (!fs.existsSync(pinPath)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return fs.readFileSync(pinPath, 'utf8').trim();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save pinned version to persist user's explicit choice
|
||||
* @param version Version to pin (e.g., "6.5.50")
|
||||
*/
|
||||
export function savePinnedVersion(version: string): void {
|
||||
const pinPath = getVersionPinPath();
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(pinPath), { recursive: true });
|
||||
fs.writeFileSync(pinPath, version, 'utf8');
|
||||
} catch {
|
||||
// Silent fail - not critical but log if verbose
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear pinned version (unpin)
|
||||
*/
|
||||
export function clearPinnedVersion(): void {
|
||||
const pinPath = getVersionPinPath();
|
||||
if (fs.existsSync(pinPath)) {
|
||||
try {
|
||||
fs.unlinkSync(pinPath);
|
||||
} catch {
|
||||
// Silent fail
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a version is currently pinned
|
||||
* @returns true if a version is pinned
|
||||
*/
|
||||
export function isVersionPinned(): boolean {
|
||||
return getPinnedVersion() !== null;
|
||||
}
|
||||
|
||||
export default BinaryManager;
|
||||
|
||||
@@ -43,6 +43,11 @@ export {
|
||||
getInstalledCliproxyVersion,
|
||||
installCliproxyVersion,
|
||||
fetchLatestCliproxyVersion,
|
||||
getPinnedVersion,
|
||||
savePinnedVersion,
|
||||
clearPinnedVersion,
|
||||
isVersionPinned,
|
||||
getVersionPinPath,
|
||||
} from './binary-manager';
|
||||
|
||||
// Config generation
|
||||
|
||||
@@ -26,6 +26,10 @@ import {
|
||||
fetchLatestCliproxyVersion,
|
||||
isCLIProxyInstalled,
|
||||
getCLIProxyPath,
|
||||
getPinnedVersion,
|
||||
savePinnedVersion,
|
||||
clearPinnedVersion,
|
||||
isVersionPinned,
|
||||
} from '../cliproxy';
|
||||
import { getAllAuthStatus, getOAuthConfig, triggerOAuth } from '../cliproxy/auth-handler';
|
||||
import { getProviderAccounts } from '../cliproxy/account-manager';
|
||||
@@ -803,8 +807,9 @@ async function showHelp(): Promise<void> {
|
||||
// Binary Commands
|
||||
console.log(subheader('Binary Commands:'));
|
||||
const binaryCmds: [string, string][] = [
|
||||
['--install <version>', 'Install a specific binary version'],
|
||||
['--latest', 'Install the latest binary version'],
|
||||
['--install <version>', 'Install and pin a specific version'],
|
||||
['--latest', 'Install the latest version (no pin)'],
|
||||
['--update', 'Unpin and update to latest version'],
|
||||
];
|
||||
const maxBinaryLen = Math.max(...binaryCmds.map(([cmd]) => cmd.length));
|
||||
for (const [cmd, desc] of binaryCmds) {
|
||||
@@ -897,6 +902,7 @@ async function showStatus(verbose: boolean): Promise<void> {
|
||||
const installed = isCLIProxyInstalled();
|
||||
const currentVersion = getInstalledCliproxyVersion();
|
||||
const binaryPath = getCLIProxyPath();
|
||||
const pinnedVersion = getPinnedVersion();
|
||||
|
||||
console.log('');
|
||||
console.log(color('CLIProxyAPI Status', 'primary'));
|
||||
@@ -904,7 +910,10 @@ async function showStatus(verbose: boolean): Promise<void> {
|
||||
|
||||
if (installed) {
|
||||
console.log(` Installed: ${color('Yes', 'success')}`);
|
||||
console.log(` Version: ${color(`v${currentVersion}`, 'info')}`);
|
||||
const versionLabel = pinnedVersion
|
||||
? `${color(`v${currentVersion}`, 'info')} ${color('(pinned)', 'warning')}`
|
||||
: color(`v${currentVersion}`, 'info');
|
||||
console.log(` Version: ${versionLabel}`);
|
||||
console.log(` Binary: ${dim(binaryPath)}`);
|
||||
} else {
|
||||
console.log(` Installed: ${color('No', 'error')}`);
|
||||
@@ -919,11 +928,19 @@ async function showStatus(verbose: boolean): Promise<void> {
|
||||
const latestVersion = await fetchLatestCliproxyVersion();
|
||||
|
||||
if (latestVersion !== currentVersion) {
|
||||
console.log(
|
||||
` Latest: ${color(`v${latestVersion}`, 'success')} ${dim('(update available)')}`
|
||||
);
|
||||
console.log('');
|
||||
console.log(` ${dim(`Run "ccs cliproxy --latest" to update`)}`);
|
||||
if (pinnedVersion) {
|
||||
console.log(
|
||||
` Latest: ${color(`v${latestVersion}`, 'success')} ${dim('(pinned to v' + pinnedVersion + ')')}`
|
||||
);
|
||||
console.log('');
|
||||
console.log(` ${dim('Run "ccs cliproxy --update" to unpin and update')}`);
|
||||
} else {
|
||||
console.log(
|
||||
` Latest: ${color(`v${latestVersion}`, 'success')} ${dim('(update available)')}`
|
||||
);
|
||||
console.log('');
|
||||
console.log(` ${dim('Run "ccs cliproxy --latest" to update')}`);
|
||||
}
|
||||
} else {
|
||||
console.log(` Latest: ${color(`v${latestVersion}`, 'success')} ${dim('(up to date)')}`);
|
||||
}
|
||||
@@ -940,7 +957,7 @@ async function showStatus(verbose: boolean): Promise<void> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a specific version
|
||||
* Install a specific version (pins the version to prevent auto-update)
|
||||
*/
|
||||
async function installVersion(version: string, verbose: boolean): Promise<void> {
|
||||
// Validate version format (basic semver check)
|
||||
@@ -954,8 +971,18 @@ async function installVersion(version: string, verbose: boolean): Promise<void>
|
||||
|
||||
try {
|
||||
await installCliproxyVersion(version, verbose);
|
||||
|
||||
// Pin the version to prevent auto-update
|
||||
savePinnedVersion(version);
|
||||
|
||||
console.log('');
|
||||
console.log(ok(`CLIProxyAPI v${version} installed (pinned)`));
|
||||
console.log('');
|
||||
console.log(dim('This version will be used until you run:'));
|
||||
console.log(
|
||||
` ${color('ccs cliproxy --update', 'command')} ${dim('# Update to latest and unpin')}`
|
||||
);
|
||||
console.log('');
|
||||
console.log(ok(`CLIProxyAPI v${version} installed successfully`));
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
console.error('');
|
||||
@@ -974,7 +1001,7 @@ async function installVersion(version: string, verbose: boolean): Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* Install latest version
|
||||
* Install latest version (clears any version pin)
|
||||
*/
|
||||
async function installLatest(verbose: boolean): Promise<void> {
|
||||
console.log(info('Fetching latest CLIProxyAPI version...'));
|
||||
@@ -982,8 +1009,9 @@ async function installLatest(verbose: boolean): Promise<void> {
|
||||
try {
|
||||
const latestVersion = await fetchLatestCliproxyVersion();
|
||||
const currentVersion = getInstalledCliproxyVersion();
|
||||
const wasPinned = isVersionPinned();
|
||||
|
||||
if (isCLIProxyInstalled() && latestVersion === currentVersion) {
|
||||
if (isCLIProxyInstalled() && latestVersion === currentVersion && !wasPinned) {
|
||||
console.log(ok(`Already running latest version: v${latestVersion}`));
|
||||
return;
|
||||
}
|
||||
@@ -992,11 +1020,20 @@ async function installLatest(verbose: boolean): Promise<void> {
|
||||
if (isCLIProxyInstalled()) {
|
||||
console.log(info(`Current version: v${currentVersion}`));
|
||||
}
|
||||
if (wasPinned) {
|
||||
console.log(info(`Removing version pin (was v${getPinnedVersion()})`));
|
||||
}
|
||||
console.log('');
|
||||
|
||||
await installCliproxyVersion(latestVersion, verbose);
|
||||
|
||||
// Clear any version pin so auto-update works again
|
||||
clearPinnedVersion();
|
||||
|
||||
console.log('');
|
||||
console.log(ok(`CLIProxyAPI updated to v${latestVersion}`));
|
||||
console.log(dim('Auto-update is now enabled.'));
|
||||
console.log('');
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
console.error(fail(`Failed to install latest version: ${err.message}`));
|
||||
@@ -1057,6 +1094,12 @@ export async function handleCliproxyCommand(args: string[]): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle --update (unpin and update to latest)
|
||||
if (args.includes('--update')) {
|
||||
await installLatest(verbose);
|
||||
return;
|
||||
}
|
||||
|
||||
// Default: show status
|
||||
await showStatus(verbose);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user