mirror of
https://github.com/openai/codex.git
synced 2026-11-05 20:10:33 +00:00
Add marketplace list command
This commit is contained in:
@@ -16,6 +16,7 @@ use std::path::PathBuf;
|
||||
use std::time::SystemTime;
|
||||
use std::time::UNIX_EPOCH;
|
||||
|
||||
mod list;
|
||||
mod metadata;
|
||||
mod ops;
|
||||
|
||||
@@ -32,6 +33,8 @@ pub struct MarketplaceCli {
|
||||
enum MarketplaceSubcommand {
|
||||
/// Add a marketplace repository or local marketplace directory.
|
||||
Add(AddMarketplaceArgs),
|
||||
/// List configured marketplaces.
|
||||
List(list::ListMarketplaceArgs),
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
@@ -78,6 +81,7 @@ impl MarketplaceCli {
|
||||
|
||||
match subcommand {
|
||||
MarketplaceSubcommand::Add(args) => run_add(args).await?,
|
||||
MarketplaceSubcommand::List(args) => list::run_list(args).await?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
use super::metadata;
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use codex_core::config::find_codex_home;
|
||||
use codex_core::plugins::marketplace_install_root;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub(super) struct ListMarketplaceArgs {
|
||||
/// Print the configured marketplaces as JSON.
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
}
|
||||
|
||||
pub(super) async fn run_list(args: ListMarketplaceArgs) -> Result<()> {
|
||||
let codex_home = find_codex_home().context("failed to resolve CODEX_HOME")?;
|
||||
let install_root = marketplace_install_root(&codex_home);
|
||||
let marketplaces = metadata::configured_marketplaces(&codex_home, &install_root)?;
|
||||
|
||||
if args.json {
|
||||
let json = marketplaces
|
||||
.into_iter()
|
||||
.map(|marketplace| {
|
||||
let install_metadata = marketplace.install_metadata;
|
||||
let mut json = serde_json::Map::new();
|
||||
json.insert(
|
||||
"name".to_string(),
|
||||
serde_json::Value::String(marketplace.name),
|
||||
);
|
||||
json.insert(
|
||||
"sourceType".to_string(),
|
||||
serde_json::Value::String(install_metadata.config_source_type().to_string()),
|
||||
);
|
||||
match install_metadata.config_source_type() {
|
||||
"directory" => {
|
||||
json.insert(
|
||||
"path".to_string(),
|
||||
serde_json::Value::String(install_metadata.config_source()),
|
||||
);
|
||||
}
|
||||
"git" => {
|
||||
json.insert(
|
||||
"url".to_string(),
|
||||
serde_json::Value::String(install_metadata.config_source()),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
json.insert(
|
||||
"installLocation".to_string(),
|
||||
serde_json::Value::String(marketplace.install_root.display().to_string()),
|
||||
);
|
||||
serde_json::Value::Object(json)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let output = serde_json::to_string_pretty(&json)?;
|
||||
println!("{output}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if marketplaces.is_empty() {
|
||||
println!("No marketplaces configured.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for marketplace in marketplaces {
|
||||
println!("{}", marketplace.name);
|
||||
println!(
|
||||
" Source: {} {}",
|
||||
marketplace.install_metadata.config_source_type(),
|
||||
marketplace.install_metadata.source_display()
|
||||
);
|
||||
println!(" Install root: {}", marketplace.install_root.display());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,11 +1,22 @@
|
||||
use super::MarketplaceSource;
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::bail;
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_config::config_toml::ConfigToml;
|
||||
use codex_config::types::MarketplaceConfig;
|
||||
use codex_config::types::MarketplaceSourceType;
|
||||
use codex_core::plugins::validate_marketplace_root;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(super) struct ConfiguredMarketplace {
|
||||
pub(super) name: String,
|
||||
pub(super) install_root: PathBuf,
|
||||
pub(super) install_metadata: MarketplaceInstallMetadata,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(super) struct MarketplaceInstallMetadata {
|
||||
source: InstalledMarketplaceSource,
|
||||
@@ -51,6 +62,28 @@ pub(super) fn installed_marketplace_root_for_source(
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub(super) fn configured_marketplaces(
|
||||
codex_home: &Path,
|
||||
install_root: &Path,
|
||||
) -> Result<Vec<ConfiguredMarketplace>> {
|
||||
let config_path = codex_home.join(CONFIG_TOML_FILE);
|
||||
let config = read_user_config(&config_path)?;
|
||||
let mut marketplaces = config
|
||||
.marketplaces
|
||||
.into_iter()
|
||||
.map(|(name, config)| {
|
||||
let install_metadata = MarketplaceInstallMetadata::from_config(&name, &config)?;
|
||||
Ok(ConfiguredMarketplace {
|
||||
install_root: install_root.join(&name),
|
||||
name,
|
||||
install_metadata,
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
marketplaces.sort_unstable_by(|left, right| left.name.cmp(&right.name));
|
||||
Ok(marketplaces)
|
||||
}
|
||||
|
||||
impl MarketplaceInstallMetadata {
|
||||
pub(super) fn from_source(source: &MarketplaceSource, sparse_paths: &[String]) -> Self {
|
||||
let source = match source {
|
||||
@@ -66,6 +99,27 @@ impl MarketplaceInstallMetadata {
|
||||
Self { source }
|
||||
}
|
||||
|
||||
fn from_config(marketplace_name: &str, config: &MarketplaceConfig) -> Result<Self> {
|
||||
let Some(source_type) = config.source_type else {
|
||||
bail!("marketplace `{marketplace_name}` is missing source_type in user config.toml");
|
||||
};
|
||||
let Some(source) = config.source.as_ref() else {
|
||||
bail!("marketplace `{marketplace_name}` is missing source in user config.toml");
|
||||
};
|
||||
|
||||
let source = match source_type {
|
||||
MarketplaceSourceType::Directory => InstalledMarketplaceSource::LocalDirectory {
|
||||
path: PathBuf::from(source),
|
||||
},
|
||||
MarketplaceSourceType::Git => InstalledMarketplaceSource::Git {
|
||||
url: source.clone(),
|
||||
ref_name: config.ref_name.clone(),
|
||||
sparse_paths: config.sparse_paths.clone().unwrap_or_default(),
|
||||
},
|
||||
};
|
||||
Ok(Self { source })
|
||||
}
|
||||
|
||||
pub(super) fn config_source_type(&self) -> &'static str {
|
||||
match &self.source {
|
||||
InstalledMarketplaceSource::LocalDirectory { .. } => "directory",
|
||||
@@ -94,6 +148,16 @@ impl MarketplaceInstallMetadata {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn source_display(&self) -> String {
|
||||
match &self.source {
|
||||
InstalledMarketplaceSource::LocalDirectory { path } => path.display().to_string(),
|
||||
InstalledMarketplaceSource::Git { url, ref_name, .. } => match ref_name {
|
||||
Some(ref_name) => format!("{url}#{ref_name}"),
|
||||
None => url.clone(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn matches_config(&self, marketplace: &toml::Value) -> bool {
|
||||
marketplace.get("source_type").and_then(toml::Value::as_str)
|
||||
== Some(self.config_source_type())
|
||||
@@ -117,3 +181,13 @@ fn config_sparse_paths(marketplace: &toml::Value) -> Vec<String> {
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn read_user_config(config_path: &Path) -> Result<ConfigToml> {
|
||||
match std::fs::read_to_string(config_path) {
|
||||
Ok(raw) => toml::from_str::<ConfigToml>(&raw)
|
||||
.with_context(|| format!("failed to parse user config {}", config_path.display())),
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(ConfigToml::default()),
|
||||
Err(err) => Err(err)
|
||||
.with_context(|| format!("failed to read user config {}", config_path.display())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use codex_core::plugins::marketplace_install_root;
|
||||
use codex_core::plugins::validate_marketplace_root;
|
||||
use predicates::str::contains;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value as JsonValue;
|
||||
use std::path::Path;
|
||||
use tempfile::TempDir;
|
||||
|
||||
@@ -284,3 +285,89 @@ async fn marketplace_add_rejects_ref_for_local_directory() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn marketplace_list_prints_configured_marketplaces() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let source = TempDir::new()?;
|
||||
write_marketplace_source(source.path(), "first install")?;
|
||||
|
||||
codex_command(codex_home.path())?
|
||||
.args(["marketplace", "add", source.path().to_str().unwrap()])
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
codex_command(codex_home.path())?
|
||||
.args(["marketplace", "list"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("debug"))
|
||||
.stdout(contains("Source: directory"))
|
||||
.stdout(contains(
|
||||
source.path().canonicalize()?.display().to_string(),
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn marketplace_list_json_includes_source_and_install_location() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let source = TempDir::new()?;
|
||||
write_marketplace_source(source.path(), "first install")?;
|
||||
|
||||
codex_command(codex_home.path())?
|
||||
.args(["marketplace", "add", source.path().to_str().unwrap()])
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let output = codex_command(codex_home.path())?
|
||||
.args(["marketplace", "list", "--json"])
|
||||
.output()?;
|
||||
assert!(output.status.success());
|
||||
|
||||
let json: JsonValue = serde_json::from_slice(&output.stdout)?;
|
||||
let entries = json
|
||||
.as_array()
|
||||
.context("list --json should return an array")?;
|
||||
let expected_source = source.path().canonicalize()?.display().to_string();
|
||||
let expected_install_root = marketplace_install_root(codex_home.path())
|
||||
.join("debug")
|
||||
.canonicalize()?
|
||||
.display()
|
||||
.to_string();
|
||||
assert_eq!(entries.len(), 1);
|
||||
assert_eq!(
|
||||
entries[0].get("name").and_then(JsonValue::as_str),
|
||||
Some("debug")
|
||||
);
|
||||
assert_eq!(
|
||||
entries[0].get("sourceType").and_then(JsonValue::as_str),
|
||||
Some("directory")
|
||||
);
|
||||
assert_eq!(
|
||||
entries[0].get("path").and_then(JsonValue::as_str),
|
||||
Some(expected_source.as_str())
|
||||
);
|
||||
assert_eq!(
|
||||
entries[0]
|
||||
.get("installLocation")
|
||||
.and_then(JsonValue::as_str),
|
||||
Some(expected_install_root.as_str())
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn marketplace_list_without_marketplaces_prints_empty_message() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
codex_command(codex_home.path())?
|
||||
.args(["marketplace", "list"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("No marketplaces configured."));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user