fix: stale update banner + skill sync dirty worktree crash (#358)
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline failed

This commit was merged in pull request #358.
This commit is contained in:
2026-04-03 02:04:05 +00:00
parent 381b0eed7b
commit 0cf80dab8c
4 changed files with 62 additions and 29 deletions

View File

@@ -122,10 +122,18 @@ export function semverLt(a: string, b: string): boolean {
// ─── Cache ──────────────────────────────────────────────────────────────────
function readCache(): UpdateCheckResult | null {
/** Cache stores only the latest registry version (the expensive network call).
* The installed version is always checked fresh — it's a local `npm ls`. */
interface RegistryCache {
latest: string;
checkedAt: string;
registry: string;
}
function readCache(): RegistryCache | null {
try {
if (!existsSync(CACHE_FILE)) return null;
const raw = JSON.parse(readFileSync(CACHE_FILE, 'utf-8')) as UpdateCheckResult;
const raw = JSON.parse(readFileSync(CACHE_FILE, 'utf-8')) as RegistryCache;
const age = Date.now() - new Date(raw.checkedAt).getTime();
if (age > CACHE_TTL_MS) return null;
return raw;
@@ -134,10 +142,10 @@ function readCache(): UpdateCheckResult | null {
}
}
function writeCache(result: UpdateCheckResult): void {
function writeCache(entry: RegistryCache): void {
try {
mkdirSync(CACHE_DIR, { recursive: true });
writeFileSync(CACHE_FILE, JSON.stringify(result, null, 2) + '\n', 'utf-8');
writeFileSync(CACHE_FILE, JSON.stringify(entry, null, 2) + '\n', 'utf-8');
} catch {
// Best-effort — cache is not critical
}
@@ -174,29 +182,40 @@ export function getLatestVersion(): string {
}
/**
* Perform an update check — uses cache when fresh, otherwise hits registry.
* Perform an update check — uses registry cache when fresh, always checks
* installed version fresh (local npm ls is cheap, caching it causes stale
* "update available" banners after an upgrade).
* Never throws.
*/
export function checkForUpdate(options?: { skipCache?: boolean }): UpdateCheckResult {
const current = getInstalledVersion();
let latest: string;
let checkedAt: string;
if (!options?.skipCache) {
const cached = readCache();
if (cached) return cached;
if (cached) {
latest = cached.latest;
checkedAt = cached.checkedAt;
} else {
latest = getLatestVersion();
checkedAt = new Date().toISOString();
writeCache({ latest, checkedAt, registry: REGISTRY });
}
} else {
latest = getLatestVersion();
checkedAt = new Date().toISOString();
writeCache({ latest, checkedAt, registry: REGISTRY });
}
const current = getInstalledVersion();
const latest = getLatestVersion();
const updateAvailable = !!(current && latest && semverLt(current, latest));
const result: UpdateCheckResult = {
return {
current,
latest,
updateAvailable,
checkedAt: new Date().toISOString(),
updateAvailable: !!(current && latest && semverLt(current, latest)),
checkedAt,
registry: REGISTRY,
};
writeCache(result);
return result;
}
/**