mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
@@ -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<_>>(),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user