aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates
AgeCommit message (Collapse)Author
2026-08-06test(plugin): cover composer/installers beyond a fresh installnsfisis
A single install leaves the installer contract half tested: it never reaches `update`, never runs over an already-installed tree, and never reaches the plugin's own `uninstall()` override — the one chaining onto the promise `LibraryInstaller::uninstall` returns. Nor does it touch the configuration surface real projects use, `installer-paths` and `installer-name`, which the plugin reads back through the package proxy. All three now compare command output as well as the resulting tree against upstream Composer. Progress bar frames are dropped from that comparison: Shirabe renders them differently for every install, including projects with no plugin at all, so they say nothing about the plugin under test. With those covered the plugin joins the verified list in the README.
2026-08-06test(plugin): compare a composer/installers run against upstream Composernsfisis
The fixture project pins composer/installers 2.3.0 and requires two packages of framework-specific types, so the plugin's LibraryInstaller subclass decides where they land. Upstream Composer and Shirabe install the same project and the whole resulting tree is compared. The plugin tarball is fetched by `fixtures/e2e-installers/fetch` into a git-ignored directory and pinned by a digest over the extracted files, so the third-party source never enters this repository and the test skips itself while the directory is absent. The tree is staged and only moved into place once verified, so an unverified tree is never observable under the name the test looks for.
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-06test(plugin): compare a plugin-provided installer against upstream Composernsfisis
The fixture plugin registers an InstallerInterface implementation of its own and installs a package of a custom type with it, recording every contract call it receives. A fresh install produces a byte-identical project tree on both implementations, trace included. A second test pins the divergence a re-run and a `remove` expose: the two implementations consult getInstaller at different points, so the trace order — and its length once `remove` re-creates the Composer instance — differs. It is written in full and marked ignored rather than trimmed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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-05test(plugin): compare plugin-provided command execution against upstreamnsfisis
A Shirabe-authored CommandProvider fixture plugin (greet) runs under both implementations: exit codes, the command's own output, help/list rendering, alias resolution, a validation failure, and a file recording the shared object graph the command observed (root package, strict application FQCN, and the exit code of the built-in about command it invoked) must match. 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(symfony-console): expose the input __toString on InputInterfacensfisis
Every Symfony input already ports __toString as a Display impl, but callers holding a dyn InputInterface could not reach it; forwarding a command run across the plugin RPC boundary needs the stringified input. 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(php-shim): render stdClass-shaped objects in var_exportnsfisis
PhpMixed::Object hit a todo!(); the capability-name validation message in PluginManager (var_export of an invalid getCapabilities value) reaches it with a stdClass. Match the PHP 8.5.8 output byte for byte — the "(object) array(...)" shape, properties indented one space deeper than array elements, keys always quoted strings — verified against the real PHP as the oracle and pinned by a unit test. Other classes would render as \Class::__set_state(...), which stays out of reach because PhpMixed::Object carries no class name. 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-04test(plugin): compare a real-plugin install against upstream Composernsfisis
Run upstream Composer and Shirabe over pristine copies of a fixture project at the same path and require composer.lock, the whole vendor tree (including the plugin-generated GeneratedConfig.php), the plugin's IO lines and the exit code to match byte for byte. The plugin under test (phpstan/extension-installer 1.4.3) is downloaded by fixtures/e2e/fetch — pinned to an upstream commit and hash-verified — into a git-ignored directory rather than committed; the test skips while it is absent, like the other real-PHP prerequisites. Its dependencies are minimal stand-ins resolved from local repositories, so test runs stay offline. A zip dist would exercise the known lossy-string byte-precision debt in RemoteFilesystem, so the fixture serves the plugin through a path dist until that is resolved. 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): generate proxy stubs for the package and repository graphnsfisis
Emit the eleven stubs a subscriber plugin's object-graph calls reach (BasePackage through RootPackage, the installed-repository hierarchy, RepositoryManager, InstallationManager). The generator learns the member kinds these classes need: builtin interfaces contribute no closure entries, public static properties are materialized, public instance properties forward through __get/__set, __toString forwards like a plain method, __clone throws (proxy clone semantics are still an open design question), and a subclass stub may add interfaces to its inherited surface. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04test(plugin): tolerate the _composer_tmp rename in the removal testnsfisis
The PHP worker is shared across the test binary, so whichever subscriber test runs first owns the bare class name and every later install registers the plugin under a _composer_tmpN rename (as upstream does). The exact-name lookup made the removal test depend on lock acquisition order. 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>
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>
2026-08-03feat(event-dispatcher): run composer.json PHP scripts through the RPC workernsfisis
Implement the two script execution paths that previously stopped at todo!(): a Class::method listener is invoked as CallStaticMethod with the event crossing the boundary as a proxy-stub handle, and a Command-class listener runs inside a throwaway bare Symfony Application hosted by the worker via a generated snippet, its BufferedOutput written back through the dispatcher's IO. makeAutoloader is ported for real (canonical-package hash, setDevMode, buildPackageMap/parseAutoloads/createLoader), and the class_exists/is_callable/is_a/defined guards now query the worker, whose script autoloader resolves classes by asking the Rust-side ClassLoader over the reverse channel. EventInterface gains as_any (the IOInterface downcast pattern) so the concrete event type is reachable behind the trait object. Application::do_run now registers ScriptAliasCommand entries as typed commands, unblocking the run-script --list/alias tests; the dev-mode-to-generator test is ported with local mockall mocks. The remaining ignored tests carry re-verified reasons: the listener methods live on the PHPUnit test class itself (unloadable in the worker), or the test needs live import of a user PHP Command class into the Application. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03test(json): serialize the tests sharing the tabs2.json scratch filensfisis
test_preserve_indentation_after_read and test_overwrites_indentation_by_default copy to and delete the same fixture path; PHPUnit runs them serially, the parallel Rust harness let them race and flake. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03feat(php-rpc): rework the RPC channel into the tagged plugin protocolnsfisis
Replace the name\0arg framing with the plugin wire protocol: tagged frames with corr_id multiplexing, a MAX_FRAME_LEN bound, a thread-ID based reentrant SessionLock, and the PluginValue codec (encoder plus the first recursive decoder, iterative with a 512-level depth cap). Float formatting is ported from php-src into shirabe-php-src so the encoder is byte-compatible with serialize() under serialize_precision=-1, which the spawned worker now pins. The worker gains a standing dispatch loop, CallRustMethod reentrancy, hand-written Event proxy stubs, and explicit-error answers for everything not implemented yet. The public query API (get_php_version and friends) is unchanged and now rides the new protocol; the codec is verified against real PHP by roundtrip oracle tests covering floats, non-UTF-8 bytes and deep nesting. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03docs(plugin): record dispositions of plugin-boundary open questionsnsfisis
The Package family settles on pure rust-proxy: it is genuinely mutable (60+ setters, and Composer itself mutates packages in flight), so the planned snapshot-with-writeback treatment is dropped rather than filed as an override. The other open questions remain open with their interim behavior pinned instead of resolved: proxy-side getLoop() access and the ConsoleIO table/progress-bar members are explicit errors, and the InstalledVersions state a plugin observes after a Rust-side dump is undefined, marked TODO(plugin) at the reload site. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02fix(repository): resolve remaining late-binding hazards from the auditnsfisis
Three fixes for the ComposerRepository/FilesystemRepository/PlatformRepository hazards where inner-composition delegation skipped PHP's late-bound virtual dispatch: - ComposerRepository::has_package now builds its packageMap through the late-bound getPackages() equivalent, so lazy-providers repos surface the LogicException and available-packages repos load their package list, as in PHP, instead of silently answering false from the raw array. - RepositoryInterface::get_repo_name returns anyhow::Result<String>: PHP's getRepoName() counts through the late-bound initialize(), which is fallible in file-reading subclasses. FilesystemRepository and PackageRepository now run that initialization instead of freezing the inner array repository to an empty state (which also made a later write() truncate installed.json). Supporting changes keep the initialization chain callable from &self: JsonFile::read takes &self (indent moved into a RefCell), FilesystemRepository dev_mode became a Cell, and WritableArrayRepository dev_package_names a RefCell. - PlatformRepository::new routes constructor packages through its own add_package so the override handling and full platform initialization run as they do via PHP's parent constructor; the inner find_package/add_package delegations inside add_package (and ComposerRepository::add_package) gained the same is_initialized guard, since the constructor path would otherwise freeze the repository. Same defect class as 7db937af, 97b5211a and 3e367f78. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02fix(repository): restore late-bound initialize in ComposerRepository ↵nsfisis
fallthroughs findPackage()/findPackages()/search() delegate their non-lazy, non-provider fallthrough to the inner ArrayRepository, whose self-initialization froze the packages array before ComposerRepository::initialize could read the root file, so a plain v1-style repo (inline "packages" in packages.json) always answered empty. Guard the delegations with the same is_initialized() check used by count()/hasPackage(). getProviders() had the inverse defect: PHP reads the raw $this->packages property, but the port went through count(), whose initialize poisoned the initialization flag so the root file would never load afterwards. Check the raw field for non-empty instead, matching PHP's truthiness test. Same defect class as 97b5211a and the 3e367f78 downloader fix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02fix(repository): restore late-bound initialize in FilesystemRepository ↵nsfisis
add/remove ArrayRepository::addPackage()/removePackage() rely on PHP late binding to run FilesystemRepository::initialize (reading the file) on first touch. The inner delegation skipped that: an add on a not-yet-read repository froze the array to just the added packages (a later write() would truncate installed.json), and a remove hit the packages-initialized expect(). Guard both with ensure_initialized(), matching the pattern used by the read paths. Same defect class as the 3e367f78 downloader fix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02test(bootstrap): port the remaining env setup from tests/bootstrap.phpnsfisis
Add NO_COLOR=1, the COMPOSER/COMPOSER_VENDOR_DIR/COMPOSER_BIN_DIR clears and the timezone normalization, and make bootstrap() Once-guarded so additional call sites stay safe. Wiring it into every test binary still needs a libtest setup hook (ctor crate or fixture-level calls), recorded in the TODO. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02feat(php-shim): implement HTTP last-response-headers store and ASCII ↵nsfisis
mb_check_encoding http_get_last_response_headers()/http_clear_last_response_headers() now back a thread-local store with a recording hook for the (still unported) HTTP stream layer; with no request recorded they return None, matching PHP. mb_check_encoding gains the trivial ASCII arm; other encodings still need the mbstring tables and stay todo!(). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02fix(json): propagate JsonFile::encode errors instead of unwrappingnsfisis
PHP's JsonFile::encode throws a RuntimeException when json_encode fails; the port swallowed that into an .unwrap() marked TODO(phase-c). Return anyhow::Result from encode/encode_with_options and propagate at every call site (print_table and list_repositories become Result-returning to carry it). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02feat(console): return shared style handles from OutputFormatter::get_stylensfisis
PHP's getStyle returns the shared style instance (reference semantics), which the previous Box-returning signature could not express, leaving a todo!(). Store each style behind Rc<RefCell<...>> and hand out handle clones, per the shared-ownership policy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02chore(php-shim): drop the unused Phar APIs left as todo!()nsfisis
Phar::running has its only Composer call site in SelfUpdateCommand, and self-update will not go through a phar in Shirabe. The .phar writing API (and the SHA512 constant it takes) is only used by Composer's Compiler, which packages composer.phar and has no counterpart in a native binary. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02chore(todo): consolidate TODO comments into the five fixed marker tagsnsfisis
Retag every Shirabe-authored TODO comment to one of the fixed tags: phase-c, phase-d, plugin, php-runtime, phase-e. Upstream-authored TODO comments from Composer/Symfony are left untouched to preserve the ported code shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02refactor(php-shim): introduce PhpClass for reporting PHP class namesnsfisis
Rust has no runtime class name, so `Command::get_class` existed purely to let each command hand back its PHP class name, supplied through the two-argument variant of `delegate_command_trait_impls_to_inner!` at the impl site. Replace it with a general `PhpClass` trait plus an `impl_php_class!` macro, so the name is stated once next to the type definition and the mechanism is reusable outside commands. `Command` gains `PhpClass` as a supertrait and drops `get_class`, and `VcsDriverKind`'s hand-rolled `php_class_name` table moves onto the trait. Behavior is unchanged: the same class-name strings are reported, and the base command state still panics when asked for a name it cannot supply. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02fix(command): wire the missed suggested values and correct update's packages ↵nsfisis
args Review of the suggested-values wiring against the PHP originals found: - audit --format (Auditor::FORMATS) and --abandoned (Auditor::ABANDONEDS) had no suggestions; only ignore-severity had been converted - the same constant-passing shape was missed on --audit-format (Auditor::FORMATS) in update, install, require, create-project and remove - update's packages argument used suggest_installed_package(false, true), but PHP's suggestInstalledPackage(false) expands to (false, false); the stray true came from an incorrect old TODO comment, and made platform packages appear among the candidates With these, every one of the 47 suggestedValues sites in the PHP command definitions has a matching new5/new6 call. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02test(completion): port CommandCompletionTester and CompletionFunctionalTestnsfisis
Adds the Symfony console test helper (Tester/CommandCompletionTester) and ports every CompletionFunctionalTest data-provider entry as an individual test. The tests reproduce the PHP environment by chdir'ing into the vendored composer/ checkout (its composer.json/lock provide the installed packages, scripts and package properties the expectations reference); the Packagist-backed entries query the live repository exactly like the PHP test does. Only `exec ` is ignored: its expectations require the dev checkout's fully installed vendor/bin, which the vendored checkout does not ship. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02feat(command): wire suggested values into every command definitionnsfisis
Ports the per-command completion metadata that PHP passes as the suggestedValues constructor argument, resolving all TODO(cli-completion) markers: - CompletionTrait providers on 18 argument/option sites (installed/root/ available package names, package types, prefer-install) - static value lists (--format on show/outdated/search/fund/licenses/ check-platform-reqs, archive's FORMATS, audit --ignore-severity, update --bump-after-update, repository's action list) - command-specific closures: ConfigCommand::suggest_setting_keys, ShowCommand::suggest_package_based_on_mode, RepositoryCommand's suggest_repo_names/suggest_type_for_add, exec/run-script inline closures (downcast from the this argument, as the closures are bound to their concrete command in PHP) - GlobalCommand::complete, delegating completion to the wrapped subcommand through CompletionInput::from_string - a complete() override on every Composer command forwarding to base_command_complete (BaseCommand inheritance restoration) Also fixes CompleteCommand to call merge_application_definition(true) as PHP's default-argument call does; with false the application-level "command" argument was missing from the bound definition, shifting every argument-position detection by one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02feat(command): port CompletionTrait's suggestion providersnsfisis
The seven suggest* methods resolve package names, types, and install preferences for shell completion. PHP returns $this-bound closures; here each method returns a SuggestedValues whose closure receives the bound command as `this` at call time. The blanket impl over every BaseCommand mirrors PHP's per-command `use CompletionTrait;` (the methods are private there, so the wider visibility is observationally equivalent). Notable PHP shapes kept: the hintsToFind counter machine iterates a by-value copy per package (continue 2 -> labelled continue), and suggestAvailablePackage pins an exact vendor match before truncating to $max entries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02feat(symfony-console): restore CompletionInput's ArgvInput inheritance surfacensfisis
PHP's CompletionInput extends ArgvInput, so it can be passed anywhere an InputInterface is expected (GlobalCommand::complete binds and forwards it, suggestion closures read options and arguments from it). The Rust port only embedded the ArgvInput, so none of that surface was reachable. Implement InputInterface by forwarding to the embedded ArgvInput, with bind dispatching to the specialized CompletionInput::bind (PHP's virtual dispatch), derive Clone, and teach GlobalCommand::input_to_string the CompletionInput branch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02feat(console-input): port the suggested-values backport onto ↵nsfisis
InputArgument/InputOption Composer backports symfony/console 6.1's $suggestedValues parameter in Composer\Console\Input\{InputArgument,InputOption}; the Rust newtypes had dropped it. PHP closures are bound to the command ($this), but a command cannot capture a handle to itself while configure() runs inside new(), so the closure receives the bound command as an explicit `this` argument at call time instead. - add SuggestedValues (list | this-taking closure) and wire it through InputArgument::new5 / InputOption::new6 and their complete() methods - track Composer-typed definition entries by name in BaseCommandData side maps, standing in for PHP's instanceof checks (set_definition converts entries to the Symfony types for storage) - add base_command_complete, the BaseCommand::complete dispatch shared by every Composer command - introduce BaseCommand::base_command_data and make command_data a default method on top of it Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02feat(symfony-console): implement the shell completion command plumbingnsfisis
The _complete and completion commands were registered but always panicked: get_class_of_command / instantiate_completion_output / tail_debug_log were todo!() and the completion.bash resource was not shipped. - make Command::complete return anyhow::Result so completion errors propagate to CompleteCommand's catch-all (exit code 2) like PHP - add Command::get_class as the port hook for PHP's get_class() debug log; every command supplies its PHP FQCN via the delegation macro - embed Resources/completion.bash at compile time (single-binary port); get_supported_shells becomes a static list - implement tail_debug_log by moving the shared output handle into the 'static process callback - add OutputInterface::as_console_output so unsupported-shell errors go to stderr as in PHP - fix CompletionInput::bind to keep the argument name PHP assigns in the foreach head even when the loop breaks on the first unset argument; application-level completion always hit this and returned no suggestions Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02feat(php-shim): implement syscall-backed functions via the nix cratensfisis
Several shim functions were left as todo!() because the standard library exposes no equivalent and no syscall crate was available. Adding nix unblocks them: - proc_open now wires descriptors beyond stderr, creating the pipe itself and installing the child end with dup2(2) from pre_exec - proc_terminate and posix_kill deliver arbitrary signals via kill(2) - get_current_user reports the owner of the running executable - php_uname answers every mode from uname(2) instead of only "s" and "r" It also closes gaps that were previously approximated: - fstat stats the stdio streams and pipes rather than reporting failure - touch stamps mtime/atime on an existing path, including directories - is_writable/is_executable use access(2) instead of permission bits - umask falls back to the read-modify-write umask(2) off Linux The hand-written repr(C) structs and extern "C" declarations for getpwuid, utime, statvfs, fcntl and select are replaced by their nix wrappers, which in turn lets disk_free_space drop its Linux-only cfg. cli_set_process_title, setproctitle and the pcntl_signal pair stay as todo!(): the first two need access to the process's own argv block, and the latter two depend on the signal-handling subsystem rather than on sigaction(2) itself. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02fix(io): propagate ask/select errors instead of panickingnsfisis
IOInterface::ask/select now return anyhow::Result<PhpMixed>, and ConsoleIO::ask_question forwards QuestionHelper errors (validator failures, MissingInputException) instead of collapsing them with .expect(). In PHP these exceptions propagate from QuestionHelper through ConsoleIO to the caller, so callers such as UpdateCommand's interactive package selection must be able to observe them; the MissingInputException is wrapped with its concrete type preserved so Application's ExceptionInterface downcast keeps working. All call sites now propagate with `?` (Perforce::query_p4_user becomes Result-returning: PHP declares it void but exceptions still escape), and the previously ignored test_interactive_mode_throws_if_no_package_entered passes. ask_confirmation/ask_and_hide_answer still collapse errors; extending propagation to them is left as TODO(phase-c) pending a decision. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02fix(symfony-console): dispatch parse_token to CompletionInput in bindnsfisis
PHP's Input::bind() -> ArgvInput::parse() calls $this->parseToken(), which late-binds to CompletionInput::parseToken; that override swallows per-token RuntimeExceptions so an incomplete command line still parses. Delegating CompletionInput::bind to ArgvInput::bind pinned the call to ArgvInput's parseToken, aborting the whole parse on the first invalid token and leaving CompletionInput::parse_token dead. parseToken is protected and not on any trait, so ArgvInput::base_bind threads the concrete implementation in as a callback instead of a trait object. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02fix(repository): run lazy initialization in count/has_packagensfisis
PHP's ArrayRepository::count()/hasPackage() call $this->initialize(), which late-binds to the concrete repository class and lazily loads its packages. The Rust pass-throughs skipped that: they ran ArrayRepository's stub initialize instead, returning 0/false and marking the repository initialized with an empty package list, which made ensure_initialized() skip the real initialization forever after. Take &mut self in RepositoryInterface::count/has_package so the lazy repositories (Filesystem, Platform, Composer) can guard with their real initialize, and return Result from has_package since that initialization can fail (PHP propagates the exception). InstallerInterface::is_installed and InstallationManager::is_package_installed/mark_alias_installed propagate the same way, which also resolves the TODO(phase-d) markers on Package/Path/Artifact/Vcs repositories about initialization errors being swallowed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02fix(downloader): restore late binding in getLocalChanges/update pathsnsfisis
PHP's FileDownloader::getLocalChanges and ::update call $this->download() / $this->install() / $this->remove() / $this->getInstallOperationAppendix(), which late-bind to the concrete downloader class. The Rust port embeds the parent as `inner`, so delegating these methods to FileDownloader pinned the calls to FileDownloader's own implementations: `status` built the compare tree without extracting the archive (flagging every file of dist-installed packages as changed), and `update` re-installed the raw dist file instead of extracting it. Thread the concrete downloader in as `this: &dyn DownloaderInterface` via shared helpers (base_get_local_changes / base_update) and pass `self` from each delegating downloader. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02refactor(php-shim): remove unused curl functions/constantsnsfisis