export interface AutoSyncContext { realMountDir: string; realProjectDir: string; isExcluded: (relPosix: string) => boolean; /** * Normalized directory names that drive any-depth `isExcluded` matches. * Used purely to hint `@parcel/watcher` which subtrees to skip subscribing * to. The in-handler `isSyncCandidate` filter remains authoritative. */ excludedAnyDepthNames: readonly string[]; /** * Root-anchored excluded names/prefixes such as `build` or `packages/cache`. * These are matched only from the watch root to avoid hiding legitimate * nested source directories like `src/build`. */ excludedRootPrefixes: readonly string[]; /** * Directory-only ignore patterns (ending in `/`) must only match when the * path is a directory. Callers that know the path's type pass `isDirectory`; * callers that don't should omit the second argument and fall back to the * file-form check. */ isIgnored: (relPosix: string, isDirectory?: boolean) => boolean; isReadonly: (relPosix: string) => boolean; /** * One-way project→mount paths. Project-side changes flow into the mount, * but mount-side changes never flow back. Unlike readonly, the mount copy * is left writable so tools (e.g. git) can mutate it locally; those * mutations are simply discarded on cleanup. */ isNoSyncBack: (relPosix: string) => boolean; isReservedFile: (relPosix: string) => boolean; /** * True while the mount root still looks like a live mount (its marker * file exists). Checked before any deletion is mirrored across trees: a * mount directory that was torn down externally (crash cleanup, manual * rm) must read as "the mount is gone", never as "the agent deleted * every file" — without this, autosync would faithfully propagate the * teardown as a mass delete of the user's project. */ mountRootIntact: () => boolean; /** Same guard for the project side: its disappearance must not empty the mount. */ projectRootIntact: () => boolean; /** * Sync state seeded by the mount population loop: one entry per copied * file with both sides' mtimes recorded at copy time. When present, * `startAutoSync` clones it instead of running the full-tree * content-comparison priming pass — the copy already proved both sides * identical, so re-reading every file pair only rediscovers that. */ initialState?: ReadonlyMap; } export interface AutoSyncOptions { /** * Degraded-watcher full-reconcile interval as a safety net. Default: 10_000ms. * Set to 0 or Infinity to disable periodic full reconciles. */ scanIntervalMs?: number; /** * Full-reconcile interval while both watcher subscriptions are healthy. * Default: 60_000ms, or `scanIntervalMs` when that option is explicitly set. * Set to 0 or Infinity to disable healthy-watcher full reconciles. */ healthyScanIntervalMs?: number; /** * Per-path event debounce in ms. Rapid watcher events for the same path * are coalesced into a single sync. Default: 50. */ debounceMs?: number; /** Invoked on errors during sync — logged by default consumer. */ onError?: (err: Error) => void; } export interface AutoSyncHandle { stop(opts?: { signal?: AbortSignal; }): Promise; /** Drain currently debounced watcher events. Falls back to reconcile if watchers are degraded. */ flushPending(opts?: { signal?: AbortSignal; }): Promise; /** Force a reconcile now; returns number of files copied/deleted. */ reconcile(opts?: { signal?: AbortSignal; }): Promise; /** Mount-side paths that still need a final one-shot syncBack check. */ getDirtyPaths(): IterableIterator; /** True once both watchers subscribed and no watcher error has been observed. */ watchersHealthy(): boolean; /** Cumulative files changed (copied or deleted) since autosync started. */ totalChanges(): number; /** Resolves once both watchers have completed their initial scan. */ ready(): Promise; /** * Snapshot of the per-file sync state (both sides' last-synced mtimes), * keyed by posix-relative path. Persist it alongside a kept mount and feed * it to `attachMount` so the next session's first reconcile can * distinguish deletions from creations. Run a full `reconcile()` first if * the snapshot must cover paths only reconciles visit (e.g. `.git/**` * under `includeGit`). */ exportState(): Record; } export interface FileState { mountMtimeMs?: number; projectMtimeMs?: number; } export declare function startAutoSync(ctx: AutoSyncContext, opts?: AutoSyncOptions): AutoSyncHandle; /** @internal exported for the adversarial confinement suite. */ export declare function isSymlinkTarget(target: string): boolean; /** * Exported for the adversarial confinement suite in * auto-sync-confinement.test.ts. Not part of the package's public API — the * test drives the real resolver rather than a copy of it, because a copy proves * nothing about this code. */ export declare function resolveSafeWriteTarget(root: string, candidate: string): string | null; /** * Copy `source` onto `target` without ever writing *through* whatever `target` * currently names. * * The content is written to a temporary sibling inside the already-validated * parent directory and then renamed over the target. That is what makes this * safe, and it closes two confirmed escapes that a check-then-copy sequence * could not: * * - **Hardlink.** A hardlink inside the root pointing at a file outside it is * path-indistinguishable from a real file and `realpath` cannot resolve it, * because a hardlink has no target. `copyFileSync` onto that name wrote * straight through to the outside file. `rename` replaces the *directory * entry* instead, so the linked file keeps its content. * * - **TOCTOU.** `isSymlinkTarget(target)` followed by `copyFileSync(target)` * is two path lookups, and a target swapped for a symlink in between was * followed. `rename` does not follow a final symlink — it replaces it. * * It also makes the write atomic: a reader sees the old file or the new one, * never a partial or zero-length one, and an interrupted copy leaves the target * untouched. Reflink cloning is preserved, since the copy into the temporary * file still uses COPYFILE_FICLONE. */ export declare function safeCopyOnto(source: string, target: string, mode?: number): boolean;