Initial commit
This commit is contained in:
281
hooks/version_checker.js
Executable file
281
hooks/version_checker.js
Executable file
@@ -0,0 +1,281 @@
|
||||
#!/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 };
|
||||
Reference in New Issue
Block a user