mirror of
https://github.com/openai/codex.git
synced 2026-11-05 20:10:33 +00:00
Add marketplace remove command
This commit is contained in:
@@ -4,6 +4,7 @@ use anyhow::bail;
|
||||
use clap::Parser;
|
||||
use codex_config::MarketplaceConfigUpdate;
|
||||
use codex_config::record_user_marketplace;
|
||||
use codex_config::remove_user_marketplace;
|
||||
use codex_core::config::find_codex_home;
|
||||
use codex_core::plugins::OPENAI_CURATED_MARKETPLACE_NAME;
|
||||
use codex_core::plugins::marketplace_install_root;
|
||||
@@ -31,6 +32,9 @@ pub struct MarketplaceCli {
|
||||
enum MarketplaceSubcommand {
|
||||
/// Add a remote marketplace repository.
|
||||
Add(AddMarketplaceArgs),
|
||||
|
||||
/// Remove a configured marketplace.
|
||||
Remove(RemoveMarketplaceArgs),
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
@@ -51,6 +55,12 @@ struct AddMarketplaceArgs {
|
||||
sparse_paths: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
struct RemoveMarketplaceArgs {
|
||||
/// Configured marketplace name to remove.
|
||||
marketplace_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(super) enum MarketplaceSource {
|
||||
Git {
|
||||
@@ -74,6 +84,7 @@ impl MarketplaceCli {
|
||||
|
||||
match subcommand {
|
||||
MarketplaceSubcommand::Add(args) => run_add(args).await?,
|
||||
MarketplaceSubcommand::Remove(args) => run_remove(args).await?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -177,6 +188,38 @@ async fn run_add(args: AddMarketplaceArgs) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_remove(args: RemoveMarketplaceArgs) -> Result<()> {
|
||||
let RemoveMarketplaceArgs { marketplace_name } = args;
|
||||
validate_plugin_segment(&marketplace_name, "marketplace name").map_err(anyhow::Error::msg)?;
|
||||
|
||||
let codex_home = find_codex_home().context("failed to resolve CODEX_HOME")?;
|
||||
let install_root = marketplace_install_root(&codex_home);
|
||||
let destination = install_root.join(safe_marketplace_dir_name(&marketplace_name)?);
|
||||
let removed_root = ops::remove_marketplace_root(&destination).with_context(|| {
|
||||
format!(
|
||||
"failed to remove installed marketplace root {}",
|
||||
destination.display()
|
||||
)
|
||||
})?;
|
||||
let removed_config =
|
||||
remove_user_marketplace(&codex_home, &marketplace_name).with_context(|| {
|
||||
format!("failed to remove marketplace `{marketplace_name}` from user config.toml")
|
||||
})?;
|
||||
if !removed_root && !removed_config {
|
||||
bail!("marketplace `{marketplace_name}` is not configured or installed");
|
||||
}
|
||||
|
||||
println!("Removed marketplace `{marketplace_name}`.");
|
||||
if removed_root {
|
||||
println!(
|
||||
"Removed installed marketplace root: {}",
|
||||
destination.display()
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn record_added_marketplace(
|
||||
codex_home: &Path,
|
||||
marketplace_name: &str,
|
||||
@@ -559,4 +602,10 @@ mod tests {
|
||||
vec!["plugins/foo", "skills/bar"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_subcommand_parses_marketplace_name() {
|
||||
let remove = RemoveMarketplaceArgs::try_parse_from(["remove", "debug"]).unwrap();
|
||||
assert_eq!(remove.marketplace_name, "debug");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +79,15 @@ pub(super) fn replace_marketplace_root(staged_root: &Path, destination: &Path) -
|
||||
fs::rename(staged_root, destination).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub(super) fn remove_marketplace_root(root: &Path) -> Result<bool> {
|
||||
if !root.exists() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
fs::remove_dir_all(root)?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub(super) fn marketplace_staging_root(install_root: &Path) -> PathBuf {
|
||||
install_root.join(".staging")
|
||||
}
|
||||
@@ -115,4 +124,13 @@ mod tests {
|
||||
"installed"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_marketplace_root_returns_false_when_missing() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
|
||||
let removed = remove_marketplace_root(&temp_dir.path().join("missing")).unwrap();
|
||||
|
||||
assert!(!removed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
use anyhow::Result;
|
||||
use codex_config::MarketplaceConfigUpdate;
|
||||
use codex_config::record_user_marketplace;
|
||||
use codex_core::plugins::marketplace_install_root;
|
||||
use predicates::str::contains;
|
||||
use std::path::Path;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn codex_command(codex_home: &Path) -> Result<assert_cmd::Command> {
|
||||
let mut cmd = assert_cmd::Command::new(codex_utils_cargo_bin::cargo_bin("codex")?);
|
||||
cmd.env("CODEX_HOME", codex_home);
|
||||
Ok(cmd)
|
||||
}
|
||||
|
||||
fn configured_marketplace_update() -> MarketplaceConfigUpdate<'static> {
|
||||
MarketplaceConfigUpdate {
|
||||
last_updated: "2026-04-13T00:00:00Z",
|
||||
source_type: "git",
|
||||
source: "https://github.com/owner/repo.git",
|
||||
ref_name: Some("main"),
|
||||
sparse_paths: &[],
|
||||
}
|
||||
}
|
||||
|
||||
fn write_installed_marketplace(codex_home: &Path, marketplace_name: &str) -> Result<()> {
|
||||
let root = marketplace_install_root(codex_home).join(marketplace_name);
|
||||
std::fs::create_dir_all(root.join(".agents/plugins"))?;
|
||||
std::fs::write(root.join(".agents/plugins/marketplace.json"), "{}")?;
|
||||
std::fs::write(root.join("marker.txt"), "installed")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn marketplace_remove_deletes_config_and_installed_root() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
record_user_marketplace(codex_home.path(), "debug", &configured_marketplace_update())?;
|
||||
write_installed_marketplace(codex_home.path(), "debug")?;
|
||||
|
||||
codex_command(codex_home.path())?
|
||||
.args(["marketplace", "remove", "debug"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("Removed marketplace `debug`."));
|
||||
|
||||
let config_path = codex_home.path().join("config.toml");
|
||||
let config = std::fs::read_to_string(config_path)?;
|
||||
assert!(!config.contains("[marketplaces.debug]"));
|
||||
assert!(
|
||||
!marketplace_install_root(codex_home.path())
|
||||
.join("debug")
|
||||
.exists()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn marketplace_remove_rejects_unknown_marketplace() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
codex_command(codex_home.path())?
|
||||
.args(["marketplace", "remove", "debug"])
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(contains(
|
||||
"marketplace `debug` is not configured or installed",
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -61,6 +61,7 @@ pub use diagnostics::io_error_from_config_error;
|
||||
pub use fingerprint::version_for_toml;
|
||||
pub use marketplace_edit::MarketplaceConfigUpdate;
|
||||
pub use marketplace_edit::record_user_marketplace;
|
||||
pub use marketplace_edit::remove_user_marketplace;
|
||||
pub use mcp_edit::ConfigEditsBuilder;
|
||||
pub use mcp_edit::load_global_mcp_servers;
|
||||
pub use mcp_types::AppToolApproval;
|
||||
|
||||
@@ -30,6 +30,26 @@ pub fn record_user_marketplace(
|
||||
fs::write(config_path, doc.to_string())
|
||||
}
|
||||
|
||||
pub fn remove_user_marketplace(codex_home: &Path, marketplace_name: &str) -> std::io::Result<bool> {
|
||||
let config_path = codex_home.join(CONFIG_TOML_FILE);
|
||||
let mut doc = match fs::read_to_string(&config_path) {
|
||||
Ok(raw) => raw
|
||||
.parse::<DocumentMut>()
|
||||
.map_err(|err| std::io::Error::new(ErrorKind::InvalidData, err))?,
|
||||
Err(err) if err.kind() == ErrorKind::NotFound => return Ok(false),
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
|
||||
let removed = remove_marketplace(&mut doc, marketplace_name);
|
||||
if !removed {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
fs::create_dir_all(codex_home)?;
|
||||
fs::write(config_path, doc.to_string())?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn read_or_create_document(config_path: &Path) -> std::io::Result<DocumentMut> {
|
||||
match fs::read_to_string(config_path) {
|
||||
Ok(raw) => raw
|
||||
@@ -76,8 +96,68 @@ fn upsert_marketplace(
|
||||
marketplaces.insert(marketplace_name, TomlItem::Table(entry));
|
||||
}
|
||||
|
||||
fn remove_marketplace(doc: &mut DocumentMut, marketplace_name: &str) -> bool {
|
||||
let root = doc.as_table_mut();
|
||||
let Some(marketplaces_item) = root.get_mut("marketplaces") else {
|
||||
return false;
|
||||
};
|
||||
let Some(marketplaces) = marketplaces_item.as_table_mut() else {
|
||||
return false;
|
||||
};
|
||||
if marketplaces.remove(marketplace_name).is_none() {
|
||||
return false;
|
||||
}
|
||||
if marketplaces.is_empty() {
|
||||
root.remove("marketplaces");
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn new_implicit_table() -> TomlTable {
|
||||
let mut table = TomlTable::new();
|
||||
table.set_implicit(true);
|
||||
table
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn remove_user_marketplace_removes_requested_entry() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
let update = MarketplaceConfigUpdate {
|
||||
last_updated: "2026-04-13T00:00:00Z",
|
||||
source_type: "git",
|
||||
source: "https://github.com/owner/repo.git",
|
||||
ref_name: Some("main"),
|
||||
sparse_paths: &[],
|
||||
};
|
||||
record_user_marketplace(codex_home.path(), "debug", &update).unwrap();
|
||||
record_user_marketplace(codex_home.path(), "other", &update).unwrap();
|
||||
|
||||
let removed = remove_user_marketplace(codex_home.path(), "debug").unwrap();
|
||||
|
||||
assert!(removed);
|
||||
let config: toml::Value =
|
||||
toml::from_str(&fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE)).unwrap())
|
||||
.unwrap();
|
||||
let marketplaces = config
|
||||
.get("marketplaces")
|
||||
.and_then(toml::Value::as_table)
|
||||
.unwrap();
|
||||
assert_eq!(marketplaces.len(), 1);
|
||||
assert!(marketplaces.contains_key("other"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_user_marketplace_returns_false_when_missing() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
|
||||
let removed = remove_user_marketplace(codex_home.path(), "debug").unwrap();
|
||||
|
||||
assert!(!removed);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user