From 2adc272f278b1d80d160ad4d6e1f35e3b61cb156 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Thu, 18 Dec 2025 22:52:12 -0500 Subject: [PATCH 1/4] fix(cliproxy): prevent misleading update message when proxy is running When CLIProxyAPI is already running as a background process, the update cannot be applied because the old process is still in memory. Previously, the CLI would download the new binary and show "Updating CLIProxyAPI..." even though the running process remained on the old version. Now checks if proxy is running before attempting update: - If running: shows update available message with instruction to stop first - If not running: proceeds with update as before Fixes #143 --- src/cliproxy/binary-manager.ts | 37 ++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/src/cliproxy/binary-manager.ts b/src/cliproxy/binary-manager.ts index 8137c108..db9e38bf 100644 --- a/src/cliproxy/binary-manager.ts +++ b/src/cliproxy/binary-manager.ts @@ -19,7 +19,8 @@ import * as crypto from 'crypto'; import * as zlib from 'zlib'; import { ProgressIndicator } from '../utils/progress-indicator'; import { ok, info } from '../utils/ui'; -import { getBinDir, getCliproxyDir } from './config-generator'; +import { getBinDir, getCliproxyDir, CLIPROXY_DEFAULT_PORT } from './config-generator'; +import { isCliproxyRunning } from './stats-fetcher'; import { BinaryInfo, BinaryManagerConfig, @@ -103,17 +104,31 @@ export class BinaryManager { try { const updateResult = await this.checkForUpdates(); if (updateResult.hasUpdate) { - console.log( - info( - `CLIProxyAPI update available: v${updateResult.currentVersion} -> v${updateResult.latestVersion}` - ) - ); - console.log(info('Updating CLIProxyAPI...')); + // Check if CLIProxyAPI is currently running - can't update while running + const proxyRunning = await isCliproxyRunning(CLIPROXY_DEFAULT_PORT); + if (proxyRunning) { + // Proxy is running - can't update, just notify user + console.log( + info( + `CLIProxyAPI update available: v${updateResult.currentVersion} -> v${updateResult.latestVersion}` + ) + ); + console.log(info('Run "ccs cliproxy stop" then restart to apply update')); + this.log('Skipping update: CLIProxyAPI is currently running'); + } else { + // Proxy not running - safe to update + console.log( + info( + `CLIProxyAPI update available: v${updateResult.currentVersion} -> v${updateResult.latestVersion}` + ) + ); + console.log(info('Updating CLIProxyAPI...')); - // Delete old binary and download new version - this.deleteBinary(); - this.config.version = updateResult.latestVersion; - await this.downloadAndInstall(); + // Delete old binary and download new version + this.deleteBinary(); + this.config.version = updateResult.latestVersion; + await this.downloadAndInstall(); + } } } catch (error) { // Silent fail - don't block startup if update check fails From c9ad0b077934ae8418d4e97b9b02a09044ff898b Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Thu, 18 Dec 2025 23:03:45 -0500 Subject: [PATCH 2/4] feat(ui): add Stop and Restart buttons to ProxyStatusWidget - Add POST /api/cliproxy/proxy-stop API endpoint - Add proxyStop method in api-client and useStopProxy hook - Update ProxyStatusWidget with Stop and Restart controls when running - Restart = stop + delay + start (for applying CLIProxyAPI updates) Complements the fix in binary-manager.ts which now tells users to stop proxy before updates can be applied. --- src/web-server/routes.ts | 15 ++++- ui/src/components/proxy-status-widget.tsx | 81 ++++++++++++++++++----- ui/src/hooks/use-cliproxy.ts | 21 ++++++ ui/src/lib/api-client.ts | 9 +++ 4 files changed, 107 insertions(+), 19 deletions(-) diff --git a/src/web-server/routes.ts b/src/web-server/routes.ts index 9e1cd207..4f2797f7 100644 --- a/src/web-server/routes.ts +++ b/src/web-server/routes.ts @@ -44,7 +44,7 @@ import { } from '../cliproxy/account-manager'; import type { CLIProxyProvider } from '../cliproxy/types'; import { getClaudeEnvVars } from '../cliproxy/config-generator'; -import { getProxyStatus as getProxyProcessStatus } from '../cliproxy/session-tracker'; +import { getProxyStatus as getProxyProcessStatus, stopProxy } from '../cliproxy/session-tracker'; import { ensureCliproxyService } from '../cliproxy/service-manager'; // Unified config imports import { @@ -1389,6 +1389,19 @@ apiRoutes.post('/cliproxy/proxy-start', async (_req: Request, res: Response): Pr } }); +/** + * POST /api/cliproxy/proxy-stop - Stop the CLIProxy service + * Returns: { stopped, pid?, sessionCount?, error? } + */ +apiRoutes.post('/cliproxy/proxy-stop', (_req: Request, res: Response): void => { + try { + const result = stopProxy(); + res.json(result); + } catch (error) { + res.status(500).json({ error: (error as Error).message }); + } +}); + /** * GET /api/cliproxy/models - Get available models from CLIProxyAPI * Returns: { models: CliproxyModel[], byCategory: Record, totalCount: number } diff --git a/ui/src/components/proxy-status-widget.tsx b/ui/src/components/proxy-status-widget.tsx index a85981c2..946d1cff 100644 --- a/ui/src/components/proxy-status-widget.tsx +++ b/ui/src/components/proxy-status-widget.tsx @@ -1,13 +1,13 @@ /** * Proxy Status Widget * - * Displays CLIProxy process status with start button for recovery. + * Displays CLIProxy process status with start/stop/restart controls. * Shows: running state, port, session count, uptime. */ -import { Activity, Power, RefreshCw, Clock, Users } from 'lucide-react'; +import { Activity, Power, RefreshCw, Clock, Users, Square, RotateCw } from 'lucide-react'; import { Button } from '@/components/ui/button'; -import { useProxyStatus, useStartProxy } from '@/hooks/use-cliproxy'; +import { useProxyStatus, useStartProxy, useStopProxy } from '@/hooks/use-cliproxy'; import { cn } from '@/lib/utils'; function formatUptime(startedAt?: string): string { @@ -28,8 +28,18 @@ function formatUptime(startedAt?: string): string { export function ProxyStatusWidget() { const { data: status, isLoading } = useProxyStatus(); const startProxy = useStartProxy(); + const stopProxy = useStopProxy(); const isRunning = status?.running ?? false; + const isActioning = startProxy.isPending || stopProxy.isPending; + + // Restart = stop then start + const handleRestart = async () => { + await stopProxy.mutateAsync(); + // Small delay to ensure port is released + await new Promise((r) => setTimeout(r, 500)); + startProxy.mutate(); + }; return (
{isRunning && status ? ( -
- Port {status.port} - {status.sessionCount !== undefined && status.sessionCount > 0 && ( - - - {status.sessionCount} session{status.sessionCount !== 1 ? 's' : ''} - - )} - {status.startedAt && ( - - - {formatUptime(status.startedAt)} - - )} -
+ <> +
+ Port {status.port} + {status.sessionCount !== undefined && status.sessionCount > 0 && ( + + + {status.sessionCount} session{status.sessionCount !== 1 ? 's' : ''} + + )} + {status.startedAt && ( + + + {formatUptime(status.startedAt)} + + )} +
+ {/* Control buttons when running */} +
+ + +
+ ) : (
Not running diff --git a/ui/src/hooks/use-cliproxy.ts b/ui/src/hooks/use-cliproxy.ts index 9f23842e..65a5be2f 100644 --- a/ui/src/hooks/use-cliproxy.ts +++ b/ui/src/hooks/use-cliproxy.ts @@ -240,3 +240,24 @@ export function useStartProxy() { }, }); } + +export function useStopProxy() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: () => api.cliproxy.proxyStop(), + onSuccess: (data) => { + queryClient.invalidateQueries({ queryKey: ['proxy-status'] }); + if (data.stopped) { + toast.success( + `CLIProxy stopped${data.sessionCount ? ` (${data.sessionCount} session(s) disconnected)` : ''}` + ); + } else { + toast.error(data.error || 'Failed to stop CLIProxy'); + } + }, + onError: (error: Error) => { + toast.error(error.message); + }, + }); +} diff --git a/ui/src/lib/api-client.ts b/ui/src/lib/api-client.ts index 9732272b..5d5096a5 100644 --- a/ui/src/lib/api-client.ts +++ b/ui/src/lib/api-client.ts @@ -182,6 +182,14 @@ export interface ProxyStartResult { error?: string; } +/** Result from stopping proxy service */ +export interface ProxyStopResult { + stopped: boolean; + pid?: number; + sessionCount?: number; + error?: string; +} + // API export const api = { profiles: { @@ -216,6 +224,7 @@ export const api = { // Proxy process status and control proxyStatus: () => request('/cliproxy/proxy-status'), proxyStart: () => request('/cliproxy/proxy-start', { method: 'POST' }), + proxyStop: () => request('/cliproxy/proxy-stop', { method: 'POST' }), // Stats and models for Overview tab stats: () => request<{ usage: Record }>('/cliproxy/usage'), From 96762a9f6ee096570b2fe6136a4431e6ce1d1a47 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Thu, 18 Dec 2025 23:17:05 -0500 Subject: [PATCH 3/4] feat(ui): show CLIProxyAPI update availability in dashboard - Add GET /api/cliproxy/update-check endpoint - Export checkCliproxyUpdate() function from binary-manager - Add useCliproxyUpdateCheck hook with 1-hour cache - Update ProxyStatusWidget with amber "Update" badge when available - Highlight Restart button as "Update" with amber styling when update pending --- src/cliproxy/binary-manager.ts | 17 ++++++++++ src/web-server/routes.ts | 14 ++++++++ ui/src/components/proxy-status-widget.tsx | 41 +++++++++++++++++++---- ui/src/hooks/use-cliproxy.ts | 12 +++++++ ui/src/lib/api-client.ts | 9 +++++ 5 files changed, 86 insertions(+), 7 deletions(-) diff --git a/src/cliproxy/binary-manager.ts b/src/cliproxy/binary-manager.ts index db9e38bf..be2e533a 100644 --- a/src/cliproxy/binary-manager.ts +++ b/src/cliproxy/binary-manager.ts @@ -995,6 +995,23 @@ export async function fetchLatestCliproxyVersion(): Promise { return result.latestVersion; } +/** Update check result for API response */ +export interface CliproxyUpdateCheckResult { + hasUpdate: boolean; + currentVersion: string; + latestVersion: string; + fromCache: boolean; +} + +/** + * Check for CLIProxyAPI binary updates + * @returns Update check result with version info + */ +export async function checkCliproxyUpdate(): Promise { + const manager = new BinaryManager(); + return manager.checkForUpdates(); +} + /** * Get path to version pin file * @returns Absolute path to .version-pin file diff --git a/src/web-server/routes.ts b/src/web-server/routes.ts index 4f2797f7..6f68fc27 100644 --- a/src/web-server/routes.ts +++ b/src/web-server/routes.ts @@ -46,6 +46,7 @@ import type { CLIProxyProvider } from '../cliproxy/types'; import { getClaudeEnvVars } from '../cliproxy/config-generator'; import { getProxyStatus as getProxyProcessStatus, stopProxy } from '../cliproxy/session-tracker'; import { ensureCliproxyService } from '../cliproxy/service-manager'; +import { checkCliproxyUpdate } from '../cliproxy/binary-manager'; // Unified config imports import { hasUnifiedConfig, @@ -1402,6 +1403,19 @@ apiRoutes.post('/cliproxy/proxy-stop', (_req: Request, res: Response): void => { } }); +/** + * GET /api/cliproxy/update-check - Check for CLIProxyAPI binary updates + * Returns: { hasUpdate, currentVersion, latestVersion, fromCache } + */ +apiRoutes.get('/cliproxy/update-check', async (_req: Request, res: Response): Promise => { + try { + const result = await checkCliproxyUpdate(); + res.json(result); + } catch (error) { + res.status(500).json({ error: (error as Error).message }); + } +}); + /** * GET /api/cliproxy/models - Get available models from CLIProxyAPI * Returns: { models: CliproxyModel[], byCategory: Record, totalCount: number } diff --git a/ui/src/components/proxy-status-widget.tsx b/ui/src/components/proxy-status-widget.tsx index 946d1cff..161be85a 100644 --- a/ui/src/components/proxy-status-widget.tsx +++ b/ui/src/components/proxy-status-widget.tsx @@ -2,12 +2,18 @@ * Proxy Status Widget * * Displays CLIProxy process status with start/stop/restart controls. - * Shows: running state, port, session count, uptime. + * Shows: running state, port, session count, uptime, update availability. */ -import { Activity, Power, RefreshCw, Clock, Users, Square, RotateCw } from 'lucide-react'; +import { Activity, Power, RefreshCw, Clock, Users, Square, RotateCw, ArrowUp } from 'lucide-react'; import { Button } from '@/components/ui/button'; -import { useProxyStatus, useStartProxy, useStopProxy } from '@/hooks/use-cliproxy'; +import { Badge } from '@/components/ui/badge'; +import { + useProxyStatus, + useStartProxy, + useStopProxy, + useCliproxyUpdateCheck, +} from '@/hooks/use-cliproxy'; import { cn } from '@/lib/utils'; function formatUptime(startedAt?: string): string { @@ -27,11 +33,13 @@ function formatUptime(startedAt?: string): string { export function ProxyStatusWidget() { const { data: status, isLoading } = useProxyStatus(); + const { data: updateCheck } = useCliproxyUpdateCheck(); const startProxy = useStartProxy(); const stopProxy = useStopProxy(); const isRunning = status?.running ?? false; const isActioning = startProxy.isPending || stopProxy.isPending; + const hasUpdate = updateCheck?.hasUpdate ?? false; // Restart = stop then start const handleRestart = async () => { @@ -57,6 +65,16 @@ export function ProxyStatusWidget() { )} /> CLIProxy Service + {hasUpdate && ( + v${updateCheck?.latestVersion}`} + > + + Update + + )}
@@ -90,19 +108,28 @@ export function ProxyStatusWidget() { {/* Control buttons when running */}
)} + + {/* Version sync indicator */} + {updateCheck?.currentVersion && ( +
+ v{updateCheck.currentVersion} + {updateCheck.checkedAt && ( + + Synced {formatTimeAgo(updateCheck.checkedAt)} + + )} +
+ )}
); } diff --git a/ui/src/lib/api-client.ts b/ui/src/lib/api-client.ts index f45837dd..73815560 100644 --- a/ui/src/lib/api-client.ts +++ b/ui/src/lib/api-client.ts @@ -196,6 +196,7 @@ export interface CliproxyUpdateCheckResult { currentVersion: string; latestVersion: string; fromCache: boolean; + checkedAt: number; // Unix timestamp of last check } // API