mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 18:18:43 +00:00
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:
+70
-13
@@ -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 { spawn, ChildProcess } from 'child_process';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
@@ -33,6 +24,16 @@ import { handleUpdateCommand } from './commands/update-command';
|
|||||||
// Import extracted utility functions
|
// Import extracted utility functions
|
||||||
import { execClaude, escapeShellArg } from './utils/shell-executor';
|
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 ==========
|
// ========== Profile Detection ==========
|
||||||
|
|
||||||
interface DetectedProfile {
|
interface DetectedProfile {
|
||||||
@@ -66,7 +67,8 @@ async function execClaudeWithProxy(
|
|||||||
// 1. Read settings to get API key
|
// 1. Read settings to get API key
|
||||||
const settingsPath = getSettingsPath(profileName);
|
const settingsPath = getSettingsPath(profileName);
|
||||||
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
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') {
|
if (!apiKey || apiKey === 'YOUR_GLM_API_KEY_HERE') {
|
||||||
console.error('[X] GLMT profile requires Z.AI API key');
|
console.error('[X] GLMT profile requires Z.AI API key');
|
||||||
@@ -87,7 +89,7 @@ async function execClaudeWithProxy(
|
|||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
ANTHROPIC_AUTH_TOKEN: apiKey,
|
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[];
|
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> {
|
async function main(): Promise<void> {
|
||||||
const args = process.argv.slice(2);
|
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)
|
// Auto-migrate to unified config format (silent if already migrated)
|
||||||
// Skip if user is explicitly running migrate command
|
// Skip if user is explicitly running migrate command
|
||||||
if (args[0] !== 'migrate') {
|
if (firstArg !== 'migrate') {
|
||||||
const { autoMigrate } = await import('./config/migration-manager');
|
const { autoMigrate } = await import('./config/migration-manager');
|
||||||
await autoMigrate();
|
await autoMigrate();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special case: version command (check BEFORE profile detection)
|
// Special case: version command (check BEFORE profile detection)
|
||||||
const firstArg = args[0];
|
|
||||||
if (firstArg === 'version' || firstArg === '--version' || firstArg === '-v') {
|
if (firstArg === 'version' || firstArg === '--version' || firstArg === '-v') {
|
||||||
handleVersionCommand();
|
handleVersionCommand();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import * as fs from 'fs';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as https from 'https';
|
import * as https from 'https';
|
||||||
import { warn, color } from './ui';
|
|
||||||
|
|
||||||
const CACHE_DIR = path.join(os.homedir(), '.ccs', 'cache');
|
const CACHE_DIR = path.join(os.homedir(), '.ccs', 'cache');
|
||||||
const UPDATE_CHECK_FILE = path.join(CACHE_DIR, 'update-check.json');
|
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('');
|
||||||
console.log(color('═══════════════════════════════════════════════════════', 'info'));
|
console.log(warnBox(content, 'UPDATE AVAILABLE'));
|
||||||
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('');
|
console.log('');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,3 +329,36 @@ export function dismissUpdate(version: string): void {
|
|||||||
cache.dismissed_version = version;
|
cache.dismissed_version = version;
|
||||||
writeCache(cache);
|
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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user