From b0973a878c52bfb3f5d20fb83ae5b4fe065d6c18 Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Sat, 27 Jun 2026 17:00:31 +0200 Subject: [PATCH] feat(update): opt-in background auto-update (AGENTHUB_AUTO_UPDATE) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- package.json | 2 +- src/cli/commands/update.ts | 86 ++++++++++++++++++++++++++++++-------- src/cli/index.ts | 2 +- 3 files changed, 71 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 865037e..de05337 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agenthub", - "version": "0.2.1", + "version": "0.2.2", "description": "Local coordination layer for AI coding agents", "type": "module", "main": "./dist/index.js", diff --git a/src/cli/commands/update.ts b/src/cli/commands/update.ts index 96c0232..bd49dfe 100644 --- a/src/cli/commands/update.ts +++ b/src/cli/commands/update.ts @@ -5,6 +5,17 @@ import { dirname, join, parse } from 'node:path'; import { homedir } from 'node:os'; 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. */ function findPackageRoot(): string | undefined { @@ -118,15 +129,50 @@ export async function update(rootOverride?: string): Promise { console.log(`AgentHub updated: ${before} → ${packageVersion(root)} (now at ${gitCapture(root, ['rev-parse', '--short', 'HEAD'])}).`); } -function checkStampPath(): string { - return join(homedir(), '.agenthub', 'last-update-check'); +function stampPath(name: string): string { + 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 - * the install is behind its remote, then kicks off a detached background fetch - * so the next run is accurate. Never throws — a failed check must never break - * a command. + * Spawn a detached `agenthub update` that fetches + (if behind) resets, + * reinstalls and rebuilds — without blocking the current command. The next + * invocation runs the new version. `update` no-ops when already current and + * 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 { try { @@ -134,18 +180,13 @@ export function maybeNotifyUpdate(rootOverride?: string): void { if (!root || !existsSync(join(root, '.git'))) return; const branch = currentBranch(root); + const auto = autoUpdateEnabled(); - const stamp = checkStampPath(); - let last = 0; - try { - last = Number(readFileSync(stamp, 'utf-8')) || 0; - } catch { - // no stamp yet - } - if (Date.now() - last > CHECK_INTERVAL_MS) { + const stamp = stampPath('last-update-check'); + const interval = auto ? AUTO_CHECK_INTERVAL_MS : CHECK_INTERVAL_MS; + if (Date.now() - readStamp(stamp) > interval) { + writeStamp(stamp); 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(); } catch { // best-effort background refresh @@ -158,7 +199,18 @@ export function maybeNotifyUpdate(rootOverride?: string): void { } catch { 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`); } } catch { diff --git a/src/cli/index.ts b/src/cli/index.ts index afbb2bc..b6ae076 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -112,7 +112,7 @@ async function runRemote(serverUrl: string, fn: () => Promise): Promise', 'AgentHub server URL (env: AGENTHUB_SERVER)'); program