/**
 * `olam upgrade` — self-upgrade the local Olam dev stack.
 *
 * Runs the full manual sequence in one command:
 *   git fetch + git pull --ff-only
 *   npm install
 *   npm run build
 *   vite build (SPA)
 *   bash build-host-cp.sh (image)
 *   docker compose up -d --force-recreate
 *   wait for /health
 */
import type { Command } from 'commander';
import { type DockerImpl, type ImageDigests } from './bootstrap.js';
export interface UpgradeOpts {
    readonly yes: boolean;
    readonly skipImage: boolean;
    readonly skipInstall: boolean;
    readonly branch: string | null;
    /** Phase 2b — B1: roll back to :olam-rollback set instead of upgrading. */
    readonly rollback: boolean;
    /** Phase 2b — B2: bypass HEAD-drift refusal at swap boundary. */
    readonly force: boolean;
    /** Phase 2b — B3: pass --no-cache through to all three build scripts. */
    readonly noCache: boolean;
    /** Phase 2c — C2: read upgrade history instead of running an upgrade. */
    readonly history: boolean;
    /** Phase 2c — C2: --history -n <N>; --history --json. */
    readonly historyN: number;
    readonly historyJson: boolean;
    /**
     * Phase C — C6: opt into the legacy source-build path
     * (`bash build-*.sh` invoked through `resolveBuildScript()`). Only
     * works when isDevMode() is true. Default false → CLI takes the
     * pull-by-digest path that operators on installed CLIs need.
     */
    readonly fromSource: boolean;
    /**
     * Phase 1b — C2/D14: refresh manifests in ~/.olam/k8s/manifests/ during
     * a kubernetes-substrate upgrade. Requires --accept-security-regression
     * when security-sensitive fields (securityContext, capabilities, etc.) differ.
     */
    readonly forceRefreshManifests: boolean;
    /** Phase 1b — C2/D14: accept security-sensitive field regressions in manifests. */
    readonly acceptSecurityRegression: boolean;
}
/**
 * Check whether node_modules is in sync with package-lock.json.
 *
 * npm writes node_modules/.package-lock.json after every successful install.
 * If its mtime is >= package-lock.json's mtime, the last install ran after
 * the last lockfile change — node_modules is current.
 *
 * Returns true  → skip install (node_modules reflects the current lockfile).
 * Returns false → run install (lockfile changed or marker absent).
 */
export declare function isNodeModulesInSync(cwd: string): boolean;
/**
 * Decide whether the npm install step should be skipped.
 *
 * Priority:
 *   1. --skip-install flag → always skip
 *   2. node_modules in sync with lockfile → skip (log reason)
 *   3. otherwise → run install
 */
export declare function shouldSkipInstall(opts: {
    skipInstall: boolean;
}, cwd: string): {
    skip: true;
    reason: string;
} | {
    skip: false;
};
/** Check cwd looks like the olam repo root. */
export declare function validateRepoRoot(cwd: string): {
    ok: true;
} | {
    ok: false;
    error: string;
};
/** Normalise raw Commander option object into typed opts. */
export declare function parseUpgradeOpts(raw: {
    yes?: boolean;
    skipImage?: boolean;
    skipInstall?: boolean;
    branch?: string;
    rollback?: boolean;
    force?: boolean;
    noCache?: boolean;
    history?: boolean;
    n?: string | number;
    json?: boolean;
    fromSource?: boolean;
    forceRefreshManifests?: boolean;
    acceptSecurityRegression?: boolean;
}): UpgradeOpts;
/**
 * Extract the JS bundle hash from index.html.
 * Looks for the canonical Vite output pattern: `/assets/index-<hash>.js`.
 */
export declare function extractBundleHash(indexHtml: string): string | null;
/**
 * Capture HEAD SHA via `git rev-parse HEAD`. Returns null on failure.
 *
 * Phase 2a — A2: must be invoked AFTER `git pull --ff-only` so the captured
 * SHA reflects the state we're upgrading TO (not the pre-pull state). The
 * pull's whole purpose is to advance HEAD; capturing before would refuse the
 * CLI's own pull as drift at A6's swap-boundary check.
 *
 * The returned SHA is sticky for the rest of the run (no per-step re-reads);
 * A6 / B4 re-read once at the swap boundary to detect operator-driven mid-flight
 * `git checkout` / `git reset` that happen DURING the build window.
 */
export declare function captureHeadSha(cwd: string): string | null;
/** Abbreviate a 40-char SHA to 8 chars for human-readable output. */
export declare function abbreviateSha(sha: string): string;
/**
 * Check whether a docker image tag exists locally (Phase 2b — B1).
 *
 * Uses `docker image inspect` which exits 0 only when ALL specified
 * images exist locally. Single-image variant for the rollback pre-flight.
 */
