import * as plugins from './smartupdate.plugins.js'; import { UpdateCacheManager } from './smartupdate.classes.cachemanager.js'; import { UpdateNotifier } from './smartupdate.classes.notifier.js'; import { DEFAULT_CACHE_DURATION_HOURS, MILLISECONDS_PER_MINUTE, MINUTES_PER_HOUR } from './smartupdate.constants.js'; import type { ISmartUpdateOptions, IUpdateCheckOptions, IUpdateCheckResult, } from './smartupdate.interfaces.js'; import { RegistryUnavailableError, PackageNotFoundError, } from './smartupdate.errors.js'; /** * SmartUpdate - Elegant update checking for Node.js packages * * @example * ```typescript * const smartUpdate = new SmartUpdate(); * const result = await smartUpdate.checkForUpdate({ * packageName: 'lodash', * currentVersion: '1.0.0', * changelogUrl: 'https://example.com/changelog' * }); * * if (result.status === 'update-available') { * console.log(`Update available: ${result.latestVersion}`); * } * ``` */ export class SmartUpdate { public readonly npmRegistry: plugins.smartnpm.NpmRegistry; private cacheManager: UpdateCacheManager; private notifier: UpdateNotifier; private options: ISmartUpdateOptions; /** * @deprecated Use the options parameter instead of kvStore property */ public kvStore: plugins.npmextra.KeyValueStore; constructor(options: ISmartUpdateOptions = {}) { this.options = options; // Initialize npm registry this.npmRegistry = new plugins.smartnpm.NpmRegistry(options.npmRegistryOptions || {}); // Calculate cache duration in milliseconds const cacheDuration = options.cacheDuration || { hours: DEFAULT_CACHE_DURATION_HOURS }; const cacheDurationMs = (cacheDuration.hours || 0) * MINUTES_PER_HOUR * MILLISECONDS_PER_MINUTE + (cacheDuration.minutes || 0) * MILLISECONDS_PER_MINUTE + (cacheDuration.seconds || 0) * 1000; // Initialize cache manager this.cacheManager = new UpdateCacheManager({ durationMs: cacheDurationMs || DEFAULT_CACHE_DURATION_HOURS * MINUTES_PER_HOUR * MILLISECONDS_PER_MINUTE, }); // Initialize notifier this.notifier = new UpdateNotifier({ logLevel: options.logLevel || 'INFO', useColors: !options.noColor, customLogger: options.customLogger, }); // Backward compatibility: expose kvStore this.kvStore = (this.cacheManager as any).kvStore; } /** * Check for updates with caching (primarily for CLI use) * * @deprecated Use checkForUpdate with cacheStrategy: 'always' instead * @param packageName - The npm package name to check * @param currentVersion - The current version to compare against * @param changelogUrl - Optional URL to open if update is available * @returns Promise resolving to true if update available, false otherwise * * @example * ```typescript * const hasUpdate = await smartUpdate.checkForCli('my-cli', '1.0.0', 'https://changelog.url'); * ``` */ public async checkForCli( packageName: string, currentVersion: string, changelogUrl?: string ): Promise { const result = await this.checkForUpdate({ packageName, currentVersion, changelogUrl, cacheStrategy: 'always', }); return result.status === 'update-available'; } /** * Check for updates (modern API) * * @param options - Update check options * @returns Promise resolving to detailed update check result * * @example * ```typescript * const result = await smartUpdate.checkForUpdate({ * packageName: 'lodash', * currentVersion: '1.0.0', * changelogUrl: 'https://changelog.url', * openChangelog: true, * cacheStrategy: 'time-based' * }); * * console.log(result.status); // 'up-to-date' | 'update-available' | 'check-skipped' | 'error' * ``` */ public async checkForUpdate(options: IUpdateCheckOptions): Promise { const { packageName, currentVersion, changelogUrl, openChangelog = true, cacheStrategy = 'time-based', } = options; const checkTime = new Date(); try { // Check if we should use cache or check registry const cacheCheck = await this.cacheManager.shouldCheckRegistry(packageName, cacheStrategy); // If we should skip the check due to cache if (!cacheCheck.shouldCheck) { return { status: 'check-skipped', packageName, currentVersion, latestVersion: cacheCheck.cacheStatus?.latestVersion, checkTime, cacheHit: true, nextCheckTime: cacheCheck.nextCheckTime, reason: 'Rate limited - checked recently', }; } // Fetch package info from registry const npmPackage = await this.getNpmPackageFromRegistry(packageName); if (!npmPackage) { throw new RegistryUnavailableError(); } // Compare versions const versionNpm = new plugins.smartversion.SmartVersion(npmPackage.version); const versionLocal = new plugins.smartversion.SmartVersion(currentVersion); const result: IUpdateCheckResult = { status: versionNpm.greaterThan(versionLocal) ? 'update-available' : 'up-to-date', packageName: npmPackage.name, currentVersion, latestVersion: npmPackage.version, checkTime, cacheHit: false, }; // Notify user this.notifier.notifyUpdateCheckResult(result); // If update is available, handle changelog if (result.status === 'update-available' && changelogUrl && openChangelog && !process.env.CI) { this.notifier.notifyOpeningChangelog(); plugins.smartopen.openUrl(changelogUrl); } // Update cache if there's an update if (result.status === 'update-available') { const cacheStatus = this.cacheManager.createCacheStatus(npmPackage.version, false); await this.cacheManager.setCached(packageName, cacheStatus); } return result; } catch (error) { if (error instanceof RegistryUnavailableError) { this.notifier.notifyRegistryError(); } else { this.notifier.error(error instanceof Error ? error.message : String(error)); } return { status: 'error', packageName, currentVersion, checkTime, cacheHit: false, error: error instanceof Error ? error : new Error(String(error)), }; } } /** * Simple check for updates without caching * * @deprecated Use checkForUpdate with cacheStrategy: 'never' instead * @param packageName - The npm package name to check * @param currentVersion - The current version to compare against * @param changelogUrl - Optional URL to open if update is available * @returns Promise resolving to true if update available, false otherwise * * @example * ```typescript * const hasUpdate = await smartUpdate.check('lodash', '1.0.0'); * ``` */ public async check( packageName: string, currentVersion: string, changelogUrl?: string ): Promise { const result = await this.checkForUpdate({ packageName, currentVersion, changelogUrl, cacheStrategy: 'never', }); return result.status === 'update-available'; } /** * Get the latest version of a package * * @param packageName - The npm package name * @returns Promise resolving to the latest version string * * @example * ```typescript * const latestVersion = await smartUpdate.getLatestVersion('lodash'); * console.log(latestVersion); // e.g., "4.17.21" * ``` */ public async getLatestVersion(packageName: string): Promise { const npmPackage = await this.getNpmPackageFromRegistry(packageName); if (!npmPackage) { throw new PackageNotFoundError(packageName); } return npmPackage.version; } /** * Clear the cache for a specific package * * @param packageName - The package name to clear cache for * * @example * ```typescript * await smartUpdate.clearCache('lodash'); * ``` */ public async clearCache(packageName: string): Promise { await this.cacheManager.clearCache(packageName); } /** * Fetch package information from the npm registry * @private */ private async getNpmPackageFromRegistry(packageName: string): Promise { this.notifier.notifyCheckingForUpdate(packageName); try { const npmPackage = await this.npmRegistry.getPackageInfo(packageName); return npmPackage; } catch (error) { return null; } } }