import { type UpdateFetchLike, type UpdateFileIo } from '../self-update.js'; /** The daemon's own repository, the only place daemon binaries are published. */ export declare const DAEMON_REPO_RELEASES_LATEST_URL = "https://github.com/mgd34msu/goodvibes-daemon/releases/latest"; /** `https://github.com/o/r/releases/download/` for the daemon repository. */ export declare function daemonReleaseDownloadBaseUrl(tag: string): string; /** * The first daemon version released from the daemon's own repository. A binary * below this carries a compiled-in releases URL naming a repository that does * not publish daemon assets; a binary at or above it resolves its own repository * without help and must be left alone. */ export declare const DAEMON_SPLIT_FLOOR_VERSION = "1.28.0"; /** How long the `--version` probe is allowed to take before it counts as unreadable. */ export declare const DAEMON_VERSION_PROBE_TIMEOUT_MS = 5000; /** Injectable so tests never spawn a real binary. */ export type RunCommandLike = (command: string, args: readonly string[]) => { readonly status: number | null; readonly stdout: string; }; /** * Pulls the version out of `goodvibes-daemon --version` output, which the daemon * prints as `goodvibes-daemon 1.27.1`. Deliberately strict: only a dotted * numeric version is accepted, so a binary that printed a usage error, a stack * trace, or nothing at all reads as unidentifiable rather than being coerced * into a number that would justify replacing it. */ export declare function parseDaemonVersionOutput(output: string): string | null; /** Runs ` --version` and returns the parsed version, or null. */ export declare function readInstalledDaemonVersion(binaryPath: string, runCommand?: RunCommandLike): string | null; /** True when `version` predates the daemon's own repository. */ export declare function isPreSplitDaemonVersion(version: string, floorVersion?: string): boolean; export type DaemonHandoverSkipReason = /** The operator turned launch-time binary swapping off. */ 'disabled' /** No `goodvibes-daemon` binary was found beside this install. */ | 'no-daemon-binary' /** The daemon is package-manager managed; swapping it in place would fight the package manager. */ | 'not-swappable-install' /** The binary did not answer `--version` with something identifiable. */ | 'version-unreadable' /** The installed daemon already comes from its own repository. */ | 'already-split' /** No daemon assets are published for this platform/arch. */ | 'unsupported-platform'; export type DaemonHandoverDecision = { readonly action: 'skip'; readonly reason: DaemonHandoverSkipReason; } | { readonly action: 'handover'; readonly binaryPath: string; readonly fromVersion: string; readonly assetName: string; }; export interface DaemonHandoverDecisionInput { /** The resolved `goodvibes-daemon` path, or null when none was found. */ readonly binaryPath: string | null; /** The version that binary reported, or null when it could not be read. */ readonly installedVersion: string | null; readonly platform: NodeJS.Platform; readonly arch: string; /** Defaults to {@link DAEMON_SPLIT_FLOOR_VERSION}. */ readonly floorVersion?: string; } /** * The whole decision, as a pure function of what was observed. Every skip * carries the reason it skipped so the caller can say which one happened * instead of reporting a single undifferentiated "nothing to do". */ export declare function decideDaemonHandover(input: DaemonHandoverDecisionInput): DaemonHandoverDecision; /** * Tracks whether the swap has begun, so a budget that runs out can say what is * actually true: cancelled before anything was touched, or the file WAS * replaced and only the restart is outstanding. Reads leave the flag alone. */ export interface DaemonHandoverProgress { begun: boolean; tag: string | null; } export declare function createDaemonHandoverProgress(): DaemonHandoverProgress; /** The failure an aborted handover ends with, raised only while nothing has been written. */ export declare const HANDOVER_ABORTED_MESSAGE = "daemon handover cancelled before any file was replaced"; export interface PerformDaemonHandoverOptions { readonly fetchImpl: UpdateFetchLike; /** The daemon binary to replace. */ readonly binaryPath: string; /** The release asset that replaces it. */ readonly assetName: string; readonly signal?: AbortSignal; readonly progress?: DaemonHandoverProgress; /** Injectable so tests never touch a real file. */ readonly io?: UpdateFileIo; readonly releasesLatestUrl?: string; readonly downloadBaseUrl?: (tag: string) => string; /** Defaults to {@link DAEMON_SPLIT_FLOOR_VERSION}. */ readonly floorVersion?: string; } /** * Resolves the daemon repository's current release, verifies the daemon asset * against that release's SHA256SUMS.txt, and swaps it into place with the * outgoing binary kept at `.previous`. * * The floor is re-checked against the RESOLVED tag, not only against the * installed version: a handover exists to move a daemon onto its own release * line, and a release below the split floor is not on that line. Without this * check an unexpected tag would be installed simply because it was newer than * what was there. */ export declare function performDaemonHandover(options: PerformDaemonHandoverOptions): Promise<{ readonly tag: string; }>; export interface DaemonRestartOutcome { /** True when a restart was actually issued and the service manager accepted it. */ readonly restarted: boolean; readonly unitName: string; /** What happened, in the words the receipt uses. */ readonly detail: string; } /** * The daemon's service name as this host has it: the configured override when * one is set, the installer's managed name otherwise. */ export declare function resolveHandoverServiceName(configManager: { get(key: string): unknown; }): string; /** * Restarts the handed-over daemon so the RUNNING process is the new binary. * A swap replaces the file; the process that opened the old inode keeps running * it until it is restarted, so without this the handover would be true on disk * and false in memory. * * Only systemd user units are restarted here, and only when the unit is * genuinely active. On every other platform and posture this reports honestly * that the new binary takes effect on the daemon's next start rather than * printing a command that would not work. */ export declare function restartHandedOverDaemon(platform: NodeJS.Platform, configManager: { get(key: string): unknown; }, runCommand?: RunCommandLike): DaemonRestartOutcome; export type DaemonHandoverOutcome = { readonly action: 'skipped'; readonly reason: DaemonHandoverSkipReason; } | { readonly action: 'handed-over'; readonly fromVersion: string; readonly toTag: string; readonly restarted: boolean; readonly unitName: string; } /** The budget ran out before anything was written; the download was cancelled. */ | { readonly action: 'deferred'; } /** The budget ran out after the swap began; the binary WAS replaced. */ | { readonly action: 'swapped-needs-restart'; readonly toTag: string; } | { readonly action: 'failed'; readonly detail: string; }; export interface RunDaemonHandoverOptions { readonly fetchImpl: UpdateFetchLike; /** The resolved `goodvibes-daemon` path, or null when none was found. */ readonly binaryPath: string | null; readonly platform: NodeJS.Platform; readonly arch: string; readonly configManager: { get(key: string): unknown; }; readonly print: (line: string) => void; /** Defaults to {@link DAEMON_SPLIT_FLOOR_VERSION}. */ readonly floorVersion?: string; /** Injectable so tests observe the probe instead of spawning a binary. */ readonly runCommand?: RunCommandLike; /** Injectable so tests observe the swap instead of replacing a real file. */ readonly performHandover?: (options: PerformDaemonHandoverOptions) => Promise<{ readonly tag: string; }>; readonly restartDaemon?: typeof restartHandedOverDaemon; readonly io?: UpdateFileIo; readonly timeoutMs?: number; } /** Default budget for the whole handover, generous (a daemon binary is large) but bounded. */ export declare const DAEMON_HANDOVER_TIMEOUT_MS = 90000; /** * The complete handover: probe, decide, swap, restart, report. Never throws, a * surface's launch is never held hostage by the state of the daemon beside it. */ export declare function runDaemonHandover(options: RunDaemonHandoverOptions): Promise; //# sourceMappingURL=daemon-handover.d.ts.map