mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 02:14:32 +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:
1 parent
932a74e199
commit
a7ba1a1983
3 files changed
+140
-12
No files matched your search
@@ -39,6 +39,9 @@ import {
|
|||||||
/** Cache duration for version check (1 hour in milliseconds) */
|
/** Cache duration for version check (1 hour in milliseconds) */
|
||||||
const VERSION_CACHE_DURATION_MS = 60 * 60 * 1000;
|
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 */
|
/** GitHub API URL for latest release */
|
||||||
const GITHUB_API_LATEST_RELEASE =
|
const GITHUB_API_LATEST_RELEASE =
|
||||||
'https://api.github.com/repos/router-for-me/CLIProxyAPI/releases/latest';
|
'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
|
* Convenience function to ensure binary is available
|
||||||
|
* Respects version pin if set by user via 'ccs cliproxy --install <version>'
|
||||||
* @returns Path to CLIProxyAPI executable
|
* @returns Path to CLIProxyAPI executable
|
||||||
*/
|
*/
|
||||||
export async function ensureCLIProxyBinary(verbose = false): Promise<string> {
|
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 });
|
const manager = new BinaryManager({ verbose });
|
||||||
return manager.ensureBinary();
|
return manager.ensureBinary();
|
||||||
}
|
}
|
||||||
@@ -960,4 +980,64 @@ export async function fetchLatestCliproxyVersion(): Promise<string> {
|
|||||||
return result.latestVersion;
|
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;
|
export default BinaryManager;
|
||||||
@@ -43,6 +43,11 @@ export {
|
|||||||
getInstalledCliproxyVersion,
|
getInstalledCliproxyVersion,
|
||||||
installCliproxyVersion,
|
installCliproxyVersion,
|
||||||
fetchLatestCliproxyVersion,
|
fetchLatestCliproxyVersion,
|
||||||
|
getPinnedVersion,
|
||||||
|
savePinnedVersion,
|
||||||
|
clearPinnedVersion,
|
||||||
|
isVersionPinned,
|
||||||
|
getVersionPinPath,
|
||||||
} from './binary-manager';
|
} from './binary-manager';
|
||||||
|
|
||||||
// Config generation
|
// Config generation
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ import {
|
|||||||
fetchLatestCliproxyVersion,
|
fetchLatestCliproxyVersion,
|
||||||
isCLIProxyInstalled,
|
isCLIProxyInstalled,
|
||||||
getCLIProxyPath,
|
getCLIProxyPath,
|
||||||
|
getPinnedVersion,
|
||||||
|
savePinnedVersion,
|
||||||
|
clearPinnedVersion,
|
||||||
|
isVersionPinned,
|
||||||
} from '../cliproxy';
|
} from '../cliproxy';
|
||||||
import { getAllAuthStatus, getOAuthConfig, triggerOAuth } from '../cliproxy/auth-handler';
|
import { getAllAuthStatus, getOAuthConfig, triggerOAuth } from '../cliproxy/auth-handler';
|
||||||
import { getProviderAccounts } from '../cliproxy/account-manager';
|
import { getProviderAccounts } from '../cliproxy/account-manager';
|
||||||
@@ -803,8 +807,9 @@ async function showHelp(): Promise<void> {
|
|||||||
// Binary Commands
|
// Binary Commands
|
||||||
console.log(subheader('Binary Commands:'));
|
console.log(subheader('Binary Commands:'));
|
||||||
const binaryCmds: [string, string][] = [
|
const binaryCmds: [string, string][] = [
|
||||||
['--install <version>', 'Install a specific binary version'],
|
['--install <version>', 'Install and pin a specific version'],
|
||||||
['--latest', 'Install the latest binary version'],
|
['--latest', 'Install the latest version (no pin)'],
|
||||||
|
['--update', 'Unpin and update to latest version'],
|
||||||
];
|
];
|
||||||
const maxBinaryLen = Math.max(...binaryCmds.map(([cmd]) => cmd.length));
|
const maxBinaryLen = Math.max(...binaryCmds.map(([cmd]) => cmd.length));
|
||||||
for (const [cmd, desc] of binaryCmds) {
|
for (const [cmd, desc] of binaryCmds) {
|
||||||
@@ -897,6 +902,7 @@ async function showStatus(verbose: boolean): Promise<void> {
|
|||||||
const installed = isCLIProxyInstalled();
|
const installed = isCLIProxyInstalled();
|
||||||
const currentVersion = getInstalledCliproxyVersion();
|
const currentVersion = getInstalledCliproxyVersion();
|
||||||
const binaryPath = getCLIProxyPath();
|
const binaryPath = getCLIProxyPath();
|
||||||
|
const pinnedVersion = getPinnedVersion();
|
||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log(color('CLIProxyAPI Status', 'primary'));
|
console.log(color('CLIProxyAPI Status', 'primary'));
|
||||||
@@ -904,7 +910,10 @@ async function showStatus(verbose: boolean): Promise<void> {
|
|||||||
|
|
||||||
if (installed) {
|
if (installed) {
|
||||||
console.log(` Installed: ${color('Yes', 'success')}`);
|
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)}`);
|
console.log(` Binary: ${dim(binaryPath)}`);
|
||||||
} else {
|
} else {
|
||||||
console.log(` Installed: ${color('No', 'error')}`);
|
console.log(` Installed: ${color('No', 'error')}`);
|
||||||
@@ -919,11 +928,19 @@ async function showStatus(verbose: boolean): Promise<void> {
|
|||||||
const latestVersion = await fetchLatestCliproxyVersion();
|
const latestVersion = await fetchLatestCliproxyVersion();
|
||||||
|
|
||||||
if (latestVersion !== currentVersion) {
|
if (latestVersion !== currentVersion) {
|
||||||
console.log(
|
if (pinnedVersion) {
|
||||||
` Latest: ${color(`v${latestVersion}`, 'success')} ${dim('(update available)')}`
|
console.log(
|
||||||
);
|
` Latest: ${color(`v${latestVersion}`, 'success')} ${dim('(pinned to v' + pinnedVersion + ')')}`
|
||||||
console.log('');
|
);
|
||||||
console.log(` ${dim(`Run "ccs cliproxy --latest" to update`)}`);
|
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 {
|
} else {
|
||||||
console.log(` Latest: ${color(`v${latestVersion}`, 'success')} ${dim('(up to date)')}`);
|
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> {
|
async function installVersion(version: string, verbose: boolean): Promise<void> {
|
||||||
// Validate version format (basic semver check)
|
// Validate version format (basic semver check)
|
||||||
@@ -954,8 +971,18 @@ async function installVersion(version: string, verbose: boolean): Promise<void>
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await installCliproxyVersion(version, verbose);
|
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('');
|
||||||
console.log(ok(`CLIProxyAPI v${version} installed successfully`));
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const err = error as Error;
|
const err = error as Error;
|
||||||
console.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> {
|
async function installLatest(verbose: boolean): Promise<void> {
|
||||||
console.log(info('Fetching latest CLIProxyAPI version...'));
|
console.log(info('Fetching latest CLIProxyAPI version...'));
|
||||||
@@ -982,8 +1009,9 @@ async function installLatest(verbose: boolean): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
const latestVersion = await fetchLatestCliproxyVersion();
|
const latestVersion = await fetchLatestCliproxyVersion();
|
||||||
const currentVersion = getInstalledCliproxyVersion();
|
const currentVersion = getInstalledCliproxyVersion();
|
||||||
|
const wasPinned = isVersionPinned();
|
||||||
|
|
||||||
if (isCLIProxyInstalled() && latestVersion === currentVersion) {
|
if (isCLIProxyInstalled() && latestVersion === currentVersion && !wasPinned) {
|
||||||
console.log(ok(`Already running latest version: v${latestVersion}`));
|
console.log(ok(`Already running latest version: v${latestVersion}`));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -992,11 +1020,20 @@ async function installLatest(verbose: boolean): Promise<void> {
|
|||||||
if (isCLIProxyInstalled()) {
|
if (isCLIProxyInstalled()) {
|
||||||
console.log(info(`Current version: v${currentVersion}`));
|
console.log(info(`Current version: v${currentVersion}`));
|
||||||
}
|
}
|
||||||
|
if (wasPinned) {
|
||||||
|
console.log(info(`Removing version pin (was v${getPinnedVersion()})`));
|
||||||
|
}
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
await installCliproxyVersion(latestVersion, verbose);
|
await installCliproxyVersion(latestVersion, verbose);
|
||||||
|
|
||||||
|
// Clear any version pin so auto-update works again
|
||||||
|
clearPinnedVersion();
|
||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log(ok(`CLIProxyAPI updated to v${latestVersion}`));
|
console.log(ok(`CLIProxyAPI updated to v${latestVersion}`));
|
||||||
|
console.log(dim('Auto-update is now enabled.'));
|
||||||
|
console.log('');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const err = error as Error;
|
const err = error as Error;
|
||||||
console.error(fail(`Failed to install latest version: ${err.message}`));
|
console.error(fail(`Failed to install latest version: ${err.message}`));
|
||||||
@@ -1057,6 +1094,12 @@ export async function handleCliproxyCommand(args: string[]): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle --update (unpin and update to latest)
|
||||||
|
if (args.includes('--update')) {
|
||||||
|
await installLatest(verbose);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Default: show status
|
// Default: show status
|
||||||
await showStatus(verbose);
|
await showStatus(verbose);
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user