import { type BuildAnswer, type Fetch, type UpdateChannel } from "./build-service.js"; import { type NpmRegistryResolver } from "./npm-registry.js"; /** * Live progress of an install, for callers that display it (the auto-update * banner). `installing` covers the binary swap and package-manager runs, which * report no byte count. */ export type UpdateProgress = { phase: "downloading"; version: string; received: number; total: number; } | { phase: "installing"; version: string; }; /** Receives {@link UpdateProgress} while an update installs. */ export type UpdateProgressListener = (progress: UpdateProgress) => void; /** Distribution channel advertised by a release's published npm manifest. */ export type ReleaseDist = "npm" | "binary"; /** npm package names a release installs: the agent package and its natives companion. */ export interface ReleasePackages { pkg: string; natives: string; } /** Parsed `omp.rename` pointer: the new agent package name and optional new natives name. */ export interface ReleaseRename { pkg: string; natives?: string; } export interface ReleaseInfo { tag: string; version: string; /** Parsed `omp.dist` from the registry manifest; undefined when absent. */ dist?: ReleaseDist; /** npm names to install, resolved after following any `omp.rename` pointers. */ packages: ReleasePackages; /** * Registry URL the version was resolved from; bun/npm installs pin to it * so the install sees the same catalog as the check. See #1686. */ registry: string; } /** * Parse the `omp.dist` field from a published package manifest. * * Forward-compatibility contract with future releases: a release that is not * installable as an npm package (e.g. a native rewrite) publishes * `"omp": { "dist": "binary" }` in its package.json. Any value other than * "npm" — including values this updater does not know yet — maps to "binary" * so already-deployed updaters never run a package-manager install against a * release that no longer supports it. */ export declare function resolveReleaseDist(manifest: unknown): ReleaseDist | undefined; /** * Parse the `omp.rename` pointer from a published package manifest. * * Forward-compatibility contract for renaming the npm package: the final * version published under an old name is a stub whose manifest carries * `"omp": { "rename": { "package": "", "natives": "" }, "dist": "binary" }`. * Updaters that understand `rename` follow the pointer and resolve the * release from the renamed package instead ({@link getLatestRelease}); * older deployed updaters ignore it and take the `dist: "binary"` escape * hatch, replacing the install with the standalone release binary rather than * installing the stub via bun/npm. * * The renamed package's own manifest MUST declare `"dist": "npm"` (so * package-manager installs stay package-managed across a major bump) and * MUST continue the old version line (a version reset would compare as * "already up to date" against the running build). */ export declare function resolveReleaseRename(manifest: unknown): ReleaseRename | undefined; /** * Whether the update must bypass bun/npm and install the release binary. * * An explicit `omp.dist` wins in both directions. Without one, a release with * a higher major than the running build is assumed not npm-installable: the * runtime may have changed out from under the package layout, and the pinned * `@oh-my-pi/pi-natives*` companions ({@link buildBunInstallArgs}) may not * exist at that version, which would strand bun/npm-managed installs behind a * hard install failure. Homebrew and mise installs are unaffected — both * already install the standalone release binaries. */ export declare function shouldForceBinaryUpdate(release: { version: string; dist?: ReleaseDist; }, currentVersion?: string): boolean; export interface VerifiedBinaryDownloadOptions { url: string; targetPath: string; expectedSize: number; expectedDigest: string; fetchImpl?: Fetch; /** Bytes received so far, after each chunk. */ onProgress?: (received: number) => void; } /** * Download a file and verify its build-service-reported size and SHA-256 * digest (`sha256:`), leaving it executable. Nothing is left at * `targetPath` when the download fails or does not match. */ export declare function downloadVerifiedBinary(options: VerifiedBinaryDownloadOptions): Promise; /** Result from running the installed binary and parsing its reported version. */ export interface InstalledVersionVerification { ok: boolean; actual?: string; path?: string; } /** Paths and verifier used while replacing a downloaded binary update. */ export interface BinaryReplacementOptions { targetPath: string; tempPath: string; backupPath: string; expectedVersion: string; verifyInstalledVersion: (expectedVersion: string) => Promise; } /** * Parse update subcommand arguments. * Returns undefined if not an update command. */ export declare function parseUpdateArgs(args: string[]): { force: boolean; check: boolean; plugins: boolean; channel?: UpdateChannel; } | undefined; /** * Whether this omp runs from a source checkout (the dev launcher or a `bun link` of * `src/cli.ts`) rather than a compiled binary or a package-manager install, which * always lives under `node_modules`. Checkouts update through git, so update * prompts are noise there. */ export declare function isSourceCheckout(): boolean; /** * Name of the app that manages the omp at `binaryPath`, or undefined when none does. * * A manager (Tern) keeps `manager.json` (`{"manager": "tern", "name": "Tern"}`) two * directories above every binary it installs (`/versions/`, * `/bin/omp.exe`) and updates them itself, so `omp update` must leave them alone. * A missing or unreadable manifest means unmanaged. */ export declare function managedInstallName(binaryPath: string): Promise; /** * Whether the running omp may install its own updates: not a source checkout * (git updates it), not kept by a managing app (Tern updates its omp itself), * and not in the Nix store. Judged from the running build alone, without the * package-manager probes `omp update` runs. */ export declare function canSelfUpdate(): Promise; type UpdateMethod = "brew" | "mise" | "nix" | "bun" | "npm" | "binary"; interface UpdateMethodResolutionOptions { homebrewPrefix?: string; miseBinDirs?: readonly string[]; miseDataDir?: string; npmBinDir?: string; /** Bun's configured global package directory, independent of its bin directory. */ bunGlobalDir?: string; /** * Whether the resolved omp path is a plain file (the standalone binary) * rather than a package-manager symlink. Stops a binary install from being * misrouted to npm/bun when the global bin dir overlaps the installer's * target directory. */ ompIsRegularFile?: boolean; /** * Absolute path named by the bin entry's first symlink hop. This deliberately * preserves a global package symlink instead of resolving into its checkout. */ ompLinkTarget?: string; /** * Whether bun's launcher metadata (`.bunx`) sits beside the resolved * launcher. Bun writes that sidecar next to every `.exe` shim it installs, so * its presence is what makes a regular-file launcher in bun's bin dir * bun-managed rather than a standalone binary that took the launcher over. */ bunShimMarker?: boolean; /** * Whether package-manager routing (bun/npm) is permitted. Binary-only * releases pass `false`: a manager launcher then resolves to `"binary"` and * is taken over in place rather than reinstalled through its manager. Defaults * to `true` in {@link resolveUpdateMethod} so callers that only classify need * not set it. */ allowPackageManagers?: boolean; } type UpdateTarget = { method: "brew"; } | { method: "mise"; } | { method: "nix"; } | { method: "bun"; path?: string; } | { method: "npm"; path?: string; } | { method: "binary"; path: string; replacesSymlink: boolean; validateExistingTarget: boolean; }; export declare function resolveUpdateMethodForTest(ompPath: string, bunBinDir: string | undefined, options?: UpdateMethodResolutionOptions): UpdateMethod; /** Resolve an update target from the concrete PATH entry selected by the shell. */ export declare function resolveUpdateTargetFromPath(ompPath: string, bunBinDir: string | undefined, options: UpdateMethodResolutionOptions & { allowPackageManagers: boolean; }): UpdateTarget; /** * Get the latest release info from the npm registry, following `omp.rename` * pointers ({@link resolveReleaseRename}) when the package has moved to a new * npm name. Version, dist, and install names all come from the final manifest * in the chain. This is the version source of package-manager installs (bun, * npm, Homebrew, mise, Nix); standalone binaries ask the build service * ({@link fetchBuild}) instead. * * The registry comes from the user's npm/bun configuration * ({@link loadNpmRegistryResolver}), so a configured feed is honored. */ export declare function getLatestRelease(options?: { timeoutMs?: number; channel?: UpdateChannel; registries?: NpmRegistryResolver; }): Promise; interface BunInstallCachePruneResult { scannedPackages: number; removedEntries: number; } /** * Prune Bun's package cache so each package keeps only its newest cached version. * * Bun stores package cache entries as both a package marker directory * (`react/19.2.6@@@1`) and a materialized package directory * (`react@19.2.6@@@1`). Global `omp` updates can leave one full copy per * release. The marker and materialized entries are removed together so the * cache stays internally consistent. */ export declare function pruneBunInstallCache(cacheDir: string, packageNames?: Set): Promise; interface BunGlobalInstallLocations { globalDir?: string; globalBinDir?: string; cacheDir?: string; } /** Resolve Bun's global node_modules root from explicit, default, or cache locations. */ export declare function resolveBunGlobalNodeModulesDirFromLocations({ globalDir, globalBinDir, cacheDir, }: BunGlobalInstallLocations): string | undefined; /** * Parse the version a launcher reports from `omp --version` output * (`omp/X.Y.Z`, or a prerelease such as `omp/X.Y.Z-canary.1`). * * The prerelease suffix is preserved so a correctly installed canary build * verifies as up to date instead of appearing to report a stale `X.Y.Z` and * being mistaken for an unreplaced launcher. */ export declare function parseReportedVersion(output: string): string | undefined; /** * Run a specific binary and check if it reports the expected version. */ declare function verifyBinaryAtPath(binaryPath: string, expectedVersion: string): Promise; /** * Run the PATH-resolved omp binary and check if it reports the expected version. */ export declare function verifyInstalledVersion(expectedVersion: string): Promise; /** * Best-effort removal of binary-update leftovers from earlier runs. * * Each self-update writes to `...new` and moves the * previous executable to `...bak` before swapping the * new one in. On Windows a backup cannot be deleted while the updating process * is alive (it is the running process image), so it is left for a later run to * reclaim once its owning process has exited. On macOS the sweep must also * spare a backup that any process still maps as its executable image — * unlinking it would break the live process's TCC permission attribution — so * a live image survives until its process exits. A `.new` temp file or a * `.patch` download (`...patch`) only survives a hard * kill mid-update; it is reaped once older than the download window, which a * live download cannot exceed without timing out and cleaning up after * itself — so a concurrent run's in-progress temp is never deleted. Legacy * fixed `.bak` / `.new` names (from before suffixes were made * unique) are matched too, so users upgrading from a buggy release get the * orphaned files cleaned up. */ export declare function sweepStaleUpdateArtifacts(targetPath: string): Promise; /** * Atomically replace the installed binary and roll back if version verification fails. */ export declare function replaceBinaryForUpdate(options: BinaryReplacementOptions): Promise; /** * Build the bun argv used to globally install a specific omp version. * * The version is selected by querying the resolved registry in * {@link getLatestRelease} ({@link ReleaseInfo.registry}), so the install * MUST observe the same catalog: * * - `--registry=` pins the install to the registry the * check used. That is the user's configured feed when one is set, and * {@link DEFAULT_NPM_REGISTRY} otherwise, so a bun whose own resolution * differs (project bunfig, lagging mirror) cannot reject a version the * check already advertised. * - `--no-cache` tells bun to ignore its on-disk manifest snapshot so it * re-fetches metadata from that registry on every invocation. * * Together these two flags make `omp update` produce exactly the registry * lookup the version check just performed. See #1686. * * Also pins {@link NATIVES_PACKAGE} and the platform-specific * `@oh-my-pi/pi-natives-` leaf to `expectedVersion`. `bun install -g` * does not reliably refresh transitive `optionalDependencies` when the * top-level package is the only one bumped, so the native addon and its * version sentinel can drift out of sync with the freshly installed * `@oh-my-pi/pi-coding-agent` and the loader aborts at * `validateLoadedBindings` on the next launch * (`The .node file on disk is from a different release than this loader`). * Listing the natives explicitly forces bun to replace them in lock-step. * The leaf is added only on tags the release pipeline actually publishes * ({@link SUPPORTED_NATIVE_TAGS}) so unsupported platforms still fail with * the original "no matching version" message instead of `EBADPLATFORM`. * See #1824. * * `--minimum-release-age=0` overrides an ambient bunfig `minimumReleaseAge` * for this invocation: self-update installs the exact pinned manifests the * version check just advertised, so a supply-chain cooldown must not reject * them (same check-vs-installer divergence class as #1686). Older bun * releases predate the flag and reject unknown options, so callers pass * `supportsReleaseAgeOverride` from a help-text probe. */ export declare function buildBunInstallArgs(expectedVersion: string, nativeTag?: string, packages?: ReleasePackages, options?: { registry?: string; supportsReleaseAgeOverride?: boolean; }): string[]; /** * Build the npm argv used to update npm-managed global installs. * Pins `--registry` to the checked registry for the same reason as * {@link buildBunInstallArgs}. * * `force` is set only for rename migrations: npm refuses to write the `omp` * bin while the old package still owns it (`EEXIST`), and the migration * installs the new package BEFORE removing the old one so a failed install * never leaves the user without a working `omp`. */ export declare function buildNpmInstallArgs(expectedVersion: string, nativeTag?: string, packages?: ReleasePackages, flags?: { force?: boolean; registry?: string; }): string[]; /** * Env overlay for npm self-update installs. Setting the same key, * `npm_config_min_release_age: "0"`, outranks any ambient `min-release-age` * or `before` from npmrc files on every npm that supports release-age gates * (env beats file sources; age 0 flattens to no cutoff). Do NOT use * `npm_config_before` here: npm 11.10.0-11.14.x declared `before` and * `min-release-age` mutually exclusive across sources and abort config * loading with a TypeError when one comes from env and the other from an * npmrc file, turning the targeted scenario into a hard npm failure. * npm predating the feature warns "Unknown env config" for the key, so * callers pass `supportsReleaseAgeOverride` from a probe. */ export declare function buildNpmUpdateEnv(base?: Record, supportsReleaseAgeOverride?: boolean): Record; export declare function buildHomebrewUpdateArgs(force: boolean): string[]; /** * Build the attended mise update command. * * `--before 0s` overrides global and per-tool release-age settings for this * invocation. Unlike `MISE_MINIMUM_RELEASE_AGE`, the command option has the * precedence required when the tool entry itself sets `minimum_release_age`. * `--before` is accepted by mise releases with release-age filtering and is * the hidden compatibility name for `--minimum-release-age` in current mise. * Older mise releases predate the option and reject unknown flags, so callers * omit it when the installed command help does not advertise either name. */ export declare function buildMiseUpgradeArgs(supportsReleaseAgeOverride?: boolean): string[]; export declare function buildMiseUpdateEnv(base?: Record): Record; export declare function buildMiseForceInstallArgs(expectedVersion: string): string[]; /** * Old-name globals a rename migration removes after the new install exists: * the set difference between the old install's top-level globals * ({@link buildVersionedPackageInstallArgs} installs the agent, natives core, * and platform leaf explicitly) and the resolved install's. An agent-only * rename keeps the natives names, and removing them would strip the addon * the new install just pinned. */ export declare function buildRenameCleanupPackages(packages: ReleasePackages, nativeTag?: string): string[]; /** Injectable shell steps for {@link migrateRenamedInstall}; commands return process exit codes. */ export interface RenameMigrationSteps { /** Globally install the new package names. MUST be idempotent: re-running re-links the `omp` bin. */ install(): Promise; /** Remove the old-name globals. */ removeOld(): Promise; /** Check the PATH-resolved `omp` against the expected version. */ verify(): Promise; } /** * Migrate a package-manager install across an `omp.rename` hop without a * window where no working `omp` exists: * * 1. Install the new package FIRST. Nothing has been removed yet, so a * failure here leaves the old install fully functional. * 2. Remove the old-name globals. Failure is non-fatal: a stale package * wastes disk, but the bin already points at the new install. * 3. Verify the PATH-resolved `omp`. If the removal deleted the shared bin * link (manager-dependent), re-run the idempotent install to restore it * and verify again; only a repeated failure aborts, with a recovery hint. */ export declare function migrateRenamedInstall(release: ReleaseInfo, steps: RenameMigrationSteps): Promise; /** Injectable steps for {@link updateViaManager}; mirrors {@link RenameMigrationSteps}. */ export interface ManagerUpdateSteps { /** Manager name used in progress and recovery messages. */ manager: string; /** * Run the manager's global install. Resolves to the PATH-resolved launcher * check, or `undefined` when a rename migration already verified and * reported its own result. */ install(): Promise; /** Re-check the PATH-resolved launcher after the install threw. */ verify(): Promise; /** Take `launcherPath` over with the standalone release binary. */ repair(launcherPath: string): Promise; } /** * Run a package-manager self-update, repairing the launcher when the manager * succeeds without updating it or leaves no working launcher behind. * * A global reinstall has to replace files the running process still holds open. * On Windows that is unavoidable — the launcher image, the loaded native addon, * and the package tree being executed are all locked. Bun writes the new * `.bunx` metadata before ignoring EBUSY from the launcher replacement, while * npm can retire its shims before an install fails. * * A successful install whose PATH launcher still reports an older version * means the manager no longer controls that launcher, so it is taken over with * the standalone binary. A newer launcher may have been installed by a * concurrent update and is left untouched. If the install itself failed, a * launcher that still reports a version is preserved and the manager error is * surfaced; only a missing or unusable launcher is repaired. */ export declare function updateViaManager(release: ReleaseInfo, launcherPath: string | undefined, steps: ManagerUpdateSteps): Promise; /** * Applies an HDiffPatch single-stream patch to `oldPath`, writing `outPath`; * `applyBinaryPatch` from pi-natives in production. */ export type BinaryPatchApplier = (oldPath: string, patchPath: string, outPath: string) => Promise; /** Network and patching seams for installing a {@link BuildAnswer}, plus its progress listener. */ export interface BuildDownloadOptions { fetchImpl?: Fetch; applyPatch?: BinaryPatchApplier; onProgress?: UpdateProgressListener; } /** * Install the answered build at `targetPath`, replacing an existing file. The * existing file is the patch base when it is the build the patch starts from. */ export declare function updateViaBinaryAt(targetPath: string, build: BuildAnswer, options?: BuildDownloadOptions & { /** Refuse replacement unless the existing path is a non-script OMP executable. */ validateExistingTarget?: boolean; verifyInstalledVersion?: typeof verifyInstalledVersion; }): Promise; /** * Take over a Windows script-launcher install for a binary-only release. * * npm-managed Windows installs are launched through script shims * (`omp`/`omp.cmd`/`omp.ps1`) that cannot be overwritten with a native * executable. The release binary is installed as `omp.exe` beside them and * the shims are then renamed aside: cmd.exe would already prefer `.exe` via * PATHEXT, but PowerShell resolves `.ps1` first, so the takeover only sticks * once the shims are out of the way. A working launcher exists at every * step — the exe lands before any shim moves, a shim that refuses to move * (a running `.cmd` can be renamed but may be held open some other way) is * rewritten in place as a forwarder to the exe, and a failed version * verification moves everything back. */ export declare function updateViaShimTakeover(shimPath: string, build: BuildAnswer, options?: BuildDownloadOptions & { verifyBinary?: typeof verifyBinaryAtPath; }): Promise; /** Options for {@link runUpdateCommand}: the `omp update` flags, plus a progress listener for background runs. */ export interface UpdateOptions { force: boolean; check: boolean; /** Switch to and persist this channel; unset keeps `update.channel`. */ channel?: UpdateChannel; onProgress?: UpdateProgressListener; } /** How {@link runUpdateCommand} ended without an error. */ export type UpdateOutcome = /** Nothing newer to install. */ { kind: "current"; } /** `--check` found `version`; nothing was installed. */ | { kind: "available"; version: string; } /** This install cannot update itself (a managing app, Nix, a channel its installer lacks). */ | { kind: "unsupported"; reason: string; } /** The installer ran for `version`; whether the launcher now reports it was printed, not checked. */ | { kind: "installed"; version: string; }; /** * Run the update command. * * The installer that owns the PATH `omp` decides where the version comes * from: a standalone binary asks the build service, a package-manager install * asks npm. When no target resolves, the running build's kind decides, so * `--check` still answers; installing then fails on the unresolved target. * * Progress and results print to stdout. Throws when the check or the install * fails, or when another update holds the install lock. */ export declare function runUpdateCommand(opts: UpdateOptions): Promise; /** * Print update command help. */ export declare function printUpdateHelp(): void; export {};