/**
 * Fleet-wide device operations — pick online targets and run a command on each.
 *
 * Used by `agents fleet update` / `agents fleet run` (aliases of the same
 * subcommands under `agents devices`). Offline devices are skipped with a
 * reason so a single dead node never blocks the rest of the rollout. Per-device
 * throws (misconfigured auth, etc.) become `failed` rows — they never abort
 * the remaining devices.
 */
import type { DeviceProfile, DeviceRegistry } from './registry.js';
import type { DeviceStats } from './health.js';
export type FleetSkipReason = 'offline' | 'no-address';
/** npm dist-tags / semver pins only — rejects shell metacharacters. */
export declare const FLEET_VERSION_RE: RegExp;
export interface FleetTarget {
    device: DeviceProfile;
    /** When set, this device is not reached (skip with reason). */
    skip?: FleetSkipReason;
}
export interface FleetRunResult {
    name: string;
    status: 'ok' | 'failed' | 'skipped';
    code: number | null;
    reason?: FleetSkipReason | string;
    /** Truncated combined stderr/stdout for failures. */
    detail?: string;
}
export interface FanOutDeviceTarget {
    name: string;
    skip?: FleetSkipReason | string;
}
export interface FanOutDeviceResult<T> {
    name: string;
    status: 'ok' | 'failed' | 'skipped';
    value?: T;
    error?: string;
    reason?: FleetSkipReason | string;
}
/**
 * Classify each registered device for a fleet operation.
 *
 * - Tailscale-offline → skip `offline`
 * - No address → skip `no-address`
 * - Everything else is a target (including this machine, reached over ssh when
 *   it has a registry address — same path as any other box).
 */
export declare function planFleetTargets(reg: DeviceRegistry): FleetTarget[];
/**
 * Remote fan-out targets for the fleet health/drift gates (`fleet status`,
 * `doctor --check --devices`): every planned device except this machine.
 * Offline / no-address devices are kept — those are genuine faults a gate should
 * surface — so their `skip` reason still flows through as an `unreachable` row.
 */
export declare function remoteFleetTargets(planned: FleetTarget[], self: string): FleetTarget[];
/**
 * Decide whether a fleet-health target should skip the expensive version+doctor
 * dials (`agents fleet status`). The cheap stats probe (~2.5s, same registry
 * address) has already tried this box one step earlier. If it came back
 * unreachable, dialing `agents --version` (15s) + `agents doctor --json` (30s)
 * would almost certainly fail the same way — just 45s slower — so one
 * genuinely-offline box would stall the whole matrix (the ~60s hang, RUSH-1964).
 *
 * We trust the stats verdict on the DEFAULT path, not only under
 * `--refresh`/`--live`: `probeDeviceStats` and the version/doctor dials share
 * one ssh path (`fleetDialTarget`), so reachability is not per-probe. The
 * verdict is either freshly probed this run or daemon-warmed (~3min) with the
 * live write-back from RUSH-1965, so a box that came online in the last few
 * minutes is the only false-negative window — it renders `unreachable` until the
 * next stats warm, which beats letting it hang the status glance for 45s.
 *
 * An existing skip (offline/no-address from {@link planFleetTargets})
 * always wins — those are classified before any probe.
 */
export declare function fleetHealthSkip(currentSkip: FleetSkipReason | string | undefined, stats: DeviceStats | undefined): FleetSkipReason | string | undefined;
/** Human label for a skip reason. */
export declare function skipLabel(reason: FleetSkipReason): string;
/**
 * Run `cmd` on one device via the same ssh path as `agents ssh <name> …`.
 * Captures stdout/stderr (not inherited) so the fleet table can summarize.
 * Throws from buildSshInvocation are returned as a non-zero result so a single
 * misconfigured device cannot abort the fleet loop.
 */
export declare function runOnDevice(device: DeviceProfile, cmd: string[], opts?: {
    timeoutMs?: number;
}): {
    code: number | null;
    stdout: string;
    stderr: string;
};
/**
 * Run `cmd` on THIS machine directly — no ssh. Used by {@link runFleet} for the
 * self target: a box frequently can't ssh to itself (no self-authorized key, as
 * `agents fleet update` hit trying to reach zion from zion) and doesn't need to —
 * `agents upgrade` etc. runs identically as a local process. Mirrors
 * {@link runOnDevice}'s return shape and never throws. The argv is space-joined
 * and evaluated by a shell — matching the POSIX-shell ssh path (so PATH-resolved
 * `agents`, quoting, and `;`/`&&` behave the same). It does NOT replicate the
 * powershell-device encoding runOnDevice uses (`connect.ts` base64 path): a
 * Windows self runs under the default OS shell (cmd.exe), which still resolves
 * `agents` on PATH for the only self commands that matter (`agents upgrade …`).
 */
export declare function runLocalCommand(cmd: string[], opts?: {
    timeoutMs?: number;
}): {
    code: number | null;
    stdout: string;
    stderr: string;
};
/**
 * Build `agents upgrade --yes` argv, optionally pinned to a version/dist-tag.
 * Rejects anything that is not a plain npm version/tag token so a version pin
 * cannot inject shell metacharacters into the remote command line.
 */
export declare function upgradeCommand(version?: string): string[];
export interface RunFleetOptions {
    /**
     * Name of THIS machine. Its target runs the command **locally** (no ssh) — a
     * box can't reliably ssh to itself and doesn't need to. Omit to ssh every
     * target (the old behaviour). Callers pass `machineId()`.
     */
    self?: string;
    /** Injectable ssh runner (tests). */
    runner?: typeof runOnDevice;
    /** Injectable local runner (tests). */
    localRunner?: typeof runLocalCommand;
}
/**
 * Execute a command across planned targets. Pure orchestration over
 * {@link runOnDevice} / {@link runLocalCommand}; testable by injecting either.
 * The `self` target runs locally so `agents fleet update` upgrades this machine
 * too instead of failing to ssh to itself. Per-device throws from a runner are
 * recorded as `failed` so one bad device never aborts the rest.
 */
export declare function runFleet(targets: FleetTarget[], cmd: string[], opts?: RunFleetOptions): FleetRunResult[];
export interface FanOutDeviceOptions {
    /**
     * Per-device deadline in milliseconds. When set, any probe that does not
     * settle within this window is abandoned via `Promise.race` against a
     * rejection timer and recorded as a `failed` result with the message
     * `'timed out'`. There is no AbortController — the underlying probe
     * continues running in the background; cancellation of the in-flight work
     * is the caller's responsibility. In practice `probeRemoteAuth` relies on
     * `sshExecAsync`'s own 15 s timer to kill the ssh child independently.
     * The per-device ssh timeout passed directly to {@link sshExecAsync} is
     * the first line of defence; this acts as a hard backstop so one slow
     * device can never stall the entire fan-out past its budget.
     */
    perDeviceTimeoutMs?: number;
}
/** Run one async probe per device in parallel, preserving input order. */
export declare function fanOutDevices<T, Target extends FanOutDeviceTarget = FanOutDeviceTarget>(targets: Target[], probe: (target: Target) => Promise<T>, opts?: FanOutDeviceOptions): Promise<FanOutDeviceResult<T>[]>;
