aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-07-20 22:58:49 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-07-20 23:04:15 +0300
commit69d07dba84411db33bf1f5191a69f80e5069c6ea (patch)
treee954eefb66346a37cfff8f8d68aaf91d74e18c3e /src
parentd956a6b56dabac9690260e50a9e03372510877d4 (diff)
fix: make plugin state global across sessions
refs #1174
Diffstat (limited to 'src')
-rw-r--r--src/app/api/plugins/manifest.rs7
-rw-r--r--src/app/api/plugins/mod.rs229
-rw-r--r--src/app/api/plugins/runtime.rs4
-rw-r--r--src/cli/plugin.rs37
-rw-r--r--src/persist/plugin_registry.rs107
-rw-r--r--src/plugin_paths.rs15
6 files changed, 271 insertions, 128 deletions
diff --git a/src/app/api/plugins/manifest.rs b/src/app/api/plugins/manifest.rs
index f6464285..657f8257 100644
--- a/src/app/api/plugins/manifest.rs
+++ b/src/app/api/plugins/manifest.rs
@@ -292,12 +292,7 @@ pub(super) fn normalize_plugin_source(
let plugin_root = std::path::PathBuf::from(&plugin.plugin_root)
.canonicalize()
.map_err(|err| ("invalid_plugin_source", err.to_string()))?;
- let expected = crate::session::data_dir()
- .join("plugins")
- .join("github")
- .join(crate::api::schema::plugin_managed_path_component(
- &plugin.plugin_id,
- ))
+ let expected = crate::plugin_paths::managed_checkout_path(&plugin.plugin_id)
.canonicalize()
.map_err(|err| ("invalid_plugin_source", err.to_string()))?;
if managed_path != expected {
diff --git a/src/app/api/plugins/mod.rs b/src/app/api/plugins/mod.rs
index e48a1506..d7a7b0ed 100644
--- a/src/app/api/plugins/mod.rs
+++ b/src/app/api/plugins/mod.rs
@@ -25,6 +25,46 @@ pub(crate) use manifest::load_plugin_manifest;
use runtime::{read_capped_plugin_output, MAX_PLUGIN_COMMANDS_IN_FLIGHT};
impl App {
+ fn replace_installed_plugins(&mut self, entries: Vec<InstalledPluginInfo>) {
+ let entries =
+ crate::persist::plugin_registry::reload_manifests(entries, |path, enabled| {
+ load_plugin_manifest(path, enabled).map_err(|(_, message)| message)
+ });
+ self.state.installed_plugins = entries
+ .into_iter()
+ .map(|plugin| (plugin.plugin_id.clone(), plugin))
+ .collect();
+ }
+
+ fn refresh_installed_plugins(&mut self) -> std::io::Result<()> {
+ if self.no_session {
+ return Ok(());
+ }
+ let entries = crate::persist::plugin_registry::try_load()?;
+ self.replace_installed_plugins(entries);
+ Ok(())
+ }
+
+ fn update_installed_plugins<T>(
+ &mut self,
+ mutation: impl FnOnce(&mut crate::app::state::InstalledPluginRegistry) -> T,
+ ) -> std::io::Result<T> {
+ if self.no_session {
+ return Ok(mutation(&mut self.state.installed_plugins));
+ }
+ let (result, entries) = crate::persist::plugin_registry::update(|entries| {
+ let mut registry = entries
+ .drain(..)
+ .map(|plugin| (plugin.plugin_id.clone(), plugin))
+ .collect();
+ let result = mutation(&mut registry);
+ *entries = registry.into_values().collect();
+ result
+ })?;
+ self.replace_installed_plugins(entries);
+ Ok(result)
+ }
+
pub(super) fn handle_plugin_link(&mut self, id: String, params: PluginLinkParams) -> String {
let mut plugin = match load_plugin_manifest(&params.path, params.enabled) {
Ok(plugin) => plugin,
@@ -39,21 +79,9 @@ impl App {
if let Err(err) = env::ensure_plugin_user_dirs(&plugin) {
return encode_error(id, "plugin_user_dir_create_failed", err.to_string());
}
- let previous = self.state.installed_plugins.get(&plugin.plugin_id).cloned();
- self.state
- .installed_plugins
- .insert(plugin.plugin_id.clone(), plugin.clone());
- if let Err(err) = self.save_plugin_registry() {
- match previous {
- Some(previous) => {
- self.state
- .installed_plugins
- .insert(previous.plugin_id.clone(), previous);
- }
- None => {
- self.state.installed_plugins.remove(&plugin.plugin_id);
- }
- }
+ if let Err(err) = self.update_installed_plugins(|plugins| {
+ plugins.insert(plugin.plugin_id.clone(), plugin.clone());
+ }) {
return encode_error(id, "plugin_registry_save_failed", err.to_string());
}
encode_success(id, ResponseResult::PluginLinked { plugin })
@@ -64,6 +92,9 @@ impl App {
Ok(plugin_id) => plugin_id,
Err(response) => return response,
};
+ if let Err(err) = self.refresh_installed_plugins() {
+ return encode_error(id, "plugin_registry_load_failed", err.to_string());
+ }
let mut plugins = self
.state
.installed_plugins
@@ -87,29 +118,18 @@ impl App {
let Some(plugin_id) = normalize_plugin_id(&params.plugin_id) else {
return invalid_plugin_id(id);
};
- let previous = self.state.installed_plugins.remove(&plugin_id);
- let removed = previous.is_some();
- let previous_panes = if removed {
- Some(self.state.plugin_panes.clone())
- } else {
- None
- };
+ let removed =
+ match self.update_installed_plugins(|plugins| plugins.remove(&plugin_id).is_some()) {
+ Ok(removed) => removed,
+ Err(err) => {
+ return encode_error(id, "plugin_registry_save_failed", err.to_string());
+ }
+ };
if removed {
// Drop plugin_panes records for this plugin (panes keep running).
self.state
.plugin_panes
.retain(|_, record| record.plugin_id != plugin_id);
- if let Err(err) = self.save_plugin_registry() {
- if let Some(previous) = previous {
- self.state
- .installed_plugins
- .insert(plugin_id.clone(), previous);
- }
- if let Some(previous_panes) = previous_panes {
- self.state.plugin_panes = previous_panes;
- }
- return encode_error(id, "plugin_registry_save_failed", err.to_string());
- }
self.clear_agent_view_for_source(&format!("plugin:{plugin_id}"));
}
encode_success(id, ResponseResult::PluginUnlinked { plugin_id, removed })
@@ -140,6 +160,9 @@ impl App {
Ok(plugin_id) => plugin_id,
Err(response) => return response,
};
+ if let Err(err) = self.refresh_installed_plugins() {
+ return encode_error(id, "plugin_registry_load_failed", err.to_string());
+ }
let mut actions = manifest_actions(&self.state.installed_plugins)
.filter(|action| {
plugin_id
@@ -156,6 +179,9 @@ impl App {
id: String,
params: PluginActionInvokeParams,
) -> String {
+ if let Err(err) = self.refresh_installed_plugins() {
+ return encode_error(id, "plugin_registry_load_failed", err.to_string());
+ }
let (plugin, action) =
match self.find_plugin_action(params.plugin_id.as_deref(), &params.action_id) {
Ok(pair) => pair,
@@ -200,6 +226,8 @@ impl App {
&mut self,
action_id: String,
) -> Result<(), String> {
+ self.refresh_installed_plugins()
+ .map_err(|err| format!("failed to load plugin registry: {err}"))?;
let (plugin, action) = self
.find_plugin_action(None, &action_id)
.map_err(|(_, message)| message)?;
@@ -230,6 +258,8 @@ impl App {
url: &str,
pane_id: crate::layout::PaneId,
) -> Result<bool, String> {
+ self.refresh_installed_plugins()
+ .map_err(|err| format!("failed to load plugin registry: {err}"))?;
let Some((plugin, handler)) = self.find_plugin_link_handler(url) else {
return Ok(false);
};
@@ -308,6 +338,9 @@ impl App {
id: String,
params: PluginPaneOpenParams,
) -> String {
+ if let Err(err) = self.refresh_installed_plugins() {
+ return encode_error(id, "plugin_registry_load_failed", err.to_string());
+ }
let Some(plugin_id) = normalize_plugin_id(&params.plugin_id) else {
return invalid_plugin_id(id);
};
@@ -580,16 +613,21 @@ impl App {
let Some(plugin_id) = normalize_plugin_id(&plugin_id) else {
return invalid_plugin_id(id);
};
- let Some(plugin) = self.state.installed_plugins.get_mut(&plugin_id) else {
- return encode_error(id, "plugin_not_found", "plugin not found");
- };
- let previous_enabled = plugin.enabled;
- plugin.enabled = enabled;
- if let Err(err) = self.save_plugin_registry() {
- if let Some(plugin) = self.state.installed_plugins.get_mut(&plugin_id) {
- plugin.enabled = previous_enabled;
+ let found = match self.update_installed_plugins(|plugins| {
+ if let Some(plugin) = plugins.get_mut(&plugin_id) {
+ plugin.enabled = enabled;
+ true
+ } else {
+ false
}
- return encode_error(id, "plugin_registry_save_failed", err.to_string());
+ }) {
+ Ok(found) => found,
+ Err(err) => {
+ return encode_error(id, "plugin_registry_save_failed", err.to_string());
+ }
+ };
+ if !found {
+ return encode_error(id, "plugin_not_found", "plugin not found");
}
let Some(plugin) = self.state.installed_plugins.get(&plugin_id).cloned() else {
return encode_error(id, "plugin_not_found", "plugin not found");
@@ -603,19 +641,6 @@ impl App {
encode_success(id, ResponseResult::PluginDisabled { plugin })
}
}
-
- pub(crate) fn save_plugin_registry(&self) -> std::io::Result<()> {
- if self.no_session {
- return Ok(());
- }
- let plugins = self
- .state
- .installed_plugins
- .values()
- .cloned()
- .collect::<Vec<_>>();
- crate::persist::plugin_registry::save(&plugins)
- }
}
fn invalid_plugin_id(id: String) -> String {
@@ -2141,6 +2166,100 @@ command = ["sh", "-c", "printf %s ${{HERDR_PANE_ID-unset}} > '{}'; sleep 1"]
assert_eq!(value["error"]["code"], "plugin_manifest_unavailable");
}
+ #[test]
+ fn non_cli_plugin_consumers_refresh_global_enabled_state() {
+ let _guard = crate::config::test_config_env_lock().lock().unwrap();
+ let previous_config_home = std::env::var_os("XDG_CONFIG_HOME");
+ let base = unique_temp_path("plugin-global-refresh");
+ std::env::set_var("XDG_CONFIG_HOME", &base);
+ let root = base.join("plugin");
+ write_manifest(&root);
+ let plugin = load_plugin_manifest(&root.display().to_string(), false).unwrap();
+ crate::persist::plugin_registry::update(|plugins| {
+ plugins.retain(|entry| entry.plugin_id != plugin.plugin_id);
+ plugins.push(plugin.clone());
+ })
+ .unwrap();
+
+ let mut app = test_app();
+ app.no_session = false;
+ let workspace = crate::workspace::Workspace::test_new("plugin-refresh");
+ let pane_id = workspace.tabs[0].root_pane;
+ app.state.workspaces = vec![workspace];
+ app.state.ensure_test_terminals();
+ app.state.active = Some(0);
+ app.state.selected = 0;
+
+ let make_stale = |app: &mut App| {
+ let mut stale = plugin.clone();
+ stale.enabled = true;
+ app.state
+ .installed_plugins
+ .insert(stale.plugin_id.clone(), stale);
+ };
+
+ make_stale(&mut app);
+ assert!(app
+ .invoke_plugin_action_from_keybind("bootstrap".into())
+ .unwrap_err()
+ .contains("disabled"));
+
+ make_stale(&mut app);
+ assert!(!app
+ .invoke_plugin_link_handler_for_url(
+ "https://github.com/ogulcancelik/herdr/issues/1174",
+ pane_id,
+ )
+ .unwrap());
+
+ make_stale(&mut app);
+ let pane = app.handle_api_request(Request {
+ id: "pane-disabled".into(),
+ method: Method::PluginPaneOpen(PluginPaneOpenParams {
+ plugin_id: "example.worktree-bootstrap".into(),
+ entrypoint: "board".into(),
+ placement: Some(PluginPanePlacement::Overlay),
+ width: None,
+ height: None,
+ workspace_id: None,
+ target_pane_id: None,
+ direction: None,
+ cwd: None,
+ focus: true,
+ env: std::collections::HashMap::new(),
+ }),
+ });
+ let pane: serde_json::Value = serde_json::from_str(&pane).unwrap();
+ assert_eq!(pane["error"]["code"], "plugin_disabled");
+
+ make_stale(&mut app);
+ let logs_before = app.state.plugin_command_logs.len();
+ let workspace = app.workspace_info(0);
+ app.run_plugin_event_hooks(&crate::api::schema::EventEnvelope {
+ event: crate::api::schema::EventKind::WorktreeCreated,
+ data: crate::api::schema::EventData::WorktreeCreated {
+ workspace: workspace.clone(),
+ worktree: crate::api::schema::WorktreeInfo {
+ path: "/tmp/repo".into(),
+ branch: Some("feature".into()),
+ is_bare: false,
+ is_detached: false,
+ is_prunable: false,
+ is_linked_worktree: true,
+ open_workspace_id: Some(workspace.workspace_id),
+ label: "feature".into(),
+ },
+ },
+ });
+ assert_eq!(app.state.plugin_command_logs.len(), logs_before);
+
+ let _ = std::fs::remove_dir_all(&base);
+ match previous_config_home {
+ Some(previous) => std::env::set_var("XDG_CONFIG_HOME", previous),
+ None => std::env::remove_var("XDG_CONFIG_HOME"),
+ }
+ }
+
#[cfg(unix)]
#[test]
fn manifest_action_invoke_runs_command_and_captures_log() {
diff --git a/src/app/api/plugins/runtime.rs b/src/app/api/plugins/runtime.rs
index 71004fe5..9106886c 100644
--- a/src/app/api/plugins/runtime.rs
+++ b/src/app/api/plugins/runtime.rs
@@ -220,6 +220,10 @@ impl App {
if !crate::api::schema::PLUGIN_HOOK_EVENT_KINDS.contains(&event.event) {
return;
}
+ if let Err(err) = self.refresh_installed_plugins() {
+ tracing::warn!(err = %err, "failed to refresh plugin registry before event hooks");
+ return;
+ }
let plugins = self
.state
.installed_plugins
diff --git a/src/cli/plugin.rs b/src/cli/plugin.rs
index 5c051cc6..bd162e40 100644
--- a/src/cli/plugin.rs
+++ b/src/cli/plugin.rs
@@ -205,7 +205,7 @@ fn plugin_install(args: &[String]) -> std::io::Result<i32> {
let post_build_plugin = load_cli_plugin_manifest(&manifest_root, true)?;
ensure_manifest_unchanged_after_build(&preview_plugin, &post_build_plugin)?;
- let final_checkout = managed_checkout_path(&preview_plugin.plugin_id);
+ let final_checkout = crate::plugin_paths::managed_checkout_path(&preview_plugin.plugin_id);
let backup_checkout = temp_root.join("previous-checkout");
let mut backup_moved = false;
if final_checkout.exists() {
@@ -296,14 +296,15 @@ fn plugin_uninstall(args: &[String]) -> std::io::Result<i32> {
}
}
Err(err) if is_connection_error(&err) => {
- let mut plugins = crate::persist::plugin_registry::load();
- let before = plugins.len();
- plugins.retain(|plugin| plugin.plugin_id != plugin_id);
- if before == plugins.len() {
+ let (removed, _) = crate::persist::plugin_registry::update(|plugins| {
+ let before = plugins.len();
+ plugins.retain(|plugin| plugin.plugin_id != plugin_id);
+ before != plugins.len()
+ })?;
+ if !removed {
eprintln!("plugin not installed: {target}");
return Ok(1);
}
- crate::persist::plugin_registry::save(&plugins)?;
}
Err(err) => return Err(err),
}
@@ -947,12 +948,14 @@ fn register_installed_plugin(
Ok(())
}
Err(err) if is_connection_error(&err) => {
- let mut plugins = crate::persist::plugin_registry::load();
- plugins.retain(|entry| entry.plugin_id != plugin.plugin_id);
crate::plugin_paths::ensure_plugin_user_dirs(&plugin.plugin_id)
.map_err(InstallFailure::Rollback)?;
- plugins.push(plugin);
- crate::persist::plugin_registry::save(&plugins).map_err(InstallFailure::Rollback)
+ crate::persist::plugin_registry::update(|plugins| {
+ plugins.retain(|entry| entry.plugin_id != plugin.plugin_id);
+ plugins.push(plugin);
+ })
+ .map(|_| ())
+ .map_err(InstallFailure::Rollback)
}
Err(err) => Err(InstallFailure::Rollback(err)),
}
@@ -1533,7 +1536,7 @@ fn confirm(prompt: &str) -> std::io::Result<bool> {
}
fn create_plugin_temp_dir(label: &str) -> std::io::Result<PathBuf> {
- let path = managed_plugins_dir().join(format!(
+ let path = crate::plugin_paths::managed_plugins_dir().join(format!(
".tmp-{label}-{}-{}",
std::process::id(),
current_unix_ms()
@@ -1542,16 +1545,6 @@ fn create_plugin_temp_dir(label: &str) -> std::io::Result<PathBuf> {
Ok(path)
}
-fn managed_plugins_dir() -> PathBuf {
- crate::session::data_dir().join("plugins")
-}
-
-fn managed_checkout_path(plugin_id: &str) -> PathBuf {
- managed_plugins_dir()
- .join("github")
- .join(crate::api::schema::plugin_managed_path_component(plugin_id))
-}
-
fn remove_managed_plugin_files(plugin: &InstalledPluginInfo) -> std::io::Result<()> {
if plugin.source.kind != PluginSourceKind::Github {
return Ok(());
@@ -1590,7 +1583,7 @@ fn is_expected_managed_path(plugin: &InstalledPluginInfo, path: &Path) -> bool {
let Ok(path) = path.canonicalize() else {
return false;
};
- let expected = managed_checkout_path(&plugin.plugin_id);
+ let expected = crate::plugin_paths::managed_checkout_path(&plugin.plugin_id);
let Ok(expected) = expected.canonicalize() else {
return false;
};
diff --git a/src/persist/plugin_registry.rs b/src/persist/plugin_registry.rs
index 5cd1a1f5..04cdd845 100644
--- a/src/persist/plugin_registry.rs
+++ b/src/persist/plugin_registry.rs
@@ -1,3 +1,4 @@
+use std::fs::OpenOptions;
use std::path::{Path, PathBuf};
use tracing::warn;
@@ -5,9 +6,29 @@ use tracing::warn;
use crate::api::schema::InstalledPluginInfo;
pub const MANIFEST_UNAVAILABLE_WARNING_PREFIX: &str = "manifest unavailable: ";
+const REGISTRY_LOCK_FILE: &str = ".plugins.lock";
fn registry_path() -> PathBuf {
- crate::session::data_dir().join("plugins.json")
+ crate::config::config_dir().join("plugins.json")
+}
+
+fn registry_lock_path() -> PathBuf {
+ crate::config::config_dir().join(REGISTRY_LOCK_FILE)
+}
+
+fn with_registry_lock<T>(operation: impl FnOnce() -> std::io::Result<T>) -> std::io::Result<T> {
+ let lock_path = registry_lock_path();
+ if let Some(parent) = lock_path.parent() {
+ std::fs::create_dir_all(parent)?;
+ }
+ let lock = OpenOptions::new()
+ .create(true)
+ .truncate(false)
+ .read(true)
+ .write(true)
+ .open(lock_path)?;
+ lock.lock()?;
+ operation()
}
fn save_json_to_path<T: serde::Serialize + ?Sized>(path: &Path, value: &T) -> std::io::Result<()> {
@@ -16,7 +37,7 @@ fn save_json_to_path<T: serde::Serialize + ?Sized>(path: &Path, value: &T) -> st
}
let json = serde_json::to_string_pretty(value)?;
let tmp_path = path.with_extension("json.tmp");
- std::fs::write(&tmp_path, &json)?;
+ std::fs::write(&tmp_path, json)?;
#[cfg(windows)]
if path.exists() {
if let Err(err) = std::fs::remove_file(path) {
@@ -31,42 +52,58 @@ fn save_json_to_path<T: serde::Serialize + ?Sized>(path: &Path, value: &T) -> st
Ok(())
}
-/// Atomically write `plugins.json` next to `session.json`.
-pub fn save(plugins: &[InstalledPluginInfo]) -> std::io::Result<()> {
- let path = registry_path();
- save_to_path(&path, plugins)
-}
-
pub fn save_to_path(path: &Path, plugins: &[InstalledPluginInfo]) -> std::io::Result<()> {
save_json_to_path(path, plugins)
}
-/// Load `plugins.json`. Returns an empty vec on any failure so a corrupt or
-/// missing file never blocks server startup.
-pub fn load() -> Vec<InstalledPluginInfo> {
- load_from_path(&registry_path())
+pub fn update<T>(
+ mutation: impl FnOnce(&mut Vec<InstalledPluginInfo>) -> T,
+) -> std::io::Result<(T, Vec<InstalledPluginInfo>)> {
+ with_registry_lock(|| {
+ let mut plugins = load_from_path_strict(&registry_path())?;
+ let result = mutation(&mut plugins);
+ plugins.sort_by(|left, right| left.plugin_id.cmp(&right.plugin_id));
+ save_to_path(&registry_path(), &plugins)?;
+ Ok((result, plugins))
+ })
}
-pub fn load_from_path(path: &Path) -> Vec<InstalledPluginInfo> {
- if !path.exists() {
- return Vec::new();
- }
- let content = match std::fs::read_to_string(path) {
- Ok(c) => c,
+pub fn try_load() -> std::io::Result<Vec<InstalledPluginInfo>> {
+ with_registry_lock(|| load_from_path_strict(&registry_path()))
+}
+
+/// Load the global registry. Returns an empty vec on failure so a corrupt or
+/// missing file never blocks server startup; mutations still use strict reads.
+pub fn load() -> Vec<InstalledPluginInfo> {
+ match try_load() {
+ Ok(plugins) => plugins,
Err(err) => {
- warn!(path = %path.display(), err = %err, "failed to read plugin registry");
- return Vec::new();
+ warn!(path = %registry_path().display(), err = %err, "failed to load plugin registry");
+ Vec::new()
}
- };
- match serde_json::from_str::<Vec<InstalledPluginInfo>>(&content) {
+ }
+}
+
+#[cfg(test)]
+pub fn load_from_path(path: &Path) -> Vec<InstalledPluginInfo> {
+ match load_from_path_strict(path) {
Ok(entries) => entries,
Err(err) => {
- warn!(path = %path.display(), err = %err, "failed to parse plugin registry, starting with empty registry");
+ warn!(path = %path.display(), err = %err, "failed to read plugin registry");
Vec::new()
}
}
}
+fn load_from_path_strict(path: &Path) -> std::io::Result<Vec<InstalledPluginInfo>> {
+ if !path.exists() {
+ return Ok(Vec::new());
+ }
+ let content = std::fs::read_to_string(path)?;
+ serde_json::from_str::<Vec<InstalledPluginInfo>>(&content)
+ .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))
+}
+
/// Re-read each entry's manifest from disk using the provided reload function.
///
/// If the manifest parses successfully, replace cached fields but keep the
@@ -160,10 +197,12 @@ mod tests {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).unwrap();
}
- std::fs::write(&path, b"this is not valid json {{{{").unwrap();
+ let corrupt = b"this is not valid json {{{{";
+ std::fs::write(&path, corrupt).unwrap();
- let loaded = load_from_path(&path);
- assert!(loaded.is_empty());
+ assert!(load_from_path_strict(&path).is_err());
+ assert!(load_from_path(&path).is_empty());
+ assert_eq!(std::fs::read(path).unwrap(), corrupt);
}
#[test]
@@ -231,22 +270,6 @@ mod tests {
}
#[test]
- fn atomic_write_temp_file_is_cleaned_up_on_rename_failure() {
- // Write to a path whose parent does not yet exist, then verify the
- // tmp file is removed when the write fails mid-way. Here we just
- // confirm a successful write leaves no .tmp file behind.
- let path = temp_registry_path("cleanup");
- save_to_path(&path, &[sample_plugin("example.cleanup")]).unwrap();
-
- let tmp = path.with_extension("json.tmp");
- assert!(
- !tmp.exists(),
- "tmp file should be cleaned up after successful rename"
- );
- assert!(path.exists());
- }
-
- #[test]
fn save_replaces_existing_registry_file() {
let path = temp_registry_path("replace-existing");
save_to_path(&path, &[sample_plugin("example.first")]).unwrap();
diff --git a/src/plugin_paths.rs b/src/plugin_paths.rs
index 18a16a77..62eed4ff 100644
--- a/src/plugin_paths.rs
+++ b/src/plugin_paths.rs
@@ -2,9 +2,18 @@ use std::path::{Path, PathBuf};
const PLUGIN_CONFIG_PATH_COMPONENT_MAX_CHARS: usize = 120;
+pub(crate) fn managed_plugins_dir() -> PathBuf {
+ crate::config::config_dir().join("plugins")
+}
+
+pub(crate) fn managed_checkout_path(plugin_id: &str) -> PathBuf {
+ managed_plugins_dir()
+ .join("github")
+ .join(crate::api::schema::plugin_managed_path_component(plugin_id))
+}
+
pub(crate) fn plugin_config_dir(plugin_id: &str) -> PathBuf {
- crate::config::config_dir()
- .join("plugins")
+ managed_plugins_dir()
.join("config")
.join(plugin_config_path_component(plugin_id))
}
@@ -37,7 +46,7 @@ fn ensure_plugin_config_dir(plugin_id: &str) -> std::io::Result<()> {
}
fn legacy_plugin_config_dirs(plugin_id: &str) -> Vec<PathBuf> {
- let plugins_dir = crate::config::config_dir().join("plugins");
+ let plugins_dir = managed_plugins_dir();
let old_unhashed =
(!matches!(plugin_id, "config" | "github")).then(|| plugins_dir.join(plugin_id));
let current_hashed =