mirror of
https://github.com/openai/codex.git
synced 2026-05-18 18:22:39 +00:00
feat(cli): add plugin marketplace commands
This commit is contained in:
1
codex-rs/Cargo.lock
generated
1
codex-rs/Cargo.lock
generated
@@ -2223,6 +2223,7 @@ dependencies = [
|
||||
"codex-mcp-server",
|
||||
"codex-memories-write",
|
||||
"codex-models-manager",
|
||||
"codex-plugin",
|
||||
"codex-protocol",
|
||||
"codex-responses-api-proxy",
|
||||
"codex-rmcp-client",
|
||||
|
||||
@@ -41,6 +41,7 @@ codex-memories-write = { workspace = true }
|
||||
codex-mcp = { workspace = true }
|
||||
codex-mcp-server = { workspace = true }
|
||||
codex-models-manager = { workspace = true }
|
||||
codex-plugin = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-responses-api-proxy = { workspace = true }
|
||||
codex-rmcp-client = { workspace = true }
|
||||
|
||||
@@ -48,11 +48,13 @@ mod app_cmd;
|
||||
mod desktop_app;
|
||||
mod marketplace_cmd;
|
||||
mod mcp_cmd;
|
||||
mod plugin_cmd;
|
||||
#[cfg(not(windows))]
|
||||
mod wsl_paths;
|
||||
|
||||
use crate::marketplace_cmd::MarketplaceCli;
|
||||
use crate::mcp_cmd::McpCli;
|
||||
use crate::plugin_cmd::PluginCli;
|
||||
use crate::plugin_cmd::PluginSubcommand;
|
||||
|
||||
use codex_core::build_models_manager;
|
||||
use codex_core::config::Config;
|
||||
@@ -181,22 +183,6 @@ enum Subcommand {
|
||||
Features(FeaturesCli),
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(bin_name = "codex plugin")]
|
||||
struct PluginCli {
|
||||
#[clap(flatten)]
|
||||
pub config_overrides: CliConfigOverrides,
|
||||
|
||||
#[command(subcommand)]
|
||||
subcommand: PluginSubcommand,
|
||||
}
|
||||
|
||||
#[derive(Debug, clap::Subcommand)]
|
||||
enum PluginSubcommand {
|
||||
/// Manage plugin marketplaces for Codex.
|
||||
Marketplace(MarketplaceCli),
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
struct CompletionCommand {
|
||||
/// Shell to generate completions for
|
||||
@@ -886,10 +872,28 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
} = plugin_cli;
|
||||
prepend_config_flags(&mut config_overrides, root_config_overrides.clone());
|
||||
match subcommand {
|
||||
PluginSubcommand::Add(args) => {
|
||||
let overrides = config_overrides
|
||||
.parse_overrides()
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
plugin_cmd::run_plugin_add(overrides, args).await?;
|
||||
}
|
||||
PluginSubcommand::List(args) => {
|
||||
let overrides = config_overrides
|
||||
.parse_overrides()
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
plugin_cmd::run_plugin_list(overrides, args).await?;
|
||||
}
|
||||
PluginSubcommand::Marketplace(mut marketplace_cli) => {
|
||||
prepend_config_flags(&mut marketplace_cli.config_overrides, config_overrides);
|
||||
marketplace_cli.run().await?;
|
||||
}
|
||||
PluginSubcommand::Remove(args) => {
|
||||
let overrides = config_overrides
|
||||
.parse_overrides()
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
plugin_cmd::run_plugin_remove(overrides, args).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(Subcommand::AppServer(app_server_cli)) => {
|
||||
@@ -2040,6 +2044,7 @@ mod tests {
|
||||
|
||||
for (subcommand, usage) in [
|
||||
("add", "Usage: codex plugin marketplace add"),
|
||||
("list", "Usage: codex plugin marketplace list"),
|
||||
("upgrade", "Usage: codex plugin marketplace upgrade"),
|
||||
("remove", "Usage: codex plugin marketplace remove"),
|
||||
] {
|
||||
@@ -2066,6 +2071,45 @@ mod tests {
|
||||
assert!(matches!(cli.subcommand, Some(Subcommand::Plugin(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_add_parses_under_plugin() {
|
||||
let cli = MultitoolCli::try_parse_from([
|
||||
"codex",
|
||||
"plugin",
|
||||
"add",
|
||||
"sample",
|
||||
"--marketplace",
|
||||
"debug",
|
||||
])
|
||||
.expect("parse");
|
||||
|
||||
assert!(matches!(cli.subcommand, Some(Subcommand::Plugin(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_list_parses_under_plugin() {
|
||||
let cli =
|
||||
MultitoolCli::try_parse_from(["codex", "plugin", "list", "--marketplace", "debug"])
|
||||
.expect("parse");
|
||||
|
||||
assert!(matches!(cli.subcommand, Some(Subcommand::Plugin(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_remove_parses_under_plugin() {
|
||||
let cli = MultitoolCli::try_parse_from([
|
||||
"codex",
|
||||
"plugin",
|
||||
"remove",
|
||||
"sample",
|
||||
"--marketplace",
|
||||
"debug",
|
||||
])
|
||||
.expect("parse");
|
||||
|
||||
assert!(matches!(cli.subcommand, Some(Subcommand::Plugin(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_parses_as_update_subcommand() {
|
||||
let cli = MultitoolCli::try_parse_from(["codex", "update"]).expect("parse");
|
||||
|
||||
@@ -6,10 +6,13 @@ use codex_core::config::Config;
|
||||
use codex_core::config::find_codex_home;
|
||||
use codex_core_plugins::PluginMarketplaceUpgradeOutcome;
|
||||
use codex_core_plugins::PluginsManager;
|
||||
use codex_core_plugins::installed_marketplaces::marketplace_install_root;
|
||||
use codex_core_plugins::installed_marketplaces::resolve_configured_marketplace_root;
|
||||
use codex_core_plugins::marketplace_add::MarketplaceAddRequest;
|
||||
use codex_core_plugins::marketplace_add::add_marketplace;
|
||||
use codex_core_plugins::marketplace_remove::MarketplaceRemoveRequest;
|
||||
use codex_core_plugins::marketplace_remove::remove_marketplace;
|
||||
use codex_plugin::validate_plugin_segment;
|
||||
use codex_utils_cli::CliConfigOverrides;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
@@ -25,6 +28,7 @@ pub struct MarketplaceCli {
|
||||
#[derive(Debug, clap::Subcommand)]
|
||||
enum MarketplaceSubcommand {
|
||||
Add(AddMarketplaceArgs),
|
||||
List,
|
||||
Upgrade(UpgradeMarketplaceArgs),
|
||||
Remove(RemoveMarketplaceArgs),
|
||||
}
|
||||
@@ -73,6 +77,7 @@ impl MarketplaceCli {
|
||||
|
||||
match subcommand {
|
||||
MarketplaceSubcommand::Add(args) => run_add(args).await?,
|
||||
MarketplaceSubcommand::List => run_list(overrides).await?,
|
||||
MarketplaceSubcommand::Upgrade(args) => run_upgrade(overrides, args).await?,
|
||||
MarketplaceSubcommand::Remove(args) => run_remove(args).await?,
|
||||
}
|
||||
@@ -118,6 +123,48 @@ async fn run_add(args: AddMarketplaceArgs) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_list(overrides: Vec<(String, toml::Value)>) -> Result<()> {
|
||||
let config = Config::load_with_cli_overrides(overrides)
|
||||
.await
|
||||
.context("failed to load configuration")?;
|
||||
let configured_marketplaces = config
|
||||
.config_layer_stack
|
||||
.get_user_layer()
|
||||
.and_then(|layer| layer.config.get("marketplaces"))
|
||||
.and_then(toml::Value::as_table);
|
||||
let Some(configured_marketplaces) = configured_marketplaces else {
|
||||
println!("No configured plugin marketplaces.");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if configured_marketplaces.is_empty() {
|
||||
println!("No configured plugin marketplaces.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let default_install_root = marketplace_install_root(config.codex_home.as_path());
|
||||
for (marketplace_name, marketplace) in configured_marketplaces {
|
||||
if !marketplace.is_table() {
|
||||
eprintln!("Ignoring invalid marketplace `{marketplace_name}`: expected table.");
|
||||
continue;
|
||||
}
|
||||
if let Err(err) = validate_plugin_segment(marketplace_name, "marketplace name") {
|
||||
eprintln!("Ignoring invalid marketplace `{marketplace_name}`: {err}.");
|
||||
continue;
|
||||
}
|
||||
let root = resolve_configured_marketplace_root(
|
||||
marketplace_name,
|
||||
marketplace,
|
||||
default_install_root.as_path(),
|
||||
)
|
||||
.map(|root| root.display().to_string())
|
||||
.unwrap_or_else(|| "<invalid source>".to_string());
|
||||
println!("{marketplace_name}\t{root}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_upgrade(
|
||||
overrides: Vec<(String, toml::Value)>,
|
||||
args: UpgradeMarketplaceArgs,
|
||||
|
||||
280
codex-rs/cli/src/plugin_cmd.rs
Normal file
280
codex-rs/cli/src/plugin_cmd.rs
Normal file
@@ -0,0 +1,280 @@
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::bail;
|
||||
use clap::Parser;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::find_codex_home;
|
||||
use codex_core_plugins::ConfiguredMarketplace;
|
||||
use codex_core_plugins::PluginInstallRequest;
|
||||
use codex_core_plugins::PluginsConfigInput;
|
||||
use codex_core_plugins::PluginsManager;
|
||||
use codex_plugin::PluginId;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_cli::CliConfigOverrides;
|
||||
|
||||
use crate::marketplace_cmd::MarketplaceCli;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(bin_name = "codex plugin")]
|
||||
pub struct PluginCli {
|
||||
#[clap(flatten)]
|
||||
pub config_overrides: CliConfigOverrides,
|
||||
|
||||
#[command(subcommand)]
|
||||
pub subcommand: PluginSubcommand,
|
||||
}
|
||||
|
||||
#[derive(Debug, clap::Subcommand)]
|
||||
pub enum PluginSubcommand {
|
||||
/// Install a plugin from a marketplace.
|
||||
Add(AddPluginArgs),
|
||||
|
||||
/// List marketplace plugins.
|
||||
List(ListPluginsArgs),
|
||||
|
||||
/// Manage plugin marketplaces for Codex.
|
||||
Marketplace(MarketplaceCli),
|
||||
|
||||
/// Remove an installed plugin.
|
||||
Remove(RemovePluginArgs),
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(bin_name = "codex plugin add")]
|
||||
pub struct AddPluginArgs {
|
||||
/// Plugin to install. Accepts <plugin> with --marketplace or <plugin>@<marketplace>.
|
||||
plugin: String,
|
||||
|
||||
/// Marketplace name containing the plugin.
|
||||
#[arg(long = "marketplace", short = 'm')]
|
||||
marketplace_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(bin_name = "codex plugin list")]
|
||||
pub struct ListPluginsArgs {
|
||||
/// Only list plugins in this marketplace.
|
||||
#[arg(long = "marketplace", short = 'm')]
|
||||
marketplace_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(bin_name = "codex plugin remove")]
|
||||
pub struct RemovePluginArgs {
|
||||
/// Plugin to remove. Accepts <plugin> with --marketplace or <plugin>@<marketplace>.
|
||||
plugin: String,
|
||||
|
||||
/// Marketplace name containing the plugin.
|
||||
#[arg(long = "marketplace", short = 'm')]
|
||||
marketplace_name: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn run_plugin_add(
|
||||
overrides: Vec<(String, toml::Value)>,
|
||||
args: AddPluginArgs,
|
||||
) -> Result<()> {
|
||||
let PluginCommandContext {
|
||||
plugins_input,
|
||||
manager,
|
||||
} = load_plugin_command_context(overrides).await?;
|
||||
let PluginSelection {
|
||||
plugin_name,
|
||||
marketplace_name,
|
||||
..
|
||||
} = parse_plugin_selection(args.plugin, args.marketplace_name)?;
|
||||
let marketplace =
|
||||
find_marketplace_for_plugin(&manager, &plugins_input, &marketplace_name, &plugin_name)?;
|
||||
let outcome = manager
|
||||
.install_plugin(PluginInstallRequest {
|
||||
plugin_name,
|
||||
marketplace_path: marketplace.path,
|
||||
})
|
||||
.await?;
|
||||
|
||||
println!(
|
||||
"Added plugin `{}` from marketplace `{}`.",
|
||||
outcome.plugin_id.plugin_name, outcome.plugin_id.marketplace_name
|
||||
);
|
||||
println!(
|
||||
"Installed plugin root: {}",
|
||||
outcome.installed_path.as_path().display()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn run_plugin_list(
|
||||
overrides: Vec<(String, toml::Value)>,
|
||||
args: ListPluginsArgs,
|
||||
) -> Result<()> {
|
||||
let PluginCommandContext {
|
||||
plugins_input,
|
||||
manager,
|
||||
..
|
||||
} = load_plugin_command_context(overrides).await?;
|
||||
let current_dir = AbsolutePathBuf::try_from(std::env::current_dir()?)
|
||||
.context("failed to resolve current directory")?;
|
||||
let outcome = manager
|
||||
.list_marketplaces_for_config(&plugins_input, &[current_dir])
|
||||
.context("failed to list marketplace plugins")?;
|
||||
|
||||
let marketplaces = outcome
|
||||
.marketplaces
|
||||
.into_iter()
|
||||
.filter(|marketplace| {
|
||||
args.marketplace_name
|
||||
.as_ref()
|
||||
.is_none_or(|name| marketplace.name == *name)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if marketplaces.is_empty() {
|
||||
if let Some(marketplace_name) = args.marketplace_name {
|
||||
println!("No plugins found in marketplace `{marketplace_name}`.");
|
||||
} else {
|
||||
println!("No marketplace plugins found.");
|
||||
}
|
||||
} else {
|
||||
for marketplace in marketplaces {
|
||||
println!("Marketplace `{}`", marketplace.name);
|
||||
println!("Path: {}", marketplace.path.as_path().display());
|
||||
for plugin in &marketplace.plugins {
|
||||
let state = if plugin.installed && plugin.enabled {
|
||||
"installed, enabled"
|
||||
} else if plugin.installed {
|
||||
"installed, disabled"
|
||||
} else {
|
||||
"not installed"
|
||||
};
|
||||
println!(" {} ({state})", plugin.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for error in outcome.errors {
|
||||
eprintln!(
|
||||
"Failed to load marketplace {}: {}",
|
||||
error.path.as_path().display(),
|
||||
error.message
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn run_plugin_remove(
|
||||
overrides: Vec<(String, toml::Value)>,
|
||||
args: RemovePluginArgs,
|
||||
) -> Result<()> {
|
||||
let PluginCommandContext { manager, .. } = load_plugin_command_context(overrides).await?;
|
||||
let selection = parse_plugin_selection(args.plugin, args.marketplace_name)?;
|
||||
|
||||
manager.uninstall_plugin(selection.plugin_key).await?;
|
||||
println!(
|
||||
"Removed plugin `{}` from marketplace `{}`.",
|
||||
selection.plugin_name, selection.marketplace_name
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct PluginCommandContext {
|
||||
plugins_input: PluginsConfigInput,
|
||||
manager: PluginsManager,
|
||||
}
|
||||
|
||||
async fn load_plugin_command_context(
|
||||
overrides: Vec<(String, toml::Value)>,
|
||||
) -> Result<PluginCommandContext> {
|
||||
let codex_home = find_codex_home().context("failed to resolve CODEX_HOME")?;
|
||||
let config = Config::load_with_cli_overrides(overrides)
|
||||
.await
|
||||
.context("failed to load configuration")?;
|
||||
let plugins_input = config.plugins_config_input();
|
||||
let manager = PluginsManager::new(codex_home.to_path_buf());
|
||||
Ok(PluginCommandContext {
|
||||
plugins_input,
|
||||
manager,
|
||||
})
|
||||
}
|
||||
|
||||
struct PluginSelection {
|
||||
plugin_name: String,
|
||||
marketplace_name: String,
|
||||
plugin_key: String,
|
||||
}
|
||||
|
||||
fn parse_plugin_selection(
|
||||
plugin: String,
|
||||
marketplace_name: Option<String>,
|
||||
) -> Result<PluginSelection> {
|
||||
match (PluginId::parse(&plugin), marketplace_name) {
|
||||
(Ok(plugin_id), None) => {
|
||||
let plugin_key = plugin_id.as_key();
|
||||
Ok(PluginSelection {
|
||||
plugin_name: plugin_id.plugin_name,
|
||||
marketplace_name: plugin_id.marketplace_name,
|
||||
plugin_key,
|
||||
})
|
||||
}
|
||||
(Ok(plugin_id), Some(marketplace_name)) => {
|
||||
if plugin_id.marketplace_name != marketplace_name {
|
||||
bail!(
|
||||
"plugin id `{}` belongs to marketplace `{}`, but --marketplace specified `{}`",
|
||||
plugin,
|
||||
plugin_id.marketplace_name,
|
||||
marketplace_name
|
||||
);
|
||||
}
|
||||
let plugin_key = plugin_id.as_key();
|
||||
Ok(PluginSelection {
|
||||
plugin_name: plugin_id.plugin_name,
|
||||
marketplace_name: plugin_id.marketplace_name,
|
||||
plugin_key,
|
||||
})
|
||||
}
|
||||
(Err(_), Some(marketplace_name)) => {
|
||||
let plugin_id = PluginId::new(plugin, marketplace_name)?;
|
||||
let plugin_key = plugin_id.as_key();
|
||||
Ok(PluginSelection {
|
||||
plugin_name: plugin_id.plugin_name,
|
||||
marketplace_name: plugin_id.marketplace_name,
|
||||
plugin_key,
|
||||
})
|
||||
}
|
||||
(Err(_), None) => {
|
||||
bail!("plugin requires --marketplace unless passed as <plugin>@<marketplace>")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn find_marketplace_for_plugin(
|
||||
manager: &PluginsManager,
|
||||
plugins_input: &PluginsConfigInput,
|
||||
marketplace_name: &str,
|
||||
plugin_name: &str,
|
||||
) -> Result<ConfiguredMarketplace> {
|
||||
let current_dir = AbsolutePathBuf::try_from(std::env::current_dir()?)
|
||||
.context("failed to resolve current directory")?;
|
||||
let matches = manager
|
||||
.list_marketplaces_for_config(plugins_input, &[current_dir])
|
||||
.context("failed to list marketplace plugins")?
|
||||
.marketplaces
|
||||
.into_iter()
|
||||
.filter(|marketplace| marketplace.name == marketplace_name)
|
||||
.filter(|marketplace| {
|
||||
marketplace
|
||||
.plugins
|
||||
.iter()
|
||||
.any(|plugin| plugin.name == plugin_name)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
match matches.as_slice() {
|
||||
[] => bail!("plugin `{plugin_name}` was not found in marketplace `{marketplace_name}`"),
|
||||
[marketplace] => Ok(marketplace.clone()),
|
||||
_ => bail!(
|
||||
"plugin `{plugin_name}` in marketplace `{marketplace_name}` matched multiple marketplace roots"
|
||||
),
|
||||
}
|
||||
}
|
||||
178
codex-rs/cli/tests/plugin_cli.rs
Normal file
178
codex-rs/cli/tests/plugin_cli.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
use anyhow::Result;
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_config::MarketplaceConfigUpdate;
|
||||
use codex_config::record_user_marketplace;
|
||||
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_local_marketplace(source: &str) -> MarketplaceConfigUpdate<'_> {
|
||||
MarketplaceConfigUpdate {
|
||||
last_updated: "2026-05-06T00:00:00Z",
|
||||
last_revision: None,
|
||||
source_type: "local",
|
||||
source,
|
||||
ref_name: None,
|
||||
sparse_paths: &[],
|
||||
}
|
||||
}
|
||||
|
||||
fn write_plugins_enabled_config(codex_home: &Path) -> Result<()> {
|
||||
std::fs::write(
|
||||
codex_home.join(CONFIG_TOML_FILE),
|
||||
r#"[features]
|
||||
plugins = true
|
||||
"#,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_marketplace_source(source: &Path) -> Result<()> {
|
||||
std::fs::create_dir_all(source.join(".agents/plugins"))?;
|
||||
std::fs::create_dir_all(source.join("plugins/sample/.codex-plugin"))?;
|
||||
std::fs::write(
|
||||
source.join(".agents/plugins/marketplace.json"),
|
||||
r#"{
|
||||
"name": "debug",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "sample",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./plugins/sample"
|
||||
}
|
||||
}
|
||||
]
|
||||
}"#,
|
||||
)?;
|
||||
std::fs::write(
|
||||
source.join("plugins/sample/.codex-plugin/plugin.json"),
|
||||
r#"{"name":"sample","description":"Sample plugin"}"#,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn marketplace_list_shows_configured_marketplace_names() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let source = TempDir::new()?;
|
||||
write_plugins_enabled_config(codex_home.path())?;
|
||||
write_marketplace_source(source.path())?;
|
||||
let source_path = source.path().to_string_lossy().into_owned();
|
||||
record_user_marketplace(
|
||||
codex_home.path(),
|
||||
"debug",
|
||||
&configured_local_marketplace(&source_path),
|
||||
)?;
|
||||
|
||||
codex_command(codex_home.path())?
|
||||
.args(["plugin", "marketplace", "list"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("debug"))
|
||||
.stdout(contains(source.path().display().to_string()));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_list_shows_plugins_grouped_by_marketplace() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let source = TempDir::new()?;
|
||||
write_plugins_enabled_config(codex_home.path())?;
|
||||
write_marketplace_source(source.path())?;
|
||||
let source_path = source.path().to_string_lossy().into_owned();
|
||||
record_user_marketplace(
|
||||
codex_home.path(),
|
||||
"debug",
|
||||
&configured_local_marketplace(&source_path),
|
||||
)?;
|
||||
|
||||
codex_command(codex_home.path())?
|
||||
.args(["plugin", "list"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("Marketplace `debug`"))
|
||||
.stdout(contains("sample@debug (not installed)"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_add_and_remove_updates_installed_plugin_config() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let source = TempDir::new()?;
|
||||
write_plugins_enabled_config(codex_home.path())?;
|
||||
write_marketplace_source(source.path())?;
|
||||
let source_path = source.path().to_string_lossy().into_owned();
|
||||
record_user_marketplace(
|
||||
codex_home.path(),
|
||||
"debug",
|
||||
&configured_local_marketplace(&source_path),
|
||||
)?;
|
||||
|
||||
codex_command(codex_home.path())?
|
||||
.args(["plugin", "add", "sample@debug"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("Added plugin `sample` from marketplace `debug`."));
|
||||
|
||||
let config = std::fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE))?;
|
||||
assert!(config.contains("[plugins.\"sample@debug\"]"));
|
||||
|
||||
codex_command(codex_home.path())?
|
||||
.args(["plugin", "remove", "sample", "--marketplace", "debug"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains(
|
||||
"Removed plugin `sample` from marketplace `debug`.",
|
||||
));
|
||||
|
||||
let config = std::fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE))?;
|
||||
assert!(!config.contains("[plugins.\"sample@debug\"]"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_remove_works_after_marketplace_is_removed() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let source = TempDir::new()?;
|
||||
write_plugins_enabled_config(codex_home.path())?;
|
||||
write_marketplace_source(source.path())?;
|
||||
let source_path = source.path().to_string_lossy().into_owned();
|
||||
record_user_marketplace(
|
||||
codex_home.path(),
|
||||
"debug",
|
||||
&configured_local_marketplace(&source_path),
|
||||
)?;
|
||||
|
||||
codex_command(codex_home.path())?
|
||||
.args(["plugin", "add", "sample", "--marketplace", "debug"])
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
codex_command(codex_home.path())?
|
||||
.args(["plugin", "marketplace", "remove", "debug"])
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
codex_command(codex_home.path())?
|
||||
.args(["plugin", "remove", "sample@debug"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains(
|
||||
"Removed plugin `sample` from marketplace `debug`.",
|
||||
));
|
||||
|
||||
let config = std::fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE))?;
|
||||
assert!(!config.contains("[plugins.\"sample@debug\"]"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user