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:
Steve Yegge
2025-12-20 18:03:12 -08:00
5 changed files with 409 additions and 292 deletions

View File

@@ -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

View File

@@ -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)
}
})
}