/** * CLI self-update — the first step of `celilo system update` per * CELILO_UPDATE D2. * * Compares the running CLI version against the latest published * `@celilo/cli` on npm and runs `bun update -g @celilo/cli` if newer. * On success the caller re-execs with the new binary so subsequent * module work uses the new code. * * Both the version-check and the bun-update invocation are * dependency-injected so the orchestrator can be tested against * canned responses without touching npm or the running shell. */ import { compareSemver } from '../audit/cli-version'; import type { SelfUpdateResult } from './types'; export type CliVersionFetcher = () => Promise; export type CliUpdater = () => Promise<{ ok: boolean; stderr: string }>; export interface SelfUpdateDeps { /** Currently running CLI version (read from package.json). */ installedVersion: string; /** Resolves the latest @celilo/cli on npm; returns null on transport failure. */ fetcher: CliVersionFetcher; /** Runs `bun update -g @celilo/cli` (or equivalent). */ updater: CliUpdater; /** * If true, skip self-update entirely. Set when `system update` * detects the CLI is running from local source (cele2e dev loop) * — there's nothing to update against and any npm install would * step on the mounted source tree. */ devMode?: boolean; } export async function performSelfUpdate(deps: SelfUpdateDeps): Promise { if (deps.devMode) { return { performed: false, reason: 'dev-mode' }; } const latest = await deps.fetcher(); if (latest === null) { return { performed: false, reason: 'no-network' }; } if (compareSemver(deps.installedVersion, latest) >= 0) { return { performed: false, reason: 'already-current' }; } const result = await deps.updater(); if (!result.ok) { throw new Error(`bun update failed: ${result.stderr || 'unknown error'}`); } return { performed: true, from: deps.installedVersion, to: latest }; }