aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/src/plugin/php_plugin_proxy.rs
AgeCommit message (Collapse)Author
2026-08-31feat(plugin): serve Loop as a proxy stubnsfisis
Composer::getLoop() answers, so a plugin reaches the graph's own downloader and executor through the route Composer's own docblocks point plugin authors at, rather than through an instance of its own. wait() drains the promises it is given and rethrows the first rejection once the group is done, which is what React\Promise\all() hands PHP. abortJobs() has nothing to cancel while every request settles before the call that started it returns. A non-null $progress is an explicit error: a ProgressBar is a symfony/console object each world runs its own implementation of, so there is none to hand across. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31feat(plugin): serve HttpDownloader's async surfacensfisis
add() and addCopy() answer with a promise, and enableAsync(), wait() and countActiveJobs() answer alongside them. The Rust future runs to completion before the promise is handed over, so requests a plugin starts together run one after another rather than overlapping; overlapping them needs a promise representation that crosses the boundary unresolved. Everything else the surface does is preserved. add() still refuses a downloader outside a Loop, and it does so by throwing out of the call the way PHP does, where a failed request instead arrives as a rejection the caller handles — __shirabe_rejected_promise is the failure half of the resolved- promise helper. wait() and countActiveJobs() answer for a downloader with no outstanding job, which, once every request settles before its call returns, it never has. This is where HttpDownloader parts company with ProcessExecutor, whose async surface stays an explicit error: executeAsync() resolves its promise with a Symfony Process, whose state is the proc_open() resource of whichever process called start(), where a request resolves its promise with a Response. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31feat(plugin): carry Http\Response across as a materialized valuensfisis
A response is built for one request and the graph never retains it, so there is no entity for a handle to point at. The child holds a real instance instead, revived from the object record the wire carries, and collect() frees the copy each world holds — which is what that method is for. The value-object rule rejects the class only because collect() assigns to $this, so the category comes from an overrides.list entry. HttpDownloader::get() and copy() answer with one. Two gaps stay: decodeJson() reaches Composer\Json\JsonFile, which a guard shadows, and Composer answers a curl request with the CurlResponse subclass where this port flattens the value into a Response. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31feat(plugin): serve HttpDownloader as a proxy stubnsfisis
A downloader carries its own options, TLS defaults and request backends, and what it shares with the graph is the IO it collects authentication into and the config it reads. Plugin code writing `new HttpDownloader($io, $config)` therefore allocates a Rust-side entity of its own rather than a second downloader the graph knows nothing about, and the guard that shadowed the class in the worker is gone. The request surface is not served yet: get() and copy() have no wire representation for the Response they return, and the async surface resolves its promises with one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30feat(plugin): carry an exception's class and state across the boundarynsfisis
A Rust-side failure reached plugin code as a RuntimeException whose message carried the name of the call that failed, so `catch (TransportException $e)` never matched and the status code the plugin branches on was gone. The Throw frame now names the class the exception was thrown as and carries the state that class declares beyond message and code. \Shirabe\MaterializedThrowable rebuilds it in the child: `new $class($message, $code)` for a class whose constructor has \Exception's shape, then the properties by reflection. A class the child cannot build that way keeps the RuntimeException shape. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30feat(plugin): serve ProcessExecutor as a proxy stubnsfisis
Composer reaches this class two ways: the object graph hands one out through Composer::getLoop()->getProcessExecutor(), and plugins write `new ProcessExecutor($io)` freely. Both bind to a Rust-side entity, so the timeout the run shares -- seeded from process-timeout and rewritten while the run is in flight -- has one value instead of one per world, and the executor can still be passed to the classes that take one (`new Filesystem($process)`). Three things the stub generator was missing came with it: - By-ref parameters. The call carries their positions and the answer carries what each holds afterwards; a position the answer omits was never assigned to, which is what PHP does with an untouched by-ref parameter. ProcessExecutor::execute is the only one on a proxied class. - Argument arity, reproduced where the real body reads func_num_args(). execute($cmd) forwards the child's output and execute($cmd, $out) captures it, and nothing but the argument count separates the two. - Static methods that cannot run in the worker. One that reads a static property the Rust side owns, or that reaches a guarded class, forwards through __shirabeCallStatic instead of being materialized. That also fixes Filesystem::isLocalPath and getPlatformPath, whose materialized bodies called the guarded Composer\Util\Platform. The async surface stays an explicit error. executeAsync resolves its promise with a Symfony Process, whose proc_open() resource and pipes belong to whichever process called start(), so a Rust-side spawn has none to hand back; running the real start() in the worker needs a promise representation that crosses the boundary unresolved. The fixture project drives the whole synchronous surface from plugin code and compares the trace against upstream Composer byte for byte. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23docs(plugin): bring boundary text in line with the implementationnsfisis
Comments that pointed at design notes kept outside the repository are dead ends for anyone reading only the tree, so what each of them explained now lives in a tagged TODO at the site it applies to. Several of those sites also stated something the implementation does not do, and the TODOs record the actual gap instead: the two halves of the codec recognize handle descriptors by different rules, the scripts Command path drops the exception class and collects output in a BufferedOutput that cannot carry an interactive command, find_shortest_path panics where PHP throws, and the package dispatch hand-rolls the variant selection AnyPackage should own. The classifier document likewise described rust-snapshot, plugin-constructible and several of the open questions as designed rather than as built. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22refactor(plugin): decode RPC values through conversion traitsnsfisis
The R-table dispatchers hand-rolled argument decoding and return encoding per method, spread over 24 helpers. `FromPluginArg` and `ToPluginValue` replace them: blanket impls for `Option<T>`, `Vec<T>` and `IndexMap<String, T>` compose over a handful of leaf types, so a composite like `Vec<IndexMap<String, String>>` needs no helper of its own, and the lossy UTF-8 conversion of a PHP string lives in one impl instead of a dozen call sites. Argument errors now read `{method} expects {expected} at position {position}` rather than a message written per call site. `arg_or` takes its default for an explicit null too, not only for an omitted argument; every parameter it serves is declared non-nullable in the proxy stubs, so PHP never passes one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20fix(event-dispatcher): let a PHP script reach the Composer object graphnsfisis
A PHP script listener that walks the event it receives (Laravel's Illuminate\Foundation\ComposerScripts::postAutoloadDump asks for $event->getComposer()->getConfig()->get('vendor-dir')) aborted the run with `unknown Rust handle 2`. The event's getComposer/getIO answers register an entity in the R table and hand back its rhandle, but ScriptRpcDispatcher resolved only rhandle 0 and the one event handle of the call in flight, so it could not serve a method on a handle it had just minted itself. The R-table lookup PluginRpcDispatcher already does is now dispatch_r_table_method, shared by both. The stubs that graph hands out extend and implement the real Composer contracts (Composer\Package\PackageInterface and the rest), which live in the Composer PHP runtime and not among the generated stubs or guards, so execute_event_php_script loads that runtime the way the plugin and command-class paths do. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19perf(package): hand out link maps behind Rcnsfisis
PackageInterface::getRequires() and friends return the array of Link objects; in PHP that is a copy-on-write array of object references, so a caller pays nothing to look at it. The port returned IndexMap<String, Link> by value, so every call deep-cloned the whole map, keys and constraints included. Pool building calls these accessors once per package per candidate, which put IndexMap::clone at 13.5% of `require laravel/laravel`. Store the maps as Rc<IndexMap<String, Link>> and return a handle. Callers that mutate the map clone it explicitly at the point of mutation, matching where PHP would separate the array. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19fix(input): thread a typed InputValue through the input layernsfisis
Options and arguments were stored and passed as PhpMixed even though Symfony only ever puts a string, a bool, a list of strings or null in one. get_option already narrowed to InputOptionValue at the boundary; this widens that enum into InputValue and pushes it through InputInterface, InputOption/InputArgument defaults, the Input storage, ArgvInput/ArrayInput/StringInput/CompletionInput, Command::add_option and add_argument, and the Composer-side wrappers. Two neighbouring string|int unions get types of their own: InputDefinition::{get_argument,has_argument} take an ArgumentName, and ArrayInput keys its parameters by ParameterName. has_parameter_option and get_parameter_option take the values they look for as &[&str], which is what PHP's `(array) $values` cast produced anyway. Two behaviours change along the way. Input::set_option on a negated option now negates with PHP's loose bool cast rather than treating a non-bool as false, matching `!$value`. ArrayInput::parse now resolves an integer key to an argument position instead of looking up an argument literally named "0". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16feat(plugin): proxy Composer\Util\Filesystem into the plugin workernsfisis
The class was shadowed by a guard, so a plugin doing `new Filesystem()` got an explicit error. It is classified rust-proxy and plugin-constructible and the Rust port is complete, so listing it as a stub target and answering its public surface from the entity is all it takes. The constructor rejects a caller-supplied ProcessExecutor: that class has no proxy stub, so the argument could only be a second instance the Rust side never sees. findShortestPath re-checks its arguments at the boundary because the port panics where PHP throws, and a panic would take the process down instead of reaching the plugin's catch block. phpstan/extension-installer matches upstream Composer byte for byte again.
2026-08-16refactor: narrow pub(crate) items to privatensfisis
Porting mapped every PHP `protected` member onto `pub(crate)`, which is wider than nearly all of them need. Each item demoted here is reached only from the module that defines it, so the crate-wide visibility conveyed nothing. Every `pub(crate)` that survives has at least one reader in another module of the same crate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12docs(todo): retag TODO markers by root causensfisis
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10feat(console): import scripts Command classes as application commandsnsfisis
`Application::do_run` registers a `composer.json` script whose value names a `Symfony\Component\Console\Command\Command` subclass as a live command. The class checks and `new $dummy($script)` need a real PHP runtime, so they run in the worker: the command object lives there and this side keeps a metadata mirror for `list`/`help`, forwarding a run to the worker-side console application it is added to. The name and description fixups are applied to the worker-side object, so both sides carry the same values. Loading the Composer PHP runtime into the worker is gated on the Rust-side `ClassLoader`s resolving the class to a file, keeping that load out of every run whose scripts are plain shell commands. The worker-side console application handoff now accepts commands registered after it was published, since the scripts scan runs after plugin commands are collected. Whether it was published is tracked per application: the handoff is process-wide, so a second application must replace it rather than extend it. `shirabe_php_shim::is_subclass_of` has no callers left.
2026-08-10refactor(symfony): hide leaf modules behind their parent re-exportsnsfisis
Every parent module in the symfony-* crates already re-exported its leaf modules with `pub use`, so each item was reachable by two paths. Make the leaf modules private and route all callers through the single re-exported path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09feat(plugin): let a Rust-implemented plugin cross into the workernsfisis
PluginManager::getPluginCapability hands the plugin itself to the capability constructor. Only a PHP-implemented plugin had an entity the child could receive, so a Rust-implemented one bailed out; it now crosses as a handle to an R-table entity behind Shirabe\RustPluginStub, or its Capable flavour, since `$plugin instanceof Capable` is what decides whether Composer asks a plugin for capabilities at all. This is what the two capability tests of PluginInstallerTest were waiting on: the mocked Capable plugin is Rust-side, and one of them asserts the identity of the plugin read back out of $capability->args.
2026-08-09refactor(symfony-console): extract symfony/console into the ↵nsfisis
shirabe-symfony-console crate Move `Symfony\Component\Console` out of shirabe-external-packages and into its own crate, so the path is `shirabe_symfony_console::application::Application` instead of `shirabe_external_packages::symfony::console::application::Application`. The `delegate_to_inner!` and `delegate_command_trait_impls_to_inner!` macros move with it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08feat(php-shim): give ported exceptions PHP's class hierarchynsfisis
Ported exceptions were flat structs reached with `downcast_ref`, so Composer's `catch (\RuntimeException $e)` only matched the exact leaf type and `get_class($e)` had nothing to report. Each exception now embeds an instance of the class it extends and travels inside an `AnyThrowable`; `Catch::catch`/`catch_mut` walk that chain, and `PhpClass::php_class_name` yields the PHP FQCN. Dropping the `std::error::Error` impls from the exception types leaves `AnyThrowable` as the only route into an `anyhow::Error`, so the walk cannot be bypassed. A `no_exception_downcast` linter catches the `downcast::<X>()` calls that would now silently answer `None`. Three sites change behavior as a result: the `TransportException` exit-code override reaches `MaxFileSizeExceededException`, the `catch (\LogicException)` in findSimilar() reaches its subclasses, and rendered exception titles carry the real class name rather than a guess. `get_class_err()` is no longer a `todo!()`, which re-enables FilesystemRepositoryTest::testCorruptedRepositoryFile. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07feat(installer): dispatch package eventsnsfisis
InstallationManager left both PRE_PACKAGE_* and POST_PACKAGE_* as empty stubs, so a subscriber never ran at all and the difference from upstream was silent rather than an explicit error. Operations cross the boundary as R-table entities with generated proxy stubs. Materializing them the way a Link crosses is not possible: a materialized value is revived by unserialize() on the child side, so its properties never pass through the wire decoder and a nested handle descriptor would not come back as a stub — and an operation always holds a PackageInterface. execute() now shares one Rc per operation through the whole batch pipeline, so a plugin sees one object for both the pre- and the post-event of an operation, as it does in PHP. POST_PACKAGE_* also moves out of the operation's promise chain into the post-exec callback list PHP runs after waitOnPromises(). The stub generator materializes non-public class constants verbatim now, which the operation classes need for their `protected const TYPE`: a constant has no entity behind it, so a copy in the worker cannot diverge, and keeping the declared visibility exposes nothing the real class hides. The E2E fixture added here compares the recorded events against upstream Composer. It also surfaced that upstream starts an operation's chain where it is built (a null prepare() becomes an already-fulfilled React promise whose handlers run through the immediately drained queue) while this port only drives its futures in wait_on_promises, so the repository state a pre-event observes differs; that half of the comparison is a separate `#[ignore]`d test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07fix(plugin): make proxy stub property access an explicit errornsfisis
A proxy stub declares none of the real class's instance properties, and the `__get`/`__set` forwarders were emitted only for classes that declare a public one. Every other property access therefore got PHP's own answer for an undeclared property — null on a read, a dynamic property on a write — so plugin code reading state the entity holds ran on with null and failed somewhere else entirely, or not at all. Emit the forwarders on every root stub, add `__isset`/`__unset` alongside them so `isset()` cannot answer false silently either, and serve all four from one dispatcher that answers the state the Rust-side entity exposes and rejects every other name. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07feat(plugin): cross alias packages over RPC as proxy stubsnsfisis
AliasPackage, CompleteAliasPackage and RootAliasPackage now have generated proxy stubs, so a package handed to a plugin no longer has to be a real package: it crosses as the stub matching its concrete variant, answers getAliasOf / setRootPackageAlias / isRootPackageAlias / hasSelfVersionRequires, and can be constructed from plugin code. The setters RootPackageInterface declares are routed through that interface for every root package instead of through the base Package state. Only the alias variant needs it -- RootAliasPackage overrides all nine to write through to the package it aliases -- but a real RootPackage delegates to the same base state either way, so both take one path. An alias of an alias has no representation here, so narrowing the constructor argument to a real package is an explicit error rather than a silent demotion. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06feat(plugin): let links and release dates cross the RPC boundarynsfisis
The link getters and setters and the release date accessors were explicit errors for every non-empty value, because an immutable value has no entity to point a handle at. They now cross as materialized values: the descriptor names the real class and the constructor arguments, and each side builds a genuine instance of its own. The semver constraint a link holds is encoded structurally rather than re-parsed from its string form, so the pretty strings and the conjunctive flag survive the crossing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06feat(plugin): widen the RPC surface to LibraryInstaller-based pluginsnsfisis
An installer that extends LibraryInstaller reaches for the Composer object graph in ways the proxy did not answer: the download manager and the config, the writable repository methods, a React promise as its own return value, and `new Package(...)` from its supports() path. * `Composer::getConfig`/`getDownloadManager` are dispatched, and both classes become generated proxy stubs. Their reads and mutators answer from the Rust entity; the surfaces needing stubs of their own (ConfigSourceInterface, DownloaderInterface) stay explicit errors. * The download manager's futures are driven to completion and handed back as already-settled React promises, since PHP declares a non-nullable PromiseInterface there. A promise a plugin returns is drained the same way: settled yields its value, rejected re-raises, pending is an explicit error. * Proxy stubs now carry the real class's constructor and ask the Rust side to allocate the entity; reviving a stub for an existing entity binds the handle without running it. Classes Rust cannot build name themselves in the error. * The package proxy covers `Package`'s own setters, `CompletePackage`'s metadata, `RootPackage`'s root-only state, and `BasePackage::$id`. Link values and release dates still have no wire image, so the methods carrying them remain explicit errors.
2026-08-06feat(plugin): run plugin-provided installers through the RPC workernsfisis
A plugin can now hand an InstallerInterface implementation to InstallationManager::addInstaller across the wire, and a legacy composer-installer package is loaded as one; both are backed by a PhpInstallerProxy forwarding the whole installer contract to the entity in the PHP worker. An installer returning a real promise is an explicit error until promises can cross the boundary. InstallationManager takes installers as shared handles instead of boxes, so the object identity removeInstaller and PluginManager's registeredPlugins compare against survives registration, and holds them in a RefCell: Installer::run keeps a shared borrow of the manager for the whole run, and a plugin activated inside it registers its installer from there. The type cache keys on the installer itself, like upstream, so re-entrant registration cannot leave a stale index behind. InstallerInterface::supports is fallible for the same reason getCapabilities and getCommands are: it answers over RPC. Cloning a proxy stub clones the Rust-side entity and rebinds the copy to the fresh handle. Previously only the classes declaring __clone got a throwing body, and the rest let two stubs share (and twice release) one handle. Package entities answer with AnyPackage::dup, which already carries BasePackage::__clone and the RootAliasPackage override; the others are an explicit error. The package proxy covers the whole PackageInterface surface; only the link maps and the release date still lack a wire image for their value objects. PluginManager gains a test-only seam for the reported Plugin API version, and the three PluginInstallerTest cases that need it are ported. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05feat(plugin): run plugin-provided commands in a worker-side applicationnsfisis
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>
2026-08-05feat(plugin): discover and list plugin-provided commandsnsfisis
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>
2026-08-04feat(plugin): instantiate plugin capabilities through the PHP RPC workernsfisis
getPluginCapability was a no-op returning None. Now it runs the real flow: class_exists in the worker, new $capabilityClass($ctorArgs) with the plugin's own phandle spliced in as $ctorArgs['plugin'], both instanceof checks answered by is_a in the child, and a per-interface Rust adapter over the resulting entity (CommandProvider and the plain Capability marker; anything else is an explicit error). getPluginCapabilities now propagates errors instead of swallowing them. Capable::get_capabilities widens from IndexMap<String, String> to IndexMap<String, PhpMixed>: upstream casts the return with (array) and validates only the queried key, so the narrow type both rejected maps Composer accepts and made the invalidImplementationClassNames data provider unrepresentable. The old code also returned ' 0 ' (trimmed to the falsy '0') as a valid class name where upstream throws; the rewritten validation follows upstream's empty/is_string/trim sequence. CommandProvider::get_commands returns BaseCommand adapters whose name is read back over RPC after the PHP constructor ran configure(); executing one still needs the PHP-side Symfony Application and stays an explicit error. The is_array / instanceof BaseCommand checks that upstream's Application::getPluginCommands performs on the raw getCommands value live in the adapter, because Vec<Box<dyn BaseCommand>> asserts every element up front. Ports testCommandProviderCapability (plugin-v8 end to end against the real worker) and testQueryingWithInvalidCapabilityClassNameThrows (all eight provider cases); the two tests that pass a PHPUnit mock plugin into PHP stay ignored — a Rust-native mock has no PHP-side entity to cross the boundary as $ctorArgs['plugin']. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04feat(plugin): expose the composer object graph to plugins over RPCnsfisis
Widen the R table beyond Composer/IO with InstallationManager, RepositoryManager, repository and package entities, interned by pointer identity through a shared register_entity. Script events answer getComposer/getIO, Composer hands out its graph getters, repositories list their packages as per-variant stubs, package getters cover the extension-installer surface (getRequires only while empty: the Link snapshot encoding does not exist yet), and getInstallPath resolves its package argument back through the R table. Everything else stays an explicit error. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04feat(plugin): dispatch plugin event subscribers through the RPC workernsfisis
Wires the addPlugin subscriber branch end to end: EventSubscriberInterface and Capable become fallible and dyn-compatible (their sole implementor is the PHP plugin proxy, which answers getSubscribedEvents over RPC), listeners register as Callable::PhpMethod and are invoked with a per-call event handle, and the R table now drops entries when a child-side stub destructs. The R table keeps its IndexMap with monotonically increasing handles, so released handles are never reused and no generation counter is needed. Upstream has no subscriber-plugin test, so the path is covered by a Shirabe-owned fixture exercising all three getSubscribedEvents shapes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04feat(plugin): generate the worker proxy stubs from the Composer sourcesnsfisis
Replace the hand-written proxy stubs under crates/shirabe-php-rpc/php/stubs with output of scripts/plugin-stub-generator, a deterministic emitter that derives every stub from the Composer checkout and the classifier report. Anything it cannot faithfully proxy (by-ref/variadic parameters, magic methods, public properties, diverging omitted overrides, stale stub files, a STUB_FILES entry missing in lib.rs) fails generation instead of degrading silently, so future Composer releases surface new members as explicit errors rather than silent gaps. Regenerating the stubs also normalizes the hand-written inconsistencies (uniform guarded constructors, import-based type spellings) and fixes real gaps the review of the generated diff uncovered: the BaseIO authentication methods now carry the real class's untyped signatures, and the previously missing ConsoleIO::sanitize is materialized together with its private static helper. A cargo test runs generate-stubs --check to keep the committed stubs, the generator and the embedded list from drifting apart. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04fix(plugin): resolve review findings in the plugin activation flownsfisis
InstallationManager::execute now takes &self (the mock recorder moved into a RefCell) and its callers hold only shared borrows: plugin registration inside a batch re-enters the same manager handle through Composer::getInstallationManager()->getInstallPath(), which panicked on the RefCell re-borrow under the &mut shape — the same re-entrancy the repository side already fixed, unreachable from the ported tests because they call PluginInstaller::install directly like PHPUnit does. The worker-side InstalledVersions mirror now matches the full tail of FilesystemRepository::write: unconditional reload plus the reflection-based selfDir/installedIsLocalDir restore. The previous class_exists(false) guard rested on a lazy-load assumption that does not hold in the worker (its real ClassLoader only knows the Composer checkout's vendor dir, so a later lazy load would read the checkout's installed.php, not the project's); the mirror is now skipped only when the class is not autoloadable at all, i.e. no plugin runtime and hence no observer code. Boot-time seeding stays TODO(plugin). Also from the review: registered_plugins entries are removed only after the deactivate/uninstall loop (PHP unsets last, and a throw must leave the entry observable); extra.class keeps associative-array values and fails loudly on non-strings instead of silently dropping them; the two discarded write() results now propagate (they carry the reload-push failure); register_package's allow-plugins skip message is DEBUG like the addPlugin side; the loader-eviction divergence of REGISTERED_LOADERS and the lossy UTF-8 spots carry searchable markers; the test-only proxy downcast follows the __ naming rule; the R-table dispatch clones the entity out instead of holding the table borrow across the handler; the IO/PartialComposer stubs turn a plugin-side `new NullIO()` into an explicit error instead of an ArgumentCountError; and the empty() emulation covers float 0.0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04feat(plugin): activate plugins through the PHP RPC workernsfisis
Implement the remainder of PluginManager::registerPackage: the plugin autoload map is built by the ported createLoader/parseAutoloads and served to the worker over the existing reverse-RPC autoloader, files entries go through a composerRequire-equivalent glue call, and already-defined classes take the upstream _composer_tmp rename/eval path. Instantiation uses the new NewObject/CallPhpMethod lanes backed by a P table in the worker; PhpPluginProxy adapts the resulting handle to PluginInterface, with $composer/$io exposed to plugin callbacks via an R table (unsupported methods stay explicit errors). Hand-written proxy stubs cover Composer, PartialComposer and the IO hierarchy, and the stub autoloader is re-prepended after loading the Composer PHP runtime so its vendor autoloader cannot shadow proxied FQCNs. FilesystemRepository::write now mirrors InstalledVersions::reload into a running worker (class_exists-guarded, so an unloaded class keeps its upstream lazy-load behavior), removing the previously undefined observation window. The installer pipeline passes the installed repository as a shared handle instead of a long-lived `&mut dyn`: plugin registration runs inside InstallationManager::execute and re-enters the same local repository through the RepositoryManager, which would panic on the RefCell re-borrow under the old shape. PluginInterface lifecycle methods now take an owned ComposerHandle (plugins retain $composer past the call) and return anyhow::Result (PHP plugin code may throw); the plugin list uses shared ownership so the identity comparison of removePlugin survives the dual storage in registeredPlugins, matching PHP reference semantics. Ports the activate/upgrade/uninstall tests of PluginInstallerTest, serialized across the shared worker process whose persistent class table is exactly what exercises the rename path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>