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
« 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."""
3from __future__ import annotations
5import os
6import shutil
7import tempfile
8from pathlib import Path
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"
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}")
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