140 lines
4.3 KiB
JavaScript
140 lines
4.3 KiB
JavaScript
#!/usr/bin/env node
|
|
// Hook: update-check.mjs
|
|
// Event: UserPromptSubmit
|
|
// Purpose: Check for newer plugin versions (max 1x/24h, cached).
|
|
//
|
|
// Protocol:
|
|
// - Read JSON from stdin (consume, don't use)
|
|
// - If newer version available: exit 0, stdout JSON { systemMessage: "..." }
|
|
// - Otherwise: exit 0 silently
|
|
// - Never block the user (always exit 0)
|
|
//
|
|
// Disable: LLM_SECURITY_UPDATE_CHECK=off
|
|
|
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
import { resolve, dirname } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { homedir } from 'node:os';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Exports for testing
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
|
|
/**
|
|
* Return true if `remote` is a newer semver than `local`.
|
|
* Simple numeric comparison — no pre-release/build metadata.
|
|
*/
|
|
export function isNewer(remote, local) {
|
|
const r = remote.split('.').map(Number);
|
|
const l = local.split('.').map(Number);
|
|
for (let i = 0; i < Math.max(r.length, l.length); i++) {
|
|
const rv = r[i] ?? 0;
|
|
const lv = l[i] ?? 0;
|
|
if (rv > lv) return true;
|
|
if (rv < lv) return false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Main (only runs when executed directly, not when imported for tests)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const isDirectExecution = process.argv[1] &&
|
|
resolve(process.argv[1]) === resolve(__dirname, 'update-check.mjs');
|
|
|
|
if (isDirectExecution) {
|
|
main().catch(() => process.exit(0));
|
|
}
|
|
|
|
async function main() {
|
|
// Opt-out
|
|
if (process.env.LLM_SECURITY_UPDATE_CHECK === 'off') {
|
|
process.exit(0);
|
|
}
|
|
|
|
// Consume stdin (prevent pipe errors)
|
|
try { readFileSync(0, 'utf8'); } catch { /* ignore */ }
|
|
|
|
// Resolve plugin root
|
|
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT || resolve(__dirname, '../..');
|
|
|
|
// Read installed version
|
|
let installed;
|
|
try {
|
|
const pluginJson = JSON.parse(readFileSync(resolve(pluginRoot, '.claude-plugin/plugin.json'), 'utf8'));
|
|
installed = pluginJson.version;
|
|
} catch {
|
|
process.exit(0);
|
|
}
|
|
|
|
// Read repo URL
|
|
let repoUrl;
|
|
try {
|
|
const pkg = JSON.parse(readFileSync(resolve(pluginRoot, 'package.json'), 'utf8'));
|
|
repoUrl = pkg.repository?.url;
|
|
} catch {
|
|
process.exit(0);
|
|
}
|
|
|
|
if (!installed || !repoUrl) process.exit(0);
|
|
|
|
// Cache
|
|
const cacheDir = resolve(homedir(), '.cache/llm-security');
|
|
const cachePath = resolve(cacheDir, 'update-check.json');
|
|
|
|
// Check cache
|
|
try {
|
|
if (existsSync(cachePath)) {
|
|
const cache = JSON.parse(readFileSync(cachePath, 'utf8'));
|
|
if (Date.now() - cache.checkedAt < CHECK_INTERVAL_MS) {
|
|
// Cache is fresh
|
|
if (cache.latestVersion && isNewer(cache.latestVersion, installed)) {
|
|
console.log(JSON.stringify({
|
|
systemMessage: `🔄 llm-security v${installed} → v${cache.latestVersion} available. Update: ${repoUrl}`
|
|
}));
|
|
}
|
|
process.exit(0);
|
|
}
|
|
}
|
|
} catch {
|
|
// Corrupt cache — proceed to fetch
|
|
}
|
|
|
|
// Fetch latest version from Forgejo raw API
|
|
const fetchUrl = `${repoUrl}/raw/branch/main/.claude-plugin/plugin.json`;
|
|
try {
|
|
const controller = new AbortController();
|
|
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
const res = await fetch(fetchUrl, { signal: controller.signal });
|
|
clearTimeout(timeout);
|
|
|
|
if (!res.ok) process.exit(0);
|
|
|
|
const remote = JSON.parse(await res.text());
|
|
const latestVersion = remote.version;
|
|
if (!latestVersion) process.exit(0);
|
|
|
|
// Write cache
|
|
try {
|
|
mkdirSync(cacheDir, { recursive: true });
|
|
writeFileSync(cachePath, JSON.stringify({ checkedAt: Date.now(), latestVersion }));
|
|
} catch {
|
|
// Cache write failure is non-fatal
|
|
}
|
|
|
|
// Notify if newer
|
|
if (isNewer(latestVersion, installed)) {
|
|
console.log(JSON.stringify({
|
|
systemMessage: `🔄 llm-security v${installed} → v${latestVersion} available. Update: ${repoUrl}`
|
|
}));
|
|
}
|
|
} catch {
|
|
// Network error, timeout, parse error — silent exit
|
|
}
|
|
|
|
process.exit(0);
|
|
}
|