aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/src/console/application.rs
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-08-05 02:48:41 +0900
committernsfisis <nsfisis@gmail.com>2026-08-05 02:48:41 +0900
commitee5d0e84c2060f10c20a4b3e519bf0f928fd170d (patch)
treeb7ab50ac5674cebc88bea60e5a2c33a59b4482d2 /crates/shirabe/src/console/application.rs
parent11cdaae87c37e479fea6c39447041c312410b101 (diff)
downloadphp-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/application.rs')
-rw-r--r--crates/shirabe/src/console/application.rs75
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)?;