export declare function imageExists(tag: string): boolean;
/**
 * Pre-flight check for `olam upgrade --rollback` (Phase 2b — B1).
 *
 * Verifies that all three `:olam-rollback` tags exist. Returns an error
 * message naming the missing image(s) when any are absent — typically
 * the first-upgrade case where no prior canonical existed for one or
 * more components, leaving the rollback set incoherent (see audit A6-001).
 *
 * Returns null when all three are present (rollback is safe to proceed).
 */
export declare function checkRollbackSetExists(plan: ReadonlyArray<SwapPlan>): string | null;
/**
 * Per-image smoke result (Phase 2a — A5).
 *
 * `docker create + docker inspect` validates the freshly-built `:olam-next`
 * image is well-formed AND its baked OLAM_BUILD_SHA label matches the
 * captured target SHA. Catches build-corrupt cases (compile errors hidden,
 * missing files, etc.) before A6's atomic swap touches canonical tags.
 *
 * Does NOT bind ports (docker create only allocates the container's
 * filesystem; doesn't start the entrypoint), so it's safe to run while
 * the existing host-cp/auth containers are running on their canonical ports.
 *
 * Runtime startup bugs are caught later by /api/version/status post-recreate
 * (A8), per the local-dev/dogfood priority axis (logic correctness > security
 * hardening).
 */
export interface SmokeResult {
    readonly image: string;
    readonly ok: boolean;
    readonly bakedSha: string | null;
    readonly error?: string;
}
export declare function smokeImage(image: string, targetSha: string): SmokeResult;
/**
 * Per-image swap mapping (Phase 2a — A6).
 *
 * Auth uses `:local` (NOT `:latest`) per AuthContainerController.DEFAULT_IMAGE.
 * Host-cp + devbox use `:latest`. The transient `:olam-next` and rollback
 * `:olam-rollback` namespaces are uniform across all three.
 */
export interface SwapPlan {
    readonly transient: string;
    readonly canonical: string;
    readonly rollback: string;
}
export declare const PRODUCTION_SWAP_PLAN: ReadonlyArray<SwapPlan>;
/** Result of a single `docker tag` op. */
interface DockerTagResult {
    readonly ok: boolean;
    readonly error?: string;
}
/**
 * Run `docker tag <source> <dest>`. Returns ok=false with stderr trimmed
 * on failure (e.g. source image absent). No retry — caller decides.
 *
 * Per audit A6-003: spawnSync may throw synchronously under fork pressure
 * (libuv clone(2) failures). The try/catch ensures performAtomicSwap can
 * always proceed to its summary phase — a thrown exception escaping
 * dockerTag would leak the upgrade lock and produce no SwapResult, which
 * confuses both the operator AND Phase 2b's --rollback recovery path.
 */
export declare function dockerTag(source: string, dest: string): DockerTagResult;
/**
 * Per-image swap result. The two phases (rollback-save + canonical-advance)
 * are tracked separately so SIGKILL-recovery paths can diagnose partial state.
 */
