feat(update): opt-in background auto-update (AGENTHUB_AUTO_UPDATE)

Set AGENTHUB_AUTO_UPDATE=1 and the install keeps itself current with no
manual `agenthub update` — ideal for the Windows client.

maybeNotifyUpdate now:
- auto mode: refreshes the behind-count hourly (vs 24h) and, when behind,
  spawns a detached `agenthub update` (fetch → reset → install → build)
  that doesn't block the current command; the next invocation runs the
  new version. A 10-min cooldown stamp prevents a second build spawning
  while one is in flight.
- otherwise: unchanged "run `agenthub update`" hint.

Safe to enable anywhere: `update` no-ops when current and refuses to
discard unpushed local commits, so the authoring machine is protected.
Cross-platform (no OS scheduler needed). Bump 0.2.1 -> 0.2.2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-06-27 17:00:31 +02:00
parent 353c139796
commit b0973a878c
3 changed files with 71 additions and 19 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "agenthub", "name": "agenthub",
"version": "0.2.1", "version": "0.2.2",
"description": "Local coordination layer for AI coding agents", "description": "Local coordination layer for AI coding agents",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",

View File

@ -5,6 +5,17 @@ import { dirname, join, parse } from 'node:path';
import { homedir } from 'node:os'; import { homedir } from 'node:os';
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
// In auto-update mode we refresh more often so a freshly pushed version is
// picked up within the hour, and we cool down between background updates so we
// never spawn a second reset+build while one is still running.
const AUTO_CHECK_INTERVAL_MS = 60 * 60 * 1000;
const AUTO_UPDATE_COOLDOWN_MS = 10 * 60 * 1000;
/** Opt-in: set AGENTHUB_AUTO_UPDATE=1 to self-update in the background. */
function autoUpdateEnabled(): boolean {
const v = process.env.AGENTHUB_AUTO_UPDATE;
return !!v && v !== '0' && v.toLowerCase() !== 'false';
}
/** Locate the agenthub package install dir by walking up from this file. */ /** Locate the agenthub package install dir by walking up from this file. */
function findPackageRoot(): string | undefined { function findPackageRoot(): string | undefined {
@ -118,15 +129,50 @@ export async function update(rootOverride?: string): Promise<void> {
console.log(`AgentHub updated: ${before}${packageVersion(root)} (now at ${gitCapture(root, ['rev-parse', '--short', 'HEAD'])}).`); console.log(`AgentHub updated: ${before}${packageVersion(root)} (now at ${gitCapture(root, ['rev-parse', '--short', 'HEAD'])}).`);
} }
function checkStampPath(): string { function stampPath(name: string): string {
return join(homedir(), '.agenthub', 'last-update-check'); return join(homedir(), '.agenthub', name);
}
function readStamp(path: string): number {
try {
return Number(readFileSync(path, 'utf-8')) || 0;
} catch {
return 0;
}
}
function writeStamp(path: string): void {
try {
mkdirSync(dirname(path), { recursive: true });
writeFileSync(path, String(Date.now()), 'utf-8');
} catch {
// best-effort
}
} }
/** /**
* Non-blocking, throttled update hint. Prints a one-line notice to stderr when * Spawn a detached `agenthub update` that fetches + (if behind) resets,
* the install is behind its remote, then kicks off a detached background fetch * reinstalls and rebuilds without blocking the current command. The next
* so the next run is accurate. Never throws a failed check must never break * invocation runs the new version. `update` no-ops when already current and
* a command. * refuses to discard unpushed local commits, so this is safe to fire
* automatically (e.g. on the authoring machine).
*/
function spawnBackgroundUpdate(root: string): void {
try {
const entry = join(root, 'bin', 'agenthub.js');
spawn(process.execPath, [entry, 'update'], { cwd: root, detached: true, stdio: 'ignore' }).unref();
} catch {
// best-effort
}
}
/**
* Non-blocking, throttled update check. Kicks off a detached background fetch so
* the behind-count stays fresh, then either:
* - AGENTHUB_AUTO_UPDATE set self-updates in the background when behind
* (cooled down so concurrent builds can't pile up); or
* - otherwise prints a one-line "run `agenthub update`" hint.
* Never throws a failed check must never break a command.
*/ */
export function maybeNotifyUpdate(rootOverride?: string): void { export function maybeNotifyUpdate(rootOverride?: string): void {
try { try {
@ -134,18 +180,13 @@ export function maybeNotifyUpdate(rootOverride?: string): void {
if (!root || !existsSync(join(root, '.git'))) return; if (!root || !existsSync(join(root, '.git'))) return;
const branch = currentBranch(root); const branch = currentBranch(root);
const auto = autoUpdateEnabled();
const stamp = checkStampPath(); const stamp = stampPath('last-update-check');
let last = 0; const interval = auto ? AUTO_CHECK_INTERVAL_MS : CHECK_INTERVAL_MS;
if (Date.now() - readStamp(stamp) > interval) {
writeStamp(stamp);
try { try {
last = Number(readFileSync(stamp, 'utf-8')) || 0;
} catch {
// no stamp yet
}
if (Date.now() - last > CHECK_INTERVAL_MS) {
try {
mkdirSync(dirname(stamp), { recursive: true });
writeFileSync(stamp, String(Date.now()), 'utf-8');
spawn('git', ['fetch', '--quiet', 'origin', branch], { cwd: root, detached: true, stdio: 'ignore' }).unref(); spawn('git', ['fetch', '--quiet', 'origin', branch], { cwd: root, detached: true, stdio: 'ignore' }).unref();
} catch { } catch {
// best-effort background refresh // best-effort background refresh
@ -158,7 +199,18 @@ export function maybeNotifyUpdate(rootOverride?: string): void {
} catch { } catch {
return; return;
} }
if (behind !== '0') { if (behind === '0') return;
if (auto) {
// Only one background update per cooldown window — a second command while
// the reset+build is in flight must not spawn a competing update.
const autoStamp = stampPath('last-auto-update');
if (Date.now() - readStamp(autoStamp) > AUTO_UPDATE_COOLDOWN_MS) {
writeStamp(autoStamp);
process.stderr.write(`\n🔄 AgentHub: updating in background (${behind} commit(s) behind)…\n\n`);
spawnBackgroundUpdate(root);
}
} else {
process.stderr.write(`\n🔄 A newer AgentHub is available (${behind} commit(s) behind). Run \`agenthub update\`.\n\n`); process.stderr.write(`\n🔄 A newer AgentHub is available (${behind} commit(s) behind). Run \`agenthub update\`.\n\n`);
} }
} catch { } catch {

View File

@ -112,7 +112,7 @@ async function runRemote(serverUrl: string, fn: () => Promise<void>): Promise<vo
export function createProgram(cwd: string): Command { export function createProgram(cwd: string): Command {
const program = new Command('agenthub') const program = new Command('agenthub')
.description('Local coordination layer for AI coding agents') .description('Local coordination layer for AI coding agents')
.version('0.2.1') .version('0.2.2')
.option('--server <url>', 'AgentHub server URL (env: AGENTHUB_SERVER)'); .option('--server <url>', 'AgentHub server URL (env: AGENTHUB_SERVER)');
program program