/** * Edit-preserving writes for what init ships into a project. * * Files (skills, agents, commands, helpers), keyed by project-relative path * against the sha256 recorded in `.monomind/init-manifest.json` `files`: * - missing, or already identical to the new content: written / left; * - exactly what monomind left there last time: an untouched install, so it * is replaced with the new version; * - different from what monomind left: the user edited it. It is kept, the * new version is written beside it as `.monomind-new`, and the run * reports it. `--force` does not change this; * - never recorded (installed before hashes existed): provenance unknown. A * file that matches the new version once ownership-marker lines and blank * lines are ignored is adopted. Otherwise it is kept like an edit, unless * the caller replaces unrecorded files (`init --force`, and `init upgrade` * for helpers), in which case it is first copied to the run's backup * directory `.monomind/backups//`. * Hashes are taken at the end of the run (`finalize`), because platform * adapters and doctor rewrite some of these files after they are copied. * * Managed blocks (CLAUDE.md, AGENTS.md, GEMINI.md, shared_instructions.md), * keyed `#` in `blocks`: a block whose body differs from what * monomind last wrote is left alone with a warning — or, under `--force`, * backed up and replaced, and the run says so. */ import type { ManagedBlockForm } from '../platform-adapters/types.js'; import type { InitResult } from './types.js'; export declare const NEW_VERSION_SUFFIX = ".monomind-new"; /** Backup directories kept by pruneBackups (retirement ones are never pruned). */ export declare const BACKUPS_KEPT = 5; export interface FileGuardOptions { /** Replace a differing file that has no recorded hash (after backing it up). */ replaceUnrecorded: boolean; /** `--force`: replace an edited managed block (after backing it up). */ force?: boolean; /** `--if-missing`: never write over a file that already exists, no matter * its content — no `.monomind-new`, no adoption, no merge. Every such * path is recorded in `skippedExisting` instead of `kept`. */ ifMissing?: boolean; } export type GuardOutcome = 'written' | 'unchanged' | 'kept'; export declare class FileGuard { readonly targetDir: string; private readonly options; /** Project-relative paths kept because the user edited them. */ readonly kept: string[]; /** Project-relative paths left alone under `--if-missing` because they * already existed — reported in the run's `skipped`, not `kept`. */ readonly skippedExisting: string[]; /** Files `guardBlock` has already merged a block into THIS run — lets a * second block writer (e.g. a platform adapter, after `writeClaudeMd` * just created CLAUDE.md fresh) keep adding to a file this same run * created, while `--if-missing` still blocks one that predates the run. */ private readonly blockWrittenFiles; /** Messages the run must show the user. */ readonly warnings: string[]; readonly backupDir: string; private readonly files; private readonly blocks; private readonly touched; constructor(targetDir: string, options: FileGuardOptions); private rel; /** Absolute paths of the files kept this run, for installs that must skip them. */ keptPaths(): Set; private backup; /** Write `content` to `dest` unless that would overwrite a user edit. */ write(dest: string, content: string | Buffer, mode?: number): GuardOutcome; /** An unrecorded file may be replaced: it is ours modulo markers, or the * caller replaces unrecorded files and a backup was taken first. */ private adoptable; /** Record the hash of a file a writer just wrote without the guard (the * converted .opencode/.kimi-code mirrors), so `cleanup` can prove it is * an untouched install. Hashed at `finalize`, like everything else. */ record(file: string): void; copyFile(src: string, dest: string): GuardOutcome; /** copyDirRecursive through the guard. Returns whether any file inside was * actually written (new or changed) — false when every file already * existed and matched (or, under `--if-missing`, simply already existed). * `skip` leaves out any source file or directory it returns true for. */ copyDir(src: string, dest: string, skip?: (srcPath: string) => boolean): boolean; /** * The content to write for `file` with `generated` merged into its * `marker` block, or null when the block was edited and must be kept. */ mergeBlock(file: string, existing: string, marker: string, generated: string): string | null; /** * mergeBlock for a block of any delimiter form (platform adapters write * `monomind:start ` blocks), keyed and recorded the same way. */ guardBlock(file: string, existing: string, marker: string, generated: string, form: ManagedBlockForm): string | null; /** Record hashes for what was written; call once every writer has run. */ finalize(): void; /** Persist hashes now (a writer called outside a full run has no finalize). */ flush(): void; } /** The run's guard, created on first use. */ export declare function guardFor(targetDir: string, options: { force?: boolean; preserveEdits?: boolean; ifMissing?: boolean; }, result: InitResult): FileGuard; /** Record the run's hashes and move its kept files and warnings into `result`. */ export declare function finalizeGuard(result: InitResult): void; /** * Whether `file` is one init kept because the user edited it: a * `.monomind-new` copy sits beside it, or its content no longer matches the * hash init recorded. Other tools that refresh shipped files (doctor --fix) * must leave such a file alone. */ export declare function isKeptUserEdit(targetDir: string, file: string): boolean; /** The warning a run prints for the files it kept, or '' when there are none. */ export declare function formatKeptFiles(kept: readonly string[] | undefined): string; /** * Keep the newest `keep` backup directories. A directory holding retired * entries (`retired/`) is never removed: the manifest points at it and it can * be the only copy of a user's file. */ export declare function pruneBackups(targetDir: string, keep?: number): string[]; //# sourceMappingURL=file-guard.d.ts.map