/** * Single install point for placing a file or directory at its destination. * * Every appearance of data on a live path goes through it — both on the * server and locally on download. It rests on one invariant: * * A whole copy exists at every point in time. Nothing is deleted until * the replacement has succeeded. The error handler never touches the * last remaining copy. * * Hence the order of work: data is always written next to the target under * a temporary name, verified there, and moved into place with a single * rename. Anything that fails before that rename cleans up only the * temporary path. Anything that fails after it is already a warning, not * a failure: the replacement has taken effect. * * The protocol itself does not know where the data lives. File operations * come from outside: transport commands on the server, plain fs locally. */ export type PathKind = 'file' | 'directory' | 'symlink' | 'missing'; /** * Outcome of a mount-point check. `unknown` means the server did not answer * with device numbers: there was nothing to check with, which is not the * same as a checked "not a mount point". */ export type MountCheck = 'separate' | 'same' | 'unknown'; /** File operations the protocol is built from */ export interface PathOps { /** * What currently sits at the path. A symlink is a distinct kind: a broken * link is invisible to both `test -e` and `test -d`, and without this * distinction the replacement would fail with a baffling "path does not * exist". */ inspect(path: string): Promise; /** Create the parent directory if it does not exist */ ensureParent(path: string): Promise; /** * Rename. Must behave like `mv -T`: never nest into an occupied target, * refuse instead. Plain `mv` of a directory onto a directory nests it * inside and reports success, on both BusyBox and coreutils. */ rename(from: string, to: string): Promise; /** Remove the path entirely */ removeTree(path: string): Promise; /** Whether the path sits on a separate filesystem (a mount point) */ isSeparateFilesystem?(path: string): Promise; /** * Paths in the directory that resemble our temporary names. * * Needed only to report them to a human: they cannot be cleaned up * automatically, because the name alone cannot tell a stray leftover * apart from another call's temporary path that is filling in right now. */ listArtifacts?(directory: string): Promise; } /** What was found next to the target, and whether the list is complete */ export interface ArtifactScan { paths: string[]; /** The server's response was cut off at the limit: there may be more leftovers */ truncated?: boolean; } export interface InstallPlan { /** The path the caller asked for */ finalPath: string; kind: 'file' | 'directory'; /** * Write the data to a temporary path. * * `existing` is what the survey found at the target: staging that keeps * part of the target has to know whether there is anything to keep. */ stage: (stagingPath: string, existing: PathKind) => Promise; /** Verify the temporary path before the replacement: a rejection reason, or null */ verify?: (stagingPath: string) => Promise; /** * Permissions and ownership after the replacement; a failure here does not * undo it. What could not be applied comes back as warnings rather than * through a variable the caller keeps outside: a value written from inside * a callback is read as an answer even when the callback never ran. */ finalize?: (finalPath: string) => Promise; } export interface InstallOutcome { path: string; /** What went wrong after the replacement had already taken effect */ warnings: string[]; } export declare class InstallError extends Error { /** * What a human must read alongside the failure: where their data ended * up. Without this field, a warning like "the live path is empty, the * copy sits next to it at X" would be lost in exactly the case it was * written for. */ readonly warnings: string[]; constructor(message: string, warnings?: string[], cause?: unknown); } /** * Put data in place. * * Returns warnings instead of swallowing them: "the file was replaced but * permissions did not apply" is a different answer than "everything * succeeded", and different again from "the operation failed". */ export declare function install(ops: PathOps, plan: InstallPlan): Promise;