aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/src
AgeCommit message (Collapse)Author
2026-08-09refactor(php-shim): make the fs mutators return Resultnsfisis
mkdir, rmdir, unlink and symlink each had a bool version and a _result twin returning the io::Error, which left two names for one call. Keep only the Result form and let the callers that want a boolean spell out .is_ok(). Call sites that discard the outcome, as their PHP originals do, are unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09refactor(installed-versions): drop the Rust port, which has no readersnsfisis
InstalledVersions is a runtime API for plugins and project code; Composer itself never reads it. Its consumers run in the PHP worker against the copy FilesystemRepository dumps to vendor/composer/InstalledVersions.php, whose static state is already kept in sync by __shirabe_installed_versions_reload. Nothing in Rust read the mirrored statics, so reload() and the reflection setters were no-ops. The tests covered only the Rust port, not the PHP class the worker loads, so they assert nothing about compatibility; they are left as todo!() skeletons. This also removes the shim functions method_exists, php_dir and require_php_file, whose only caller was the deleted module. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09refactor(installer): drop the GC control callsnsfisis
Composer turns the cycle collector off around the dependency solver, but Rust has no GC, so the gc_collect_cycles/gc_disable/gc_enable shims were no-ops. Remove the call sites and the now-callerless shim functions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09refactor(installed-versions): merge the name lists without call_user_func_arraynsfisis
The only live caller passed the constant 'array_merge', so the list concatenation is written out in place, as ClassLoader::get_prefixes already does. That leaves the shim function without callers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09fix(dependency-resolver): query the PHP runtime for the php versionnsfisis
Problem reported the platform php version from the shim's compile-time PHP_VERSION constant, and the shim's phpversion() hit todo!() for any extension. Go through the RPC bridge instead, as PHP's Composer\Platform\Runtime does, and drop the shim function that now has no callers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09feat(archiver): preserve file permissions in zip archivesnsfisis
The shim reported setExternalAttributesName as missing, so ZipArchiver took the branch for libzip below 0.11.2 and every archived entry got the zip crate's default mode. The attributes now travel as arguments to add_file and add_empty_dir, because the crate fixes an entry's external attributes when the entry is started and offers no way to amend one already written. unix_permissions keeps only the low 9 mode bits, on top of which the crate restores S_IFREG and S_IFDIR, so setuid/setgid/sticky bits and other file types are still dropped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09refactor(env): split and join PATH lists with std::envnsfisis
The shim's PATH_SEPARATOR was hardcoded to ":", so every PATH list was split and joined on the wrong character off Unix. std::env::split_paths and join_paths use the platform's separator, which also lets the bin-dir membership test in EventDispatcher compare list entries instead of regex-matching the raw PATH string. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09refactor(path): build paths with Path::joinnsfisis
Concatenating a directory, MAIN_SEPARATOR and a component reimplements what Path::join already does. Joining directly also drops the separator from the intermediate values it used to be baked into, such as ClassLoader's $pathEnd. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09refactor(php-shim): use std::path::MAIN_SEPARATOR over a shim constantnsfisis
The shim's DIRECTORY_SEPARATOR was hardcoded to "/", so every ported `'\\' === DIRECTORY_SEPARATOR` check compared against a constant that does not track the target platform. std::path::MAIN_SEPARATOR and MAIN_SEPARATOR_STR carry the same meaning as PHP's constant and resolve per platform, so the Windows branches are selected on Windows targets. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09test(console): cover the dev build warning branchnsfisis
The deadline is a constant baked in at build time, so PHP's define() of COMPOSER_DEV_WARNING_TIME cannot make Application take that branch. Application now holds the deadline in a field that __set_dev_warning_time overrides, letting testDevWarning run instead of staying ignored. The define() shim, whose only caller was that test, goes away with it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09fix(json): let an \Error out of JsonFile::read untouchednsfisis
The port's fall-through stood in for PHP's `catch (\Exception $e)`, which does not catch an `\Error`; a TypeError reaching JsonFile::read would come back out as a `\RuntimeException` saying the file could not be read. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09fix(archiver): narrow PharArchiver's catch to \UnexpectedValueExceptionnsfisis
PharArchiver::archive catches only `\UnexpectedValueException` and wraps it in a `\RuntimeException` naming the target and sources; anything else leaves the method as it was thrown. The port wrapped every error, so the two `\RuntimeException('Can not compress to %s format')` raised inside the `try` came back out with a second message around them and their code replaced. Reading the message and code off the caught exception rather than off whatever arrived also drops the guesswork the old `map_or(0, ..)` had to do. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09fix(exception): carry PHP's $previous through the ported throw sitesnsfisis
Composer hands the exception it caught to the one it throws in its place, so `getPrevious()` reaches the cause and Application's renderer prints the whole chain. Every ported site dropped it, because the flat exception structs had nowhere to put one. `AnyThrowable::into_previous` turns the caught error into that argument, and the 13 sites now pass it. `getCode()` came along for the ride at the four sites that derive the new exception's code from the caught one (PharArchiver, ArrayLoader x2), and ComposerRepository's message now names the caught exception's class instead of the literal "Exception". GitHubDriver::attemptCloneFallback took the previous exception's message and appended it to its own, which no `\RuntimeException('Fallback to git driver disabled')` in Composer ever says; it now chains it instead. Git::syncMirror restores what PHP's `finally` does to an exception in flight: the `git remote set-url` that scrubs credentials back out of the URL runs in a `finally`, and when it fails PHP propagates *its* exception over the one already leaving, chaining the displaced one as previous. The port discarded the finally's result, so a failure to scrub the URL was reported as a successful mirror sync. `AnyThrowable::set_previous` models the engine-level chaining. 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-08feat(composer): bake the dev build warning deadline in at build timensfisis
Composer defines COMPOSER_DEV_WARNING_TIME from its phar stub when the compiled version is a commit hash rather than a tag, so the value exists only in the build artifact. It was a todo!() in the PHP shim, leaving the warning branch in Application::do_run unreachable. A build script now derives it the way Compiler does, from git describe and the HEAD commit date, and composer::COMPOSER_DEV_WARNING_TIME holds the result as a Rust constant instead of a runtime-defined one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08refactor(php-shim): dispatch spl_object_hash on Rc and referencesnsfisis
Getting a rule's identity meant spl_object_hash(&*rule.borrow()), so the solver borrowed a RefCell just to read an address, and a second function spl_object_hash_process existed because one generic fn cannot tell Rc<T> from &T. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08refactor(semver): replace Constraint's OP_*/STR_OP_* with CmpOpnsfisis
PHP's version_compare takes its operator as a string, so the shim's port did too, and Constraint carried two families of operator constants plus translation tables to convert between the string form and its own int codes. Five copies of those tables had accumulated across Constraint, CompilingMatcher and the plugin value bridge. version_compare now takes a CmpOp, which makes an invalid operator unrepresentable and removes the tables' reason to exist. Constraint stores a CmpOp and keeps only the string parsing its constructor needs; getOperator, compile and CompilingMatcher::match speak CmpOp as well. PHP's OP_* numbering stays observable: a plugin reads the raw integer off the Constraint object over RPC, so get_operator_constant and its new inverse hold that 0..5 mapping. 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-07fix(autoload-generator): survive listeners that re-enter dump()nsfisis
AutoloadGenerator::dump dispatches PRE_AUTOLOAD_DUMP / POST_AUTOLOAD_DUMP, and EventDispatcher::make_autoloader answers those by walking back into the Composer graph -- the local repository, the installation manager, and the generator itself. Installer::run held mutable borrows of all three across the dump call, so any plugin subscribing to either event panicked with "RefCell already borrowed" before its listener ran. dump() and build_package_map() now take the local repository, the installation manager and the locker as shared handles instead of &mut dyn, and the borrow is taken where it is used. The generator itself is re-entered, not just borrowed twice: PHP listeners reach it through Composer::getAutoloadGenerator() and call setDevMode() while dump() is running, and make_autoloader asks the same object for a package map. That is not expressible behind &mut self, so the mutable state moves into Cell/RefCell fields and the whole AutoloadGeneratorInterface takes &self. 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-07refactor(platform-repository): fetch the PHP runtime in one RPC callnsfisis
The RuntimeInterface seam asked the worker one question at a time: a round trip per loaded extension, per ReflectionExtension::info() output and per constant, so a single `show --platform` cost 70 to 100 of them. A `platform` dispatch entry now answers all of it as one PHP array, which shirabe-php-rpc decodes into a OnceLock-cached PlatformInfo, the way the diagnose command already works. Composer\Platform\Runtime therefore has no Rust counterpart any more. Its work belongs to the running interpreter, and invoke()/construct() could only be ported as a whitelist that panicked on anything unlisted; it is ported as PHP into the worker instead, and PlatformRepository reads the answers off PlatformInfo. Accessors panic on a name the payload does not carry, so the worker and its consumers cannot drift apart unnoticed. The tests describe the runtime as payload data where they used to mock the seam, with the datasets unchanged. The one loss is the call-count assertion of test_inet_pton_regression: the payload reports the result of `@inet_pton('::')` rather than answering a call. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07refactor: merge split inherent impl blocks into one per typensfisis
Enable clippy::multiple_inherent_impl and fix the 21 sites it reports. Types whose inherent methods were spread across two or three impl blocks now keep them in a single block; only the impl headers move, no method bodies change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07chore: use shirabe_php_shim::impl_php_classnsfisis
2026-08-07test: port the tests left as todo!() stubsnsfisis
Replace the todo!() bodies with real ports. Four autoload-generator tests now run for real; the rest stay #[ignore]d, but each ignore reason now names the concrete missing symbol instead of a vague subsystem. Production additions the ports need: the deprecated AuthHelper::addAuthenticationHeader wrapper, EventDispatcher::__set_dispatch_script_override as the seam for PHPUnit onlyMethods(['dispatchScript']), and a define() stub in the shim. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07feat(php-rpc): cross materialized values as PHP object recordsnsfisis
A materialized value used to cross as a constructor call: the class name, the arguments, and any post-construction setter. Describing a real instance that way needed a ReflectionProperty read for every field the class exposes no getter for, and state no constructor takes (an unset pretty string, a Link built without a pretty constraint) had no faithful call to describe it at all. The value now crosses as the object record serialize() writes for it, which unserialize() revives without running a constructor, so both sides transfer the state itself instead of a recipe for rebuilding it. The PHP half keeps only the class list (also the allowed_classes list of every frame payload) and the UTC rebasing of dates; describe(), build() and the reflection are gone. The wire codec gains O: records (PluginValue::PhpObject) and r: back references, whose resolution reproduces PHP's numbering of every value in a payload; a cyclic object graph and a PHP reference (R:) are rejected. Two behaviours change with it: a date crosses carrying timezone_type 3 "UTC" rather than a +00:00 offset, which is what ArrayLoader builds a release date as, and a Link subclass crosses as a P-table entity instead of being silently downgraded to a plain Link. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06fix(platform-repository): report the icu and imagick librariesnsfisis
PlatformRepository probes ResourceBundle, IntlChar and Imagick to derive lib-icu-cldr, lib-icu-unicode and lib-imagick-imagemagick. Those probes ran against the shim's hard-coded class_exists allowlist, which never names them, so the packages were silently missing: on a machine with intl, `show --platform` listed fewer libraries than upstream Composer does. The runtime seam now asks the real PHP: hasClass over RPC, and construct / invoke through the worker for the three classes PlatformRepository reaches. A live PHP object has no PhpMixed counterpart, so the seam answers with the entries the caller reads off it. The seam's own callers read those entries instead of returning null and the empty string. Two addLibrary calls also had replaces and provides swapped, dropping `lib-libxslt replaces lib-xsl` and `lib-zip-libzip replaces lib-zip`. `show --platform` now matches upstream Composer byte for byte, and all 59 provideLibraryTestCases datasets pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06fix(repository-manager): fail explicitly on an unknown repository classnsfisis
createRepository instantiates `new $class(...)` from the name registered for the type. The port dispatches over the classes it implements, and the remaining arm was a todo!(). setRepositoryClass is public API, so a plugin registering a class of its own reached it and panicked. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06fix(download-manager): name the downloader class in its LogicExceptionnsfisis
getDownloaderForPackage reports get_class($downloader) when the resolved downloader's installation source does not match. Rust has no runtime class name, so the message was built from a shim stub that panicked instead — the error could never be returned. DownloaderInterface now requires PhpClass, the trait already used for the same purpose on Command, and each downloader states the name PHP reports. That leaves get_class_obj without callers, so it is gone. 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-06refactor(php-shim): take &str and return String from pathinfonsfisis
pathinfo only ever receives a string and only ever returns one for the single-component options it supports, so the PhpMixed wrapping forced every call site to pack and unpack the value again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06refactor: replace literal-list in_array_strict with matches!nsfisis
Call sites whose haystack was an inline array of literals (or a local built solely to feed one) had to wrap both sides in PhpMixed just to compare, allocating a String per element on every call. matches! does the same test against the underlying &str/i64/Option directly, so the PhpMixed round trip and its .to_string()/.clone()/.iter().map() conversions are gone. Sites whose haystack is a runtime value or a named constant array are left on in_array_strict: inlining a named constant would duplicate its contents at the call site. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06refactor(php-shim): split in_array into strict and loose variantsnsfisis
2026-08-06style(autoload): use raw strings for the PHP heredoc templatesnsfisis
The generated-file templates are heredocs in Composer's AutoloadGenerator. Writing them as raw string literals keeps the emitted PHP laid out the way it is written out, matching the blocks in getAutoloadRealFile and getStaticFile that already use them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06style(binary-installer): use raw strings for the PHP heredoc templatesnsfisis
The proxy templates are heredocs in Composer's BinaryInstaller. Writing them as raw string literals keeps the generated PHP and shell code laid out the way it is emitted, instead of one escaped line. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06refactor(class-map-generator): narrow scanPaths' $path to a stringnsfisis
The docblock allows string|iterable<SplFileInfo>, but every call site in Composer passes a string, so the iterable branch was only a todo!(). Taking &str drops it together with the InvalidArgumentException that the type now rules out. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06refactor(spdx-licenses): return a dedicated type from getLicenseByIdentifiernsfisis
PHP hands back a positional array, so every caller had to reach into a PhpMixed list by index and fall back to a default when an element was missing or the wrong variant -- fallbacks that could never fire, since the list shape is fixed. LicenseMetadata names the four elements and makes the accesses total. This also drops the crate's last use of PhpMixed, and with it the dependency on shirabe-php-shim. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06chore: drop @param/@return tags that only restate Rust typesnsfisis
The ported docblocks copied @param and @return straight from the PHP source. When such a tag carries nothing but a type and an argument name, the Rust signature already states it, so the line is noise. Tags whose text adds prose beyond the type are kept. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06feat(console): override TransportException's code in doRunnsfisis
PHP mutates the caught exception's protected $code via ReflectionProperty. The port takes ownership of the exception through anyhow's downcast, writes ERROR_TRANSPORT_EXCEPTION into its public code field and re-wraps it, so no reflection equivalent is needed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06refactor(php-shim): take filesystem paths as impl AsRef<Path>nsfisis
The shim's filesystem entry points took `&str` even though each one resolves to a local path through `std::fs` or a syscall, so callers holding a `PathBuf` had to stringify it at the call site. They now take `impl AsRef<Path>`, the form `file_exists`, `is_dir`, `unlink` and the rest of the already-converted set use. `Phar`, `PharData` and `ZipArchive` keep their archive path as a `PathBuf`. `PharData::compress` names the compressed sibling by appending the suffix to the file name rather than formatting the path into a `String`. Arguments PHP resolves through a stream wrapper (`fopen`, `file_put_contents`, `include`) still take `&str`, as do the byte-string operations (`dirname`, `basename`, `pathinfo`) and archive-internal entry names, which are `/`-joined logical names rather than OS paths. 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-05test(plugin): pin a composer-normalize fixture and compare its listingnsfisis
The pinned plugin and its real dependency tree (nine packages, fetched by commit and verified by tree hash into the git-ignored ext/) install under both implementations, and the list/help renderings of the plugin-provided normalize command must match upstream byte for byte. The execution comparison is written but ignored: NormalizeCommand builds a second, in-process Composer instance, and the worker's proxy stubs reject native construction of the classes that path instantiates. Stale comments about the missing worker-side application are updated to the current facts. Co-Authored-By: Claude Fable 5 <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-04docs: state code facts instead of porting-phase progressnsfisis
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-04fix(package): keep list-shaped license arrays in ValidatingArrayLoadernsfisis
PHP's `(array)` cast passes an array through unchanged, but the port only matched the map shape and wrapped a JSON list (e.g. ["MIT"]) as a single license value, which then failed the is-string check and was silently dropped from composer.lock / installed.json with a bogus warning. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04feat(plugin): remove subscribed listeners when a plugin is removednsfisis
Wire removePlugin to EventDispatcher::removeListener now that subscriber listeners carry the plugin's P-table handle: PHP's $candidate[0] === $listener identity check maps to phandle equality on Callable::PhpMethod. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>