Coverage for src/lilbee/cli/launchers/skill_install.py: 100%

34 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-11-06 08:17 +0000

1"""Install the bundled lilbee-mcp guidance skill into an agent's skills directory.""" 

2 

3from __future__ import annotations 

4 

5import os 

6import shutil 

7import tempfile 

8from pathlib import Path 

9 

10_PACKAGE_ROOT = Path(__file__).resolve().parents[2] 

11_PACKAGED_SKILL = _PACKAGE_ROOT / "skills" / "lilbee_mcp" 

12_CHECKOUT_SKILL = _PACKAGE_ROOT.parents[1] / "plugins" / "lilbee" / "skills" / "lilbee-mcp" 

13 

14 

15def bundled_skill_dir() -> Path: 

16 """Return the installed skill directory, or the checkout one in a source tree.""" 

17 for candidate in (_PACKAGED_SKILL, _CHECKOUT_SKILL): 

18 if (candidate / "SKILL.md").is_file(): 

19 return candidate 

20 raise FileNotFoundError(f"lilbee-mcp skill not found at {_PACKAGED_SKILL} or {_CHECKOUT_SKILL}") 

21 

22 

23def install_bundled_skill(dest: Path) -> Path | None: 

24 """Copy the bundled lilbee-mcp skill into *dest*; skip (return None) if it exists.""" 

25 if dest.exists(): 

26 return None 

27 source = bundled_skill_dir() 

28 dest.parent.mkdir(parents=True, exist_ok=True) 

29 # Stage in a sibling temp dir and atomically rename so a partial copy never 

30 # leaves a half-written skill dir that exists() would then skip forever. 

31 staging = Path(tempfile.mkdtemp(dir=dest.parent, prefix=".lilbee-mcp-")) 

32 try: 

33 for entry in source.iterdir(): 

34 if entry.is_file(): 

35 (staging / entry.name).write_bytes(entry.read_bytes()) 

36 try: 

37 os.replace(staging, dest) 

38 except OSError: 

39 # On Windows, os.replace into an existing dest can race with another 

40 # installer. If dest now exists, the skill is already installed. 

41 shutil.rmtree(staging, ignore_errors=True) 

42 if dest.exists(): 

43 return None 

44 raise 

45 except BaseException: 

46 shutil.rmtree(staging, ignore_errors=True) 

47 raise 

48 return dest