/** * Locked, validated, backed-up JSON writes for the app-owned config files. * * Four invariants, in order (ADR-0026): * * 1. **Cross-process mutual exclusion.** Every write holds ZCode's own file * lock, so a settings write never discards a change the desktop app is * making at the same moment. * 2. **Read-modify-write, never rewrite.** The mutator receives the parsed * current document and returns the next one, so keys this module knows * nothing about survive byte-for-byte. The same file carries skills, MCP, * hooks, plugins and agent-runtime settings owned by different writers. * 3. **Self-validation before the bytes land.** `validate(encode(next))` * must succeed; a candidate that cannot round-trip is refused, because the * failure mode otherwise is the backend silently degrading to an empty * overlay while the user watches their models disappear. * 4. **Backup then atomic rename.** The previous content is copied to * `.bak-` (newest 5 kept) before a temp file is renamed * over the target. A crash mid-write leaves either the old file or the new * one, never a torn document. * * A corrupt existing file REFUSES the write instead of being treated as `{}`: * silently rewriting a damaged document from an empty base would destroy * whatever is still recoverable in it. * * Scope of invariant 1, precisely: ZCode takes this lock for * `provider_config.json`, `credentials.json` and the builtin-provider config, * so those three are genuinely mutually excluded against a running desktop app. * `cli/config.json` is NOT locked by the app's own writer (it does a bare * temp+rename), so for that file the lock only excludes other BRIDGE * processes — concurrent edits still lose one side's change, exactly as they * do between the app's own multiple processes. Invariants 2-4 hold everywhere. */ import { type FileLockOptions } from "./file-lock.js"; /** How many timestamped backups to keep per file. */ export declare const BACKUP_KEEP = 5; /** Serialization for the config files this module owns: 2-space, trailing newline. */ export declare function encodeJson(value: unknown): string; /** * Read + parse a config file. * * @returns the parsed document, or `null` when the file is absent. * @throws when the file exists but is not a JSON object — the caller must * refuse the write rather than base it on an empty document. */ export declare function readJsonDocument(file: string): Promise | null>; export interface WriteJsonOptions extends FileLockOptions { /** * Serializer for the bytes that land on disk. Defaults to * {@link encodeJson} (2-space + trailing newline); pass a custom one for * files whose canonical form differs. */ encode?: (doc: Record) => string; /** * Round-trip check on the candidate document. Return `false` (or throw) to * refuse the write; nothing is backed up or renamed in that case. */ validate?: (doc: Record) => boolean | string; /** Skip the pre-write backup (used by the restore path, which re-backs-up). */ skipBackup?: boolean; } export interface WriteJsonResult { /** The document actually written. */ doc: Record; /** Backup path taken before the write, when one existed. */ backup?: string; } /** * Read-modify-write one config file under the file lock. * * @param mutator receives the parsed current document (an empty object when * the file is absent) and returns the next document. Returning the same * reference is a no-op that still validates and writes. * @throws on a corrupt existing file, a failed validation, or a lock timeout. */ export declare function writeJsonAtomic(file: string, mutator: (current: Record) => Record | Promise>, options?: WriteJsonOptions): Promise; /** * Write a non-JSON text file (agent markdown) atomically. * * The config files are JSON and go through `writeJsonAtomic`; agent markdown is * a different format but has the same crash-safety requirement — a plain * `writeFile` truncates in place, so a crash mid-write leaves a torn file with * no backup to recover from. Same shape as the JSON path minus the JSON encode * and validation: backup, temp file in the same directory, rename. * * No file lock: ZCode's own writer for `~/.zcode/agents/*.md` does not take the * config lock, so taking it here would buy mutual exclusion only against other * bridge processes while adding contention the app never has. */ export declare function writeTextAtomic(file: string, content: string, options?: { skipBackup?: boolean; }): Promise; /** List the backups currently held for a config file, newest first. */ export declare function listBackups(file: string): Promise>; /** * Rebuild an ISO timestamp from the flattened backup stamp. * * `new Date().toISOString()` yields `2026-09-22T14:10:00.000Z`; the backup name * flattens `:` and `.` to `-`, giving `2026-09-22T14-10-00-000Z`. The date half * keeps its dashes, so only the trailing four segments are restored. */ export declare function backupStampToIso(stamp: string): string; //# sourceMappingURL=atomic-write.d.ts.map