use super::ConfiguredGitMarketplace;use codex_config::types::MarketplaceSourceType;use serde::Deserialize;use serde::Serialize;use std::path::Path;use std::path::PathBuf;use tempfile::TempDir;use tracing::warn;const MARKETPLACE_INSTALL_METADATA_FILE: &str = ".codex-marketplace-install.json";#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]#[serde(rename_all = "snake_case")]struct InstalledMarketplaceMetadata { source_type: MarketplaceSourceType, source: String, ref_name: Option<String>, sparse_paths: Vec<String>, revision: String,}pub(super) fn installed_marketplace_metadata_matches( root: &Path, marketplace: &ConfiguredGitMarketplace, revision: &str,) -> bool { let metadata = match std::fs::read_to_string(installed_marketplace_metadata_path(root)) { Ok(metadata) => metadata, Err(_) => return false, }; let metadata = match serde_json::from_str::<InstalledMarketplaceMetadata>(&metadata) { Ok(metadata) => metadata, Err(err) => { warn!( marketplace = marketplace.name, error = %err, "failed to parse activated marketplace metadata" ); return false; } }; metadata == installed_marketplace_metadata(marketplace, revision)}pub(super) fn write_installed_marketplace_metadata( root: &Path, marketplace: &ConfiguredGitMarketplace, revision: &str,) -> Result<(), String> { let metadata = installed_marketplace_metadata(marketplace, revision); let contents = serde_json::to_string_pretty(&metadata) .map_err(|err| format!("failed to serialize activated marketplace metadata: {err}"))?; std::fs::write(installed_marketplace_metadata_path(root), contents) .map_err(|err| format!("failed to write activated marketplace metadata: {err}"))}pub(super) fn activate_marketplace_root( destination: &Path, staged_dir: TempDir, after_activate: impl FnOnce() -> Result<(), String>,) -> Result<(), String> { let staged_root = staged_dir.path(); let Some(parent) = destination.parent() else { return Err(format!( "failed to determine marketplace install parent for {}", destination.display() )); }; std::fs::create_dir_all(parent).map_err(|err| { format!( "failed to create marketplace install parent {}: {err}", parent.display() ) })?; if destination.exists() { let backup_dir = tempfile::Builder::new() .prefix("marketplace-backup-") .tempdir_in(parent) .map_err(|err| { format!( "failed to create marketplace backup directory in {}: {err}", parent.display() ) })?; let backup_root = backup_dir.path().join("root"); std::fs::rename(destination, &backup_root).map_err(|err| { format!( "failed to move previous marketplace root out of the way at {}: {err}", destination.display() ) })?; if let Err(err) = std::fs::rename(staged_root, destination) { let rollback_result = std::fs::rename(&backup_root, destination); return match rollback_result { Ok(()) => Err(format!( "failed to activate upgraded marketplace at {}: {err}", destination.display() )), Err(rollback_err) => { let backup_path = backup_dir.keep().join("root"); Err(format!( "failed to activate upgraded marketplace at {}: {err}; failed to restore previous marketplace root (left at {}): {rollback_err}", destination.display(), backup_path.display() )) } }; } if let Err(err) = after_activate() { let remove_result = std::fs::remove_dir_all(destination); let rollback_result = remove_result.and_then(|()| std::fs::rename(&backup_root, destination)); return match rollback_result { Ok(()) => Err(err), Err(rollback_err) => { let backup_path = backup_dir.keep().join("root"); Err(format!( "{err}; failed to restore previous marketplace root at {} (left at {}): {rollback_err}", destination.display(), backup_path.display() )) } }; } return Ok(()); } std::fs::rename(staged_root, destination).map_err(|err| { format!( "failed to activate upgraded marketplace at {}: {err}", destination.display() ) })?; if let Err(err) = after_activate() { let remove_result = std::fs::remove_dir_all(destination); return match remove_result { Ok(()) => Err(err), Err(remove_err) => Err(format!( "{err}; failed to remove newly activated marketplace root at {}: {remove_err}", destination.display() )), }; } Ok(())}fn installed_marketplace_metadata( marketplace: &ConfiguredGitMarketplace, revision: &str,) -> InstalledMarketplaceMetadata { InstalledMarketplaceMetadata { source_type: MarketplaceSourceType::Git, source: marketplace.source.clone(), ref_name: marketplace.ref_name.clone(), sparse_paths: marketplace.sparse_paths.clone(), revision: revision.to_string(), }}fn installed_marketplace_metadata_path(root: &Path) -> PathBuf { root.join(MARKETPLACE_INSTALL_METADATA_FILE)}