mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 16:16:52 +00:00
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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user