diff --git a/src/utils/versionCache.ts b/src/utils/versionCache.ts new file mode 100644 index 0000000..a83522a --- /dev/null +++ b/src/utils/versionCache.ts @@ -0,0 +1,131 @@ +import { homedir } from 'os'; +import { join } from 'path'; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'; +import { Logger } from './logger.js'; +import semver from 'semver'; + +const logger = new Logger({ name: 'version-cache' }); + +/** + * Interface for the version cache data structure + */ +export interface VersionCacheData { + /** The cached version string */ + version: string; +} + +/** + * Class to manage version cache in the user's home directory + */ +export class VersionCache { + private readonly cacheDir: string; + private readonly cacheFile: string; + + constructor() { + this.cacheDir = join(homedir(), '.mycoder'); + this.cacheFile = join(this.cacheDir, 'version-cache.json'); + + // Ensure cache directory exists + this.ensureCacheDir(); + } + + /** + * Ensures the cache directory exists + */ + private ensureCacheDir(): void { + try { + if (!existsSync(this.cacheDir)) { + mkdirSync(this.cacheDir, { recursive: true }); + } + } catch (error) { + logger.warn( + 'Failed to create cache directory ~/.mycoder:', + error instanceof Error ? error.message : String(error) + ); + } + } + + /** + * Reads the cache data from disk + * @returns The cached data or null if not found/invalid + */ + read(): VersionCacheData | null { + try { + if (!existsSync(this.cacheFile)) { + return null; + } + + const data = JSON.parse( + readFileSync(this.cacheFile, 'utf-8') + ) as VersionCacheData; + + // Validate required fields + if (!data || typeof data.version !== 'string') { + return null; + } + + return data; + } catch (error) { + logger.warn( + 'Error reading version cache:', + error instanceof Error ? error.message : String(error) + ); + return null; + } + } + + /** + * Writes data to the cache file + * @param data The version cache data to write + */ + write(data: VersionCacheData): void { + try { + writeFileSync(this.cacheFile, JSON.stringify(data, null, 2)); + } catch (error) { + logger.warn( + 'Error writing version cache:', + error instanceof Error ? error.message : String(error) + ); + } + } + + /** + * Checks if the cached version is greater than the current version + * @param currentVersion The current package version + * @returns true if cached version > current version, false otherwise + */ + shouldCheckServer(currentVersion: string): boolean { + try { + const data = this.read(); + if (!data) { + return true; // No cache, should check server + } + + // If cached version is greater than current, we should check server + // as an upgrade is likely available + return semver.gt(data.version, currentVersion); + } catch (error) { + logger.warn( + 'Error comparing versions:', + error instanceof Error ? error.message : String(error) + ); + return true; // On error, check server to be safe + } + } + + /** + * Clears the cache file + */ + clear(): void { + try { + if (existsSync(this.cacheFile)) { + writeFileSync(this.cacheFile, ''); + } + } catch (error) { + logger.warn( + 'Error clearing version cache:', + error instanceof Error ? error.message : String(error) + ); + } + } +} \ No newline at end of file diff --git a/src/utils/versionCheck.ts b/src/utils/versionCheck.ts index 1c2d6da..23279ce 100644 --- a/src/utils/versionCheck.ts +++ b/src/utils/versionCheck.ts @@ -2,9 +2,12 @@ import { Logger } from "./logger.js"; import chalk from "chalk"; import { createRequire } from "module"; import type { PackageJson } from "type-fest"; +import { VersionCache } from './versionCache.js'; +import semver from 'semver'; const require = createRequire(import.meta.url); const logger = new Logger({ name: "version-check" }); +const versionCache = new VersionCache(); /** * Gets the current package info from package.json @@ -60,16 +63,53 @@ export function generateUpgradeMessage( latestVersion: string, packageName: string, ): string | null { - return currentVersion !== latestVersion + return semver.gt(latestVersion, currentVersion) ? chalk.green( ` Update available: ${currentVersion} → ${latestVersion}\n Run 'npm install -g ${packageName}' to update`, ) : null; } +/** + * Updates the version cache + */ +async function updateVersionCache( + packageName: string, + version: string, +): Promise { + try { + versionCache.write({ version }); + } catch (error) { + logger.warn( + "Error updating version cache:", + error instanceof Error ? error.message : String(error), + ); + } +} + +/** + * Performs a background version check and updates the cache + */ +async function backgroundVersionCheck( + packageName: string, +): Promise { + try { + const latestVersion = await fetchLatestVersion(packageName); + if (latestVersion) { + await updateVersionCache(packageName, latestVersion); + } + } catch (error) { + logger.warn( + "Background version check failed:", + error instanceof Error ? error.message : String(error), + ); + } +} + /** * Checks if a newer version of the package is available on npm. - * Only runs check when package is installed globally. + * - Immediately returns cached version check result if available + * - Always performs a background check to update cache for next time * * @returns Upgrade message string if update available, null otherwise */ @@ -82,13 +122,32 @@ export async function checkForUpdates(): Promise { return null; } - const latestVersion = await fetchLatestVersion(packageName); + // Always trigger background version check for next time + setImmediate(() => { + backgroundVersionCheck(packageName).catch(error => { + logger.warn( + "Background version check failed:", + error instanceof Error ? error.message : String(error), + ); + }); + }); + + // Use cached data for immediate response if available + const cachedData = versionCache.read(); + if (cachedData) { + return generateUpgradeMessage(currentVersion, cachedData.version, packageName); + } + // No cache available, need to check server + const latestVersion = await fetchLatestVersion(packageName); if (!latestVersion) { logger.warn("Unable to determine latest published version"); return null; } + // Update cache with result + await updateVersionCache(packageName, latestVersion); + return generateUpgradeMessage(currentVersion, latestVersion, packageName); } catch (error) { // Log error but don't throw to handle gracefully @@ -98,4 +157,4 @@ export async function checkForUpdates(): Promise { ); return null; } -} +} \ No newline at end of file