[codex] Add marketplace remove command and shared logic (#17752)

## Summary

Move the marketplace remove implementation into shared core logic so
both the CLI command and follow-up app-server RPC can reuse the same
behavior.

This change:
- adds a shared `codex_core::plugins::remove_marketplace(...)` flow
- moves validation, config removal, and installed-root deletion out of
the CLI
- keeps the CLI as a thin wrapper over the shared implementation
- adds focused core coverage for the shared remove path

## Validation

- `just fmt`
- focused local coverage for the shared remove path
- heavier follow-up validation deferred to stacked PR CI
This commit is contained in:
xli-oai
2026-04-17 21:44:47 -07:00
committed by GitHub
Unverified
parent 6b39d0c657
commit e9c70fff3f
7 changed files with 629 additions and 0 deletions
+13
View File
@@ -1729,6 +1729,15 @@ mod tests {
assert!(matches!(cli.subcommand, Some(Subcommand::Plugin(_))));
}
#[test]
fn plugin_marketplace_remove_parses_under_plugin() {
let cli =
MultitoolCli::try_parse_from(["codex", "plugin", "marketplace", "remove", "debug"])
.expect("parse");
assert!(matches!(cli.subcommand, Some(Subcommand::Plugin(_))));
}
#[test]
fn marketplace_no_longer_parses_at_top_level() {
let add_result =
@@ -1738,6 +1747,10 @@ mod tests {
let upgrade_result =
MultitoolCli::try_parse_from(["codex", "marketplace", "upgrade", "debug"]);
assert!(upgrade_result.is_err());
let remove_result =
MultitoolCli::try_parse_from(["codex", "marketplace", "remove", "debug"]);
assert!(remove_result.is_err());
}
fn sample_exit_info(conversation_id: Option<&str>, thread_name: Option<&str>) -> AppExitInfo {
+36
View File
@@ -5,9 +5,11 @@ use clap::Parser;
use codex_core::config::Config;
use codex_core::config::find_codex_home;
use codex_core::plugins::MarketplaceAddRequest;
use codex_core::plugins::MarketplaceRemoveRequest;
use codex_core::plugins::PluginMarketplaceUpgradeOutcome;
use codex_core::plugins::PluginsManager;
use codex_core::plugins::add_marketplace;
use codex_core::plugins::remove_marketplace;
use codex_utils_cli::CliConfigOverrides;
#[derive(Debug, Parser)]
@@ -23,6 +25,7 @@ pub struct MarketplaceCli {
enum MarketplaceSubcommand {
Add(AddMarketplaceArgs),
Upgrade(UpgradeMarketplaceArgs),
Remove(RemoveMarketplaceArgs),
}
#[derive(Debug, Parser)]
@@ -47,6 +50,12 @@ struct UpgradeMarketplaceArgs {
marketplace_name: Option<String>,
}
#[derive(Debug, Parser)]
struct RemoveMarketplaceArgs {
/// Configured marketplace name to remove.
marketplace_name: String,
}
impl MarketplaceCli {
pub async fn run(self) -> Result<()> {
let MarketplaceCli {
@@ -61,6 +70,7 @@ impl MarketplaceCli {
match subcommand {
MarketplaceSubcommand::Add(args) => run_add(args).await?,
MarketplaceSubcommand::Upgrade(args) => run_upgrade(overrides, args).await?,
MarketplaceSubcommand::Remove(args) => run_remove(args).await?,
}
Ok(())
@@ -120,6 +130,26 @@ async fn run_upgrade(
print_upgrade_outcome(&outcome, marketplace_name.as_deref())
}
async fn run_remove(args: RemoveMarketplaceArgs) -> Result<()> {
let RemoveMarketplaceArgs { marketplace_name } = args;
let codex_home = find_codex_home().context("failed to resolve CODEX_HOME")?;
let outcome = remove_marketplace(
codex_home.to_path_buf(),
MarketplaceRemoveRequest { marketplace_name },
)
.await?;
println!("Removed marketplace `{}`.", outcome.marketplace_name);
if let Some(installed_root) = outcome.removed_installed_root {
println!(
"Removed installed marketplace root: {}",
installed_root.as_path().display()
);
}
Ok(())
}
fn print_upgrade_outcome(
outcome: &PluginMarketplaceUpgradeOutcome,
marketplace_name: Option<&str>,
@@ -201,4 +231,10 @@ mod tests {
let upgrade_one = UpgradeMarketplaceArgs::try_parse_from(["upgrade", "debug"]).unwrap();
assert_eq!(upgrade_one.marketplace_name.as_deref(), Some("debug"));
}
#[test]
fn remove_subcommand_parses_marketplace_name() {
let remove = RemoveMarketplaceArgs::try_parse_from(["remove", "debug"]).unwrap();
assert_eq!(remove.marketplace_name, "debug");
}
}
+70
View File
@@ -0,0 +1,70 @@
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",
last_revision: None,
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(["plugin", "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(["plugin", "marketplace", "remove", "debug"])
.assert()
.failure()
.stderr(contains(
"marketplace `debug` is not configured or installed",
));
Ok(())
}
+3
View File
@@ -63,7 +63,10 @@ pub use diagnostics::format_config_error_with_source;
pub use diagnostics::io_error_from_config_error;
pub use fingerprint::version_for_toml;
pub use marketplace_edit::MarketplaceConfigUpdate;
pub use marketplace_edit::RemoveMarketplaceConfigOutcome;
pub use marketplace_edit::record_user_marketplace;
pub use marketplace_edit::remove_user_marketplace;
pub use marketplace_edit::remove_user_marketplace_config;
pub use mcp_edit::ConfigEditsBuilder;
pub use mcp_edit::load_global_mcp_servers;
pub use mcp_types::AppToolApproval;
+189
View File
@@ -19,6 +19,13 @@ pub struct MarketplaceConfigUpdate<'a> {
pub sparse_paths: &'a [String],
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RemoveMarketplaceConfigOutcome {
Removed,
NotFound,
NameCaseMismatch { configured_name: String },
}
pub fn record_user_marketplace(
codex_home: &Path,
marketplace_name: &str,
@@ -31,6 +38,36 @@ 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 outcome = remove_user_marketplace_config(codex_home, marketplace_name)?;
Ok(outcome == RemoveMarketplaceConfigOutcome::Removed)
}
pub fn remove_user_marketplace_config(
codex_home: &Path,
marketplace_name: &str,
) -> std::io::Result<RemoveMarketplaceConfigOutcome> {
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(RemoveMarketplaceConfigOutcome::NotFound);
}
Err(err) => return Err(err),
};
let outcome = remove_marketplace(&mut doc, marketplace_name);
if outcome != RemoveMarketplaceConfigOutcome::Removed {
return Ok(outcome);
}
fs::create_dir_all(codex_home)?;
fs::write(config_path, doc.to_string())?;
Ok(RemoveMarketplaceConfigOutcome::Removed)
}
fn read_or_create_document(config_path: &Path) -> std::io::Result<DocumentMut> {
match fs::read_to_string(config_path) {
Ok(raw) => raw
@@ -80,8 +117,160 @@ fn upsert_marketplace(
marketplaces.insert(marketplace_name, TomlItem::Table(entry));
}
fn remove_marketplace(
doc: &mut DocumentMut,
marketplace_name: &str,
) -> RemoveMarketplaceConfigOutcome {
let root = doc.as_table_mut();
let Some(marketplaces_item) = root.get_mut("marketplaces") else {
return RemoveMarketplaceConfigOutcome::NotFound;
};
let mut remove_marketplaces = false;
let outcome = match marketplaces_item {
TomlItem::Table(marketplaces) => {
let outcome = if marketplaces.remove(marketplace_name).is_some() {
RemoveMarketplaceConfigOutcome::Removed
} else if let Some(configured_name) =
case_mismatched_key(marketplaces.iter().map(|(key, _)| key), marketplace_name)
{
RemoveMarketplaceConfigOutcome::NameCaseMismatch { configured_name }
} else {
RemoveMarketplaceConfigOutcome::NotFound
};
remove_marketplaces = marketplaces.is_empty();
outcome
}
TomlItem::Value(value) => {
let Some(marketplaces) = value.as_inline_table_mut() else {
return RemoveMarketplaceConfigOutcome::NotFound;
};
let outcome = if marketplaces.remove(marketplace_name).is_some() {
RemoveMarketplaceConfigOutcome::Removed
} else if let Some(configured_name) =
case_mismatched_key(marketplaces.iter().map(|(key, _)| key), marketplace_name)
{
RemoveMarketplaceConfigOutcome::NameCaseMismatch { configured_name }
} else {
RemoveMarketplaceConfigOutcome::NotFound
};
remove_marketplaces = marketplaces.is_empty();
outcome
}
_ => RemoveMarketplaceConfigOutcome::NotFound,
};
if outcome == RemoveMarketplaceConfigOutcome::Removed && remove_marketplaces {
root.remove("marketplaces");
}
outcome
}
fn case_mismatched_key<'a>(
mut keys: impl Iterator<Item = &'a str>,
requested_name: &str,
) -> Option<String> {
keys.find(|key| *key != requested_name && key.eq_ignore_ascii_case(requested_name))
.map(str::to_string)
}
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",
last_revision: None,
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);
}
#[test]
fn remove_user_marketplace_config_reports_case_mismatch() {
let codex_home = TempDir::new().unwrap();
let update = MarketplaceConfigUpdate {
last_updated: "2026-04-13T00:00:00Z",
last_revision: None,
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();
let outcome = remove_user_marketplace_config(codex_home.path(), "Debug").unwrap();
assert_eq!(
outcome,
RemoveMarketplaceConfigOutcome::NameCaseMismatch {
configured_name: "debug".to_string()
}
);
}
#[test]
fn remove_user_marketplace_config_removes_inline_table_entry() {
let codex_home = TempDir::new().unwrap();
fs::write(
codex_home.path().join(CONFIG_TOML_FILE),
r#"
marketplaces = {
debug = { source_type = "git", source = "https://github.com/owner/repo.git" },
other = { source_type = "local", source = "/tmp/marketplace" },
}
"#,
)
.unwrap();
let outcome = remove_user_marketplace_config(codex_home.path(), "debug").unwrap();
assert_eq!(outcome, RemoveMarketplaceConfigOutcome::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"));
}
}
@@ -0,0 +1,313 @@
use crate::plugins::marketplace_install_root;
use crate::plugins::validate_plugin_segment;
use codex_config::RemoveMarketplaceConfigOutcome;
use codex_config::remove_user_marketplace_config;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MarketplaceRemoveRequest {
pub marketplace_name: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MarketplaceRemoveOutcome {
pub marketplace_name: String,
pub removed_installed_root: Option<AbsolutePathBuf>,
}
#[derive(Debug, thiserror::Error)]
pub enum MarketplaceRemoveError {
#[error("{0}")]
InvalidRequest(String),
#[error("{0}")]
Internal(String),
}
pub async fn remove_marketplace(
codex_home: PathBuf,
request: MarketplaceRemoveRequest,
) -> Result<MarketplaceRemoveOutcome, MarketplaceRemoveError> {
tokio::task::spawn_blocking(move || remove_marketplace_sync(codex_home.as_path(), request))
.await
.map_err(|err| {
MarketplaceRemoveError::Internal(format!("failed to remove marketplace: {err}"))
})?
}
fn remove_marketplace_sync(
codex_home: &Path,
request: MarketplaceRemoveRequest,
) -> Result<MarketplaceRemoveOutcome, MarketplaceRemoveError> {
let marketplace_name = request.marketplace_name;
validate_plugin_segment(&marketplace_name, "marketplace name")
.map_err(MarketplaceRemoveError::InvalidRequest)?;
let destination = marketplace_install_root(codex_home).join(&marketplace_name);
let config_outcome =
remove_user_marketplace_config(codex_home, &marketplace_name).map_err(|err| {
MarketplaceRemoveError::Internal(format!(
"failed to remove marketplace '{marketplace_name}' from user config.toml: {err}"
))
})?;
if let RemoveMarketplaceConfigOutcome::NameCaseMismatch { configured_name } = &config_outcome {
return Err(MarketplaceRemoveError::InvalidRequest(format!(
"marketplace `{marketplace_name}` does not match configured marketplace `{configured_name}` exactly"
)));
}
let removed_config = config_outcome == RemoveMarketplaceConfigOutcome::Removed;
let removed_installed_root = remove_marketplace_root(&destination)?;
if removed_installed_root.is_none() && !removed_config {
return Err(MarketplaceRemoveError::InvalidRequest(format!(
"marketplace `{marketplace_name}` is not configured or installed"
)));
}
Ok(MarketplaceRemoveOutcome {
marketplace_name,
removed_installed_root,
})
}
fn remove_marketplace_root(root: &Path) -> Result<Option<AbsolutePathBuf>, MarketplaceRemoveError> {
if !root.exists() {
return Ok(None);
}
let removed_root = AbsolutePathBuf::try_from(root.to_path_buf()).map_err(|err| {
MarketplaceRemoveError::Internal(format!(
"failed to resolve installed marketplace root {}: {err}",
root.display()
))
})?;
let metadata = fs::symlink_metadata(root).map_err(|err| {
MarketplaceRemoveError::Internal(format!(
"failed to inspect installed marketplace root {}: {err}",
root.display()
))
})?;
let remove_result = if metadata.is_dir() {
fs::remove_dir_all(root)
} else {
fs::remove_file(root)
};
remove_result.map_err(|err| {
MarketplaceRemoveError::Internal(format!(
"failed to remove installed marketplace root {}: {err}",
root.display()
))
})?;
Ok(Some(removed_root))
}
#[cfg(test)]
mod tests {
use super::*;
use codex_config::MarketplaceConfigUpdate;
use codex_config::record_user_marketplace;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
#[test]
fn remove_marketplace_sync_removes_config_and_installed_root() {
let codex_home = TempDir::new().unwrap();
record_user_marketplace(
codex_home.path(),
"debug",
&MarketplaceConfigUpdate {
last_updated: "2026-04-13T00:00:00Z",
last_revision: None,
source_type: "git",
source: "https://github.com/owner/repo.git",
ref_name: Some("main"),
sparse_paths: &[],
},
)
.unwrap();
let installed_root = marketplace_install_root(codex_home.path()).join("debug");
fs::create_dir_all(installed_root.join(".agents/plugins")).unwrap();
fs::write(
installed_root.join(".agents/plugins/marketplace.json"),
"{}",
)
.unwrap();
let outcome = remove_marketplace_sync(
codex_home.path(),
MarketplaceRemoveRequest {
marketplace_name: "debug".to_string(),
},
)
.unwrap();
assert_eq!(outcome.marketplace_name, "debug");
assert_eq!(
outcome.removed_installed_root,
Some(AbsolutePathBuf::try_from(installed_root.clone()).unwrap())
);
let config =
fs::read_to_string(codex_home.path().join(codex_config::CONFIG_TOML_FILE)).unwrap();
assert!(!config.contains("[marketplaces.debug]"));
assert!(!installed_root.exists());
}
#[test]
fn remove_marketplace_sync_rejects_unknown_marketplace() {
let codex_home = TempDir::new().unwrap();
let err = remove_marketplace_sync(
codex_home.path(),
MarketplaceRemoveRequest {
marketplace_name: "debug".to_string(),
},
)
.unwrap_err();
assert_eq!(
err.to_string(),
"marketplace `debug` is not configured or installed"
);
}
#[test]
fn remove_marketplace_sync_rejects_case_mismatched_configured_name() {
let codex_home = TempDir::new().unwrap();
record_user_marketplace(
codex_home.path(),
"debug",
&MarketplaceConfigUpdate {
last_updated: "2026-04-13T00:00:00Z",
last_revision: None,
source_type: "git",
source: "https://github.com/owner/repo.git",
ref_name: Some("main"),
sparse_paths: &[],
},
)
.unwrap();
let installed_root = marketplace_install_root(codex_home.path()).join("debug");
fs::create_dir_all(&installed_root).unwrap();
let err = remove_marketplace_sync(
codex_home.path(),
MarketplaceRemoveRequest {
marketplace_name: "Debug".to_string(),
},
)
.unwrap_err();
assert_eq!(
err.to_string(),
"marketplace `Debug` does not match configured marketplace `debug` exactly"
);
assert!(installed_root.exists());
let config =
fs::read_to_string(codex_home.path().join(codex_config::CONFIG_TOML_FILE)).unwrap();
assert!(config.contains("[marketplaces.debug]"));
}
#[test]
fn remove_marketplace_sync_keeps_installed_root_when_config_removal_fails() {
let codex_home = TempDir::new().unwrap();
fs::write(
codex_home.path().join(codex_config::CONFIG_TOML_FILE),
"[marketplaces.debug\n",
)
.unwrap();
let installed_root = marketplace_install_root(codex_home.path()).join("debug");
fs::create_dir_all(&installed_root).unwrap();
let err = remove_marketplace_sync(
codex_home.path(),
MarketplaceRemoveRequest {
marketplace_name: "debug".to_string(),
},
)
.unwrap_err();
assert!(
err.to_string()
.contains("failed to remove marketplace 'debug' from user config.toml")
);
assert!(installed_root.exists());
}
#[test]
fn remove_marketplace_sync_removes_file_installed_root() {
let codex_home = TempDir::new().unwrap();
record_user_marketplace(
codex_home.path(),
"debug",
&MarketplaceConfigUpdate {
last_updated: "2026-04-13T00:00:00Z",
last_revision: None,
source_type: "git",
source: "https://github.com/owner/repo.git",
ref_name: Some("main"),
sparse_paths: &[],
},
)
.unwrap();
let installed_root = marketplace_install_root(codex_home.path()).join("debug");
fs::create_dir_all(installed_root.parent().unwrap()).unwrap();
fs::write(&installed_root, "corrupt install root").unwrap();
let outcome = remove_marketplace_sync(
codex_home.path(),
MarketplaceRemoveRequest {
marketplace_name: "debug".to_string(),
},
)
.unwrap();
assert_eq!(
outcome,
MarketplaceRemoveOutcome {
marketplace_name: "debug".to_string(),
removed_installed_root: Some(
AbsolutePathBuf::try_from(installed_root.clone()).unwrap()
),
}
);
assert!(!installed_root.exists());
let config =
fs::read_to_string(codex_home.path().join(codex_config::CONFIG_TOML_FILE)).unwrap();
assert!(!config.contains("[marketplaces.debug]"));
}
#[test]
fn remove_marketplace_sync_removes_inline_config_entry() {
let codex_home = TempDir::new().unwrap();
fs::write(
codex_home.path().join(codex_config::CONFIG_TOML_FILE),
r#"
marketplaces = { debug = { source_type = "git", source = "https://github.com/owner/repo.git" } }
"#,
)
.unwrap();
let installed_root = marketplace_install_root(codex_home.path()).join("debug");
fs::create_dir_all(&installed_root).unwrap();
let outcome = remove_marketplace_sync(
codex_home.path(),
MarketplaceRemoveRequest {
marketplace_name: "debug".to_string(),
},
)
.unwrap();
assert_eq!(outcome.marketplace_name, "debug");
assert_eq!(
outcome.removed_installed_root,
Some(AbsolutePathBuf::try_from(installed_root.clone()).unwrap())
);
assert!(!installed_root.exists());
let config =
fs::read_to_string(codex_home.path().join(codex_config::CONFIG_TOML_FILE)).unwrap();
assert!(!config.contains("debug"));
}
}
+5
View File
@@ -5,6 +5,7 @@ mod injection;
mod installed_marketplaces;
mod manager;
mod marketplace_add;
mod marketplace_remove;
mod mentions;
mod render;
mod startup_sync;
@@ -52,6 +53,10 @@ pub use marketplace_add::MarketplaceAddRequest;
pub use marketplace_add::add_marketplace;
pub(crate) use marketplace_add::is_local_marketplace_source;
pub(crate) use marketplace_add::parse_marketplace_source;
pub use marketplace_remove::MarketplaceRemoveError;
pub use marketplace_remove::MarketplaceRemoveOutcome;
pub use marketplace_remove::MarketplaceRemoveRequest;
pub use marketplace_remove::remove_marketplace;
pub(crate) use render::render_explicit_plugin_instructions;
pub(crate) use render::render_plugins_section;
pub(crate) use startup_sync::curated_plugins_repo_path;