aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-php-rpc/php/runtime/Composer/Console
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-08-05 03:58:03 +0900
committernsfisis <nsfisis@gmail.com>2026-08-05 03:58:03 +0900
commit9b444a9a879b75a6af3d3c7ba8b9a4294574c3ec (patch)
treeb1041b55bc3ed36a391370cc2f5ececc8cb386b2 /crates/shirabe-php-rpc/php/runtime/Composer/Console
parentbe3458128ef09b3d1074b134f2822162e077e165 (diff)
downloadphp-shirabe-9b444a9a879b75a6af3d3c7ba8b9a4294574c3ec.tar.gz
php-shirabe-9b444a9a879b75a6af3d3c7ba8b9a4294574c3ec.tar.zst
php-shirabe-9b444a9a879b75a6af3d3c7ba8b9a4294574c3ec.zip
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 <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe-php-rpc/php/runtime/Composer/Console')
-rw-r--r--crates/shirabe-php-rpc/php/runtime/Composer/Console/Application.php142
1 files changed, 142 insertions, 0 deletions
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 @@
+<?php
+
+// Shirabe's own definition of Composer\Console\Application for the plugin worker. The real
+// class file is never autoloaded in this process: plugin-provided commands run here under a
+// genuine Symfony console application, and this class supplies the Composer-specific surface
+// (getIO()/getComposer()/...) backed by the Rust process over RPC. Defining the same FQCN
+// instead of subclassing the real class keeps both `instanceof` and strict `get_class()`
+// comparisons identical to upstream.
+//
+// Unlike the files under stubs/, the classes under runtime/ are hand-written: they are
+// two-world implementations with behavior of their own, not mechanical proxies, so the stub
+// generator does not manage them.
+
+namespace Composer\Console;
+
+use Composer\Composer;
+use Composer\IO\IOInterface;
+use Symfony\Component\Console\Application as BaseApplication;
+use Symfony\Component\Console\Input\InputDefinition;
+use Symfony\Component\Console\Input\InputOption;
+
+class Application extends BaseApplication
+{
+ private static bool $shirabeConstructing = false;
+
+ /** @var Composer|null */
+ private $shirabeComposer = null;
+ /** @var IOInterface */
+ private $shirabeIo;
+ /** @var string|null */
+ private $shirabeInitialWorkingDirectory = null;
+ private bool $shirabeDisablePluginsByDefault = false;
+ private bool $shirabeDisableScriptsByDefault = false;
+
+ /**
+ * Builds the worker-side application from the Rust-side handoff. $config keys:
+ * `composer` (Composer proxy stub or null), `io` (IO proxy stub),
+ * `initialWorkingDirectory` (?string), `disablePluginsByDefault` (bool),
+ * `disableScriptsByDefault` (bool), `rustCommands` (metadata rows for
+ * \Shirabe\RustCommandStub), `pluginCommands` (live command entities from the P table).
+ */
+ public static function __shirabeBoot(array $config): self
+ {
+ self::$shirabeConstructing = true;
+ try {
+ $app = new self();
+ } finally {
+ self::$shirabeConstructing = false;
+ }
+ $app->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;
+ }
+}