Use BTreeMap for lockfile plugins
Name is the key instead of a field. BTreeMap serializes sorted,
so the explicit sort in save() is no longer needed. Selective update
simplifies from retain+push to insert.
Assisted-by: Claude Opus 4.6 via pi
diff --git a/src/git.rs b/src/git.rs
index fca878e..44e80b6 100644
--- a/src/git.rs
+++ b/src/git.rs
@@ -122,7 +122,7 @@ pub fn prune_worktrees(repos_dir: &Path, lockfile: &crate::lockfile::Lockfile) -
// Build a map: repo_path -> set of used rev prefixes
let mut used: HashMap<PathBuf, HashSet<String>> = HashMap::new();
- for plugin in &lockfile.plugins {
+ for plugin in lockfile.plugins.values() {
let rp = repo_path(repos_dir, &plugin.src);
used.entry(rp)
.or_default()
diff --git a/src/lockfile.rs b/src/lockfile.rs
index 1c97c4d..cbe306f 100644
--- a/src/lockfile.rs
+++ b/src/lockfile.rs
@@ -1,15 +1,16 @@
+use std::collections::BTreeMap;
+
use color_eyre::eyre::{Context, Result};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Lockfile {
#[serde(default)]
- pub plugins: Vec<LockedPlugin>,
+ pub plugins: BTreeMap<String, LockedPlugin>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LockedPlugin {
- pub name: String,
pub src: String,
pub path: String,
pub rev: String,
@@ -26,9 +27,7 @@ pub fn load() -> Result<Lockfile> {
pub fn save(lockfile: &Lockfile) -> Result<()> {
let path = crate::paths::lockfile_path()?;
- let mut sorted = lockfile.clone();
- sorted.plugins.sort_by(|a, b| a.name.cmp(&b.name));
- let contents = serde_yml::to_string(&sorted).context("failed to serialize lockfile")?;
+ let contents = serde_yml::to_string(lockfile).context("failed to serialize lockfile")?;
std::fs::write(&path, contents)
.with_context(|| format!("failed to write lockfile: {}", path.display()))?;
Ok(())
diff --git a/src/main.rs b/src/main.rs
index e26645a..9eee922 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -76,7 +76,12 @@ fn do_update(names: &[String]) -> Result<()> {
let all_plugins = config::load().context("failed to load config")?;
let (plugins, mut lockfile) = if names.is_empty() {
- (all_plugins, lockfile::Lockfile { plugins: vec![] })
+ (
+ all_plugins,
+ lockfile::Lockfile {
+ plugins: Default::default(),
+ },
+ )
} else {
let known: std::collections::HashSet<&str> =
all_plugins.iter().map(|p| p.name.as_str()).collect();
@@ -121,17 +126,14 @@ fn do_update(names: &[String]) -> Result<()> {
let path = source.trim_start_matches("./").to_string();
info!(" found at {}", path);
- // Remove existing entry for this plugin if doing selective update
- if !names.is_empty() {
- lockfile.plugins.retain(|p| p.name != plugin.name);
- }
-
- lockfile.plugins.push(lockfile::LockedPlugin {
- name: plugin.name.clone(),
- src: plugin.src.clone(),
- path,
- rev,
- });
+ lockfile.plugins.insert(
+ plugin.name.clone(),
+ lockfile::LockedPlugin {
+ src: plugin.src.clone(),
+ path,
+ rev,
+ },
+ );
}
lockfile::save(&lockfile).context("failed to save lockfile")?;
@@ -146,7 +148,7 @@ fn do_install() -> Result<()> {
let skills_dir = paths::skills_dir()?;
let cache_dir = paths::cache_dir();
- for plugin in &lockfile.plugins {
+ 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)?;
@@ -154,7 +156,7 @@ fn do_install() -> Result<()> {
if !skill_source.is_dir() {
bail!(
"plugin '{}': path '{}' does not exist in repo at rev {}",
- plugin.name,
+ name,
plugin.path,
&plugin.rev[..12]
);
@@ -164,12 +166,12 @@ fn do_install() -> Result<()> {
if !skill_source.join("SKILL.md").is_file() {
bail!(
"plugin '{}': path '{}' has no SKILL.md — only skills are supported",
- plugin.name,
+ name,
plugin.path
);
}
- let symlink_path = skills_dir.join(&plugin.name);
+ let symlink_path = skills_dir.join(name);
// Remove existing symlink if present
if symlink_path.symlink_metadata().is_ok() {
@@ -189,13 +191,9 @@ fn do_install() -> Result<()> {
)
})?;
- info!("installed {} -> {}", plugin.name, skill_source.display());
+ info!("installed {} -> {}", name, skill_source.display());
}
- // Clean up stale pinch-managed symlinks
- let locked_names: std::collections::HashSet<_> =
- lockfile.plugins.iter().map(|p| p.name.as_str()).collect();
-
for entry in std::fs::read_dir(&skills_dir)? {
let entry = entry?;
let path = entry.path();
@@ -211,7 +209,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)
- && !locked_names.contains(name_str.as_ref())
+ && !lockfile.plugins.contains_key(name_str.as_ref())
{
std::fs::remove_file(&path)?;
info!("removed stale symlink: {}", name_str);