From 81479f9e3f4c8e935ea8a5843cac5ebca6c1bc4d Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Sun, 3 May 2026 21:22:07 +0600 Subject: [PATCH] plugin fix --- README.md | 2 +- src/Composer/Plugin.php | 51 ++++++++++++++++---- tests/Composer/PluginTest.php | 89 +++++++++++++++++++++++++++++++++++ 3 files changed, 131 insertions(+), 11 deletions(-) create mode 100644 tests/Composer/PluginTest.php diff --git a/README.md b/README.md index ad482b8..7a6946c 100644 --- a/README.md +++ b/README.md @@ -490,7 +490,7 @@ This package also has a root `post-autoload-dump` script: "post-autoload-dump": "@php bin/install-captainhook.php" ``` -That helper keeps hooks installed for this repository. Consuming projects get automatic hook installation from the PHPForge Composer plugin with project `captainhook.json` when present, otherwise with the bundled PHPForge `captainhook.json`. +That helper keeps hooks installed for this repository. Consuming projects get automatic hook installation from the PHPForge Composer plugin: it uses project `captainhook.json` when present, otherwise it copies the bundled `captainhook.json` into project root and installs hooks from there. ## GitHub Actions diff --git a/src/Composer/Plugin.php b/src/Composer/Plugin.php index 1b983d4..017a15d 100644 --- a/src/Composer/Plugin.php +++ b/src/Composer/Plugin.php @@ -52,30 +52,61 @@ public function getCapabilities(): array public function installHooks(Event $event): void { - $configPath = Paths::config('captainhook.json'); + try { + $configPath = $this->ensureProjectCaptainHookConfig(); - if (!is_file($configPath)) { - return; - } + if (!is_string($configPath)) { + return; + } + + $process = new Process(CaptainHook::installCommand($configPath), getcwd() ?: null); + $process->setTimeout(null); + $process->run(); - $process = new Process(CaptainHook::installCommand($configPath), getcwd() ?: null); - $process->setTimeout(null); - $process->run(); + if ($process->isSuccessful()) { + return; + } - if (!$process->isSuccessful()) { $message = (trim($process->getErrorOutput()) ?: trim($process->getOutput())) ?: 'CaptainHook install failed.'; + throw new \RuntimeException($message); + } catch (\RuntimeException $exception) { if (getenv('IC_HOOKS_STRICT') !== '0') { - throw new \RuntimeException($message); + throw $exception; } $event->getIO()->writeError('PHPForge could not install CaptainHook hooks; continuing because IC_HOOKS_STRICT=0.'); - $event->getIO()->writeError($message); + $event->getIO()->writeError($exception->getMessage()); } } public function uninstall(Composer $composer, IOInterface $io): void {} + private function ensureProjectCaptainHookConfig(): ?string + { + $projectConfig = Paths::projectRootPath() . DIRECTORY_SEPARATOR . 'captainhook.json'; + + if (is_file($projectConfig)) { + return $projectConfig; + } + + $bundledConfig = Paths::bundledConfigFileOrNull('captainhook.json'); + + if (!is_string($bundledConfig) || !is_file($bundledConfig)) { + return null; + } + + if (!copy($bundledConfig, $projectConfig)) { + throw new \RuntimeException(sprintf( + 'Failed to copy bundled CaptainHook config from "%s" to "%s".', + $bundledConfig, + $projectConfig, + )); + } + + return $projectConfig; + } + private function reportMissingAllowPlugins(): void { $composerJson = (getcwd() ?: '') . DIRECTORY_SEPARATOR . 'composer.json'; diff --git a/tests/Composer/PluginTest.php b/tests/Composer/PluginTest.php new file mode 100644 index 0000000..052e4a6 --- /dev/null +++ b/tests/Composer/PluginTest.php @@ -0,0 +1,89 @@ +isDir()) { + rmdir($item->getPathname()); + + continue; + } + + unlink($item->getPathname()); + } + + rmdir($path); +} + +it('copies bundled captainhook config into project root when missing', function (): void { + $originalCwd = getcwd(); + $projectRoot = sys_get_temp_dir().DIRECTORY_SEPARATOR.'phpforge-plugin-'.uniqid('', true); + $vendorResources = $projectRoot.DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR.'infocyph'.DIRECTORY_SEPARATOR.'phpforge'.DIRECTORY_SEPARATOR.'resources'; + $vendorBin = $projectRoot.DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR.'bin'; + $bundledConfig = dirname(__DIR__, 2).DIRECTORY_SEPARATOR.'resources'.DIRECTORY_SEPARATOR.'captainhook.json'; + + mkdir($vendorResources, 0755, true); + mkdir($vendorBin, 0755, true); + file_put_contents($projectRoot.DIRECTORY_SEPARATOR.'composer.json', '{"name":"example/project"}'); + copy($bundledConfig, $vendorResources.DIRECTORY_SEPARATOR.'captainhook.json'); + file_put_contents($vendorBin.DIRECTORY_SEPARATOR.'captainhook', " $plugin->installHooks($event))->not->toThrow(RuntimeException::class); + expect(is_file($projectConfig))->toBeTrue(); + expect(file_get_contents($projectConfig))->toBe(file_get_contents($bundledConfig)); + } finally { + if (is_string($originalCwd)) { + chdir($originalCwd); + } + + removePluginTestTree($projectRoot); + } +}); + +it('keeps strict hook installation when project captainhook config exists', function (): void { + $originalCwd = getcwd(); + $projectRoot = sys_get_temp_dir().DIRECTORY_SEPARATOR.'phpforge-plugin-'.uniqid('', true); + + mkdir($projectRoot, 0755, true); + file_put_contents($projectRoot.DIRECTORY_SEPARATOR.'composer.json', '{"name":"example/project"}'); + file_put_contents($projectRoot.DIRECTORY_SEPARATOR.'captainhook.json', '{}'); + + chdir($projectRoot); + + try { + $event = new Event(ScriptEvents::POST_AUTOLOAD_DUMP, new Composer(), new NullIO()); + $plugin = new Plugin(); + + expect(fn () => $plugin->installHooks($event))->toThrow(RuntimeException::class); + } finally { + if (is_string($originalCwd)) { + chdir($originalCwd); + } + + removePluginTestTree($projectRoot); + } +});