mirror of
https://github.com/openai/codex.git
synced 2026-10-30 19:18:49 +00:00
feat(cli): add plugin reload command
This commit is contained in:
Generated
+1
@@ -2223,6 +2223,7 @@ dependencies = [
|
||||
"clap_complete",
|
||||
"codex-api",
|
||||
"codex-app-server",
|
||||
"codex-app-server-client",
|
||||
"codex-app-server-daemon",
|
||||
"codex-app-server-protocol",
|
||||
"codex-app-server-test-client",
|
||||
|
||||
@@ -22,6 +22,7 @@ anyhow = { workspace = true }
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
clap_complete = { workspace = true }
|
||||
codex-app-server = { workspace = true }
|
||||
codex-app-server-client = { workspace = true }
|
||||
codex-app-server-daemon = { workspace = true }
|
||||
codex-app-server-protocol = { workspace = true }
|
||||
codex-app-server-test-client = { workspace = true }
|
||||
|
||||
@@ -957,6 +957,12 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
prepend_config_flags(&mut marketplace_cli.config_overrides, config_overrides);
|
||||
marketplace_cli.run().await?;
|
||||
}
|
||||
PluginSubcommand::Reload(args) => {
|
||||
let overrides = config_overrides
|
||||
.parse_overrides()
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
plugin_cmd::run_plugin_reload(overrides, args).await?;
|
||||
}
|
||||
PluginSubcommand::Remove(args) => {
|
||||
let overrides = config_overrides
|
||||
.parse_overrides()
|
||||
@@ -2540,6 +2546,21 @@ mod tests {
|
||||
assert!(matches!(cli.subcommand, Some(Subcommand::Plugin(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_reload_parses_under_plugin() {
|
||||
let cli = MultitoolCli::try_parse_from(["codex", "plugin", "reload"]).expect("parse");
|
||||
|
||||
assert!(matches!(cli.subcommand, Some(Subcommand::Plugin(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugins_still_parses_as_prompt_text() {
|
||||
let cli = MultitoolCli::try_parse_from(["codex", "plugins"]).expect("parse");
|
||||
|
||||
assert!(cli.subcommand.is_none());
|
||||
assert_eq!(cli.interactive.prompt.as_deref(), Some("plugins"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_parses_as_update_subcommand() {
|
||||
let cli = MultitoolCli::try_parse_from(["codex", "update"]).expect("parse");
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::bail;
|
||||
use clap::Parser;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::find_codex_home;
|
||||
@@ -12,6 +11,7 @@ use codex_core_plugins::installed_marketplaces::marketplace_install_root;
|
||||
use codex_core_plugins::installed_marketplaces::resolve_configured_marketplace_root;
|
||||
use codex_core_plugins::marketplace::MarketplaceListError;
|
||||
use codex_core_plugins::marketplace::find_marketplace_manifest_path;
|
||||
use codex_core_skills::SkillsManager;
|
||||
use codex_plugin::PluginId;
|
||||
use codex_plugin::validate_plugin_segment;
|
||||
use codex_utils_cli::CliConfigOverrides;
|
||||
@@ -43,6 +43,9 @@ pub enum PluginSubcommand {
|
||||
/// Add, list, upgrade, or remove configured plugin marketplaces.
|
||||
Marketplace(MarketplaceCli),
|
||||
|
||||
/// Reload Codex's local installed-plugin state from disk/cache and refresh plugin-backed runtime state.
|
||||
Reload(ReloadPluginsArgs),
|
||||
|
||||
/// Remove an installed plugin from local config and cache.
|
||||
///
|
||||
/// Pass either `PLUGIN@MARKETPLACE` or pass `PLUGIN` with
|
||||
@@ -91,6 +94,14 @@ pub struct RemovePluginArgs {
|
||||
marketplace_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(
|
||||
bin_name = "codex plugin reload",
|
||||
about = "Reload Codex's local installed-plugin state from marketplaces",
|
||||
after_help = "What this does:\n Clears Codex's local plugin and skill caches so the next local load re-scans installed plugin state from disk/cache.\n\nWhat happens next:\n Updated plugin context is picked up in new CLI sessions and other fresh loads.\n Existing app-server-backed threads are not refreshed.\n Plugin instructions and skills already loaded into an existing thread are not reread."
|
||||
)]
|
||||
pub struct ReloadPluginsArgs;
|
||||
|
||||
pub async fn run_plugin_add(
|
||||
overrides: Vec<(String, toml::Value)>,
|
||||
args: AddPluginArgs,
|
||||
@@ -203,6 +214,28 @@ pub async fn run_plugin_remove(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn run_plugin_reload(
|
||||
overrides: Vec<(String, toml::Value)>,
|
||||
_: ReloadPluginsArgs,
|
||||
) -> Result<()> {
|
||||
let config = Config::load_with_cli_overrides(overrides)
|
||||
.await
|
||||
.context("failed to load configuration")?;
|
||||
let codex_home = find_codex_home().context("failed to resolve CODEX_HOME")?;
|
||||
let plugins_manager = PluginsManager::new(codex_home.to_path_buf());
|
||||
let skills_manager = SkillsManager::new(codex_home, config.bundled_skills_enabled());
|
||||
|
||||
plugins_manager.clear_cache();
|
||||
skills_manager.clear_cache();
|
||||
|
||||
println!("Cleared Codex's local plugin and skill caches.");
|
||||
println!("Updated plugin context will be picked up in new CLI sessions and other fresh loads.");
|
||||
println!("Existing app-server-backed threads are not refreshed.");
|
||||
println!("Plugin instructions and skills already loaded into an existing thread stay as-is.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct PluginCommandContext {
|
||||
codex_home: PathBuf,
|
||||
plugins_input: PluginsConfigInput,
|
||||
|
||||
@@ -2,7 +2,6 @@ use anyhow::Result;
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_config::MarketplaceConfigUpdate;
|
||||
use codex_config::record_user_marketplace;
|
||||
use predicates::prelude::PredicateBooleanExt;
|
||||
use predicates::str::contains;
|
||||
use std::path::Path;
|
||||
use tempfile::TempDir;
|
||||
@@ -293,6 +292,32 @@ async fn plugin_remove_works_after_marketplace_is_removed() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_reload_command_help_uses_singular_plugin_name() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
codex_command(codex_home.path())?
|
||||
.args(["plugin", "reload", "--help"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("Usage: codex plugin reload"))
|
||||
.stdout(contains(
|
||||
"Reload Codex's local installed-plugin state from marketplaces",
|
||||
))
|
||||
.stdout(contains("Clears Codex's local plugin and skill caches"))
|
||||
.stdout(contains(
|
||||
"Updated plugin context is picked up in new CLI sessions and other fresh loads",
|
||||
))
|
||||
.stdout(contains(
|
||||
"Existing app-server-backed threads are not refreshed",
|
||||
))
|
||||
.stdout(contains(
|
||||
"Plugin instructions and skills already loaded into an existing thread are not reread",
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_add_rejects_cached_plugins_without_authorizing_marketplace_snapshot() -> Result<()>
|
||||
{
|
||||
|
||||
@@ -414,6 +414,10 @@ impl Session {
|
||||
self.services.agent_control.session_id()
|
||||
}
|
||||
|
||||
pub(crate) fn plugins_manager(&self) -> Arc<PluginsManager> {
|
||||
Arc::clone(&self.services.plugins_manager)
|
||||
}
|
||||
|
||||
#[instrument(name = "session_init", level = "info", skip_all)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[expect(
|
||||
|
||||
@@ -15,6 +15,8 @@ pub(crate) mod multi_agents_spec;
|
||||
pub(crate) mod multi_agents_v2;
|
||||
mod plan;
|
||||
pub(crate) mod plan_spec;
|
||||
mod reload_plugins;
|
||||
pub(crate) mod reload_plugins_spec;
|
||||
mod request_permissions;
|
||||
mod request_plugin_install;
|
||||
pub(crate) mod request_plugin_install_spec;
|
||||
@@ -59,6 +61,7 @@ pub use mcp_resource::ListMcpResourceTemplatesHandler;
|
||||
pub use mcp_resource::ListMcpResourcesHandler;
|
||||
pub use mcp_resource::ReadMcpResourceHandler;
|
||||
pub use plan::PlanHandler;
|
||||
pub use reload_plugins::ReloadPluginsHandler;
|
||||
pub use request_permissions::RequestPermissionsHandler;
|
||||
pub use request_plugin_install::RequestPluginInstallHandler;
|
||||
pub use request_user_input::RequestUserInputHandler;
|
||||
|
||||
@@ -16,6 +16,7 @@ use crate::tools::handlers::ListMcpResourcesHandler;
|
||||
use crate::tools::handlers::McpHandler;
|
||||
use crate::tools::handlers::PlanHandler;
|
||||
use crate::tools::handlers::ReadMcpResourceHandler;
|
||||
use crate::tools::handlers::ReloadPluginsHandler;
|
||||
use crate::tools::handlers::RequestPermissionsHandler;
|
||||
use crate::tools::handlers::RequestPluginInstallHandler;
|
||||
use crate::tools::handlers::RequestUserInputHandler;
|
||||
@@ -45,6 +46,7 @@ use crate::tools::handlers::multi_agents_v2::ListAgentsHandler as ListAgentsHand
|
||||
use crate::tools::handlers::multi_agents_v2::SendMessageHandler as SendMessageHandlerV2;
|
||||
use crate::tools::handlers::multi_agents_v2::SpawnAgentHandler as SpawnAgentHandlerV2;
|
||||
use crate::tools::handlers::multi_agents_v2::WaitAgentHandler as WaitAgentHandlerV2;
|
||||
use crate::tools::handlers::reload_plugins_spec::RELOAD_PLUGINS_TOOL_NAME;
|
||||
use crate::tools::handlers::view_image_spec::ViewImageToolOptions;
|
||||
use crate::tools::hosted_spec::WebSearchToolOptions;
|
||||
use crate::tools::hosted_spec::create_image_generation_tool;
|
||||
@@ -435,6 +437,14 @@ fn collect_tool_executors(
|
||||
executors.push(Arc::new(TestSyncHandler));
|
||||
}
|
||||
|
||||
if config
|
||||
.experimental_supported_tools
|
||||
.iter()
|
||||
.any(|tool| tool == RELOAD_PLUGINS_TOOL_NAME)
|
||||
{
|
||||
executors.push(Arc::new(ReloadPluginsHandler));
|
||||
}
|
||||
|
||||
if config.environment_mode.has_environment() {
|
||||
let include_environment_id =
|
||||
matches!(config.environment_mode, ToolEnvironmentMode::Multiple);
|
||||
|
||||
@@ -14,6 +14,7 @@ use crate::tools::handlers::multi_agents_spec::create_spawn_agent_tool_v2;
|
||||
use crate::tools::handlers::multi_agents_spec::create_wait_agent_tool_v1;
|
||||
use crate::tools::handlers::multi_agents_spec::create_wait_agent_tool_v2;
|
||||
use crate::tools::handlers::plan_spec::create_update_plan_tool;
|
||||
use crate::tools::handlers::reload_plugins_spec::RELOAD_PLUGINS_TOOL_NAME;
|
||||
use crate::tools::handlers::request_user_input_spec::REQUEST_USER_INPUT_TOOL_NAME;
|
||||
use crate::tools::handlers::request_user_input_spec::create_request_user_input_tool;
|
||||
use crate::tools::handlers::request_user_input_spec::request_user_input_tool_description;
|
||||
@@ -1392,6 +1393,36 @@ fn test_test_model_info_includes_sync_tool() {
|
||||
assert!(tools.iter().any(|tool| tool.name() == "test_sync_tool"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_model_info_includes_reload_plugins_tool() {
|
||||
let mut model_info = model_info();
|
||||
model_info.experimental_supported_tools = vec![RELOAD_PLUGINS_TOOL_NAME.to_string()];
|
||||
let features = Features::with_defaults();
|
||||
let available_models = Vec::new();
|
||||
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
||||
model_info: &model_info,
|
||||
available_models: &available_models,
|
||||
features: &features,
|
||||
image_generation_tool_auth_allowed: true,
|
||||
web_search_mode: Some(WebSearchMode::Cached),
|
||||
session_source: SessionSource::Cli,
|
||||
permission_profile: &PermissionProfile::Disabled,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
});
|
||||
let (tools, _) = build_specs(
|
||||
&tools_config,
|
||||
/*mcp_tools*/ None,
|
||||
/*deferred_mcp_tools*/ None,
|
||||
&[],
|
||||
);
|
||||
|
||||
assert!(
|
||||
tools
|
||||
.iter()
|
||||
.any(|tool| tool.name() == RELOAD_PLUGINS_TOOL_NAME)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_specs_mcp_tools_converted() {
|
||||
let model_info = model_info();
|
||||
|
||||
Reference in New Issue
Block a user