mirror of
https://github.com/ryan4yin/nix-config.git
synced 2026-09-09 19:41:46 +02:00
fix(agents): preserve existing rules during symlink installation
This commit is contained in:
+2
-1
@@ -39,7 +39,8 @@ Behavior:
|
|||||||
|
|
||||||
- Each target is handled independently.
|
- Each target is handled independently.
|
||||||
- Missing destination directories are skipped.
|
- Missing destination directories are skipped.
|
||||||
- Existing destination file/symlink is replaced with a symlink to this repo source file.
|
- Existing regular files are preserved as `.bak` backups (numbered when a backup already exists).
|
||||||
|
- Destination links are replaced atomically; a failed link creation leaves the destination intact.
|
||||||
|
|
||||||
The installer links only `AGENTS.md`; it does not install permission configuration, skills, or CLIs.
|
The installer links only `AGENTS.md`; it does not install permission configuration, skills, or CLIs.
|
||||||
The repository-root `AGENTS.md` contains guidance for this Nix configuration repository. It is not
|
The repository-root `AGENTS.md` contains guidance for this Nix configuration repository. It is not
|
||||||
|
|||||||
+18
-3
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
@@ -12,10 +13,24 @@ def install_one(target_dir: Path, source_file: Path, target_name: str) -> None:
|
|||||||
|
|
||||||
target_file = target_dir / target_name
|
target_file = target_dir / target_name
|
||||||
|
|
||||||
if target_file.exists() or target_file.is_symlink():
|
if target_file.is_file() and not target_file.is_symlink():
|
||||||
target_file.unlink()
|
# Preserve the old inode without reading its contents or replacing an earlier backup.
|
||||||
|
index = 0
|
||||||
|
while True:
|
||||||
|
suffix = ".bak" if index == 0 else f".bak.{index}"
|
||||||
|
backup = target_file.with_name(target_file.name + suffix)
|
||||||
|
try:
|
||||||
|
os.link(target_file, backup)
|
||||||
|
break
|
||||||
|
except FileExistsError:
|
||||||
|
index += 1
|
||||||
|
print(f"backed up {target_file} -> {backup}")
|
||||||
|
|
||||||
target_file.symlink_to(source_file)
|
# Create the link on the same filesystem before atomically replacing the destination.
|
||||||
|
with tempfile.TemporaryDirectory(prefix=".install-rules-", dir=target_dir) as staging:
|
||||||
|
link = Path(staging) / target_name
|
||||||
|
link.symlink_to(source_file)
|
||||||
|
link.replace(target_file)
|
||||||
print(f"linked {target_file} -> {source_file}")
|
print(f"linked {target_file} -> {source_file}")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import importlib.util
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
|
||||||
|
spec = importlib.util.spec_from_file_location(
|
||||||
|
"install_rules", Path(__file__).with_name("install-rules.py")
|
||||||
|
)
|
||||||
|
installer = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(installer)
|
||||||
|
|
||||||
|
|
||||||
|
class InstallRulesTests(unittest.TestCase):
|
||||||
|
def test_existing_file_is_backed_up_without_overwriting_backup(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
source = root / "source.md"
|
||||||
|
source.write_text("global rules")
|
||||||
|
target = root / "AGENTS.md"
|
||||||
|
target.write_text("personal rules")
|
||||||
|
backup = root / "AGENTS.md.bak"
|
||||||
|
backup.write_text("older rules")
|
||||||
|
installer.install_one(root, source, target.name)
|
||||||
|
self.assertEqual(target.resolve(), source)
|
||||||
|
self.assertEqual(backup.read_text(), "older rules")
|
||||||
|
self.assertEqual((root / "AGENTS.md.bak.1").read_text(), "personal rules")
|
||||||
|
|
||||||
|
def test_failed_link_creation_preserves_existing_file(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
source = root / "source.md"
|
||||||
|
source.write_text("global rules")
|
||||||
|
target = root / "AGENTS.md"
|
||||||
|
target.write_text("personal rules")
|
||||||
|
with patch.object(Path, "symlink_to", side_effect=OSError("link failed")):
|
||||||
|
with self.assertRaises(OSError):
|
||||||
|
installer.install_one(root, source, target.name)
|
||||||
|
self.assertTrue(target.is_file())
|
||||||
|
self.assertFalse(target.is_symlink())
|
||||||
|
self.assertEqual(target.read_text(), "personal rules")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user