Merge PR #665: fix(setup): preserve symlinks in atomicWriteFile
Add ResolveForWrite helper that resolves symlinks before writing, so atomic writes go to the symlink target instead of replacing the symlink. This prevents `bd setup claude` from overwriting nix/home-manager managed ~/.claude/settings.json symlinks. Closes #665 Co-authored-by: qmx <qmx@qmx.me>
This commit is contained in:
@@ -69,6 +69,23 @@ func FindMoleculesJSONLInDir(dbDir string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// ResolveForWrite returns the path to write to, resolving symlinks.
|
||||
// If path is a symlink, returns the resolved target path.
|
||||
// If path doesn't exist, returns path unchanged (new file).
|
||||
func ResolveForWrite(path string) (string, error) {
|
||||
info, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return path, nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
return filepath.EvalSymlinks(path)
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// CanonicalizePath converts a path to its canonical form by:
|
||||
// 1. Converting to absolute path
|
||||
// 2. Resolving symlinks
|
||||
|
||||
@@ -179,3 +179,56 @@ func TestCanonicalizePathSymlink(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveForWrite(t *testing.T) {
|
||||
t.Run("regular file", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
file := filepath.Join(tmpDir, "regular.txt")
|
||||
if err := os.WriteFile(file, []byte("test"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, err := ResolveForWrite(file)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got != file {
|
||||
t.Errorf("got %q, want %q", got, file)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("symlink", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
target := filepath.Join(tmpDir, "target.txt")
|
||||
if err := os.WriteFile(target, []byte("test"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
link := filepath.Join(tmpDir, "link.txt")
|
||||
if err := os.Symlink(target, link); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, err := ResolveForWrite(link)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
// Resolve target too - on macOS, /var is symlink to /private/var
|
||||
wantTarget, _ := filepath.EvalSymlinks(target)
|
||||
if got != wantTarget {
|
||||
t.Errorf("got %q, want %q", got, wantTarget)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non-existent", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
newFile := filepath.Join(tmpDir, "new.txt")
|
||||
|
||||
got, err := ResolveForWrite(newFile)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got != newFile {
|
||||
t.Errorf("got %q, want %q", got, newFile)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user