Reject unsupported plugin types, symlink individual skills
Check for commands/, agents/, hooks/, .mcp.json, and .lsp.json at the
plugin root. Symlink each subdirectory under skills/ rather than the
plugin directory itself.

Assisted-by: Claude Opus 4.6 via pi
change vznypkxlokqltuppqxksovtksnqzkmuk
commit 9ba8587a5283bb82d93ac9f5682d3335c1336973
author Alpha Chen <alpha@kejadlen.dev>
date
parent lwusvywk
diff --git a/src/main.rs b/src/main.rs
index 9eee922..f347ff1 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -147,13 +147,14 @@ fn do_install() -> Result<()> {
     let repos_dir = paths::repos_dir()?;
     let skills_dir = paths::skills_dir()?;
     let cache_dir = paths::cache_dir();
+    let mut installed_skills = std::collections::HashSet::new();
 
     for (name, plugin) in &lockfile.plugins {
         let repo_path = git::repo_path(&repos_dir, &plugin.src);
         let wt_path = git::ensure_worktree(&repo_path, &plugin.rev)?;
 
-        let skill_source = wt_path.join(&plugin.path);
-        if !skill_source.is_dir() {
+        let plugin_root = wt_path.join(&plugin.path);
+        if !plugin_root.is_dir() {
             bail!(
                 "plugin '{}': path '{}' does not exist in repo at rev {}",
                 name,
@@ -162,36 +163,70 @@ fn do_install() -> Result<()> {
             );
         }
 
-        // Verify it's a skill (has SKILL.md)
-        if !skill_source.join("SKILL.md").is_file() {
+        // Reject unsupported plugin types
+        for unsupported in ["commands", "agents", "hooks"] {
+            if plugin_root.join(unsupported).is_dir() {
+                bail!(
+                    "plugin '{}': contains '{}/' — only skills are supported",
+                    name,
+                    unsupported
+                );
+            }
+        }
+        for unsupported in [".mcp.json", ".lsp.json"] {
+            if plugin_root.join(unsupported).is_file() {
+                bail!(
+                    "plugin '{}': contains '{}' — only skills are supported",
+                    name,
+                    unsupported
+                );
+            }
+        }
+
+        // Symlink each skill under skills/
+        let skills_source = plugin_root.join("skills");
+        if !skills_source.is_dir() {
             bail!(
-                "plugin '{}': path '{}' has no SKILL.md — only skills are supported",
+                "plugin '{}': no skills/ directory in '{}'",
                 name,
                 plugin.path
             );
         }
 
-        let symlink_path = skills_dir.join(name);
+        for entry in std::fs::read_dir(&skills_source)? {
+            let entry = entry?;
+            if !entry.path().is_dir() {
+                continue;
+            }
+
+            let skill_name = entry.file_name();
+            let symlink_path = skills_dir.join(&skill_name);
+
+            // Remove existing symlink if present
+            if symlink_path.symlink_metadata().is_ok() {
+                std::fs::remove_file(&symlink_path).with_context(|| {
+                    format!(
+                        "failed to remove existing symlink: {}",
+                        symlink_path.display()
+                    )
+                })?;
+            }
 
-        // Remove existing symlink if present
-        if symlink_path.symlink_metadata().is_ok() {
-            std::fs::remove_file(&symlink_path).with_context(|| {
+            std::os::unix::fs::symlink(entry.path(), &symlink_path).with_context(|| {
                 format!(
-                    "failed to remove existing symlink: {}",
-                    symlink_path.display()
+                    "failed to create symlink: {} -> {}",
+                    symlink_path.display(),
+                    entry.path().display()
                 )
             })?;
-        }
 
-        std::os::unix::fs::symlink(&skill_source, &symlink_path).with_context(|| {
-            format!(
-                "failed to create symlink: {} -> {}",
-                symlink_path.display(),
-                skill_source.display()
-            )
-        })?;
-
-        info!("installed {} -> {}", name, skill_source.display());
+            info!(
+                "installed {} -> {}",
+                skill_name.to_string_lossy(),
+                entry.path().display()
+            );
+            installed_skills.insert(skill_name);
+        }
     }
 
     for entry in std::fs::read_dir(&skills_dir)? {
@@ -209,7 +244,7 @@ fn do_install() -> Result<()> {
         // Only touch symlinks pointing into our cache
         if let Ok(target) = std::fs::read_link(&path)
             && target.starts_with(&cache_dir)
-            && !lockfile.plugins.contains_key(name_str.as_ref())
+            && !installed_skills.contains(&name)
         {
             std::fs::remove_file(&path)?;
             info!("removed stale symlink: {}", name_str);