Documentation
¶
Index ¶
- Variables
- func AreHardlinked(a, b string) (bool, error)
- func EnsureManagedGitignore(repoRoot string, ignorePaths []string) error
- func FindFile(basePath string, exts []string) string
- func Hardlink(srcPath, dstPath string) error
- func HardlinkReplacing(srcPath, dstPath string, backup func(path string) error) error
- func IsDirEntry(path string) bool
- func IsManagedFileLink(path string) bool
- func IsManagedLink(linkPath, target string) bool
- func IsManagedLinkUnder(linkPath, prefix string) bool
- func IsSymlinkTo(linkPath, target string) bool
- func IsSymlinkUnder(linkPath, prefix string) bool
- func ManagedLinkTarget(linkPath string) (string, bool)
- func RemoveIfHardlinkedToAny(path string, sources []string) (bool, error)
- func RemoveIfSymlinkUnder(linkPath, prefix string) error
- func Symlink(target, linkPath string) error
- func SymlinkReplacing(target, linkPath string, backup func(path string) error) error
Constants ¶
This section is empty.
Variables ¶
var ErrUnmanagedTarget = errors.New("link path occupied by an unmanaged entry")
ErrUnmanagedTarget is returned by Symlink when linkPath is occupied by an entry dot-agents does not own (a regular file, or a non-empty directory) rather than a managed link or an idempotent re-link of the same canonical file. Callers can errors.Is() this to distinguish "user data is in the way" from an I/O failure, and decide to surface a conflict or route through SymlinkReplacing with an explicit backup.
Functions ¶
func AreHardlinked ¶
AreHardlinked checks whether two paths share the same inode.
func EnsureManagedGitignore ¶ added in v0.4.0
EnsureManagedGitignore writes the idempotent da-owned block into the consuming project's `.gitignore` (rooted at repoRoot) so every materialized output in ignorePaths — projected links, generated platform configs, materialized asset units — plus the always-ignored machine-local overlay are excluded from git, while the committed `.agentsrc.json`/`.agentsrc.lock` contract stays tracked (§15 / D14 / R8).
It is convergent: the managed block is regenerated (sorted + de-duplicated) on every call, user-authored content outside the markers is preserved, and re-running with the same inputs produces byte-identical output (no append-duplication). A missing `.gitignore` is created; an empty repoRoot is an error. The block is always present (it carries the always-ignored overlay) even when ignorePaths is empty.
func FindFile ¶
FindFile tries each extension suffix in order and returns the first match, or empty string if none found.
func Hardlink ¶
Hardlink creates a hard link at dstPath pointing to the same inode as srcPath. Idempotent (already-hard-linked → no-op) and, by the same ownership contract as Symlink, NEVER destroys unmanaged user data: an existing dst that is not a managed link we own (regular file, non-empty dir, user-owned link) yields ErrUnmanagedTarget. A caller that legitimately intends to replace such an entry uses HardlinkReplacing with an explicit backup.
func HardlinkReplacing ¶
HardlinkReplacing behaves like Hardlink but backs up an unmanaged occupant (via the caller-supplied backup) before replacing it; a backup failure aborts and preserves the entry.
func IsDirEntry ¶
IsDirEntry reports whether the entry at path is a directory, following symlinks. Use this instead of e.IsDir() when entries may be symlinks to directories.
func IsManagedFileLink ¶
IsManagedFileLink reports whether the entry at path is a managed link to a file when the canonical target is unknown to the caller. It is the target-free companion to IsManagedLink for code paths (e.g. removal of rendered managed files) that must distinguish "a managed link we must preserve" from "a plain regular file we wrote and may delete".
- POSIX: the entry is a symlink (os.Lstat reports os.ModeSymlink).
- Windows: the entry is a hard link — no reparse point, but a managed file link always shares its inode with the canonical source, so its link count is >= 2. A file dot-agents rendered itself has a link count of 1.
A directory junction is reported by the symlink branch on Windows (Go's os.Lstat sets os.ModeSymlink for IO_REPARSE_TAG_MOUNT_POINT).
func IsManagedLink ¶
IsManagedLink reports whether linkPath is a managed reference to target. This is the single cross-platform predicate the link contract is built on:
- POSIX: a symlink whose target == target.
- Windows: a symlink or junction resolving to target (junction targets are absolute, so an absolute/clean-normalized compare is also tried), OR a hard link to the same canonical file (os.SameFile).
On POSIX the hard-link branch is still honored (a hardlinked managed file is a valid managed reference there too), so behavior is uniform.
func IsManagedLinkUnder ¶
IsManagedLinkUnder reports whether linkPath is a managed link whose resolved target lies under prefix. Only resolvable links (symlink / junction) can answer this; a hard link has no target path to test against prefix, so it is reported false (parity with the prior symlink-only behavior on POSIX).
func IsSymlinkTo ¶
IsSymlinkTo is retained for callers/tests that specifically assert the POSIX-symlink contract; it now delegates to the OS-aware predicate.
func IsSymlinkUnder ¶
IsSymlinkUnder delegates to the OS-aware predicate.
func ManagedLinkTarget ¶
ManagedLinkTarget returns the path linkPath references, and true, when linkPath is a *resolvable* managed link — a POSIX symlink or a Windows junction (Go's os.Readlink resolves IO_REPARSE_TAG_MOUNT_POINT). A hard link has no reparse point and therefore no resolvable target; use IsManagedLink with a known target for the hard-link case.
func RemoveIfHardlinkedToAny ¶
RemoveIfHardlinkedToAny removes path and returns true if path is hard linked to any of the given candidate source files (same inode / file index). This is the file-link analogue of RemoveIfSymlinkUnder for the Windows model, where managed files are hard links with no reparse point to resolve against a prefix — the caller supplies the canonical sources it manages. Promoted from the cursor platform, which has used this pattern in production since .mdc rule files cannot be symlinks. Returns (matched, err): matched=true once a hard-linked source is found; err is non-nil only when removal of a matched managed link failed. Callers MUST distinguish (false,nil)=not-managed, (true,nil)=removed, (true,err)=managed-but-removal-failed — silently dropping the error makes da remove/doctor report success while an active managed file is left behind.
func RemoveIfSymlinkUnder ¶
RemoveIfSymlinkUnder removes linkPath if it is a managed link whose resolved target starts with prefix. Resolvable links (POSIX symlink / Windows junction) are handled here; a Windows hard-linked *file* has no resolvable target, so callers that manage file links and know the candidate canonical sources should additionally use RemoveIfHardlinkedToAny.
func Symlink ¶
Symlink creates or updates a managed link at linkPath pointing to target. It is idempotent for managed state (correct link / same canonical inode / stale managed link / empty squat dir) but, by contract, NEVER destroys unmanaged user data: an existing regular file or non-empty directory that is not a managed link yields ErrUnmanagedTarget and is left intact. A caller that legitimately intends to replace such an entry must go through SymlinkReplacing with an explicit backup.
"Managed link" is OS-specific: a POSIX symlink, or on Windows a directory junction (dirs) / hard link (files). The hard-link case has no reparse point, so idempotency for it is detected by inode identity (os.SameFile) rather than os.Readlink.
func SymlinkReplacing ¶
SymlinkReplacing behaves like Symlink but, when linkPath holds an unmanaged regular file or non-empty directory, first invokes backup(path) (a caller-supplied preservation step — e.g. mirror/backup at the commands layer; links must not depend on it) and only then removes the entry and installs the managed link. If backup returns an error the original entry is left untouched and the error is propagated (no data loss). A nil backup is identical to Symlink (refuse).
Types ¶
This section is empty.