282 lines
8.3 KiB
JavaScript
Executable File
282 lines
8.3 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
/**
|
|
* Contextune Version Checker
|
|
*
|
|
* Checks for plugin updates by comparing local version with remote version.
|
|
* Runs on SessionStart hook to notify users of available updates.
|
|
*
|
|
* Features:
|
|
* - Fetches latest version from GitHub
|
|
* - Caches check (once per day)
|
|
* - Non-blocking (never fails session start)
|
|
* - Friendly upgrade notifications
|
|
* - Tracks check history in observability DB
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const https = require('https');
|
|
const { execSync } = require('child_process');
|
|
|
|
// Configuration
|
|
const GITHUB_OWNER = 'Shakes-tzd';
|
|
const GITHUB_REPO = 'contextune';
|
|
const PLUGIN_JSON_URL = `https://raw.githubusercontent.com/${GITHUB_OWNER}/${GITHUB_REPO}/master/.claude-plugin/plugin.json`;
|
|
const CHECK_INTERVAL_HOURS = 24; // Check once per day
|
|
const CACHE_FILE = path.join(process.env.HOME || process.env.USERPROFILE, '.claude', 'plugins', 'contextune', 'data', 'version_cache.json');
|
|
|
|
/**
|
|
* Get current installed version
|
|
*/
|
|
function getCurrentVersion() {
|
|
try {
|
|
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT;
|
|
if (!pluginRoot) {
|
|
return null;
|
|
}
|
|
|
|
const pluginJsonPath = path.join(pluginRoot, '.claude-plugin', 'plugin.json');
|
|
const pluginJson = JSON.parse(fs.readFileSync(pluginJsonPath, 'utf-8'));
|
|
return pluginJson.version;
|
|
} catch (error) {
|
|
console.error(`Version check error (local): ${error.message}`);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch latest version from GitHub
|
|
*/
|
|
function fetchLatestVersion() {
|
|
return new Promise((resolve, reject) => {
|
|
const request = https.get(PLUGIN_JSON_URL, { timeout: 3000 }, (response) => {
|
|
let data = '';
|
|
|
|
response.on('data', (chunk) => {
|
|
data += chunk;
|
|
});
|
|
|
|
response.on('end', () => {
|
|
try {
|
|
if (response.statusCode === 200) {
|
|
const pluginJson = JSON.parse(data);
|
|
resolve(pluginJson.version);
|
|
} else {
|
|
reject(new Error(`HTTP ${response.statusCode}`));
|
|
}
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
});
|
|
});
|
|
|
|
request.on('error', reject);
|
|
request.on('timeout', () => {
|
|
request.destroy();
|
|
reject(new Error('Request timeout'));
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Compare version strings (semver-like)
|
|
*/
|
|
function compareVersions(current, latest) {
|
|
const currentParts = current.split('.').map(Number);
|
|
const latestParts = latest.split('.').map(Number);
|
|
|
|
for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
|
|
const currentPart = currentParts[i] || 0;
|
|
const latestPart = latestParts[i] || 0;
|
|
|
|
if (latestPart > currentPart) return 1; // Update available
|
|
if (latestPart < currentPart) return -1; // Current is newer (dev version)
|
|
}
|
|
|
|
return 0; // Versions are equal
|
|
}
|
|
|
|
/**
|
|
* Get cached version check result
|
|
*/
|
|
function getCachedCheck() {
|
|
try {
|
|
if (!fs.existsSync(CACHE_FILE)) {
|
|
return null;
|
|
}
|
|
|
|
const cache = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf-8'));
|
|
const cacheAge = Date.now() - cache.timestamp;
|
|
const cacheValid = cacheAge < CHECK_INTERVAL_HOURS * 60 * 60 * 1000;
|
|
|
|
return cacheValid ? cache : null;
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save version check result to cache
|
|
*/
|
|
function saveCachedCheck(currentVersion, latestVersion, updateAvailable) {
|
|
try {
|
|
const cacheDir = path.dirname(CACHE_FILE);
|
|
if (!fs.existsSync(cacheDir)) {
|
|
fs.mkdirSync(cacheDir, { recursive: true });
|
|
}
|
|
|
|
const cache = {
|
|
timestamp: Date.now(),
|
|
currentVersion,
|
|
latestVersion,
|
|
updateAvailable,
|
|
lastCheck: new Date().toISOString()
|
|
};
|
|
|
|
fs.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2));
|
|
} catch (error) {
|
|
// Silent fail - caching is not critical
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Record version check in observability database
|
|
*/
|
|
function recordVersionCheck(currentVersion, latestVersion, updateAvailable) {
|
|
try {
|
|
const dbFile = path.join(process.env.CLAUDE_PLUGIN_ROOT || '', '.contextune', 'observability.db');
|
|
|
|
if (!fs.existsSync(dbFile)) {
|
|
return; // DB doesn't exist yet
|
|
}
|
|
|
|
const query = `
|
|
INSERT INTO version_checks (check_time, current_version, latest_version, update_available)
|
|
VALUES (${Date.now() / 1000}, '${currentVersion}', '${latestVersion}', ${updateAvailable ? 1 : 0})
|
|
`;
|
|
|
|
execSync(`sqlite3 "${dbFile}" "${query}"`, {
|
|
stdio: 'pipe',
|
|
timeout: 1000
|
|
});
|
|
} catch (error) {
|
|
// Silent fail - observability is not critical
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate update notification message
|
|
*/
|
|
function generateUpdateMessage(currentVersion, latestVersion) {
|
|
return `
|
|
╭─────────────────────────────────────────────────╮
|
|
│ 🎉 Contextune Update Available! │
|
|
├─────────────────────────────────────────────────┤
|
|
│ │
|
|
│ Current: v${currentVersion.padEnd(10)} → Latest: v${latestVersion} │
|
|
│ │
|
|
│ 📦 What's New: │
|
|
│ • Performance improvements │
|
|
│ • Bug fixes and enhancements │
|
|
│ • See full changelog on GitHub │
|
|
│ │
|
|
│ 🔄 To Update: │
|
|
│ /plugin update contextune │
|
|
│ │
|
|
│ 📚 Release Notes: │
|
|
│ github.com/${GITHUB_OWNER}/${GITHUB_REPO}/releases │
|
|
│ │
|
|
╰─────────────────────────────────────────────────╯
|
|
|
|
💡 Tip: Keep Contextune updated for the latest features and fixes!
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* Main version check logic
|
|
*/
|
|
async function checkVersion() {
|
|
try {
|
|
// Get current version
|
|
const currentVersion = getCurrentVersion();
|
|
if (!currentVersion) {
|
|
console.error('Could not determine current version');
|
|
return;
|
|
}
|
|
|
|
// Check cache first
|
|
const cached = getCachedCheck();
|
|
if (cached) {
|
|
if (cached.updateAvailable) {
|
|
console.log(generateUpdateMessage(cached.currentVersion, cached.latestVersion));
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Fetch latest version from GitHub
|
|
const latestVersion = await fetchLatestVersion();
|
|
|
|
// Compare versions
|
|
const comparison = compareVersions(currentVersion, latestVersion);
|
|
const updateAvailable = comparison > 0;
|
|
|
|
// Save to cache
|
|
saveCachedCheck(currentVersion, latestVersion, updateAvailable);
|
|
|
|
// Record in observability DB
|
|
recordVersionCheck(currentVersion, latestVersion, updateAvailable);
|
|
|
|
// Show notification if update available
|
|
if (updateAvailable) {
|
|
console.log(generateUpdateMessage(currentVersion, latestVersion));
|
|
} else {
|
|
console.error(`Contextune v${currentVersion} (latest)`);
|
|
}
|
|
|
|
} catch (error) {
|
|
// Silent fail - version check should never block session start
|
|
console.error(`Version check skipped: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize version checks table in observability DB
|
|
*/
|
|
function initializeDatabase() {
|
|
try {
|
|
const dbFile = path.join(process.env.CLAUDE_PLUGIN_ROOT || '', '.contextune', 'observability.db');
|
|
|
|
if (!fs.existsSync(dbFile)) {
|
|
return; // DB doesn't exist yet, will be created by other hooks
|
|
}
|
|
|
|
const createTableQuery = `
|
|
CREATE TABLE IF NOT EXISTS version_checks (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
check_time REAL NOT NULL,
|
|
current_version TEXT NOT NULL,
|
|
latest_version TEXT NOT NULL,
|
|
update_available INTEGER NOT NULL,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
`;
|
|
|
|
execSync(`sqlite3 "${dbFile}" "${createTableQuery}"`, {
|
|
stdio: 'pipe',
|
|
timeout: 2000
|
|
});
|
|
} catch (error) {
|
|
// Silent fail
|
|
}
|
|
}
|
|
|
|
// Run version check
|
|
if (require.main === module) {
|
|
initializeDatabase();
|
|
checkVersion().catch(err => {
|
|
console.error(`Version check failed: ${err.message}`);
|
|
});
|
|
}
|
|
|
|
module.exports = { checkVersion, compareVersions };
|