From b85be6ddf8a30d01688ed8a98f525709289be87f Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Mon, 13 Jul 2026 21:16:48 +0100 Subject: [PATCH 1/2] bump: upgrade dependencies --- composer.lock | 430 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 375 insertions(+), 55 deletions(-) diff --git a/composer.lock b/composer.lock index 9f472e2..ffe7cf6 100644 --- a/composer.lock +++ b/composer.lock @@ -342,6 +342,55 @@ }, "time": "2025-09-14T11:18:39+00:00" }, + { + "name": "phpgt/async", + "version": "v1.0.3", + "source": { + "type": "git", + "url": "https://github.com/phpgt/Async.git", + "reference": "d1f3679f4200e32457b57211f49b999641ef8fa4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpgt/Async/zipball/d1f3679f4200e32457b57211f49b999641ef8fa4", + "reference": "d1f3679f4200e32457b57211f49b999641ef8fa4", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "php": ">=8.1", + "phpgt/promise": "^2.0" + }, + "require-dev": { + "phpmd/phpmd": "^2.13", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.1", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "GT\\Async\\": "./src", + "Gt\\Async\\": "./src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Promise-based non-blocking operations.", + "support": { + "issues": "https://github.com/phpgt/Async/issues", + "source": "https://github.com/phpgt/Async/tree/v1.0.3" + }, + "funding": [ + { + "url": "https://github.com/sponsors/PhpGt", + "type": "github" + } + ], + "time": "2026-05-04T16:34:25+00:00" + }, { "name": "phpgt/build", "version": "v1.4.0", @@ -517,16 +566,16 @@ }, { "name": "phpgt/cron", - "version": "v1.1.0", + "version": "v1.2.0", "source": { "type": "git", "url": "https://github.com/phpgt/Cron.git", - "reference": "8a9ea03e5f3656f00375d772ed1e218d161c944b" + "reference": "15ec73221d852685b51320c69acc64e00bba9dda" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpgt/Cron/zipball/8a9ea03e5f3656f00375d772ed1e218d161c944b", - "reference": "8a9ea03e5f3656f00375d772ed1e218d161c944b", + "url": "https://api.github.com/repos/phpgt/Cron/zipball/15ec73221d852685b51320c69acc64e00bba9dda", + "reference": "15ec73221d852685b51320c69acc64e00bba9dda", "shasum": "" }, "require": { @@ -540,7 +589,7 @@ "phpmd/phpmd": "^2.13", "phpstan/phpstan": "^1.10", "phpunit/phpunit": "^10.1", - "squizlabs/php_codesniffer": "^3.7" + "squizlabs/php_codesniffer": "^4.0" }, "bin": [ "bin/cron" @@ -548,6 +597,7 @@ "type": "library", "autoload": { "psr-4": { + "GT\\Cron\\": "./src/", "Gt\\Cron\\": "./src/" } }, @@ -555,7 +605,69 @@ "description": "Run scripts or static functions at regular intervals.", "support": { "issues": "https://github.com/phpgt/Cron/issues", - "source": "https://github.com/phpgt/Cron/tree/v1.1.0" + "source": "https://github.com/phpgt/Cron/tree/v1.2.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/phpgt", + "type": "github" + } + ], + "time": "2026-06-25T21:46:17+00:00" + }, + { + "name": "phpgt/curl", + "version": "v3.2.2", + "source": { + "type": "git", + "url": "https://github.com/phpgt/Curl.git", + "reference": "86338d0be868c775b62250357e8fba47b0dcf5fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpgt/Curl/zipball/86338d0be868c775b62250357e8fba47b0dcf5fd", + "reference": "86338d0be868c775b62250357e8fba47b0dcf5fd", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "php": ">=8.4", + "phpgt/json": "^2.2" + }, + "require-dev": { + "phpmd/phpmd": "^2.15", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^12.5", + "squizlabs/php_codesniffer": "^4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "GT\\Curl\\": "./src", + "Gt\\Curl\\": "./src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Greg Bowler", + "email": "greg.bowler@g105b.com" + } + ], + "description": "cURL object wrapper.", + "keywords": [ + "curl", + "curl_multi", + "http", + "interface" + ], + "support": { + "issues": "https://github.com/phpgt/Curl/issues", + "source": "https://github.com/phpgt/Curl/tree/v3.2.2" }, "funding": [ { @@ -563,20 +675,20 @@ "type": "github" } ], - "time": "2026-04-07T22:05:39+00:00" + "time": "2026-04-19T15:25:00+00:00" }, { "name": "phpgt/daemon", - "version": "v1.1.6", + "version": "v1.1.7", "source": { "type": "git", "url": "https://github.com/phpgt/Daemon.git", - "reference": "cc932f51523988df8d27c05ee1122b2922806a13" + "reference": "b537b1c713e481ec6a97d70143e559829f3c7f4c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpgt/Daemon/zipball/cc932f51523988df8d27c05ee1122b2922806a13", - "reference": "cc932f51523988df8d27c05ee1122b2922806a13", + "url": "https://api.github.com/repos/phpgt/Daemon/zipball/b537b1c713e481ec6a97d70143e559829f3c7f4c", + "reference": "b537b1c713e481ec6a97d70143e559829f3c7f4c", "shasum": "" }, "require": { @@ -600,7 +712,7 @@ "description": "Background script execution with cross-platform compatible streaming.", "support": { "issues": "https://github.com/phpgt/Daemon/issues", - "source": "https://github.com/phpgt/Daemon/tree/v1.1.6" + "source": "https://github.com/phpgt/Daemon/tree/v1.1.7" }, "funding": [ { @@ -608,7 +720,7 @@ "type": "github" } ], - "time": "2026-06-04T07:48:06+00:00" + "time": "2026-07-10T09:53:00+00:00" }, { "name": "phpgt/database", @@ -737,35 +849,43 @@ }, { "name": "phpgt/http", - "version": "v1.1.10", + "version": "v1.3.8", "source": { "type": "git", - "url": "https://github.com/PhpGt/Http.git", - "reference": "8b2b6b99d218147c203c466a9bbf12cdb8971942" + "url": "https://github.com/phpgt/Http.git", + "reference": "5e12783b8a339340fd886217a805ae70bbf82b23" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PhpGt/Http/zipball/8b2b6b99d218147c203c466a9bbf12cdb8971942", - "reference": "8b2b6b99d218147c203c466a9bbf12cdb8971942", + "url": "https://api.github.com/repos/phpgt/Http/zipball/5e12783b8a339340fd886217a805ae70bbf82b23", + "reference": "5e12783b8a339340fd886217a805ae70bbf82b23", "shasum": "" }, "require": { - "php": ">=8.1", - "phpgt/input": "^1.2", + "ext-curl": "*", + "ext-dom": "*", + "ext-fileinfo": "*", + "php": ">=8.4", + "phpgt/async": "^1.0", + "phpgt/curl": "^3.2", + "phpgt/input": "^1.3", + "phpgt/json": "^2.2", + "phpgt/promise": "^2.4", + "phpgt/propfunc": "^1.0", + "phpgt/typesafegetter": "^1.3", "psr/http-message": "^2.0", "willdurand/negotiation": "3.1.0" }, "require-dev": { "phpmd/phpmd": "^2.13", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^10.1", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^11.5", "squizlabs/php_codesniffer": "^3.7" }, "type": "library", "extra": { "branch-alias": { - "dev-ci": "1.2.3-dev", - "dev-master": "1.2.3-dev" + "dev-master": "1.3-dev" } }, "autoload": { @@ -779,8 +899,8 @@ ], "description": "PSR-7 HTTP message implementation.", "support": { - "issues": "https://github.com/PhpGt/Http/issues", - "source": "https://github.com/PhpGt/Http/tree/v1.1.10" + "issues": "https://github.com/phpgt/Http/issues", + "source": "https://github.com/phpgt/Http/tree/v1.3.8" }, "funding": [ { @@ -788,7 +908,7 @@ "type": "github" } ], - "time": "2023-05-03T10:42:38+00:00" + "time": "2026-03-19T18:22:33+00:00" }, { "name": "phpgt/input", @@ -846,16 +966,16 @@ }, { "name": "phpgt/json", - "version": "v2.2.1", + "version": "v2.3.0", "source": { "type": "git", "url": "https://github.com/phpgt/Json.git", - "reference": "df697c3fc7b3527ae3a01d3f96b1bfbab80feee2" + "reference": "d35835d4fb273a6e83106ceed79f45534c07d674" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpgt/Json/zipball/df697c3fc7b3527ae3a01d3f96b1bfbab80feee2", - "reference": "df697c3fc7b3527ae3a01d3f96b1bfbab80feee2", + "url": "https://api.github.com/repos/phpgt/Json/zipball/d35835d4fb273a6e83106ceed79f45534c07d674", + "reference": "d35835d4fb273a6e83106ceed79f45534c07d674", "shasum": "" }, "require": { @@ -890,7 +1010,7 @@ "description": " Structured, type-safe, immutable JSON objects.", "support": { "issues": "https://github.com/phpgt/Json/issues", - "source": "https://github.com/phpgt/Json/tree/v2.2.1" + "source": "https://github.com/phpgt/Json/tree/v2.3.0" }, "funding": [ { @@ -898,7 +1018,117 @@ "type": "github" } ], - "time": "2026-04-21T13:31:54+00:00" + "time": "2026-07-09T17:54:44+00:00" + }, + { + "name": "phpgt/promise", + "version": "v2.4.1", + "source": { + "type": "git", + "url": "https://github.com/phpgt/Promise.git", + "reference": "bf781780866fe5c52d20c071740e07c59df4c1c0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpgt/Promise/zipball/bf781780866fe5c52d20c071740e07c59df4c1c0", + "reference": "bf781780866fe5c52d20c071740e07c59df4c1c0", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpmd/phpmd": "^2.13", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^12.1", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "GT\\Promise\\": "./src", + "Gt\\Promise\\": "./src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Pleasantly work with asynchronous code.", + "keywords": [ + "W3C", + "async", + "asynchronous", + "callback", + "concurrency", + "concurrent", + "deferred", + "promise", + "then" + ], + "support": { + "issues": "https://github.com/phpgt/Promise/issues", + "source": "https://github.com/phpgt/Promise/tree/v2.4.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/phpgt", + "type": "github" + } + ], + "time": "2026-05-04T15:49:22+00:00" + }, + { + "name": "phpgt/propfunc", + "version": "v1.0.1", + "source": { + "type": "git", + "url": "https://github.com/PhpGt/PropFunc.git", + "reference": "091213649e89ff22d1ef640b46fbee5215c65520" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PhpGt/PropFunc/zipball/091213649e89ff22d1ef640b46fbee5215c65520", + "reference": "091213649e89ff22d1ef640b46fbee5215c65520", + "shasum": "" + }, + "require": { + "php": ">=8.0" + }, + "require-dev": { + "phpstan/phpstan": ">=0.12", + "phpunit/phpunit": ">=9.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Gt\\PropFunc\\": "./src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Greg Bowler", + "email": "greg.bowler@g105b.com", + "homepage": "https://www.g105b.com", + "role": "Developer" + } + ], + "description": "Property accessor and mutator functions.", + "support": { + "issues": "https://github.com/PhpGt/PropFunc/issues", + "source": "https://github.com/PhpGt/PropFunc/tree/v1.0.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/PhpGt", + "type": "github" + } + ], + "time": "2021-03-23T12:46:44+00:00" }, { "name": "phpgt/server", @@ -998,20 +1228,20 @@ }, { "name": "phpgt/sqlbuilder", - "version": "v1.0.1", + "version": "v1.1.0", "source": { "type": "git", - "url": "https://github.com/PhpGt/SqlBuilder.git", - "reference": "ac60cad74bb1ac2d8d667e142a56fd6c9d23291a" + "url": "https://github.com/phpgt/SqlBuilder.git", + "reference": "557289307cc188f708586d1d4e59b0bf77a3d608" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PhpGt/SqlBuilder/zipball/ac60cad74bb1ac2d8d667e142a56fd6c9d23291a", - "reference": "ac60cad74bb1ac2d8d667e142a56fd6c9d23291a", + "url": "https://api.github.com/repos/phpgt/SqlBuilder/zipball/557289307cc188f708586d1d4e59b0bf77a3d608", + "reference": "557289307cc188f708586d1d4e59b0bf77a3d608", "shasum": "" }, "require": { - "php": ">=8.0" + "php": ">=8.4" }, "require-dev": { "phpmd/phpmd": "^2.13", @@ -1022,22 +1252,23 @@ "type": "library", "autoload": { "psr-4": { + "GT\\SqlBuilder\\": "./src", "Gt\\SqlBuilder\\": "./src" } }, "notification-url": "https://packagist.org/downloads/", "description": "Object oriented representation of SQL queries.", "support": { - "issues": "https://github.com/PhpGt/SqlBuilder/issues", - "source": "https://github.com/PhpGt/SqlBuilder/tree/v1.0.1" + "issues": "https://github.com/phpgt/SqlBuilder/issues", + "source": "https://github.com/phpgt/SqlBuilder/tree/v1.1.0" }, "funding": [ { - "url": "https://github.com/sponsors/PhpGt", + "url": "https://github.com/sponsors/phpgt", "type": "github" } ], - "time": "2023-09-17T12:33:25+00:00" + "time": "2026-05-06T17:09:03+00:00" }, { "name": "phpgt/sync", @@ -2333,6 +2564,93 @@ ], "time": "2026-04-10T16:19:22+00:00" }, + { + "name": "symfony/polyfill-deepclone", + "version": "v1.40.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-deepclone.git", + "reference": "dca4ccba5f360070b574414dce4c1e7a559844fa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-deepclone/zipball/dca4ccba5f360070b574414dce4c1e7a559844fa", + "reference": "dca4ccba5f360070b574414dce4c1e7a559844fa", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "provide": { + "ext-deepclone": "*" + }, + "suggest": { + "ext-deepclone": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\DeepClone\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the deepclone extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "deepclone", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-deepclone/tree/v1.40.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-12T07:27:17+00:00" + }, { "name": "symfony/polyfill-mbstring", "version": "v1.38.2", @@ -2507,26 +2825,27 @@ }, { "name": "symfony/var-exporter", - "version": "v7.4.14", + "version": "v8.1.1", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "0118811b1d59f323bf131250b3fb919febfece28" + "reference": "75b74315b4e4be40e5534cf9c5cc30dd0907ed71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/0118811b1d59f323bf131250b3fb919febfece28", - "reference": "0118811b1d59f323bf131250b3fb919febfece28", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/75b74315b4e4be40e5534cf9c5cc30dd0907ed71", + "reference": "75b74315b4e4be40e5534cf9c5cc30dd0907ed71", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3" + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-deepclone": "^1.40" }, "require-dev": { - "symfony/property-access": "^6.4|^7.0|^8.0", - "symfony/serializer": "^6.4|^7.0|^8.0", - "symfony/var-dumper": "^6.4|^7.0|^8.0" + "symfony/property-access": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -2551,11 +2870,12 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Allows exporting any serializable PHP data structure to plain PHP code", + "description": "Provides tools to export, instantiate, hydrate, clone and lazy-load PHP objects", "homepage": "https://symfony.com", "keywords": [ "clone", "construct", + "deep-clone", "export", "hydrate", "instantiate", @@ -2564,7 +2884,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v7.4.14" + "source": "https://github.com/symfony/var-exporter/tree/v8.1.1" }, "funding": [ { @@ -2584,7 +2904,7 @@ "type": "tidelift" } ], - "time": "2026-06-27T08:41:53+00:00" + "time": "2026-06-27T09:05:56+00:00" } ], "aliases": [], From 3e9ea2fa87805110d7bf9356d85d351392cad6a8 Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Tue, 14 Jul 2026 10:10:19 +0100 Subject: [PATCH 2/2] feature: migrate db and install client side deps on create closes #88 closes #91 closes #83 --- src/Command/CreateCommand.php | 202 +++++++++++++++++++++++++++++++--- 1 file changed, 185 insertions(+), 17 deletions(-) diff --git a/src/Command/CreateCommand.php b/src/Command/CreateCommand.php index 2505aed..0de0e4a 100644 --- a/src/Command/CreateCommand.php +++ b/src/Command/CreateCommand.php @@ -7,9 +7,15 @@ use Gt\Cli\Parameter\NamedParameter; use Gt\Cli\Parameter\Parameter; use Gt\Cli\Stream; +use GT\Daemon\CommandNotFoundException; +use GT\Daemon\Process; use GT\GtCommand\Blueprint\BlueprintCollection; +/** @SuppressWarnings(PHPMD.ExcessiveClassComplexity) */ class CreateCommand extends Command { + const MIGRATION_DIRECTORY_NOT_FOUND_EXCEPTION + = "GT\Database\Migration\MigrationDirectoryNotFoundException"; + /** * TODO: Simplify this function * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -33,30 +39,19 @@ public function run(?ArgumentValueList $arguments = null):int { sleep(1); $process = $selectedBlueprint->createProject($name); - $process->exec(); - - do { - $this->write($process->getOutput()); - $this->write($process->getErrorOutput(), Stream::ERROR); - usleep(100_000); - } - while($process->isRunning()); + $this->execAndStreamProcess($process); $process = $selectedBlueprint->updateDependencies($name); - $process->exec(); + $code = $this->execAndStreamProcess($process); - do { - $this->write($process->getOutput()); - $this->write($process->getErrorOutput(), Stream::ERROR); - usleep(100_000); - } - while($process->isRunning()); - - if($code = $process->getExitCode()) { + if($code) { $this->writeLine("There was an error installing the blueprint (exit code $code)"); exit($code); // phpcs:ignore } + $this->runMigration($name); + $this->runNpmInstall($name); + $this->writeLine(); $this->writeLine("Your new application is created!"); $this->writeLine("Would you like to run it now? (Y/N)"); @@ -242,5 +237,178 @@ private function readNamespaceForArguments(ArgumentValueList $arguments):string return $this->readValidNamespace($arguments->get("namespace", "")); } + private function runMigration(string $dir):void { + $this->writeLine(); + $this->writeLine("Running database migrations..."); + + $process = new Process("gt", "migrate"); + $process->setExecCwd($dir); + $code = $this->execAndStreamProcess( + $process, + self::MIGRATION_DIRECTORY_NOT_FOUND_EXCEPTION + ); + + if($code) { + $this->writeLine("There was an error running migrations (exit code $code)"); + exit($code); // phpcs:ignore + } + } + + private function runNpmInstall(string $dir):void { + if(!is_file("$dir/package.json")) { + return; + } + + if(!$this->isCommandAvailable("npm")) { + return; + } + + $this->writeLine(); + $this->writeLine("Installing npm dependencies..."); + + $process = new Process("npm", "install"); + $process->setExecCwd($dir); + $code = $this->execAndStreamProcess($process); + + if($code) { + $this->writeLine("There was an error installing npm dependencies (exit code $code)"); + exit($code); // phpcs:ignore + } + } + + private function isCommandAvailable(string $command):bool { + try { + $process = new Process($command, "--version"); + $process->exec(); + } + catch(CommandNotFoundException) { + return false; + } + + do { + $process->getOutput(); + $process->getErrorOutput(); + usleep(100_000); + } + while($process->isRunning()); + + return $process->getExitCode() === 0; + } + + private function execAndStreamProcess( + Process $process, + ?string $suppressErrorWhenFirstLineContains = null + ):int { + $errorBuffer = ""; + $suppressError = null; + $process->exec(); + + do { + $this->write($process->getOutput()); + $this->writeErrorOutput( + $process->getErrorOutput(), + $suppressErrorWhenFirstLineContains, + $errorBuffer, + $suppressError + ); + usleep(100_000); + } + while($process->isRunning()); + + $this->write($process->getOutput()); + $this->writeErrorOutput( + $process->getErrorOutput(), + $suppressErrorWhenFirstLineContains, + $errorBuffer, + $suppressError + ); + + $this->flushErrorBuffer( + $suppressErrorWhenFirstLineContains, + $errorBuffer, + $suppressError + ); + + if($suppressError) { + return 0; + } + + return $process->getExitCode() ?? 127; + } + + private function writeErrorOutput( + string $errorOutput, + ?string $suppressErrorWhenFirstLineContains, + string &$errorBuffer, + ?bool &$suppressError + ):void { + if($errorOutput === "") { + return; + } + + if(!$suppressErrorWhenFirstLineContains || $suppressError === false) { + $this->write($errorOutput, Stream::ERROR); + return; + } + + if($suppressError === true) { + return; + } + + $errorBuffer .= $errorOutput; + if(!str_contains($errorBuffer, PHP_EOL)) { + return; + } + + $suppressError = $this->shouldSuppressErrorOutput( + $errorBuffer, + $suppressErrorWhenFirstLineContains + ); + + if(!$suppressError) { + $this->write($errorBuffer, Stream::ERROR); + } + + $errorBuffer = ""; + } + + private function flushErrorBuffer( + ?string $suppressErrorWhenFirstLineContains, + string &$errorBuffer, + ?bool &$suppressError + ):void { + if($errorBuffer === "") { + return; + } + + if(!$suppressErrorWhenFirstLineContains) { + $this->write($errorBuffer, Stream::ERROR); + $errorBuffer = ""; + return; + } + + $suppressError ??= $this->shouldSuppressErrorOutput( + $errorBuffer, + $suppressErrorWhenFirstLineContains + ); + + if(!$suppressError) { + $this->write($errorBuffer, Stream::ERROR); + } + + $errorBuffer = ""; + } + + private function shouldSuppressErrorOutput( + string $errorBuffer, + string $suppressErrorWhenFirstLineContains + ):bool { + $firstLine = strtok($errorBuffer, PHP_EOL); + + return str_contains( + $firstLine ?: "", + $suppressErrorWhenFirstLineContains + ); + } }