refactor: Nest plugins under marketplaces in config
Removes the indirection of a separate top-level plugins map where each
entry back-references a marketplace by name.

Assisted-by: Claude Opus 4.6 via pi
change vxwmwmznosorvnlmrrokqvwloqpznsor
commit dcbd16e82d7a1e3db160f21fc3be55860f71809b
author Alpha Chen <alpha@kejadlen.dev>
date
parent lzztqmmy
diff --git a/README.md b/README.md
index 3778a7f..89967da 100644
--- a/README.md
+++ b/README.md
@@ -60,22 +60,19 @@ appear in different files at different versions.
 marketplaces:
   my-skills:
     src: https://github.com/user/skills-repo
-    version: main                 # default version (branch/tag) for plugins from this marketplace
+    version: main                 # branch/tag to track
     manifest: custom/path/marketplace.json  # optional, defaults to .claude-plugin/marketplace.json
-
-plugins:
-  jj:
-    marketplace: my-skills
-
-  gh-pr:
-    marketplace: my-skills
+    plugins:
+      - jj
+      - gh-pr
 ```
 
-Every plugin references a named marketplace (inheriting `src`, `version`, and
-`manifest`). The plugin path within the repo is discovered from
-`.claude-plugin/marketplace.json` (or the marketplace's `manifest` override).
+Plugins are listed under their marketplace. Each plugin inherits the
+marketplace's `src`, `version`, and `manifest`. The plugin path within the repo
+is discovered from `.claude-plugin/marketplace.json` (or the marketplace's
+`manifest` override).
 
-Removing a skill is done by removing it from the config and re-running
+Removing a plugin is done by removing it from the config and re-running
 `pinch install`.
 
 ## Lockfile
diff --git a/src/config.rs b/src/config.rs
index fbe009b..605b45a 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,7 +1,7 @@
 use std::collections::BTreeMap;
 use std::collections::HashSet;
 
-use color_eyre::eyre::{Result, WrapErr, bail, eyre};
+use color_eyre::eyre::{Result, WrapErr, bail};
 use serde::Deserialize;
 
 #[derive(Debug, Clone)]
@@ -44,32 +44,23 @@ pub fn load() -> Result<Vec<Plugin>> {
         let file: ConfigFile = serde_yml::from_str(&contents)
             .wrap_err_with(|| format!("failed to parse {}", path.display()))?;
 
-        let marketplaces = &file.marketplaces;
-
-        for (name, entry) in file.plugins {
-            if !seen_names.insert(name.clone()) {
-                bail!(
-                    "duplicate plugin name '{}' (found in {})",
+        for (_mkt_name, mkt) in file.marketplaces {
+            for name in mkt.plugins {
+                if !seen_names.insert(name.clone()) {
+                    bail!(
+                        "duplicate plugin name '{}' (found in {})",
+                        name,
+                        path.display()
+                    );
+                }
+
+                plugins.push(Plugin {
                     name,
-                    path.display()
-                );
+                    src: mkt.src.clone(),
+                    version: mkt.version.clone(),
+                    manifest: mkt.manifest.clone(),
+                });
             }
-
-            let mkt = marketplaces.get(&entry.marketplace).ok_or_else(|| {
-                eyre!(
-                    "plugin '{}' references unknown marketplace '{}' (in {})",
-                    name,
-                    entry.marketplace,
-                    path.display()
-                )
-            })?;
-
-            plugins.push(Plugin {
-                name,
-                src: mkt.src.clone(),
-                version: mkt.version.clone(),
-                manifest: mkt.manifest.clone(),
-            });
         }
     }
 
@@ -81,17 +72,12 @@ struct MarketplaceEntry {
     src: String,
     version: String,
     manifest: Option<String>,
-}
-
-#[derive(Deserialize)]
-struct PluginEntry {
-    marketplace: String,
+    #[serde(default)]
+    plugins: Vec<String>,
 }
 
 #[derive(Deserialize)]
 struct ConfigFile {
     #[serde(default)]
     marketplaces: BTreeMap<String, MarketplaceEntry>,
-    #[serde(default)]
-    plugins: BTreeMap<String, PluginEntry>,
 }