Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions src/utils/versionCache.ts
Original file line number Diff line number Diff line change
@@ -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';

Check failure on line 5 in src/utils/versionCache.ts

View workflow job for this annotation

GitHub Actions / ci

Cannot find module 'semver' or its corresponding type declarations.

Check failure on line 5 in src/utils/versionCache.ts

View workflow job for this annotation

GitHub Actions / ci

Cannot find module 'semver' or its corresponding type declarations.

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)
);
}
}
}
67 changes: 63 additions & 4 deletions src/utils/versionCheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
import chalk from "chalk";
import { createRequire } from "module";
import type { PackageJson } from "type-fest";
import { VersionCache } from './versionCache.js';
import semver from 'semver';

Check failure on line 6 in src/utils/versionCheck.ts

View workflow job for this annotation

GitHub Actions / ci

Cannot find module 'semver' or its corresponding type declarations.

Check failure on line 6 in src/utils/versionCheck.ts

View workflow job for this annotation

GitHub Actions / ci

Cannot find module 'semver' or its corresponding type declarations.

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
Expand Down Expand Up @@ -60,16 +63,53 @@
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<void> {
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<void> {
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
*/
Expand All @@ -82,13 +122,32 @@
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
Expand All @@ -98,4 +157,4 @@
);
return null;
}
}
}
Loading