@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
JavaScript
/**
* 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} 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} --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