diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-08-05 02:48:41 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-08-05 02:48:41 +0900 |
| commit | ee5d0e84c2060f10c20a4b3e519bf0f928fd170d (patch) | |
| tree | b7ab50ac5674cebc88bea60e5a2c33a59b4482d2 /crates/shirabe/src/console | |
| parent | 11cdaae87c37e479fea6c39447041c312410b101 (diff) | |
| download | php-shirabe-ee5d0e84c2060f10c20a4b3e519bf0f928fd170d.tar.gz php-shirabe-ee5d0e84c2060f10c20a4b3e519bf0f928fd170d.tar.zst php-shirabe-ee5d0e84c2060f10c20a4b3e519bf0f928fd170d.zip | |
feat(plugin): discover and list plugin-provided commands
Port Application::getPluginCommands: resolve the local composer with
plugins force-enabled, fall back to Factory::createGlobal, and collect
commands from CommandProvider capability adapters. PhpCommandProxy now
mirrors name/description/aliases/hidden over RPC so `list` output
matches upstream; input definitions remain TODO(plugin).
PhpClass::php_class_name returns an owned String because PHP-backed
proxies only know their class at runtime (the override-skip warning
prints get_class). CommandProvider::getCommands hands out shared
Rc<RefCell> handles since the commands are stored in the application.
Factory::createGlobal now propagates createConfig errors instead of
swallowing them, and Application::getComposer only catches the
exception classes upstream catches, so a ParsingException reaches
doRun's GithubActionError path as in Composer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe/src/console')
| -rw-r--r-- | crates/shirabe/src/console/application.rs | 75 |
1 files changed, 63 insertions, 12 deletions
diff --git a/crates/shirabe/src/console/application.rs b/crates/shirabe/src/console/application.rs index e6cfad35..cf57a5bf 100644 --- a/crates/shirabe/src/console/application.rs +++ b/crates/shirabe/src/console/application.rs @@ -416,14 +416,10 @@ impl Application { match Factory::create(io_for_factory, None, disable_plugins_enum, disable_scripts) { Ok(c) => self.composer = Some(c.upcast()), Err(e) => { - if e.downcast_ref::<JsonValidationException>().is_some() - || e.downcast_ref::<RuntimeException>().is_some() + if e.downcast_ref::<shirabe_php_shim::InvalidArgumentException>() + .is_some() { if required { - return Err(e); - } - } else { - if required { self.io.write_error(&e.to_string()); if self.are_exceptions_caught() { // PHP calls `exit(1)` here, terminating before parent::run() can @@ -433,6 +429,19 @@ impl Application { } return Err(e); } + } else if e.downcast_ref::<JsonValidationException>().is_some() + || e.downcast_ref::<RuntimeException>().is_some() + // PHP's `catch (RuntimeException)` also catches subclasses; + // NoSslException is the one Factory::create raises. + || e.downcast_ref::<NoSslException>().is_some() + { + if required { + return Err(e); + } + } else { + // Anything else (e.g. seld/jsonlint's ParsingException) propagates + // regardless of $required, feeding doRun's GithubActionError path. + return Err(e); } } } @@ -584,8 +593,53 @@ impl Application { fn get_plugin_commands( &mut self, ) -> anyhow::Result<Vec<std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>>> { - // TODO(plugin): plugin command discovery is part of the plugin API - Ok(vec![]) + let mut commands: Vec<std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>> = vec![]; + + let mut composer = self.get_composer(false, Some(false), None)?; + if composer.is_none() { + let disable_plugins = if self.disable_plugins_by_default { + crate::factory::DisablePlugins::All + } else { + crate::factory::DisablePlugins::None + }; + composer = Factory::create_global( + self.io.clone(), + disable_plugins, + self.disable_scripts_by_default, + )?; + } + + if let Some(composer) = composer { + let composer = composer + .as_full() + .expect("Factory::create and Factory::create_global build a full Composer"); + let pm = composer.borrow().get_plugin_manager(); + let mut ctor_args: IndexMap<String, shirabe_php_rpc::PluginValue> = IndexMap::new(); + ctor_args.insert( + "composer".to_string(), + crate::plugin::composer_handle_value(&composer), + ); + ctor_args.insert("io".to_string(), crate::plugin::io_handle_value(&self.io)?); + let capabilities = pm.borrow().get_plugin_capabilities( + "Composer\\Plugin\\Capability\\CommandProvider", + ctor_args, + )?; + for capability in capabilities { + let provider = capability.as_command_provider().expect( + "get_plugin_capability builds a CommandProvider adapter for this capability class", + ); + // The is_array / instanceof BaseCommand checks PHP performs on the raw + // getCommands value live in the adapter. + let new_commands = provider.get_commands()?; + commands.extend( + new_commands.into_iter().map(|command| { + command as std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>> + }), + ); + } + } + + Ok(commands) } /// Get the working directory at startup time @@ -2141,10 +2195,7 @@ impl ApplicationHandle { for command in plugin_commands { let cmd_name = command.borrow().get_name().unwrap_or_default(); if application.borrow_mut().has(&cmd_name) { - // TODO(plugin): PHP uses get_class($command) for the skipped-command class - // name. Plugin command discovery (get_plugin_commands) is unimplemented, so - // this loop never runs; wire the concrete class name with the plugin API. - let cls = String::new(); + let cls = command.borrow().php_class_name(); io.write_error(&format!("<warning>Plugin command {} ({}) would override a Composer command and has been skipped</warning>", cmd_name, cls)); } else { self.add(command)?; |
