UNPKG

@hivetechs/hive-ai

Version:

Real-time streaming AI consensus platform with HTTP+SSE MCP integration for Claude Code, VS Code, Cursor, and Windsurf - powered by OpenRouter's unified API

331 lines 13.7 kB
/** * Package Update Checker System * Handles npm package version checking and notifications for hive.ai CLI * Extends the existing auto-updater pattern for consistency */ import * as fs from 'fs'; import * as path from 'path'; import { execSync } from 'child_process'; export class PackageUpdateChecker { packageName = '@hivetechs/hive-ai'; constructor() { // No file-based config needed for new product } async getUpdateStatus() { // Load from unified database try { const { getConfig } = await import('../../storage/unified-database.js'); const statusData = await getConfig('package_update_status'); if (statusData) { return JSON.parse(statusData); } } catch (error) { console.debug('[PackageUpdateChecker] Database read failed, creating default'); } // Default status for first run const now = new Date(); return { lastCheck: new Date(0).toISOString(), // Force first check lastNotified: new Date(0).toISOString(), currentVersion: this.getCurrentVersion(), latestVersion: this.getCurrentVersion(), availableUpdate: false, updateType: 'none', nextCheck: new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(), // Weekly default notificationPreferences: { enabled: true, frequency: 'weekly', types: ['major', 'minor', 'patch'], lastDismissed: undefined } }; } async saveUpdateStatus(status) { try { const { setConfig } = await import('../../storage/unified-database.js'); await setConfig('package_update_status', JSON.stringify(status)); } catch (error) { console.error('Warning: Could not save package update status to database:', error instanceof Error ? error.message : 'Unknown error'); throw error; } } getCurrentVersion() { try { // Try to get version from package.json in current directory const packagePath = path.join(process.cwd(), 'package.json'); if (fs.existsSync(packagePath)) { const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8')); if (pkg.name === this.packageName) { return pkg.version; } } // Fallback: try to get from globally installed package const result = execSync(`npm list -g ${this.packageName} --depth=0 --json`, { encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] }); const npmData = JSON.parse(result); const packageInfo = npmData.dependencies?.[this.packageName]; return packageInfo?.version || 'unknown'; } catch (error) { // Return fallback version if we can't determine current version return 'unknown'; } } async getLatestVersion() { try { // Use npm view to get latest beta version info const result = execSync(`npm view ${this.packageName}@beta version`, { encoding: 'utf8', timeout: 10000, stdio: ['ignore', 'pipe', 'ignore'] }); return result.trim(); } catch (error) { throw new Error(`Failed to fetch latest version: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async getPackageInfo() { try { const result = execSync(`npm view ${this.packageName}@beta --json`, { encoding: 'utf8', timeout: 10000, stdio: ['ignore', 'pipe', 'ignore'] }); const packageData = JSON.parse(result); const currentVersion = this.getCurrentVersion(); const latestVersion = packageData.version; return { currentVersion, latestVersion, releaseType: this.determineReleaseType(currentVersion, latestVersion), releaseDate: packageData.time?.[latestVersion] || new Date().toISOString(), changelogUrl: `https://github.com/hivetechs/hive.ai/releases/tag/v${latestVersion}`, importance: this.determineImportance(currentVersion, latestVersion), updateAvailable: this.compareVersions(currentVersion, latestVersion) < 0 }; } catch (error) { throw new Error(`Failed to fetch package info: ${error instanceof Error ? error.message : 'Unknown error'}`); } } compareVersions(current, latest) { // Enhanced semver comparison that handles beta versions const parseVersion = (v) => { const cleaned = v.replace(/^v/, ''); const [main, prerelease] = cleaned.split('-'); const mainParts = main.split('.').map(n => parseInt(n, 10)); // Handle prerelease (e.g., beta.13) let prereleaseNumber = 0; if (prerelease) { const betaMatch = prerelease.match(/beta\.?(\d+)/); if (betaMatch) { prereleaseNumber = parseInt(betaMatch[1], 10); } } return { main: mainParts, prerelease: prereleaseNumber }; }; const currentVersion = parseVersion(current); const latestVersion = parseVersion(latest); // Compare main version parts first for (let i = 0; i < Math.max(currentVersion.main.length, latestVersion.main.length); i++) { const currentPart = currentVersion.main[i] || 0; const latestPart = latestVersion.main[i] || 0; if (currentPart < latestPart) return -1; if (currentPart > latestPart) return 1; } // If main versions are equal, compare prerelease numbers if (currentVersion.prerelease < latestVersion.prerelease) return -1; if (currentVersion.prerelease > latestVersion.prerelease) return 1; return 0; } determineReleaseType(current, latest) { if (this.compareVersions(current, latest) >= 0) return 'none'; const parseVersion = (v) => { const cleaned = v.replace(/^v/, '').split('-')[0]; return cleaned.split('.').map(n => parseInt(n, 10)); }; const currentParts = parseVersion(current); const latestParts = parseVersion(latest); // Simple heuristic for release type if (latestParts[0] > currentParts[0]) return 'major'; if (latestParts[1] > currentParts[1]) return 'minor'; return 'patch'; } determineImportance(current, latest) { const releaseType = this.determineReleaseType(current, latest); switch (releaseType) { case 'major': return 'important'; case 'minor': return 'important'; case 'patch': return 'optional'; case 'none': return 'optional'; default: return 'optional'; } } async shouldCheckForUpdate() { const status = await this.getUpdateStatus(); const now = new Date(); const nextCheck = new Date(status.nextCheck); return now >= nextCheck; } shouldNotify(status, updateInfo) { if (!status.notificationPreferences.enabled) return false; if (!updateInfo.updateAvailable) return false; if (updateInfo.releaseType === 'none') return false; if (!status.notificationPreferences.types.includes(updateInfo.releaseType)) return false; // Check if we've already notified about this version const lastNotified = new Date(status.lastNotified); const now = new Date(); // Frequency-based notification throttling const frequencyMs = { daily: 24 * 60 * 60 * 1000, weekly: 7 * 24 * 60 * 60 * 1000, monthly: 30 * 24 * 60 * 60 * 1000, never: Infinity }; const minInterval = frequencyMs[status.notificationPreferences.frequency]; return (now.getTime() - lastNotified.getTime()) >= minInterval; } getNextCheckTime(frequency = 'weekly') { const now = new Date(); const intervals = { daily: 24 * 60 * 60 * 1000, weekly: 7 * 24 * 60 * 60 * 1000, monthly: 30 * 24 * 60 * 60 * 1000, never: 365 * 24 * 60 * 60 * 1000 // Check yearly even if notifications disabled }; return new Date(now.getTime() + intervals[frequency]); } async checkForUpdatesAsync() { const startTime = Date.now(); try { const currentVersion = this.getCurrentVersion(); const latestVersion = await this.getLatestVersion(); const updateAvailable = this.compareVersions(currentVersion, latestVersion) < 0; const updateType = updateAvailable ? this.determineReleaseType(currentVersion, latestVersion) : 'none'; // Update status const now = new Date(); const currentStatus = await this.getUpdateStatus(); const shouldNotify = updateAvailable && this.shouldNotify(currentStatus, { currentVersion, latestVersion, releaseType: updateType, releaseDate: now.toISOString(), changelogUrl: '', importance: this.determineImportance(currentVersion, latestVersion), updateAvailable }); const newStatus = { ...currentStatus, lastCheck: now.toISOString(), currentVersion, latestVersion, availableUpdate: updateAvailable, updateType, nextCheck: this.getNextCheckTime(currentStatus.notificationPreferences.frequency).toISOString(), lastNotified: shouldNotify ? now.toISOString() : currentStatus.lastNotified }; await this.saveUpdateStatus(newStatus); const timeTaken = Date.now() - startTime; return { checked: true, updateAvailable, currentVersion, latestVersion, updateType, timeTaken, summary: updateAvailable ? `Update available: ${currentVersion} → ${latestVersion} (${updateType})` : `Package up to date: ${currentVersion}`, shouldNotify }; } catch (error) { // On error, schedule next check for 4 hours later const now = new Date(); const status = await this.getUpdateStatus(); status.lastCheck = now.toISOString(); status.nextCheck = new Date(now.getTime() + 4 * 60 * 60 * 1000).toISOString(); await this.saveUpdateStatus(status); return { checked: false, updateAvailable: false, currentVersion: this.getCurrentVersion(), latestVersion: 'unknown', updateType: 'none', timeTaken: Date.now() - startTime, summary: `Update check failed: ${error instanceof Error ? error.message : 'Unknown error'}`, shouldNotify: false }; } } async performUpdateCheckIfNeeded() { if (!(await this.shouldCheckForUpdate())) { return null; // No check needed } return await this.checkForUpdatesAsync(); } async getPackageUpdateStatus() { return await this.getUpdateStatus(); } async updateNotificationPreferences(preferences) { const status = await this.getUpdateStatus(); status.notificationPreferences = { ...status.notificationPreferences, ...preferences }; // Update next check time if frequency changed if (preferences.frequency) { status.nextCheck = this.getNextCheckTime(preferences.frequency).toISOString(); } await this.saveUpdateStatus(status); } async getLastUpdateInfo() { const status = await this.getUpdateStatus(); const lastCheck = new Date(status.lastCheck); const now = new Date(); if (lastCheck.getTime() === 0) { return 'Package updates: Never checked'; } const hoursAgo = Math.floor((now.getTime() - lastCheck.getTime()) / (1000 * 60 * 60)); if (status.availableUpdate) { return `Package update available: ${status.currentVersion} → ${status.latestVersion} (${status.updateType})`; } if (hoursAgo < 1) { return `Package up to date: ${status.currentVersion} (checked recently)`; } else if (hoursAgo < 24) { return `Package up to date: ${status.currentVersion} (checked ${hoursAgo}h ago)`; } else { const daysAgo = Math.floor(hoursAgo / 24); return `Package up to date: ${status.currentVersion} (checked ${daysAgo}d ago)`; } } async getDetailedUpdateInfo() { try { return await this.getPackageInfo(); } catch (error) { return null; } } } export const packageUpdateChecker = new PackageUpdateChecker(); //# sourceMappingURL=package-update-checker.js.map