feat(cliproxy): add version management UI with install/restart controls

Implements comprehensive version management for CLIProxyAPI Plus:

Backend APIs:
- GET /versions: fetch available versions from GitHub releases
- POST /install: install specific version with force flag for unstable
- POST /restart: restart proxy without version change

Frontend:
- 4-button layout: Restart, Update/Downgrade, Stop, Settings gear
- Collapsible version picker with dropdown + manual input
- Confirmation dialog for unstable versions (>6.6.80)
- Progressive disclosure (version picker hidden by default)

Ref: plans/260105-1250-cliproxy-version-management/
This commit is contained in:
kaitranntt
2026-01-05 13:12:46 -05:00
parent 8a56a43989
commit a69b2e9d10
7 changed files with 569 additions and 15 deletions
+17
View File
@@ -27,3 +27,20 @@ export const VERSION_PIN_FILE = '.version-pin';
/** GitHub API URL for latest release (CLIProxyAPIPlus fork with Kiro + Copilot support) */
export const GITHUB_API_LATEST_RELEASE =
'https://api.github.com/repos/router-for-me/CLIProxyAPIPlus/releases/latest';
/** GitHub API URL for all releases */
export const GITHUB_API_ALL_RELEASES =
'https://api.github.com/repos/router-for-me/CLIProxyAPIPlus/releases';
/** Version list cache structure */
export interface VersionListCache {
versions: string[];
latestStable: string;
latest: string;
checkedAt: number;
}
/** Version list result from API */
export interface VersionListResult extends VersionListCache {
fromCache: boolean;
}
+55 -1
View File
@@ -6,7 +6,12 @@
import * as fs from 'fs';
import * as path from 'path';
import { getCliproxyDir, getBinDir } from '../config-generator';
import { VersionCache, VERSION_CACHE_DURATION_MS, VERSION_PIN_FILE } from './types';
import {
VersionCache,
VERSION_CACHE_DURATION_MS,
VERSION_PIN_FILE,
VersionListCache,
} from './types';
/**
* Get path to version cache file
@@ -140,3 +145,52 @@ export function clearPinnedVersion(): void {
export function isVersionPinned(): boolean {
return getPinnedVersion() !== null;
}
// ==================== Version List Cache ====================
const VERSION_LIST_CACHE_FILE = '.version-list-cache.json';
/**
* Get path to version list cache file
*/
export function getVersionListCachePath(): string {
return path.join(getCliproxyDir(), VERSION_LIST_CACHE_FILE);
}
/**
* Read version list cache if still valid
*/
export function readVersionListCache(): VersionListCache | null {
const cachePath = getVersionListCachePath();
if (!fs.existsSync(cachePath)) {
return null;
}
try {
const content = fs.readFileSync(cachePath, 'utf8');
const cache: VersionListCache = JSON.parse(content);
// Check if cache is still valid (1 hour)
if (Date.now() - cache.checkedAt < VERSION_CACHE_DURATION_MS) {
return cache;
}
return null;
} catch {
return null;
}
}
/**
* Write version list to cache
*/
export function writeVersionListCache(cache: VersionListCache): void {
const cachePath = getVersionListCachePath();
try {
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
fs.writeFileSync(cachePath, JSON.stringify(cache), 'utf8');
} catch {
// Silent fail - caching is optional
}
}
+56 -2
View File
@@ -4,8 +4,20 @@
*/
import { fetchJson } from './downloader';
import { readVersionCache, writeVersionCache, readInstalledVersion } from './version-cache';
import { UpdateCheckResult, GITHUB_API_LATEST_RELEASE } from './types';
import {
readVersionCache,
writeVersionCache,
readInstalledVersion,
readVersionListCache,
writeVersionListCache,
} from './version-cache';
import {
UpdateCheckResult,
GITHUB_API_LATEST_RELEASE,
GITHUB_API_ALL_RELEASES,
VersionListResult,
} from './types';
import { CLIPROXY_MAX_STABLE_VERSION } from '../platform-detector';
/**
* Compare semver versions (true if latest > current)
@@ -85,3 +97,45 @@ export async function checkForUpdates(
checkedAt: now,
};
}
/**
* Fetch all available versions from GitHub releases
* Caches result for 1 hour to avoid rate limiting
*/
export async function fetchAllVersions(verbose = false): Promise<VersionListResult> {
// Try cache first
const cache = readVersionListCache();
if (cache) {
if (verbose) {
console.error(`[cliproxy] Using cached version list (${cache.versions.length} versions)`);
}
return { ...cache, fromCache: true };
}
// Fetch from GitHub API
const response = await fetchJson(GITHUB_API_ALL_RELEASES, verbose);
// Extract and normalize versions
const releases = response as unknown as Array<{ tag_name: string }>;
const versions = releases
.map((r) => r.tag_name.replace(/^v/, ''))
.filter((v) => /^\d+\.\d+\.\d+(-\d+)?$/.test(v)); // Valid semver only
const latest = versions[0] || '';
// Find latest stable (not newer than max stable)
const latestStable =
versions.find((v) => !isNewerVersion(v, CLIPROXY_MAX_STABLE_VERSION)) ||
CLIPROXY_MAX_STABLE_VERSION;
const result: VersionListResult = {
versions,
latestStable,
latest,
fromCache: false,
checkedAt: Date.now(),
};
writeVersionListCache(result);
return result;
}