feat(plugins): tabulate plugin list output (#23727)

## Summary
- render `codex plugin list` as one table per marketplace with the
marketplace manifest path shown above each table
- surface the installed plugin version in the CLI output by threading
`installed_version` through marketplace listing state
- narrow the system-root exemption so only known bundled/runtime
marketplaces skip missing-manifest failures, and keep `VERSION` empty
for cached-but-unconfigured plugins

## Rationale
The plugin list UX was hard to scan as a flat list and did not show
which installed version was active. This change makes the CLI output
easier to read in the real multi-marketplace case, keeps the plugin path
visible, fixes the Sapphire regression where bundled/runtime marketplace
roots were blocking `plugin list`, and addresses the two review findings
that came out of the follow-up deep review.

## Key Decisions
- kept the CLI output grouped per marketplace instead of one global
table so the marketplace path can live with the rows it owns
- kept `VERSION` as the installed version, which means it is empty until
a plugin is actually installed
- handled the bundled/runtime regression in the CLI snapshot validation
path rather than widening app-server protocol or changing marketplace
loading behavior
- narrowed the exemption to known system marketplace names plus expected
system paths, so user-configured marketplaces under those directories
still fail loudly
- gated `installed_version` on actual installed state so `VERSION`
cannot show stale cache state for `not installed` rows

## Validation
- `just fmt`
- Sapphire: `cargo test -p codex-cli --test plugin_cli` (`14 passed; 0
failed`)
- Sapphire smoke test: bundled/runtime roots still work
  - `cargo run -q -p codex-cli -- plugin add sample@debug`
  - `cargo run -q -p codex-cli -- plugin list`
- verified the bundled/runtime-root scenario no longer errors and shows
the expected marketplace table output
- Sapphire smoke test: custom marketplace under bundled path still
errors
- verified `failed to load configured marketplace snapshot(s)` for
`custom-marketplace`
- Sapphire smoke test: cached-but-unconfigured plugin hides version
- verified `sample@debug not installed` renders with an empty `VERSION`
column

## Sample Output
```text
/tmp/custom-marketplace/plugin.json
NAME          VERSION  STATUS         DESCRIPTION
sample@debug  1.0.0    enabled        Debug sample plugin
other@local            not installed  Local development plugin
```
This commit is contained in:
Casey Chow
2026-05-20 18:04:49 -04:00
committed by GitHub
parent eee3e60db3
commit 3075061bdd
4 changed files with 336 additions and 27 deletions
+105 -9
View File
@@ -5,6 +5,7 @@ use clap::Parser;
use codex_core::config::Config;
use codex_core::config::find_codex_home;
use codex_core_plugins::ConfiguredMarketplace;
use codex_core_plugins::OPENAI_BUNDLED_MARKETPLACE_NAME;
use codex_core_plugins::PluginInstallRequest;
use codex_core_plugins::PluginsConfigInput;
use codex_core_plugins::PluginsManager;
@@ -15,10 +16,14 @@ use codex_core_plugins::marketplace::find_marketplace_manifest_path;
use codex_plugin::PluginId;
use codex_plugin::validate_plugin_segment;
use codex_utils_cli::CliConfigOverrides;
use std::path::Path;
use std::path::PathBuf;
use crate::marketplace_cmd::MarketplaceCli;
const OPENAI_BUNDLED_ALPHA_MARKETPLACE_NAME: &str = "openai-bundled-alpha";
const OPENAI_PRIMARY_RUNTIME_MARKETPLACE_NAME: &str = "openai-primary-runtime";
#[derive(Debug, Parser)]
#[command(bin_name = "codex plugin")]
pub struct PluginCli {
@@ -168,9 +173,13 @@ pub async fn run_plugin_list(
println!("No marketplace plugins found.");
}
} else {
for marketplace in marketplaces {
println!("Marketplace `{}`", marketplace.name);
println!("Path: {}", marketplace.path.as_path().display());
for (index, marketplace) in marketplaces.into_iter().enumerate() {
let mut rows = Vec::new();
let mut plugin_width = "PLUGIN".len();
let mut status_width = "STATUS".len();
let mut installed_version_width = "VERSION".len();
let mut path_width = "PATH".len();
for plugin in &marketplace.plugins {
let state = if plugin.installed && plugin.enabled {
"installed, enabled"
@@ -179,7 +188,51 @@ pub async fn run_plugin_list(
} else {
"not installed"
};
println!(" {} ({state})", plugin.id);
let installed_version = plugin.installed_version.clone().unwrap_or_default();
let path = match &plugin.source {
codex_core_plugins::marketplace::MarketplacePluginSource::Local { path } => {
path.as_path().display().to_string()
}
codex_core_plugins::marketplace::MarketplacePluginSource::Git {
url,
path,
ref_name,
sha,
} => {
let mut parts = vec![url.clone()];
if let Some(path) = path {
parts.push(format!("path `{path}`"));
}
if let Some(ref_name) = ref_name {
parts.push(format!("ref `{ref_name}`"));
}
if let Some(sha) = sha {
parts.push(format!("sha `{sha}`"));
}
parts.join(", ")
}
};
plugin_width = plugin_width.max(plugin.id.len());
status_width = status_width.max(state.len());
installed_version_width = installed_version_width.max(installed_version.len());
path_width = path_width.max(path.len());
rows.push((plugin.id.clone(), state, installed_version, path));
}
if index > 0 {
println!();
}
println!("Marketplace `{}`", marketplace.name);
println!("{}", marketplace.path.as_path().display());
println!();
println!(
"{:<plugin_width$} {:<status_width$} {:<installed_version_width$} {:<path_width$}",
"PLUGIN", "STATUS", "VERSION", "PATH"
);
for (plugin, status, installed_version, path) in rows {
println!(
"{plugin:<plugin_width$} {status:<status_width$} {installed_version:<installed_version_width$} {path:<path_width$}"
);
}
}
}
@@ -381,11 +434,16 @@ fn configured_marketplace_snapshot_issues(
};
match find_marketplace_manifest_path(&root) {
Some(path) => manifest_paths.push((configured_name.clone(), path)),
None => issues.push(ConfiguredMarketplaceSnapshotIssue {
marketplace_name: configured_name.clone(),
path: root,
message: "marketplace root does not contain a supported manifest".to_string(),
}),
None => {
if is_implicit_system_marketplace_root(configured_name, codex_home, &root) {
continue;
}
issues.push(ConfiguredMarketplaceSnapshotIssue {
marketplace_name: configured_name.clone(),
path: root,
message: "marketplace root does not contain a supported manifest".to_string(),
});
}
}
}
@@ -403,3 +461,41 @@ fn configured_marketplace_snapshot_issues(
}
issues
}
fn is_implicit_system_marketplace_root(
marketplace_name: &str,
_codex_home: &Path,
root: &Path,
) -> bool {
if matches!(
marketplace_name,
OPENAI_BUNDLED_MARKETPLACE_NAME | OPENAI_BUNDLED_ALPHA_MARKETPLACE_NAME
) && path_ends_with(root, &[".tmp", "bundled-marketplaces", marketplace_name])
{
return true;
}
marketplace_name == OPENAI_PRIMARY_RUNTIME_MARKETPLACE_NAME
&& path_ends_with(
root,
&[
"codex-runtimes",
"codex-primary-runtime",
"plugins",
marketplace_name,
],
)
}
fn path_ends_with(path: &Path, suffix: &[&str]) -> bool {
let path_components = path
.components()
.map(|component| component.as_os_str().to_string_lossy().into_owned())
.collect::<Vec<_>>();
path_components.as_slice().ends_with(
&suffix
.iter()
.map(std::string::ToString::to_string)
.collect::<Vec<_>>(),
)
}