export interface SwapStepResult {
    readonly image: string;
    /** Did rollback-save (canonical → :olam-rollback) succeed? false on first-upgrade — non-fatal. */
    readonly rollbackSaved: boolean;
    /** Did canonical-advance (:olam-next → canonical) succeed? Fatal if false. */
    readonly canonicalAdvanced: boolean;
    readonly rollbackError?: string;
    readonly canonicalError?: string;
}
export interface SwapResult {
    readonly ok: boolean;
    readonly steps: ReadonlyArray<SwapStepResult>;
    readonly partialAdvance: boolean;
    /**
     * True when ALL three :olam-rollback tags were successfully written before
     * any canonical-advance was attempted. False when at least one Phase-1
     * rollback-save failed (typically first-upgrade where the canonical didn't
     * exist; Phase 2b's --rollback pre-flight detects empty-or-partial
     * rollback set and refuses).
     *
     * Per audit A6-001: the partial-Phase-1 + Phase-2-fail combination would
     * otherwise emit a misleading "run --rollback" hint when the rollback set
     * is INCOHERENT and `--rollback` would either partially restore or refuse.
     * The summary string branches on this flag.
     */
    readonly rollbackCoherent: boolean;
    readonly summary: string;
}
/**
 * Atomic-ish 3-image set swap.
 *
 * Six sequential `docker tag` ops in two phases:
 *
 *   Phase 1 (rollback-save):
 *     1. canonical → :olam-rollback (image 1)
 *     2. canonical → :olam-rollback (image 2)
 *     3. canonical → :olam-rollback (image 3)
 *
 *   Phase 2 (canonical-advance):
 *     4. :olam-next → canonical (image 1)
 *     5. :olam-next → canonical (image 2)
 *     6. :olam-next → canonical (image 3)
 *
 * Invariants:
 *
 * - **First-upgrade tolerance**: any of steps 1-3 may fail with "no such
 *   image" if the operator has never had a canonical-tagged image. Those
 *   failures are NON-FATAL (recorded in rollbackError but not aborted) —
 *   `:olam-rollback` simply doesn't exist for that image; Phase 2b's
 *   `--rollback` pre-flight detects this and refuses.
 *
 * - **Canonical-advance fatality**: any failure in steps 4-6 is fatal.
 *   The swap is partially advanced; canonical tags are now mixed (some
 *   at SHA-Y, some at SHA-X). Operator runs `olam upgrade --rollback`
 *   (Phase 2b) which uses the FULL `:olam-rollback` set written in Phase 1
 *   to restore coherent prior state.
 *
 * - **SIGKILL recovery**: if killed during Phase 1, partial `:olam-rollback`
 *   exists but canonical is intact — operator's next `olam upgrade` succeeds
 *   normally (the partial `:olam-rollback` is overwritten by the next
 *   successful run). If killed during Phase 2, canonical is mixed —
 *   operator must `olam upgrade --rollback` to recover.
 *
 * The "atomic-ish" qualifier: `docker tag` is per-image atomic (POSIX rename
 * of a symbolic name), but the SET of 3 canonical tags is updated sequentially
 * across ~1s wall-clock. Sub-second window is acceptable for solo-dev/dogfood
 * per the plan's local-dev/dogfood priority axis.
 */
export declare function performAtomicSwap(plan: ReadonlyArray<SwapPlan>): SwapResult;
/**
 * Inverse of performAtomicSwap — restore canonical from :olam-rollback
 * (Phase 2b — B1). Three sequential `docker tag` ops:
 *
 *   docker tag olam-auth:olam-rollback     olam-auth:local
 *   docker tag olam-devbox:olam-rollback   olam-devbox:latest
 *   docker tag olam-host-cp:olam-rollback  olam-host-cp:latest
 *
 * No two-phase ceremony — the source `:olam-rollback` set is already a
 * coherent prior-good captured by a previous successful `olam upgrade`,
 * so we don't need to preserve current canonical (it's known-broken,
 * which is why we're rolling back).
 *
 * Caller MUST pre-flight via `checkRollbackSetExists()` before invoking.
 * Behavior on missing source is per-image fatal (returns ok=false +
 * error naming the missing image).
 */
export declare function performRollbackSwap(plan: ReadonlyArray<SwapPlan>): {
    ok: boolean;
    results: ReadonlyArray<{
        image: string;
        ok: boolean;
        error?: string;
    }>;
    summary: string;
};
/**
 * Snapshot of /api/version/status response (Phase 1 contract).
 *
 * Mirrors the VersionSnapshot type from packages/host-cp/src/version-status.mjs.
 * Kept inline (no shared module import) to avoid CLI ↔ host-cp import cycles.
 */
export interface VersionSnapshot {
    readonly hostCp: {
        readonly running: string;
        readonly latest: string;
        readonly upgradeAvailable: boolean;
    };
    readonly authService: {
        readonly running: string;
        readonly latest: string;
        readonly upgradeAvailable: boolean;
    };
    readonly devbox: {
        readonly running: string;
        readonly latest: string;
        readonly upgradeAvailable: boolean;
    };
    readonly operatorHead: string;
    readonly checkedAt: string;
}
/**
 * Poll /api/version/status until all three component `.running` SHAs match
 * `targetSha`, or until `timeoutMs` elapses. Returns the final snapshot.
 *
 * Phase 2a — A8: this is the success criterion for the entire upgrade.
 * After A6's atomic swap + A7's recreate, the new images should report
 * the new SHA via OLAM_BUILD_SHA baked at build time. Round-trip through
 * Phase 1's detection path closes the loop.
 *
 * Returns:
 *   - { matched: true, snapshot } when all three SHAs equal targetSha within timeout.
 *   - { matched: false, snapshot } when timeout expires; caller decides
 *     whether to warn (recreate succeeded but propagation slow) or error.
 *   - { matched: false, snapshot: null } when /api/version/status is
 *     unreachable for the entire timeout (host-cp didn't come back up).
 */
