From 84c731074103d80062e0c3640d6037f2dfca4e66 Mon Sep 17 00:00:00 2001 From: mychidarko <26604242+mychidarko@users.noreply.github.com> Date: Mon, 9 Nov 2026 15:31:53 +0000 Subject: [PATCH] feat: point apps behind on Leaf majors to the upgrade guide after install/update --- src/InstallCommand.php | 21 ++++++- src/UpdateCommand.php | 5 ++ src/Utils/Upgrade.php | 131 +++++++++++++++++++++++++++++++++++++++++ tests/upgrade.test.php | 81 +++++++++++++++++++++++++ 4 files changed, 236 insertions(+), 2 deletions(-) create mode 100644 src/Utils/Upgrade.php create mode 100644 tests/upgrade.test.php diff --git a/src/InstallCommand.php b/src/InstallCommand.php index 2623c95..49151a3 100644 --- a/src/InstallCommand.php +++ b/src/InstallCommand.php @@ -4,6 +4,7 @@ namespace Leaf\Console; +use Leaf\Console\Utils\Upgrade; use Leaf\Sprout\Command; class InstallCommand extends Command @@ -33,9 +34,25 @@ protected function handle(): int return 1; } - return 0; + return $this->finish(); } - return sprout()->composer()->install()->isSuccessful() ? 0 : 1; + if (!sprout()->composer()->install()->isSuccessful()) { + return 1; + } + + return $this->finish(); + } + + /** + * Print one plain line if the app is behind on Leaf majors. + */ + protected function finish(): int + { + if ($nudge = Upgrade::nudge()) { + $this->writeln($nudge); + } + + return 0; } } diff --git a/src/UpdateCommand.php b/src/UpdateCommand.php index 705d922..841febd 100644 --- a/src/UpdateCommand.php +++ b/src/UpdateCommand.php @@ -4,6 +4,7 @@ namespace Leaf\Console; +use Leaf\Console\Utils\Upgrade; use Leaf\Sprout\Command; class UpdateCommand extends Command @@ -20,6 +21,10 @@ protected function handle(): int if (sprout()->composer(true)->install('leafs/cli --ansi')->isSuccessful()) { $this->writeln('Leaf CLI installed successfully!'); + if ($nudge = Upgrade::nudge()) { + $this->writeln($nudge); + } + return 0; } } diff --git a/src/Utils/Upgrade.php b/src/Utils/Upgrade.php new file mode 100644 index 0000000..63b71c4 --- /dev/null +++ b/src/Utils/Upgrade.php @@ -0,0 +1,131 @@ + "$name {$versions[0]} -> {$versions[1]}", + array_keys($outdated), + $outdated + )); + + return "Newer Leaf majors available: $list. See " . static::GUIDE; + } catch (\Throwable $e) { + return null; + } + } + + /** + * Read a package's installed version from composer.lock. Returns null + * for missing packages and non-numeric (dev branch) versions. + */ + public static function lockedVersion(string $directory, string $package): ?string + { + if (!is_file("$directory/composer.lock")) { + return null; + } + + $lock = json_decode((string) file_get_contents("$directory/composer.lock"), true); + + if (!is_array($lock)) { + return null; + } + + foreach (array_merge($lock['packages'] ?? [], $lock['packages-dev'] ?? []) as $installed) { + if (($installed['name'] ?? null) === $package) { + return static::normalize((string) ($installed['version'] ?? '')); + } + } + + return null; + } + + /** + * Direct leafs/* dependencies with a newer major, via + * `composer outdated --direct --major-only`. Empty on any failure. + * + * @return array + */ + public static function outdatedLeafModules(string $directory): array + { + $process = Process::fromShellCommandline( + Core::findComposer() . ' outdated --direct --major-only --format=json --no-interaction --no-ansi', + $directory, + ['COMPOSER_NO_INTERACTION' => '1'] + ); + $process->setTimeout(30); + $process->run(); + + if (!$process->isSuccessful()) { + return []; + } + + $report = json_decode($process->getOutput(), true); + $found = []; + + foreach ($report['installed'] ?? [] as $package) { + $name = $package['name'] ?? ''; + + if (strpos($name, 'leafs/') !== 0) { + continue; + } + + $current = static::normalize((string) ($package['version'] ?? '')); + $latest = static::normalize((string) ($package['latest'] ?? '')); + + if ($current && $latest && (int) $latest > (int) $current) { + $found[$name] = [$current, $latest]; + } + } + + return $found; + } + + /** + * "v4.4.1" -> "4.4.1"; dev branches -> null. + */ + protected static function normalize(string $version): ?string + { + $version = ltrim(trim($version), 'vV'); + + return preg_match('/^\d+(\.\d+)*$/', $version) ? $version : null; + } +} diff --git a/tests/upgrade.test.php b/tests/upgrade.test.php new file mode 100644 index 0000000..9a024e7 --- /dev/null +++ b/tests/upgrade.test.php @@ -0,0 +1,81 @@ + version pairs. + */ +function lockWith(array $packages): void +{ + file_put_contents('composer.lock', json_encode([ + 'packages' => array_map( + fn ($name, $version) => ['name' => $name, 'version' => $version], + array_keys($packages), + $packages + ), + 'packages-dev' => [], + ])); +} + +/** + * Core::findComposer() prefers ./composer.phar, so a PHP script there + * stands in for composer on every platform. + */ +function fakeComposer(string $outdatedJson, int $exitCode = 0): void +{ + file_put_contents('composer.phar', ' 'v4.4.1', 'leafs/http' => 'v4.2']); + + expect(Upgrade::nudge()) + ->toBe('leafs/leaf 4.4.1 installed, Leaf 5 is available. See https://leafphp.dev/docs/upgrade-guide'); +}); + +test('lists leafs modules with a newer major', function () { + lockWith(['leafs/leaf' => 'v5.1.0']); + fakeComposer(json_encode(['installed' => [ + ['name' => 'leafs/form', 'version' => 'v3.2', 'latest' => 'v5.0.1'], + ['name' => 'symfony/yaml', 'version' => 'v6.4.0', 'latest' => 'v7.1.0'], + ]])); + + expect(Upgrade::nudge()) + ->toBe('Newer Leaf majors available: leafs/form 3.2 -> 5.0.1. See https://leafphp.dev/docs/upgrade-guide'); +}); + +test('stays silent when everything is current', function () { + lockWith(['leafs/leaf' => 'v5.1.0']); + fakeComposer(json_encode(['installed' => []])); + + expect(Upgrade::nudge())->toBeNull(); +}); + +test('stays silent when composer outdated fails (offline)', function () { + lockWith(['leafs/leaf' => 'v5.1.0']); + fakeComposer('The "https://repo.packagist.org/packages.json" file could not be downloaded', 1); + + expect(Upgrade::nudge())->toBeNull(); +}); + +test('stays silent outside a composer project', function () { + unlink('composer.json'); + + expect(Upgrade::nudge())->toBeNull(); +}); + +test('ignores dev-branch versions of leafs/leaf', function () { + lockWith(['leafs/leaf' => 'dev-main']); + fakeComposer(json_encode(['installed' => []])); + + expect(Upgrade::nudge())->toBeNull(); +});