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:
kaitranntt
2025-12-12 05:36:49 -05:00
parent 932a74e199
commit a7ba1a1983
3 changed files with 140 additions and 12 deletions
+80
View File
@@ -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;
+5
View File
@@ -43,6 +43,11 @@ export {
getInstalledCliproxyVersion,
installCliproxyVersion,
fetchLatestCliproxyVersion,
getPinnedVersion,
savePinnedVersion,
clearPinnedVersion,
isVersionPinned,
getVersionPinPath,
} from './binary-manager';
// Config generation
+55 -12
View File
@@ -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);
}