From 8a0ad5308262ba14fb0fe23ff8a87f3c5ecaa139 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Thu, 18 Dec 2025 06:12:40 -0500 Subject: [PATCH] feat(update): add automatic update check on startup - check cached update result synchronously on every command - show warnBox notification if update available - skip check for version/help/update commands - refresh cache in background if stale (>24h) - use proper UI components (warnBox) for notification --- src/ccs.ts | 83 +++++++++++++++++++++++++++++++------ src/utils/update-checker.ts | 57 +++++++++++++++++++++---- 2 files changed, 119 insertions(+), 21 deletions(-) diff --git a/src/ccs.ts b/src/ccs.ts index 288b6873..2477cfb4 100644 --- a/src/ccs.ts +++ b/src/ccs.ts @@ -1,12 +1,3 @@ -#!/usr/bin/env node -/** - * CCS (Claude Code Switch) - Entry Point - * - * Instant profile switching for Claude CLI. - * Supports multiple accounts, alternative models (GLM, Kimi), - * and cost-optimized delegation. - */ - import { spawn, ChildProcess } from 'child_process'; import * as path from 'path'; import * as fs from 'fs'; @@ -33,6 +24,16 @@ import { handleUpdateCommand } from './commands/update-command'; // Import extracted utility functions import { execClaude, escapeShellArg } from './utils/shell-executor'; +// Version and Update check utilities +import { getVersion } from './utils/version'; +import { + checkForUpdates, + showUpdateNotification, + checkCachedUpdate, + isCacheStale, +} from './utils/update-checker'; +import { detectInstallationMethod } from './utils/package-manager-detector'; + // ========== Profile Detection ========== interface DetectedProfile { @@ -66,7 +67,8 @@ async function execClaudeWithProxy( // 1. Read settings to get API key const settingsPath = getSettingsPath(profileName); const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); - const apiKey = settings.env.ANTHROPIC_AUTH_TOKEN; + const envData = settings.env; + const apiKey = envData['ANTHROPIC_AUTH_TOKEN']; if (!apiKey || apiKey === 'YOUR_GLM_API_KEY_HERE') { console.error('[X] GLMT profile requires Z.AI API key'); @@ -87,7 +89,7 @@ async function execClaudeWithProxy( env: { ...process.env, ANTHROPIC_AUTH_TOKEN: apiKey, - ANTHROPIC_BASE_URL: settings.env.ANTHROPIC_BASE_URL, + ANTHROPIC_BASE_URL: envData['ANTHROPIC_BASE_URL'], }, }); @@ -211,18 +213,73 @@ interface ProfileError extends Error { suggestions?: string[]; } +/** + * Perform background update check (refreshes cache, no notification) + */ +async function refreshUpdateCache(): Promise { + try { + const currentVersion = getVersion(); + const installMethod = detectInstallationMethod(); + // Force=true to always fetch fresh data + await checkForUpdates(currentVersion, true, installMethod); + } catch (_e) { + // Silently fail - update check shouldn't crash main CLI + } +} + +/** + * Show update notification if cached result indicates update available + * Returns true if notification was shown + */ +async function showCachedUpdateNotification(): Promise { + try { + const currentVersion = getVersion(); + const updateInfo = checkCachedUpdate(currentVersion); + + if (updateInfo) { + await showUpdateNotification(updateInfo); + return true; + } + } catch (_e) { + // Silently fail + } + return false; +} + async function main(): Promise { const args = process.argv.slice(2); + const firstArg = args[0]; + + // Trigger update check early for ALL commands except version/help/update + // Only if TTY and not CI to avoid noise in automated environments + const skipUpdateCheck = [ + 'version', + '--version', + '-v', + 'help', + '--help', + '-h', + 'update', + '--update', + ]; + if (process.stdout.isTTY && !process.env['CI'] && !skipUpdateCheck.includes(firstArg)) { + // 1. Show cached update notification (async for proper UI) + await showCachedUpdateNotification(); + + // 2. Refresh cache in background if stale (non-blocking) + if (isCacheStale()) { + refreshUpdateCache(); + } + } // Auto-migrate to unified config format (silent if already migrated) // Skip if user is explicitly running migrate command - if (args[0] !== 'migrate') { + if (firstArg !== 'migrate') { const { autoMigrate } = await import('./config/migration-manager'); await autoMigrate(); } // Special case: version command (check BEFORE profile detection) - const firstArg = args[0]; if (firstArg === 'version' || firstArg === '--version' || firstArg === '-v') { handleVersionCommand(); } diff --git a/src/utils/update-checker.ts b/src/utils/update-checker.ts index 5a594813..c04dc585 100644 --- a/src/utils/update-checker.ts +++ b/src/utils/update-checker.ts @@ -6,7 +6,6 @@ import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import * as https from 'https'; -import { warn, color } from './ui'; const CACHE_DIR = path.join(os.homedir(), '.ccs', 'cache'); const UPDATE_CHECK_FILE = path.join(CACHE_DIR, 'update-check.json'); @@ -301,15 +300,24 @@ export async function checkForUpdates( } /** - * Show update notification + * Show update notification (async - initializes UI first) */ -export function showUpdateNotification(updateInfo: { current: string; latest: string }): void { +export async function showUpdateNotification(updateInfo: { + current: string; + latest: string; +}): Promise { + // Lazy import UI to ensure modules are loaded + const { initUI, warnBox, color } = await import('./ui'); + await initUI(); + + const content = [ + `Update available: ${updateInfo.current} -> ${updateInfo.latest}`, + '', + `Run ${color('ccs update', 'command')} to update`, + ].join('\n'); + console.log(''); - console.log(color('═══════════════════════════════════════════════════════', 'info')); - console.log(warn(`Update available: ${updateInfo.current} → ${updateInfo.latest}`)); - console.log(color('═══════════════════════════════════════════════════════', 'info')); - console.log(''); - console.log(` Run ${color('ccs update', 'command')} to update`); + console.log(warnBox(content, 'UPDATE AVAILABLE')); console.log(''); } @@ -321,3 +329,36 @@ export function dismissUpdate(version: string): void { cache.dismissed_version = version; writeCache(cache); } + +/** + * Check cached update result synchronously (no network call) + * Returns update info if cached result indicates update available, null otherwise + */ +export function checkCachedUpdate( + currentVersion: string +): { current: string; latest: string } | null { + const cache = readCache(); + + // Check if cache has a newer version + if ( + cache.latest_version && + compareVersionsWithPrerelease(cache.latest_version, currentVersion) > 0 + ) { + // Don't show if user dismissed this version + if (cache.dismissed_version === cache.latest_version) { + return null; + } + return { current: currentVersion, latest: cache.latest_version }; + } + + return null; +} + +/** + * Check if cache is stale (older than CHECK_INTERVAL) + */ +export function isCacheStale(): boolean { + const cache = readCache(); + const now = Date.now(); + return now - cache.last_check >= CHECK_INTERVAL; +}