Skip to content

Commit 5818ed6

Browse files
authored
Move marketplace add under plugin command (#18116)
## Summary - move the marketplace add CLI from `codex marketplace add` to `codex plugin marketplace add` - keep marketplace config overrides working through the nested plugin command - reject `--sparse` for local marketplace directory sources before the local-source install path bypasses git-source validation ## Validation - `just fmt` - `git diff --check` - `cargo test -p codex-cli` - `cargo test -p codex-core marketplace_add -- --nocapture` - `cargo test -p codex-core install_plugin_updates_config_with_relative_path_and_plugin_key -- --nocapture` - `xli-test-marketplace-cli` local isolated matrix: `T1`, `L1`-`L10`
1 parent bf6e7e1 commit 5818ed6

4 files changed

Lines changed: 163 additions & 11 deletions

File tree

codex-rs/cli/src/main.rs

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,8 @@ enum Subcommand {
111111
/// Manage external MCP servers for Codex.
112112
Mcp(McpCli),
113113

114-
/// Manage plugin marketplaces for Codex.
115-
Marketplace(MarketplaceCli),
114+
/// Manage Codex plugins.
115+
Plugin(PluginCli),
116116

117117
/// Start Codex as an MCP server (stdio).
118118
McpServer,
@@ -170,6 +170,21 @@ enum Subcommand {
170170
Features(FeaturesCli),
171171
}
172172

173+
#[derive(Debug, Parser)]
174+
struct PluginCli {
175+
#[clap(flatten)]
176+
pub config_overrides: CliConfigOverrides,
177+
178+
#[command(subcommand)]
179+
subcommand: PluginSubcommand,
180+
}
181+
182+
#[derive(Debug, clap::Subcommand)]
183+
enum PluginSubcommand {
184+
/// Manage plugin marketplaces for Codex.
185+
Marketplace(MarketplaceCli),
186+
}
187+
173188
#[derive(Debug, Parser)]
174189
struct CompletionCommand {
175190
/// Shell to generate completions for
@@ -726,17 +741,23 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
726741
prepend_config_flags(&mut mcp_cli.config_overrides, root_config_overrides.clone());
727742
mcp_cli.run().await?;
728743
}
729-
Some(Subcommand::Marketplace(mut marketplace_cli)) => {
744+
Some(Subcommand::Plugin(plugin_cli)) => {
730745
reject_remote_mode_for_subcommand(
731746
root_remote.as_deref(),
732747
root_remote_auth_token_env.as_deref(),
733-
"marketplace",
748+
"plugin",
734749
)?;
735-
prepend_config_flags(
736-
&mut marketplace_cli.config_overrides,
737-
root_config_overrides.clone(),
738-
);
739-
marketplace_cli.run().await?;
750+
let PluginCli {
751+
mut config_overrides,
752+
subcommand,
753+
} = plugin_cli;
754+
prepend_config_flags(&mut config_overrides, root_config_overrides.clone());
755+
match subcommand {
756+
PluginSubcommand::Marketplace(mut marketplace_cli) => {
757+
prepend_config_flags(&mut marketplace_cli.config_overrides, config_overrides);
758+
marketplace_cli.run().await?;
759+
}
760+
}
740761
}
741762
Some(Subcommand::AppServer(app_server_cli)) => {
742763
let AppServerCommand {
@@ -1690,6 +1711,35 @@ mod tests {
16901711
assert!(matches!(cli.subcommand, Some(Subcommand::Responses(_))));
16911712
}
16921713

1714+
#[test]
1715+
fn plugin_marketplace_add_parses_under_plugin() {
1716+
let cli =
1717+
MultitoolCli::try_parse_from(["codex", "plugin", "marketplace", "add", "owner/repo"])
1718+
.expect("parse");
1719+
1720+
assert!(matches!(cli.subcommand, Some(Subcommand::Plugin(_))));
1721+
}
1722+
1723+
#[test]
1724+
fn plugin_marketplace_upgrade_parses_under_plugin() {
1725+
let cli =
1726+
MultitoolCli::try_parse_from(["codex", "plugin", "marketplace", "upgrade", "debug"])
1727+
.expect("parse");
1728+
1729+
assert!(matches!(cli.subcommand, Some(Subcommand::Plugin(_))));
1730+
}
1731+
1732+
#[test]
1733+
fn marketplace_no_longer_parses_at_top_level() {
1734+
let add_result =
1735+
MultitoolCli::try_parse_from(["codex", "marketplace", "add", "owner/repo"]);
1736+
assert!(add_result.is_err());
1737+
1738+
let upgrade_result =
1739+
MultitoolCli::try_parse_from(["codex", "marketplace", "upgrade", "debug"]);
1740+
assert!(upgrade_result.is_err());
1741+
}
1742+
16931743
fn sample_exit_info(conversation_id: Option<&str>, thread_name: Option<&str>) -> AppExitInfo {
16941744
let token_usage = TokenUsage {
16951745
output_tokens: 2,

codex-rs/cli/tests/marketplace_add.rs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ async fn marketplace_add_local_directory_source() -> Result<()> {
4848

4949
codex_command(codex_home.path())?
5050
.current_dir(source_parent)
51-
.args(["marketplace", "add", source_arg.as_str()])
51+
.args(["plugin", "marketplace", "add", source_arg.as_str()])
5252
.assert()
5353
.success();
5454

@@ -78,7 +78,12 @@ async fn marketplace_add_rejects_local_manifest_file_source() -> Result<()> {
7878
let manifest_path = source.path().join(".agents/plugins/marketplace.json");
7979

8080
codex_command(codex_home.path())?
81-
.args(["marketplace", "add", manifest_path.to_str().unwrap()])
81+
.args([
82+
"plugin",
83+
"marketplace",
84+
"add",
85+
manifest_path.to_str().unwrap(),
86+
])
8287
.assert()
8388
.failure()
8489
.stderr(contains(
@@ -87,3 +92,27 @@ async fn marketplace_add_rejects_local_manifest_file_source() -> Result<()> {
8792

8893
Ok(())
8994
}
95+
96+
#[tokio::test]
97+
async fn marketplace_add_rejects_sparse_for_local_directory_source() -> Result<()> {
98+
let codex_home = TempDir::new()?;
99+
let source = TempDir::new()?;
100+
write_marketplace_source(source.path(), "local ref")?;
101+
102+
codex_command(codex_home.path())?
103+
.args([
104+
"plugin",
105+
"marketplace",
106+
"add",
107+
"--sparse",
108+
".agents",
109+
source.path().to_str().unwrap(),
110+
])
111+
.assert()
112+
.failure()
113+
.stderr(contains(
114+
"--sparse is only supported for git marketplace sources",
115+
));
116+
117+
Ok(())
118+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
use anyhow::Result;
2+
use predicates::str::contains;
3+
use std::path::Path;
4+
use tempfile::TempDir;
5+
6+
fn codex_command(codex_home: &Path) -> Result<assert_cmd::Command> {
7+
let mut cmd = assert_cmd::Command::new(codex_utils_cargo_bin::cargo_bin("codex")?);
8+
cmd.env("CODEX_HOME", codex_home);
9+
Ok(cmd)
10+
}
11+
12+
#[tokio::test]
13+
async fn marketplace_upgrade_runs_under_plugin() -> Result<()> {
14+
let codex_home = TempDir::new()?;
15+
16+
codex_command(codex_home.path())?
17+
.args(["plugin", "marketplace", "upgrade"])
18+
.assert()
19+
.success()
20+
.stdout(contains("No configured Git marketplaces to upgrade."));
21+
22+
Ok(())
23+
}
24+
25+
#[tokio::test]
26+
async fn marketplace_upgrade_no_longer_runs_at_top_level() -> Result<()> {
27+
let codex_home = TempDir::new()?;
28+
29+
codex_command(codex_home.path())?
30+
.args(["marketplace", "upgrade"])
31+
.assert()
32+
.failure()
33+
.stderr(contains("unexpected argument 'upgrade' found"));
34+
35+
Ok(())
36+
}

codex-rs/core/src/plugins/marketplace_add.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@ where
7777
sparse_paths,
7878
} = request;
7979
let source = parse_marketplace_source(&source, ref_name)?;
80+
if !sparse_paths.is_empty() && !matches!(source, MarketplaceSource::Git { .. }) {
81+
return Err(MarketplaceAddError::InvalidRequest(
82+
"--sparse is only supported for git marketplace sources".to_string(),
83+
));
84+
}
8085

8186
let install_root = marketplace_install_root(codex_home);
8287
fs::create_dir_all(&install_root).map_err(|err| {
@@ -287,6 +292,38 @@ mod tests {
287292
Ok(())
288293
}
289294

295+
#[test]
296+
fn add_marketplace_sync_rejects_sparse_checkout_for_local_directory_source() -> Result<()> {
297+
let codex_home = TempDir::new()?;
298+
let source_root = TempDir::new()?;
299+
write_marketplace_source(source_root.path(), "local copy")?;
300+
301+
let err = add_marketplace_sync_with_cloner(
302+
codex_home.path(),
303+
MarketplaceAddRequest {
304+
source: source_root.path().display().to_string(),
305+
ref_name: None,
306+
sparse_paths: vec![".agents".to_string()],
307+
},
308+
|_url, _ref_name, _sparse_paths, _destination| {
309+
panic!("git cloner should not be called for local marketplace sources")
310+
},
311+
)
312+
.unwrap_err();
313+
314+
assert_eq!(
315+
err.to_string(),
316+
"--sparse is only supported for git marketplace sources"
317+
);
318+
assert!(
319+
!codex_home
320+
.path()
321+
.join(codex_config::CONFIG_TOML_FILE)
322+
.exists()
323+
);
324+
Ok(())
325+
}
326+
290327
#[test]
291328
fn add_marketplace_sync_treats_existing_local_directory_source_as_already_added() -> Result<()>
292329
{

0 commit comments

Comments
 (0)