Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions src/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,17 +83,22 @@ def _resolve_targets(target_args: tuple, yes: bool) -> List[str]:
return targets


def _apply_skill_to_targets(skill: dict, target_list: list) -> int:
"""Write a skill directly to each target's skill directory. Returns applied count."""
def _link_skill_to_targets(skill: dict, target_list: list) -> int:
"""Symlink a skill from ~/.apc/skills/ into each target's skill directory.

Uses link_skills() so the tool's skill dir always points back to the
canonical source — never a stale copy.
Returns number of targets linked.
"""
skills_dir = get_skills_dir()
count = 0
for target_name in target_list:
try:
applier = get_applier(target_name)
manifest = applier.get_manifest()
applied = applier.apply_skills([skill], manifest)
linked = applier.link_skills([skill], skills_dir, manifest)
manifest.save()
count += applied
count += linked
except Exception as e:
click.echo(f" ! {target_name}: {e}", err=True)
return count
Expand Down Expand Up @@ -232,7 +237,7 @@ def install(repo, skills, install_all, targets, branch, list_only, yes):
save_skill_file(skill["name"], raw_content)

# Apply directly to each target target
_apply_skill_to_targets(skill, target_list)
_link_skill_to_targets(skill, target_list)

# Save metadata to local cache
existing = load_skills()
Expand Down
59 changes: 59 additions & 0 deletions tests/test_appliers.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,3 +424,62 @@ def test_openclaw_reads_existing_memory(self):

if __name__ == "__main__":
unittest.main()


class TestOpenClawApplier(unittest.TestCase):
def setUp(self):
self.tmpdir = Path(tempfile.mkdtemp())
self.skills_dir = self.tmpdir / ".openclaw" / "skills"
self.skills_dir.mkdir(parents=True)
self.manifest_path = self.tmpdir / "manifest.json"

def _manifest(self) -> "ToolManifest":
return ToolManifest("openclaw", path=self.manifest_path)

def _skill(self, name="test-skill"):
return {"name": name, "description": "A test skill", "body": "# Test\nDo things."}

def test_apply_skills_clean_dir(self):
"""apply_skills writes SKILL.md into a per-skill subdirectory."""
manifest = self._manifest()
with patch("appliers.openclaw._openclaw_skills_dir", return_value=self.skills_dir):
from appliers.openclaw import OpenClawApplier

applier = OpenClawApplier()
count = applier.apply_skills([self._skill()], manifest)

self.assertEqual(count, 1)
skill_md = self.skills_dir / "test-skill" / "SKILL.md"
self.assertTrue(skill_md.exists())
self.assertIn("test-skill", manifest.managed_skill_names())

def test_apply_skills_does_not_create_symlinks(self):
"""apply_skills (collected skills path) always writes real directories, never symlinks.

Installed skills are linked via link_skills(). apply_skills() is only called
for collected skills which have no source directory — so it must create a real dir.
"""
manifest = self._manifest()
with patch("appliers.openclaw._openclaw_skills_dir", return_value=self.skills_dir):
from appliers.openclaw import OpenClawApplier

applier = OpenClawApplier()
applier.apply_skills([self._skill()], manifest)

skill_dir = self.skills_dir / "test-skill"
self.assertTrue(skill_dir.is_dir())
self.assertFalse(skill_dir.is_symlink(), "apply_skills must create a real dir, not a symlink")

def test_apply_skills_multiple_skills(self):
"""apply_skills handles multiple skills in one call."""
skills = [self._skill("alpha"), self._skill("beta")]
manifest = self._manifest()
with patch("appliers.openclaw._openclaw_skills_dir", return_value=self.skills_dir):
from appliers.openclaw import OpenClawApplier

applier = OpenClawApplier()
count = applier.apply_skills(skills, manifest)

self.assertEqual(count, 2)
self.assertTrue((self.skills_dir / "alpha" / "SKILL.md").exists())
self.assertTrue((self.skills_dir / "beta" / "SKILL.md").exists())
Loading
Loading