import { type DeviceInput } from './registry.js';
import { type TailscaleNode } from './tailscale.js';
import type { PendingDevice } from './pending.js';
/**
 * The login user to stamp onto newly-synced devices. Tailscale status carries a
 * node's OS and address but NOT the account you ssh in as, so we materialize the
 * local operator's username — tailnet devices are overwhelmingly one person's
 * boxes, and this is exactly the account ssh would already fall back to. Pinning
 * it in the registry makes `--device <device>` dial that account no matter which
 * machine launches the fan-out (a peer whose local user differs otherwise dials
 * the wrong account). Returns undefined when the username isn't a safe ssh
 * identifier, so a weird value never lands in the registry. */
export declare function localLoginUser(): string | undefined;
/**
 * Reduce a raw OS username to a safe ssh account, or undefined. Windows reports
 * the login as `COMPUTER\user` / `DOMAIN\user`; the ssh account is the bare name
 * after the backslash — without this strip the `\` fails the charset guard and
 * Windows boxes never pin a user. Pure, so the platform-specific munging is
 * unit-tested without reading the real OS user. */
export declare function sanitizeLoginUser(raw: string | undefined): string | undefined;
/**
 * Fill in a device's login user during sync WITHOUT ever clobbering an account
 * the user pinned. Precedence: an existing registered user wins; else the local
 * operator's username; else leave it unset (ssh's implicit local default still
 * applies). Pure so the "never overwrite an explicit user" guard is unit-tested
 * without a tailnet. */
export declare function withDefaultUser(input: DeviceInput, prevUser: string | undefined, localUser: string | undefined): DeviceInput;
/**
 * bootstrap — register every non-ignored node (opt-out). First-run `agents
 *   setup` and manual `agents devices sync`, so the fleet is usable out of box.
 * refresh — only refresh reachability of already-registered nodes; a brand-new
 *   node is NOT auto-added, it is surfaced as `pending` for the user to approve
 *   (opt-in). Ongoing autosync and the daemon probe use this, so newcomers flow
 *   through the menu-bar "NEW DEVICES → Register / Ignore" gate instead of
 *   silently landing in the registry.
 */
export type DeviceSyncMode = 'bootstrap' | 'refresh';
export interface DeviceSyncResult {
    /** False when discovery could not run (e.g. tailscale absent) in soft mode. */
    ok: boolean;
    /** Number of tailscale nodes upserted into the registry. */
    synced: number;
    /** Names upserted, for explicit onboarding surfaces to persist approval. */
    syncedNames: string[];
    /** Nodes discovered but neither registered-before nor ignored (name+platform). */
    pending: PendingDevice[];
    /** Populated when ok is false: why discovery was skipped. */
    reason?: string;
}
/**
 * The nodes automatic discovery is allowed to see: sharee nodes (shared INTO
 * the tailnet by another user, e.g. a funnel ingress relay) are not the
 * operator's machines, so they must never be bootstrap-registered nor surfaced
 * as pending. Explicit paths (`devices register <name>`, `devices add`, the
 * `fleet:` manifest bootstrap) are deliberate user actions and stay unfiltered.
 * Pure so the policy is unit-testable without a tailnet.
 */
export declare function discoverableNodes(nodes: TailscaleNode[]): TailscaleNode[];
/**
 * Default checked-state for a node in the interactive sync picker. Pressing
 * Enter registers every checked node, so "checked" must mean "auto-sync would
 * register this": not dismissed, and not a sharee node — unless the user
 * already deliberately registered that sharee node, in which case Enter must
 * keep the fleet as-is. Pure so the default matrix is unit-testable without a
 * prompt.
 */
export declare function defaultPickerChecked(node: TailscaleNode, registered: Set<string>, ignored: Set<string>): boolean;
/**
 * Node names present on the tailnet but neither already in the registry nor on
 * the ignore-list — i.e. genuinely new devices worth surfacing. Pure so the
 * flag matrix is unit-testable without a live tailnet.
 */
export declare function computePendingDevices(nodes: TailscaleNode[], registered: Iterable<string>, ignored: Iterable<string>): string[];
/**
 * Which discovered nodes to upsert this run — the mode-defining decision, pure
 * so it is unit-testable without a tailnet. Ignored nodes are always skipped.
 * In `refresh` mode a node that isn't already registered is skipped too (it
 * stays pending for approval); `bootstrap` includes every non-ignored node.
 */
export declare function selectNodesToUpsert(nodes: TailscaleNode[], registered: Set<string>, ignored: Set<string>, mode: DeviceSyncMode): TailscaleNode[];
/**
 * Ingest `tailscale status --json` into the registry. In soft mode a missing
 * tailscale binary / unreachable daemon resolves to `{ ok: false }` instead of
 * throwing, so callers wiring this into setup/sync never abort the whole run.
 * The `pending` list is computed against the registry state BEFORE this sync so
 * "new" means "not previously registered and not ignored".
 */
export declare function runDeviceSync(opts?: {
    soft?: boolean;
    mode?: DeviceSyncMode;
}): Promise<DeviceSyncResult>;
export interface EnsureDevicesResult {
    /** Names newly resolved from Tailscale and upserted into the registry. */
    registered: string[];
    /** Names that could not be resolved (not on the tailnet / tailscale absent). */
    unresolved: string[];
}
/**
 * Pure decision for `ensureDevicesRegistered`: split the wanted names into those
 * to resolve+register (missing from the registry, present on the tailnet, not
 * ignored) vs. unresolved (missing and either off-tailnet or ignored). Already-
 * registered names need nothing. Pure so the bootstrap's filter matrix is
 * unit-testable without a tailnet or registry writes.
 */
export interface WantedPartition {
    toRegister: string[];
    unresolved: string[];
}
export declare function partitionWantedDevices(wanted: string[], registered: Set<string>, tailscaleNames: Set<string>, ignored: Set<string>): WantedPartition;
/**
 * Fresh-machine bootstrap for `agents apply`: given the device names a `fleet:`
 * manifest wants, register any that aren't in the local registry by resolving
 * them LIVE from Tailscale — so a freshly-cloned `agents.yaml` (which carries
 * names only, never IPs/usernames) reconstructs its roster with zero committed
 * connection details. Soft by design: a missing tailscale binary or an offline
 * name yields `unresolved` rather than throwing, so `apply` degrades to "these
 * devices aren't reachable yet" instead of aborting. Reuses the same
 * parse→withDefaultUser→upsert path as `runDeviceSync`, so a bootstrapped device
 * is identical to a synced one.
 */
export declare function ensureDevicesRegistered(wantedNames: string[]): Promise<EnsureDevicesResult>;
/**
 * The register/remove/ignore decision for the interactive curation picker.
 * Pure so the highest-risk reconcile logic is unit-testable without a tailnet
 * or a live prompt. `keep` is the set the user left checked; everything else is
 * dismissed. Checked => register (and un-ignore if it was ignored). Unchecked
 * => remove from the registry if it was there, and ignore it so auto-sync never
 * re-adds it.
 */
export interface DeviceReconciliation {
    toRegister: string[];
    toUnignore: string[];
    toRemove: string[];
    toIgnore: string[];
}
export declare function planDeviceReconciliation(allNames: Iterable<string>, keep: Iterable<string>, registered: Iterable<string>, ignored: Iterable<string>): DeviceReconciliation;
