Skip to content

Commit 748c5fd

Browse files
grichaclaude
andcommitted
Add auto-update checker
Checks npm registry for newer versions and prompts user to update. Caches check results for 24 hours to avoid spamming. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 7b46306 commit 748c5fd

3 files changed

Lines changed: 97 additions & 1 deletion

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@gricha/perry",
3-
"version": "0.1.4",
3+
"version": "0.1.5",
44
"description": "Self-contained CLI for spinning up Docker-in-Docker development environments with SSH and proxy helpers.",
55
"type": "module",
66
"bin": {

src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
import { loadAgentConfig, getConfigDir, ensureConfigDir } from './config/loader';
1818
import { buildImage } from './docker';
1919
import { DEFAULT_AGENT_PORT, WORKSPACE_IMAGE_LOCAL } from './shared/constants';
20+
import { checkForUpdates } from './update-checker';
2021

2122
const program = new Command();
2223

@@ -513,4 +514,6 @@ function formatUptime(seconds: number): string {
513514
return parts.join(' ');
514515
}
515516

517+
checkForUpdates(pkg.version);
518+
516519
program.parse();

src/update-checker.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { homedir } from 'os';
2+
import { join } from 'path';
3+
import { mkdir, readFile, writeFile } from 'fs/promises';
4+
5+
const PACKAGE_NAME = '@gricha/perry';
6+
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
7+
8+
interface UpdateCache {
9+
lastCheck: number;
10+
latestVersion: string | null;
11+
}
12+
13+
async function getCacheDir(): Promise<string> {
14+
const dir = join(homedir(), '.config', 'perry');
15+
await mkdir(dir, { recursive: true });
16+
return dir;
17+
}
18+
19+
async function readCache(): Promise<UpdateCache | null> {
20+
try {
21+
const cacheFile = join(await getCacheDir(), 'update-cache.json');
22+
const content = await readFile(cacheFile, 'utf-8');
23+
return JSON.parse(content);
24+
} catch {
25+
return null;
26+
}
27+
}
28+
29+
async function writeCache(cache: UpdateCache): Promise<void> {
30+
try {
31+
const cacheFile = join(await getCacheDir(), 'update-cache.json');
32+
await writeFile(cacheFile, JSON.stringify(cache));
33+
} catch {
34+
// Ignore cache write errors
35+
}
36+
}
37+
38+
async function fetchLatestVersion(): Promise<string | null> {
39+
try {
40+
const response = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
41+
signal: AbortSignal.timeout(3000),
42+
});
43+
if (!response.ok) return null;
44+
const data = (await response.json()) as { version?: string };
45+
return data.version || null;
46+
} catch {
47+
return null;
48+
}
49+
}
50+
51+
function compareVersions(current: string, latest: string): number {
52+
const currentParts = current.split('.').map(Number);
53+
const latestParts = latest.split('.').map(Number);
54+
55+
for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
56+
const c = currentParts[i] || 0;
57+
const l = latestParts[i] || 0;
58+
if (l > c) return 1;
59+
if (l < c) return -1;
60+
}
61+
return 0;
62+
}
63+
64+
export async function checkForUpdates(currentVersion: string): Promise<void> {
65+
try {
66+
const cache = await readCache();
67+
const now = Date.now();
68+
69+
let latestVersion: string | null = null;
70+
71+
if (cache && now - cache.lastCheck < CHECK_INTERVAL_MS) {
72+
latestVersion = cache.latestVersion;
73+
} else {
74+
latestVersion = await fetchLatestVersion();
75+
await writeCache({ lastCheck: now, latestVersion });
76+
}
77+
78+
if (latestVersion && compareVersions(currentVersion, latestVersion) > 0) {
79+
console.log('');
80+
console.log(`\x1b[33m╭─────────────────────────────────────────────────────────╮\x1b[0m`);
81+
console.log(
82+
`\x1b[33m│\x1b[0m Update available: \x1b[90m${currentVersion}\x1b[0m → \x1b[32m${latestVersion}\x1b[0m \x1b[33m│\x1b[0m`
83+
);
84+
console.log(
85+
`\x1b[33m│\x1b[0m Run \x1b[36mnpm install -g ${PACKAGE_NAME}\x1b[0m to update \x1b[33m│\x1b[0m`
86+
);
87+
console.log(`\x1b[33m╰─────────────────────────────────────────────────────────╯\x1b[0m`);
88+
console.log('');
89+
}
90+
} catch {
91+
// Silently ignore update check errors
92+
}
93+
}

0 commit comments

Comments
 (0)