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
This commit is contained in:
kaitranntt
2025-12-18 06:12:40 -05:00
parent a0751ed604
commit 8a0ad53082
2 changed files with 119 additions and 21 deletions
+70 -13
View File
@@ -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<void> {
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<boolean> {
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<void> {
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();
}
+49 -8
View File
@@ -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<void> {
// 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;
}