export declare function waitForVersionMatch(targetSha: string, timeoutMs?: number, pollIntervalMs?: number): Promise<{
    matched: boolean;
    snapshot: VersionSnapshot | null;
}>;
/**
 * Format a version-snapshot mismatch into a readable per-component diff
 * (Phase 2a — A8). Used in the timeout-warn path so the operator sees
 * which component is lagging.
 */
export declare function formatVersionMismatch(targetSha: string, snapshot: VersionSnapshot | null): string;
/**
 * Phase C — C6: default `olam upgrade` flow for installed CLIs.
 *
 * Mirrors `bootstrap.ts`'s parallel-pull-by-digest pattern, then swaps
 * the running stack to the freshly-pulled images via
 * `docker compose -f packages/host-cp/compose.yaml up -d --force-recreate`
 * (host-cp + docker-socket-proxy) plus AuthContainerController for the
 * auth-service container (which does not live in the compose file —
 * matches the recreate path used elsewhere in this module).
 *
 * Reuses `pullImageWithRetry`, `loadImageDigests`, `realDocker`,
 * `checkProtocolOverlap`, and `parseProtocolVersionsLabel` so retry +
 * coalesce + handshake semantics stay identical to bootstrap. NOT a
 * reimplementation — every primitive is imported.
 *
 * Exit codes:
 *   - 0 — stack swapped to new digests successfully.
 *   - EXIT_BOOTSTRAP_PULL_FAILED (3) — at least one image pull failed.
 *   - EXIT_PROTOCOL_MISMATCH (4) — handshake refused on no-overlap.
 *   - EXIT_GENERIC_ERROR (1) — daemon unreachable, recreate failed,
 *     auth recreate failed, etc.
 */
export interface UpgradePullResult {
    readonly exitCode: number;
    readonly summary: string;
}
export interface UpgradePullDeps {
    readonly docker?: DockerImpl;
    readonly digests?: ImageDigests;
    readonly registry?: string;
    /**
     * Compose driver. Default: real `runCompose` from host-cp.ts. Tests
     * inject a stub to avoid spawning `docker compose`.
     */
    readonly runComposeImpl?: (args: readonly string[], composeFile: string, extraEnv: Record<string, string>) => {
        ok: boolean;
        stdout: string;
        stderr: string;
    };
    /**
     * Auth-service recreate driver. Default: stop + rm + controller.start().
     * Tests inject a stub so AuthContainerController doesn't spawn docker.
     */
    readonly recreateAuth?: () => Promise<{
        ok: boolean;
        error?: string;
    }>;
    /**
     * mcp-auth-service recreate driver. Default: stop + rm + start the
     * mcp-auth container. Only invoked when the mcp-auth digest is present
     * in the digests manifest — older tarballs without the entry skip this
     * step entirely (tracker test "exits 0 and skips mcp-auth recreate when
     * digest absent"). Tests inject a stub.
     */
    readonly recreateMcpAuth?: () => Promise<{
        ok: boolean;
        error?: string;
    }>;
    /**
     * Path to packages/host-cp/compose.yaml. Defaults to findComposeFile()
     * (bundled path first, then monorepo cwd fallbacks).
     * Tests inject a tmp path to avoid depending on a real compose.yaml.
     */
    readonly composeFile?: string;
    /** Operator-state auth secret. Default: readAuthSecret(). */
    readonly authSecret?: string | null;
    /**
     * Docker-tag driver. Default: real `dockerTag()` (spawnSync). Tests
     * inject a stub so the swap step doesn't depend on a docker daemon
     * being present.
     */
    readonly tagImpl?: (from: string, to: string) => {
        ok: boolean;
        error?: string;
    };
    /**
     * Container-inspect driver — used by the pre-rename orphan guard before
     * compose recreate (Step 5b). Returns the container's compose project /
     * service labels when the container exists; reports `exists: false`
     * when the daemon says "no such object". Default: real `docker inspect`
     * via spawnSync. Tests inject a stub to simulate a pre-rename orphan
     * (PR #596/#597 host-cp → olam-host-cp service rename collision).
     */
    readonly inspectContainerImpl?: (containerName: string) => {
        ok: boolean;
        exists: boolean;
        project: string;
        service: string;
        stderr: string;
    };
    /**
     * Container-remove driver — paired with inspectContainerImpl above.
     * Default: real `docker rm -f <name>` via spawnSync. Tests inject a
     * stub to record removal calls without spawning docker.
     */
    readonly removeContainerImpl?: (containerName: string) => {
        ok: boolean;
        stderr: string;
    };
}
export declare function runUpgradePullByDigest(deps?: UpgradePullDeps): Promise<UpgradePullResult>;
export declare function registerUpgrade(program: Command): void;
export {};
//# sourceMappingURL=upgrade.d.ts.map