mirror of
https://github.com/openai/codex.git
synced 2026-06-01 19:02:59 +00:00
chore: plugin/uninstall endpoint (#14111)
add `plugin/uninstall` app-server endpoint to fully rm plugin from plugins cache dir and rm entry from user config file. plugin-enablement is session-scoped, so uninstalls are only picked up in new sessions (like installs). added tests.
This commit is contained in:
@@ -10,6 +10,7 @@ use super::plugin_manifest_name;
|
||||
use super::plugin_manifest_paths;
|
||||
use super::store::DEFAULT_PLUGIN_VERSION;
|
||||
use super::store::PluginId;
|
||||
use super::store::PluginIdError;
|
||||
use super::store::PluginInstallResult;
|
||||
use super::store::PluginStore;
|
||||
use super::store::PluginStoreError;
|
||||
@@ -18,6 +19,8 @@ use crate::config::Config;
|
||||
use crate::config::ConfigService;
|
||||
use crate::config::ConfigServiceError;
|
||||
use crate::config::ConfigToml;
|
||||
use crate::config::edit::ConfigEdit;
|
||||
use crate::config::edit::ConfigEditsBuilder;
|
||||
use crate::config::profile::ConfigProfile;
|
||||
use crate::config::types::McpServerConfig;
|
||||
use crate::config::types::PluginConfig;
|
||||
@@ -290,6 +293,24 @@ impl PluginsManager {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn uninstall_plugin(&self, plugin_id: String) -> Result<(), PluginUninstallError> {
|
||||
let plugin_id = PluginId::parse(&plugin_id)?;
|
||||
let store = self.store.clone();
|
||||
let plugin_id_for_store = plugin_id.clone();
|
||||
tokio::task::spawn_blocking(move || store.uninstall(&plugin_id_for_store))
|
||||
.await
|
||||
.map_err(PluginUninstallError::join)??;
|
||||
|
||||
ConfigEditsBuilder::new(&self.codex_home)
|
||||
.with_edits([ConfigEdit::ClearPath {
|
||||
segments: vec!["plugins".to_string(), plugin_id.as_key()],
|
||||
}])
|
||||
.apply()
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn list_marketplaces_for_config(
|
||||
&self,
|
||||
config: &Config,
|
||||
@@ -428,6 +449,31 @@ impl PluginInstallError {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum PluginUninstallError {
|
||||
#[error("{0}")]
|
||||
InvalidPluginId(#[from] PluginIdError),
|
||||
|
||||
#[error("{0}")]
|
||||
Store(#[from] PluginStoreError),
|
||||
|
||||
#[error("{0}")]
|
||||
Config(#[from] anyhow::Error),
|
||||
|
||||
#[error("failed to join plugin uninstall task: {0}")]
|
||||
Join(#[from] tokio::task::JoinError),
|
||||
}
|
||||
|
||||
impl PluginUninstallError {
|
||||
fn join(source: tokio::task::JoinError) -> Self {
|
||||
Self::Join(source)
|
||||
}
|
||||
|
||||
pub fn is_invalid_request(&self) -> bool {
|
||||
matches!(self, Self::InvalidPluginId(_))
|
||||
}
|
||||
}
|
||||
|
||||
fn plugins_feature_enabled_from_stack(config_layer_stack: &ConfigLayerStack) -> bool {
|
||||
// Plugins are intentionally opt-in from the persisted user config only. Project config
|
||||
// layers should not be able to enable plugin loading for a checkout.
|
||||
@@ -1553,6 +1599,43 @@ mod tests {
|
||||
assert!(config.contains("enabled = true"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn uninstall_plugin_removes_cache_and_config_entry() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
write_plugin(
|
||||
&tmp.path().join("plugins/cache/debug"),
|
||||
"sample-plugin/local",
|
||||
"sample-plugin",
|
||||
);
|
||||
write_file(
|
||||
&tmp.path().join(CONFIG_TOML_FILE),
|
||||
r#"[features]
|
||||
plugins = true
|
||||
|
||||
[plugins."sample-plugin@debug"]
|
||||
enabled = true
|
||||
"#,
|
||||
);
|
||||
|
||||
let manager = PluginsManager::new(tmp.path().to_path_buf());
|
||||
manager
|
||||
.uninstall_plugin("sample-plugin@debug".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
manager
|
||||
.uninstall_plugin("sample-plugin@debug".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
!tmp.path()
|
||||
.join("plugins/cache/debug/sample-plugin")
|
||||
.exists()
|
||||
);
|
||||
let config = fs::read_to_string(tmp.path().join(CONFIG_TOML_FILE)).unwrap();
|
||||
assert!(!config.contains(r#"[plugins."sample-plugin@debug"]"#));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_marketplaces_includes_enabled_state() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -17,6 +17,7 @@ pub use manager::PluginCapabilitySummary;
|
||||
pub use manager::PluginInstallError;
|
||||
pub use manager::PluginInstallRequest;
|
||||
pub use manager::PluginLoadOutcome;
|
||||
pub use manager::PluginUninstallError;
|
||||
pub use manager::PluginsManager;
|
||||
pub use manager::load_plugin_apps;
|
||||
pub(crate) use manager::plugin_namespace_for_skill_path;
|
||||
|
||||
@@ -135,6 +135,15 @@ impl PluginStore {
|
||||
installed_path,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn uninstall(&self, plugin_id: &PluginId) -> Result<(), PluginStoreError> {
|
||||
let plugin_path = self
|
||||
.root
|
||||
.as_path()
|
||||
.join(&plugin_id.marketplace_name)
|
||||
.join(&plugin_id.plugin_name);
|
||||
remove_existing_target(&plugin_path)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
||||
Reference in New Issue
Block a user