/** * Backup and restore core module. * * Produces full-`.cleo/` snapshots containing the consolidated project store * (cleo.db), config.json and project-info.json, plus the docs byte surfaces * (T13704: `.cleo/blobs/` manifest + bytes and the `.cleo/attachments/` byte * tree docs fetch reads), using the safest available method for each file * type: * * - SQLite databases: `VACUUM INTO` via the live native handle (see * {@link ../store/sqlite-backup.ts}). This is the ONLY safe way to * snapshot a WAL-mode SQLite database while it is open — raw filesystem * copies can capture torn writes or stale WAL frames. * * - JSON files: atomic tmp-then-rename via {@link atomicWriteSync} so a * partial write can never corrupt the backup target. * * Snapshots are recorded under `.cleo/backups/sqlite/` with a JSON sidecar * (`{backupId}.meta.json`) enumerating which files were captured and how. * Restores read the same sidecars and materialize each file back into the * live `.cleo/` directory. * * This is the backing store for the `cleo backup` and `cleo restore backup` * CLI verbs (see packages/cleo/src/cli/commands/backup.ts and restore.ts). * * ## Canonical backup path (T10315 · ADR-013 §10 · Saga T10281 / Epic T10284) * * Both this module and {@link ../store/sqlite-backup.ts} (the auto session-end * snapshotter) write to the SAME directory: `.cleo/backups/sqlite/`. The two * producers use distinguishable filename schemes that coexist: * * - `vacuumIntoBackupAll` writes ONE `tasks-YYYYMMDD-HHmmss.db` per run for * the unified project store (no sidecar); older `brain-*` / `conduit-*` * files predate T13891. * - `createBackup` writes `.` + `.meta.json`, * where `backupId = -YYYYMMDD-HHmmss` (matching the same local-time * timestamp format). * * The legacy `.cleo/backups/snapshot/` directory is retained as a read-only * fallthrough for one deprecation window — `listSystemBackups` enumerates * both directories and tags legacy entries via `legacy: true`, and * `restoreBackup` searches the legacy directory if no candidate is found in * the canonical directory. A one-time `DeprecationWarning` fires when the * legacy directory is consulted. * * @task T4783 * @task T5158 — extended to use VACUUM INTO for .db files and atomicWrite for JSON * @task T10315 — ratified `.cleo/backups/sqlite/` as the single canonical path * (ADR-013 §10). The previous `.cleo/backups/snapshot/` is now * a deprecated read-only fallthrough for one release. * @task T13724 — per-target capture failures (e.g. the T12687 worktree-build * VACUUM INTO denial) are recorded in the meta sidecar under * `captureErrors` instead of being silently dropped. */ import type { StoreRestoreResult } from '@cleocode/contracts'; import type { AbandonableLock } from '../store/lock.js'; /** Internal: reset the once-flag (test seam). */ export declare function _resetLegacyWarningOnce(): void; /** * A backup target that existed but failed to capture (T13724): e.g. the * project store whose `VACUUM INTO` the T12687 worktree-build authorizer * refuses, or a JSON file that could not be read. Recorded in the meta * sidecar under `captureErrors` so a partial backup — especially a * store-less one — is never silent. Mirrors how `docsBytes.error` records * a failed docs capture. */ export interface BackupCaptureError { /** The backup target that failed (e.g. `cleo.db`, `config.json`). */ target: string; /** Why the capture failed (the thrown error's message). */ error: string; } /** Result shape returned by {@link createBackup}. */ export interface BackupResult { /** Unique backup identifier (timestamped). */ backupId: string; /** Absolute path to the directory containing the snapshot files. */ path: string; /** ISO-8601 timestamp when the backup was created. */ timestamp: string; /** Backup category (`snapshot`, `safety`, `migration`). */ type: string; /** Files that were successfully captured into this backup. */ files: string[]; /** Why the copy was discarded instead of published (T13293), when it was. */ skipped?: string; /** * Per-target capture failures (T13724), present only when at least one * target that existed could not be captured. Also recorded in the meta * sidecar; the CLI surfaces these as `W_BACKUP_CAPTURE_FAILED` warnings. */ captureErrors?: BackupCaptureError[]; } /** Result shape returned by {@link restoreBackup}. */ export interface RestoreResult { /** Whether any files were actually restored (false if none matched). */ restored: boolean; /** The backup identifier that was restored. */ backupId: string; /** ISO-8601 timestamp of the original backup. */ timestamp: string; /** File names that were successfully restored back into `.cleo/`. */ filesRestored: string[]; } /** * Create a backup of the canonical CLEO data files. * * Produces safe copies via VACUUM INTO (for SQLite) and atomicWrite * (for JSON) into `.cleo/backups/sqlite/`. Writes a `{backupId}.meta.json` * sidecar describing the snapshot. * * The backup file naming uses the unified `YYYYMMDD-HHmmss` local-time format * (matching `sqlite-backup.ts:formatTimestamp`) so all snapshot files in * `.cleo/backups/sqlite/` share one timestamp convention. The `type` field * (`snapshot` by default) is embedded in the `backupId` to distinguish manual * snapshots from auto-snapshots and from `safety`/`migration` backups. * * Opens both `tasks.db` and `brain.db` through their canonical drizzle * accessors before snapshotting so that the native DB handles are live * when `safeSqliteSnapshot` asks for them. This makes the function * self-contained — callers do not need to pre-open the DBs. * * Async because opening the database engines requires async migration * reconciliation (ADR-012). The CLI dispatch layer awaits this result. * * @task T10315 — write target moved from `.cleo/backups/snapshot/` to * `.cleo/backups/sqlite/` per ADR-013 §10. */ export declare function createBackup(projectRoot: string, opts?: { type?: string; note?: string; /** * Maximum number of backup files to keep per type directory. * Oldest files are rotated out when this cap is exceeded. * Defaults to {@link DEFAULT_MAX_SNAPSHOTS} (10). * * @task T9194 */ maxSnapshots?: number; }): Promise; /** A single backup entry returned by listSystemBackups. */ export interface BackupEntry { /** Unique backup identifier (timestamped). */ backupId: string; /** Backup category (`snapshot`, `safety`, `migration`). */ type: string; /** ISO-8601 timestamp when the backup was created. */ timestamp: string; /** Optional human-readable note attached at creation time. */ note?: string; /** File names captured in this backup. */ files: string[]; /** * `true` when this entry was discovered under the deprecated legacy * `.cleo/backups/snapshot/` directory. Surfaces in the `cleo backup list` * envelope so the operator knows the entry is read-only and will become * unreachable in the release following T10315. * * @task T10315 */ legacy?: boolean; /** * `true` when the backup is pinned: rotation never deletes it (T12535, the * snapshot a twin collapse took before changing the store). */ pinned?: boolean; /** Why it is pinned. */ pinnedReason?: string; /** Which store the backup is of: `project` (`.cleo/cleo.db`) or `global` (`/cleo.db`). */ scope?: 'project' | 'global'; /** * What its store file holds (T13245): `tasks`, `brain` and `conduit` for a * project backup (one `cleo.db` copy, or the old `tasks.db`/`brain.db` * labels, which are identical copies of it), `global` for the global store. */ contains?: string[]; } /** * The file name {@link createBackup} gives its copy of the project store * (`cleo.db.`; before T13245 two identical copies labelled * `tasks.db` and `brain.db`). */ export declare const PROJECT_STORE_BACKUP_FILE = "cleo.db"; /** * List all available system backups (`snapshot`, `safety`, `migration`). * * Reads `.meta.json` sidecar files written by {@link createBackup}. Walks * both the canonical `.cleo/backups/sqlite/` directory AND the deprecated * `.cleo/backups/snapshot/` directory (ADR-013 §10 read-side deprecation * window). Entries discovered under the legacy directory are tagged with * `legacy: true` so callers can surface a warning. * * This is a pure read operation — it does not modify any files. A one-time * `DeprecationWarning` is emitted via `process.emitWarning` when the legacy * directory yields ≥1 entry. * * @task T4783 * @task T10315 — added canonical-dir scan + legacy-dir read fallthrough. */ export declare function listSystemBackups(projectRoot: string): BackupEntry[]; /** * List the backups of the global store (`/cleo.db`, T13245), newest * first. Read-only. * * @returns The global backups, each tagged `scope: 'global'`. * @task T13245 */ export declare function listGlobalBackups(): BackupEntry[]; /** * Stale threshold of the automatic global backup lock (T13293). `VACUUM INTO` * is synchronous: while it runs, the event loop is blocked and * proper-lockfile cannot refresh the lock's mtime, so a copy longer than the * threshold would let another process take the lock as stale and start a * second copy. Measured on an SSD (2026-10-06): 70 MB in 0.29 s, 1 GB in * 2.4 s; a slow or FUSE-mounted disk can be 20-50x slower, so 1 GB may take * ~2 min. The threshold is 3x that worst case with a 10-minute floor, like * `EXODUS_LOCK_STALE_MS`. */ export declare const GLOBAL_BACKUP_LOCK_STALE_MS = 600000; /** Minimum age of the newest automatic global backup before another is taken. */ export declare const AUTO_GLOBAL_BACKUP_INTERVAL_MS: number; /** * The session-end backup of the global store (T13245): an `auto` backup via * {@link createGlobalBackup}, at most once per * {@link AUTO_GLOBAL_BACKUP_INTERVAL_MS} (the global store is shared by every * project, so every session end would otherwise copy it). Single-flight across * processes under a lock with the age re-checked inside it, and admitted by the * governor as `db-heavy` (T13286). Never throws. * * @param now - Clock (tests). * @param opts - The admission (tests); defaults to a non-blocking `db-heavy` governor admission. * @returns The backup id written, or `null` when skipped or failed. * @task T13245 */ export declare function autoGlobalBackup(now?: Date, opts?: { admit?: () => Promise<{ release: () => Promise; } | null>; /** The single-flight lock (tests): takes the compromise callback, returns the held lock. */ lock?: (onCompromised: (err: Error) => void) => Promise; }): Promise; /** * Back up the global store (`/cleo.db`: the global brain, nexus, * agent registry; T13245) as one `VACUUM INTO` copy under * `/backups/sqlite/cleo.db.`, with a sidecar, rotated * like project backups. `cleo restore backup --scope global --id ` * restores it. * * @param opts - Backup type, note and rotation cap. * @returns What was written (`files` is empty when the store could not be opened). * A failed store capture is reported in `captureErrors` (T13724) — the * sidecar is only written when the store was captured, so a fully failed * global backup is surfaced via the result (and the CLI warning it drives), * not via a sidecar that would suppress the next hourly `auto` backup. * @task T13245 */ export declare function createGlobalBackup(opts?: { type?: string; note?: string; maxSnapshots?: number; /** * Checked after the copy and before it is published: a reason to discard * it (the caller's lock was lost during the copy, T13293), or `null`. A * discarded copy is deleted; nothing is renamed, listed or rotated. */ discardIf?: () => string | null; }): Promise; /** * Restore a backup into the live `.cleo/` directory. * * This operation overwrites the in-place copies of the files recorded in * the backup's sidecar. SQLite files are restored via a plain `copyFileSync` * because restore runs BEFORE the next CLEO process opens the database — no * WAL is active at that point — so a filesystem copy is safe. Callers must * ensure no CLEO process is concurrently writing to the target database. * * JSON files are restored via `atomicWrite` (tmp-then-rename) so a crash * mid-restore cannot produce a truncated config. * * Search order (T10315 / ADR-013 §10): canonical `.cleo/backups/sqlite/` * first, then legacy `.cleo/backups/{snapshot,safety,migration}/` as a * read-only fallthrough (emits a one-time DeprecationWarning if used). * * @task T10315 */ export declare function restoreBackup(projectRoot: string, params: { backupId: string; force?: boolean; confirmOwnerStore?: boolean; cwd: string; /** Leave the store-file labels to {@link restoreBackupById} (T13245). */ skipStoreFiles?: boolean; }): RestoreResult; /** Result of {@link restoreBackupById}. */ export interface BackupIdRestoreResult extends RestoreResult { /** The store restore (`null` when the backup holds no store file). */ store: StoreRestoreResult | null; } /** * Restore a backup by id: its store file through {@link restoreStoreSnapshot} * onto the live `.cleo/cleo.db` (verified, live writers refused, the replaced * store kept), then its JSON files as {@link restoreBackup} does (T13245). * The store goes first: when it is refused, nothing is restored. * * @param projectRoot - Absolute path to the project root. * @param params - The backup id, the worktree confirmation and the invocation directory. * @returns What was restored. * @task T13245 */ export declare function restoreBackupById(projectRoot: string, params: { backupId: string; force?: boolean; confirmOwnerStore?: boolean; cwd: string; /** `global`: a backup of `/cleo.db` ({@link createGlobalBackup}). */ scope?: 'project' | 'global'; }): Promise; /** Result of restoring an individual file from backup. */ export interface FileRestoreResult { /** Whether the file was actually restored. */ restored: boolean; /** The filename that was restored. */ file: string; /** The backup file path restored from. */ from: string; /** The target path that was written. */ targetPath: string; /** Whether this was a dry-run. */ dryRun?: boolean; } /** * Restore an individual file (tasks.db or config.json) from the most recent backup. * * Moves the backing logic from `backupRestore` in system-engine.ts into core. * Uses `getTaskPath` / `getConfigPath` from `../paths.js` (respects CLEO_DIR). * Imports `listBackups` and `restoreFromBackup` from the store layer. * * @param projectRoot - Absolute path to the project root * @param fileName - File to restore: 'tasks.db' or 'config.json' * @param opts - Restore flags; `cwd` (the invocation directory) is required (T12680) * @returns Result of the restore operation * * @task T5329 * @task T1571 */ export declare function fileRestore(projectRoot: string, fileName: string, opts: { dryRun?: boolean; confirmOwnerStore?: boolean; cwd: string; }): Promise; //# sourceMappingURL=backup.d.ts.map