mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
e9c70fff3f
## 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
277 lines
9.2 KiB
Rust
277 lines
9.2 KiB
Rust
use std::fs;
|
|
use std::io::ErrorKind;
|
|
use std::path::Path;
|
|
|
|
use toml_edit::DocumentMut;
|
|
use toml_edit::Item as TomlItem;
|
|
use toml_edit::Table as TomlTable;
|
|
use toml_edit::Value as TomlValue;
|
|
use toml_edit::value;
|
|
|
|
use crate::CONFIG_TOML_FILE;
|
|
|
|
pub struct MarketplaceConfigUpdate<'a> {
|
|
pub last_updated: &'a str,
|
|
pub last_revision: Option<&'a str>,
|
|
pub source_type: &'a str,
|
|
pub source: &'a str,
|
|
pub ref_name: Option<&'a str>,
|
|
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,
|
|
update: &MarketplaceConfigUpdate<'_>,
|
|
) -> std::io::Result<()> {
|
|
let config_path = codex_home.join(CONFIG_TOML_FILE);
|
|
let mut doc = read_or_create_document(&config_path)?;
|
|
upsert_marketplace(&mut doc, marketplace_name, update);
|
|
fs::create_dir_all(codex_home)?;
|
|
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
|
|
.parse::<DocumentMut>()
|
|
.map_err(|err| std::io::Error::new(ErrorKind::InvalidData, err)),
|
|
Err(err) if err.kind() == ErrorKind::NotFound => Ok(DocumentMut::new()),
|
|
Err(err) => Err(err),
|
|
}
|
|
}
|
|
|
|
fn upsert_marketplace(
|
|
doc: &mut DocumentMut,
|
|
marketplace_name: &str,
|
|
update: &MarketplaceConfigUpdate<'_>,
|
|
) {
|
|
let root = doc.as_table_mut();
|
|
if !root.contains_key("marketplaces") {
|
|
root.insert("marketplaces", TomlItem::Table(new_implicit_table()));
|
|
}
|
|
|
|
let Some(marketplaces_item) = root.get_mut("marketplaces") else {
|
|
return;
|
|
};
|
|
if !marketplaces_item.is_table() {
|
|
*marketplaces_item = TomlItem::Table(new_implicit_table());
|
|
}
|
|
|
|
let Some(marketplaces) = marketplaces_item.as_table_mut() else {
|
|
return;
|
|
};
|
|
let mut entry = TomlTable::new();
|
|
entry.set_implicit(false);
|
|
entry["last_updated"] = value(update.last_updated.to_string());
|
|
if let Some(last_revision) = update.last_revision {
|
|
entry["last_revision"] = value(last_revision.to_string());
|
|
}
|
|
entry["source_type"] = value(update.source_type.to_string());
|
|
entry["source"] = value(update.source.to_string());
|
|
if let Some(ref_name) = update.ref_name {
|
|
entry["ref"] = value(ref_name.to_string());
|
|
}
|
|
if !update.sparse_paths.is_empty() {
|
|
entry["sparse_paths"] = TomlItem::Value(TomlValue::Array(
|
|
update.sparse_paths.iter().map(String::as_str).collect(),
|
|
));
|
|
}
|
|
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"));
|
|
}
|
|
}
|