@stackmemoryai/stackmemory
Version:
Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.
219 lines (218 loc) • 6.72 kB
JavaScript
import { fileURLToPath as __fileURLToPath } from 'url';
import { dirname as __pathDirname } from 'path';
const __filename = __fileURLToPath(import.meta.url);
const __dirname = __pathDirname(__filename);
import { execFileSync } from "child_process";
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
import { join } from "path";
import { homedir } from "os";
import { logger } from "../monitoring/logger.js";
import {
ErrorCode,
getErrorMessage,
wrapError
} from "../errors/index.js";
import { withTimeout, gracefulDegrade } from "../errors/recovery.js";
class UpdateChecker {
static CACHE_FILE = join(
homedir(),
".stackmemory",
"update-check.json"
);
static CHECK_INTERVAL = 24 * 60 * 60 * 1e3;
// 24 hours
static PACKAGE_NAME = "@stackmemoryai/stackmemory";
/**
* Check for updates and display notification if needed
*/
static async checkForUpdates(currentVersion, silent = false) {
try {
const cache = this.loadCache();
const now = Date.now();
if (cache && now - cache.lastChecked < this.CHECK_INTERVAL) {
if (!silent && cache.latestVersion && cache.latestVersion !== currentVersion) {
this.displayUpdateNotification(currentVersion, cache.latestVersion);
}
return;
}
const latestVersion = await this.fetchLatestVersion();
this.saveCache({
lastChecked: now,
latestVersion,
currentVersion
});
if (!silent && latestVersion && this.isNewerVersion(currentVersion, latestVersion)) {
this.displayUpdateNotification(currentVersion, latestVersion);
}
} catch (error) {
const wrappedError = wrapError(
error,
"Update check failed",
ErrorCode.INTERNAL_ERROR,
{ currentVersion, silent }
);
logger.debug("Update check failed:", {
error: getErrorMessage(error),
context: wrappedError.context
});
}
}
/**
* Fetch latest version from npm registry
*/
static async fetchLatestVersion() {
try {
const fetchVersion = async () => {
const output = execFileSync("npm", ["view", this.PACKAGE_NAME, "version"], {
encoding: "utf-8",
stdio: ["pipe", "pipe", "ignore"],
timeout: 5e3
// 5 second timeout
}).trim();
return output;
};
return await gracefulDegrade(
() => withTimeout(fetchVersion, 5e3, "npm registry timeout"),
"",
{ operation: "fetchLatestVersion", package: this.PACKAGE_NAME }
);
} catch (error) {
const wrappedError = wrapError(
error,
"Failed to fetch latest version from npm",
ErrorCode.SERVICE_UNAVAILABLE,
{ package: this.PACKAGE_NAME }
);
logger.debug("Failed to fetch latest version:", {
error: getErrorMessage(error),
context: wrappedError.context
});
return "";
}
}
/**
* Compare version strings
*/
static isNewerVersion(current, latest) {
try {
const currentParts = current.split(".").map(Number);
const latestParts = latest.split(".").map(Number);
if (currentParts.some(isNaN) || latestParts.some(isNaN)) {
logger.debug("Invalid version format:", { current, latest });
return false;
}
for (let i = 0; i < 3; i++) {
const latestPart = latestParts[i] ?? 0;
const currentPart = currentParts[i] ?? 0;
if (latestPart > currentPart) return true;
if (latestPart < currentPart) return false;
}
return false;
} catch (error) {
logger.debug("Version comparison failed:", {
error: getErrorMessage(error),
current,
latest
});
return false;
}
}
/**
* Display update notification
*/
static displayUpdateNotification(current, latest) {
console.log("\n" + "\u2500".repeat(60));
console.log("\u{1F4E6} StackMemory Update Available!");
console.log(` Current: v${current}`);
console.log(` Latest: v${latest}`);
console.log("\n Update with:");
console.log(" npm install -g @stackmemoryai/stackmemory@latest");
console.log("\u2500".repeat(60) + "\n");
}
/**
* Load update cache
*/
static loadCache() {
try {
if (!existsSync(this.CACHE_FILE)) {
return null;
}
const data = readFileSync(this.CACHE_FILE, "utf-8");
const cache = JSON.parse(data);
if (typeof cache.lastChecked !== "number" || typeof cache.latestVersion !== "string" || typeof cache.currentVersion !== "string") {
logger.debug("Invalid cache format, ignoring");
return null;
}
return cache;
} catch (error) {
const wrappedError = wrapError(
error,
"Failed to load update cache",
ErrorCode.INTERNAL_ERROR,
{ cacheFile: this.CACHE_FILE }
);
logger.debug("Failed to load update cache:", {
error: getErrorMessage(error),
context: wrappedError.context
});
return null;
}
}
/**
* Save update cache
*/
static saveCache(cache) {
try {
const dir = join(homedir(), ".stackmemory");
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true, mode: 493 });
}
const tempFile = `${this.CACHE_FILE}.tmp`;
writeFileSync(tempFile, JSON.stringify(cache, null, 2), {
mode: 420
});
if (existsSync(this.CACHE_FILE)) {
writeFileSync(this.CACHE_FILE, JSON.stringify(cache, null, 2));
} else {
writeFileSync(this.CACHE_FILE, JSON.stringify(cache, null, 2));
}
} catch (error) {
const wrappedError = wrapError(
error,
"Failed to save update cache",
ErrorCode.INTERNAL_ERROR,
{ cacheFile: this.CACHE_FILE, cache }
);
logger.debug("Failed to save update cache:", {
error: getErrorMessage(error),
context: wrappedError.context
});
}
}
/**
* Force check for updates (ignores cache)
*/
static async forceCheck(currentVersion) {
try {
const latestVersion = await this.fetchLatestVersion();
this.saveCache({
lastChecked: Date.now(),
latestVersion,
currentVersion
});
if (latestVersion) {
if (this.isNewerVersion(currentVersion, latestVersion)) {
this.displayUpdateNotification(currentVersion, latestVersion);
} else {
console.log(`\u2705 StackMemory is up to date (v${currentVersion})`);
}
}
} catch (error) {
console.error("\u274C Update check failed:", error.message);
}
}
}
export {
UpdateChecker
};
//# sourceMappingURL=update-checker.js.map