From 9b444a9a879b75a6af3d3c7ba8b9a4294574c3ec Mon Sep 17 00:00:00 2001 From: nsfisis Date: Wed, 5 Aug 2026 03:58:03 +0900 Subject: feat(plugin): run plugin-provided commands in a worker-side application A same-FQCN Composer\Console\Application, hand-written under the new php/runtime/ tree, hosts CommandProvider commands inside the PHP worker: PhpCommandProxy overrides run() and forwards the stringified input, so the real Symfony machinery binds, validates and executes against the live command object, while help/list render Rust-side from a definition read back at construction. Reverse \Shirabe\RustCommandStub rows let a plugin command invoke built-in commands back in the Rust process, keeping every command on the side whose helper set it was written for. Composer\EventDispatcher\Event moves from a generated stub to a dual-mode runtime class: the real BaseCommand::initialize constructs a PreCommandRunEvent natively in the worker, which a proxy-only constructor guard rejected. Its PRE_COMMAND_RUN dispatch reaches a new EventDispatcher stub whose dispatch supports the observably-no-op no-listener case and fails explicitly otherwise. The stub generator now accepts runtime-provided classes as stub bases (never as targets) and cross-checks the Application handoff property table against the real class. Co-Authored-By: Claude Fable 5 --- .../php/runtime/Composer/Console/Application.php | 142 +++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 crates/shirabe-php-rpc/php/runtime/Composer/Console/Application.php (limited to 'crates/shirabe-php-rpc/php/runtime/Composer/Console/Application.php') diff --git a/crates/shirabe-php-rpc/php/runtime/Composer/Console/Application.php b/crates/shirabe-php-rpc/php/runtime/Composer/Console/Application.php new file mode 100644 index 00000000..50ea31b1 --- /dev/null +++ b/crates/shirabe-php-rpc/php/runtime/Composer/Console/Application.php @@ -0,0 +1,142 @@ +setAutoExit(false); + // A command failure must reach the Rust side as an RPC throw, not get rendered here. + $app->setCatchExceptions(false); + $app->shirabeComposer = $config['composer']; + $app->shirabeIo = $config['io']; + $app->shirabeInitialWorkingDirectory = $config['initialWorkingDirectory']; + $app->shirabeDisablePluginsByDefault = $config['disablePluginsByDefault']; + $app->shirabeDisableScriptsByDefault = $config['disableScriptsByDefault']; + foreach ($config['rustCommands'] as $meta) { + $app->add(new \Shirabe\RustCommandStub($meta)); + } + foreach ($config['pluginCommands'] as $command) { + $app->add($command); + } + + return $app; + } + + public function __construct(string $name = 'Composer', string $version = '') + { + if (!self::$shirabeConstructing) { + // TODO(plugin): a plugin constructing its own Composer\Console\Application would + // run commands outside the Rust-side orchestration; whether and how to support + // that is undecided, so fail loudly instead of handing out a half-wired instance. + throw new \RuntimeException( + 'Shirabe does not support constructing Composer\Console\Application inside the plugin process yet' + ); + } + parent::__construct($name, $version !== '' ? $version : Composer::getVersion()); + } + + public function getIO(): IOInterface + { + return $this->shirabeIo; + } + + public function getComposer(bool $required = true, ?bool $disablePlugins = null, ?bool $disableScripts = null): ?Composer + { + if (null === $this->shirabeComposer && $required) { + // TODO(plugin): creating a Composer instance from scratch here would need + // Factory::create over RPC; the handoff currently always carries the instance the + // Rust side already built, so this only fires after resetComposer-style flows. + throw new \RuntimeException( + 'Shirabe cannot create a new Composer instance inside the plugin process yet' + ); + } + + return $this->shirabeComposer; + } + + public function resetComposer(): void + { + // TODO(plugin): the Composer instance is owned by the Rust process; dropping only the + // worker-side reference would desynchronize the two worlds, so this needs a + // reset-and-refetch round trip that does not exist yet. + throw new \RuntimeException( + 'Shirabe does not support resetComposer() inside the plugin process yet' + ); + } + + /** + * @return string|null + */ + public function getInitialWorkingDirectory() + { + return $this->shirabeInitialWorkingDirectory; + } + + public function getDisablePluginsByDefault(): bool + { + return $this->shirabeDisablePluginsByDefault; + } + + public function getDisableScriptsByDefault(): bool + { + return $this->shirabeDisableScriptsByDefault; + } + + // Same as the real class: without Composer's global options in the definition, binding a + // forwarded command line that carries e.g. --working-dir would fail here even though the + // Rust side already consumed the option. + protected function getDefaultInputDefinition(): InputDefinition + { + $definition = parent::getDefaultInputDefinition(); + $definition->addOption(new InputOption('--profile', null, InputOption::VALUE_NONE, 'Display timing and memory usage information')); + $definition->addOption(new InputOption('--no-plugins', null, InputOption::VALUE_NONE, 'Whether to disable plugins.')); + $definition->addOption(new InputOption('--no-scripts', null, InputOption::VALUE_NONE, 'Skips the execution of all scripts defined in composer.json file.')); + $definition->addOption(new InputOption('--working-dir', '-d', InputOption::VALUE_REQUIRED, 'If specified, use the given directory as working directory.')); + $definition->addOption(new InputOption('--no-cache', null, InputOption::VALUE_NONE, 'Prevent use of the cache')); + + return $definition; + } +} -- cgit v1.3.1